2 Commits

87 changed files with 629 additions and 2202 deletions

1
.gitignore vendored
View File

@@ -400,7 +400,6 @@ FodyWeavers.xsd
# Style builds
**/style.min.css
**/package-lock.json
**/bun.lock
# Secrets
**/.env

View File

@@ -1,7 +0,0 @@
namespace Moonlight.Api.Configuration;
public class ContainerHelperOptions
{
public bool IsEnabled { get; set; }
public string Url { get; set; } = "http://helper:8080";
}

View File

@@ -4,7 +4,6 @@ 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; }

View File

@@ -1,6 +0,0 @@
namespace Moonlight.Api.Configuration;
public class SettingsOptions
{
public int CacheMinutes { get; set; } = 3;
}

View File

@@ -1,5 +1,4 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Moonlight.Api.Database.Entities;
@@ -11,6 +10,5 @@ public class SettingsOption
public required string Key { get; set; }
[MaxLength(4096)]
[Column(TypeName = "jsonb")]
public required string ValueJson { get; set; }
public required string Value { get; set; }
}

View File

@@ -1,251 +0,0 @@
// <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
}
}
}

View File

@@ -1,46 +0,0 @@
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: "");
}
}
}

View File

@@ -136,10 +136,10 @@ namespace Moonlight.Api.Database.Migrations
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("ValueJson")
b.Property<string>("Value")
.IsRequired()
.HasMaxLength(4096)
.HasColumnType("jsonb");
.HasColumnType("character varying(4096)");
b.HasKey("Id");

View File

@@ -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.Admin.ApiKeys;
using Moonlight.Shared.Http.Requests.ApiKeys;
using Moonlight.Shared.Http.Responses;
using Moonlight.Shared.Http.Responses.Admin.ApiKeys;
using Moonlight.Shared.Http.Responses.ApiKeys;
namespace Moonlight.Api.Http.Controllers.Admin;

View File

@@ -1,56 +0,0 @@
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();
}
}

View File

@@ -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.Admin.Users;
using Moonlight.Shared.Http.Responses.Users;
namespace Moonlight.Api.Http.Controllers.Admin;

View File

@@ -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.Admin.Roles;
using Moonlight.Shared.Http.Requests.Roles;
using Moonlight.Shared.Http.Responses;
using Moonlight.Shared.Http.Responses.Admin;

View File

@@ -1,129 +0,0 @@
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();
}
}

View File

@@ -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.Admin.Themes;
using Moonlight.Shared.Http.Requests.Themes;
using Moonlight.Shared.Http.Responses;
using Moonlight.Shared.Http.Responses.Admin.Themes;
using Moonlight.Shared.Http.Responses.Themes;
namespace Moonlight.Api.Http.Controllers.Admin;

View File

@@ -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.Admin.Users;
using Moonlight.Shared.Http.Requests.Users;
using Moonlight.Shared.Http.Responses;
using Moonlight.Shared.Http.Responses.Admin.Users;
using Moonlight.Shared.Http.Responses.Users;
namespace Moonlight.Api.Http.Controllers.Admin.Users;

View File

@@ -1,7 +1,7 @@
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Moonlight.Shared.Http.Responses.Admin.Auth;
using Moonlight.Shared.Http.Responses.Auth;
namespace Moonlight.Api.Http.Controllers;

View File

@@ -1,7 +1,7 @@
using Microsoft.AspNetCore.Mvc;
using Moonlight.Api.Mappers;
using Moonlight.Api.Services;
using Moonlight.Shared.Http.Responses.Admin.Frontend;
using Moonlight.Shared.Http.Responses.Frontend;
namespace Moonlight.Api.Http.Controllers;

View File

@@ -1,13 +0,0 @@
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");
}

View File

@@ -1,20 +0,0 @@
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
}

View File

@@ -1,10 +0,0 @@
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; }
}

View File

@@ -1,3 +0,0 @@
namespace Moonlight.Api.Http.Services.ContainerHelper.Requests;
public record RequestRebuildDto(bool NoBuildCache);

View File

@@ -1,3 +0,0 @@
namespace Moonlight.Api.Http.Services.ContainerHelper.Requests;
public record SetVersionDto(string Version);

View File

@@ -1,29 +0,0 @@
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;
}
}
}

View File

@@ -1,7 +1,7 @@
using System.Diagnostics.CodeAnalysis;
using Moonlight.Api.Database.Entities;
using Moonlight.Shared.Http.Requests.Admin.ApiKeys;
using Moonlight.Shared.Http.Responses.Admin.ApiKeys;
using Moonlight.Shared.Http.Requests.ApiKeys;
using Moonlight.Shared.Http.Responses.ApiKeys;
using Riok.Mapperly.Abstractions;
namespace Moonlight.Api.Mappers;

View File

@@ -1,13 +0,0 @@
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);
}

View File

@@ -1,6 +1,6 @@
using System.Diagnostics.CodeAnalysis;
using Moonlight.Api.Models;
using Moonlight.Shared.Http.Responses.Admin.Frontend;
using Moonlight.Shared.Http.Responses.Frontend;
using Riok.Mapperly.Abstractions;
namespace Moonlight.Api.Mappers;

View File

@@ -1,6 +1,6 @@
using System.Diagnostics.CodeAnalysis;
using Moonlight.Api.Database.Entities;
using Moonlight.Shared.Http.Requests.Admin.Roles;
using Moonlight.Shared.Http.Requests.Roles;
using Moonlight.Shared.Http.Responses.Admin;
using Riok.Mapperly.Abstractions;

