From 3bb4e7daab7b280f16a07a6c3228e32e01f9ee6b Mon Sep 17 00:00:00 2001 From: Marcel Baumgartner Date: Fri, 13 Oct 2023 21:42:12 +0200 Subject: [PATCH] Added event system, soft error handler and added some things from helio --- Moonlight/App/Configuration/ConfigV1.cs | 64 ++++++++ Moonlight/App/Database/DataContext.cs | 42 ++++++ Moonlight/App/Database/Entities/User.cs | 19 +++ Moonlight/App/Exceptions/DisplayException.cs | 16 ++ Moonlight/App/Helpers/EventSystem.cs | 140 ++++++++++++++++++ .../App/Models/Abstractions/FlagStorage.cs | 50 +++++++ .../Models/Abstractions/PermissionStorage.cs | 34 +++++ Moonlight/App/Models/Abstractions/Session.cs | 12 ++ .../App/Models/Abstractions/Subscriber.cs | 8 + Moonlight/App/Models/Enums/Permission.cs | 14 ++ Moonlight/App/Models/Enums/UserFlag.cs | 8 + Moonlight/App/Services/ConfigService.cs | 32 ++++ Moonlight/App/Services/SessionService.cs | 38 +++++ Moonlight/Moonlight.csproj | 13 +- Moonlight/Program.cs | 7 + .../Components/Forms/ConfirmButton.razor | 66 +++++++++ .../Shared/Components/Forms/WButton.razor | 48 ++++++ .../Shared/Components/Partials/Sidebar.razor | 1 + .../Partials/SoftErrorHandler.razor | 70 +++++++++ Moonlight/Shared/Layouts/MainLayout.razor | 4 +- Moonlight/Shared/Views/Index.razor | 19 ++- Moonlight/_Imports.razor | 3 +- 22 files changed, 699 insertions(+), 9 deletions(-) create mode 100644 Moonlight/App/Configuration/ConfigV1.cs create mode 100644 Moonlight/App/Database/DataContext.cs create mode 100644 Moonlight/App/Database/Entities/User.cs create mode 100644 Moonlight/App/Exceptions/DisplayException.cs create mode 100644 Moonlight/App/Helpers/EventSystem.cs create mode 100644 Moonlight/App/Models/Abstractions/FlagStorage.cs create mode 100644 Moonlight/App/Models/Abstractions/PermissionStorage.cs create mode 100644 Moonlight/App/Models/Abstractions/Session.cs create mode 100644 Moonlight/App/Models/Abstractions/Subscriber.cs create mode 100644 Moonlight/App/Models/Enums/Permission.cs create mode 100644 Moonlight/App/Models/Enums/UserFlag.cs create mode 100644 Moonlight/App/Services/ConfigService.cs create mode 100644 Moonlight/App/Services/SessionService.cs create mode 100644 Moonlight/Shared/Components/Forms/ConfirmButton.razor create mode 100644 Moonlight/Shared/Components/Forms/WButton.razor create mode 100644 Moonlight/Shared/Components/Partials/SoftErrorHandler.razor diff --git a/Moonlight/App/Configuration/ConfigV1.cs b/Moonlight/App/Configuration/ConfigV1.cs new file mode 100644 index 00000000..642ef103 --- /dev/null +++ b/Moonlight/App/Configuration/ConfigV1.cs @@ -0,0 +1,64 @@ +using System.ComponentModel; +using Moonlight.App.Helpers; +using Newtonsoft.Json; + +namespace Moonlight.App.Configuration; + +public class ConfigV1 +{ + [JsonProperty("AppUrl")] + [Description("The url with which moonlight is accessible from the internet. It must not end with a /")] + public string AppUrl { get; set; } = "http://your-moonlight-instance-without-slash.owo"; + + [JsonProperty("Security")] public SecurityData Security { get; set; } = new(); + [JsonProperty("Database")] public DatabaseData Database { get; set; } = new(); + [JsonProperty("MailServer")] public MailServerData MailServer { get; set; } = new(); + + public class SecurityData + { + [JsonProperty("Token")] + [Description("The security token helio will use to encrypt various things like tokens")] + public string Token { get; set; } = Guid.NewGuid().ToString().Replace("-", ""); + + [JsonProperty("EnableEmailVerify")] + [Description("This will users force to verify their email address if they havent already")] + public bool EnableEmailVerify { get; set; } = false; + } + + public class DatabaseData + { + [JsonProperty("UseSqlite")] + public bool UseSqlite { get; set; } = false; + + [JsonProperty("SqlitePath")] + public string SqlitePath { get; set; } = PathBuilder.File("storage", "data.sqlite"); + + [JsonProperty("Host")] + public string Host { get; set; } = "your.db.host"; + + [JsonProperty("Port")] + public int Port { get; set; } = 3306; + + [JsonProperty("Username")] + public string Username { get; set; } = "moonlight_user"; + + [JsonProperty("Password")] + public string Password { get; set; } = "s3cr3t"; + + [JsonProperty("Database")] + public string Database { get; set; } = "moonlight_db"; + } + + public class MailServerData + { + [JsonProperty("Host")] public string Host { get; set; } = "your.email.host"; + + [JsonProperty("Port")] public int Port { get; set; } = 465; + + [JsonProperty("Email")] public string Email { get; set; } = "noreply@your.email.host"; + + [JsonProperty("Password")] public string Password { get; set; } = "s3cr3t"; + + [JsonProperty("UseSsl")] public bool UseSsl { get; set; } = true; + } +} \ No newline at end of file diff --git a/Moonlight/App/Database/DataContext.cs b/Moonlight/App/Database/DataContext.cs new file mode 100644 index 00000000..d48f8e24 --- /dev/null +++ b/Moonlight/App/Database/DataContext.cs @@ -0,0 +1,42 @@ +using Microsoft.EntityFrameworkCore; +using Moonlight.App.Database.Entities; +using Moonlight.App.Services; + +namespace Moonlight.App.Database; + +public class DataContext : DbContext +{ + private readonly ConfigService ConfigService; + + public DbSet Users { get; set; } + + public DataContext(ConfigService configService) + { + ConfigService = configService; + } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + if (!optionsBuilder.IsConfigured) + { + var config = ConfigService.Get().Database; + + if (config.UseSqlite) + optionsBuilder.UseSqlite($"Data Source={config.SqlitePath}"); + else + { + var connectionString = $"host={config.Host};" + + $"port={config.Port};" + + $"database={config.Database};" + + $"uid={config.Username};" + + $"pwd={config.Password}"; + + optionsBuilder.UseMySql( + connectionString, + ServerVersion.AutoDetect(connectionString), + builder => builder.EnableRetryOnFailure(5) + ); + } + } + } +} \ No newline at end of file diff --git a/Moonlight/App/Database/Entities/User.cs b/Moonlight/App/Database/Entities/User.cs new file mode 100644 index 00000000..ef43c138 --- /dev/null +++ b/Moonlight/App/Database/Entities/User.cs @@ -0,0 +1,19 @@ +namespace Moonlight.App.Database.Entities; + +public class User +{ + public int Id { get; set; } + public string Username { get; set; } + public string Email { get; set; } + public string Password { get; set; } + public string? Avatar { get; set; } = null; + public string? TotpKey { get; set; } = null; + + // Meta data + public string Flags { get; set; } = ""; + public int Permissions { get; set; } = 0; + + // Timestamps + public DateTime TokenValidTimestamp { get; set; } = DateTime.UtcNow.AddMinutes(-10); + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; +} \ No newline at end of file diff --git a/Moonlight/App/Exceptions/DisplayException.cs b/Moonlight/App/Exceptions/DisplayException.cs new file mode 100644 index 00000000..5f2df095 --- /dev/null +++ b/Moonlight/App/Exceptions/DisplayException.cs @@ -0,0 +1,16 @@ +namespace Moonlight.App.Exceptions; + +public class DisplayException : Exception +{ + public DisplayException() + { + } + + public DisplayException(string message) : base(message) + { + } + + public DisplayException(string message, Exception inner) : base(message, inner) + { + } +} \ No newline at end of file diff --git a/Moonlight/App/Helpers/EventSystem.cs b/Moonlight/App/Helpers/EventSystem.cs new file mode 100644 index 00000000..e2eca127 --- /dev/null +++ b/Moonlight/App/Helpers/EventSystem.cs @@ -0,0 +1,140 @@ +using System.Diagnostics; +using Moonlight.App.Models.Abstractions; + +namespace Moonlight.App.Helpers; + +public class EventSystem +{ + private readonly List Subscribers = new(); + + private readonly bool Debug = false; + private readonly bool DisableWarning = false; + private readonly TimeSpan TookToLongTime = TimeSpan.FromSeconds(1); + + public Task On(string id, object handle, Func action) + { + if (Debug) + Logger.Debug($"{handle} subscribed to '{id}'"); + + lock (Subscribers) + { + if (!Subscribers.Any(x => x.Id == id && x.Handle == handle)) + { + Subscribers.Add(new() + { + Action = action, + Handle = handle, + Id = id + }); + } + } + + return Task.CompletedTask; + } + + public Task Emit(string id, object? data = null) + { + Subscriber[] subscribers; + + lock (Subscribers) + { + subscribers = Subscribers + .Where(x => x.Id == id) + .ToArray(); + } + + var tasks = new List(); + + foreach (var subscriber in subscribers) + { + tasks.Add(new Task(() => + { + var stopWatch = new Stopwatch(); + stopWatch.Start(); + + var del = (Delegate)subscriber.Action; + + try + { + ((Task)del.DynamicInvoke(data)!).Wait(); + } + catch (Exception e) + { + Logger.Warn($"Error emitting '{subscriber.Id} on {subscriber.Handle}'"); + Logger.Warn(e); + } + + stopWatch.Stop(); + + if (!DisableWarning) + { + if (stopWatch.Elapsed.TotalMilliseconds > TookToLongTime.TotalMilliseconds) + { + Logger.Warn( + $"Subscriber {subscriber.Handle} for event '{subscriber.Id}' took long to process. {stopWatch.Elapsed.TotalMilliseconds}ms"); + } + } + + if (Debug) + { + Logger.Debug( + $"Subscriber {subscriber.Handle} for event '{subscriber.Id}' took {stopWatch.Elapsed.TotalMilliseconds}ms"); + } + })); + } + + foreach (var task in tasks) + { + task.Start(); + } + + Task.Run(() => + { + Task.WaitAll(tasks.ToArray()); + + if (Debug) + Logger.Debug($"Completed all event tasks for '{id}' and removed object from storage"); + }); + + if (Debug) + Logger.Debug($"Completed event emit '{id}'"); + + return Task.CompletedTask; + } + + public Task Off(string id, object handle) + { + if (Debug) + Logger.Debug($"{handle} unsubscribed to '{id}'"); + + lock (Subscribers) + { + Subscribers.RemoveAll(x => x.Id == id && x.Handle == handle); + } + + return Task.CompletedTask; + } + + public Task WaitForEvent(string id, object handle, Func? filter = null) + { + var taskCompletionSource = new TaskCompletionSource(); + + Func action = async data => + { + if (filter == null) + { + taskCompletionSource.SetResult(data); + await Off(id, handle); + } + else if (filter.Invoke(data)) + { + taskCompletionSource.SetResult(data); + await Off(id, handle); + } + }; + + On(id, handle, action); + + return taskCompletionSource.Task; + } +} \ No newline at end of file diff --git a/Moonlight/App/Models/Abstractions/FlagStorage.cs b/Moonlight/App/Models/Abstractions/FlagStorage.cs new file mode 100644 index 00000000..7ec57b91 --- /dev/null +++ b/Moonlight/App/Models/Abstractions/FlagStorage.cs @@ -0,0 +1,50 @@ +using Moonlight.App.Models.Enums; + +namespace Moonlight.App.Models.Abstractions; + +public class FlagStorage +{ + private readonly List FlagList; + + public UserFlag[] Flags => FlagList + .Select(x => Enum.Parse(typeof(UserFlag), x)) + .Select(x => (UserFlag)x) + .ToArray(); + + public string[] RawFlags => FlagList.ToArray(); + public string RawFlagString => string.Join(";", FlagList); + + public bool this[UserFlag flag] + { + get => Flags.Contains(flag); + set => Set(flag.ToString(), value); + } + + public bool this[string flagName] + { + get => FlagList.Contains(flagName); + set => Set(flagName, value); + } + + public FlagStorage(string flagString) + { + FlagList = flagString + .Split(";") + .Where(x => !string.IsNullOrEmpty(x)) + .ToList(); + } + + public void Set(string flagName, bool shouldAdd) + { + if (shouldAdd) + { + if(!FlagList.Contains(flagName)) + FlagList.Add(flagName); + } + else + { + if (FlagList.Contains(flagName)) + FlagList.Remove(flagName); + } + } +} \ No newline at end of file diff --git a/Moonlight/App/Models/Abstractions/PermissionStorage.cs b/Moonlight/App/Models/Abstractions/PermissionStorage.cs new file mode 100644 index 00000000..8a85b6f3 --- /dev/null +++ b/Moonlight/App/Models/Abstractions/PermissionStorage.cs @@ -0,0 +1,34 @@ +using Moonlight.App.Models.Enums; + +namespace Moonlight.App.Models.Abstractions; + +public class PermissionStorage +{ + public readonly int PermissionInteger; + + public PermissionStorage(int permissionInteger) + { + PermissionInteger = permissionInteger; + } + + public Permission[] Permissions => GetPermissions(); + + public Permission[] GetPermissions() + { + return GetAllPermissions() + .Where(x => (int)x <= PermissionInteger) + .ToArray(); + } + + public static Permission[] GetAllPermissions() + { + return Enum.GetValues(); + } + + public static Permission GetFromInteger(int id) + { + return GetAllPermissions().First(x => (int)x == id); + } + + public bool this[Permission permission] => Permissions.Contains(permission); +} \ No newline at end of file diff --git a/Moonlight/App/Models/Abstractions/Session.cs b/Moonlight/App/Models/Abstractions/Session.cs new file mode 100644 index 00000000..e076266d --- /dev/null +++ b/Moonlight/App/Models/Abstractions/Session.cs @@ -0,0 +1,12 @@ +using Moonlight.App.Database.Entities; + +namespace Moonlight.App.Models.Abstractions; + +public class Session +{ + public string Ip { get; set; } = "N/A"; + public string Url { get; set; } = "N/A"; + public User? User { get; set; } + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; // To remove inactive sessions +} \ No newline at end of file diff --git a/Moonlight/App/Models/Abstractions/Subscriber.cs b/Moonlight/App/Models/Abstractions/Subscriber.cs new file mode 100644 index 00000000..97d4944c --- /dev/null +++ b/Moonlight/App/Models/Abstractions/Subscriber.cs @@ -0,0 +1,8 @@ +namespace Moonlight.App.Models.Abstractions; + +public class Subscriber +{ + public string Id { get; set; } + public object Action { get; set; } + public object Handle { get; set; } +} \ No newline at end of file diff --git a/Moonlight/App/Models/Enums/Permission.cs b/Moonlight/App/Models/Enums/Permission.cs new file mode 100644 index 00000000..4b2e8ca7 --- /dev/null +++ b/Moonlight/App/Models/Enums/Permission.cs @@ -0,0 +1,14 @@ +namespace Moonlight.App.Models.Enums; + +public enum Permission +{ + Default = 0, + AdminMenu = 999, + AdminOverview = 1000, + AdminUsers = 1001, + AdminSessions = 1002, + AdminUsersEdit = 1003, + AdminTickets = 1004, + AdminViewExceptions = 1999, + AdminRoot = 2000 +} \ No newline at end of file diff --git a/Moonlight/App/Models/Enums/UserFlag.cs b/Moonlight/App/Models/Enums/UserFlag.cs new file mode 100644 index 00000000..d6fcd355 --- /dev/null +++ b/Moonlight/App/Models/Enums/UserFlag.cs @@ -0,0 +1,8 @@ +namespace Moonlight.App.Models.Enums; + +public enum UserFlag +{ + MailVerified, + PasswordPending, + TotpEnabled +} \ No newline at end of file diff --git a/Moonlight/App/Services/ConfigService.cs b/Moonlight/App/Services/ConfigService.cs new file mode 100644 index 00000000..7f74598d --- /dev/null +++ b/Moonlight/App/Services/ConfigService.cs @@ -0,0 +1,32 @@ +using Moonlight.App.Configuration; +using Moonlight.App.Helpers; +using Newtonsoft.Json; + +namespace Moonlight.App.Services; + +public class ConfigService +{ + private readonly string Path = PathBuilder.File("storage", "config.json"); + private ConfigV1 Data; + + public ConfigService() + { + Reload(); + } + + public void Reload() + { + if(!File.Exists(Path)) + File.WriteAllText(Path, "{}"); + + var text = File.ReadAllText(Path); + Data = JsonConvert.DeserializeObject(text) ?? new(); + text = JsonConvert.SerializeObject(Data, Formatting.Indented); + File.WriteAllText(Path, text); + } + + public ConfigV1 Get() + { + return Data; + } +} \ No newline at end of file diff --git a/Moonlight/App/Services/SessionService.cs b/Moonlight/App/Services/SessionService.cs new file mode 100644 index 00000000..e013c3fe --- /dev/null +++ b/Moonlight/App/Services/SessionService.cs @@ -0,0 +1,38 @@ +using Moonlight.App.Models.Abstractions; + +namespace Moonlight.App.Services; + +public class SessionService +{ + private readonly List AllSessions = new(); + + public Session[] Sessions => GetSessions(); + + public Task Register(Session session) + { + lock (AllSessions) + { + AllSessions.Add(session); + } + + return Task.CompletedTask; + } + + public Task Unregister(Session session) + { + lock (AllSessions) + { + AllSessions.Remove(session); + } + + return Task.CompletedTask; + } + + public Session[] GetSessions() + { + lock (AllSessions) + { + return AllSessions.ToArray(); + } + } +} \ No newline at end of file diff --git a/Moonlight/Moonlight.csproj b/Moonlight/Moonlight.csproj index 0564111b..384b9e46 100644 --- a/Moonlight/Moonlight.csproj +++ b/Moonlight/Moonlight.csproj @@ -14,21 +14,22 @@ - - - - - - + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + diff --git a/Moonlight/Program.cs b/Moonlight/Program.cs index fd8d3507..2c3db5b0 100644 --- a/Moonlight/Program.cs +++ b/Moonlight/Program.cs @@ -1,6 +1,8 @@ +using Moonlight.App.Database; using Moonlight.App.Extensions; using Moonlight.App.Helpers; using Moonlight.App.Helpers.LogMigrator; +using Moonlight.App.Services; using Serilog; Directory.CreateDirectory(PathBuilder.Dir("storage")); @@ -17,6 +19,11 @@ Log.Logger = logConfig.CreateLogger(); var builder = WebApplication.CreateBuilder(args); +builder.Services.AddDbContext(); + +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); + builder.Services.AddRazorPages(); builder.Services.AddServerSideBlazor(); builder.Services.AddHttpContextAccessor(); diff --git a/Moonlight/Shared/Components/Forms/ConfirmButton.razor b/Moonlight/Shared/Components/Forms/ConfirmButton.razor new file mode 100644 index 00000000..fc920b8f --- /dev/null +++ b/Moonlight/Shared/Components/Forms/ConfirmButton.razor @@ -0,0 +1,66 @@ +@if (ShowConfirm) +{ +
+ + +
+} +else +{ + if (Working) + { + + } + else + { + + } +} + +@code +{ + private bool Working { get; set; } = false; + private bool ShowConfirm = false; + + [Parameter] + public string CssClasses { get; set; } = "btn-primary"; + + [Parameter] + public string Text { get; set; } = ""; + + [Parameter] + public string WorkingText { get; set; } = ""; + + [Parameter] + public Func? OnClick { get; set; } + + [Parameter] + public RenderFragment ChildContent { get; set; } + + private async Task SetConfirm(bool b) + { + ShowConfirm = b; + await InvokeAsync(StateHasChanged); + } + + private async Task Do() + { + Working = true; + ShowConfirm = false; + StateHasChanged(); + await Task.Run(async () => + { + if (OnClick != null) + await OnClick.Invoke(); + + Working = false; + await InvokeAsync(StateHasChanged); + }); + } +} \ No newline at end of file diff --git a/Moonlight/Shared/Components/Forms/WButton.razor b/Moonlight/Shared/Components/Forms/WButton.razor new file mode 100644 index 00000000..330644bf --- /dev/null +++ b/Moonlight/Shared/Components/Forms/WButton.razor @@ -0,0 +1,48 @@ +@if (!Working) +{ + +} +else +{ + +} + +@code +{ + private bool Working { get; set; } = false; + + [Parameter] + public string CssClasses { get; set; } = "btn-primary"; + + [Parameter] + public string Text { get; set; } = ""; + + [Parameter] + public string WorkingText { get; set; } = ""; + + [Parameter] + public Func? OnClick { get; set; } + + [Parameter] + public RenderFragment ChildContent { get; set; } + + private async Task Do() + { + Working = true; + StateHasChanged(); + await Task.Run(async () => + { + if (OnClick != null) + await OnClick.Invoke(); + + Working = false; + await InvokeAsync(StateHasChanged); + }); + } +} \ No newline at end of file diff --git a/Moonlight/Shared/Components/Partials/Sidebar.razor b/Moonlight/Shared/Components/Partials/Sidebar.razor index 401bab6e..d625e5b9 100644 --- a/Moonlight/Shared/Components/Partials/Sidebar.razor +++ b/Moonlight/Shared/Components/Partials/Sidebar.razor @@ -1,4 +1,5 @@ @using Moonlight.Shared.Layouts +