From 248af498a8abf6c5437ea858bb9732ef88f2dfd8 Mon Sep 17 00:00:00 2001 From: ChiaraBm Date: Wed, 21 Jan 2026 17:01:32 +0100 Subject: [PATCH 1/4] Added setup wizard component for initial installation flow and integrated it into app routing. --- Moonlight.Frontend/UI/App.razor | 58 ++++--- .../UI/Shared/Components/Setup.razor | 147 ++++++++++++++++++ 2 files changed, 182 insertions(+), 23 deletions(-) create mode 100644 Moonlight.Frontend/UI/Shared/Components/Setup.razor diff --git a/Moonlight.Frontend/UI/App.razor b/Moonlight.Frontend/UI/App.razor index f9c663dd..edadd0bd 100644 --- a/Moonlight.Frontend/UI/App.razor +++ b/Moonlight.Frontend/UI/App.razor @@ -2,10 +2,13 @@ @using LucideBlazor @using Microsoft.AspNetCore.Components.Authorization @using Moonlight.Frontend.UI.Shared +@using Moonlight.Frontend.UI.Shared.Components @using ShadcnBlazor.Emptys @using Moonlight.Frontend.UI.Shared.Components.Auth @using Moonlight.Frontend.UI.Shared.Partials +@inject NavigationManager Navigation + @@ -30,33 +33,42 @@ } else { - + var uri = new Uri(Navigation.Uri); + + if (uri.LocalPath.StartsWith("/setup")) + { + + } + else + { + + } } - @if (context is HttpRequestException { StatusCode: HttpStatusCode.Unauthorized }) - { - - } - else - { -
- - - - - - - Critical Application Error - - - @context.ToString() - - - -
- } + @if (context is HttpRequestException { StatusCode: HttpStatusCode.Unauthorized }) + { + + } + else + { +
+ + + + + + + Critical Application Error + + + @context.ToString() + + + +
+ }
\ No newline at end of file diff --git a/Moonlight.Frontend/UI/Shared/Components/Setup.razor b/Moonlight.Frontend/UI/Shared/Components/Setup.razor new file mode 100644 index 00000000..947b6a00 --- /dev/null +++ b/Moonlight.Frontend/UI/Shared/Components/Setup.razor @@ -0,0 +1,147 @@ +@using LucideBlazor +@using ShadcnBlazor.Cards +@using ShadcnBlazor.Spinners +@using ShadcnBlazor.Buttons +@using ShadcnBlazor.Inputs +@using ShadcnBlazor.Labels + +
+ + @if (!IsLoaded) + { +
+ @if (CurrentStep == 0) + { +
+ Moonlight Logo +
+ +
+

Welcome to Moonlight Panel

+

+ You successfully installed moonlight. Now you are ready to perform some initial steps to complete your installation +

+
+ } + else if (CurrentStep == 1) + { +
+ +
+ +
+

Admin Account Creation

+

+ To continue please fill in the account details of the user you want to use as the initial administrator account. + If you use an external OIDC provider, these details need to match with your desired OIDC account +

+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ } + else if (CurrentStep == 2) + { +
+ +
+ +
+

You are all set!

+

+ You are now ready to finish the initial setup. + The configured options will be applied to the instance. + You will be redirected to the login after changes have been applied successfully +

+
+ } +
+
+ @for (var step = 0; step < StepCount; step++) + { + if (step == CurrentStep) + { +
+
+ } + else + { +
+
+ } + } +
+
+ @if (CurrentStep > 0) + { + + } + @if (CurrentStep != StepCount - 1) + { + + } + else + { + + } +
+
+
+ } + else + { +
+ +
+ } +
+
+ +@code +{ + private bool IsLoaded = false; + + private int CurrentStep = 0; + private int StepCount = 3; + + private string Email; + private string Username; + private string Password; + + private void Navigate(int diff) => CurrentStep += diff; +} \ No newline at end of file -- 2.49.1 From bb5737bd0b046bd604bf96330dceb4ee1675d511 Mon Sep 17 00:00:00 2001 From: ChiaraBm Date: Thu, 22 Jan 2026 16:24:53 +0100 Subject: [PATCH 2/4] Started implementing setup wizard backend for initial instance configuration. Adjusted ui --- .../Configuration/SettingsOptions.cs | 6 + .../Http/Controllers/Admin/SetupController.cs | 127 ++++++++++++++++++ Moonlight.Api/Services/SettingsService.cs | 79 +++++++++++ Moonlight.Api/Startup/Startup.Auth.cs | 3 + .../UI/Shared/Components/Setup.razor | 43 ++++-- .../Http/Requests/Seup/ApplySetupDto.cs | 8 ++ 6 files changed, 257 insertions(+), 9 deletions(-) create mode 100644 Moonlight.Api/Configuration/SettingsOptions.cs create mode 100644 Moonlight.Api/Http/Controllers/Admin/SetupController.cs create mode 100644 Moonlight.Api/Services/SettingsService.cs create mode 100644 Moonlight.Shared/Http/Requests/Seup/ApplySetupDto.cs diff --git a/Moonlight.Api/Configuration/SettingsOptions.cs b/Moonlight.Api/Configuration/SettingsOptions.cs new file mode 100644 index 00000000..6ba1021e --- /dev/null +++ b/Moonlight.Api/Configuration/SettingsOptions.cs @@ -0,0 +1,6 @@ +namespace Moonlight.Api.Configuration; + +public class SettingsOptions +{ + public int CacheMinutes { get; set; } = 3; +} \ No newline at end of file diff --git a/Moonlight.Api/Http/Controllers/Admin/SetupController.cs b/Moonlight.Api/Http/Controllers/Admin/SetupController.cs new file mode 100644 index 00000000..56d2a479 --- /dev/null +++ b/Moonlight.Api/Http/Controllers/Admin/SetupController.cs @@ -0,0 +1,127 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Moonlight.Api.Database; +using Moonlight.Api.Database.Entities; +using Moonlight.Api.Services; +using Moonlight.Shared; +using Moonlight.Shared.Http.Requests.Seup; + +namespace Moonlight.Api.Http.Controllers.Admin; + +[ApiController] +[Route("api/admin/setup")] +public class SetupController : Controller +{ + private readonly SettingsService SettingsService; + private readonly DatabaseRepository UsersRepository; + private readonly DatabaseRepository RolesRepository; + + private const string StateSettingsKey = "Moonlight.Api.Setup.State"; + + public SetupController( + SettingsService settingsService, + DatabaseRepository usersRepository, + DatabaseRepository rolesRepository + ) + { + SettingsService = settingsService; + UsersRepository = usersRepository; + RolesRepository = rolesRepository; + } + + [HttpGet] + [AllowAnonymous] + public async Task GetSetupAsync() + { + var hasBeenSetup = await SettingsService.GetValueAsync(StateSettingsKey); + + if (!string.IsNullOrWhiteSpace(hasBeenSetup) && hasBeenSetup.Equals("true", StringComparison.OrdinalIgnoreCase)) + return Problem("This instance is already configured", statusCode: 405); + + return Ok(); + } + + [HttpPost] + [AllowAnonymous] + public async Task ApplySetupAsync([FromBody] ApplySetupDto dto) + { + var adminRole = await RolesRepository + .Query() + .FirstOrDefaultAsync(x => x.Name == "Administrators"); + + if (adminRole == null) + { + adminRole = await RolesRepository.AddAsync(new Role() + { + Name = "Administrators", + Description = "Automatically generated group for full administrator permissions", + Permissions = [ + Permissions.ApiKeys.View, + Permissions.ApiKeys.Create, + Permissions.ApiKeys.Edit, + Permissions.ApiKeys.Delete, + + Permissions.Roles.View, + Permissions.Roles.Create, + Permissions.Roles.Edit, + Permissions.Roles.Delete, + Permissions.Roles.Members, + + Permissions.Users.View, + Permissions.Users.Create, + Permissions.Users.Edit, + Permissions.Users.Delete, + Permissions.Users.Logout, + + Permissions.Themes.View, + Permissions.Themes.Create, + Permissions.Themes.Edit, + Permissions.Themes.Delete, + + Permissions.System.Info, + Permissions.System.Diagnose, + ] + }); + } + + + var user = await UsersRepository + .Query() + .FirstOrDefaultAsync(u => u.Email == dto.AdminEmail); + + if (user == null) + { + await UsersRepository.AddAsync(new User() + { + Email = dto.AdminEmail, + Username = dto.AdminUsername, + RoleMemberships = [ + new RoleMember() + { + Role = adminRole, + CreatedAt = DateTimeOffset.UtcNow, + UpdatedAt = DateTimeOffset.UtcNow + } + ], + CreatedAt = DateTimeOffset.UtcNow, + UpdatedAt = DateTimeOffset.UtcNow + }); + } + else + { + user.RoleMemberships.Add(new RoleMember() + { + Role = adminRole, + CreatedAt = DateTimeOffset.UtcNow, + UpdatedAt = DateTimeOffset.UtcNow + }); + + await UsersRepository.UpdateAsync(user); + } + + await SettingsService.SetValueAsync(StateSettingsKey, "true"); + + return NoContent(); + } +} \ No newline at end of file diff --git a/Moonlight.Api/Services/SettingsService.cs b/Moonlight.Api/Services/SettingsService.cs new file mode 100644 index 00000000..1d0e7a63 --- /dev/null +++ b/Moonlight.Api/Services/SettingsService.cs @@ -0,0 +1,79 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; +using Moonlight.Api.Configuration; +using Moonlight.Api.Database; +using Moonlight.Api.Database.Entities; + +namespace Moonlight.Api.Services; + +public class SettingsService +{ + private readonly DatabaseRepository Repository; + private readonly IOptions Options; + private readonly IMemoryCache Cache; + + private const string CacheKey = "Moonlight.Api.SettingsService.{0}"; + + public SettingsService( + DatabaseRepository repository, + IOptions options, + IMemoryCache cache + ) + { + Repository = repository; + Cache = cache; + Options = options; + } + + public async Task GetValueAsync(string key) + { + var cacheKey = string.Format(CacheKey, key); + + if (!Cache.TryGetValue(cacheKey, out var value)) + { + var retrievedValue = await Repository + .Query() + .Where(x => x.Key == key) + .Select(o => o.Value) + .FirstOrDefaultAsync(); + + Cache.Set( + cacheKey, + retrievedValue, + TimeSpan.FromMinutes(Options.Value.CacheMinutes) + ); + + return retrievedValue; + } + + return value; + } + + public async Task SetValueAsync(string key, string value) + { + var cacheKey = string.Format(CacheKey, key); + + var option = await Repository + .Query() + .FirstOrDefaultAsync(x => x.Key == key); + + if (option != null) + { + option.Value = value; + await Repository.UpdateAsync(option); + } + else + { + option = new SettingsOption() + { + Key = key, + Value = value + }; + + await Repository.AddAsync(option); + } + + Cache.Remove(cacheKey); + } +} \ No newline at end of file diff --git a/Moonlight.Api/Startup/Startup.Auth.cs b/Moonlight.Api/Startup/Startup.Auth.cs index fba19bd3..03560c09 100644 --- a/Moonlight.Api/Startup/Startup.Auth.cs +++ b/Moonlight.Api/Startup/Startup.Auth.cs @@ -100,6 +100,9 @@ public partial class Startup builder.Services.AddSingleton(); builder.Services.AddSingleton(); + + builder.Services.AddOptions().BindConfiguration("Moonlight:Settings"); + builder.Services.AddScoped(); } private static void UseAuth(WebApplication application) diff --git a/Moonlight.Frontend/UI/Shared/Components/Setup.razor b/Moonlight.Frontend/UI/Shared/Components/Setup.razor index 947b6a00..20fc8365 100644 --- a/Moonlight.Frontend/UI/Shared/Components/Setup.razor +++ b/Moonlight.Frontend/UI/Shared/Components/Setup.razor @@ -1,13 +1,17 @@ @using LucideBlazor +@using Moonlight.Shared.Http.Requests.Seup @using ShadcnBlazor.Cards @using ShadcnBlazor.Spinners @using ShadcnBlazor.Buttons @using ShadcnBlazor.Inputs @using ShadcnBlazor.Labels +@inject HttpClient HttpClient +@inject NavigationManager Navigation +
- @if (!IsLoaded) + @if (IsLoaded) {
@if (CurrentStep == 0) @@ -43,14 +47,14 @@
@@ -58,7 +62,7 @@
@@ -114,7 +118,7 @@ } else { - @@ -134,14 +138,35 @@ @code { - private bool IsLoaded = false; + private bool IsLoaded; private int CurrentStep = 0; private int StepCount = 3; - private string Email; - private string Username; - private string Password; + private ApplySetupDto SetupDto = new(); private void Navigate(int diff) => CurrentStep += diff; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if(!firstRender) + return; + + var response = await HttpClient.GetAsync("api/admin/setup"); + + if (!response.IsSuccessStatusCode) + { + Navigation.NavigateTo("/", true); + return; + } + + IsLoaded = true; + await InvokeAsync(StateHasChanged); + } + + private async Task ApplyAsync() + { + await HttpClient.PostAsJsonAsync("api/admin/setup", SetupDto); + Navigation.NavigateTo("/", true); + } } \ No newline at end of file diff --git a/Moonlight.Shared/Http/Requests/Seup/ApplySetupDto.cs b/Moonlight.Shared/Http/Requests/Seup/ApplySetupDto.cs new file mode 100644 index 00000000..42455fa9 --- /dev/null +++ b/Moonlight.Shared/Http/Requests/Seup/ApplySetupDto.cs @@ -0,0 +1,8 @@ +namespace Moonlight.Shared.Http.Requests.Seup; + +public class ApplySetupDto +{ + public string AdminUsername { get; set; } + public string AdminEmail { get; set; } + public string AdminPassword { get; set; } +} \ No newline at end of file -- 2.49.1 From 743f41cbe8b2bf4cbf0e23b1bca76520ca890259 Mon Sep 17 00:00:00 2001 From: ChiaraBm Date: Thu, 29 Jan 2026 14:47:56 +0100 Subject: [PATCH 3/4] Updated settings service to support generic types with JSON serialization/deserialization, adjusted setup wizard checks and modified database schema for proper JSONB storage. --- .../Database/Entities/SettingsOption.cs | 4 +- ...witchedToJsonForSettingsOption.Designer.cs | 251 ++++++++++++++++++ ...9134620_SwitchedToJsonForSettingsOption.cs | 46 ++++ .../Migrations/DataContextModelSnapshot.cs | 4 +- .../Http/Controllers/Admin/SetupController.cs | 8 +- Moonlight.Api/Services/SettingsService.cs | 46 ++-- 6 files changed, 331 insertions(+), 28 deletions(-) create mode 100644 Moonlight.Api/Database/Migrations/20260129134620_SwitchedToJsonForSettingsOption.Designer.cs create mode 100644 Moonlight.Api/Database/Migrations/20260129134620_SwitchedToJsonForSettingsOption.cs diff --git a/Moonlight.Api/Database/Entities/SettingsOption.cs b/Moonlight.Api/Database/Entities/SettingsOption.cs index 5e35d194..74cf210a 100644 --- a/Moonlight.Api/Database/Entities/SettingsOption.cs +++ b/Moonlight.Api/Database/Entities/SettingsOption.cs @@ -1,4 +1,5 @@ using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; namespace Moonlight.Api.Database.Entities; @@ -10,5 +11,6 @@ public class SettingsOption public required string Key { get; set; } [MaxLength(4096)] - public required string Value { get; set; } + [Column(TypeName = "jsonb")] + public required string ValueJson { get; set; } } \ No newline at end of file diff --git a/Moonlight.Api/Database/Migrations/20260129134620_SwitchedToJsonForSettingsOption.Designer.cs b/Moonlight.Api/Database/Migrations/20260129134620_SwitchedToJsonForSettingsOption.Designer.cs new file mode 100644 index 00000000..97ae800d --- /dev/null +++ b/Moonlight.Api/Database/Migrations/20260129134620_SwitchedToJsonForSettingsOption.Designer.cs @@ -0,0 +1,251 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Moonlight.Api.Database; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Moonlight.Api.Database.Migrations +{ + [DbContext(typeof(DataContext))] + [Migration("20260129134620_SwitchedToJsonForSettingsOption")] + partial class SwitchedToJsonForSettingsOption + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("core") + .HasAnnotation("ProductVersion", "10.0.1") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Moonlight.Api.Database.Entities.ApiKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.PrimitiveCollection("Permissions") + .IsRequired() + .HasColumnType("text[]"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("ApiKeys", "core"); + }); + + modelBuilder.Entity("Moonlight.Api.Database.Entities.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.PrimitiveCollection("Permissions") + .IsRequired() + .HasColumnType("text[]"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Roles", "core"); + }); + + modelBuilder.Entity("Moonlight.Api.Database.Entities.RoleMember", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RoleId") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.HasIndex("UserId"); + + b.ToTable("RoleMembers", "core"); + }); + + modelBuilder.Entity("Moonlight.Api.Database.Entities.SettingsOption", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Key") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ValueJson") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("jsonb"); + + b.HasKey("Id"); + + b.ToTable("SettingsOptions", "core"); + }); + + modelBuilder.Entity("Moonlight.Api.Database.Entities.Theme", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Author") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("CssContent") + .IsRequired() + .HasMaxLength(20000) + .HasColumnType("character varying(20000)"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("Version") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.HasKey("Id"); + + b.ToTable("Themes", "core"); + }); + + modelBuilder.Entity("Moonlight.Api.Database.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(254) + .HasColumnType("character varying(254)"); + + b.Property("InvalidateTimestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.ToTable("Users", "core"); + }); + + modelBuilder.Entity("Moonlight.Api.Database.Entities.RoleMember", b => + { + b.HasOne("Moonlight.Api.Database.Entities.Role", "Role") + .WithMany("Members") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Moonlight.Api.Database.Entities.User", "User") + .WithMany("RoleMemberships") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Role"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Moonlight.Api.Database.Entities.Role", b => + { + b.Navigation("Members"); + }); + + modelBuilder.Entity("Moonlight.Api.Database.Entities.User", b => + { + b.Navigation("RoleMemberships"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Moonlight.Api/Database/Migrations/20260129134620_SwitchedToJsonForSettingsOption.cs b/Moonlight.Api/Database/Migrations/20260129134620_SwitchedToJsonForSettingsOption.cs new file mode 100644 index 00000000..376d6285 --- /dev/null +++ b/Moonlight.Api/Database/Migrations/20260129134620_SwitchedToJsonForSettingsOption.cs @@ -0,0 +1,46 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Moonlight.Api.Database.Migrations +{ + /// + public partial class SwitchedToJsonForSettingsOption : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Value", + schema: "core", + table: "SettingsOptions"); + + migrationBuilder.AddColumn( + name: "ValueJson", + schema: "core", + table: "SettingsOptions", + type: "jsonb", + maxLength: 4096, + nullable: false, + defaultValue: ""); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "ValueJson", + schema: "core", + table: "SettingsOptions"); + + migrationBuilder.AddColumn( + name: "Value", + schema: "core", + table: "SettingsOptions", + type: "character varying(4096)", + maxLength: 4096, + nullable: false, + defaultValue: ""); + } + } +} diff --git a/Moonlight.Api/Database/Migrations/DataContextModelSnapshot.cs b/Moonlight.Api/Database/Migrations/DataContextModelSnapshot.cs index 57359491..53294dcf 100644 --- a/Moonlight.Api/Database/Migrations/DataContextModelSnapshot.cs +++ b/Moonlight.Api/Database/Migrations/DataContextModelSnapshot.cs @@ -136,10 +136,10 @@ namespace Moonlight.Api.Database.Migrations .HasMaxLength(256) .HasColumnType("character varying(256)"); - b.Property("Value") + b.Property("ValueJson") .IsRequired() .HasMaxLength(4096) - .HasColumnType("character varying(4096)"); + .HasColumnType("jsonb"); b.HasKey("Id"); diff --git a/Moonlight.Api/Http/Controllers/Admin/SetupController.cs b/Moonlight.Api/Http/Controllers/Admin/SetupController.cs index 56d2a479..eb1bb8c5 100644 --- a/Moonlight.Api/Http/Controllers/Admin/SetupController.cs +++ b/Moonlight.Api/Http/Controllers/Admin/SetupController.cs @@ -34,12 +34,12 @@ public class SetupController : Controller [AllowAnonymous] public async Task GetSetupAsync() { - var hasBeenSetup = await SettingsService.GetValueAsync(StateSettingsKey); + var hasBeenSetup = await SettingsService.GetValueAsync(StateSettingsKey); - if (!string.IsNullOrWhiteSpace(hasBeenSetup) && hasBeenSetup.Equals("true", StringComparison.OrdinalIgnoreCase)) + if (hasBeenSetup) return Problem("This instance is already configured", statusCode: 405); - return Ok(); + return NoContent(); } [HttpPost] @@ -120,7 +120,7 @@ public class SetupController : Controller await UsersRepository.UpdateAsync(user); } - await SettingsService.SetValueAsync(StateSettingsKey, "true"); + await SettingsService.SetValueAsync(StateSettingsKey, true); return NoContent(); } diff --git a/Moonlight.Api/Services/SettingsService.cs b/Moonlight.Api/Services/SettingsService.cs index 1d0e7a63..f6361fbb 100644 --- a/Moonlight.Api/Services/SettingsService.cs +++ b/Moonlight.Api/Services/SettingsService.cs @@ -1,4 +1,5 @@ -using Microsoft.EntityFrameworkCore; +using System.Text.Json; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; using Moonlight.Api.Configuration; @@ -26,31 +27,32 @@ public class SettingsService Options = options; } - public async Task GetValueAsync(string key) + public async Task GetValueAsync(string key) { var cacheKey = string.Format(CacheKey, key); - if (!Cache.TryGetValue(cacheKey, out var value)) - { - var retrievedValue = await Repository - .Query() - .Where(x => x.Key == key) - .Select(o => o.Value) - .FirstOrDefaultAsync(); + if (Cache.TryGetValue(cacheKey, out var value)) + return JsonSerializer.Deserialize(value!); + + value = await Repository + .Query() + .Where(x => x.Key == key) + .Select(o => o.ValueJson) + .FirstOrDefaultAsync(); + + if(string.IsNullOrEmpty(value)) + return default; - Cache.Set( - cacheKey, - retrievedValue, - TimeSpan.FromMinutes(Options.Value.CacheMinutes) - ); + Cache.Set( + cacheKey, + value, + TimeSpan.FromMinutes(Options.Value.CacheMinutes) + ); - return retrievedValue; - } - - return value; + return JsonSerializer.Deserialize(value); } - public async Task SetValueAsync(string key, string value) + public async Task SetValueAsync(string key, T value) { var cacheKey = string.Format(CacheKey, key); @@ -58,9 +60,11 @@ public class SettingsService .Query() .FirstOrDefaultAsync(x => x.Key == key); + var json = JsonSerializer.Serialize(value); + if (option != null) { - option.Value = value; + option.ValueJson = json; await Repository.UpdateAsync(option); } else @@ -68,7 +72,7 @@ public class SettingsService option = new SettingsOption() { Key = key, - Value = value + ValueJson = json }; await Repository.AddAsync(option); -- 2.49.1 From 604588246ddbc1e42721fcada777010ffa2cbf68 Mon Sep 17 00:00:00 2001 From: ChiaraBm Date: Mon, 9 Feb 2026 07:47:38 +0100 Subject: [PATCH 4/4] Added validation to setup dto --- .../Http/Requests/Seup/ApplySetupDto.cs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/Moonlight.Shared/Http/Requests/Seup/ApplySetupDto.cs b/Moonlight.Shared/Http/Requests/Seup/ApplySetupDto.cs index 42455fa9..28c29239 100644 --- a/Moonlight.Shared/Http/Requests/Seup/ApplySetupDto.cs +++ b/Moonlight.Shared/Http/Requests/Seup/ApplySetupDto.cs @@ -1,8 +1,20 @@ -namespace Moonlight.Shared.Http.Requests.Seup; +using System.ComponentModel.DataAnnotations; + +namespace Moonlight.Shared.Http.Requests.Seup; public class ApplySetupDto { + [Required] + [MinLength(3)] + [MaxLength(32)] public string AdminUsername { get; set; } + + [Required] + [EmailAddress] public string AdminEmail { get; set; } + + [Required] + [MinLength(8)] + [MaxLength(64)] public string AdminPassword { get; set; } } \ No newline at end of file -- 2.49.1