View File

@@ -1,7 +1,7 @@
using System.Diagnostics.CodeAnalysis;
using Moonlight.Api.Database.Entities;
using Moonlight.Shared.Http.Requests.Admin.Themes;
using Moonlight.Shared.Http.Responses.Admin.Themes;
using Moonlight.Shared.Http.Requests.Themes;
using Moonlight.Shared.Http.Responses.Themes;
using Riok.Mapperly.Abstractions;
namespace Moonlight.Api.Mappers;

View File

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

View File

@@ -35,8 +35,4 @@
<Visible Condition="'%(NuGetItemType)' == 'Content'">false</Visible>
</Content>
</ItemGroup>
<ItemGroup>
<Folder Include="Http\Services\ContainerHelper\Responses\" />
</ItemGroup>
</Project>

View File

@@ -1,117 +0,0 @@
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}");
}
}

View File

@@ -1,83 +0,0 @@
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);
}
}

View File

@@ -74,12 +74,6 @@ 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();
@@ -106,9 +100,6 @@ 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)

View File

@@ -44,15 +44,6 @@ 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>();
}

View File

@@ -1,30 +0,0 @@
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);
}
}
}
}
}

View File

@@ -28,7 +28,6 @@ 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"),

View File

@@ -1,6 +1,6 @@
using System.Diagnostics.CodeAnalysis;
using Moonlight.Shared.Http.Requests.Admin.ApiKeys;
using Moonlight.Shared.Http.Responses.Admin.ApiKeys;
using Moonlight.Shared.Http.Requests.ApiKeys;
using Moonlight.Shared.Http.Responses.ApiKeys;
using Riok.Mapperly.Abstractions;
namespace Moonlight.Frontend.Mappers;

View File

@@ -1,5 +1,5 @@
using System.Diagnostics.CodeAnalysis;
using Moonlight.Shared.Http.Requests.Admin.Roles;
using Moonlight.Shared.Http.Requests.Roles;
using Moonlight.Shared.Http.Responses.Admin;
using Riok.Mapperly.Abstractions;

View File

@@ -1,6 +1,6 @@
using System.Diagnostics.CodeAnalysis;
using Moonlight.Shared.Http.Requests.Admin.Themes;
using Moonlight.Shared.Http.Responses.Admin.Themes;
using Moonlight.Shared.Http.Requests.Themes;
using Moonlight.Shared.Http.Responses.Themes;
using Riok.Mapperly.Abstractions;
namespace Moonlight.Frontend.Mappers;

View File

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

View File

@@ -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.11" />
<PackageReference Include="ShadcnBlazor.Extras" Version="1.0.11" />
<PackageReference Include="ShadcnBlazor" Version="1.0.9" />
<PackageReference Include="ShadcnBlazor.Extras" Version="1.0.9" />
</ItemGroup>
<ItemGroup>

View File

@@ -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.Admin.Auth;
using Moonlight.Shared.Http.Responses.Auth;
namespace Moonlight.Frontend.Services;

View File

@@ -1,18 +1,13 @@
@using Moonlight.Frontend.Helpers
@using Moonlight.Frontend.UI.Admin.Components
@using Moonlight.Shared.Http.Requests.Admin.ApiKeys
@using Moonlight.Shared.Http.Responses
@using Moonlight.Frontend.UI.Admin.Components
@using Moonlight.Shared.Http.Requests.ApiKeys
@using ShadcnBlazor.Dialogs
@using ShadcnBlazor.Extras.Forms
@using ShadcnBlazor.Extras.Toasts
@using ShadcnBlazor.Fields
@using ShadcnBlazor.Extras.Common
@using ShadcnBlazor.Extras.FormHandlers
@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>
@@ -20,74 +15,56 @@
</DialogDescription>
</DialogHeader>
<EnhancedEditForm Model="Request" OnValidSubmit="OnSubmitAsync">
<FormHandler @ref="FormHandler" Model="Request" OnValidSubmit="SubmitAsync">
<div class="flex flex-col gap-6">
<FormValidationSummary />
<FieldGroup>
<DataAnnotationsValidator/>
<FormValidationSummary/>
<div class="grid gap-2">
<Label for="keyName">Name</Label>
<InputField @bind-Value="Request.Name" id="keyName" placeholder="My API key" />
</div>
<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>
<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>
@code
{
[Parameter] public Func<Task> OnSubmit { get; set; }
[Parameter] public Func<CreateApiKeyDto, Task> OnSubmit { get; set; }
private CreateApiKeyDto Request;
private FormHandler FormHandler;
private List<string> Permissions = new();
protected override void OnInitialized()
{
Request = new()
{
Permissions = []
};
Request = new();
}
private async Task<bool> OnSubmitAsync(EditContext editContext, ValidationMessageStore validationMessageStore)
private async Task SubmitAsync()
{
Request.Permissions = Permissions.ToArray();
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 OnSubmit.Invoke(Request);
await CloseAsync();
return true;
}
}

View File

