Compare commits
23 Commits
826714962e
...
6d854d82d3
| Author | SHA1 | Date | |
|---|---|---|---|
| 6d854d82d3 | |||
| ac1c28d20d | |||
| 4daf986f3e | |||
| cc7f55c988 | |||
| 11a2f9818a | |||
| 6a151394a7 | |||
| 178ac5ac20 | |||
| 91944a5ef6 | |||
| affdadf3aa | |||
| 8d9a7bb8b3 | |||
| 1f631be1c7 | |||
| 5b4959771c | |||
| b8e1bbb28c | |||
| 09b11cc4ad | |||
| 660319afec | |||
| 8181404f0c | |||
| e1207b8d9b | |||
| 97a676ccd7 | |||
| 136620f1e6 | |||
| 9b11360a0e | |||
| deb69e6014 | |||
| 4e96905fb2 | |||
| e2f344ab4e |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -400,6 +400,7 @@ FodyWeavers.xsd
|
||||
# Style builds
|
||||
**/style.min.css
|
||||
**/package-lock.json
|
||||
**/bun.lock
|
||||
|
||||
# Secrets
|
||||
**/.env
|
||||
|
||||
7
Moonlight.Api/Configuration/ContainerHelperOptions.cs
Normal file
7
Moonlight.Api/Configuration/ContainerHelperOptions.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace Moonlight.Api.Configuration;
|
||||
|
||||
public class ContainerHelperOptions
|
||||
{
|
||||
public bool IsEnabled { get; set; }
|
||||
public string Url { get; set; } = "http://helper:8080";
|
||||
}
|
||||
@@ -4,6 +4,7 @@ public class OidcOptions
|
||||
{
|
||||
public string Authority { get; set; }
|
||||
public bool RequireHttpsMetadata { get; set; } = true;
|
||||
public bool DisableHttpsOnlyCookies { get; set; }
|
||||
public string ResponseType { get; set; } = "code";
|
||||
public string[]? Scopes { get; set; }
|
||||
public string ClientId { get; set; }
|
||||
|
||||
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");
|
||||
|
||||
|
||||
@@ -6,9 +6,9 @@ using Moonlight.Api.Database.Entities;
|
||||
using Moonlight.Api.Mappers;
|
||||
using Moonlight.Shared;
|
||||
using Moonlight.Shared.Http.Requests;
|
||||
using Moonlight.Shared.Http.Requests.ApiKeys;
|
||||
using Moonlight.Shared.Http.Requests.Admin.ApiKeys;
|
||||
using Moonlight.Shared.Http.Responses;
|
||||
using Moonlight.Shared.Http.Responses.ApiKeys;
|
||||
using Moonlight.Shared.Http.Responses.Admin.ApiKeys;
|
||||
|
||||
namespace Moonlight.Api.Http.Controllers.Admin;
|
||||
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Moonlight.Api.Configuration;
|
||||
using Moonlight.Api.Mappers;
|
||||
using Moonlight.Api.Services;
|
||||
using Moonlight.Shared;
|
||||
using Moonlight.Shared.Http.Requests.Admin.ContainerHelper;
|
||||
using Moonlight.Shared.Http.Responses.Admin;
|
||||
|
||||
namespace Moonlight.Api.Http.Controllers.Admin;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/admin/ch")]
|
||||
[Authorize(Policy = Permissions.System.Instance)]
|
||||
public class ContainerHelperController : Controller
|
||||
{
|
||||
private readonly ContainerHelperService ContainerHelperService;
|
||||
private readonly IOptions<ContainerHelperOptions> Options;
|
||||
|
||||
public ContainerHelperController(ContainerHelperService containerHelperService, IOptions<ContainerHelperOptions> options)
|
||||
{
|
||||
ContainerHelperService = containerHelperService;
|
||||
Options = options;
|
||||
}
|
||||
|
||||
[HttpGet("status")]
|
||||
public async Task<ActionResult<ContainerHelperStatusDto>> GetStatusAsync()
|
||||
{
|
||||
if (!Options.Value.IsEnabled)
|
||||
return new ContainerHelperStatusDto(false, false);
|
||||
|
||||
var status = await ContainerHelperService.CheckConnectionAsync();
|
||||
|
||||
return new ContainerHelperStatusDto(true, status);
|
||||
}
|
||||
|
||||
[HttpPost("rebuild")]
|
||||
public Task<IResult> RebuildAsync([FromBody] RequestRebuildDto request)
|
||||
{
|
||||
var result = ContainerHelperService.RebuildAsync(request.NoBuildCache);
|
||||
var mappedResult = result.Select(ContainerHelperMapper.ToDto);
|
||||
|
||||
return Task.FromResult<IResult>(
|
||||
TypedResults.ServerSentEvents(mappedResult)
|
||||
);
|
||||
}
|
||||
|
||||
[HttpPost("version")]
|
||||
public async Task<ActionResult> SetVersionAsync([FromBody] SetVersionDto request)
|
||||
{
|
||||
await ContainerHelperService.SetVersionAsync(request.Version);
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ using Moonlight.Api.Database.Entities;
|
||||
using Moonlight.Api.Mappers;
|
||||
using Moonlight.Shared;
|
||||
using Moonlight.Shared.Http.Responses;
|
||||
using Moonlight.Shared.Http.Responses.Users;
|
||||
using Moonlight.Shared.Http.Responses.Admin.Users;
|
||||
|
||||
namespace Moonlight.Api.Http.Controllers.Admin;
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ using Moonlight.Api.Database.Entities;
|
||||
using Moonlight.Api.Mappers;
|
||||
using Moonlight.Shared;
|
||||
using Moonlight.Shared.Http.Requests;
|
||||
using Moonlight.Shared.Http.Requests.Roles;
|
||||
using Moonlight.Shared.Http.Requests.Admin.Roles;
|
||||
using Moonlight.Shared.Http.Responses;
|
||||
using Moonlight.Shared.Http.Responses.Admin;
|
||||
|
||||
|
||||
129
Moonlight.Api/Http/Controllers/Admin/SetupController.cs
Normal file
129
Moonlight.Api/Http/Controllers/Admin/SetupController.cs
Normal file
@@ -0,0 +1,129 @@
|
||||
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,
|
||||
Permissions.System.Versions,
|
||||
Permissions.System.Instance,
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -7,9 +7,9 @@ using Moonlight.Api.Mappers;
|
||||
using Moonlight.Api.Services;
|
||||
using Moonlight.Shared;
|
||||
using Moonlight.Shared.Http.Requests;
|
||||
using Moonlight.Shared.Http.Requests.Themes;
|
||||
using Moonlight.Shared.Http.Requests.Admin.Themes;
|
||||
using Moonlight.Shared.Http.Responses;
|
||||
using Moonlight.Shared.Http.Responses.Themes;
|
||||
using Moonlight.Shared.Http.Responses.Admin.Themes;
|
||||
|
||||
namespace Moonlight.Api.Http.Controllers.Admin;
|
||||
|
||||
|
||||
@@ -6,9 +6,9 @@ using Moonlight.Api.Database.Entities;
|
||||
using Moonlight.Api.Mappers;
|
||||
using Moonlight.Shared;
|
||||
using Moonlight.Shared.Http.Requests;
|
||||
using Moonlight.Shared.Http.Requests.Users;
|
||||
using Moonlight.Shared.Http.Requests.Admin.Users;
|
||||
using Moonlight.Shared.Http.Responses;
|
||||
using Moonlight.Shared.Http.Responses.Users;
|
||||
using Moonlight.Shared.Http.Responses.Admin.Users;
|
||||
|
||||
namespace Moonlight.Api.Http.Controllers.Admin.Users;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Moonlight.Shared.Http.Responses.Auth;
|
||||
using Moonlight.Shared.Http.Responses.Admin.Auth;
|
||||
|
||||
namespace Moonlight.Api.Http.Controllers;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Moonlight.Api.Mappers;
|
||||
using Moonlight.Api.Services;
|
||||
using Moonlight.Shared.Http.Responses.Frontend;
|
||||
using Moonlight.Shared.Http.Responses.Admin.Frontend;
|
||||
|
||||
namespace Moonlight.Api.Http.Controllers;
|
||||
|
||||
|
||||
13
Moonlight.Api/Http/Controllers/PingController.cs
Normal file
13
Moonlight.Api/Http/Controllers/PingController.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Moonlight.Api.Http.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/ping")]
|
||||
public class PingController : Controller
|
||||
{
|
||||
[HttpGet]
|
||||
[AllowAnonymous]
|
||||
public IActionResult Get() => Ok("Pong");
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Moonlight.Api.Http.Services.ContainerHelper.Events;
|
||||
|
||||
public struct RebuildEventDto
|
||||
{
|
||||
[JsonPropertyName("type")]
|
||||
public RebuildEventType Type { get; set; }
|
||||
|
||||
[JsonPropertyName("data")]
|
||||
public string Data { get; set; }
|
||||
}
|
||||
|
||||
public enum RebuildEventType
|
||||
{
|
||||
Log = 0,
|
||||
Failed = 1,
|
||||
Succeeded = 2,
|
||||
Step = 3
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace Moonlight.Api.Http.Services.ContainerHelper;
|
||||
|
||||
public class ProblemDetails
|
||||
{
|
||||
public string Type { get; set; }
|
||||
public string Title { get; set; }
|
||||
public int Status { get; set; }
|
||||
public string? Detail { get; set; }
|
||||
public Dictionary<string, string[]>? Errors { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace Moonlight.Api.Http.Services.ContainerHelper.Requests;
|
||||
|
||||
public record RequestRebuildDto(bool NoBuildCache);
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace Moonlight.Api.Http.Services.ContainerHelper.Requests;
|
||||
|
||||
public record SetVersionDto(string Version);
|
||||
@@ -0,0 +1,29 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Moonlight.Api.Http.Services.ContainerHelper.Events;
|
||||
using Moonlight.Api.Http.Services.ContainerHelper.Requests;
|
||||
|
||||
namespace Moonlight.Api.Http.Services.ContainerHelper;
|
||||
|
||||
[JsonSerializable(typeof(SetVersionDto))]
|
||||
[JsonSerializable(typeof(ProblemDetails))]
|
||||
[JsonSerializable(typeof(RebuildEventDto))]
|
||||
[JsonSerializable(typeof(RequestRebuildDto))]
|
||||
public partial class SerializationContext : JsonSerializerContext
|
||||
{
|
||||
private static JsonSerializerOptions? InternalTunedOptions;
|
||||
|
||||
public static JsonSerializerOptions TunedOptions
|
||||
{
|
||||
get
|
||||
{
|
||||
if (InternalTunedOptions != null)
|
||||
return InternalTunedOptions;
|
||||
|
||||
InternalTunedOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web);
|
||||
InternalTunedOptions.TypeInfoResolverChain.Add(Default);
|
||||
|
||||
return InternalTunedOptions;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Moonlight.Api.Database.Entities;
|
||||
using Moonlight.Shared.Http.Requests.ApiKeys;
|
||||
using Moonlight.Shared.Http.Responses.ApiKeys;
|
||||
using Moonlight.Shared.Http.Requests.Admin.ApiKeys;
|
||||
using Moonlight.Shared.Http.Responses.Admin.ApiKeys;
|
||||
using Riok.Mapperly.Abstractions;
|
||||
|
||||
namespace Moonlight.Api.Mappers;
|
||||
|
||||
13
Moonlight.Api/Mappers/ContainerHelperMapper.cs
Normal file
13
Moonlight.Api/Mappers/ContainerHelperMapper.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Moonlight.Shared.Http.Events;
|
||||
using Riok.Mapperly.Abstractions;
|
||||
|
||||
namespace Moonlight.Api.Mappers;
|
||||
|
||||
[Mapper]
|
||||
[SuppressMessage("Mapper", "RMG020:No members are mapped in an object mapping")]
|
||||
[SuppressMessage("Mapper", "RMG012:No members are mapped in an object mapping")]
|
||||
public static partial class ContainerHelperMapper
|
||||
{
|
||||
public static partial RebuildEventDto ToDto(Http.Services.ContainerHelper.Events.RebuildEventDto rebuildEventDto);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Moonlight.Api.Models;
|
||||
using Moonlight.Shared.Http.Responses.Frontend;
|
||||
using Moonlight.Shared.Http.Responses.Admin.Frontend;
|
||||
using Riok.Mapperly.Abstractions;
|
||||
|
||||
namespace Moonlight.Api.Mappers;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Moonlight.Api.Database.Entities;
|
||||
using Moonlight.Shared.Http.Requests.Roles;
|
||||
using Moonlight.Shared.Http.Requests.Admin.Roles;
|
||||
using Moonlight.Shared.Http.Responses.Admin;
|
||||
using Riok.Mapperly.Abstractions;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Moonlight.Api.Database.Entities;
|
||||
using Moonlight.Shared.Http.Requests.Themes;
|
||||
using Moonlight.Shared.Http.Responses.Themes;
|
||||
using Moonlight.Shared.Http.Requests.Admin.Themes;
|
||||
using Moonlight.Shared.Http.Responses.Admin.Themes;
|
||||
using Riok.Mapperly.Abstractions;
|
||||
|
||||
namespace Moonlight.Api.Mappers;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Riok.Mapperly.Abstractions;
|
||||
using Moonlight.Shared.Http.Requests.Users;
|
||||
using Moonlight.Shared.Http.Responses.Users;
|
||||
using Moonlight.Api.Database.Entities;
|
||||
using Moonlight.Shared.Http.Requests.Admin.Users;
|
||||
using Moonlight.Shared.Http.Responses.Admin.Users;
|
||||
|
||||
namespace Moonlight.Api.Mappers;
|
||||
|
||||
|
||||
@@ -35,4 +35,8 @@
|
||||
<Visible Condition="'%(NuGetItemType)' == 'Content'">false</Visible>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Http\Services\ContainerHelper\Responses\" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
117
Moonlight.Api/Services/ContainerHelperService.cs
Normal file
117
Moonlight.Api/Services/ContainerHelperService.cs
Normal file
@@ -0,0 +1,117 @@
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using Moonlight.Api.Http.Services.ContainerHelper;
|
||||
using Moonlight.Api.Http.Services.ContainerHelper.Requests;
|
||||
using Moonlight.Api.Http.Services.ContainerHelper.Events;
|
||||
|
||||
namespace Moonlight.Api.Services;
|
||||
|
||||
public class ContainerHelperService
|
||||
{
|
||||
private readonly IHttpClientFactory HttpClientFactory;
|
||||
|
||||
public ContainerHelperService(IHttpClientFactory httpClientFactory)
|
||||
{
|
||||
HttpClientFactory = httpClientFactory;
|
||||
}
|
||||
|
||||
public async Task<bool> CheckConnectionAsync()
|
||||
{
|
||||
var client = HttpClientFactory.CreateClient("ContainerHelper");
|
||||
|
||||
try
|
||||
{
|
||||
var response = await client.GetAsync("api/ping");
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async IAsyncEnumerable<RebuildEventDto> RebuildAsync(bool noBuildCache)
|
||||
{
|
||||
var client = HttpClientFactory.CreateClient("ContainerHelper");
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, "api/rebuild");
|
||||
|
||||
request.Content = JsonContent.Create(
|
||||
new RequestRebuildDto(noBuildCache),
|
||||
null,
|
||||
SerializationContext.TunedOptions
|
||||
);
|
||||
|
||||
var response = await client.SendAsync(
|
||||
request,
|
||||
HttpCompletionOption.ResponseHeadersRead
|
||||
);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var responseText = await response.Content.ReadAsStringAsync();
|
||||
|
||||
yield return new RebuildEventDto()
|
||||
{
|
||||
Type = RebuildEventType.Failed,
|
||||
Data = responseText
|
||||
};
|
||||
|
||||
yield break;
|
||||
}
|
||||
|
||||
await using var responseStream = await response.Content.ReadAsStreamAsync();
|
||||
using var streamReader = new StreamReader(responseStream);
|
||||
|
||||
do
|
||||
{
|
||||
var line = await streamReader.ReadLineAsync();
|
||||
|
||||
if (line == null)
|
||||
break;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(line))
|
||||
continue;
|
||||
|
||||
var data = line.Trim("data: ");
|
||||
var deserializedData = JsonSerializer.Deserialize<RebuildEventDto>(data, SerializationContext.TunedOptions);
|
||||
|
||||
yield return deserializedData;
|
||||
|
||||
// Exit if service will go down for a clean exit
|
||||
if (deserializedData is { Type: RebuildEventType.Step, Data: "ServiceDown" })
|
||||
yield break;
|
||||
} while (true);
|
||||
|
||||
yield return new RebuildEventDto()
|
||||
{
|
||||
Type = RebuildEventType.Succeeded,
|
||||
Data = string.Empty
|
||||
};
|
||||
}
|
||||
|
||||
public async Task SetVersionAsync(string version)
|
||||
{
|
||||
var client = HttpClientFactory.CreateClient("ContainerHelper");
|
||||
|
||||
var response = await client.PostAsJsonAsync(
|
||||
"api/configuration/version",
|
||||
new SetVersionDto(version),
|
||||
SerializationContext.TunedOptions
|
||||
);
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
return;
|
||||
|
||||
var problemDetails =
|
||||
await response.Content.ReadFromJsonAsync<ProblemDetails>(SerializationContext.TunedOptions);
|
||||
|
||||
if (problemDetails == null)
|
||||
throw new HttpRequestException($"Failed to set version: {response.ReasonPhrase}");
|
||||
|
||||
throw new HttpRequestException($"Failed to set version: {problemDetails.Detail ?? problemDetails.Title}");
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -74,6 +74,12 @@ public partial class Startup
|
||||
options.Authority = oidcOptions.Authority;
|
||||
options.RequireHttpsMetadata = oidcOptions.RequireHttpsMetadata;
|
||||
|
||||
if (oidcOptions.DisableHttpsOnlyCookies)
|
||||
{
|
||||
options.NonceCookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
|
||||
options.CorrelationCookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
|
||||
}
|
||||
|
||||
var scopes = oidcOptions.Scopes ?? ["openid", "email", "profile"];
|
||||
|
||||
options.Scope.Clear();
|
||||
@@ -100,6 +106,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)
|
||||
|
||||
@@ -43,7 +43,16 @@ public partial class Startup
|
||||
|
||||
builder.Services.AddOptions<VersionOptions>().BindConfiguration("Moonlight:Version");
|
||||
builder.Services.AddSingleton<VersionService>();
|
||||
|
||||
builder.Services.AddOptions<ContainerHelperOptions>().BindConfiguration("Moonlight:ContainerHelper");
|
||||
builder.Services.AddSingleton<ContainerHelperService>();
|
||||
|
||||
builder.Services.AddHttpClient("ContainerHelper", (provider, client) =>
|
||||
{
|
||||
var options = provider.GetRequiredService<IOptions<ContainerHelperOptions>>();
|
||||
client.BaseAddress = new Uri(options.Value.IsEnabled ? options.Value.Url : "http://you-should-fail.invalid");
|
||||
});
|
||||
|
||||
builder.Services.AddScoped<UserDeletionService>();
|
||||
builder.Services.AddScoped<UserLogoutService>();
|
||||
}
|
||||
|
||||
30
Moonlight.Frontend/Helpers/ProblemDetailsHelper.cs
Normal file
30
Moonlight.Frontend/Helpers/ProblemDetailsHelper.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using System.Net.Http.Json;
|
||||
using Microsoft.AspNetCore.Components.Forms;
|
||||
using Moonlight.Shared.Http.Responses;
|
||||
|
||||
namespace Moonlight.Frontend.Helpers;
|
||||
|
||||
public static class ProblemDetailsHelper
|
||||
{
|
||||
public static async Task HandleProblemDetailsAsync(HttpResponseMessage response, object model, ValidationMessageStore validationMessageStore)
|
||||
{
|
||||
var problemDetails = await response.Content.ReadFromJsonAsync<ProblemDetails>();
|
||||
|
||||
if (problemDetails == null)
|
||||
response.EnsureSuccessStatusCode(); // Trigger exception when unable to parse
|
||||
else
|
||||
{
|
||||
if(!string.IsNullOrEmpty(problemDetails.Detail))
|
||||
validationMessageStore.Add(new FieldIdentifier(model, string.Empty), problemDetails.Detail);
|
||||
|
||||
if (problemDetails.Errors != null)
|
||||
{
|
||||
foreach (var error in problemDetails.Errors)
|
||||
{
|
||||
foreach (var message in error.Value)
|
||||
validationMessageStore.Add(new FieldIdentifier(model, error.Key), message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,7 @@ public sealed class PermissionProvider : IPermissionProvider
|
||||
new Permission(Permissions.System.Info, "Info", "View system info"),
|
||||
new Permission(Permissions.System.Diagnose, "Diagnose", "Run diagnostics"),
|
||||
new Permission(Permissions.System.Versions, "Versions", "Look at the available versions"),
|
||||
new Permission(Permissions.System.Instance, "Instance", "Update the moonlight instance and add plugins"),
|
||||
]),
|
||||
new PermissionCategory("API Keys", typeof(KeyIcon), [
|
||||
new Permission(Permissions.ApiKeys.Create, "Create", "Create new API keys"),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Moonlight.Shared.Http.Requests.ApiKeys;
|
||||
using Moonlight.Shared.Http.Responses.ApiKeys;
|
||||
using Moonlight.Shared.Http.Requests.Admin.ApiKeys;
|
||||
using Moonlight.Shared.Http.Responses.Admin.ApiKeys;
|
||||
using Riok.Mapperly.Abstractions;
|
||||
|
||||
namespace Moonlight.Frontend.Mappers;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Moonlight.Shared.Http.Requests.Roles;
|
||||
using Moonlight.Shared.Http.Requests.Admin.Roles;
|
||||
using Moonlight.Shared.Http.Responses.Admin;
|
||||
using Riok.Mapperly.Abstractions;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Moonlight.Shared.Http.Requests.Themes;
|
||||
using Moonlight.Shared.Http.Responses.Themes;
|
||||
using Moonlight.Shared.Http.Requests.Admin.Themes;
|
||||
using Moonlight.Shared.Http.Responses.Admin.Themes;
|
||||
using Riok.Mapperly.Abstractions;
|
||||
|
||||
namespace Moonlight.Frontend.Mappers;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Moonlight.Shared.Http.Requests.Admin.Users;
|
||||
using Moonlight.Shared.Http.Responses.Admin.Users;
|
||||
using Riok.Mapperly.Abstractions;
|
||||
using Moonlight.Shared.Http.Requests.Users;
|
||||
using Moonlight.Shared.Http.Responses.Users;
|
||||
|
||||
namespace Moonlight.Frontend.Mappers;
|
||||
|
||||
|
||||
@@ -24,8 +24,8 @@
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="10.0.1"/>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.1"/>
|
||||
<PackageReference Include="Riok.Mapperly" Version="4.3.1-next.0"/>
|
||||
<PackageReference Include="ShadcnBlazor" Version="1.0.9" />
|
||||
<PackageReference Include="ShadcnBlazor.Extras" Version="1.0.9" />
|
||||
<PackageReference Include="ShadcnBlazor" Version="1.0.11" />
|
||||
<PackageReference Include="ShadcnBlazor.Extras" Version="1.0.11" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -3,7 +3,7 @@ using System.Net.Http.Json;
|
||||
using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moonlight.Shared.Http.Responses.Auth;
|
||||
using Moonlight.Shared.Http.Responses.Admin.Auth;
|
||||
|
||||
namespace Moonlight.Frontend.Services;
|
||||
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
@using Moonlight.Frontend.UI.Admin.Components
|
||||
@using Moonlight.Shared.Http.Requests.ApiKeys
|
||||
@using Moonlight.Frontend.Helpers
|
||||
@using Moonlight.Frontend.UI.Admin.Components
|
||||
@using Moonlight.Shared.Http.Requests.Admin.ApiKeys
|
||||
@using Moonlight.Shared.Http.Responses
|
||||
@using ShadcnBlazor.Dialogs
|
||||
@using ShadcnBlazor.Extras.Common
|
||||
@using ShadcnBlazor.Extras.FormHandlers
|
||||
@using ShadcnBlazor.Extras.Forms
|
||||
@using ShadcnBlazor.Extras.Toasts
|
||||
@using ShadcnBlazor.Fields
|
||||
@using ShadcnBlazor.Inputs
|
||||
@using ShadcnBlazor.Labels
|
||||
|
||||
@inherits ShadcnBlazor.Extras.Dialogs.DialogBase
|
||||
|
||||
@inject HttpClient HttpClient
|
||||
@inject ToastService ToastService
|
||||
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create new API key</DialogTitle>
|
||||
<DialogDescription>
|
||||
@@ -15,56 +20,74 @@
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<FormHandler @ref="FormHandler" Model="Request" OnValidSubmit="SubmitAsync">
|
||||
<div class="flex flex-col gap-6">
|
||||
<FormValidationSummary />
|
||||
<EnhancedEditForm Model="Request" OnValidSubmit="OnSubmitAsync">
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="keyName">Name</Label>
|
||||
<InputField @bind-Value="Request.Name" id="keyName" placeholder="My API key" />
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="keyDescription">Description</Label>
|
||||
<textarea
|
||||
@bind="Request.Description"
|
||||
id="keyDescription"
|
||||
maxlength="100"
|
||||
class="border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm"
|
||||
placeholder="What this key is for">
|
||||
|
||||
</textarea>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label>Permissions</Label>
|
||||
<PermissionSelector Permissions="Permissions" />
|
||||
</div>
|
||||
</div>
|
||||
</FormHandler>
|
||||
|
||||
<DialogFooter ClassName="justify-end gap-x-1">
|
||||
<WButtom OnClick="() => FormHandler.SubmitAsync()">Save changes</WButtom>
|
||||
</DialogFooter>
|
||||
<FieldGroup>
|
||||
<DataAnnotationsValidator/>
|
||||
<FormValidationSummary/>
|
||||
|
||||
<FieldSet>
|
||||
<Field>
|
||||
<FieldLabel for="keyName">Name</FieldLabel>
|
||||
<TextInputField @bind-Value="Request.Name" id="keyName" placeholder="My API key"/>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel for="keyDescription">Description</FieldLabel>
|
||||
<TextareaInputField @bind-Value="Request.Description" id="keyDescription" placeholder="What this key is for"/>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel>Permissions</FieldLabel>
|
||||
<FieldContent>
|
||||
<PermissionSelector Permissions="Permissions"/>
|
||||
</FieldContent>
|
||||
</Field>
|
||||
</FieldSet>
|
||||
<Field Orientation="FieldOrientation.Horizontal" ClassName="justify-end">
|
||||
<SubmitButton>Save changes</SubmitButton>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</EnhancedEditForm>
|
||||
|
||||
@code
|
||||
{
|
||||
[Parameter] public Func<CreateApiKeyDto, Task> OnSubmit { get; set; }
|
||||
[Parameter] public Func<Task> OnSubmit { get; set; }
|
||||
|
||||
private CreateApiKeyDto Request;
|
||||
private FormHandler FormHandler;
|
||||
|
||||
private List<string> Permissions = new();
|
||||
private List<string> Permissions = new();
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
Request = new();
|
||||
Request = new()
|
||||
{
|
||||
Permissions = []
|
||||
};
|
||||
}
|
||||
|
||||
private async Task SubmitAsync()
|
||||
private async Task<bool> OnSubmitAsync(EditContext editContext, ValidationMessageStore validationMessageStore)
|
||||
{
|
||||
Request.Permissions = Permissions.ToArray();
|
||||
await OnSubmit.Invoke(Request);
|
||||
|
||||
var response = await HttpClient.PostAsJsonAsync(
|
||||
"/api/admin/apiKeys",
|
||||
Request,
|
||||
Constants.SerializerOptions
|
||||
);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
await ProblemDetailsHelper.HandleProblemDetailsAsync(response, Request, validationMessageStore);
|
||||
return false;
|
||||
}
|
||||
|
||||
await ToastService.SuccessAsync(
|
||||
"API Key creation",
|
||||
$"Successfully created API key {Request.Name}"
|
||||
);
|
||||
|
||||
await OnSubmit.Invoke();
|
||||
|
||||
await CloseAsync();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,17 @@
|
||||
@using Moonlight.Frontend.Helpers
|
||||
@using Moonlight.Frontend.UI.Admin.Components
|
||||
@using Moonlight.Shared.Http.Requests.Roles
|
||||
@using ShadcnBlazor.Buttons
|
||||
@using Moonlight.Shared.Http.Requests.Admin.Roles
|
||||
@using ShadcnBlazor.Dialogs
|
||||
@using ShadcnBlazor.Extras.Common
|
||||
@using ShadcnBlazor.Extras.FormHandlers
|
||||
@using ShadcnBlazor.Extras.Forms
|
||||
@using ShadcnBlazor.Extras.Toasts
|
||||
@using ShadcnBlazor.Fields
|
||||
@using ShadcnBlazor.Inputs
|
||||
@using ShadcnBlazor.Labels
|
||||
|
||||
@inherits ShadcnBlazor.Extras.Dialogs.DialogBase
|
||||
|
||||
@inject HttpClient HttpClient
|
||||
@inject ToastService ToastService
|
||||
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
Create new role
|
||||
@@ -18,49 +21,43 @@
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<FormHandler @ref="FormHandler" Model="Request" OnValidSubmit="OnSubmitAsync">
|
||||
<div class="flex flex-col gap-6">
|
||||
|
||||
<EnhancedEditForm Model="Request" OnValidSubmit="OnSubmitAsync">
|
||||
<FieldGroup>
|
||||
<FormValidationSummary/>
|
||||
<DataAnnotationsValidator/>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="roleName">Name</Label>
|
||||
<InputField
|
||||
@bind-Value="Request.Name"
|
||||
id="roleName"
|
||||
placeholder="My fancy role"/>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="roleDescription">Description</Label>
|
||||
<textarea
|
||||
@bind="Request.Description"
|
||||
id="roleDescription"
|
||||
maxlength="100"
|
||||
class="border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm"
|
||||
placeholder="Describe what the role should be used for">
|
||||
|
||||
</textarea>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label>Permissions</Label>
|
||||
<PermissionSelector Permissions="Permissions" />
|
||||
</div>
|
||||
</div>
|
||||
</FormHandler>
|
||||
|
||||
<DialogFooter ClassName="justify-end gap-x-1">
|
||||
<WButtom OnClick="SubmitAsync">Save changes</WButtom>
|
||||
</DialogFooter>
|
||||
<FieldSet>
|
||||
<Field>
|
||||
<FieldLabel for="roleName">Name</FieldLabel>
|
||||
<TextInputField
|
||||
@bind-Value="Request.Name"
|
||||
id="roleName"
|
||||
placeholder="My fancy role"/>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel for="keyDescription">Description</FieldLabel>
|
||||
<TextareaInputField @bind-Value="Request.Description" id="keyDescription"
|
||||
placeholder="Describe what the role should be used for"/>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel>Permissions</FieldLabel>
|
||||
<FieldContent>
|
||||
<PermissionSelector Permissions="Permissions"/>
|
||||
</FieldContent>
|
||||
</Field>
|
||||
</FieldSet>
|
||||
<Field Orientation="FieldOrientation.Horizontal" ClassName="justify-end">
|
||||
<SubmitButton>Save changes</SubmitButton>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</EnhancedEditForm>
|
||||
|
||||
@code
|
||||
{
|
||||
[Parameter] public Func<CreateRoleDto, Task> OnSubmit { get; set; }
|
||||
[Parameter] public Func<Task> OnSubmit { get; set; }
|
||||
|
||||
private CreateRoleDto Request;
|
||||
private List<string> Permissions;
|
||||
private FormHandler FormHandler;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
@@ -68,19 +65,31 @@
|
||||
{
|
||||
Permissions = []
|
||||
};
|
||||
|
||||
|
||||
Permissions = new();
|
||||
}
|
||||
|
||||
private async Task SubmitAsync()
|
||||
private async Task<bool> OnSubmitAsync(EditContext editContext, ValidationMessageStore validationMessageStore)
|
||||
{
|
||||
Request.Permissions = Permissions.ToArray();
|
||||
await FormHandler.SubmitAsync();
|
||||
}
|
||||
|
||||
var response = await HttpClient.PostAsJsonAsync(
|
||||
"api/admin/roles",
|
||||
Request,
|
||||
Constants.SerializerOptions
|
||||
);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
await ProblemDetailsHelper.HandleProblemDetailsAsync(response, Request, validationMessageStore);
|
||||
return false;
|
||||
}
|
||||
|
||||
private async Task OnSubmitAsync()
|
||||
{
|
||||
await OnSubmit.Invoke(Request);
|
||||
await ToastService.SuccessAsync("Role creation", $"Role {Request.Name} has been successfully created");
|
||||
|
||||
await OnSubmit.Invoke();
|
||||
await CloseAsync();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
@using Moonlight.Shared.Http.Requests.Users
|
||||
@using Moonlight.Frontend.Helpers
|
||||
@using Moonlight.Shared.Http.Requests.Admin.Users
|
||||
@using Moonlight.Shared.Http.Responses
|
||||
@using ShadcnBlazor.Dialogs
|
||||
@using ShadcnBlazor.Extras.Common
|
||||
@using ShadcnBlazor.Extras.FormHandlers
|
||||
@using ShadcnBlazor.Extras.Forms
|
||||
@using ShadcnBlazor.Extras.Toasts
|
||||
@using ShadcnBlazor.Fields
|
||||
@using ShadcnBlazor.Inputs
|
||||
@using ShadcnBlazor.Labels
|
||||
|
||||
@inherits ShadcnBlazor.Extras.Dialogs.DialogBase
|
||||
|
||||
@inject HttpClient HttpClient
|
||||
@inject ToastService ToastService
|
||||
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
Create new user
|
||||
@@ -16,50 +21,67 @@
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<FormHandler @ref="FormHandler" Model="Request" OnValidSubmit="SubmitAsync">
|
||||
<div class="flex flex-col gap-6">
|
||||
<EnhancedEditForm Model="Request" OnValidSubmit="OnSubmitAsync">
|
||||
|
||||
<FieldGroup>
|
||||
<FormValidationSummary/>
|
||||
<DataAnnotationsValidator />
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="username">Username</Label>
|
||||
<InputField
|
||||
@bind-Value="Request.Username"
|
||||
id="username"
|
||||
placeholder="Name of the user"/>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="emailAddress">Email Address</Label>
|
||||
<InputField
|
||||
@bind-Value="Request.Email"
|
||||
id="emailAddress"
|
||||
Type="email"
|
||||
placeholder="email@of.user"/>
|
||||
</div>
|
||||
</div>
|
||||
</FormHandler>
|
||||
|
||||
<DialogFooter ClassName="justify-end gap-x-1">
|
||||
<WButtom OnClick="_ => FormHandler.SubmitAsync()">Save changes</WButtom>
|
||||
</DialogFooter>
|
||||
<FieldSet>
|
||||
<Field>
|
||||
<FieldLabel for="username">Username</FieldLabel>
|
||||
<TextInputField
|
||||
@bind-Value="Request.Username"
|
||||
id="username"
|
||||
placeholder="Name of the user"/>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel for="emailAddress">Email Address</FieldLabel>
|
||||
<TextInputField
|
||||
@bind-Value="Request.Email"
|
||||
id="emailAddress"
|
||||
placeholder="email@of.user"/>
|
||||
</Field>
|
||||
</FieldSet>
|
||||
<Field Orientation="FieldOrientation.Horizontal" ClassName="justify-end">
|
||||
<SubmitButton>Save changes</SubmitButton>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</EnhancedEditForm>
|
||||
|
||||
@code
|
||||
{
|
||||
[Parameter] public Func<CreateUserDto, Task> OnSubmit { get; set; }
|
||||
[Parameter] public Func<Task> OnCompleted { get; set; }
|
||||
|
||||
private CreateUserDto Request;
|
||||
private FormHandler FormHandler;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
Request = new();
|
||||
}
|
||||
|
||||
private async Task SubmitAsync()
|
||||
private async Task<bool> OnSubmitAsync(EditContext editContext, ValidationMessageStore validationMessageStore)
|
||||
{
|
||||
await OnSubmit.Invoke(Request);
|
||||
var response = await HttpClient.PostAsJsonAsync(
|
||||
"/api/admin/users",
|
||||
Request,
|
||||
Constants.SerializerOptions
|
||||
);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
await ProblemDetailsHelper.HandleProblemDetailsAsync(response, Request, validationMessageStore);
|
||||
return false;
|
||||
}
|
||||
|
||||
await ToastService.SuccessAsync(
|
||||
"User creation",
|
||||
$"Successfully created user {Request.Username}"
|
||||
);
|
||||
|
||||
await OnCompleted.Invoke();
|
||||
await CloseAsync();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
@using LucideBlazor
|
||||
@using Moonlight.Shared.Http.Responses
|
||||
@using Moonlight.Shared.Http.Responses.Admin
|
||||
@using Moonlight.Shared.Http.Responses.Users
|
||||
@using Moonlight.Shared.Http.Responses.Admin.Users
|
||||
@using ShadcnBlazor.Buttons
|
||||
@using ShadcnBlazor.DataGrids
|
||||
@using ShadcnBlazor.Dialogs
|
||||
@@ -32,9 +32,9 @@
|
||||
SearchPlaceholder="Search user"
|
||||
ValueSelector="dto => dto.Username"
|
||||
Source="LoadUsersAsync"/>
|
||||
<WButtom OnClick="AddAsync" Variant="ButtonVariant.Outline" Size="ButtonSize.Icon">
|
||||
<WButton OnClick="AddAsync" Variant="ButtonVariant.Outline" Size="ButtonSize.Icon">
|
||||
<PlusIcon/>
|
||||
</WButtom>
|
||||
</WButton>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
@@ -50,9 +50,9 @@
|
||||
<CellTemplate>
|
||||
<TableCell>
|
||||
<div class="flex justify-end me-1.5">
|
||||
<WButtom OnClick="_ => RemoveAsync(context)" Variant="ButtonVariant.Destructive" Size="ButtonSize.Icon">
|
||||
<WButton OnClick="_ => RemoveAsync(context)" Variant="ButtonVariant.Destructive" Size="ButtonSize.Icon">
|
||||
<TrashIcon/>
|
||||
</WButtom>
|
||||
</WButton>
|
||||
</div>
|
||||
</TableCell>
|
||||
</CellTemplate>
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
@using Moonlight.Frontend.Mappers
|
||||
@using Moonlight.Frontend.Helpers
|
||||
@using Moonlight.Frontend.Mappers
|
||||
@using Moonlight.Frontend.UI.Admin.Components
|
||||
@using Moonlight.Shared.Http.Requests.ApiKeys
|
||||
@using Moonlight.Shared.Http.Responses.ApiKeys
|
||||
@using Moonlight.Shared.Http.Requests.Admin.ApiKeys
|
||||
@using Moonlight.Shared.Http.Responses.Admin.ApiKeys
|
||||
@using ShadcnBlazor.Dialogs
|
||||
@using ShadcnBlazor.Extras.Common
|
||||
@using ShadcnBlazor.Extras.FormHandlers
|
||||
@using ShadcnBlazor.Extras.Forms
|
||||
@using ShadcnBlazor.Extras.Toasts
|
||||
@using ShadcnBlazor.Fields
|
||||
@using ShadcnBlazor.Inputs
|
||||
@using ShadcnBlazor.Labels
|
||||
|
||||
@inherits ShadcnBlazor.Extras.Dialogs.DialogBase
|
||||
|
||||
@inject HttpClient HttpClient
|
||||
@inject ToastService ToastService
|
||||
|
||||
<DialogHeader>
|
||||
<DialogTitle>Update API key</DialogTitle>
|
||||
<DialogDescription>
|
||||
@@ -17,45 +21,39 @@
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<FormHandler @ref="FormHandler" Model="Request" OnValidSubmit="SubmitAsync">
|
||||
<div class="flex flex-col gap-6">
|
||||
<FormValidationSummary />
|
||||
<EnhancedEditForm Model="Request" OnValidSubmit="OnSubmitAsync">
|
||||
<FieldGroup>
|
||||
<FormValidationSummary/>
|
||||
<DataAnnotationsValidator/>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="keyName">Name</Label>
|
||||
<InputField @bind-Value="Request.Name" id="keyName" placeholder="My API key" />
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="keyDescription">Description</Label>
|
||||
<textarea
|
||||
@bind="Request.Description"
|
||||
id="keyDescription"
|
||||
maxlength="100"
|
||||
class="border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm"
|
||||
placeholder="What this key is for">
|
||||
|
||||
</textarea>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label>Permissions</Label>
|
||||
<PermissionSelector Permissions="Permissions" />
|
||||
</div>
|
||||
</div>
|
||||
</FormHandler>
|
||||
|
||||
<DialogFooter ClassName="justify-end gap-x-1">
|
||||
<WButtom OnClick="_ => FormHandler.SubmitAsync()">Save changes</WButtom>
|
||||
</DialogFooter>
|
||||
<FieldSet>
|
||||
<Field>
|
||||
<FieldLabel for="keyName">Name</FieldLabel>
|
||||
<TextInputField @bind-Value="Request.Name" id="keyName" placeholder="My API key"/>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel for="keyDescription">Description</FieldLabel>
|
||||
<TextareaInputField @bind-Value="Request.Description" id="keyDescription" placeholder="What this key is for"/>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel>Permissions</FieldLabel>
|
||||
<FieldContent>
|
||||
<PermissionSelector Permissions="Permissions"/>
|
||||
</FieldContent>
|
||||
</Field>
|
||||
</FieldSet>
|
||||
<Field Orientation="FieldOrientation.Horizontal" ClassName="justify-end">
|
||||
<SubmitButton>Save changes</SubmitButton>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</EnhancedEditForm>
|
||||
|
||||
@code
|
||||
{
|
||||
[Parameter] public Func<UpdateApiKeyDto, Task> OnSubmit { get; set; }
|
||||
[Parameter] public Func<Task> OnSubmit { get; set; }
|
||||
[Parameter] public ApiKeyDto Key { get; set; }
|
||||
|
||||
private UpdateApiKeyDto Request;
|
||||
private FormHandler FormHandler;
|
||||
private List<string> Permissions = new();
|
||||
|
||||
protected override void OnInitialized()
|
||||
@@ -64,10 +62,30 @@
|
||||
Permissions = Key.Permissions.ToList();
|
||||
}
|
||||
|
||||
private async Task SubmitAsync()
|
||||
private async Task<bool> OnSubmitAsync(EditContext editContext, ValidationMessageStore validationMessageStore)
|
||||
{
|
||||
Request.Permissions = Permissions.ToArray();
|
||||
await OnSubmit.Invoke(Request);
|
||||
|
||||
var response = await HttpClient.PatchAsJsonAsync(
|
||||
$"/api/admin/apiKeys/{Key.Id}",
|
||||
Request,
|
||||
Constants.SerializerOptions
|
||||
);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
await ProblemDetailsHelper.HandleProblemDetailsAsync(response, Request, validationMessageStore);
|
||||
return false;
|
||||
}
|
||||
|
||||
await ToastService.SuccessAsync(
|
||||
"API Key update",
|
||||
$"Successfully updated API key {Request.Name}"
|
||||
);
|
||||
|
||||
await OnSubmit.Invoke();
|
||||
await CloseAsync();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,37 +1,38 @@
|
||||
@inherits ShadcnBlazor.Extras.Dialogs.DialogBase
|
||||
|
||||
@using System.Text.Json
|
||||
@using LucideBlazor
|
||||
@using Moonlight.Shared.Http
|
||||
@using Moonlight.Shared.Http.Events
|
||||
@using Moonlight.Shared.Http.Requests.Admin.ContainerHelper
|
||||
@using ShadcnBlazor.Buttons
|
||||
@using ShadcnBlazor.Dialogs
|
||||
@using ShadcnBlazor.Extras.AlertDialogs
|
||||
@using ShadcnBlazor.Progresses
|
||||
@using ShadcnBlazor.Spinners
|
||||
|
||||
@inject AlertDialogService AlertService
|
||||
@inject HttpClient HttpClient
|
||||
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
Updating...
|
||||
Updating instance to @Version...
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div class="text-base flex flex-col p-2 gap-y-0.5">
|
||||
@for (var i = 0; i < Steps.Length; i++)
|
||||
{
|
||||
if (CurrentStep == i)
|
||||
<div class="grid grid-cols-1 xl:grid-cols-2 w-full gap-5">
|
||||
<div class="text-base flex flex-col p-2 gap-y-1">
|
||||
@for (var i = 0; i < Steps.Length; i++)
|
||||
{
|
||||
<div class="flex flex-row items-center gap-x-2">
|
||||
<Spinner ClassName="size-4" />
|
||||
<span>
|
||||
@Steps[i]
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
if (i < CurrentStep)
|
||||
if (CurrentStep == i)
|
||||
{
|
||||
<div class="flex flex-row items-center gap-x-2">
|
||||
<CheckIcon ClassName="text-green-500 size-4" />
|
||||
<div class="flex flex-row items-center gap-x-1">
|
||||
@if (IsFailed)
|
||||
{
|
||||
<CircleXIcon ClassName="text-red-500 size-5"/>
|
||||
}
|
||||
else
|
||||
{
|
||||
<Spinner ClassName="size-5"/>
|
||||
}
|
||||
<span>
|
||||
@Steps[i]
|
||||
</span>
|
||||
@@ -39,81 +40,205 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="text-muted-foreground flex flex-row items-center gap-x-2">
|
||||
<span class="size-4"></span>
|
||||
@Steps[i]
|
||||
</div>
|
||||
if (i < CurrentStep)
|
||||
{
|
||||
<div class="flex flex-row items-center gap-x-1 text-muted-foreground">
|
||||
<CircleCheckIcon ClassName="text-green-500 size-5"/>
|
||||
<span>
|
||||
@Steps[i]
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="text-muted-foreground flex flex-row items-center gap-x-1">
|
||||
<span class="size-5"></span>
|
||||
@Steps[i]
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</div>
|
||||
<div class="bg-black text-white rounded-lg font-mono h-96 flex flex-col-reverse overflow-auto p-3 scrollbar-thin">
|
||||
@for (var i = LogLines.Count - 1; i >= 0; i--)
|
||||
{
|
||||
<div>
|
||||
@LogLines[i]
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Progress Value="@Progress"></Progress>
|
||||
</DialogFooter>
|
||||
@if (CurrentStep == Steps.Length || IsFailed)
|
||||
{
|
||||
<DialogFooter ClassName="justify-end">
|
||||
<Button Variant="ButtonVariant.Outline" @onclick="CloseAsync">Close</Button>
|
||||
</DialogFooter>
|
||||
}
|
||||
else
|
||||
{
|
||||
<DialogFooter>
|
||||
<Progress ClassName="my-1" Value="@Progress"></Progress>
|
||||
</DialogFooter>
|
||||
}
|
||||
|
||||
@code
|
||||
{
|
||||
private int Progress = 0;
|
||||
[Parameter] public string Version { get; set; }
|
||||
[Parameter] public bool NoBuildCache { get; set; }
|
||||
|
||||
private bool IsFailed;
|
||||
private int Progress;
|
||||
private int CurrentStep;
|
||||
|
||||
private string[] Steps =
|
||||
private readonly string[] Steps =
|
||||
[
|
||||
"Preparing",
|
||||
"Updating configuration files",
|
||||
"Building docker image",
|
||||
"Redeploying container instance",
|
||||
"Waiting for container instance to start up",
|
||||
"Update complete"
|
||||
"Checking", // 0
|
||||
"Updating configuration files", // 1
|
||||
"Starting rebuild task", // 2
|
||||
"Building docker image", // 3
|
||||
"Redeploying container instance", // 4
|
||||
"Waiting for container instance to start up", // 5
|
||||
"Update complete" // 6
|
||||
];
|
||||
|
||||
private readonly List<string?> LogLines = new();
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (!firstRender)
|
||||
return;
|
||||
|
||||
// Checking
|
||||
CurrentStep = 0;
|
||||
Progress = 0;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
|
||||
await Task.Delay(2000);
|
||||
|
||||
// Update configuration
|
||||
CurrentStep = 1;
|
||||
Progress = 20;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
|
||||
await Task.Delay(6000);
|
||||
await HttpClient.PostAsJsonAsync("api/admin/ch/version", new SetVersionDto()
|
||||
{
|
||||
Version = Version
|
||||
}, SerializationContext.TunedOptions);
|
||||
|
||||
// Starting rebuild task
|
||||
CurrentStep = 2;
|
||||
Progress = 40;
|
||||
Progress = 30;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
|
||||
await Task.Delay(2000);
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, "api/admin/ch/rebuild");
|
||||
|
||||
CurrentStep = 3;
|
||||
Progress = 60;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
|
||||
await Task.Delay(4000);
|
||||
|
||||
CurrentStep = 4;
|
||||
Progress = 80;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
|
||||
await Task.Delay(4000);
|
||||
|
||||
CurrentStep = 5;
|
||||
Progress = 100;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
|
||||
await Task.Delay(1000);
|
||||
|
||||
await AlertService.SuccessAsync(
|
||||
"Update completed",
|
||||
"Update successfully completed. Please refresh the page to load new frontend changes"
|
||||
request.Content = JsonContent.Create(
|
||||
new RequestRebuildDto(NoBuildCache),
|
||||
null,
|
||||
SerializationContext.TunedOptions
|
||||
);
|
||||
|
||||
await CloseAsync();
|
||||
var response = await HttpClient.SendAsync(
|
||||
request,
|
||||
HttpCompletionOption.ResponseHeadersRead
|
||||
);
|
||||
|
||||
await using var responseStream = await response.Content.ReadAsStreamAsync();
|
||||
using var streamReader = new StreamReader(responseStream);
|
||||
|
||||
do
|
||||
{
|
||||
try
|
||||
{
|
||||
var line = await streamReader.ReadLineAsync();
|
||||
|
||||
if (line == null)
|
||||
break;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(line))
|
||||
continue;
|
||||
|
||||
var data = line.Trim("data: ");
|
||||
var deserializedData = JsonSerializer.Deserialize<RebuildEventDto>(data, Constants.SerializerOptions);
|
||||
|
||||
switch (deserializedData.Type)
|
||||
{
|
||||
case RebuildEventType.Log:
|
||||
LogLines.Add(deserializedData.Data);
|
||||
break;
|
||||
|
||||
case RebuildEventType.Step:
|
||||
|
||||
switch (deserializedData.Data)
|
||||
{
|
||||
case "BuildImage":
|
||||
|
||||
// Building docker image
|
||||
|
||||
CurrentStep = 3;
|
||||
Progress = 40;
|
||||
|
||||
await InvokeAsync(StateHasChanged);
|
||||
break;
|
||||
|
||||
case "ServiceDown":
|
||||
|
||||
// Redeploying container instance
|
||||
|
||||
CurrentStep = 4;
|
||||
Progress = 60;
|
||||
|
||||
await InvokeAsync(StateHasChanged);
|
||||
break;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case RebuildEventType.Failed:
|
||||
|
||||
IsFailed = true;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
break;
|
||||
}
|
||||
} while (true);
|
||||
|
||||
// Waiting for container instance to start up
|
||||
|
||||
CurrentStep = 5;
|
||||
Progress = 90;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
|
||||
// Wait some time for instance to shut down
|
||||
await Task.Delay(TimeSpan.FromSeconds(5));
|
||||
|
||||
// Ping instance until its reachable again
|
||||
while (true)
|
||||
{
|
||||
try
|
||||
{
|
||||
await HttpClient.GetStringAsync("api/ping");
|
||||
break;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Ignored
|
||||
}
|
||||
|
||||
await Task.Delay(3000);
|
||||
}
|
||||
|
||||
// Update complete
|
||||
CurrentStep = 7;
|
||||
Progress = 100;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
@using Moonlight.Frontend.Helpers
|
||||
@using Moonlight.Frontend.Mappers
|
||||
@using Moonlight.Frontend.UI.Admin.Components
|
||||
@using Moonlight.Shared.Http.Requests.Roles
|
||||
@using Moonlight.Shared.Http.Requests.Admin.Roles
|
||||
@using Moonlight.Shared.Http.Responses.Admin
|
||||
@using ShadcnBlazor.Buttons
|
||||
@using ShadcnBlazor.Dialogs
|
||||
@using ShadcnBlazor.Extras.Common
|
||||
@using ShadcnBlazor.Extras.FormHandlers
|
||||
@using ShadcnBlazor.Extras.Forms
|
||||
@using ShadcnBlazor.Extras.Toasts
|
||||
@using ShadcnBlazor.Fields
|
||||
@using ShadcnBlazor.Inputs
|
||||
@using ShadcnBlazor.Labels
|
||||
|
||||
@inherits ShadcnBlazor.Extras.Dialogs.DialogBase
|
||||
|
||||
@inject HttpClient HttpClient
|
||||
@inject ToastService ToastService
|
||||
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
Update @Role.Name
|
||||
@@ -20,50 +23,44 @@
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<FormHandler @ref="FormHandler" Model="Request" OnValidSubmit="OnSubmitAsync">
|
||||
<div class="flex flex-col gap-6">
|
||||
|
||||
<EnhancedEditForm Model="Request" OnValidSubmit="OnSubmitAsync">
|
||||
<FieldGroup>
|
||||
<FormValidationSummary/>
|
||||
<DataAnnotationsValidator/>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="roleName">Name</Label>
|
||||
<InputField
|
||||
@bind-Value="Request.Name"
|
||||
id="roleName"
|
||||
placeholder="My fancy role"/>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="roleDescription">Description</Label>
|
||||
<textarea
|
||||
@bind="Request.Description"
|
||||
id="roleDescription"
|
||||
maxlength="100"
|
||||
class="border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm"
|
||||
placeholder="Describe what the role should be used for">
|
||||
|
||||
</textarea>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label>Permissions</Label>
|
||||
<PermissionSelector Permissions="Permissions" />
|
||||
</div>
|
||||
</div>
|
||||
</FormHandler>
|
||||
|
||||
<DialogFooter ClassName="justify-end gap-x-1">
|
||||
<WButtom OnClick="SubmitAsync">Save changes</WButtom>
|
||||
</DialogFooter>
|
||||
<FieldSet>
|
||||
<Field>
|
||||
<FieldLabel for="roleName">Name</FieldLabel>
|
||||
<TextInputField
|
||||
@bind-Value="Request.Name"
|
||||
id="roleName"
|
||||
placeholder="My fancy role"/>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel for="keyDescription">Description</FieldLabel>
|
||||
<TextareaInputField @bind-Value="Request.Description" id="keyDescription"
|
||||
placeholder="Describe what the role should be used for"/>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel>Permissions</FieldLabel>
|
||||
<FieldContent>
|
||||
<PermissionSelector Permissions="Permissions"/>
|
||||
</FieldContent>
|
||||
</Field>
|
||||
</FieldSet>
|
||||
<Field Orientation="FieldOrientation.Horizontal" ClassName="justify-end">
|
||||
<SubmitButton>Save changes</SubmitButton>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</EnhancedEditForm>
|
||||
|
||||
@code
|
||||
{
|
||||
[Parameter] public Func<UpdateRoleDto, Task> OnSubmit { get; set; }
|
||||
[Parameter] public Func<Task> OnSubmit { get; set; }
|
||||
[Parameter] public RoleDto Role { get; set; }
|
||||
|
||||
|
||||
private UpdateRoleDto Request;
|
||||
private List<string> Permissions;
|
||||
private FormHandler FormHandler;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
@@ -71,15 +68,27 @@
|
||||
Permissions = Role.Permissions.ToList();
|
||||
}
|
||||
|
||||
private async Task SubmitAsync()
|
||||
private async Task<bool> OnSubmitAsync(EditContext editContext, ValidationMessageStore validationMessageStore)
|
||||
{
|
||||
Request.Permissions = Permissions.ToArray();
|
||||
await FormHandler.SubmitAsync();
|
||||
}
|
||||
|
||||
var response = await HttpClient.PatchAsJsonAsync(
|
||||
$"api/admin/roles/{Role.Id}",
|
||||
Request,
|
||||
Constants.SerializerOptions
|
||||
);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
await ProblemDetailsHelper.HandleProblemDetailsAsync(response, Request, validationMessageStore);
|
||||
return false;
|
||||
}
|
||||
|
||||
private async Task OnSubmitAsync()
|
||||
{
|
||||
await OnSubmit.Invoke(Request);
|
||||
await ToastService.SuccessAsync("Role update", $"Role {Request.Name} has been successfully updated");
|
||||
|
||||
await OnSubmit.Invoke();
|
||||
await CloseAsync();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
@using Moonlight.Frontend.Mappers
|
||||
@using Moonlight.Shared.Http.Requests.Users
|
||||
@using Moonlight.Shared.Http.Responses.Users
|
||||
@using Moonlight.Frontend.Helpers
|
||||
@using Moonlight.Frontend.Mappers
|
||||
@using Moonlight.Shared.Http.Requests.Admin.Users
|
||||
@using Moonlight.Shared.Http.Responses
|
||||
@using Moonlight.Shared.Http.Responses.Admin.Users
|
||||
@using ShadcnBlazor.Dialogs
|
||||
@using ShadcnBlazor.Extras.Common
|
||||
@using ShadcnBlazor.Extras.FormHandlers
|
||||
@using ShadcnBlazor.Extras.Forms
|
||||
@using ShadcnBlazor.Extras.Toasts
|
||||
@using ShadcnBlazor.Fields
|
||||
@using ShadcnBlazor.Inputs
|
||||
@using ShadcnBlazor.Labels
|
||||
|
||||
@inherits ShadcnBlazor.Extras.Dialogs.DialogBase
|
||||
|
||||
@inject HttpClient HttpClient
|
||||
@inject ToastService ToastService
|
||||
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
Update @User.Username
|
||||
@@ -18,51 +23,66 @@
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<FormHandler @ref="FormHandler" Model="Request" OnValidSubmit="SubmitAsync">
|
||||
<div class="flex flex-col gap-6">
|
||||
|
||||
<EnhancedEditForm Model="Request" OnValidSubmit="OnSubmitAsync">
|
||||
<FieldGroup>
|
||||
<FormValidationSummary/>
|
||||
<DataAnnotationsValidator/>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="username">Username</Label>
|
||||
<InputField
|
||||
@bind-Value="Request.Username"
|
||||
id="username"
|
||||
placeholder="Name of the user"/>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="emailAddress">Email Address</Label>
|
||||
<InputField
|
||||
@bind-Value="Request.Email"
|
||||
id="emailAddress"
|
||||
Type="email"
|
||||
placeholder="email@of.user"/>
|
||||
</div>
|
||||
</div>
|
||||
</FormHandler>
|
||||
|
||||
<DialogFooter ClassName="justify-end gap-x-1">
|
||||
<WButtom OnClick="_ => FormHandler.SubmitAsync()">Save changes</WButtom>
|
||||
</DialogFooter>
|
||||
<FieldSet>
|
||||
<Field>
|
||||
<FieldLabel for="username">Username</FieldLabel>
|
||||
<TextInputField
|
||||
@bind-Value="Request.Username"
|
||||
id="username"
|
||||
placeholder="Name of the user"/>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel for="emailAddress">Email Address</FieldLabel>
|
||||
<TextInputField
|
||||
@bind-Value="Request.Email"
|
||||
id="emailAddress"
|
||||
placeholder="email@of.user"/>
|
||||
</Field>
|
||||
</FieldSet>
|
||||
<Field Orientation="FieldOrientation.Horizontal" ClassName="justify-end">
|
||||
<SubmitButton>Save changes</SubmitButton>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</EnhancedEditForm>
|
||||
|
||||
@code
|
||||
{
|
||||
[Parameter] public Func<UpdateUserDto, Task> OnSubmit { get; set; }
|
||||
[Parameter] public Func<Task> OnCompleted { get; set; }
|
||||
[Parameter] public UserDto User { get; set; }
|
||||
|
||||
|
||||
private UpdateUserDto Request;
|
||||
private FormHandler FormHandler;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
Request = UserMapper.ToUpdate(User);
|
||||
}
|
||||
|
||||
private async Task SubmitAsync()
|
||||
private async Task<bool> OnSubmitAsync(EditContext editContext, ValidationMessageStore validationMessageStore)
|
||||
{
|
||||
await OnSubmit.Invoke(Request);
|
||||
var response = await HttpClient.PatchAsJsonAsync(
|
||||
$"/api/admin/users/{User.Id}",
|
||||
Request
|
||||
);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
await ProblemDetailsHelper.HandleProblemDetailsAsync(response, Request, validationMessageStore);
|
||||
return false;
|
||||
}
|
||||
|
||||
await ToastService.SuccessAsync(
|
||||
"User update",
|
||||
$"Successfully updated user {Request.Username}"
|
||||
);
|
||||
|
||||
await OnCompleted.Invoke();
|
||||
await CloseAsync();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,7 +133,11 @@
|
||||
{
|
||||
<CardTitle ClassName="text-lg text-primary">Update available</CardTitle>
|
||||
<CardAction ClassName="self-center">
|
||||
<Button @onclick="LaunchUpdateModalAsync">Update</Button>
|
||||
<Button>
|
||||
<Slot>
|
||||
<a href="/admin/system?tab=instance" @attributes="context">Update</a>
|
||||
</Slot>
|
||||
</Button>
|
||||
</CardAction>
|
||||
}
|
||||
</CardHeader>
|
||||
@@ -156,9 +160,4 @@
|
||||
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private async Task LaunchUpdateModalAsync() => await DialogService.LaunchAsync<UpdateInstanceModal>(onConfigure: model =>
|
||||
{
|
||||
model.ShowCloseButton = false;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
@using Moonlight.Shared.Http.Requests.ApiKeys
|
||||
@using Moonlight.Shared.Http.Responses.ApiKeys
|
||||
@using LucideBlazor
|
||||
@using LucideBlazor
|
||||
@using Microsoft.AspNetCore.Authorization
|
||||
@using Microsoft.AspNetCore.Components.Authorization
|
||||
@using Moonlight.Frontend.UI.Admin.Modals
|
||||
@using Moonlight.Shared
|
||||
@using Moonlight.Shared.Http.Requests
|
||||
@using Moonlight.Shared.Http.Responses
|
||||
@using Moonlight.Shared.Http.Responses.Admin.ApiKeys
|
||||
@using ShadcnBlazor.DataGrids
|
||||
@using ShadcnBlazor.Dropdowns
|
||||
@using ShadcnBlazor.Extras.AlertDialogs
|
||||
@@ -123,19 +122,8 @@
|
||||
{
|
||||
await DialogService.LaunchAsync<CreateApiKeyDialog>(parameters =>
|
||||
{
|
||||
parameters[nameof(CreateApiKeyDialog.OnSubmit)] = async (CreateApiKeyDto dto) =>
|
||||
parameters[nameof(CreateApiKeyDialog.OnSubmit)] = async () =>
|
||||
{
|
||||
await HttpClient.PostAsJsonAsync(
|
||||
"/api/admin/apiKeys",
|
||||
dto,
|
||||
Constants.SerializerOptions
|
||||
);
|
||||
|
||||
await ToastService.SuccessAsync(
|
||||
"API Key creation",
|
||||
$"Successfully created API key {dto.Name}"
|
||||
);
|
||||
|
||||
await Grid.RefreshAsync();
|
||||
};
|
||||
});
|
||||
@@ -146,19 +134,8 @@
|
||||
await DialogService.LaunchAsync<UpdateApiKeyDialog>(parameters =>
|
||||
{
|
||||
parameters[nameof(UpdateApiKeyDialog.Key)] = key;
|
||||
parameters[nameof(UpdateApiKeyDialog.OnSubmit)] = async (UpdateApiKeyDto dto) =>
|
||||
parameters[nameof(UpdateApiKeyDialog.OnSubmit)] = async () =>
|
||||
{
|
||||
await HttpClient.PatchAsJsonAsync(
|
||||
$"/api/admin/apiKeys/{key.Id}",
|
||||
dto,
|
||||
Constants.SerializerOptions
|
||||
);
|
||||
|
||||
await ToastService.SuccessAsync(
|
||||
"API Key update",
|
||||
$"Successfully updated API key {dto.Name}"
|
||||
);
|
||||
|
||||
await Grid.RefreshAsync();
|
||||
};
|
||||
});
|
||||
|
||||
@@ -44,10 +44,10 @@
|
||||
</Alert>
|
||||
</CardContent>
|
||||
<CardFooter ClassName="justify-end">
|
||||
<WButtom OnClick="DiagnoseAsync" disabled="@(!AccessResult.Succeeded)">
|
||||
<WButton OnClick="DiagnoseAsync" disabled="@(!AccessResult.Succeeded)">
|
||||
<StethoscopeIcon/>
|
||||
Start diagnostics
|
||||
</WButtom>
|
||||
</WButton>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -31,17 +31,13 @@
|
||||
<HeartPulseIcon/>
|
||||
Diagnose
|
||||
</TabsTrigger>
|
||||
<TabsTrigger Value="instance" Disabled="@(!InstanceResult.Succeeded || !VersionsResult.Succeeded)">
|
||||
<ContainerIcon/>
|
||||
Instance
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent Value="settings">
|
||||
<Card ClassName="mt-5">
|
||||
<CardContent>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-5">
|
||||
<div class="col-span-1 grid gap-3">
|
||||
<Label for="instance-name">Instance Name</Label>
|
||||
<InputField id="instance-name" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter ClassName="justify-end">
|
||||
<Button>
|
||||
<SaveIcon />
|
||||
@@ -65,6 +61,12 @@
|
||||
<Moonlight.Frontend.UI.Admin.Views.Sys.Themes.Index />
|
||||
</TabsContent>
|
||||
}
|
||||
@if (InstanceResult.Succeeded && VersionsResult.Succeeded)
|
||||
{
|
||||
<TabsContent Value="instance">
|
||||
<Instance />
|
||||
</TabsContent>
|
||||
}
|
||||
</Tabs>
|
||||
|
||||
@code
|
||||
@@ -77,6 +79,8 @@
|
||||
|
||||
private AuthorizationResult ApiKeyAccess;
|
||||
private AuthorizationResult ThemesAccess;
|
||||
private AuthorizationResult InstanceResult;
|
||||
private AuthorizationResult VersionsResult;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
@@ -84,6 +88,8 @@
|
||||
|
||||
ApiKeyAccess = await AuthorizationService.AuthorizeAsync(authState.User, Permissions.ApiKeys.View);
|
||||
ThemesAccess = await AuthorizationService.AuthorizeAsync(authState.User, Permissions.Themes.View);
|
||||
InstanceResult = await AuthorizationService.AuthorizeAsync(authState.User, Permissions.System.Versions);
|
||||
VersionsResult = await AuthorizationService.AuthorizeAsync(authState.User, Permissions.System.Instance);
|
||||
}
|
||||
|
||||
private void OnTabChanged(string name)
|
||||
|
||||
221
Moonlight.Frontend/UI/Admin/Views/Sys/Instance.razor
Normal file
221
Moonlight.Frontend/UI/Admin/Views/Sys/Instance.razor
Normal file
@@ -0,0 +1,221 @@
|
||||
@using LucideBlazor
|
||||
@using Moonlight.Frontend.UI.Admin.Modals
|
||||
@using Moonlight.Shared.Http.Responses.Admin
|
||||
@using ShadcnBlazor.Cards
|
||||
@using ShadcnBlazor.Emptys
|
||||
@using ShadcnBlazor.Buttons
|
||||
@using ShadcnBlazor.Extras.AlertDialogs
|
||||
@using ShadcnBlazor.Extras.Common
|
||||
@using ShadcnBlazor.Extras.Dialogs
|
||||
@using ShadcnBlazor.Fields
|
||||
@using ShadcnBlazor.Selects
|
||||
@using ShadcnBlazor.Switches
|
||||
|
||||
@inject HttpClient HttpClient
|
||||
@inject DialogService DialogService
|
||||
@inject AlertDialogService AlertDialogService
|
||||
|
||||
<div class="mt-5">
|
||||
<LazyLoader Load="LoadAsync">
|
||||
@if (StatusDto.IsEnabled)
|
||||
{
|
||||
if (StatusDto.IsReachable)
|
||||
{
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-5">
|
||||
<Card ClassName="col-span-1">
|
||||
<CardHeader>
|
||||
<CardTitle>Version</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<FieldGroup>
|
||||
<FieldSet>
|
||||
<Field>
|
||||
<FieldLabel>Version / Branch</FieldLabel>
|
||||
<FieldContent>
|
||||
<Select DefaultValue="@SelectedVersion" @bind-Value="SelectedVersion">
|
||||
<SelectTrigger ClassName="w-64">
|
||||
<SelectValue/>
|
||||
</SelectTrigger>
|
||||
<SelectContent ClassName="w-64">
|
||||
@foreach (var version in Versions)
|
||||
{
|
||||
var displayName = version.Identifier;
|
||||
|
||||
if (version.IsDevelopment)
|
||||
displayName += " (dev)";
|
||||
|
||||
if (version.IsPreRelease)
|
||||
displayName += " (beta)";
|
||||
|
||||
<SelectItem Value="@version.Identifier">
|
||||
@displayName
|
||||
</SelectItem>
|
||||
}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FieldContent>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel>Bypass Build Cache</FieldLabel>
|
||||
<FieldContent>
|
||||
<Switch @bind-Value="NoBuildCache"/>
|
||||
</FieldContent>
|
||||
</Field>
|
||||
</FieldSet>
|
||||
<Field Orientation="FieldOrientation.Horizontal">
|
||||
<Button @onclick="AskApplyAsync">Apply</Button>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card ClassName="col-span-1">
|
||||
<CardHeader>
|
||||
<CardTitle>Plugins</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Empty>
|
||||
<EmptyHeader>
|
||||
<EmptyMedia Variant="EmptyMediaVariant.Icon">
|
||||
<SearchIcon/>
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>No Plugins found</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
No plugins found in instance configuration
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
</Empty>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<Empty>
|
||||
<EmptyHeader>
|
||||
<EmptyMedia Variant="EmptyMediaVariant.Icon">
|
||||
<CircleAlertIcon ClassName="text-red-500"/>
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>Container Helper unreachable</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
The container helper is unreachable. No management actions are available
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
</Empty>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<Empty>
|
||||
<EmptyHeader>
|
||||
<EmptyMedia Variant="EmptyMediaVariant.Icon">
|
||||
<ToggleLeftIcon/>
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>Container Helper is disabled</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
The container helper is disabled on this instance.
|
||||
This might be due to running a multiple container moonlight setup
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
</Empty>
|
||||
}
|
||||
</LazyLoader>
|
||||
</div>
|
||||
|
||||
@code
|
||||
{
|
||||
private ContainerHelperStatusDto StatusDto;
|
||||
private string SelectedVersion = "v2.1";
|
||||
private bool NoBuildCache;
|
||||
|
||||
private VersionDto[] Versions;
|
||||
|
||||
private async Task LoadAsync(LazyLoader _)
|
||||
{
|
||||
StatusDto = (await HttpClient.GetFromJsonAsync<ContainerHelperStatusDto>("api/admin/ch/status"))!;
|
||||
|
||||
var currentVersion = await HttpClient.GetFromJsonAsync<VersionDto>("api/admin/versions/instance");
|
||||
|
||||
if (currentVersion != null)
|
||||
SelectedVersion = currentVersion.Identifier;
|
||||
|
||||
Versions = (await HttpClient.GetFromJsonAsync<VersionDto[]>("api/admin/versions"))!;
|
||||
}
|
||||
|
||||
private async Task ApplyAsync()
|
||||
{
|
||||
await DialogService.LaunchAsync<UpdateInstanceModal>(
|
||||
parameters =>
|
||||
{
|
||||
parameters[nameof(UpdateInstanceModal.Version)] = SelectedVersion;
|
||||
parameters[nameof(UpdateInstanceModal.NoBuildCache)] = NoBuildCache;
|
||||
},
|
||||
onConfigure: model =>
|
||||
{
|
||||
model.ShowCloseButton = false;
|
||||
model.ClassName = "sm:max-w-4xl!";
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private async Task AskApplyAsync()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(SelectedVersion))
|
||||
return;
|
||||
|
||||
var version = Versions.First(x => x.Identifier == SelectedVersion);
|
||||
|
||||
var shouldContinue = await ConfirmRiskyVersionAsync(
|
||||
"Moonlight Rebuild",
|
||||
"If you continue the moonlight instance will become unavailable during the rebuild process. This will impact users on this instance"
|
||||
);
|
||||
|
||||
if (!shouldContinue)
|
||||
return;
|
||||
|
||||
if (version.IsDevelopment)
|
||||
{
|
||||
shouldContinue = await ConfirmRiskyVersionAsync(
|
||||
"Development Version",
|
||||
"You are about to install development a version. This can break your instance. Continue at your own risk"
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (version.IsPreRelease)
|
||||
{
|
||||
shouldContinue = await ConfirmRiskyVersionAsync(
|
||||
"Beta / Pre-Release Version",
|
||||
"You are about to install a version marked as pre-release / beta. This can break your instance. Continue at your own risk"
|
||||
);
|
||||
}
|
||||
else
|
||||
shouldContinue = true;
|
||||
}
|
||||
|
||||
if (!shouldContinue)
|
||||
return;
|
||||
|
||||
await ApplyAsync();
|
||||
}
|
||||
|
||||
private async Task<bool> ConfirmRiskyVersionAsync(string title, string message)
|
||||
{
|
||||
var tcs = new TaskCompletionSource();
|
||||
var confirmed = false;
|
||||
|
||||
await AlertDialogService.ConfirmDangerAsync(
|
||||
title,
|
||||
message,
|
||||
() =>
|
||||
{
|
||||
confirmed = true;
|
||||
tcs.SetResult();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
);
|
||||
|
||||
await tcs.Task;
|
||||
|
||||
return confirmed;
|
||||
}
|
||||
}
|
||||
@@ -3,14 +3,15 @@
|
||||
@using Microsoft.AspNetCore.Authorization
|
||||
@using Moonlight.Shared
|
||||
@using LucideBlazor
|
||||
@using Moonlight.Frontend.Helpers
|
||||
@using Moonlight.Frontend.Services
|
||||
@using Moonlight.Shared.Http.Requests.Themes
|
||||
@using Moonlight.Shared.Http.Requests.Admin.Themes
|
||||
@using ShadcnBlazor.Buttons
|
||||
@using ShadcnBlazor.Labels
|
||||
@using ShadcnBlazor.Cards
|
||||
@using ShadcnBlazor.Extras.Editors
|
||||
@using ShadcnBlazor.Extras.FormHandlers
|
||||
@using ShadcnBlazor.Extras.Forms
|
||||
@using ShadcnBlazor.Extras.Toasts
|
||||
@using ShadcnBlazor.Fields
|
||||
@using ShadcnBlazor.Inputs
|
||||
@using ShadcnBlazor.Switches
|
||||
|
||||
@@ -21,86 +22,89 @@
|
||||
@inject ToastService ToastService
|
||||
@inject FrontendService FrontendService
|
||||
|
||||
<div class="flex flex-row justify-between">
|
||||
<div class="flex flex-col">
|
||||
<h1 class="text-xl font-semibold">Create theme</h1>
|
||||
<div class="text-muted-foreground">
|
||||
Create a new theme
|
||||
<EnhancedEditForm Model="Request" OnValidSubmit="OnSubmitAsync" Context="editFormContext">
|
||||
<div class="flex flex-row justify-between">
|
||||
<div class="flex flex-col">
|
||||
<h1 class="text-xl font-semibold">Create theme</h1>
|
||||
<div class="text-muted-foreground">
|
||||
Create a new theme
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row gap-x-1.5">
|
||||
<Button Variant="ButtonVariant.Secondary">
|
||||
<Slot>
|
||||
<a href="/admin/system?tab=themes" @attributes="context">
|
||||
<ChevronLeftIcon/>
|
||||
Back
|
||||
</a>
|
||||
</Slot>
|
||||
</Button>
|
||||
<SubmitButton>
|
||||
<CheckIcon/>
|
||||
Continue
|
||||
</SubmitButton>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row gap-x-1.5">
|
||||
<Button Variant="ButtonVariant.Secondary">
|
||||
<Slot>
|
||||
<a href="/admin/system?tab=themes" @attributes="context">
|
||||
<ChevronLeftIcon/>
|
||||
Back
|
||||
</a>
|
||||
</Slot>
|
||||
</Button>
|
||||
<Button @onclick="SubmitAsync">
|
||||
<CheckIcon/>
|
||||
Continue
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-8">
|
||||
<Card>
|
||||
<CardContent>
|
||||
<FormHandler @ref="Form" OnValidSubmit="OnSubmitAsync" Model="Request">
|
||||
<div class="flex flex-col gap-6">
|
||||
|
||||
<FormValidationSummary />
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-5">
|
||||
<div class="col-span-1 grid gap-2">
|
||||
<Label for="themeName">Name</Label>
|
||||
<InputField
|
||||
<div class="mt-8">
|
||||
<Card>
|
||||
<CardContent>
|
||||
<FieldGroup>
|
||||
<FormValidationSummary/>
|
||||
<DataAnnotationsValidator/>
|
||||
|
||||
<FieldSet ClassName="grid grid-cols-1 lg:grid-cols-2">
|
||||
<Field>
|
||||
<FieldLabel for="themeName">Name</FieldLabel>
|
||||
<TextInputField
|
||||
@bind-Value="Request.Name"
|
||||
id="themeName"
|
||||
placeholder="My cool theme"/>
|
||||
</div>
|
||||
</Field>
|
||||
|
||||
<div class="col-span-1 grid gap-2">
|
||||
<Label for="themeVersion">Version</Label>
|
||||
<InputField
|
||||
<Field>
|
||||
<FieldLabel for="themeVersion">Version</FieldLabel>
|
||||
<TextInputField
|
||||
@bind-Value="Request.Version"
|
||||
id="themeVersion"
|
||||
Type="text"
|
||||
placeholder="1.0.0"/>
|
||||
</div>
|
||||
|
||||
<div class="col-span-1 grid gap-2">
|
||||
<Label for="themeAuthor">Author</Label>
|
||||
<InputField
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<FieldLabel for="themeAuthor">Author</FieldLabel>
|
||||
<TextInputField
|
||||
@bind-Value="Request.Author"
|
||||
id="themeAuthor"
|
||||
Type="text"
|
||||
placeholder="Your name"/>
|
||||
</div>
|
||||
|
||||
<div class="col-span-1 grid gap-2">
|
||||
<Label for="themeAuthor">Is Enabled</Label>
|
||||
<Switch @bind-Value="Request.IsEnabled" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.cm-editor {
|
||||
max-height: 400px;
|
||||
min-height: 400px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="themeAuthor">CSS Content</Label>
|
||||
<Editor @ref="Editor" Language="EditorLanguage.Css" InitialValue="@Request.CssContent"/>
|
||||
</div>
|
||||
</div>
|
||||
</FormHandler>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<FieldLabel for="themeAuthor">Is Enabled</FieldLabel>
|
||||
<FieldContent>
|
||||
<Switch @bind-Value="Request.IsEnabled"/>
|
||||
</FieldContent>
|
||||
</Field>
|
||||
</FieldSet>
|
||||
<Field>
|
||||
<style>
|
||||
.cm-editor {
|
||||
max-height: 400px;
|
||||
min-height: 400px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<FieldLabel for="themeAuthor">CSS Content</FieldLabel>
|
||||
<FieldContent>
|
||||
<Editor @ref="Editor" Language="EditorLanguage.Css" InitialValue="@Request.CssContent"/>
|
||||
</FieldContent>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</EnhancedEditForm>
|
||||
|
||||
@code
|
||||
{
|
||||
@@ -109,22 +113,23 @@
|
||||
CssContent = "/* Define your css here */"
|
||||
};
|
||||
|
||||
private FormHandler Form;
|
||||
private Editor Editor;
|
||||
|
||||
private async Task SubmitAsync()
|
||||
private async Task<bool> OnSubmitAsync(EditContext editContext, ValidationMessageStore validationMessageStore)
|
||||
{
|
||||
Request.CssContent = await Editor.GetValueAsync();
|
||||
await Form.SubmitAsync();
|
||||
}
|
||||
|
||||
private async Task OnSubmitAsync()
|
||||
{
|
||||
await HttpClient.PostAsJsonAsync(
|
||||
var response = await HttpClient.PostAsJsonAsync(
|
||||
"/api/admin/themes",
|
||||
Request,
|
||||
Constants.SerializerOptions
|
||||
);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
await ProblemDetailsHelper.HandleProblemDetailsAsync(response, Request, validationMessageStore);
|
||||
return false;
|
||||
}
|
||||
|
||||
await ToastService.SuccessAsync(
|
||||
"Theme creation",
|
||||
@@ -134,5 +139,7 @@
|
||||
await FrontendService.ReloadAsync();
|
||||
|
||||
Navigation.NavigateTo("/admin/system?tab=themes");
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
@using Moonlight.Shared
|
||||
@using Moonlight.Shared.Http.Requests
|
||||
@using Moonlight.Shared.Http.Responses
|
||||
@using Moonlight.Shared.Http.Responses.Themes
|
||||
@using Moonlight.Shared.Http.Responses.Admin.Themes
|
||||
@using ShadcnBlazor.DataGrids
|
||||
@using ShadcnBlazor.Dropdowns
|
||||
@using ShadcnBlazor.Extras.AlertDialogs
|
||||
|
||||
@@ -3,17 +3,18 @@
|
||||
@using Microsoft.AspNetCore.Authorization
|
||||
@using Moonlight.Shared
|
||||
@using LucideBlazor
|
||||
@using Moonlight.Frontend.Helpers
|
||||
@using Moonlight.Frontend.Mappers
|
||||
@using Moonlight.Frontend.Services
|
||||
@using Moonlight.Shared.Http.Requests.Themes
|
||||
@using Moonlight.Shared.Http.Responses.Themes
|
||||
@using Moonlight.Shared.Http.Requests.Admin.Themes
|
||||
@using Moonlight.Shared.Http.Responses.Admin.Themes
|
||||
@using ShadcnBlazor.Buttons
|
||||
@using ShadcnBlazor.Labels
|
||||
@using ShadcnBlazor.Cards
|
||||
@using ShadcnBlazor.Extras.Common
|
||||
@using ShadcnBlazor.Extras.Editors
|
||||
@using ShadcnBlazor.Extras.FormHandlers
|
||||
@using ShadcnBlazor.Extras.Forms
|
||||
@using ShadcnBlazor.Extras.Toasts
|
||||
@using ShadcnBlazor.Fields
|
||||
@using ShadcnBlazor.Inputs
|
||||
@using ShadcnBlazor.Switches
|
||||
|
||||
@@ -24,99 +25,102 @@
|
||||
@inject ToastService ToastService
|
||||
@inject FrontendService FrontendService
|
||||
|
||||
<div class="flex flex-row justify-between">
|
||||
<div class="flex flex-col">
|
||||
<h1 class="text-xl font-semibold">Update theme</h1>
|
||||
<div class="text-muted-foreground">
|
||||
Update the theme
|
||||
<LazyLoader Load="LoadAsync">
|
||||
<EnhancedEditForm Model="Request" OnValidSubmit="OnSubmitAsync" Context="editFormContext">
|
||||
<div class="flex flex-row justify-between">
|
||||
<div class="flex flex-col">
|
||||
<h1 class="text-xl font-semibold">Update theme</h1>
|
||||
<div class="text-muted-foreground">
|
||||
Update the theme
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row gap-x-1.5">
|
||||
<Button Variant="ButtonVariant.Secondary">
|
||||
<Slot>
|
||||
<a href="/admin/system?tab=themes" @attributes="context">
|
||||
<ChevronLeftIcon/>
|
||||
Back
|
||||
</a>
|
||||
</Slot>
|
||||
</Button>
|
||||
<SubmitButton>
|
||||
<CheckIcon/>
|
||||
Continue
|
||||
</SubmitButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row gap-x-1.5">
|
||||
<Button Variant="ButtonVariant.Secondary">
|
||||
<Slot>
|
||||
<a href="/admin/system?tab=themes" @attributes="context">
|
||||
<ChevronLeftIcon/>
|
||||
Back
|
||||
</a>
|
||||
</Slot>
|
||||
</Button>
|
||||
<Button @onclick="SubmitAsync">
|
||||
<CheckIcon/>
|
||||
Continue
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-8">
|
||||
<Card>
|
||||
<CardContent>
|
||||
<LazyLoader Load="LoadAsync">
|
||||
<FormHandler @ref="Form" OnValidSubmit="OnSubmitAsync" Model="Request">
|
||||
<div class="flex flex-col gap-6">
|
||||
|
||||
<div class="mt-8">
|
||||
<Card>
|
||||
<CardContent>
|
||||
<FieldGroup>
|
||||
<FormValidationSummary/>
|
||||
<DataAnnotationsValidator/>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-5">
|
||||
<div class="col-span-1 grid gap-2">
|
||||
<Label for="themeName">Name</Label>
|
||||
<InputField
|
||||
<FieldSet ClassName="grid grid-cols-1 lg:grid-cols-2">
|
||||
<Field>
|
||||
<FieldLabel for="themeName">Name</FieldLabel>
|
||||
<TextInputField
|
||||
@bind-Value="Request.Name"
|
||||
id="themeName"
|
||||
placeholder="My cool theme"/>
|
||||
</div>
|
||||
</Field>
|
||||
|
||||
<div class="col-span-1 grid gap-2">
|
||||
<Label for="themeVersion">Version</Label>
|
||||
<InputField
|
||||
<Field>
|
||||
<FieldLabel for="themeVersion">Version</FieldLabel>
|
||||
<TextInputField
|
||||
@bind-Value="Request.Version"
|
||||
id="themeVersion"
|
||||
Type="text"
|
||||
placeholder="1.0.0"/>
|
||||
</div>
|
||||
</Field>
|
||||
|
||||
<div class="col-span-1 grid gap-2">
|
||||
<Label for="themeAuthor">Author</Label>
|
||||
<InputField
|
||||
<Field>
|
||||
<FieldLabel for="themeAuthor">Author</FieldLabel>
|
||||
<TextInputField
|
||||
@bind-Value="Request.Author"
|
||||
id="themeAuthor"
|
||||
Type="text"
|
||||
placeholder="Your name"/>
|
||||
</div>
|
||||
</Field>
|
||||
|
||||
<div class="col-span-1 grid gap-2">
|
||||
<Label for="themeAuthor">Is Enabled</Label>
|
||||
<Switch @bind-Value="Request.IsEnabled" />
|
||||
</div>
|
||||
</div>
|
||||
<Field>
|
||||
<FieldLabel for="themeAuthor">Is Enabled</FieldLabel>
|
||||
<FieldContent>
|
||||
<Switch @bind-Value="Request.IsEnabled"/>
|
||||
</FieldContent>
|
||||
</Field>
|
||||
</FieldSet>
|
||||
<Field>
|
||||
<style>
|
||||
.cm-editor {
|
||||
max-height: 400px;
|
||||
min-height: 400px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
.cm-editor {
|
||||
max-height: 400px;
|
||||
min-height: 400px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="themeAuthor">CSS Content</Label>
|
||||
<Editor @ref="Editor" Language="EditorLanguage.Css" InitialValue="@Request.CssContent"/>
|
||||
</div>
|
||||
</div>
|
||||
</FormHandler>
|
||||
</LazyLoader>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
<FieldLabel for="themeAuthor">CSS Content</FieldLabel>
|
||||
<FieldContent>
|
||||
<Editor @ref="Editor" Language="EditorLanguage.Css"
|
||||
InitialValue="@Request.CssContent"/>
|
||||
</FieldContent>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</EnhancedEditForm>
|
||||
</LazyLoader>
|
||||
|
||||
@code
|
||||
{
|
||||
[Parameter] public int Id { get; set; }
|
||||
|
||||
|
||||
private UpdateThemeDto Request;
|
||||
private ThemeDto Theme;
|
||||
|
||||
private FormHandler Form;
|
||||
|
||||
private Editor Editor;
|
||||
|
||||
|
||||
private async Task LoadAsync(LazyLoader _)
|
||||
{
|
||||
var theme = await HttpClient.GetFromJsonAsync<ThemeDto>($"api/admin/themes/{Id}");
|
||||
@@ -125,19 +129,21 @@
|
||||
Request = ThemeMapper.ToUpdate(Theme);
|
||||
}
|
||||
|
||||
private async Task SubmitAsync()
|
||||
private async Task<bool> OnSubmitAsync(EditContext editContext, ValidationMessageStore validationMessageStore)
|
||||
{
|
||||
Request.CssContent = await Editor.GetValueAsync();
|
||||
await Form.SubmitAsync();
|
||||
}
|
||||
|
||||
private async Task OnSubmitAsync()
|
||||
{
|
||||
await HttpClient.PatchAsJsonAsync(
|
||||
var response = await HttpClient.PatchAsJsonAsync(
|
||||
$"/api/admin/themes/{Theme.Id}",
|
||||
Request,
|
||||
Constants.SerializerOptions
|
||||
);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
await ProblemDetailsHelper.HandleProblemDetailsAsync(response, Request, validationMessageStore);
|
||||
return false;
|
||||
}
|
||||
|
||||
await ToastService.SuccessAsync(
|
||||
"Theme update",
|
||||
@@ -147,5 +153,7 @@
|
||||
await FrontendService.ReloadAsync();
|
||||
|
||||
Navigation.NavigateTo("/admin/system?tab=themes");
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@
|
||||
@using Moonlight.Frontend.UI.Admin.Modals
|
||||
@using Moonlight.Shared
|
||||
@using Moonlight.Shared.Http.Requests
|
||||
@using Moonlight.Shared.Http.Requests.Roles
|
||||
@using Moonlight.Shared.Http.Responses
|
||||
@using Moonlight.Shared.Http.Responses.Admin
|
||||
@using ShadcnBlazor.DataGrids
|
||||
@@ -134,15 +133,8 @@
|
||||
{
|
||||
await DialogService.LaunchAsync<CreateRoleDialog>(parameters =>
|
||||
{
|
||||
parameters[nameof(CreateRoleDialog.OnSubmit)] = async Task (CreateRoleDto request) =>
|
||||
parameters[nameof(CreateRoleDialog.OnSubmit)] = async Task () =>
|
||||
{
|
||||
await HttpClient.PostAsJsonAsync(
|
||||
"api/admin/roles",
|
||||
request,
|
||||
Constants.SerializerOptions
|
||||
);
|
||||
|
||||
await ToastService.SuccessAsync("Role creation", $"Role {request.Name} has been successfully created");
|
||||
await Grid.RefreshAsync();
|
||||
};
|
||||
});
|
||||
@@ -153,15 +145,8 @@
|
||||
await DialogService.LaunchAsync<UpdateRoleDialog>(parameters =>
|
||||
{
|
||||
parameters[nameof(UpdateRoleDialog.Role)] = role;
|
||||
parameters[nameof(UpdateRoleDialog.OnSubmit)] = async Task (UpdateRoleDto request) =>
|
||||
parameters[nameof(UpdateRoleDialog.OnSubmit)] = async Task () =>
|
||||
{
|
||||
await HttpClient.PatchAsJsonAsync(
|
||||
$"api/admin/roles/{role.Id}",
|
||||
request,
|
||||
Constants.SerializerOptions
|
||||
);
|
||||
|
||||
await ToastService.SuccessAsync("Role update", $"Role {request.Name} has been successfully updated");
|
||||
await Grid.RefreshAsync();
|
||||
};
|
||||
});
|
||||
|
||||
@@ -10,9 +10,8 @@
|
||||
@using ShadcnBlazor.Extras.Toasts
|
||||
@using ShadcnBlazor.Tabels
|
||||
@using Moonlight.Shared.Http.Requests
|
||||
@using Moonlight.Shared.Http.Requests.Users
|
||||
@using Moonlight.Shared.Http.Responses
|
||||
@using Moonlight.Shared.Http.Responses.Users
|
||||
@using Moonlight.Shared.Http.Responses.Admin.Users
|
||||
@using ShadcnBlazor.Extras.Dialogs
|
||||
|
||||
@inject HttpClient HttpClient
|
||||
@@ -132,19 +131,8 @@
|
||||
{
|
||||
await DialogService.LaunchAsync<CreateUserDialog>(parameters =>
|
||||
{
|
||||
parameters[nameof(CreateUserDialog.OnSubmit)] = async (CreateUserDto dto) =>
|
||||
parameters[nameof(CreateUserDialog.OnCompleted)] = async () =>
|
||||
{
|
||||
await HttpClient.PostAsJsonAsync(
|
||||
"/api/admin/users",
|
||||
dto,
|
||||
Constants.SerializerOptions
|
||||
);
|
||||
|
||||
await ToastService.SuccessAsync(
|
||||
"User creation",
|
||||
$"Successfully created user {dto.Username}"
|
||||
);
|
||||
|
||||
await Grid.RefreshAsync();
|
||||
};
|
||||
});
|
||||
@@ -155,18 +143,8 @@
|
||||
await DialogService.LaunchAsync<UpdateUserDialog>(parameters =>
|
||||
{
|
||||
parameters[nameof(UpdateUserDialog.User)] = user;
|
||||
parameters[nameof(CreateUserDialog.OnSubmit)] = async (UpdateUserDto dto) =>
|
||||
parameters[nameof(UpdateUserDialog.OnCompleted)] = async () =>
|
||||
{
|
||||
await HttpClient.PatchAsJsonAsync(
|
||||
$"/api/admin/users/{user.Id}",
|
||||
dto
|
||||
);
|
||||
|
||||
await ToastService.SuccessAsync(
|
||||
"User update",
|
||||
$"Successfully updated user {dto.Username}"
|
||||
);
|
||||
|
||||
await Grid.RefreshAsync();
|
||||
};
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
@@ -30,33 +33,42 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
<Authentication/>
|
||||
var uri = new Uri(Navigation.Uri);
|
||||
|
||||
if (uri.LocalPath.StartsWith("/setup"))
|
||||
{
|
||||
<Setup />
|
||||
}
|
||||
else
|
||||
{
|
||||
<Authentication/>
|
||||
}
|
||||
}
|
||||
</NotAuthorized>
|
||||
</AuthorizeView>
|
||||
</ChildContent>
|
||||
<ErrorContent>
|
||||
@if (context is HttpRequestException { StatusCode: HttpStatusCode.Unauthorized })
|
||||
{
|
||||
<Authentication/>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="m-10">
|
||||
<Empty>
|
||||
<EmptyHeader>
|
||||
<EmptyMedia Variant="EmptyMediaVariant.Icon">
|
||||
<OctagonAlertIcon ClassName="text-red-500/80"/>
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>
|
||||
Critical Application Error
|
||||
</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
@context.ToString()
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
</Empty>
|
||||
</div>
|
||||
}
|
||||
@if (context is HttpRequestException { StatusCode: HttpStatusCode.Unauthorized })
|
||||
{
|
||||
<Authentication/>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="m-10">
|
||||
<Empty>
|
||||
<EmptyHeader>
|
||||
<EmptyMedia Variant="EmptyMediaVariant.Icon">
|
||||
<OctagonAlertIcon ClassName="text-red-500/80"/>
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>
|
||||
Critical Application Error
|
||||
</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
@context.ToString()
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
</Empty>
|
||||
</div>
|
||||
}
|
||||
</ErrorContent>
|
||||
</ErrorBoundary>
|
||||
@@ -1,7 +1,7 @@
|
||||
@using ShadcnBlazor.Cards
|
||||
@using Moonlight.Shared.Http.Responses.Admin.Auth
|
||||
@using ShadcnBlazor.Cards
|
||||
@using ShadcnBlazor.Spinners
|
||||
@using ShadcnBlazor.Buttons
|
||||
@using Moonlight.Shared.Http.Responses.Auth
|
||||
|
||||
@inject HttpClient HttpClient
|
||||
@inject NavigationManager Navigation
|
||||
|
||||
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>
|
||||
<TextInputField
|
||||
@bind-Value="SetupDto.AdminUsername"
|
||||
id="username"
|
||||
placeholder="someoneelse"/>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="email">Email</Label>
|
||||
<TextInputField
|
||||
@bind-Value="SetupDto.AdminEmail"
|
||||
id="email"
|
||||
Type="email"
|
||||
placeholder="a@cool.email"/>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="password">Password</Label>
|
||||
<TextInputField
|
||||
@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/Events/RebuildEventDto.cs
Normal file
20
Moonlight.Shared/Http/Events/RebuildEventDto.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Moonlight.Shared.Http.Events;
|
||||
|
||||
public struct RebuildEventDto
|
||||
{
|
||||
[JsonPropertyName("type")]
|
||||
public RebuildEventType Type { get; set; }
|
||||
|
||||
[JsonPropertyName("data")]
|
||||
public string Data { get; set; }
|
||||
}
|
||||
|
||||
public enum RebuildEventType
|
||||
{
|
||||
Log = 0,
|
||||
Failed = 1,
|
||||
Succeeded = 2,
|
||||
Step = 3
|
||||
}
|
||||
@@ -1,13 +1,16 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Moonlight.Shared.Http.Requests.ApiKeys;
|
||||
namespace Moonlight.Shared.Http.Requests.Admin.ApiKeys;
|
||||
|
||||
public class CreateApiKeyDto
|
||||
{
|
||||
[Required]
|
||||
[MaxLength(30)]
|
||||
public string Name { get; set; }
|
||||
|
||||
|
||||
[MaxLength(300)] public string Description { get; set; } = "";
|
||||
|
||||
|
||||
[Required]
|
||||
public string[] Permissions { get; set; }
|
||||
}
|
||||
@@ -1,13 +1,15 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Moonlight.Shared.Http.Requests.ApiKeys;
|
||||
namespace Moonlight.Shared.Http.Requests.Admin.ApiKeys;
|
||||
|
||||
public class UpdateApiKeyDto
|
||||
{
|
||||
[Required]
|
||||
[MaxLength(30)]
|
||||
public string Name { get; set; }
|
||||
|
||||
|
||||
[MaxLength(300)] public string Description { get; set; } = "";
|
||||
|
||||
[Required]
|
||||
public string[] Permissions { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace Moonlight.Shared.Http.Requests.Admin.ContainerHelper;
|
||||
|
||||
public class RequestRebuildDto
|
||||
{
|
||||
public bool NoBuildCache { get; set; }
|
||||
|
||||
public RequestRebuildDto()
|
||||
{
|
||||
}
|
||||
|
||||
public RequestRebuildDto(bool noBuildCache)
|
||||
{
|
||||
NoBuildCache = noBuildCache;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Moonlight.Shared.Http.Requests.Admin.ContainerHelper;
|
||||
|
||||
public class SetVersionDto
|
||||
{
|
||||
[Required]
|
||||
[RegularExpression(@"^(?!\/|.*\/\/|.*\.\.|.*\/$)[A-Za-z0-9._/-]+$", ErrorMessage = "Invalid version format")]
|
||||
public string Version { get; set; }
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Moonlight.Shared.Http.Requests.Roles;
|
||||
namespace Moonlight.Shared.Http.Requests.Admin.Roles;
|
||||
|
||||
public class CreateRoleDto
|
||||
{
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Moonlight.Shared.Http.Requests.Roles;
|
||||
namespace Moonlight.Shared.Http.Requests.Admin.Roles;
|
||||
|
||||
public class UpdateRoleDto
|
||||
{
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Moonlight.Shared.Http.Requests.Themes;
|
||||
namespace Moonlight.Shared.Http.Requests.Admin.Themes;
|
||||
|
||||
public class CreateThemeDto
|
||||
{
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Moonlight.Shared.Http.Requests.Themes;
|
||||
namespace Moonlight.Shared.Http.Requests.Admin.Themes;
|
||||
|
||||
public class UpdateThemeDto
|
||||
{
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Moonlight.Shared.Http.Requests.Users;
|
||||
namespace Moonlight.Shared.Http.Requests.Admin.Users;
|
||||
|
||||
public class CreateUserDto
|
||||
{
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Moonlight.Shared.Http.Requests.Users;
|
||||
namespace Moonlight.Shared.Http.Requests.Admin.Users;
|
||||
|
||||
public class UpdateUserDto
|
||||
{
|
||||
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; }
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
namespace Moonlight.Shared.Http.Responses.ApiKeys;
|
||||
namespace Moonlight.Shared.Http.Responses.Admin.ApiKeys;
|
||||
|
||||
public record ApiKeyDto(int Id, string Name, string Description, string[] Permissions, string Key, DateTimeOffset CreatedAt, DateTimeOffset UpdatedAt);
|
||||
3
Moonlight.Shared/Http/Responses/Admin/Auth/ClaimDto.cs
Normal file
3
Moonlight.Shared/Http/Responses/Admin/Auth/ClaimDto.cs
Normal file
@@ -0,0 +1,3 @@
|
||||
namespace Moonlight.Shared.Http.Responses.Admin.Auth;
|
||||
|
||||
public record ClaimDto(string Type, string Value);
|
||||
@@ -1,3 +1,3 @@
|
||||
namespace Moonlight.Shared.Http.Responses.Auth;
|
||||
namespace Moonlight.Shared.Http.Responses.Admin.Auth;
|
||||
|
||||
public record SchemeDto(string Name, string DisplayName);
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace Moonlight.Shared.Http.Responses.Admin;
|
||||
|
||||
public record ContainerHelperStatusDto(bool IsEnabled, bool IsReachable);
|
||||
@@ -1,3 +1,3 @@
|
||||
namespace Moonlight.Shared.Http.Responses.Frontend;
|
||||
namespace Moonlight.Shared.Http.Responses.Admin.Frontend;
|
||||
|
||||
public record FrontendConfigDto(string Name, string? ThemeCss);
|
||||
@@ -1,3 +1,3 @@
|
||||
namespace Moonlight.Shared.Http.Responses.Themes;
|
||||
namespace Moonlight.Shared.Http.Responses.Admin.Themes;
|
||||
|
||||
public record ThemeDto(int Id, string Name, string Author, string Version, string CssContent, bool IsEnabled);
|
||||
@@ -1,3 +1,3 @@
|
||||
namespace Moonlight.Shared.Http.Responses.Users;
|
||||
namespace Moonlight.Shared.Http.Responses.Admin.Users;
|
||||
|
||||
public record UserDto(int Id, string Username, string Email, DateTimeOffset CreatedAt, DateTimeOffset UpdatedAt);
|
||||
@@ -1,3 +0,0 @@
|
||||
namespace Moonlight.Shared.Http.Responses.Auth;
|
||||
|
||||
public record ClaimDto(string Type, string Value);
|
||||
10
Moonlight.Shared/Http/Responses/ProblemDetails.cs
Normal file
10
Moonlight.Shared/Http/Responses/ProblemDetails.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace Moonlight.Shared.Http.Responses;
|
||||
|
||||
public class ProblemDetails
|
||||
{
|
||||
public string Type { get; set; }
|
||||
public string Title { get; set; }
|
||||
public int Status { get; set; }
|
||||
public string? Detail { get; set; }
|
||||
public Dictionary<string, string[]>? Errors { get; set; }
|
||||
}
|
||||
@@ -1,14 +1,17 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using Moonlight.Shared.Http.Requests.ApiKeys;
|
||||
using Moonlight.Shared.Http.Requests.Roles;
|
||||
using Moonlight.Shared.Http.Requests.Themes;
|
||||
using Moonlight.Shared.Http.Requests.Users;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Moonlight.Shared.Http.Events;
|
||||
using Moonlight.Shared.Http.Requests.Admin.ApiKeys;
|
||||
using Moonlight.Shared.Http.Requests.Admin.ContainerHelper;
|
||||
using Moonlight.Shared.Http.Requests.Admin.Roles;
|
||||
using Moonlight.Shared.Http.Requests.Admin.Themes;
|
||||
using Moonlight.Shared.Http.Requests.Admin.Users;
|
||||
using Moonlight.Shared.Http.Responses;
|
||||
using Moonlight.Shared.Http.Responses.Admin;
|
||||
using Moonlight.Shared.Http.Responses.ApiKeys;
|
||||
using Moonlight.Shared.Http.Responses.Auth;
|
||||
using Moonlight.Shared.Http.Responses.Themes;
|
||||
using Moonlight.Shared.Http.Responses.Users;
|
||||
using Moonlight.Shared.Http.Responses.Admin.ApiKeys;
|
||||
using Moonlight.Shared.Http.Responses.Admin.Auth;
|
||||
using Moonlight.Shared.Http.Responses.Admin.Themes;
|
||||
using Moonlight.Shared.Http.Responses.Admin.Users;
|
||||
|
||||
namespace Moonlight.Shared.Http;
|
||||
|
||||
@@ -44,8 +47,32 @@ namespace Moonlight.Shared.Http;
|
||||
[JsonSerializable(typeof(PagedData<ThemeDto>))]
|
||||
[JsonSerializable(typeof(ThemeDto))]
|
||||
|
||||
// Events
|
||||
[JsonSerializable(typeof(RebuildEventDto))]
|
||||
|
||||
// Container Helper
|
||||
[JsonSerializable(typeof(ContainerHelperStatusDto))]
|
||||
[JsonSerializable(typeof(RequestRebuildDto))]
|
||||
[JsonSerializable(typeof(SetVersionDto))]
|
||||
|
||||
//Misc
|
||||
[JsonSerializable(typeof(VersionDto))]
|
||||
[JsonSerializable(typeof(ProblemDetails))]
|
||||
public partial class SerializationContext : JsonSerializerContext
|
||||
{
|
||||
private static JsonSerializerOptions? InternalTunedOptions;
|
||||
|
||||
public static JsonSerializerOptions TunedOptions
|
||||
{
|
||||
get
|
||||
{
|
||||
if (InternalTunedOptions != null)
|
||||
return InternalTunedOptions;
|
||||
|
||||
InternalTunedOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web);
|
||||
InternalTunedOptions.TypeInfoResolverChain.Add(Default);
|
||||
|
||||
return InternalTunedOptions;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -54,5 +54,6 @@ public static class Permissions
|
||||
public const string Info = $"{Prefix}{Section}.{nameof(Info)}";
|
||||
public const string Diagnose = $"{Prefix}{Section}.{nameof(Diagnose)}";
|
||||
public const string Versions = $"{Prefix}{Section}.{nameof(Versions)}";
|
||||
public const string Instance = $"{Prefix}{Section}.{nameof(Instance)}";
|
||||
}
|
||||
}
|
||||
30
compose.yaml
30
compose.yaml
@@ -36,4 +36,32 @@
|
||||
|
||||
# Logging
|
||||
- "Logging__LogLevel__Default=Information"
|
||||
- "Logging__LogLevel__Microsoft.AspNetCore=Warning"
|
||||
- "Logging__LogLevel__Microsoft.AspNetCore=Warning"
|
||||
- "Logging__LogLevel__System.Net.Http.HttpClient=Warning"
|
||||
|
||||
- "Moonlight__ContainerHelper__IsEnabled=true"
|
||||
- "Moonlight__ContainerHelper__Url=http://app:8080"
|
||||
|
||||
app:
|
||||
image: git.battlestati.one/moonlight-panel/container_helper
|
||||
|
||||
group_add:
|
||||
- "989"
|
||||
|
||||
environment:
|
||||
# Logging
|
||||
- "Logging__LogLevel__Default=Information"
|
||||
- "Logging__LogLevel__Microsoft.AspNetCore=Warning"
|
||||
|
||||
# Compose
|
||||
- "ContainerHelper__Compose__Directory=${PWD}"
|
||||
- "ContainerHelper__Compose__Binary=docker-compose"
|
||||
- "ContainerHelper__Service__Name=api"
|
||||
|
||||
# HTTP Proxy
|
||||
- "HTTP_PROXY=${HTTP_PROXY}"
|
||||
- "HTTPS_PROXY=${HTTPS_PROXY}"
|
||||
|
||||
volumes:
|
||||
- "${PWD}:${PWD}"
|
||||
- "/var/run/docker.sock:/var/run/docker.sock"
|
||||
Reference in New Issue
Block a user