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);