@@ -1,17 +1,14 @@
@using Moonlight.Frontend.Helpers
@using Moonlight.Frontend.UI.Admin.Components
@using Moonlight.Shared.Http.Requests.Admin.Roles
@using Moonlight.Shared.Http.Requests.Roles
@using ShadcnBlazor.Buttons
@using ShadcnBlazor.Dialogs
@using ShadcnBlazor.Extras.Forms
@using ShadcnBlazor.Extras.Toasts
@using ShadcnBlazor.Fields
@using ShadcnBlazor.Extras.Common
@using ShadcnBlazor.Extras.FormHandlers
@using ShadcnBlazor.Inputs
@using ShadcnBlazor.Labels
@inherits ShadcnBlazor.Extras.Dialogs.DialogBase
@inject HttpClient HttpClient
@inject ToastService ToastService
<DialogHeader>
<DialogTitle>
Create new role
@@ -21,43 +18,49 @@
</DialogDescription>
</DialogHeader>
<EnhancedEditForm Model="Request" OnValidSubmit="OnSubmitAsync">
<FieldGroup>
<FormValidationSummary/>
<DataAnnotationsValidator/>
<FormHandler @ref="FormHandler" Model="Request" OnValidSubmit="OnSubmitAsync">
<div class="flex flex-col gap-6">
<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>
<FormValidationSummary/>
<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>
@code
{
[Parameter] public Func<Task> OnSubmit { get; set; }
[Parameter] public Func<CreateRoleDto, Task> OnSubmit { get; set; }
private CreateRoleDto Request;
private List<string> Permissions;
private FormHandler FormHandler;
protected override void OnInitialized()
{
@@ -69,27 +72,15 @@
Permissions = new();
}
private async Task<bool> OnSubmitAsync(EditContext editContext, ValidationMessageStore validationMessageStore)
private async Task SubmitAsync()
{
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;
}
await ToastService.SuccessAsync("Role creation", $"Role {Request.Name} has been successfully created");
await OnSubmit.Invoke();
private async Task OnSubmitAsync()
{
await OnSubmit.Invoke(Request);
await CloseAsync();
return true;
}
}

View File

@@ -1,17 +1,12 @@
@using Moonlight.Frontend.Helpers
@using Moonlight.Shared.Http.Requests.Admin.Users
@using Moonlight.Shared.Http.Responses
@using Moonlight.Shared.Http.Requests.Users
@using ShadcnBlazor.Dialogs
@using ShadcnBlazor.Extras.Forms
@using ShadcnBlazor.Extras.Toasts
@using ShadcnBlazor.Fields
@using ShadcnBlazor.Extras.Common
@using ShadcnBlazor.Extras.FormHandlers
@using ShadcnBlazor.Inputs
@using ShadcnBlazor.Labels
@inherits ShadcnBlazor.Extras.Dialogs.DialogBase
@inject HttpClient HttpClient
@inject ToastService ToastService
<DialogHeader>
<DialogTitle>
Create new user
@@ -21,67 +16,50 @@
</DialogDescription>
</DialogHeader>
<EnhancedEditForm Model="Request" OnValidSubmit="OnSubmitAsync">
<FormHandler @ref="FormHandler" Model="Request" OnValidSubmit="SubmitAsync">
<div class="flex flex-col gap-6">
<FieldGroup>
<FormValidationSummary/>
<DataAnnotationsValidator />
<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>
<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>
@code
{
[Parameter] public Func<Task> OnCompleted { get; set; }
[Parameter] public Func<CreateUserDto, Task> OnSubmit { get; set; }
private CreateUserDto Request;
private FormHandler FormHandler;
protected override void OnInitialized()
{
Request = new();
}
private async Task<bool> OnSubmitAsync(EditContext editContext, ValidationMessageStore validationMessageStore)
private async Task SubmitAsync()
{
var response = await HttpClient.PostAsJsonAsync(
"/api/admin/users",
Request,
Constants.SerializerOptions
);
await OnSubmit.Invoke(Request);
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;
}
}

View File

@@ -1,7 +1,7 @@
@using LucideBlazor
@using Moonlight.Shared.Http.Responses
@using Moonlight.Shared.Http.Responses.Admin
@using Moonlight.Shared.Http.Responses.Admin.Users
@using Moonlight.Shared.Http.Responses.Users
@using ShadcnBlazor.Buttons
@using ShadcnBlazor.DataGrids
@using ShadcnBlazor.Dialogs
@@ -32,9 +32,9 @@
SearchPlaceholder="Search user"
ValueSelector="dto => dto.Username"
Source="LoadUsersAsync"/>
<WButton OnClick="AddAsync" Variant="ButtonVariant.Outline" Size="ButtonSize.Icon">
<WButtom OnClick="AddAsync" Variant="ButtonVariant.Outline" Size="ButtonSize.Icon">
<PlusIcon/>
</WButton>
</WButtom>
</div>
</div>
<div class="mt-3">
@@ -50,9 +50,9 @@
<CellTemplate>
<TableCell>
<div class="flex justify-end me-1.5">
<WButton OnClick="_ => RemoveAsync(context)" Variant="ButtonVariant.Destructive" Size="ButtonSize.Icon">
<WButtom OnClick="_ => RemoveAsync(context)" Variant="ButtonVariant.Destructive" Size="ButtonSize.Icon">
<TrashIcon/>
</WButton>
</WButtom>
</div>
</TableCell>
</CellTemplate>

View File

