2 Commits

244 changed files with 2212 additions and 4797 deletions

View File

@@ -34,7 +34,7 @@ jobs:
# Publish frontend
# We need to build it first so the class list files generate
- name: Build project
run: dotnet build Hosts/Moonlight.Frontend.Host --configuration Debug
run: dotnet build Moonlight.Frontend --configuration Debug
- name: Build tailwind styles and extract class list
working-directory: Hosts/Moonlight.Frontend.Host/Styles

4
.gitignore vendored
View File

@@ -400,10 +400,8 @@ FodyWeavers.xsd
# Style builds
**/style.min.css
**/package-lock.json
**/bun.lock
# Secrets
**/.env
**/appsettings.json
**/appsettings.Development.json
**/storage
**/appsettings.Development.json

View File

@@ -1,6 +0,0 @@
<Project>
<ItemGroup>
<!-- Put your plugin references here -->
<!-- E.g. <PackageReference Include="MoonlightServers.Api" Version="2.1.0" /> -->
</ItemGroup>
</Project>

View File

@@ -0,0 +1,10 @@
using MoonCore.PluginFramework;
using Moonlight.Api.Startup;
namespace Moonlight.Api.Host;
[PluginLoader]
public partial class AppStartupLoader : IAppStartup
{
}

View File

@@ -1,13 +1,11 @@
# Base image
FROM git.battlestati.one/moonlight-panel/app_base:moonlight AS base
FROM cgr.dev/chainguard/aspnet-runtime:latest AS base
WORKDIR /app
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
# Install required packages
RUN apt-get update; apt-get install unzip -y; apt-get clean
RUN curl -fsSL https://bun.sh/install | bash
ENV PATH="/root/.bun/bin:${PATH}"
# Build dependencies
RUN apt-get update; apt-get install nodejs npm -y; apt-get clean
# Build options
ARG BUILD_CONFIGURATION=Release
@@ -17,7 +15,7 @@ WORKDIR /src/Hosts/Moonlight.Frontend.Host/Styles
COPY ["Hosts/Moonlight.Frontend.Host/Styles/package.json", "package.json"]
RUN bun install
RUN npm install
# Restore nuget packages
WORKDIR /src
@@ -29,9 +27,6 @@ COPY ["Moonlight.Shared/Moonlight.Shared.csproj", "Moonlight.Shared/"]
COPY ["Hosts/Moonlight.Frontend.Host/Moonlight.Frontend.Host.csproj", "Hosts/Moonlight.Frontend.Host/"]
COPY ["Hosts/Moonlight.Api.Host/Moonlight.Api.Host.csproj", "Hosts/Moonlight.Api.Host/"]
COPY ["Hosts/Moonlight.Frontend.Host/Frontend.props", "Hosts/Moonlight.Frontend.Host/"]
COPY ["Hosts/Moonlight.Api.Host/Api.props", "Hosts/Moonlight.Api.Host/"]
RUN dotnet restore "Hosts/Moonlight.Api.Host/Moonlight.Api.Host.csproj"
RUN dotnet restore "Hosts/Moonlight.Frontend.Host/Moonlight.Frontend.Host.csproj"
@@ -44,7 +39,7 @@ WORKDIR "/src/Hosts/Moonlight.Frontend.Host"
RUN dotnet build "./Moonlight.Frontend.Host.csproj" -c $BUILD_CONFIGURATION -o /app/build-frontend
WORKDIR "/src/Hosts/Moonlight.Frontend.Host/Styles"
RUN bun run build
RUN npm run build
# Build projects
WORKDIR "/src/Hosts/Moonlight.Api.Host"
@@ -72,6 +67,4 @@ WORKDIR /app
COPY --from=publish /app/publish-api .
COPY --from=publish /app/publish-frontend/wwwroot ./wwwroot
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 CMD ["/usr/bin/curl", "-sf", "-o", "/dev/null", "http://localhost:8080/"]
ENTRYPOINT ["dotnet", "Moonlight.Api.Host.dll"]

View File

@@ -7,18 +7,20 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.5">
<PackageReference Include="MoonCore.PluginFramework" Version="1.0.9"/>
<PackageReference Include="MoonCore.PluginFramework.Generator" Version="1.0.3"/>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="10.0.1"/>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="SimplePlugin" Version="1.0.2"/>
<PackageReference Include="SimplePlugin.Abstractions" Version="1.0.2"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Moonlight.Api\Moonlight.Api.csproj"/>
<ProjectReference Include="..\Moonlight.Frontend.Host\Moonlight.Frontend.Host.csproj"/>
<ProjectReference Include="..\Moonlight.Frontend.Host\Moonlight.Frontend.Host.csproj" />
</ItemGroup>
<ItemGroup>
@@ -27,5 +29,4 @@
</Content>
</ItemGroup>
<Import Project="Api.props"/>
</Project>

View File

@@ -1,9 +1,22 @@
using Moonlight.Api;
using SimplePlugin.Generated;
using Moonlight.Api.Host;
var plugins = PluginRegistry
.Modules
.OfType<MoonlightPlugin>()
.ToArray();
var appLoader = new AppStartupLoader();
appLoader.Initialize();
await StartupHandler.RunAsync(args, plugins);
var builder = WebApplication.CreateBuilder(args);
appLoader.PreBuild(builder);
var app = builder.Build();
appLoader.PostBuild(app);
appLoader.PostMiddleware(app);
if (app.Environment.IsDevelopment())
app.UseWebAssemblyDebugging();
app.UseBlazorFrameworkFiles();
app.UseStaticFiles();
await app.RunAsync();

View File

@@ -0,0 +1,10 @@
using MoonCore.PluginFramework;
using Moonlight.Frontend.Startup;
namespace Moonlight.Frontend.Host;
[PluginLoader]
public partial class AppStartupLoader : IAppStartup
{
}

View File

@@ -1,6 +0,0 @@
<Project>
<ItemGroup>
<!-- Put your plugin references here -->
<!-- E.g. <PackageReference Include="MoonlightServers.Frontend" Version="2.1.0" /> -->
</ItemGroup>
</Project>

View File

@@ -12,17 +12,13 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.5" />
<PackageReference Include="Microsoft.DotNet.HotReload.WebAssembly.Browser" Version="10.0.201" />
<PackageReference Include="Microsoft.NET.ILLink.Tasks" Version="10.0.5" />
<PackageReference Include="Microsoft.NET.Sdk.WebAssembly.Pack" Version="10.0.5" />
<PackageReference Include="SimplePlugin" Version="1.0.2"/>
<PackageReference Include="SimplePlugin.Abstractions" Version="1.0.2"/>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.1"/>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.1" PrivateAssets="all"/>
<PackageReference Include="MoonCore.PluginFramework" Version="1.0.9"/>
<PackageReference Include="MoonCore.PluginFramework.Generator" Version="1.0.3"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Moonlight.Frontend\Moonlight.Frontend.csproj"/>
<ProjectReference Include="..\..\Moonlight.Frontend\Moonlight.Frontend.csproj" />
</ItemGroup>
<Import Project="Frontend.props"/>
</Project>

View File

@@ -1,9 +1,15 @@
using Moonlight.Frontend;
using SimplePlugin.Generated;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Moonlight.Frontend.Host;
var plugins = PluginRegistry
.Modules
.OfType<MoonlightPlugin>()
.ToArray();
var appLoader = new AppStartupLoader();
appLoader.Initialize();
await StartupHandler.RunAsync(args, plugins);
var builder = WebAssemblyHostBuilder.CreateDefault(args);
appLoader.PreBuild(builder);
var app = builder.Build();
appLoader.PostBuild(app);
await app.RunAsync();

View File

@@ -15,7 +15,7 @@ export default function extractTailwindClasses(opts = {}) {
},
OnceExit() {
const classArray = Array.from(classSet).sort();
fs.mkdirSync('../../../Moonlight.Frontend/Styles', {recursive: true});
fs.mkdirSync('../../../Moonlight.Frontend/Styles', { recursive: true });
fs.writeFileSync('../../../Moonlight.Frontend/Styles/Moonlight.Frontend.map', classArray.join('\n'));
console.log(`Extracted classes ${classArray.length}`);
}

