Started implementing setup wizard backend for initial instance configuration. Adjusted ui
This commit is contained in:
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;
|
||||||
|
}
|
||||||
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(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<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();
|
||||||
|
}
|
||||||
|
}
|
||||||
79
Moonlight.Api/Services/SettingsService.cs
Normal file
79
Moonlight.Api/Services/SettingsService.cs
Normal file
@@ -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<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<string?> GetValueAsync(string key)
|
||||||
|
{
|
||||||
|
var cacheKey = string.Format(CacheKey, key);
|
||||||
|
|
||||||
|
if (!Cache.TryGetValue<string>(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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -100,6 +100,9 @@ public partial class Startup
|
|||||||
|
|
||||||
builder.Services.AddSingleton<IAuthorizationHandler, PermissionAuthorizationHandler>();
|
builder.Services.AddSingleton<IAuthorizationHandler, PermissionAuthorizationHandler>();
|
||||||
builder.Services.AddSingleton<IAuthorizationPolicyProvider, PermissionPolicyProvider>();
|
builder.Services.AddSingleton<IAuthorizationPolicyProvider, PermissionPolicyProvider>();
|
||||||
|
|
||||||
|
builder.Services.AddOptions<SettingsOptions>().BindConfiguration("Moonlight:Settings");
|
||||||
|
builder.Services.AddScoped<SettingsService>();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void UseAuth(WebApplication application)
|
private static void UseAuth(WebApplication application)
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
@using LucideBlazor
|
@using LucideBlazor
|
||||||
|
@using Moonlight.Shared.Http.Requests.Seup
|
||||||
@using ShadcnBlazor.Cards
|
@using ShadcnBlazor.Cards
|
||||||
@using ShadcnBlazor.Spinners
|
@using ShadcnBlazor.Spinners
|
||||||
@using ShadcnBlazor.Buttons
|
@using ShadcnBlazor.Buttons
|
||||||
@using ShadcnBlazor.Inputs
|
@using ShadcnBlazor.Inputs
|
||||||
@using ShadcnBlazor.Labels
|
@using ShadcnBlazor.Labels
|
||||||
|
|
||||||
|
@inject HttpClient HttpClient
|
||||||
|
@inject NavigationManager Navigation
|
||||||
|
|
||||||
<div class="h-screen w-full flex items-center justify-center">
|
<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">
|
<Card ClassName="w-full max-w-[calc(100%-2rem)] lg:max-w-xl grid gap-4 p-6">
|
||||||
@if (!IsLoaded)
|
@if (IsLoaded)
|
||||||
{
|
{
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
@if (CurrentStep == 0)
|
@if (CurrentStep == 0)
|
||||||
@@ -43,14 +47,14 @@
|
|||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
<Label for="username">Username</Label>
|
<Label for="username">Username</Label>
|
||||||
<InputField
|
<InputField
|
||||||
@bind-Value="Username"
|
@bind-Value="SetupDto.AdminUsername"
|
||||||
id="username"
|
id="username"
|
||||||
placeholder="someoneelse"/>
|
placeholder="someoneelse"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
<Label for="email">Email</Label>
|
<Label for="email">Email</Label>
|
||||||
<InputField
|
<InputField
|
||||||
@bind-Value="Email"
|
@bind-Value="SetupDto.AdminEmail"
|
||||||
id="email"
|
id="email"
|
||||||
Type="email"
|
Type="email"
|
||||||
placeholder="a@cool.email"/>
|
placeholder="a@cool.email"/>
|
||||||
@@ -58,7 +62,7 @@
|
|||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
<Label for="password">Password</Label>
|
<Label for="password">Password</Label>
|
||||||
<InputField
|
<InputField
|
||||||
@bind-Value="Password"
|
@bind-Value="SetupDto.AdminPassword"
|
||||||
id="password"
|
id="password"
|
||||||
Type="password"
|
Type="password"
|
||||||
placeholder="......."/>
|
placeholder="......."/>
|
||||||
@@ -114,7 +118,7 @@
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<Button>
|
<Button @onclick="ApplyAsync">
|
||||||
Finish
|
Finish
|
||||||
<RocketIcon />
|
<RocketIcon />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -134,14 +138,35 @@
|
|||||||
|
|
||||||
@code
|
@code
|
||||||
{
|
{
|
||||||
private bool IsLoaded = false;
|
private bool IsLoaded;
|
||||||
|
|
||||||
private int CurrentStep = 0;
|
private int CurrentStep = 0;
|
||||||
private int StepCount = 3;
|
private int StepCount = 3;
|
||||||
|
|
||||||
private string Email;
|
private ApplySetupDto SetupDto = new();
|
||||||
private string Username;
|
|
||||||
private string Password;
|
|
||||||
|
|
||||||
private void Navigate(int diff) => CurrentStep += diff;
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
8
Moonlight.Shared/Http/Requests/Seup/ApplySetupDto.cs
Normal file
8
Moonlight.Shared/Http/Requests/Seup/ApplySetupDto.cs
Normal file
@@ -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; }
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user