@@ -1,19 +1,15 @@
@using Moonlight.Frontend.Helpers
@using Moonlight.Frontend.Mappers
@using Moonlight.Frontend.Mappers
@using Moonlight.Frontend.UI.Admin.Components
@using Moonlight.Shared.Http.Requests.Admin.ApiKeys
@using Moonlight.Shared.Http.Responses.Admin.ApiKeys
@using Moonlight.Shared.Http.Requests.ApiKeys
@using Moonlight.Shared.Http.Responses.ApiKeys
@using ShadcnBlazor.Dialogs
@using ShadcnBlazor.Extras.Forms
@using ShadcnBlazor.Extras.Toasts
@using ShadcnBlazor.Fields
@using ShadcnBlazor.Extras.Common
@using ShadcnBlazor.Extras.FormHandlers
@using ShadcnBlazor.Inputs
@using ShadcnBlazor.Labels
@inherits ShadcnBlazor.Extras.Dialogs.DialogBase
@inject HttpClient HttpClient
@inject ToastService ToastService
<DialogHeader>
<DialogTitle>Update API key</DialogTitle>
<DialogDescription>
@@ -21,39 +17,45 @@
</DialogDescription>
</DialogHeader>
<EnhancedEditForm Model="Request" OnValidSubmit="OnSubmitAsync">
<FieldGroup>
<FormValidationSummary/>
<DataAnnotationsValidator/>
<FormHandler @ref="FormHandler" Model="Request" OnValidSubmit="SubmitAsync">
<div class="flex flex-col gap-6">
<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>
<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>
@code
{
[Parameter] public Func<Task> OnSubmit { get; set; }
[Parameter] public Func<UpdateApiKeyDto, 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()
@@ -62,30 +64,10 @@
Permissions = Key.Permissions.ToList();
}
private async Task<bool> OnSubmitAsync(EditContext editContext, ValidationMessageStore validationMessageStore)
private async Task SubmitAsync()
{
Request.Permissions = Permissions.ToArray();
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 OnSubmit.Invoke(Request);
await CloseAsync();
return true;
}
}

View File

@@ -1,38 +1,37 @@
@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 HttpClient HttpClient
@inject AlertDialogService AlertService
<DialogHeader>
<DialogTitle>
Updating instance to @Version...
Updating...
</DialogTitle>
</DialogHeader>
<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="text-base flex flex-col p-2 gap-y-0.5">
@for (var i = 0; i < Steps.Length; i++)
{
if (CurrentStep == i)
{
if (CurrentStep == i)
<div class="flex flex-row items-center gap-x-2">
<Spinner ClassName="size-4" />
<span>
@Steps[i]
</span>
</div>
}
else
{
if (i < CurrentStep)
{
<div class="flex flex-row items-center gap-x-1">
@if (IsFailed)
{
<CircleXIcon ClassName="text-red-500 size-5"/>
}
else
{
<Spinner ClassName="size-5"/>
}
<div class="flex flex-row items-center gap-x-2">
<CheckIcon ClassName="text-green-500 size-4" />
<span>
@Steps[i]
</span>
@@ -40,205 +39,81 @@
}
else
{
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 class="text-muted-foreground flex flex-row items-center gap-x-2">
<span class="size-4"></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>
@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>
}
<DialogFooter>
<Progress Value="@Progress"></Progress>
</DialogFooter>
@code
{
[Parameter] public string Version { get; set; }
[Parameter] public bool NoBuildCache { get; set; }
private int Progress = 0;
private bool IsFailed;
private int Progress;
private int CurrentStep;
private readonly string[] Steps =
private string[] Steps =
[
"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
"Preparing",
"Updating configuration files",
"Building docker image",
"Redeploying container instance",
"Waiting for container instance to start up",
"Update complete"
];
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 HttpClient.PostAsJsonAsync("api/admin/ch/version", new SetVersionDto()
{
Version = Version
}, SerializationContext.TunedOptions);
await Task.Delay(6000);
// Starting rebuild task
CurrentStep = 2;
Progress = 30;
Progress = 40;
await InvokeAsync(StateHasChanged);
var request = new HttpRequestMessage(HttpMethod.Post, "api/admin/ch/rebuild");
await Task.Delay(2000);
request.Content = JsonContent.Create(
new RequestRebuildDto(NoBuildCache),
null,
SerializationContext.TunedOptions
);
CurrentStep = 3;
Progress = 60;
await InvokeAsync(StateHasChanged);
var response = await HttpClient.SendAsync(
request,
HttpCompletionOption.ResponseHeadersRead
);
await Task.Delay(4000);
await using var responseStream = await response.Content.ReadAsStreamAsync();
using var streamReader = new StreamReader(responseStream);
CurrentStep = 4;
Progress = 80;
await InvokeAsync(StateHasChanged);
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
await Task.Delay(4000);
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);
await Task.Delay(1000);
await AlertService.SuccessAsync(
"Update completed",
"Update successfully completed. Please refresh the page to load new frontend changes"
);
await CloseAsync();
}
}

View File

