Implemented first iteration of initial setup guide #9
@@ -1,4 +1,5 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
namespace Moonlight.Api.Database.Entities;
|
namespace Moonlight.Api.Database.Entities;
|
||||||
|
|
||||||
@@ -10,5 +11,6 @@ public class SettingsOption
|
|||||||
public required string Key { get; set; }
|
public required string Key { get; set; }
|
||||||
|
|
||||||
[MaxLength(4096)]
|
[MaxLength(4096)]
|
||||||
public required string Value { get; set; }
|
[Column(TypeName = "jsonb")]
|
||||||
|
public required string ValueJson { get; set; }
|
||||||
}
|
}
|
||||||
251
Moonlight.Api/Database/Migrations/20260129134620_SwitchedToJsonForSettingsOption.Designer.cs
generated
Normal file
251
Moonlight.Api/Database/Migrations/20260129134620_SwitchedToJsonForSettingsOption.Designer.cs
generated
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
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
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
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<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(300)
|
||||||
|
.HasColumnType("character varying(300)");
|
||||||
|
|
||||||
|
b.Property<string>("Key")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnType("character varying(32)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(30)
|
||||||
|
.HasColumnType("character varying(30)");
|
||||||
|
|
||||||
|
b.PrimitiveCollection<string[]>("Permissions")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text[]");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("ApiKeys", "core");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Moonlight.Api.Database.Entities.Role", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(300)
|
||||||
|
.HasColumnType("character varying(300)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(30)
|
||||||
|
.HasColumnType("character varying(30)");
|
||||||
|
|
||||||
|
b.PrimitiveCollection<string[]>("Permissions")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text[]");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Roles", "core");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Moonlight.Api.Database.Entities.RoleMember", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<int>("RoleId")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<int>("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<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("Key")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<string>("ValueJson")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(4096)
|
||||||
|
.HasColumnType("jsonb");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("SettingsOptions", "core");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Moonlight.Api.Database.Entities.Theme", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("Author")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(30)
|
||||||
|
.HasColumnType("character varying(30)");
|
||||||
|
|
||||||
|
b.Property<string>("CssContent")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(20000)
|
||||||
|
.HasColumnType("character varying(20000)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsEnabled")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(30)
|
||||||
|
.HasColumnType("character varying(30)");
|
||||||
|
|
||||||
|
b.Property<string>("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<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(254)
|
||||||
|
.HasColumnType("character varying(254)");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("InvalidateTimestamp")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Moonlight.Api.Database.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class SwitchedToJsonForSettingsOption : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Value",
|
||||||
|
schema: "core",
|
||||||
|
table: "SettingsOptions");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "ValueJson",
|
||||||
|
schema: "core",
|
||||||
|
table: "SettingsOptions",
|
||||||
|
type: "jsonb",
|
||||||
|
maxLength: 4096,
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: "");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "ValueJson",
|
||||||
|
schema: "core",
|
||||||
|
table: "SettingsOptions");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "Value",
|
||||||
|
schema: "core",
|
||||||
|
table: "SettingsOptions",
|
||||||
|
type: "character varying(4096)",
|
||||||
|
maxLength: 4096,
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -136,10 +136,10 @@ namespace Moonlight.Api.Database.Migrations
|
|||||||
.HasMaxLength(256)
|
.HasMaxLength(256)
|
||||||
.HasColumnType("character varying(256)");
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
b.Property<string>("Value")
|
b.Property<string>("ValueJson")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasMaxLength(4096)
|
.HasMaxLength(4096)
|
||||||
.HasColumnType("character varying(4096)");
|
.HasColumnType("jsonb");
|
||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
|||||||
@@ -34,12 +34,12 @@ public class SetupController : Controller
|
|||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
public async Task<ActionResult> GetSetupAsync()
|
public async Task<ActionResult> GetSetupAsync()
|
||||||
{
|
{
|
||||||
var hasBeenSetup = await SettingsService.GetValueAsync(StateSettingsKey);
|
var hasBeenSetup = await SettingsService.GetValueAsync<bool>(StateSettingsKey);
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(hasBeenSetup) && hasBeenSetup.Equals("true", StringComparison.OrdinalIgnoreCase))
|
if (hasBeenSetup)
|
||||||
return Problem("This instance is already configured", statusCode: 405);
|
return Problem("This instance is already configured", statusCode: 405);
|
||||||
|
|
||||||
return Ok();
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
@@ -120,7 +120,7 @@ public class SetupController : Controller
|
|||||||
await UsersRepository.UpdateAsync(user);
|
await UsersRepository.UpdateAsync(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
await SettingsService.SetValueAsync(StateSettingsKey, "true");
|
await SettingsService.SetValueAsync(StateSettingsKey, true);
|
||||||
|
|
||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using System.Text.Json;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using Moonlight.Api.Configuration;
|
using Moonlight.Api.Configuration;
|
||||||
@@ -26,31 +27,32 @@ public class SettingsService
|
|||||||
Options = options;
|
Options = options;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<string?> GetValueAsync(string key)
|
public async Task<T?> GetValueAsync<T>(string key)
|
||||||
{
|
{
|
||||||
var cacheKey = string.Format(CacheKey, key);
|
var cacheKey = string.Format(CacheKey, key);
|
||||||
|
|
||||||
if (!Cache.TryGetValue<string>(cacheKey, out var value))
|
if (Cache.TryGetValue<string>(cacheKey, out var value))
|
||||||
{
|
return JsonSerializer.Deserialize<T>(value!);
|
||||||
var retrievedValue = await Repository
|
|
||||||
.Query()
|
value = await Repository
|
||||||
.Where(x => x.Key == key)
|
.Query()
|
||||||
.Select(o => o.Value)
|
.Where(x => x.Key == key)
|
||||||
.FirstOrDefaultAsync();
|
.Select(o => o.ValueJson)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
|
||||||
|
if(string.IsNullOrEmpty(value))
|
||||||
|
return default;
|
||||||
|
|
||||||
Cache.Set(
|
Cache.Set(
|
||||||
cacheKey,
|
cacheKey,
|
||||||
retrievedValue,
|
value,
|
||||||
TimeSpan.FromMinutes(Options.Value.CacheMinutes)
|
TimeSpan.FromMinutes(Options.Value.CacheMinutes)
|
||||||
);
|
);
|
||||||
|
|
||||||
return retrievedValue;
|
return JsonSerializer.Deserialize<T>(value);
|
||||||
}
|
|
||||||
|
|
||||||
return value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SetValueAsync(string key, string value)
|
public async Task SetValueAsync<T>(string key, T value)
|
||||||
{
|
{
|
||||||
var cacheKey = string.Format(CacheKey, key);
|
var cacheKey = string.Format(CacheKey, key);
|
||||||
|
|
||||||
@@ -58,9 +60,11 @@ public class SettingsService
|
|||||||
.Query()
|
.Query()
|
||||||
.FirstOrDefaultAsync(x => x.Key == key);
|
.FirstOrDefaultAsync(x => x.Key == key);
|
||||||
|
|
||||||
|
var json = JsonSerializer.Serialize(value);
|
||||||
|
|
||||||
if (option != null)
|
if (option != null)
|
||||||
{
|
{
|
||||||
option.Value = value;
|
option.ValueJson = json;
|
||||||
await Repository.UpdateAsync(option);
|
await Repository.UpdateAsync(option);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -68,7 +72,7 @@ public class SettingsService
|
|||||||
option = new SettingsOption()
|
option = new SettingsOption()
|
||||||
{
|
{
|
||||||
Key = key,
|
Key = key,
|
||||||
Value = value
|
ValueJson = json
|
||||||
};
|
};
|
||||||
|
|
||||||
await Repository.AddAsync(option);
|
await Repository.AddAsync(option);
|
||||||
|
|||||||
Reference in New Issue
Block a user