View File

@@ -1,11 +1,11 @@
@import "tailwindcss";
@import "tw-animate-css";
@import "../bin/ShadcnBlazor/scrollbar.css";
@import "../bin/ShadcnBlazor/default-theme.css";
@import "../../../Moonlight.Frontend/bin/ShadcnBlazor/scrollbar.css";
@import "../../../Moonlight.Frontend/bin/ShadcnBlazor/default-theme.css";
@import "./theme.css";
@source "../bin/ShadcnBlazor/ShadcnBlazor.map";
@source "../../../Moonlight.Frontend/bin/ShadcnBlazor/ShadcnBlazor.map";
@source "../../../Moonlight.Api/**/*.razor";
@source "../../../Moonlight.Api/**/*.cs";
@@ -25,7 +25,6 @@
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}

View File

@@ -1,22 +1,22 @@
<!DOCTYPE html>
<html class="dark" lang="en">
<html lang="en" class="dark">
<head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Moonlight</title>
<base href="/"/>
<link id="webassembly" rel="preload"/>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=fallback" rel="stylesheet"/>
<link href="style.min.css" rel="stylesheet"/>
<base href="/" />
<link rel="preload" id="webassembly" />
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=fallback" />
<link rel="stylesheet" href="style.min.css" />
<script type="importmap"></script>
<script>
window.frontendConfig = {
STYLE_TAG_ID: 'theme-variables',
configuration: {},
applyTheme: function (cssContent) {
applyTheme: function(cssContent) {
// Find or create the style tag
let styleTag = document.getElementById(this.STYLE_TAG_ID);
@@ -30,7 +30,7 @@
styleTag.textContent = cssContent;
},
reloadConfiguration: function () {
reloadConfiguration: function (){
try {
const xhr = new XMLHttpRequest();
xhr.open('GET', '/api/frontend/config', false);
@@ -43,8 +43,8 @@
console.error('Failed to load initial theme:', error);
}
},
getConfiguration: function () {
getConfiguration: function (){
return this.configuration;
},
@@ -61,48 +61,48 @@
</head>
<body class="bg-background text-foreground">
<div id="app">
<div class="h-screen w-full flex items-center justify-center">
<div id="app">
<div class="h-screen w-full flex items-center justify-center">
<div class="flex min-w-0 flex-1 flex-col items-center justify-center gap-3 rounded-lg border-dashed p-6 text-center text-balance md:p-12">
<div class="flex max-w-sm flex-col items-center gap-2 text-center">
<div class="flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0 bg-muted text-foreground size-10 rounded-lg [&_svg:not([class*='size-'])]:size-6">
<div class="flex min-w-0 flex-1 flex-col items-center justify-center gap-3 rounded-lg border-dashed p-6 text-center text-balance md:p-12">
<div class="flex max-w-sm flex-col items-center gap-2 text-center">
<div class="flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0 bg-muted text-foreground size-10 rounded-lg [&_svg:not([class*='size-'])]:size-6">
<svg class="lucide lucide-zap-icon lucide-zap size-6" fill="none" height="24" stroke="currentColor" stroke-linecap="round"
stroke-linejoin="round" stroke-width="2" viewBox="0 0 24 24" width="24"
xmlns="http://www.w3.org/2000/svg">
<path d="M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z"/>
</svg>
</div>
</div>
<div class="text-lg font-medium tracking-tight">
Loading application
</div>
<div class="flex w-full max-w-sm min-w-0 flex-col items-center gap-4 text-sm text-balance">
<div class="bg-primary/20 w-full relative h-2 overflow-hidden rounded-full">
<div class="bg-primary h-full w-[var(--blazor-load-percentage,0)] flex-1 transition-all">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="lucide lucide-zap-icon lucide-zap size-6">
<path d="M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z"/>
</svg>
</div>
</div>
<div class="text-lg font-medium tracking-tight">
Loading application
</div>
<div class="flex w-full max-w-sm min-w-0 flex-col items-center gap-4 text-sm text-balance">
<div class="bg-primary/20 w-full relative h-2 overflow-hidden rounded-full">
<div class="bg-primary h-full w-[var(--blazor-load-percentage,0)] flex-1 transition-all">
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div id="blazor-error-ui">
An unhandled error has occurred.
<a class="reload" href=".">Reload</a>
<span class="dismiss">🗙</span>
</div>
<div id="blazor-error-ui">
An unhandled error has occurred.
<a href="." class="reload">Reload</a>
<span class="dismiss">🗙</span>
</div>
<script defer src="/_content/ShadcnBlazor/interop.js"></script>
<script defer src="/_content/ShadcnBlazor.Extras/interop.js"></script>
<script defer src="/_content/ShadcnBlazor.Extras/codemirror-bundle.js"></script>
<script src="_framework/blazor.webassembly#[.{fingerprint}].js"></script>
<script src="/_content/ShadcnBlazor/interop.js" defer></script>
<script src="/_content/ShadcnBlazor.Extras/interop.js" defer></script>
<script src="/_content/ShadcnBlazor.Extras/codemirror-bundle.js" defer></script>
<script src="_framework/blazor.webassembly#[.{fingerprint}].js"></script>
</body>
</html>

View File

@@ -1,128 +0,0 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Moonlight.Api.Admin.Sys.Settings;
using Moonlight.Api.Infrastructure.Database;
using Moonlight.Api.Infrastructure.Database.Entities;
using Moonlight.Shared;
using Moonlight.Shared.Admin.Setup;
namespace Moonlight.Api.Admin.Setup;
[ApiController]
[Route("api/admin/setup")]
public class SetupController : Controller
{
private const string StateSettingsKey = "Moonlight.Api.Setup.State";
private readonly DatabaseRepository<Role> RolesRepository;
private readonly SettingsService SettingsService;
private readonly DatabaseRepository<User> UsersRepository;
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

@@ -1,7 +0,0 @@
namespace Moonlight.Api.Admin.Sys.ApiKeys;
public class ApiOptions
{
public TimeSpan LookupCacheL1Expiry { get; set; } = TimeSpan.FromSeconds(30);
public TimeSpan LookupCacheL2Expiry { get; set; } = TimeSpan.FromMinutes(3);
}

View File

@@ -1,9 +0,0 @@
using Microsoft.AspNetCore.Authentication;
namespace Moonlight.Api.Admin.Sys.ApiKeys.Scheme;
public class ApiKeySchemeOptions : AuthenticationSchemeOptions
{
public TimeSpan LookupL1CacheTime { get; set; }
public TimeSpan LookupL2CacheTime { get; set; }
}

View File

@@ -1,53 +0,0 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using Moonlight.Shared;
using Moonlight.Shared.Admin.Sys.ContainerHelper;
namespace Moonlight.Api.Admin.Sys.ContainerHelper;
[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

@@ -1,13 +0,0 @@
using System.Diagnostics.CodeAnalysis;
using Moonlight.Shared.Admin.Sys.ContainerHelper;
using Riok.Mapperly.Abstractions;
namespace Moonlight.Api.Admin.Sys.ContainerHelper;
[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(Models.Events.RebuildEventDto rebuildEventDto);
}

View File

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

View File

@@ -1,117 +0,0 @@
using System.Net.Http.Json;
using System.Text.Json;
using Moonlight.Api.Admin.Sys.ContainerHelper.Models;
using Moonlight.Api.Admin.Sys.ContainerHelper.Models.Events;
using Moonlight.Api.Admin.Sys.ContainerHelper.Models.Requests;
namespace Moonlight.Api.Admin.Sys.ContainerHelper;
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.Default.Options
);
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.Default.Options);
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.Default.Options
);
if (response.IsSuccessStatusCode)
return;
var problemDetails =
await response.Content.ReadFromJsonAsync<ProblemDetails>(SerializationContext.Default.Options);
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,18 +0,0 @@
using System.Text.Json.Serialization;
namespace Moonlight.Api.Admin.Sys.ContainerHelper.Models.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.Admin.Sys.ContainerHelper.Models;
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.Admin.Sys.ContainerHelper.Models.Requests;
public record RequestRebuildDto(bool NoBuildCache);

View File

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

View File

@@ -1,15 +0,0 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Moonlight.Api.Admin.Sys.ContainerHelper.Models.Events;
using Moonlight.Api.Admin.Sys.ContainerHelper.Models.Requests;
namespace Moonlight.Api.Admin.Sys.ContainerHelper.Models;
[JsonSerializable(typeof(SetVersionDto))]
[JsonSerializable(typeof(ProblemDetails))]
[JsonSerializable(typeof(RebuildEventDto))]
[JsonSerializable(typeof(RequestRebuildDto))]
[JsonSourceGenerationOptions(JsonSerializerDefaults.Web)]
public partial class SerializationContext : JsonSerializerContext
{
}

View File

@@ -1,17 +0,0 @@
namespace Moonlight.Api.Admin.Sys.Diagnose;
public record DiagnoseResult(
DiagnoseLevel Level,
string Title,
string[] Tags,
string? Message,
string? StackStrace,
string? SolutionUrl,
string? ReportUrl);
public enum DiagnoseLevel
{
Error = 0,
Warning = 1,
Healthy = 2
}

View File

@@ -1,7 +0,0 @@
namespace Moonlight.Api.Admin.Sys.Settings;
public class SettingsOptions
{
public TimeSpan LookupL1CacheTime { get; set; } = TimeSpan.FromMinutes(1);
public TimeSpan LookupL2CacheTime { get; set; } = TimeSpan.FromMinutes(5);
}

View File

@@ -1,83 +0,0 @@
using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Hybrid;
using Microsoft.Extensions.Options;
using Moonlight.Api.Infrastructure.Database;
using Moonlight.Api.Infrastructure.Database.Entities;
namespace Moonlight.Api.Admin.Sys.Settings;
public class SettingsService
{
private const string CacheKey = "Moonlight.Api.SettingsService.{0}";
private readonly HybridCache HybridCache;
private readonly IOptions<SettingsOptions> Options;
private readonly DatabaseRepository<SettingsOption> Repository;
public SettingsService(
DatabaseRepository<SettingsOption> repository,
IOptions<SettingsOptions> options,
HybridCache hybridCache
)
{
Repository = repository;
HybridCache = hybridCache;
Options = options;
}
public async Task<T?> GetValueAsync<T>(string key)
{
var cacheKey = string.Format(CacheKey, key);
var value = await HybridCache.GetOrCreateAsync<string?>(
cacheKey,
async ct =>
{
return await Repository
.Query()
.Where(x => x.Key == key)
.Select(o => o.ValueJson)
.FirstOrDefaultAsync(ct);
},
new HybridCacheEntryOptions
{
LocalCacheExpiration = Options.Value.LookupL1CacheTime,
Expiration = Options.Value.LookupL2CacheTime
}
);
if (string.IsNullOrWhiteSpace(value))
return default;
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);
}
await HybridCache.RemoveAsync(cacheKey);
}
}

View File

@@ -1,47 +0,0 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Moonlight.Api.Shared.Frontend;
using Moonlight.Shared;
using Moonlight.Shared.Admin.Sys.Settings;
namespace Moonlight.Api.Admin.Sys.Settings;
[ApiController]
[Authorize(Policy = Permissions.System.Settings)]
[Route("api/admin/system/settings/whiteLabeling")]
public class WhiteLabelingController : Controller
{
private readonly FrontendService FrontendService;
private readonly SettingsService SettingsService;
public WhiteLabelingController(SettingsService settingsService, FrontendService frontendService)
{
SettingsService = settingsService;
FrontendService = frontendService;
}
[HttpGet]
public async Task<ActionResult<WhiteLabelingDto>> GetAsync()
{
var dto = new WhiteLabelingDto
{
Name = await SettingsService.GetValueAsync<string>(FrontendSettingConstants.Name) ?? "Moonlight"
};
return dto;
}
[HttpPost]
public async Task<ActionResult<WhiteLabelingDto>> PostAsync([FromBody] SetWhiteLabelingDto request)
{
await SettingsService.SetValueAsync(FrontendSettingConstants.Name, request.Name);
await FrontendService.ResetCacheAsync();
var dto = new WhiteLabelingDto
{
Name = await SettingsService.GetValueAsync<string>(FrontendSettingConstants.Name) ?? "Moonlight"
};
return dto;
}
}

View File

@@ -1,12 +0,0 @@
using VYaml.Annotations;
namespace Moonlight.Api.Admin.Sys.Themes;
[YamlObject]
public partial class ThemeTransferModel
{
public string Name { get; set; }
public string Version { get; set; }
public string Author { get; set; }
public string CssContent { get; set; }
}

View File

@@ -1,74 +0,0 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Moonlight.Api.Infrastructure.Database;
using Moonlight.Api.Infrastructure.Database.Entities;
using Moonlight.Shared;
using Moonlight.Shared.Admin.Sys.Themes;
using VYaml.Serialization;
namespace Moonlight.Api.Admin.Sys.Themes;
[ApiController]
[Route("api/admin/themes")]
[Authorize(Policy = Permissions.Themes.View)]
public class TransferController : Controller
{
private readonly DatabaseRepository<Theme> ThemeRepository;
public TransferController(DatabaseRepository<Theme> themeRepository)
{
ThemeRepository = themeRepository;
}
[HttpGet("{id:int}/export")]
public async Task<ActionResult> ExportAsync([FromRoute] int id)
{
var theme = await ThemeRepository
.Query()
.FirstOrDefaultAsync(x => x.Id == id);
if (theme == null)
return Problem("No theme with that id found", statusCode: 404);
var yml = YamlSerializer.Serialize(new ThemeTransferModel
{
Name = theme.Name,
Author = theme.Author,
CssContent = theme.CssContent,
Version = theme.Version
});
return File(yml.ToArray(), "text/yaml", $"{theme.Name}.yml");
}
[HttpPost("import")]
public async Task<ActionResult<ThemeDto>> ImportAsync()
{
var themeToImport = await YamlSerializer.DeserializeAsync<ThemeTransferModel>(Request.Body);
var existingTheme = await ThemeRepository
.Query()
.FirstOrDefaultAsync(x => x.Name == themeToImport.Name && x.Author == themeToImport.Author);
if (existingTheme == null)
{
var finalTheme = await ThemeRepository.AddAsync(new Theme
{
Name = themeToImport.Name,
Author = themeToImport.Author,
CssContent = themeToImport.CssContent,
Version = themeToImport.Version
});
return ThemeMapper.ToDto(finalTheme);
}
existingTheme.CssContent = themeToImport.CssContent;
existingTheme.Version = themeToImport.Version;
await ThemeRepository.UpdateAsync(existingTheme);
return ThemeMapper.ToDto(existingTheme);
}
}

View File

@@ -1,7 +0,0 @@
namespace Moonlight.Api.Admin.Users.Users;
public class UserOptions
{
public TimeSpan ValidationCacheL1Expiry { get; set; } = TimeSpan.FromSeconds(30);
public TimeSpan ValidationCacheL2Expiry { get; set; } = TimeSpan.FromMinutes(3);
}

View File

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

View File

@@ -1,4 +1,4 @@
namespace Moonlight.Api.Infrastructure.Database;
namespace Moonlight.Api.Configuration;
public class DatabaseOptions
{

View File

@@ -1,4 +1,4 @@
namespace Moonlight.Api.Admin.Sys.Versions;
namespace Moonlight.Api.Configuration;
public class FrontendOptions
{

View File

@@ -1,10 +1,9 @@
namespace Moonlight.Api.Infrastructure.Configuration;
namespace Moonlight.Api.Configuration;
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

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

View File

@@ -1,4 +1,4 @@
namespace Moonlight.Api.Admin.Sys.Versions;
namespace Moonlight.Api.Configuration;
public class VersionOptions
{

View File

@@ -1,18 +1,12 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using Moonlight.Api.Infrastructure.Database.Entities;
using Moonlight.Api.Configuration;
using Moonlight.Api.Database.Entities;
namespace Moonlight.Api.Infrastructure.Database;
namespace Moonlight.Api.Database;
public class DataContext : DbContext
{
private readonly IOptions<DatabaseOptions> Options;
public DataContext(IOptions<DatabaseOptions> options)
{
Options = options;
}
public DbSet<User> Users { get; set; }
public DbSet<SettingsOption> SettingsOptions { get; set; }
public DbSet<Role> Roles { get; set; }
@@ -20,29 +14,31 @@ public class DataContext : DbContext
public DbSet<ApiKey> ApiKeys { get; set; }
public DbSet<Theme> Themes { get; set; }
private readonly IOptions<DatabaseOptions> Options;
public DataContext(IOptions<DatabaseOptions> options)
{
Options = options;
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (optionsBuilder.IsConfigured)
return;
optionsBuilder.UseNpgsql(
$"Host={Options.Value.Host};" +
$"Port={Options.Value.Port};" +
$"Username={Options.Value.Username};" +
$"Password={Options.Value.Password};" +
$"Database={Options.Value.Database}",
builder =>
{
builder.MigrationsAssembly(typeof(DataContext).Assembly);
builder.MigrationsHistoryTable("MigrationsHistory", "core");
}
$"Database={Options.Value.Database}"
);
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.HasDefaultSchema("core");
base.OnModelCreating(modelBuilder);
}
}

View File

@@ -1,7 +1,7 @@
using Microsoft.EntityFrameworkCore;
using Moonlight.Api.Infrastructure.Database.Interfaces;
using Moonlight.Api.Database.Interfaces;
namespace Moonlight.Api.Infrastructure.Database;
namespace Moonlight.Api.Database;
public class DatabaseRepository<T> where T : class
{
@@ -14,10 +14,7 @@ public class DatabaseRepository<T> where T : class
Set = DataContext.Set<T>();
}
public IQueryable<T> Query()
{
return Set;
}
public IQueryable<T> Query() => Set;
public async Task<T> AddAsync(T entity)
{
@@ -26,7 +23,7 @@ public class DatabaseRepository<T> where T : class
actionTimestamps.CreatedAt = DateTimeOffset.UtcNow;
actionTimestamps.UpdatedAt = DateTimeOffset.UtcNow;
}
var final = Set.Add(entity);
await DataContext.SaveChangesAsync();
return final.Entity;
@@ -36,7 +33,7 @@ public class DatabaseRepository<T> where T : class
{
if (entity is IActionTimestamps actionTimestamps)
actionTimestamps.UpdatedAt = DateTimeOffset.UtcNow;
Set.Update(entity);
await DataContext.SaveChangesAsync();
}

View File

@@ -0,0 +1,24 @@
using System.ComponentModel.DataAnnotations;
using Moonlight.Api.Database.Interfaces;
namespace Moonlight.Api.Database.Entities;
public class ApiKey : IActionTimestamps
{
public int Id { get; set; }
[MaxLength(30)]
public required string Name { get; set; }
[MaxLength(300)]
public required string Description { get; set; }
public string[] Permissions { get; set; } = [];
[MaxLength(32)]
public string Key { get; set; }
// Action timestamps
public DateTimeOffset CreatedAt { get; set; }
public DateTimeOffset UpdatedAt { get; set; }
}

View File

@@ -1,21 +1,23 @@
using System.ComponentModel.DataAnnotations;
using Moonlight.Api.Infrastructure.Database.Interfaces;
using Moonlight.Api.Database.Interfaces;
namespace Moonlight.Api.Infrastructure.Database.Entities;
namespace Moonlight.Api.Database.Entities;
public class Role : IActionTimestamps
{
public int Id { get; set; }
[MaxLength(30)] public required string Name { get; set; }
[MaxLength(300)] public required string Description { get; set; }
[MaxLength(30)]
public required string Name { get; set; }
[MaxLength(300)]
public required string Description { get; set; }
public string[] Permissions { get; set; } = [];
// Relations
public List<RoleMember> Members { get; set; } = [];
// Action timestamps
public DateTimeOffset CreatedAt { get; set; }
public DateTimeOffset UpdatedAt { get; set; }

View File

@@ -1,6 +1,6 @@
using Moonlight.Api.Infrastructure.Database.Interfaces;
using Moonlight.Api.Database.Interfaces;
namespace Moonlight.Api.Infrastructure.Database.Entities;
namespace Moonlight.Api.Database.Entities;
public class RoleMember : IActionTimestamps
{
@@ -8,7 +8,7 @@ public class RoleMember : IActionTimestamps
public Role Role { get; set; }
public User User { get; set; }
// Action timestamps
public DateTimeOffset CreatedAt { get; set; }
public DateTimeOffset UpdatedAt { get; set; }

View File

@@ -0,0 +1,14 @@
using System.ComponentModel.DataAnnotations;
namespace Moonlight.Api.Database.Entities;
public class SettingsOption
{
public int Id { get; set; }
[MaxLength(256)]
public required string Key { get; set; }
[MaxLength(4096)]
public required string Value { get; set; }
}

View File

@@ -0,0 +1,21 @@
using System.ComponentModel.DataAnnotations;
namespace Moonlight.Api.Database.Entities;
public class Theme
{
public int Id { get; set; }
[MaxLength(30)]
public required string Name { get; set; }
[MaxLength(30)]
public required string Version { get; set; }
[MaxLength(30)]
public required string Author { get; set; }
public bool IsEnabled { get; set; }
[MaxLength(20_000)]
public required string CssContent { get; set; }
}

View File

@@ -1,23 +1,25 @@
using System.ComponentModel.DataAnnotations;
using Moonlight.Api.Infrastructure.Database.Interfaces;
using Moonlight.Api.Database.Interfaces;
namespace Moonlight.Api.Infrastructure.Database.Entities;
namespace Moonlight.Api.Database.Entities;
public class User : IActionTimestamps
{
public int Id { get; set; }
// Base information
[MaxLength(50)] public required string Username { get; set; }
[MaxLength(254)] public required string Email { get; set; }
[MaxLength(50)]
public required string Username { get; set; }
[MaxLength(254)]
public required string Email { get; set; }
// Authentication
public DateTimeOffset InvalidateTimestamp { get; set; }
// Relations
public List<RoleMember> RoleMemberships { get; set; } = [];
// Action timestamps
public DateTimeOffset CreatedAt { get; set; }
public DateTimeOffset UpdatedAt { get; set; }

View File

@@ -1,4 +1,4 @@
namespace Moonlight.Api.Infrastructure.Database.Interfaces;
namespace Moonlight.Api.Database.Interfaces;
internal interface IActionTimestamps
{

View File

@@ -5,7 +5,6 @@ using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Moonlight.Api.Database;
using Moonlight.Api.Infrastructure.Database;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable

View File

@@ -5,7 +5,6 @@ using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Moonlight.Api.Database;
using Moonlight.Api.Infrastructure.Database;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable

View File

@@ -5,7 +5,6 @@ using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Moonlight.Api.Database;
using Moonlight.Api.Infrastructure.Database;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable

View File

@@ -5,7 +5,6 @@ using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Moonlight.Api.Database;
using Moonlight.Api.Infrastructure.Database;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable

View File

@@ -5,7 +5,6 @@ using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Moonlight.Api.Database;
using Moonlight.Api.Infrastructure.Database;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable

View File

@@ -4,7 +4,6 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Moonlight.Api.Database;
using Moonlight.Api.Infrastructure.Database;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
@@ -57,9 +56,6 @@ namespace Moonlight.Api.Database.Migrations
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.Property<DateTimeOffset>("ValidUntil")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.ToTable("ApiKeys", "core");
@@ -140,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

@@ -2,7 +2,7 @@
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Logging.Console;
namespace Moonlight.Api.Infrastructure.Helpers;
namespace Moonlight.Api.Helpers;
public class AppConsoleFormatter : ConsoleFormatter
{
@@ -58,9 +58,7 @@ public class AppConsoleFormatter : ConsoleFormatter
textWriter.WriteLine(logEntry.Exception.ToString());
}
else
{
textWriter.WriteLine();
}
}
private static (string text, string color) GetLevelInfo(LogLevel logLevel)

View File

@@ -1,6 +1,6 @@
using System.Runtime.InteropServices;
namespace Moonlight.Api.Admin.Sys;
namespace Moonlight.Api.Helpers;
public class OsHelper
{
@@ -53,12 +53,17 @@ public class OsHelper
string? version = null;
foreach (var line in lines)
{
if (line.StartsWith("NAME="))
name = line.Substring(5).Trim('"');
else if (line.StartsWith("VERSION_ID="))
version = line.Substring(11).Trim('"');
}
if (!string.IsNullOrEmpty(name)) return string.IsNullOrEmpty(version) ? name : $"{name} {version}";
if (!string.IsNullOrEmpty(name))
{
return string.IsNullOrEmpty(version) ? name : $"{name} {version}";
}
}
//If for some weird reason it still uses lsb release
@@ -69,12 +74,17 @@ public class OsHelper
string? version = null;
foreach (var line in lines)
{
if (line.StartsWith("DISTRIB_ID="))
name = line.Substring(11);
else if (line.StartsWith("DISTRIB_RELEASE="))
version = line.Substring(16);
}
if (!string.IsNullOrEmpty(name)) return string.IsNullOrEmpty(version) ? name : $"{name} {version}";
if (!string.IsNullOrEmpty(name))
{
return string.IsNullOrEmpty(version) ? name : $"{name} {version}";
}
}
}
catch

View File

@@ -1,28 +1,27 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Hybrid;
using Moonlight.Api.Admin.Sys.ApiKeys.Scheme;
using Moonlight.Api.Infrastructure.Database;
using Moonlight.Api.Infrastructure.Database.Entities;
using Moonlight.Api.Database;
using Moonlight.Api.Database.Entities;
using Moonlight.Api.Mappers;
using Moonlight.Shared;
using Moonlight.Shared.Admin.Sys.ApiKeys;
using Moonlight.Shared.Shared;
using Moonlight.Shared.Http.Requests;
using Moonlight.Shared.Http.Requests.ApiKeys;
using Moonlight.Shared.Http.Responses;
using Moonlight.Shared.Http.Responses.ApiKeys;
namespace Moonlight.Api.Admin.Sys.ApiKeys;
namespace Moonlight.Api.Http.Controllers.Admin;
[Authorize]
[ApiController]
[Route("api/admin/apiKeys")]
public class ApiKeyController : Controller
{
private readonly HybridCache HybridCache;
private readonly DatabaseRepository<ApiKey> KeyRepository;
public ApiKeyController(DatabaseRepository<ApiKey> keyRepository, HybridCache hybridCache)
public ApiKeyController(DatabaseRepository<ApiKey> keyRepository)
{
KeyRepository = keyRepository;
HybridCache = hybridCache;
}
[HttpGet]
@@ -45,7 +44,9 @@ public class ApiKeyController : Controller
// Filters
if (filterOptions != null)
{
foreach (var filterOption in filterOptions.Filters)
{
query = filterOption.Key switch
{
nameof(ApiKey.Name) =>
@@ -56,6 +57,8 @@ public class ApiKeyController : Controller
_ => query
};
}
}
// Pagination
var data = await query
@@ -89,7 +92,7 @@ public class ApiKeyController : Controller
public async Task<ActionResult<ApiKeyDto>> CreateAsync([FromBody] CreateApiKeyDto request)
{
var apiKey = ApiKeyMapper.ToEntity(request);
apiKey.Key = Guid.NewGuid().ToString("N").Substring(0, 32);
var finalKey = await KeyRepository.AddAsync(apiKey);
@@ -111,8 +114,6 @@ public class ApiKeyController : Controller
ApiKeyMapper.Merge(apiKey, request);
await KeyRepository.UpdateAsync(apiKey);
await HybridCache.RemoveAsync(string.Format(ApiKeySchemeHandler.CacheKeyFormat, apiKey.Key));
return ApiKeyMapper.ToDto(apiKey);
}
@@ -128,9 +129,6 @@ public class ApiKeyController : Controller
return Problem("No API key with this id found", statusCode: 404);
await KeyRepository.RemoveAsync(apiKey);
await HybridCache.RemoveAsync(string.Format(ApiKeySchemeHandler.CacheKeyFormat, apiKey.Key));
return NoContent();
}
}

View File

@@ -1,9 +1,11 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Moonlight.Api.Mappers;
using Moonlight.Api.Services;
using Moonlight.Shared;
using Moonlight.Shared.Admin.Sys.Diagnose;
using Moonlight.Shared.Http.Responses.Admin;
namespace Moonlight.Api.Admin.Sys.Diagnose;
namespace Moonlight.Api.Http.Controllers.Admin;
[ApiController]
[Authorize(Policy = Permissions.System.Diagnose)]

View File

@@ -1,23 +1,23 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Moonlight.Api.Admin.Users.Users;
using Moonlight.Api.Infrastructure.Database;
using Moonlight.Api.Infrastructure.Database.Entities;
using Moonlight.Api.Database;
using Moonlight.Api.Database.Entities;
using Moonlight.Api.Mappers;
using Moonlight.Shared;
using Moonlight.Shared.Admin.Users.Users;
using Moonlight.Shared.Shared;
using Moonlight.Shared.Http.Responses;
using Moonlight.Shared.Http.Responses.Users;
namespace Moonlight.Api.Admin.Users.Roles;
namespace Moonlight.Api.Http.Controllers.Admin;
[ApiController]
[Authorize(Policy = Permissions.Roles.Members)]
[Route("api/admin/roles/{roleId:int}/members")]
public class RoleMembersController : Controller
{
private readonly DatabaseRepository<RoleMember> RoleMembersRepository;
private readonly DatabaseRepository<Role> RolesRepository;
private readonly DatabaseRepository<User> UsersRepository;
private readonly DatabaseRepository<Role> RolesRepository;
private readonly DatabaseRepository<RoleMember> RoleMembersRepository;
public RoleMembersController(
DatabaseRepository<User> usersRepository,
@@ -53,16 +53,19 @@ public class RoleMembersController : Controller
// Filtering
if (!string.IsNullOrWhiteSpace(searchTerm))
{
query = query.Where(x =>
EF.Functions.ILike(x.Username, $"%{searchTerm}%") ||
EF.Functions.ILike(x.Email, $"%{searchTerm}%")
);
}
// Pagination
var items = UserMapper.ProjectToDto(query
.OrderBy(x => x.Id)
.Skip(startIndex)
.Take(length))
var items = query
.OrderBy(x => x.Id)
.Skip(startIndex)
.Take(length)
.ProjectToDto()
.ToArray();
var totalCount = await query.CountAsync();
@@ -92,16 +95,19 @@ public class RoleMembersController : Controller
// Filtering
if (!string.IsNullOrWhiteSpace(searchTerm))
{
query = query.Where(x =>
EF.Functions.ILike(x.Username, $"%{searchTerm}%") ||
EF.Functions.ILike(x.Email, $"%{searchTerm}%")
);
}
// Pagination
var items = UserMapper.ProjectToDto(query
.OrderBy(x => x.Id)
.Skip(startIndex)
.Take(length))
var items = query
.OrderBy(x => x.Id)
.Skip(startIndex)
.Take(length)
.ProjectToDto()
.ToArray();
var totalCount = await query.CountAsync();

View File

@@ -1,13 +1,16 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Moonlight.Api.Infrastructure.Database;
using Moonlight.Api.Infrastructure.Database.Entities;
using Moonlight.Api.Database;
using Moonlight.Api.Database.Entities;
using Moonlight.Api.Mappers;
using Moonlight.Shared;
using Moonlight.Shared.Admin.Users.Roles;
using Moonlight.Shared.Shared;
using Moonlight.Shared.Http.Requests;
using Moonlight.Shared.Http.Requests.Roles;
using Moonlight.Shared.Http.Responses;
using Moonlight.Shared.Http.Responses.Admin;
namespace Moonlight.Api.Admin.Users.Roles;
namespace Moonlight.Api.Http.Controllers.Admin;
[ApiController]
[Route("api/admin/roles")]
@@ -36,13 +39,15 @@ public class RolesController : Controller
return Problem("Invalid length specified");
// Query building
var query = RoleRepository
.Query();
// Filters
if (filterOptions != null)
{
foreach (var filterOption in filterOptions.Filters)
{
query = filterOption.Key switch
{
nameof(Role.Name) =>
@@ -50,6 +55,8 @@ public class RolesController : Controller
_ => query
};
}
}
// Pagination
var data = await query
@@ -99,7 +106,7 @@ public class RolesController : Controller
if (role == null)
return Problem("No role with this id found", statusCode: 404);
RoleMapper.Merge(role, request);
await RoleRepository.UpdateAsync(role);

View File

@@ -1,9 +1,10 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Moonlight.Api.Services;
using Moonlight.Shared;
using Moonlight.Shared.Admin.Sys;
using Moonlight.Shared.Http.Responses.Admin;
namespace Moonlight.Api.Admin.Sys;
namespace Moonlight.Api.Http.Controllers.Admin;
[ApiController]
[Route("api/admin/system")]

View File

@@ -1,21 +1,24 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Moonlight.Api.Infrastructure.Database;
using Moonlight.Api.Infrastructure.Database.Entities;
using Moonlight.Api.Shared.Frontend;
using Moonlight.Api.Database;
using Moonlight.Api.Database.Entities;
using Moonlight.Api.Mappers;
using Moonlight.Api.Services;
using Moonlight.Shared;
using Moonlight.Shared.Admin.Sys.Themes;
using Moonlight.Shared.Shared;
using Moonlight.Shared.Http.Requests;
using Moonlight.Shared.Http.Requests.Themes;
using Moonlight.Shared.Http.Responses;
using Moonlight.Shared.Http.Responses.Themes;
namespace Moonlight.Api.Admin.Sys.Themes;
namespace Moonlight.Api.Http.Controllers.Admin;
[ApiController]
[Route("api/admin/themes")]
public class ThemesController : Controller
{
private readonly FrontendService FrontendService;
private readonly DatabaseRepository<Theme> ThemeRepository;
private readonly FrontendService FrontendService;
public ThemesController(DatabaseRepository<Theme> themeRepository, FrontendService frontendService)
{
@@ -45,7 +48,9 @@ public class ThemesController : Controller
// Filters
if (filterOptions != null)
{
foreach (var filterOption in filterOptions.Filters)
{
query = filterOption.Key switch
{
nameof(Theme.Name) =>
@@ -59,6 +64,8 @@ public class ThemesController : Controller
_ => query
};
}
}
// Pagination
var data = await query
@@ -109,7 +116,7 @@ public class ThemesController : Controller
if (theme == null)
return Problem("No theme with this id found", statusCode: 404);
ThemeMapper.Merge(theme, request);
await ThemeRepository.UpdateAsync(theme);
@@ -130,9 +137,9 @@ public class ThemesController : Controller
return Problem("No theme with this id found", statusCode: 404);
await ThemeRepository.RemoveAsync(theme);
await FrontendService.ResetCacheAsync();
return NoContent();
}
}

View File

@@ -1,19 +1,21 @@
using System.Collections.Frozen;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Moonlight.Api.Infrastructure.Database;
using Moonlight.Api.Infrastructure.Database.Entities;
using Moonlight.Api.Database;
using Moonlight.Api.Database.Entities;
using Moonlight.Api.Services;
using Moonlight.Shared;
namespace Moonlight.Api.Admin.Users.Users;
namespace Moonlight.Api.Http.Controllers.Admin.Users;
[ApiController]
[Route("api/admin/users")]
[Authorize(Policy = Permissions.Users.Delete)]
public class UserDeletionController : Controller
{
private readonly DatabaseRepository<User> Repository;
private readonly UserDeletionService UserDeletionService;
private readonly DatabaseRepository<User> Repository;
public UserDeletionController(UserDeletionService userDeletionService, DatabaseRepository<User> repository)
{
@@ -37,7 +39,7 @@ public class UserDeletionController : Controller
{
return ValidationProblem(
new ValidationProblemDetails(
new Dictionary<string, string[]>
new Dictionary<string, string[]>()
{
{
string.Empty,
@@ -47,7 +49,7 @@ public class UserDeletionController : Controller
)
);
}
await UserDeletionService.DeleteAsync(id);
return NoContent();
}

View File

@@ -1,11 +1,12 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Moonlight.Api.Infrastructure.Database;
using Moonlight.Api.Infrastructure.Database.Entities;
using Moonlight.Api.Database;
using Moonlight.Api.Database.Entities;
using Moonlight.Api.Services;
using Moonlight.Shared;
namespace Moonlight.Api.Admin.Users.Users;
namespace Moonlight.Api.Http.Controllers.Admin.Users;
[ApiController]
[Route("api/admin/users/{id:int}/logout")]

View File

@@ -1,13 +1,16 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Moonlight.Api.Infrastructure.Database;
using Moonlight.Api.Infrastructure.Database.Entities;
using Moonlight.Api.Database;
using Moonlight.Api.Database.Entities;
using Moonlight.Api.Mappers;
using Moonlight.Shared;
using Moonlight.Shared.Admin.Users.Users;
using Moonlight.Shared.Shared;
using Moonlight.Shared.Http.Requests;
using Moonlight.Shared.Http.Requests.Users;
using Moonlight.Shared.Http.Responses;
using Moonlight.Shared.Http.Responses.Users;
namespace Moonlight.Api.Admin.Users.Users;
namespace Moonlight.Api.Http.Controllers.Admin.Users;
[Authorize]
[ApiController]
@@ -37,7 +40,7 @@ public class UsersController : Controller
return Problem("Invalid length specified");
// Query building
var query = UserRepository
.Query();
@@ -48,10 +51,10 @@ public class UsersController : Controller
{
query = filterOption.Key switch
{
nameof(Infrastructure.Database.Entities.User.Email) =>
nameof(Database.Entities.User.Email) =>
query.Where(user => EF.Functions.ILike(user.Email, $"%{filterOption.Value}%")),
nameof(Infrastructure.Database.Entities.User.Username) =>
nameof(Database.Entities.User.Username) =>
query.Where(user => EF.Functions.ILike(user.Username, $"%{filterOption.Value}%")),
_ => query

View File

@@ -1,9 +1,11 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Moonlight.Api.Mappers;
using Moonlight.Api.Services;
using Moonlight.Shared;
using Moonlight.Shared.Admin.Sys.Versions;
using Moonlight.Shared.Http.Responses.Admin;
namespace Moonlight.Api.Admin.Sys.Versions;
namespace Moonlight.Api.Http.Controllers.Admin;
[ApiController]
[Route("api/admin/versions")]
@@ -35,8 +37,8 @@ public class VersionsController : Controller
public async Task<ActionResult<VersionDto>> GetLatestAsync()
{
var version = await VersionService.GetLatestVersionAsync();
if (version == null)
if(version == null)
return Problem("Unable to retrieve latest version", statusCode: 404);
return VersionMapper.ToDto(version);

View File

@@ -1,9 +1,9 @@
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Moonlight.Shared.Shared.Auth;
using Moonlight.Shared.Http.Responses.Auth;
namespace Moonlight.Api.Shared.Auth;
namespace Moonlight.Api.Http.Controllers;
[ApiController]
[Route("api/auth")]
@@ -35,7 +35,7 @@ public class AuthController : Controller
if (scheme == null || string.IsNullOrWhiteSpace(scheme.DisplayName))
return Problem("Invalid authentication scheme name", statusCode: 400);
return Challenge(new AuthenticationProperties
return Challenge(new AuthenticationProperties()
{
RedirectUri = "/"
}, scheme.Name);
@@ -56,7 +56,7 @@ public class AuthController : Controller
public Task<ActionResult> LogoutAsync()
{
return Task.FromResult<ActionResult>(
SignOut(new AuthenticationProperties
SignOut(new AuthenticationProperties()
{
RedirectUri = "/"
})

View File

@@ -1,7 +1,9 @@
using Microsoft.AspNetCore.Mvc;
using Moonlight.Shared.Shared.Frontend;
using Moonlight.Api.Mappers;
using Moonlight.Api.Services;
using Moonlight.Shared.Http.Responses.Frontend;
namespace Moonlight.Api.Shared.Frontend;
namespace Moonlight.Api.Http.Controllers;
[ApiController]
[Route("api/frontend")]

View File

@@ -2,31 +2,30 @@ using System.Security.Claims;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Authentication;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Hybrid;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Moonlight.Api.Infrastructure.Database;
using Moonlight.Api.Infrastructure.Database.Entities;
using Moonlight.Api.Database;
using Moonlight.Api.Database.Entities;
using Moonlight.Shared;
namespace Moonlight.Api.Admin.Sys.ApiKeys.Scheme;
namespace Moonlight.Api.Implementations.ApiKeyScheme;
public class ApiKeySchemeHandler : AuthenticationHandler<ApiKeySchemeOptions>
{
public const string CacheKeyFormat = $"Moonlight.Api.{nameof(ApiKeySchemeHandler)}.{{0}}";
private readonly DatabaseRepository<ApiKey> ApiKeyRepository;
private readonly HybridCache HybridCache;
private readonly IMemoryCache MemoryCache;
public ApiKeySchemeHandler(
IOptionsMonitor<ApiKeySchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
DatabaseRepository<ApiKey> apiKeyRepository,
HybridCache hybridCache
IMemoryCache memoryCache
) : base(options, logger, encoder)
{
ApiKeyRepository = apiKeyRepository;
HybridCache = hybridCache;
MemoryCache = memoryCache;
}
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
@@ -39,30 +38,24 @@ public class ApiKeySchemeHandler : AuthenticationHandler<ApiKeySchemeOptions>
if (authHeaderValue.Length > 32)
return AuthenticateResult.Fail("Invalid api key specified");
var cacheKey = string.Format(CacheKeyFormat, authHeaderValue);
if (!MemoryCache.TryGetValue<ApiKeySession>(authHeaderValue, out var apiKey))
{
apiKey = await ApiKeyRepository
.Query()
.Where(x => x.Key == authHeaderValue)
.Select(x => new ApiKeySession(x.Permissions))
.FirstOrDefaultAsync();
var apiKey = await HybridCache.GetOrCreateAsync<ApiKeySession?>(
cacheKey,
async ct =>
{
return await ApiKeyRepository
.Query()
.Where(x => x.Key == authHeaderValue)
.Select(x => new ApiKeySession(x.Permissions, x.ValidUntil))
.FirstOrDefaultAsync(ct);
},
new HybridCacheEntryOptions
{
LocalCacheExpiration = Options.LookupL1CacheTime,
Expiration = Options.LookupL2CacheTime
}
);
if (apiKey == null)
return AuthenticateResult.Fail("Invalid api key specified");
if (apiKey == null)
return AuthenticateResult.Fail("Invalid api key specified");
if (DateTimeOffset.UtcNow > apiKey.ValidUntil)
return AuthenticateResult.Fail("Api key expired");
MemoryCache.Set(authHeaderValue, apiKey, Options.LookupCacheTime);
}
else
{
if (apiKey == null)
return AuthenticateResult.Fail("Invalid api key specified");
}
return AuthenticateResult.Success(new AuthenticationTicket(
new ClaimsPrincipal(
@@ -74,5 +67,5 @@ public class ApiKeySchemeHandler : AuthenticationHandler<ApiKeySchemeOptions>
));
}
private record ApiKeySession(string[] Permissions, DateTimeOffset ValidUntil);
private record ApiKeySession(string[] Permissions);
}

View File

@@ -0,0 +1,8 @@
using Microsoft.AspNetCore.Authentication;
namespace Moonlight.Api.Implementations.ApiKeyScheme;
public class ApiKeySchemeOptions : AuthenticationSchemeOptions
{
public TimeSpan LookupCacheTime { get; set; }
}

View File

@@ -1,7 +1,7 @@
using Microsoft.AspNetCore.Authorization;
using Moonlight.Shared;
namespace Moonlight.Api.Infrastructure.Implementations;
namespace Moonlight.Api.Implementations;
public class PermissionAuthorizationHandler : AuthorizationHandler<PermissionRequirement>
{

View File

@@ -2,22 +2,22 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Options;
using Moonlight.Shared;
namespace Moonlight.Api.Infrastructure.Implementations;
namespace Moonlight.Api.Implementations;
public class PermissionPolicyProvider : IAuthorizationPolicyProvider
{
private readonly DefaultAuthorizationPolicyProvider FallbackProvider;
public PermissionPolicyProvider(IOptions<AuthorizationOptions> options)
{
FallbackProvider = new DefaultAuthorizationPolicyProvider(options);
}
public async Task<AuthorizationPolicy?> GetPolicyAsync(string policyName)
{
if (!policyName.StartsWith(Permissions.Prefix, StringComparison.OrdinalIgnoreCase))
return await FallbackProvider.GetPolicyAsync(policyName);
var policy = new AuthorizationPolicyBuilder();
policy.AddRequirements(new PermissionRequirement(policyName));
@@ -25,22 +25,18 @@ public class PermissionPolicyProvider : IAuthorizationPolicyProvider
}
public Task<AuthorizationPolicy> GetDefaultPolicyAsync()
{
return FallbackProvider.GetDefaultPolicyAsync();
}
=> FallbackProvider.GetDefaultPolicyAsync();
public Task<AuthorizationPolicy?> GetFallbackPolicyAsync()
{
return FallbackProvider.GetFallbackPolicyAsync();
}
=> FallbackProvider.GetFallbackPolicyAsync();
}
public class PermissionRequirement : IAuthorizationRequirement
{
public string Identifier { get; }
public PermissionRequirement(string identifier)
{
Identifier = identifier;
}
public string Identifier { get; }
}

View File

@@ -1,8 +1,8 @@
using Moonlight.Api.Admin.Sys;
using Moonlight.Api.Admin.Sys.Diagnose;
using Moonlight.Api.Infrastructure.Hooks;
using Moonlight.Api.Interfaces;
using Moonlight.Api.Models;
using Moonlight.Api.Services;
namespace Moonlight.Api.Infrastructure.Implementations;
namespace Moonlight.Api.Implementations;
public sealed class UpdateDiagnoseProvider : IDiagnoseProvider
{

View File

@@ -1,6 +0,0 @@
namespace Moonlight.Api.Infrastructure.Configuration;
public class CacheOptions
{
public bool EnableLayer2 { get; set; }
}

View File

@@ -1,7 +0,0 @@
namespace Moonlight.Api.Infrastructure.Configuration;
public class RedisOptions
{
public bool Enable { get; set; }
public string ConnectionString { get; set; }
}

View File

@@ -1,22 +0,0 @@
using System.ComponentModel.DataAnnotations;
using Moonlight.Api.Infrastructure.Database.Interfaces;
namespace Moonlight.Api.Infrastructure.Database.Entities;
public class ApiKey : IActionTimestamps
{
public int Id { get; set; }
[MaxLength(30)] public required string Name { get; set; }
[MaxLength(300)] public required string Description { get; set; }
public string[] Permissions { get; set; } = [];
public DateTimeOffset ValidUntil { get; set; }
[MaxLength(32)] public string Key { get; set; }
// Action timestamps
public DateTimeOffset CreatedAt { get; set; }
public DateTimeOffset UpdatedAt { get; set; }
}

View File

@@ -1,15 +0,0 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Moonlight.Api.Infrastructure.Database.Entities;
public class SettingsOption
{
public int Id { get; set; }
[MaxLength(256)] public required string Key { get; set; }
[MaxLength(4096)]
[Column(TypeName = "jsonb")]
public required string ValueJson { get; set; }
}

View File

@@ -1,18 +0,0 @@
using System.ComponentModel.DataAnnotations;
namespace Moonlight.Api.Infrastructure.Database.Entities;
public class Theme
{
public int Id { get; set; }
[MaxLength(30)] public required string Name { get; set; }
[MaxLength(30)] public required string Version { get; set; }
[MaxLength(30)] public required string Author { get; set; }
public bool IsEnabled { get; set; }
[MaxLength(20_000)] public required string CssContent { get; set; }
}

View File

@@ -1,252 +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 Moonlight.Api.Infrastructure.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

@@ -1,255 +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 Moonlight.Api.Infrastructure.Database;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Moonlight.Api.Database.Migrations
{
[DbContext(typeof(DataContext))]
[Migration("20260209114238_AddedValidUntilToApiKeys")]
partial class AddedValidUntilToApiKeys
{
/// <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.Property<DateTimeOffset>("ValidUntil")
.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,32 +0,0 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Moonlight.Api.Database.Migrations
{
/// <inheritdoc />
public partial class AddedValidUntilToApiKeys : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<DateTimeOffset>(
name: "ValidUntil",
schema: "core",
table: "ApiKeys",
type: "timestamp with time zone",
nullable: false,
defaultValue: new DateTimeOffset(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)));
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "ValidUntil",
schema: "core",
table: "ApiKeys");
}
}
}

View File

@@ -1,8 +0,0 @@
using Moonlight.Api.Infrastructure.Database.Entities;
namespace Moonlight.Api.Infrastructure.Hooks;
public interface IUserLogoutHook
{
public Task ExecuteAsync(User user);
}

View File

@@ -1,6 +1,6 @@
using Moonlight.Api.Admin.Sys.Diagnose;
using Moonlight.Api.Models;
namespace Moonlight.Api.Infrastructure.Hooks;
namespace Moonlight.Api.Interfaces;
public interface IDiagnoseProvider
{

View File

@@ -1,12 +1,12 @@
using System.Security.Claims;
using Moonlight.Api.Infrastructure.Database.Entities;
using Moonlight.Api.Database.Entities;
namespace Moonlight.Api.Infrastructure.Hooks;
namespace Moonlight.Api.Interfaces;
public interface IUserAuthHook
{
public Task<bool> SyncAsync(ClaimsPrincipal principal, User trackedUser);
// Every implementation of this function should execute as fast as possible
// as this directly impacts every api call
public Task<bool> ValidateAsync(ClaimsPrincipal principal, int userId);

View File

@@ -1,6 +1,6 @@
using Moonlight.Api.Infrastructure.Database.Entities;
using Moonlight.Api.Database.Entities;
namespace Moonlight.Api.Infrastructure.Hooks;
namespace Moonlight.Api.Interfaces;
public interface IUserDeletionHook
{

View File

@@ -0,0 +1,8 @@
using Moonlight.Api.Database.Entities;
namespace Moonlight.Api.Interfaces;
public interface IUserLogoutHook
{
public Task ExecuteAsync(User user);
}

View File

@@ -1,9 +1,10 @@
using System.Diagnostics.CodeAnalysis;
using Moonlight.Api.Infrastructure.Database.Entities;
using Moonlight.Shared.Admin.Sys.ApiKeys;
using Moonlight.Api.Database.Entities;
using Moonlight.Shared.Http.Requests.ApiKeys;
using Moonlight.Shared.Http.Responses.ApiKeys;
using Riok.Mapperly.Abstractions;
namespace Moonlight.Api.Admin.Sys.ApiKeys;
namespace Moonlight.Api.Mappers;
[Mapper]
[SuppressMessage("Mapper", "RMG020:No members are mapped in an object mapping")]

View File

@@ -1,8 +1,9 @@
using System.Diagnostics.CodeAnalysis;
using Moonlight.Shared.Admin.Sys.Diagnose;
using Moonlight.Api.Models;
using Moonlight.Shared.Http.Responses.Admin;
using Riok.Mapperly.Abstractions;
namespace Moonlight.Api.Admin.Sys.Diagnose;
namespace Moonlight.Api.Mappers;
[Mapper]
[SuppressMessage("Mapper", "RMG020:No members are mapped in an object mapping")]

View File

@@ -1,8 +1,9 @@
using System.Diagnostics.CodeAnalysis;
using Moonlight.Shared.Shared.Frontend;
using Moonlight.Api.Models;
using Moonlight.Shared.Http.Responses.Frontend;
using Riok.Mapperly.Abstractions;
namespace Moonlight.Api.Shared.Frontend;
namespace Moonlight.Api.Mappers;
[Mapper]
[SuppressMessage("Mapper", "RMG020:No members are mapped in an object mapping")]

View File

@@ -1,9 +1,10 @@
using System.Diagnostics.CodeAnalysis;
using Moonlight.Api.Infrastructure.Database.Entities;
using Moonlight.Shared.Admin.Users.Roles;
using Moonlight.Api.Database.Entities;
using Moonlight.Shared.Http.Requests.Roles;
using Moonlight.Shared.Http.Responses.Admin;
using Riok.Mapperly.Abstractions;
namespace Moonlight.Api.Admin.Users.Roles;
namespace Moonlight.Api.Mappers;
[Mapper]
[SuppressMessage("Mapper", "RMG020:Source member is not mapped to any target member")]
@@ -12,7 +13,6 @@ public static partial class RoleMapper
{
[MapProperty([nameof(Role.Members), nameof(Role.Members.Count)], nameof(RoleDto.MemberCount))]
public static partial RoleDto ToDto(Role role);
public static partial Role ToEntity(CreateRoleDto request);
public static partial void Merge([MappingTarget] Role role, UpdateRoleDto request);

View File

@@ -1,9 +1,10 @@
using System.Diagnostics.CodeAnalysis;
using Moonlight.Api.Infrastructure.Database.Entities;
using Moonlight.Shared.Admin.Sys.Themes;
using Moonlight.Api.Database.Entities;
using Moonlight.Shared.Http.Requests.Themes;
using Moonlight.Shared.Http.Responses.Themes;
using Riok.Mapperly.Abstractions;
namespace Moonlight.Api.Admin.Sys.Themes;
namespace Moonlight.Api.Mappers;
[Mapper]
[SuppressMessage("Mapper", "RMG020:No members are mapped in an object mapping")]

View File

@@ -1,9 +1,10 @@
using System.Diagnostics.CodeAnalysis;
using Moonlight.Api.Infrastructure.Database.Entities;
using Moonlight.Shared.Admin.Users.Users;
using Riok.Mapperly.Abstractions;
using Moonlight.Shared.Http.Requests.Users;
using Moonlight.Shared.Http.Responses.Users;
using Moonlight.Api.Database.Entities;
namespace Moonlight.Api.Admin.Users.Users;
namespace Moonlight.Api.Mappers;
[Mapper]
[SuppressMessage("Mapper", "RMG020:No members are mapped in an object mapping")]

View File

@@ -1,8 +1,9 @@
using System.Diagnostics.CodeAnalysis;
using Moonlight.Shared.Admin.Sys.Versions;
using Moonlight.Api.Models;
using Moonlight.Shared.Http.Responses.Admin;
using Riok.Mapperly.Abstractions;
namespace Moonlight.Api.Admin.Sys.Versions;
namespace Moonlight.Api.Mappers;
[Mapper]
[SuppressMessage("Mapper", "RMG020:Source member is not mapped to any target member")]

View File

@@ -0,0 +1,10 @@
namespace Moonlight.Api.Models;
public record DiagnoseResult(DiagnoseLevel Level, string Title, string[] Tags, string? Message, string? StackStrace, string? SolutionUrl, string? ReportUrl);
public enum DiagnoseLevel
{
Error = 0,
Warning = 1,
Healthy = 2
}

Some files were not shown because too many files have changed in this diff Show More