@@ -1,19 +1,16 @@
@using Moonlight.Frontend.Helpers
@using Moonlight.Frontend.Mappers
@using Moonlight.Frontend.UI.Admin.Components
@using Moonlight.Shared.Http.Requests.Admin.Roles
@using Moonlight.Shared.Http.Requests.Roles
@using Moonlight.Shared.Http.Responses.Admin
@using ShadcnBlazor.Buttons
@using ShadcnBlazor.Dialogs
@using ShadcnBlazor.Extras.Forms
@using ShadcnBlazor.Extras.Toasts
@using ShadcnBlazor.Fields
@using ShadcnBlazor.Extras.Common
@using ShadcnBlazor.Extras.FormHandlers
@using ShadcnBlazor.Inputs
@using ShadcnBlazor.Labels
@inherits ShadcnBlazor.Extras.Dialogs.DialogBase
@inject HttpClient HttpClient
@inject ToastService ToastService
<DialogHeader>
<DialogTitle>
Update @Role.Name
@@ -23,44 +20,50 @@
</DialogDescription>
</DialogHeader>
<EnhancedEditForm Model="Request" OnValidSubmit="OnSubmitAsync">
<FieldGroup>
<FormValidationSummary/>
<DataAnnotationsValidator/>
<FormHandler @ref="FormHandler" Model="Request" OnValidSubmit="OnSubmitAsync">
<div class="flex flex-col gap-6">
<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>
<FormValidationSummary/>
<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>
@code
{
[Parameter] public Func<Task> OnSubmit { get; set; }
[Parameter] public Func<UpdateRoleDto, Task> OnSubmit { get; set; }
[Parameter] public RoleDto Role { get; set; }
private UpdateRoleDto Request;
private List<string> Permissions;
private FormHandler FormHandler;
protected override void OnInitialized()
{
@@ -68,27 +71,15 @@
Permissions = Role.Permissions.ToList();
}
private async Task<bool> OnSubmitAsync(EditContext editContext, ValidationMessageStore validationMessageStore)
private async Task SubmitAsync()
{
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;
}
await ToastService.SuccessAsync("Role update", $"Role {Request.Name} has been successfully updated");
await OnSubmit.Invoke();
private async Task OnSubmitAsync()
{
await OnSubmit.Invoke(Request);
await CloseAsync();
return true;
}
}

View File

@@ -1,19 +1,14 @@
@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 Moonlight.Frontend.Mappers
@using Moonlight.Shared.Http.Requests.Users
@using Moonlight.Shared.Http.Responses.Users
@using ShadcnBlazor.Dialogs
@using ShadcnBlazor.Extras.Forms
@using ShadcnBlazor.Extras.Toasts
@using ShadcnBlazor.Fields
@using ShadcnBlazor.Extras.Common
@using ShadcnBlazor.Extras.FormHandlers
@using ShadcnBlazor.Inputs
@using ShadcnBlazor.Labels
@inherits ShadcnBlazor.Extras.Dialogs.DialogBase
@inject HttpClient HttpClient
@inject ToastService ToastService
<DialogHeader>
<DialogTitle>
Update @User.Username
@@ -23,66 +18,51 @@
</DialogDescription>
</DialogHeader>
<EnhancedEditForm Model="Request" OnValidSubmit="OnSubmitAsync">
<FieldGroup>
<FormValidationSummary/>
<DataAnnotationsValidator/>
<FormHandler @ref="FormHandler" Model="Request" OnValidSubmit="SubmitAsync">
<div class="flex flex-col gap-6">
<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>
<FormValidationSummary/>
<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>
@code
{
[Parameter] public Func<Task> OnCompleted { get; set; }
[Parameter] public Func<UpdateUserDto, Task> OnSubmit { 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<bool> OnSubmitAsync(EditContext editContext, ValidationMessageStore validationMessageStore)
private async Task SubmitAsync()
{
var response = await HttpClient.PatchAsJsonAsync(
$"/api/admin/users/{User.Id}",
Request
);
await OnSubmit.Invoke(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;
}
}

View File

@@ -133,11 +133,7 @@
{
<CardTitle ClassName="text-lg text-primary">Update available</CardTitle>
<CardAction ClassName="self-center">
<Button>
<Slot>
<a href="/admin/system?tab=instance" @attributes="context">Update</a>
</Slot>
</Button>
<Button @onclick="LaunchUpdateModalAsync">Update</Button>
</CardAction>
}
</CardHeader>
@@ -160,4 +156,9 @@
await InvokeAsync(StateHasChanged);
}
private async Task LaunchUpdateModalAsync() => await DialogService.LaunchAsync<UpdateInstanceModal>(onConfigure: model =>
{
model.ShowCloseButton = false;
});
}

View File

@@ -1,11 +1,12 @@
@using LucideBlazor
@using Moonlight.Shared.Http.Requests.ApiKeys
@using Moonlight.Shared.Http.Responses.ApiKeys
@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
@@ -122,8 +123,19 @@
{
await DialogService.LaunchAsync<CreateApiKeyDialog>(parameters =>
{
parameters[nameof(CreateApiKeyDialog.OnSubmit)] = async () =>
parameters[nameof(CreateApiKeyDialog.OnSubmit)] = async (CreateApiKeyDto dto) =>
{
await HttpClient.PostAsJsonAsync(
"/api/admin/apiKeys",
dto,
Constants.SerializerOptions
);
await ToastService.SuccessAsync(
"API Key creation",
$"Successfully created API key {dto.Name}"
);
await Grid.RefreshAsync();
};
});
@@ -134,8 +146,19 @@
await DialogService.LaunchAsync<UpdateApiKeyDialog>(parameters =>
{
parameters[nameof(UpdateApiKeyDialog.Key)] = key;
parameters[nameof(UpdateApiKeyDialog.OnSubmit)] = async () =>
parameters[nameof(UpdateApiKeyDialog.OnSubmit)] = async (UpdateApiKeyDto dto) =>
{
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();
};
});

View File

@@ -44,10 +44,10 @@
</Alert>
</CardContent>
<CardFooter ClassName="justify-end">
<WButton OnClick="DiagnoseAsync" disabled="@(!AccessResult.Succeeded)">
<WButtom OnClick="DiagnoseAsync" disabled="@(!AccessResult.Succeeded)">
<StethoscopeIcon/>
Start diagnostics
</WButton>
</WButtom>
</CardFooter>
</Card>
</div>

View File

@@ -31,13 +31,17 @@
<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 />
@@ -61,12 +65,6 @@
<Moonlight.Frontend.UI.Admin.Views.Sys.Themes.Index />
</TabsContent>
}
@if (InstanceResult.Succeeded && VersionsResult.Succeeded)
{
<TabsContent Value="instance">
<Instance />
</TabsContent>
}
</Tabs>
@code
@@ -79,8 +77,6 @@
private AuthorizationResult ApiKeyAccess;
private AuthorizationResult ThemesAccess;
private AuthorizationResult InstanceResult;
private AuthorizationResult VersionsResult;
protected override async Task OnInitializedAsync()
{
@@ -88,8 +84,6 @@
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)

