Added event system, soft error handler and added some things from helio

This commit is contained in:
Marcel Baumgartner 2023-10-13 21:42:12 +02:00
parent afb3a7f3a3
commit 3bb4e7daab
22 changed files with 699 additions and 9 deletions

View file

@ -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;
}
}

View file

@ -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<User> 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)
);
}
}
}
}

View file

@ -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;
}

View file

@ -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)
{
}
}

View file

@ -0,0 +1,140 @@
using System.Diagnostics;
using Moonlight.App.Models.Abstractions;
namespace Moonlight.App.Helpers;
public class EventSystem
{
private readonly List<Subscriber> Subscribers = new();
private readonly bool Debug = false;
private readonly bool DisableWarning = false;
private readonly TimeSpan TookToLongTime = TimeSpan.FromSeconds(1);
public Task On<T>(string id, object handle, Func<T, Task> 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<Task>();
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<T> WaitForEvent<T>(string id, object handle, Func<T, bool>? filter = null)
{
var taskCompletionSource = new TaskCompletionSource<T>();
Func<T, Task> 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<T>(id, handle, action);
return taskCompletionSource.Task;
}
}

View file

@ -0,0 +1,50 @@
using Moonlight.App.Models.Enums;
namespace Moonlight.App.Models.Abstractions;
public class FlagStorage
{
private readonly List<string> 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);
}
}
}

View file

@ -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<Permission>();
}
public static Permission GetFromInteger(int id)
{
return GetAllPermissions().First(x => (int)x == id);
}
public bool this[Permission permission] => Permissions.Contains(permission);
}

View file

@ -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
}

View file

@ -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; }
}

View file

@ -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
}

View file

@ -0,0 +1,8 @@
namespace Moonlight.App.Models.Enums;
public enum UserFlag
{
MailVerified,
PasswordPending,
TotpEnabled
}

View file

@ -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<ConfigV1>(text) ?? new();
text = JsonConvert.SerializeObject(Data, Formatting.Indented);
File.WriteAllText(Path, text);
}
public ConfigV1 Get()
{
return Data;
}
}

View file

@ -0,0 +1,38 @@
using Moonlight.App.Models.Abstractions;
namespace Moonlight.App.Services;
public class SessionService
{
private readonly List<Session> 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();
}
}
}

View file

@ -14,21 +14,22 @@
</ItemGroup>
<ItemGroup>
<Folder Include="App\Configuration\" />
<Folder Include="App\Database\Entities\" />
<Folder Include="App\Database\Enums\" />
<Folder Include="App\Database\Migrations\" />
<Folder Include="App\Exceptions\" />
<Folder Include="App\Http\" />
<Folder Include="App\Models\Abstractions\" />
<Folder Include="App\Models\Enums\" />
<Folder Include="App\Models\Forms\" />
<Folder Include="App\Repositories\" />
<Folder Include="App\Services\" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Ben.Demystifier" Version="0.4.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="7.0.0" />
<PackageReference Include="Serilog" Version="3.1.0-dev-02078" />
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.0-dev-00923" />
</ItemGroup>

View file

@ -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<DataContext>();
builder.Services.AddSingleton<ConfigService>();
builder.Services.AddSingleton<SessionService>();
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();
builder.Services.AddHttpContextAccessor();

View file

@ -0,0 +1,66 @@
@if (ShowConfirm)
{
<div class="btn-group">
<button class="btn btn-success me-2 rounded-end" @onclick="Do">Confirm</button>
<button class="btn btn-danger rounded-start" @onclick="() => SetConfirm(false)">Cancel</button>
</div>
}
else
{
if (Working)
{
<button class="btn @(CssClasses) disabled" disabled="">
<span class="spinner-border spinner-border-sm align-middle me-2"></span>
@WorkingText
</button>
}
else
{
<button class="btn @(CssClasses)" @onclick="() => SetConfirm(true)">
@Text
@ChildContent
</button>
}
}
@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<Task>? 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);
});
}
}

View file

@ -0,0 +1,48 @@
@if (!Working)
{
<button class="btn @(CssClasses)" @onclick="Do">
@Text
@ChildContent
</button>
}
else
{
<button class="btn @(CssClasses) disabled" disabled="">
<span class="spinner-border spinner-border-sm align-middle me-2"></span>
@WorkingText
</button>
}
@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<Task>? 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);
});
}
}

View file

@ -1,4 +1,5 @@
@using Moonlight.Shared.Layouts
<div class="app-sidebar flex-column @(Layout.ShowMobileSidebar ? "drawer drawer-start drawer-on" : "")">
<div class="app-sidebar-header d-flex flex-stack d-none d-lg-flex pt-8 pb-2">
<a href="/metronic8/demo38/../demo38/index.html" class="app-sidebar-logo">

View file

@ -0,0 +1,70 @@
@using System.Diagnostics
@using Moonlight.App.Exceptions
@inherits ErrorBoundaryBase
@if (Crashed)
{
if (Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == "Development") // TODO: Add check for admin perms to show exceptions to admins
{
if (Exception != null)
{
<div class="card border border-danger">
<div class="card-header">
<span class="card-title text-danger fw-bold fs-3">An unhandled exception occured</span>
</div>
<div class="card-body fw-bold">
@(Formatter.FormatLineBreaks(Exception.ToStringDemystified()))
</div>
</div>
}
}
else
{
<h1>Crashed lol :c</h1>
}
}
else
{
if (ErrorMessages.Any())
{
foreach (var errorMessage in ErrorMessages)
{
<div class="alert alert-danger bg-danger text-white p-3 mb-5 fw-bold">
@errorMessage
</div>
}
}
@ChildContent
}
@code
{
private bool Crashed = false;
private List<string> ErrorMessages = new();
private Exception? Exception;
protected override Task OnErrorAsync(Exception exception)
{
if (exception is DisplayException displayException)
{
ErrorMessages.Add(displayException.Message);
Task.Run(async () =>
{
await Task.Delay(TimeSpan.FromSeconds(5));
ErrorMessages.Remove(displayException.Message);
await InvokeAsync(StateHasChanged);
});
}
else
{
Exception = exception;
Crashed = true;
}
Recover();
return Task.CompletedTask;
}
}

View file

@ -1,5 +1,7 @@
@inherits LayoutComponentBase
<DefaultLayout>
@Body
<SoftErrorHandler>
@Body
</SoftErrorHandler>
</DefaultLayout>

View file

@ -1,3 +1,20 @@
@page "/"
@using Moonlight.App.Exceptions
<h1>Hello, world!</h1>
<h1>Hello, world!</h1>
<ConfirmButton Text="Crash" WorkingText="Crashing" CssClasses="btn-danger" OnClick="Do" />
<ConfirmButton Text="Crash" WorkingText="Crashing" CssClasses="btn-danger" OnClick="Do2" />
@code
{
private async Task Do()
{
throw new DisplayException("LOL");
}
private async Task Do2()
{
throw new FormatException("LOL");
}
}

View file

@ -3,4 +3,5 @@
@using Microsoft.JSInterop
@using Moonlight
@using Moonlight.App.Helpers
@using Moonlight.Shared.Components.Partials
@using Moonlight.Shared.Components.Partials
@using Moonlight.Shared.Components.Forms