Compare commits
4 Commits
c8fe11bd2b
...
8d9a7bb8b3
| Author | SHA1 | Date | |
|---|---|---|---|
| 8d9a7bb8b3 | |||
| 1f631be1c7 | |||
| 5b4959771c | |||
| b8e1bbb28c |
6
Moonlight.Api/Configuration/SettingsOptions.cs
Normal file
6
Moonlight.Api/Configuration/SettingsOptions.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace Moonlight.Api.Configuration;
|
||||
|
||||
public class SettingsOptions
|
||||
{
|
||||
public int CacheMinutes { get; set; } = 3;
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
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)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("Value")
|
||||
b.Property<string>("ValueJson")
|
||||
.IsRequired()
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("character varying(4096)");
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
|
||||
127
Moonlight.Api/Http/Controllers/Admin/SetupController.cs
Normal file
127
Moonlight.Api/Http/Controllers/Admin/SetupController.cs
Normal file
@@ -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<User> UsersRepository;
|
||||
private readonly DatabaseRepository<Role> RolesRepository;
|
||||
|
||||
private const string StateSettingsKey = "Moonlight.Api.Setup.State";
|
||||
|
||||
public SetupController(
|
||||
SettingsService settingsService,
|
||||
DatabaseRepository<User> usersRepository,
|
||||
DatabaseRepository<Role> rolesRepository
|
||||
)
|
||||
{
|
||||
SettingsService = settingsService;
|
||||
UsersRepository = usersRepository;
|
||||
RolesRepository = rolesRepository;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[AllowAnonymous]
|
||||
public async Task<ActionResult> GetSetupAsync()
|
||||
{
|
||||
var hasBeenSetup = await SettingsService.GetValueAsync<bool>(StateSettingsKey);
|
||||
|
||||
if (hasBeenSetup)
|
||||
return Problem("This instance is already configured", statusCode: 405);
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[AllowAnonymous]
|
||||
public async Task<ActionResult> 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();
|
||||
}
|
||||
}
|
||||
83
Moonlight.Api/Services/SettingsService.cs
Normal file
83
Moonlight.Api/Services/SettingsService.cs
Normal file
@@ -0,0 +1,83 @@
|
||||
using System.Text.Json;
|
||||
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<SettingsOption> Repository;
|
||||
private readonly IOptions<SettingsOptions> Options;
|
||||
private readonly IMemoryCache Cache;
|
||||
|
||||
private const string CacheKey = "Moonlight.Api.SettingsService.{0}";
|
||||
|
||||
public SettingsService(
|
||||
DatabaseRepository<SettingsOption> repository,
|
||||
IOptions<SettingsOptions> options,
|
||||
IMemoryCache cache
|
||||
)
|
||||
{
|
||||
Repository = repository;
|
||||
Cache = cache;
|
||||
Options = options;
|
||||
}
|
||||
|
||||
public async Task<T?> GetValueAsync<T>(string key)
|
||||
{
|
||||
var cacheKey = string.Format(CacheKey, key);
|
||||
|
||||
if (Cache.TryGetValue<string>(cacheKey, out var value))
|
||||
return JsonSerializer.Deserialize<T>(value!);
|
||||
|
||||
value = await Repository
|
||||
.Query()
|
||||
.Where(x => x.Key == key)
|
||||
.Select(o => o.ValueJson)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if(string.IsNullOrEmpty(value))
|
||||
return default;
|
||||
|
||||
Cache.Set(
|
||||
cacheKey,
|
||||
value,
|
||||
TimeSpan.FromMinutes(Options.Value.CacheMinutes)
|
||||
);
|
||||
|
||||
return JsonSerializer.Deserialize<T>(value);
|
||||
}
|
||||
|
||||
public async Task SetValueAsync<T>(string key, T value)
|
||||
{
|
||||
var cacheKey = string.Format(CacheKey, key);
|
||||
|
||||
var option = await Repository
|
||||
.Query()
|
||||
.FirstOrDefaultAsync(x => x.Key == key);
|
||||
|
||||
var json = JsonSerializer.Serialize(value);
|
||||
|
||||
if (option != null)
|
||||
{
|
||||
option.ValueJson = json;
|
||||
await Repository.UpdateAsync(option);
|
||||
}
|
||||
else
|
||||
{
|
||||
option = new SettingsOption()
|
||||
{
|
||||
Key = key,
|
||||
ValueJson = json
|
||||
};
|
||||
|
||||
await Repository.AddAsync(option);
|
||||
}
|
||||
|
||||
Cache.Remove(cacheKey);
|
||||
}
|
||||
}
|
||||
@@ -100,6 +100,9 @@ public partial class Startup
|
||||
|
||||
builder.Services.AddSingleton<IAuthorizationHandler, PermissionAuthorizationHandler>();
|
||||
builder.Services.AddSingleton<IAuthorizationPolicyProvider, PermissionPolicyProvider>();
|
||||
|
||||
builder.Services.AddOptions<SettingsOptions>().BindConfiguration("Moonlight:Settings");
|
||||
builder.Services.AddScoped<SettingsService>();
|
||||
}
|
||||
|
||||
private static void UseAuth(WebApplication application)
|
||||
|
||||
@@ -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
|
||||
|
||||
<ErrorBoundary>
|
||||
<ChildContent>
|
||||
<AuthorizeView>
|
||||
@@ -29,9 +32,18 @@
|
||||
<AccessDenied/>
|
||||
}
|
||||
else
|
||||
{
|
||||
var uri = new Uri(Navigation.Uri);
|
||||
|
||||
if (uri.LocalPath.StartsWith("/setup"))
|
||||
{
|
||||
<Setup />
|
||||
}
|
||||
else
|
||||
{
|
||||
<Authentication/>
|
||||
}
|
||||
}
|
||||
</NotAuthorized>
|
||||
</AuthorizeView>
|
||||
</ChildContent>
|
||||
|
||||
172
Moonlight.Frontend/UI/Shared/Components/Setup.razor
Normal file
172
Moonlight.Frontend/UI/Shared/Components/Setup.razor
Normal file
@@ -0,0 +1,172 @@
|
||||
@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
|
||||
|
||||
<div class="h-screen w-full flex items-center justify-center">
|
||||
<Card ClassName="w-full max-w-[calc(100%-2rem)] lg:max-w-xl grid gap-4 p-6">
|
||||
@if (IsLoaded)
|
||||
{
|
||||
<div class="space-y-6">
|
||||
@if (CurrentStep == 0)
|
||||
{
|
||||
<div
|
||||
class="flex h-56 items-center justify-center rounded-lg bg-linear-to-br from-gray-100 to-gray-300 dark:from-gray-800 dark:to-gray-900">
|
||||
<img alt="Moonlight Logo" class="size-34" src="/_content/Moonlight.Frontend/logo.svg" />
|
||||
</div>
|
||||
|
||||
<div class="space-y-2 text-center">
|
||||
<h2 class="text-2xl font-bold">Welcome to Moonlight Panel</h2>
|
||||
<p class="text-muted-foreground">
|
||||
You successfully installed moonlight. Now you are ready to perform some initial steps to complete your installation
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
else if (CurrentStep == 1)
|
||||
{
|
||||
<div
|
||||
class="flex h-56 items-center justify-center rounded-lg bg-linear-to-br from-gray-100 to-gray-300 dark:from-gray-800 dark:to-gray-900">
|
||||
<UserIcon ClassName="size-34 text-blue-500" />
|
||||
</div>
|
||||
|
||||
<div class="space-y-2 text-center">
|
||||
<h2 class="text-2xl font-bold">Admin Account Creation</h2>
|
||||
<p class="text-muted-foreground">
|
||||
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
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-5">
|
||||
<div class="grid gap-2">
|
||||
<Label for="username">Username</Label>
|
||||
<InputField
|
||||
@bind-Value="SetupDto.AdminUsername"
|
||||
id="username"
|
||||
placeholder="someoneelse"/>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="email">Email</Label>
|
||||
<InputField
|
||||
@bind-Value="SetupDto.AdminEmail"
|
||||
id="email"
|
||||
Type="email"
|
||||
placeholder="a@cool.email"/>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="password">Password</Label>
|
||||
<InputField
|
||||
@bind-Value="SetupDto.AdminPassword"
|
||||
id="password"
|
||||
Type="password"
|
||||
placeholder="......."/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else if (CurrentStep == 2)
|
||||
{
|
||||
<div
|
||||
class="flex h-56 items-center justify-center rounded-lg bg-linear-to-br from-gray-100 to-gray-300 dark:from-gray-800 dark:to-gray-900">
|
||||
<RocketIcon ClassName="size-34 text-blue-500" />
|
||||
</div>
|
||||
|
||||
<div class="space-y-2 text-center">
|
||||
<h2 class="text-2xl font-bold">You are all set!</h2>
|
||||
<p class="text-muted-foreground">
|
||||
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
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex gap-2">
|
||||
@for (var step = 0; step < StepCount; step++)
|
||||
{
|
||||
if (step == CurrentStep)
|
||||
{
|
||||
<div class="h-2 w-2 rounded-full transition-colors bg-foreground">
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="h-2 w-2 rounded-full transition-colors bg-muted">
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
<div class="flex gap-1.5">
|
||||
@if (CurrentStep > 0)
|
||||
{
|
||||
<Button @onclick="() => Navigate(-1)" Variant="ButtonVariant.Outline">
|
||||
<ChevronLeftIcon />
|
||||
Back
|
||||
</Button>
|
||||
}
|
||||
@if (CurrentStep != StepCount - 1)
|
||||
{
|
||||
<Button @onclick="() => Navigate(1)">
|
||||
Continue
|
||||
<ArrowRightIcon/>
|
||||
</Button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<Button @onclick="ApplyAsync">
|
||||
Finish
|
||||
<RocketIcon />
|
||||
</Button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="w-full flex justify-center items-center">
|
||||
<Spinner ClassName="size-10"/>
|
||||
</div>
|
||||
}
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@code
|
||||
{
|
||||
private bool IsLoaded;
|
||||
|
||||
private int CurrentStep = 0;
|
||||
private int StepCount = 3;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
20
Moonlight.Shared/Http/Requests/Seup/ApplySetupDto.cs
Normal file
20
Moonlight.Shared/Http/Requests/Seup/ApplySetupDto.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
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; }
|
||||
}
|
||||
Reference in New Issue
Block a user