View File

@@ -1,221 +0,0 @@
@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;
}
}

View File

@@ -3,15 +3,14 @@
@using Microsoft.AspNetCore.Authorization
@using Moonlight.Shared
@using LucideBlazor
@using Moonlight.Frontend.Helpers
@using Moonlight.Frontend.Services
@using Moonlight.Shared.Http.Requests.Admin.Themes
@using Moonlight.Shared.Http.Requests.Themes
@using ShadcnBlazor.Buttons
@using ShadcnBlazor.Labels
@using ShadcnBlazor.Cards
@using ShadcnBlazor.Extras.Editors
@using ShadcnBlazor.Extras.Forms
@using ShadcnBlazor.Extras.FormHandlers
@using ShadcnBlazor.Extras.Toasts
@using ShadcnBlazor.Fields
@using ShadcnBlazor.Inputs
@using ShadcnBlazor.Switches
@@ -22,89 +21,86 @@
@inject ToastService ToastService
@inject FrontendService FrontendService
<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 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>
<Button @onclick="SubmitAsync">
<CheckIcon/>
Continue
</Button>
</div>
</div>
<div class="mt-8">
<Card>
<CardContent>
<FieldGroup>
<FormValidationSummary/>
<DataAnnotationsValidator/>
<div class="mt-8">
<Card>
<CardContent>
<FormHandler @ref="Form" OnValidSubmit="OnSubmitAsync" Model="Request">
<div class="flex flex-col gap-6">
<FieldSet ClassName="grid grid-cols-1 lg:grid-cols-2">
<Field>
<FieldLabel for="themeName">Name</FieldLabel>
<TextInputField
<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
@bind-Value="Request.Name"
id="themeName"
placeholder="My cool theme"/>
</Field>
</div>
<Field>
<FieldLabel for="themeVersion">Version</FieldLabel>
<TextInputField
<div class="col-span-1 grid gap-2">
<Label for="themeVersion">Version</Label>
<InputField
@bind-Value="Request.Version"
id="themeVersion"
Type="text"
placeholder="1.0.0"/>
</Field>
</div>
<Field>
<FieldLabel for="themeAuthor">Author</FieldLabel>
<TextInputField
<div class="col-span-1 grid gap-2">
<Label for="themeAuthor">Author</Label>
<InputField
@bind-Value="Request.Author"
id="themeAuthor"
Type="text"
placeholder="Your name"/>
</Field>
</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>
<div class="col-span-1 grid gap-2">
<Label for="themeAuthor">Is Enabled</Label>
<Switch @bind-Value="Request.IsEnabled" />
</div>
</div>
<FieldLabel for="themeAuthor">CSS Content</FieldLabel>
<FieldContent>
<Editor @ref="Editor" Language="EditorLanguage.Css" InitialValue="@Request.CssContent"/>
</FieldContent>
</Field>
</FieldGroup>
</CardContent>
</Card>
</div>
</EnhancedEditForm>
<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>
@code
{
@@ -113,24 +109,23 @@
CssContent = "/* Define your css here */"
};
private FormHandler Form;
private Editor Editor;
private async Task<bool> OnSubmitAsync(EditContext editContext, ValidationMessageStore validationMessageStore)
private async Task SubmitAsync()
{
Request.CssContent = await Editor.GetValueAsync();
await Form.SubmitAsync();
}
var response = await HttpClient.PostAsJsonAsync(
private async Task OnSubmitAsync()
{
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",
$"Successfully created theme {Request.Name}"
@@ -139,7 +134,5 @@
await FrontendService.ReloadAsync();
Navigation.NavigateTo("/admin/system?tab=themes");
return true;
}
}

View File

@@ -4,7 +4,7 @@
@using Moonlight.Shared
@using Moonlight.Shared.Http.Requests
@using Moonlight.Shared.Http.Responses
@using Moonlight.Shared.Http.Responses.Admin.Themes
@using Moonlight.Shared.Http.Responses.Themes
@using ShadcnBlazor.DataGrids
@using ShadcnBlazor.Dropdowns
@using ShadcnBlazor.Extras.AlertDialogs

View File

