Browse Source

Tweaked WPF Converters
Finalised Licensing System
Fixed broke ConConfigureColumns in DynamicGrid<>

frogsoftware 1 year ago
parent
commit
057423ee06

+ 14 - 0
InABox.Core/Licensing/License.cs

@@ -2,6 +2,16 @@
 
 namespace InABox.Core
 {
+
+    public class LicenseRequest
+    {
+        public Guid CustomerID { get; set; }
+        
+        public String[] Addresses { get; set; } = Array.Empty<String>();
+        
+        public bool IsDynamic { get; set; }
+    }
+    
     public class LicenseData : BaseObject
     {
         public DateTime LastRenewal { get; set; }
@@ -16,6 +26,10 @@ namespace InABox.Core
         public Guid CustomerID { get; set; }
 
         public Guid[] UserTrackingItems { get; set; } = Array.Empty<Guid>();
+        
+        public String[] Addresses { get; set; } = Array.Empty<String>();
+        
+        public bool IsDynamic { get; set; }
     }
 
     public class License : Entity, IPersistent, IRemotable, ILicense<CoreLicense>

+ 81 - 24
InABox.Core/Licensing/LicenseUtils.cs

@@ -4,40 +4,96 @@ using System.Diagnostics;
 using System.Diagnostics.CodeAnalysis;
 using System.IO;
 using System.Linq;
+using System.Net.NetworkInformation;
 
 namespace InABox.Core
 {
     public static class LicenseUtils
     {
-        #region License Generation
+        #region Mac Addresses
+        
+        public static String[] GetMacAddresses()
+        {
+            return NetworkInterface
+                .GetAllNetworkInterfaces()
+                .Where(nic =>
+                    nic.OperationalStatus == OperationalStatus.Up &&
+                    nic.NetworkInterfaceType != NetworkInterfaceType.Loopback)
+                .Select(nic => nic.GetPhysicalAddress().ToString()).ToArray();
+        }
 
-        /// <summary>
-        /// Generate a new, out-of-the-box license.
-        /// </summary>
-        /// <returns>The new license, valid for 1 month.</returns>
-        public static LicenseData GenerateNewLicense()
+        public static bool ValidateMacAddresses(String[] addresses)
         {
-            return new LicenseData
-            {
-                LastRenewal = DateTime.Now,
-                Expiry = DateTime.Now.AddMonths(1),
-                CustomerID = Guid.Empty,
-                RenewalAvailable = DateTime.Now.AddMonths(1).AddDays(-7)
-            };
+            var hardware = GetMacAddresses();
+            return hardware.Any(addresses.Contains);
         }
+        
+        #endregion
+        
+        #region License Generation
+        
 
-        public static LicenseData RenewLicense(LicenseData oldLicense, DateTime renewed, DateTime newExpiry, DateTime renewAvailable)
+        public static LicenseData RenewLicense(LicenseData oldLicense, DateTime renewed, DateTime newExpiry, DateTime renewAvailable, String[] addresses)
         {
             return new LicenseData
             {
                 LastRenewal = renewed,
                 Expiry = newExpiry,
-                CustomerID = oldLicense.CustomerID,
-                RenewalAvailable = renewAvailable
+                CustomerID = oldLicense?.CustomerID ?? Guid.Empty,
+                RenewalAvailable = renewAvailable,
+                Addresses = addresses,
+                IsDynamic = oldLicense?.IsDynamic ?? false
             };
         }
-
+        
         private static readonly byte[] LicenseKey = Convert.FromBase64String("dCyTyQkj1o1rqJJQlT+Jcnkxr+OQnO4KCoF/b+6cx54=");
+        
+        public static string? EncryptLicenseRequest(LicenseRequest request)
+        {
+            return Encryption.EncryptV2(Serialization.Serialize(request), LicenseKey);
+        }
+        
+        public static bool TryEncryptLicenseRequest(LicenseRequest request, [NotNullWhen(true)] out string? result, [NotNullWhen(false)] out string? error)
+        {
+            return Encryption.TryEncryptV2(Serialization.Serialize(request), LicenseKey, out result, out error);
+        }
+        
+        /// <summary>
+        /// Decrypts <paramref name="request"/>.
+        /// </summary>
+        /// <param name="request">The request to decrypt.</param>
+        /// <returns>
+        /// The new license request, or <see langword="null"/> if errors occurred.
+        /// </returns>
+        public static bool TryDecryptLicenseRequest(string data, [NotNullWhen(true)] out LicenseRequest? result, [NotNullWhen(false)] out string? error)
+        {
+            if (!Encryption.TryDecryptV2(data, LicenseKey, out var decrypted, out error))
+            {
+                result = null;
+                return false;
+            }
+
+            result = Serialization.Deserialize<LicenseRequest>(decrypted);
+            if(result == null)
+            {
+                error = "Request deserialization failed";
+                return false;
+            }
+            return true;
+        }
+        /// <summary>
+        /// Decrypts <paramref name="request"/>, throwing an <see cref="Exception"/> on fail.
+        /// </summary>
+        /// <param name="request">The data to decrypt.</param>
+        /// <returns>
+        /// The new license request.
+        /// </returns>
+        public static LicenseRequest DecryptLicenseRequest(string data)
+        {
+            if (!TryDecryptLicenseRequest(data, out var result, out var error))
+                throw new Exception(error);
+            return result;
+        }
 
         /// <summary>
         /// Encrypts the license data.
@@ -50,6 +106,7 @@ namespace InABox.Core
         {
             return Encryption.EncryptV2(Serialization.Serialize(license), LicenseKey);
         }
+        
         public static bool TryEncryptLicense(LicenseData license, [NotNullWhen(true)] out string? result, [NotNullWhen(false)] out string? error)
         {
             return Encryption.TryEncryptV2(Serialization.Serialize(license), LicenseKey, out result, out error);
@@ -161,17 +218,17 @@ namespace InABox.Core
             return result;
         }
 
-        public static void LoadSummary(LicenseSummary summary)
+        public static void LoadSummary(LicenseFeeResponse feeResponse)
         {
             _licensefees.Clear();
             _periods.Clear();
             _userDiscounts.Clear();
 
-            foreach (var license in summary.LicenseFees)
+            foreach (var license in feeResponse.LicenseFees)
                 _licensefees[license.Key] = license.Value;
-            foreach (var (months, period) in summary.TimeDiscounts)
+            foreach (var (months, period) in feeResponse.TimeDiscounts)
                 _periods[months] = period;
-            foreach (var (users, discount) in summary.UserDiscounts)
+            foreach (var (users, discount) in feeResponse.UserDiscounts)
                 _userDiscounts[users] = discount;
         }
 
@@ -230,14 +287,14 @@ namespace InABox.Core
         #endregion
     }
 
-    public class LicenseSummaryRequest
+    public class LicenseFeeRequest
     {
         public Guid RegistrationID { get; set; }
     }
     
-    public class LicenseSummary
+    public class LicenseFeeResponse
     {
-        public LicenseSummary()
+        public LicenseFeeResponse()
         {
             LicenseFees = new Dictionary<string, double>();
             TimeDiscounts = new Dictionary<int, double>();

+ 41 - 81
InABox.Database/DbFactory.cs

@@ -1,4 +1,5 @@
 using System.Reflection;
+using FluentResults;
 using InABox.Clients;
 using InABox.Configuration;
 using InABox.Core;
@@ -157,32 +158,37 @@ public static class DbFactory
         Tampered
     }
 
-    private static LicenseValidation CheckLicenseValidity(out License? license, out LicenseData? licenseData)
+    private static LicenseValidation CheckLicenseValidity(out DateTime expiry)
     {
-        license = Provider.Load<License>().FirstOrDefault();
+        expiry = DateTime.MinValue;
+        var license = Provider.Load<License>().FirstOrDefault();
         if (license is null)
-        {
-            licenseData = null;
             return LicenseValidation.Missing;
-        }
 
-        if (!LicenseUtils.TryDecryptLicense(license.Data, out licenseData, out var error))
+        if (!LicenseUtils.TryDecryptLicense(license.Data, out var licenseData, out var error))
             return LicenseValidation.Corrupt;
-
-        if (licenseData.Expiry < DateTime.Now)
-            return LicenseValidation.Expired;
+        
+        if (!LicenseUtils.ValidateMacAddresses(licenseData.Addresses))
+            return LicenseValidation.Tampered;
 
         var userTrackingItems = Provider.Query(
             new Filter<UserTracking>(x => x.ID).InList(licenseData.UserTrackingItems),
-            new Columns<UserTracking>(x => x.ID), log: false).Rows.Select(x => x.Get<UserTracking, Guid>(x => x.ID));
+            new Columns<UserTracking>(x => x.ID)
+            , log: false
+        ).Rows
+            .Select(r => r.Get<UserTracking, Guid>(c => c.ID))
+            .ToArray();
 
         foreach(var item in licenseData.UserTrackingItems)
         {
             if (!userTrackingItems.Contains(item))
-            {
                 return LicenseValidation.Tampered;
-            }
         }
+
+        expiry = licenseData.Expiry;
+        if (licenseData.Expiry < DateTime.Now)
+            return LicenseValidation.Expired;
+        
         return LicenseValidation.Valid;
     }
 
@@ -198,6 +204,12 @@ public static class DbFactory
     {
         LogImportant($"{message} Please renew your license before then, or your database will go into read-only mode; it will be locked for saving anything until you renew your license. For help with renewing your license, please see the documentation at https://prsdigital.com.au/wiki/index.php/License_Renewal.");
     }
+    
+    public static void LogReadOnly()
+    {
+        LogImportant($"Your database is in read-only mode; please renew your license to enable database updates.");
+    }
+    
     private static void LogLicenseExpiry(DateTime expiry)
     {
         if (expiry.Date == DateTime.Today)
@@ -222,21 +234,23 @@ public static class DbFactory
         }
         ++_expiredLicenseCounter;
     }
-
-    public static void LogReadOnly()
-    {
-        LogError("Database is read-only because your license is invalid!");
-    }
-
+    
     private static void BeginReadOnly()
     {
-        LogImportant("Your database is now in read-only mode, since your license is invalid; you will be unable to save any records to the database until you renew your license. For help with renewing your license, please see the documentation at https://prsdigital.com.au/wiki/index.php/License_Renewal.");
-        _readOnly = true;
+        if (!IsReadOnly)
+        {
+            LogImportant(
+                "Your database is now in read-only mode, since your license is invalid; you will be unable to save any records to the database until you renew your license. For help with renewing your license, please see the documentation at https://prsdigital.com.au/wiki/index.php/License_Renewal.");
+            _readOnly = true;
+        }
     }
     private static void EndReadOnly()
     {
-        LogImportant("Valid license found; the database is no longer read-only.");
-        _readOnly = false;
+        if (IsReadOnly)
+        {
+            LogImportant("Valid license found; the database is no longer read-only.");
+            _readOnly = false;
+        }
     }
 
     private static void BeginLicenseCheckTimer()
@@ -249,69 +263,15 @@ public static class DbFactory
     {
         AssertLicense();
     }
-
-    private static Random LicenseIDGenerate = new Random();
-    private static void UpdateValidLicense(License license, LicenseData licenseData)
-    {
-        var ids = Provider.Query(
-            new Filter<UserTracking>(x => x.Created).IsGreaterThanOrEqualTo(licenseData.LastRenewal),
-            new Columns<UserTracking>(x => x.ID), log: false);
-        var newIDList = new List<Guid>();
-        if(ids.Rows.Count > 0)
-        {
-            for (int i = 0; i < 10; i++)
-            {
-                newIDList.Add(ids.Rows[LicenseIDGenerate.Next(0, ids.Rows.Count)].Get<UserTracking, Guid>(x => x.ID));
-            }
-        }
-        licenseData.UserTrackingItems = newIDList.ToArray();
-
-        if(LicenseUtils.TryEncryptLicense(licenseData, out var newData, out var error))
-        {
-            license.Data = newData;
-            Provider.Save(license);
-        }
-    }
-
-    private static void AssertLicense()
+    
+    public static void AssertLicense()
     {
-        var result = CheckLicenseValidity(out var license, out var licenseData);
-        if (IsReadOnly)
-        {
-            if(result == LicenseValidation.Valid)
-            {
-                EndReadOnly();
-            }
-            return;
-        }
-
-        // TODO: Switch to real system
-        if(result != LicenseValidation.Valid)
-        {
-            var newLicense = LicenseUtils.GenerateNewLicense();
-            if (LicenseUtils.TryEncryptLicense(newLicense, out var newData, out var error))
-            {
-                if (license == null)
-                    license = new License();
-                license.Data = newData;
-                Provider.Save(license);
-            }
-            else
-            {
-                Logger.Send(LogType.Error, "", $"Error updating license: {error}");
-            }
-            return;
-        }
-        else
-        {
-            return;
-        }
-
+        var result = CheckLicenseValidity(out DateTime expiry);
         switch (result)
         {
             case LicenseValidation.Valid:
-                LogLicenseExpiry(licenseData!.Expiry);
-                UpdateValidLicense(license, licenseData);
+                LogLicenseExpiry(expiry);
+                EndReadOnly();
                 break;
             case LicenseValidation.Missing:
                 LogImportant("Database is unlicensed!");

+ 1 - 0
InABox.Database/InABox.Database.csproj

@@ -7,6 +7,7 @@
     </PropertyGroup>
 
     <ItemGroup>
+        <PackageReference Include="FluentResults" Version="3.15.2" />
         <PackageReference Include="Newtonsoft.Json">
             <Version>13.0.3</Version>
         </PackageReference>

+ 12 - 0
InABox.Database/Stores/LicenseStore.cs

@@ -0,0 +1,12 @@
+using InABox.Core;
+
+namespace InABox.Database;
+
+public class LicenseStore : Store<License>
+{
+    protected override void AfterSave(License entity)
+    {
+        base.AfterSave(entity);
+        DbFactory.AssertLicense();
+    }
+}

+ 1 - 5
inabox.database.sqlite/SQLiteProvider.cs

@@ -2804,10 +2804,8 @@ namespace InABox.Database.SQLite
         {
             if (DbFactory.IsReadOnly)
             {
-                if (typeof(T).IsAssignableTo(typeof(License)))
-                {
+                if (typeof(T).IsAssignableTo(typeof(License)) || typeof(T).IsAssignableTo(typeof(UserTracking)))
                     return true;
-                }
                 DbFactory.LogReadOnly();
                 return false;
             }
@@ -2819,9 +2817,7 @@ namespace InABox.Database.SQLite
         public void Save<T>(IEnumerable<T> entities) where T : Entity
         {
             if (!CanSave<T>())
-            {
                 return;
-            }
             OnSave(entities);
         }
 

+ 0 - 24
inabox.wpf/Converters/BoolToVisibilityConverter.cs

@@ -1,24 +0,0 @@
-using System;
-using System.Globalization;
-using System.Windows;
-
-namespace InABox.WPF;
-
-public class BoolToVisibilityConverter : UtilityConverter<bool,Visibility>
-{
-    public Visibility TrueValue { get; set; }
-    public Visibility FalseValue { get; set; }
-    
-    public override Visibility Convert(bool value)
-    {
-        return value
-            ? TrueValue
-            : FalseValue;
-    }
-
-    public override bool Deconvert(Visibility value)
-    {
-        return value == TrueValue;
-    }
-    
-}

+ 19 - 0
inabox.wpf/Converters/BooleanConverter.cs

@@ -0,0 +1,19 @@
+namespace InABox.WPF;
+
+public class BooleanConverter<T> : UtilityConverter<bool, T?>
+{
+    public T? TrueValue { get; set; }
+    public T? FalseValue { get; set; }
+    
+    public override T? Convert(bool value)
+    {
+        return value
+            ? TrueValue
+            : FalseValue;
+    }
+
+    public override bool Deconvert(T? value)
+    {
+        return Equals(value,TrueValue);
+    }
+}

+ 21 - 0
inabox.wpf/Converters/BooleanToBooleanConverter.cs

@@ -0,0 +1,21 @@
+namespace InABox.WPF;
+
+public class BooleanToBooleanConverter : UtilityConverter<bool,bool>
+{
+    public bool Invert { get; set; }
+    
+    public override bool Convert(bool value)
+    {
+        return Invert
+            ? !value
+            : value;
+    }
+
+    public override bool Deconvert(bool value)
+    {
+        return Invert
+            ? !value
+            : value;
+    }
+    
+}

+ 5 - 0
inabox.wpf/Converters/BooleanToBrushConverter.cs

@@ -0,0 +1,5 @@
+using System.Windows.Media;
+
+namespace InABox.WPF;
+
+public class BooleanToBrushConverter : BooleanConverter<Brush> { }

+ 5 - 0
inabox.wpf/Converters/BooleanToImageSourceConverter.cs

@@ -0,0 +1,5 @@
+using System.Windows.Media;
+
+namespace InABox.WPF;
+
+public class BooleanToImageSourceConverter : BooleanConverter<ImageSource> { }

+ 3 - 0
inabox.wpf/Converters/BooleanToStringConverter.cs

@@ -0,0 +1,3 @@
+namespace InABox.WPF;
+
+public class BooleanToStringConverter : BooleanConverter<string> { }

+ 6 - 0
inabox.wpf/Converters/BooleanToVisibilityConverter.cs

@@ -0,0 +1,6 @@
+using System.Windows;
+using System.Windows.Media;
+
+namespace InABox.WPF;
+
+public class BooleanToVisibilityConverter : BooleanConverter<Visibility> { }

+ 7 - 2
inabox.wpf/DynamicGrid/BaseDynamicGrid.cs

@@ -55,6 +55,11 @@ namespace InABox.DynamicGrid
         {
             OnReconfigure?.Invoke(options);
         }
+        
+        public virtual event OnCustomiseColumns? OnCustomiseColumns;
+
+        protected void DoCustomiseColumnsEvent(object sender, DynamicGridColumns columns) =>
+            OnCustomiseColumns?.Invoke(sender, columns);
 
         static BaseDynamicGrid()
         {
@@ -85,7 +90,7 @@ namespace InABox.DynamicGrid
         public abstract event OnFilterRecord? OnFilterRecord;
         public event OnCreateItem? OnCreateItem;
         public abstract event OnCustomiseEditor<T>? OnCustomiseEditor;
-        public virtual event OnCustomiseColumns? OnCustomiseColumns;
+
         public abstract event OnDoubleClick? OnDoubleClick;
 
         public OnGetDynamicGridRowStyle? OnGetRowStyle { get; set; }
@@ -186,7 +191,7 @@ namespace InABox.DynamicGrid
 
         public virtual void ConfigureColumns(DynamicGridColumns columns)
         {
-            OnCustomiseColumns?.Invoke(this, columns);
+            DoCustomiseColumnsEvent(this,columns);
         }
 
         public abstract CoreRow[] SelectedRows { get; set; }

+ 1 - 1
inabox.wpf/DynamicGrid/Columns/EditorColumns/DynamicGridCheckBoxColumn.cs

@@ -92,7 +92,7 @@ public class DynamicGridCheckBoxColumn<TEntity> : DynamicGridEditorColumn<TEntit
             new Binding()
             {
                 Path = new PropertyPath(MappingName), 
-                Converter = new BoolToVisibilityConverter() { TrueValue = truevalue, FalseValue = falsevalue }, 
+                Converter = new WPF.BooleanToVisibilityConverter() { TrueValue = truevalue, FalseValue = falsevalue }, 
                 Mode = mode
             }
         );

+ 0 - 2
inabox.wpf/DynamicGrid/DynamicGrid.cs

@@ -339,8 +339,6 @@ namespace InABox.DynamicGrid
 
         public override event OnCustomiseEditor<T>? OnCustomiseEditor;
 
-        public override event OnCustomiseColumns? OnCustomiseColumns;
-
         public override event OnFilterRecord? OnFilterRecord;
 
         public override event OnDoubleClick? OnDoubleClick;

+ 1 - 1
inabox.wpf/Forms/Console/Console.xaml

@@ -9,7 +9,7 @@
         x:Name="Window"
         DataContext="{Binding ElementName=Window}">
     <UserControl.Resources>
-        <wpf:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter"
+        <wpf:BooleanToVisibilityConverter x:Key="BoolToVisibilityConverter"
                                        TrueValue="Visible" FalseValue="Collapsed"/>
 
         <Style TargetType="ItemsControl" x:Key="LogViewerStyle">

+ 1 - 0
inabox.wpf/InABox.Wpf.csproj

@@ -129,6 +129,7 @@
         <PackageReference Include="ControlzEx" Version="5.0.2" />
         <PackageReference Include="Extended.Wpf.Toolkit" Version="4.4.0" />
         <PackageReference Include="FastReport.Net.Pro" Version="2023.2.23" />
+        <PackageReference Include="FluentResults" Version="3.15.2" />
         <PackageReference Include="GhostScript.NetCore" Version="1.0.1" />
         <PackageReference Include="Microsoft.CodeAnalysis.Common" Version="4.0.1" />
         <PackageReference Include="net.sf.mpxj-ikvm" Version="10.9.0" />

+ 41 - 0
inabox.wpf/ProgressWindow/Progress.cs

@@ -3,6 +3,7 @@ using System.ComponentModel;
 using System.Linq;
 using System.Threading;
 using System.Windows.Media.Imaging;
+using FluentResults;
 
 namespace InABox.WPF
 {
@@ -90,6 +91,46 @@ namespace InABox.WPF
             return !cancellationTokenSource.IsCancellationRequested;
         }
 
+        public static Result<T> ShowModal<T>(String message, Func<IProgress<string>, Result<T>> work)
+        {
+            Result<T> result = Result.Fail<T>("Incomplete");
+            Exception? exception = null;
+
+            var progress = new ProgressForm
+            {
+                DisplayImage = DisplayImage
+            };
+            progress.Progress.Content = message;
+            progress.Loaded += (_, args) =>
+            {
+                var worker = new BackgroundWorker();
+                var update = new Progress<string>(data => progress.Progress.Content = data);
+
+                progress.OnCancelled += () =>
+                {
+                    worker.CancelAsync();
+                };
+
+                worker.DoWork += (o, e) =>
+                {
+                    try
+                    {
+                        result = work(update);
+                    }
+                    catch (Exception ex)
+                    {
+                        result = Result.Fail<T>(ex.Message);
+                    }
+                };
+                worker.RunWorkerCompleted +=
+                    (o, e) => progress.Close();
+                worker.RunWorkerAsync();
+            };
+
+            progress.ShowDialog();
+            return result;
+        }
+
         public static void ShowModal(string message, Action<IProgress<string>> work)
         {
             Exception? exception = null;

+ 1 - 1
inabox.wpf/Themes/Generic.xaml

@@ -47,7 +47,7 @@
             <Setter.Value>
                 <ControlTemplate TargetType="wpf:ZoomPanel">
                     <ControlTemplate.Resources>
-                        <wpf:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter"/>
+                        <wpf:BooleanToVisibilityConverter x:Key="BoolToVisibilityConverter"/>
                     </ControlTemplate.Resources>
                     <Grid>
                         <Grid.RowDefinitions>