@@ -3,18 +3,17 @@
@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.Admin.Themes
@using Moonlight.Shared.Http.Responses.Admin.Themes
@using Moonlight.Shared.Http.Requests.Themes
@using Moonlight.Shared.Http.Responses.Themes
@using ShadcnBlazor.Buttons
@using ShadcnBlazor.Labels
@using ShadcnBlazor.Cards
@using ShadcnBlazor.Extras.Common
@using ShadcnBlazor.Extras.Editors
@using ShadcnBlazor.Extras.Forms
@using ShadcnBlazor.Extras.FormHandlers
@using ShadcnBlazor.Extras.Toasts
@using ShadcnBlazor.Fields
@using ShadcnBlazor.Inputs
@using ShadcnBlazor.Switches
@@ -25,92 +24,88 @@
@inject ToastService ToastService
@inject FrontendService FrontendService
<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 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>
<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/>
<FieldSet ClassName="grid grid-cols-1 lg:grid-cols-2">
<Field>
<FieldLabel for="themeName">Name</FieldLabel>
<TextInputField
<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
@bind-Value="Request.Name"
id="themeName"
placeholder="My cool theme"/>
</Field>
</div>
<Field>
<FieldLabel for="themeVersion">Version</FieldLabel>
<TextInputField
<div class="col-span-1 grid gap-2">
<Label for="themeVersion">Version</Label>
<InputField
@bind-Value="Request.Version"
id="themeVersion"
Type="text"
placeholder="1.0.0"/>
</Field>
</div>
<Field>
<FieldLabel for="themeAuthor">Author</FieldLabel>
<TextInputField
<div class="col-span-1 grid gap-2">
<Label for="themeAuthor">Author</Label>
<InputField
@bind-Value="Request.Author"
id="themeAuthor"
Type="text"
placeholder="Your name"/>
</Field>
</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>
<div class="col-span-1 grid gap-2">
<Label for="themeAuthor">Is Enabled</Label>
<Switch @bind-Value="Request.IsEnabled" />
</div>
</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>
<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>
@code
{
@@ -119,6 +114,7 @@
private UpdateThemeDto Request;
private ThemeDto Theme;
private FormHandler Form;
private Editor Editor;
private async Task LoadAsync(LazyLoader _)
@@ -129,22 +125,20 @@
Request = ThemeMapper.ToUpdate(Theme);
}
private async Task<bool> OnSubmitAsync(EditContext editContext, ValidationMessageStore validationMessageStore)
private async Task SubmitAsync()
{
Request.CssContent = await Editor.GetValueAsync();
await Form.SubmitAsync();
}
var response = await HttpClient.PatchAsJsonAsync(
private async Task OnSubmitAsync()
{
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",
$"Successfully updated theme {Request.Name}"
@@ -153,7 +147,5 @@
await FrontendService.ReloadAsync();
Navigation.NavigateTo("/admin/system?tab=themes");
return true;
}
}

View File

@@ -4,6 +4,7 @@
@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
@@ -133,8 +134,15 @@
{
await DialogService.LaunchAsync<CreateRoleDialog>(parameters =>
{
parameters[nameof(CreateRoleDialog.OnSubmit)] = async Task () =>
parameters[nameof(CreateRoleDialog.OnSubmit)] = async Task (CreateRoleDto request) =>
{
await HttpClient.PostAsJsonAsync(
"api/admin/roles",
request,
Constants.SerializerOptions
);
await ToastService.SuccessAsync("Role creation", $"Role {request.Name} has been successfully created");
await Grid.RefreshAsync();
};
});
@@ -145,8 +153,15 @@
await DialogService.LaunchAsync<UpdateRoleDialog>(parameters =>
{
parameters[nameof(UpdateRoleDialog.Role)] = role;
parameters[nameof(UpdateRoleDialog.OnSubmit)] = async Task () =>
parameters[nameof(UpdateRoleDialog.OnSubmit)] = async Task (UpdateRoleDto request) =>
{
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();
};
});

View File

@@ -10,8 +10,9 @@
@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.Admin.Users
@using Moonlight.Shared.Http.Responses.Users
@using ShadcnBlazor.Extras.Dialogs
@inject HttpClient HttpClient
@@ -131,8 +132,19 @@
{
await DialogService.LaunchAsync<CreateUserDialog>(parameters =>
{
parameters[nameof(CreateUserDialog.OnCompleted)] = async () =>
parameters[nameof(CreateUserDialog.OnSubmit)] = async (CreateUserDto dto) =>
{
await HttpClient.PostAsJsonAsync(
"/api/admin/users",
dto,
Constants.SerializerOptions
);
await ToastService.SuccessAsync(
"User creation",
$"Successfully created user {dto.Username}"
);
await Grid.RefreshAsync();
};
});
@@ -143,8 +155,18 @@
await DialogService.LaunchAsync<UpdateUserDialog>(parameters =>
{
parameters[nameof(UpdateUserDialog.User)] = user;
parameters[nameof(UpdateUserDialog.OnCompleted)] = async () =>
parameters[nameof(CreateUserDialog.OnSubmit)] = async (UpdateUserDto dto) =>
{
await HttpClient.PatchAsJsonAsync(
$"/api/admin/users/{user.Id}",
dto
);
await ToastService.SuccessAsync(
"User update",
$"Successfully updated user {dto.Username}"
);
await Grid.RefreshAsync();
};
});

View File

@@ -2,13 +2,10 @@
@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>
@@ -33,42 +30,33 @@
}
else
{
var uri = new Uri(Navigation.Uri);
if (uri.LocalPath.StartsWith("/setup"))
{
<Setup />
}
else
{
<Authentication/>
}
<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>

View File

@@ -1,7 +1,7 @@
@using Moonlight.Shared.Http.Responses.Admin.Auth
@using ShadcnBlazor.Cards
@using ShadcnBlazor.Cards
@using ShadcnBlazor.Spinners
@using ShadcnBlazor.Buttons
@using Moonlight.Shared.Http.Responses.Auth
@inject HttpClient HttpClient
@inject NavigationManager Navigation

View File

@@ -1,172 +0,0 @@
@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);
}
}

View File

@@ -1,20 +0,0 @@
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
}

View File

@@ -1,15 +0,0 @@
namespace Moonlight.Shared.Http.Requests.Admin.ContainerHelper;
public class RequestRebuildDto
{
public bool NoBuildCache { get; set; }
public RequestRebuildDto()
{
}
public RequestRebuildDto(bool noBuildCache)
{
NoBuildCache = noBuildCache;
}
}

View File

@@ -1,10 +0,0 @@
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; }
}

View File

@@ -1,16 +1,13 @@
using System.ComponentModel.DataAnnotations;
namespace Moonlight.Shared.Http.Requests.Admin.ApiKeys;
namespace Moonlight.Shared.Http.Requests.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; }
}

View File

@@ -1,15 +1,13 @@
using System.ComponentModel.DataAnnotations;
namespace Moonlight.Shared.Http.Requests.Admin.ApiKeys;
namespace Moonlight.Shared.Http.Requests.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; }
}

View File

@@ -1,6 +1,6 @@
using System.ComponentModel.DataAnnotations;
namespace Moonlight.Shared.Http.Requests.Admin.Roles;
namespace Moonlight.Shared.Http.Requests.Roles;
public class CreateRoleDto
{

View File

@@ -1,6 +1,6 @@
using System.ComponentModel.DataAnnotations;
namespace Moonlight.Shared.Http.Requests.Admin.Roles;
namespace Moonlight.Shared.Http.Requests.Roles;
public class UpdateRoleDto
{

View File

@@ -1,20 +0,0 @@
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; }
}

View File

@@ -1,6 +1,6 @@
using System.ComponentModel.DataAnnotations;
namespace Moonlight.Shared.Http.Requests.Admin.Themes;
namespace Moonlight.Shared.Http.Requests.Themes;
public class CreateThemeDto
{

View File

@@ -1,6 +1,6 @@
using System.ComponentModel.DataAnnotations;
namespace Moonlight.Shared.Http.Requests.Admin.Themes;
namespace Moonlight.Shared.Http.Requests.Themes;
public class UpdateThemeDto
{

View File

@@ -1,6 +1,6 @@
using System.ComponentModel.DataAnnotations;
namespace Moonlight.Shared.Http.Requests.Admin.Users;
namespace Moonlight.Shared.Http.Requests.Users;
public class CreateUserDto
{

View File

@@ -1,6 +1,6 @@
using System.ComponentModel.DataAnnotations;
namespace Moonlight.Shared.Http.Requests.Admin.Users;
namespace Moonlight.Shared.Http.Requests.Users;
public class UpdateUserDto
{

View File

@@ -1,3 +0,0 @@
namespace Moonlight.Shared.Http.Responses.Admin.Auth;
public record ClaimDto(string Type, string Value);

View File

@@ -1,3 +0,0 @@
namespace Moonlight.Shared.Http.Responses.Admin;
public record ContainerHelperStatusDto(bool IsEnabled, bool IsReachable);

View File

@@ -1,3 +1,3 @@
namespace Moonlight.Shared.Http.Responses.Admin.ApiKeys;
namespace Moonlight.Shared.Http.Responses.ApiKeys;
public record ApiKeyDto(int Id, string Name, string Description, string[] Permissions, string Key, DateTimeOffset CreatedAt, DateTimeOffset UpdatedAt);

View File

@@ -0,0 +1,3 @@
namespace Moonlight.Shared.Http.Responses.Auth;
public record ClaimDto(string Type, string Value);

View File

@@ -1,3 +1,3 @@
namespace Moonlight.Shared.Http.Responses.Admin.Auth;
namespace Moonlight.Shared.Http.Responses.Auth;
public record SchemeDto(string Name, string DisplayName);

View File

@@ -1,3 +1,3 @@
namespace Moonlight.Shared.Http.Responses.Admin.Frontend;
namespace Moonlight.Shared.Http.Responses.Frontend;
public record FrontendConfigDto(string Name, string? ThemeCss);

View File

@@ -1,10 +0,0 @@
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; }
}

View File

@@ -1,3 +1,3 @@
namespace Moonlight.Shared.Http.Responses.Admin.Themes;
namespace Moonlight.Shared.Http.Responses.Themes;
public record ThemeDto(int Id, string Name, string Author, string Version, string CssContent, bool IsEnabled);

View File

@@ -1,3 +1,3 @@
namespace Moonlight.Shared.Http.Responses.Admin.Users;
namespace Moonlight.Shared.Http.Responses.Users;
public record UserDto(int Id, string Username, string Email, DateTimeOffset CreatedAt, DateTimeOffset UpdatedAt);

View File

@@ -1,17 +1,14 @@
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 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 Moonlight.Shared.Http.Responses;
using Moonlight.Shared.Http.Responses.Admin;
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;
using Moonlight.Shared.Http.Responses.ApiKeys;
using Moonlight.Shared.Http.Responses.Auth;
using Moonlight.Shared.Http.Responses.Themes;
using Moonlight.Shared.Http.Responses.Users;
namespace Moonlight.Shared.Http;
@@ -47,32 +44,8 @@ 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;
}
}
}

View File

@@ -54,6 +54,5 @@ 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)}";
}
}

View File

@@ -37,31 +37,3 @@
# Logging
- "Logging__LogLevel__Default=Information"
- "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"