Refactored project to module structure

This commit is contained in:
2026-03-12 22:50:15 +01:00
parent 93de9c5d00
commit 1257e8b950
219 changed files with 1231 additions and 1259 deletions

View File

@@ -1,23 +1,22 @@
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.Api.Admin.Sys.Settings;
using Moonlight.Api.Infrastructure.Database;
using Moonlight.Api.Infrastructure.Database.Entities;
using Moonlight.Shared;
using Moonlight.Shared.Http.Requests.Seup;
using Moonlight.Shared.Admin.Setup;
namespace Moonlight.Api.Http.Controllers.Admin;
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;
private readonly DatabaseRepository<Role> RolesRepository;
private const string StateSettingsKey = "Moonlight.Api.Setup.State";
public SetupController(
SettingsService settingsService,
@@ -51,41 +50,40 @@ public class SetupController : Controller
.FirstOrDefaultAsync(x => x.Name == "Administrators");
if (adminRole == null)
{
adminRole = await RolesRepository.AddAsync(new Role()
adminRole = await RolesRepository.AddAsync(new Role
{
Name = "Administrators",
Description = "Automatically generated group for full administrator permissions",
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,
Permissions.System.Instance
]
});
}
var user = await UsersRepository
@@ -94,12 +92,13 @@ public class SetupController : Controller
if (user == null)
{
await UsersRepository.AddAsync(new User()
await UsersRepository.AddAsync(new User
{
Email = dto.AdminEmail,
Username = dto.AdminUsername,
RoleMemberships = [
new RoleMember()
RoleMemberships =
[
new RoleMember
{
Role = adminRole,
CreatedAt = DateTimeOffset.UtcNow,
@@ -112,16 +111,16 @@ public class SetupController : Controller
}
else
{
user.RoleMemberships.Add(new RoleMember()
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

@@ -2,25 +2,22 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Hybrid;
using Moonlight.Api.Database;
using Moonlight.Api.Database.Entities;
using Moonlight.Api.Implementations.ApiKeyScheme;
using Moonlight.Api.Mappers;
using Moonlight.Api.Admin.Sys.ApiKeys.Scheme;
using Moonlight.Api.Infrastructure.Database;
using Moonlight.Api.Infrastructure.Database.Entities;
using Moonlight.Shared;
using Moonlight.Shared.Http.Requests;
using Moonlight.Shared.Http.Requests.Admin.ApiKeys;
using Moonlight.Shared.Http.Responses;
using Moonlight.Shared.Http.Responses.Admin.ApiKeys;
using Moonlight.Shared.Admin.Sys.ApiKeys;
using Moonlight.Shared.Shared;
namespace Moonlight.Api.Http.Controllers.Admin;
namespace Moonlight.Api.Admin.Sys.ApiKeys;
[Authorize]
[ApiController]
[Route("api/admin/apiKeys")]
public class ApiKeyController : Controller
{
private readonly DatabaseRepository<ApiKey> KeyRepository;
private readonly HybridCache HybridCache;
private readonly DatabaseRepository<ApiKey> KeyRepository;
public ApiKeyController(DatabaseRepository<ApiKey> keyRepository, HybridCache hybridCache)
{
@@ -48,9 +45,7 @@ public class ApiKeyController : Controller
// Filters
if (filterOptions != null)
{
foreach (var filterOption in filterOptions.Filters)
{
query = filterOption.Key switch
{
nameof(ApiKey.Name) =>
@@ -61,8 +56,6 @@ public class ApiKeyController : Controller
_ => query
};
}
}
// Pagination
var data = await query
@@ -96,7 +89,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);
@@ -135,9 +128,9 @@ 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,10 +1,9 @@
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.Api.Infrastructure.Database.Entities;
using Moonlight.Shared.Admin.Sys.ApiKeys;
using Riok.Mapperly.Abstractions;
namespace Moonlight.Api.Mappers;
namespace Moonlight.Api.Admin.Sys.ApiKeys;
[Mapper]
[SuppressMessage("Mapper", "RMG020:No members are mapped in an object mapping")]

View File

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

View File

@@ -5,19 +5,18 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Hybrid;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Moonlight.Api.Database;
using Moonlight.Api.Database.Entities;
using Moonlight.Api.Infrastructure.Database;
using Moonlight.Api.Infrastructure.Database.Entities;
using Moonlight.Shared;
namespace Moonlight.Api.Implementations.ApiKeyScheme;
namespace Moonlight.Api.Admin.Sys.ApiKeys.Scheme;
public class ApiKeySchemeHandler : AuthenticationHandler<ApiKeySchemeOptions>
{
public const string CacheKeyFormat = $"Moonlight.Api.{nameof(ApiKeySchemeHandler)}.{{0}}";
private readonly DatabaseRepository<ApiKey> ApiKeyRepository;
private readonly HybridCache HybridCache;
public const string CacheKeyFormat = $"Moonlight.Api.{nameof(ApiKeySchemeHandler)}.{{0}}";
public ApiKeySchemeHandler(
IOptionsMonitor<ApiKeySchemeOptions> options,
ILoggerFactory logger,
@@ -50,9 +49,9 @@ public class ApiKeySchemeHandler : AuthenticationHandler<ApiKeySchemeOptions>
.Query()
.Where(x => x.Key == authHeaderValue)
.Select(x => new ApiKeySession(x.Permissions, x.ValidUntil))
.FirstOrDefaultAsync(cancellationToken: ct);
.FirstOrDefaultAsync(ct);
},
new HybridCacheEntryOptions()
new HybridCacheEntryOptions
{
LocalCacheExpiration = Options.LookupL1CacheTime,
Expiration = Options.LookupL2CacheTime

View File

@@ -1,6 +1,6 @@
using Microsoft.AspNetCore.Authentication;
namespace Moonlight.Api.Implementations.ApiKeyScheme;
namespace Moonlight.Api.Admin.Sys.ApiKeys.Scheme;
public class ApiKeySchemeOptions : AuthenticationSchemeOptions
{

View File

@@ -1,24 +1,61 @@
using System.Diagnostics;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Moonlight.Api.Helpers;
using VersionService = Moonlight.Api.Admin.Sys.Versions.VersionService;
namespace Moonlight.Api.Services;
namespace Moonlight.Api.Admin.Sys;
public class ApplicationService : IHostedService
{
private readonly VersionService VersionService;
private readonly ILogger<ApplicationService> Logger;
private readonly VersionService VersionService;
public ApplicationService(VersionService versionService, ILogger<ApplicationService> logger)
{
VersionService = versionService;
Logger = logger;
}
public DateTimeOffset StartedAt { get; private set; }
public string VersionName { get; private set; } = "N/A";
public bool IsUpToDate { get; set; } = true;
public string OperatingSystem { get; private set; } = "N/A";
public ApplicationService(VersionService versionService, ILogger<ApplicationService> logger)
public async Task StartAsync(CancellationToken cancellationToken)
{
VersionService = versionService;
Logger = logger;
StartedAt = DateTimeOffset.UtcNow;
OperatingSystem = OsHelper.GetName();
try
{
var currentVersion = await VersionService.GetInstanceVersionAsync();
var latestVersion = await VersionService.GetLatestVersionAsync();
VersionName = currentVersion.Identifier;
IsUpToDate = latestVersion == null || currentVersion.Identifier == latestVersion.Identifier;
Logger.LogInformation("Running Moonlight Panel {version} on {operatingSystem}", VersionName,
OperatingSystem);
if (!IsUpToDate)
Logger.LogWarning("Your instance is not up-to-date");
if (currentVersion.IsDevelopment)
Logger.LogWarning("Your instance is running a development version");
if (currentVersion.IsPreRelease)
Logger.LogWarning("Your instance is running a pre-release version");
}
catch (Exception e)
{
Logger.LogError(e, "An unhandled exception occurred while fetching version details");
}
}
public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
public Task<long> GetMemoryUsageAsync()
@@ -45,41 +82,8 @@ public class ApplicationService : IHostedService
// Calculate CPU usage
var cpuUsedMs = (endCpuTime - startCpuTime).TotalMilliseconds;
var totalMsPassed = (endTime - startTime).TotalMilliseconds;
var cpuUsagePercent = (cpuUsedMs / (Environment.ProcessorCount * totalMsPassed)) * 100;
var cpuUsagePercent = cpuUsedMs / (Environment.ProcessorCount * totalMsPassed) * 100;
return Math.Round(cpuUsagePercent, 2);
}
public async Task StartAsync(CancellationToken cancellationToken)
{
StartedAt = DateTimeOffset.UtcNow;
OperatingSystem = OsHelper.GetName();
try
{
var currentVersion = await VersionService.GetInstanceVersionAsync();
var latestVersion = await VersionService.GetLatestVersionAsync();
VersionName = currentVersion.Identifier;
IsUpToDate = latestVersion == null || currentVersion.Identifier == latestVersion.Identifier;
Logger.LogInformation("Running Moonlight Panel {version} on {operatingSystem}", VersionName, OperatingSystem);
if (!IsUpToDate)
Logger.LogWarning("Your instance is not up-to-date");
if (currentVersion.IsDevelopment)
Logger.LogWarning("Your instance is running a development version");
if (currentVersion.IsPreRelease)
Logger.LogWarning("Your instance is running a pre-release version");
}
catch (Exception e)
{
Logger.LogError(e, "An unhandled exception occurred while fetching version details");
}
}
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}

View File

@@ -2,14 +2,10 @@
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;
using Moonlight.Shared.Admin.Sys.ContainerHelper;
namespace Moonlight.Api.Http.Controllers.Admin;
namespace Moonlight.Api.Admin.Sys.ContainerHelper;
[ApiController]
[Route("api/admin/ch")]
@@ -19,7 +15,8 @@ public class ContainerHelperController : Controller
private readonly ContainerHelperService ContainerHelperService;
private readonly IOptions<ContainerHelperOptions> Options;
public ContainerHelperController(ContainerHelperService containerHelperService, IOptions<ContainerHelperOptions> options)
public ContainerHelperController(ContainerHelperService containerHelperService,
IOptions<ContainerHelperOptions> options)
{
ContainerHelperService = containerHelperService;
Options = options;

View File

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

View File

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

View File

@@ -1,10 +1,10 @@
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;
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.Services;
namespace Moonlight.Api.Admin.Sys.ContainerHelper;
public class ContainerHelperService
{
@@ -53,7 +53,7 @@ public class ContainerHelperService
{
var responseText = await response.Content.ReadAsStringAsync();
yield return new RebuildEventDto()
yield return new RebuildEventDto
{
Type = RebuildEventType.Failed,
Data = responseText
@@ -76,7 +76,8 @@ public class ContainerHelperService
continue;
var data = line.Trim("data: ");
var deserializedData = JsonSerializer.Deserialize<RebuildEventDto>(data, SerializationContext.Default.Options);
var deserializedData =
JsonSerializer.Deserialize<RebuildEventDto>(data, SerializationContext.Default.Options);
yield return deserializedData;
@@ -85,7 +86,7 @@ public class ContainerHelperService
yield break;
} while (true);
yield return new RebuildEventDto()
yield return new RebuildEventDto
{
Type = RebuildEventType.Succeeded,
Data = string.Empty

View File

@@ -0,0 +1,18 @@
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,4 +1,4 @@
namespace Moonlight.Api.Http.Services.ContainerHelper;
namespace Moonlight.Api.Admin.Sys.ContainerHelper.Models;
public class ProblemDetails
{

View File

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

View File

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

View File

@@ -1,17 +1,15 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Moonlight.Api.Http.Services.ContainerHelper.Events;
using Moonlight.Api.Http.Services.ContainerHelper.Requests;
using Moonlight.Api.Admin.Sys.ContainerHelper.Models.Events;
using Moonlight.Api.Admin.Sys.ContainerHelper.Models.Requests;
namespace Moonlight.Api.Http.Services.ContainerHelper;
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,11 +1,9 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Moonlight.Api.Mappers;
using Moonlight.Api.Services;
using Moonlight.Shared;
using Moonlight.Shared.Http.Responses.Admin;
using Moonlight.Shared.Admin.Sys.Diagnose;
namespace Moonlight.Api.Http.Controllers.Admin;
namespace Moonlight.Api.Admin.Sys.Diagnose;
[ApiController]
[Authorize(Policy = Permissions.System.Diagnose)]

View File

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

View File

@@ -1,13 +1,12 @@
using Microsoft.Extensions.Logging;
using Moonlight.Api.Interfaces;
using Moonlight.Api.Models;
using Moonlight.Api.Infrastructure.Hooks;
namespace Moonlight.Api.Services;
namespace Moonlight.Api.Admin.Sys.Diagnose;
public class DiagnoseService
{
private readonly IEnumerable<IDiagnoseProvider> Providers;
private readonly ILogger<DiagnoseService> Logger;
private readonly IEnumerable<IDiagnoseProvider> Providers;
public DiagnoseService(IEnumerable<IDiagnoseProvider> providers, ILogger<DiagnoseService> logger)
{
@@ -20,7 +19,6 @@ public class DiagnoseService
var results = new List<DiagnoseResult>();
foreach (var provider in Providers)
{
try
{
results.AddRange(
@@ -31,7 +29,6 @@ public class DiagnoseService
{
Logger.LogError(e, "An unhandled error occured while processing provider");
}
}
return results.ToArray();
}

View File

@@ -1,6 +1,6 @@
using System.Runtime.InteropServices;
namespace Moonlight.Api.Helpers;
namespace Moonlight.Api.Admin.Sys;
public class OsHelper
{
@@ -53,17 +53,12 @@ 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
@@ -74,17 +69,12 @@ 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,4 +1,4 @@
namespace Moonlight.Api.Configuration;
namespace Moonlight.Api.Admin.Sys.Settings;
public class SettingsOptions
{

View File

@@ -2,25 +2,23 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Hybrid;
using Microsoft.Extensions.Options;
using Moonlight.Api.Configuration;
using Moonlight.Api.Database;
using Moonlight.Api.Database.Entities;
using Moonlight.Api.Infrastructure.Database;
using Moonlight.Api.Infrastructure.Database.Entities;
namespace Moonlight.Api.Services;
namespace Moonlight.Api.Admin.Sys.Settings;
public class SettingsService
{
private readonly DatabaseRepository<SettingsOption> Repository;
private readonly IOptions<SettingsOptions> Options;
private readonly HybridCache HybridCache;
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;
@@ -39,9 +37,9 @@ public class SettingsService
.Query()
.Where(x => x.Key == key)
.Select(o => o.ValueJson)
.FirstOrDefaultAsync(cancellationToken: ct);
.FirstOrDefaultAsync(ct);
},
new HybridCacheEntryOptions()
new HybridCacheEntryOptions
{
LocalCacheExpiration = Options.Value.LookupL1CacheTime,
Expiration = Options.Value.LookupL2CacheTime
@@ -57,13 +55,13 @@ public class SettingsService
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;
@@ -71,12 +69,12 @@ public class SettingsService
}
else
{
option = new SettingsOption()
option = new SettingsOption
{
Key = key,
ValueJson = json
};
await Repository.AddAsync(option);
}

View File

@@ -1,20 +1,18 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Moonlight.Api.Constants;
using Moonlight.Api.Services;
using Moonlight.Api.Shared.Frontend;
using Moonlight.Shared;
using Moonlight.Shared.Http.Requests.Admin.Settings;
using Moonlight.Shared.Http.Responses.Admin.Settings;
using Moonlight.Shared.Admin.Sys.Settings;
namespace Moonlight.Api.Http.Controllers.Admin.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 SettingsService SettingsService;
private readonly FrontendService FrontendService;
private readonly SettingsService SettingsService;
public WhiteLabelingController(SettingsService settingsService, FrontendService frontendService)
{
@@ -38,7 +36,7 @@ public class WhiteLabelingController : Controller
{
await SettingsService.SetValueAsync(FrontendSettingConstants.Name, request.Name);
await FrontendService.ResetCacheAsync();
var dto = new WhiteLabelingDto
{
Name = await SettingsService.GetValueAsync<string>(FrontendSettingConstants.Name) ?? "Moonlight"

View File

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

View File

@@ -1,10 +1,9 @@
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.Api.Infrastructure.Database.Entities;
using Moonlight.Shared.Admin.Sys.Themes;
using Riok.Mapperly.Abstractions;
namespace Moonlight.Api.Mappers;
namespace Moonlight.Api.Admin.Sys.Themes;
[Mapper]
[SuppressMessage("Mapper", "RMG020:No members are mapped in an object mapping")]

View File

@@ -1,6 +1,6 @@
using VYaml.Annotations;
namespace Moonlight.Api.Models;
namespace Moonlight.Api.Admin.Sys.Themes;
[YamlObject]
public partial class ThemeTransferModel

View File

@@ -1,24 +1,21 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Moonlight.Api.Database;
using Moonlight.Api.Database.Entities;
using Moonlight.Api.Mappers;
using Moonlight.Api.Services;
using Moonlight.Api.Infrastructure.Database;
using Moonlight.Api.Infrastructure.Database.Entities;
using Moonlight.Api.Shared.Frontend;
using Moonlight.Shared;
using Moonlight.Shared.Http.Requests;
using Moonlight.Shared.Http.Requests.Admin.Themes;
using Moonlight.Shared.Http.Responses;
using Moonlight.Shared.Http.Responses.Admin.Themes;
using Moonlight.Shared.Admin.Sys.Themes;
using Moonlight.Shared.Shared;
namespace Moonlight.Api.Http.Controllers.Admin.Themes;
namespace Moonlight.Api.Admin.Sys.Themes;
[ApiController]
[Route("api/admin/themes")]
public class ThemesController : Controller
{
private readonly DatabaseRepository<Theme> ThemeRepository;
private readonly FrontendService FrontendService;
private readonly DatabaseRepository<Theme> ThemeRepository;
public ThemesController(DatabaseRepository<Theme> themeRepository, FrontendService frontendService)
{
@@ -48,9 +45,7 @@ public class ThemesController : Controller
// Filters
if (filterOptions != null)
{
foreach (var filterOption in filterOptions.Filters)
{
query = filterOption.Key switch
{
nameof(Theme.Name) =>
@@ -64,8 +59,6 @@ public class ThemesController : Controller
_ => query
};
}
}
// Pagination
var data = await query
@@ -116,7 +109,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);
@@ -137,9 +130,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,15 +1,13 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Moonlight.Api.Database;
using Moonlight.Api.Database.Entities;
using Moonlight.Api.Mappers;
using Moonlight.Api.Models;
using Moonlight.Api.Infrastructure.Database;
using Moonlight.Api.Infrastructure.Database.Entities;
using Moonlight.Shared;
using Moonlight.Shared.Http.Responses.Admin.Themes;
using Moonlight.Shared.Admin.Sys.Themes;
using VYaml.Serialization;
namespace Moonlight.Api.Http.Controllers.Admin.Themes;
namespace Moonlight.Api.Admin.Sys.Themes;
[ApiController]
[Route("api/admin/themes")]
@@ -33,7 +31,7 @@ public class TransferController : Controller
if (theme == null)
return Problem("No theme with that id found", statusCode: 404);
var yml = YamlSerializer.Serialize(new ThemeTransferModel()
var yml = YamlSerializer.Serialize(new ThemeTransferModel
{
Name = theme.Name,
Author = theme.Author,
@@ -55,7 +53,7 @@ public class TransferController : Controller
if (existingTheme == null)
{
var finalTheme = await ThemeRepository.AddAsync(new Theme()
var finalTheme = await ThemeRepository.AddAsync(new Theme
{
Name = themeToImport.Name,
Author = themeToImport.Author,

View File

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

View File

@@ -1,4 +1,4 @@
namespace Moonlight.Api.Models;
namespace Moonlight.Api.Admin.Sys.Versions;
// Notes:
// Identifier - This needs to be the branch to clone to build this version if

View File

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

View File

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

View File

@@ -1,22 +1,16 @@
using System.Text.Json.Nodes;
using System.Text.RegularExpressions;
using Microsoft.Extensions.Options;
using Moonlight.Api.Configuration;
using Moonlight.Api.Models;
namespace Moonlight.Api.Services;
namespace Moonlight.Api.Admin.Sys.Versions;
public partial class VersionService
{
private readonly IOptions<VersionOptions> Options;
private readonly IHttpClientFactory HttpClientFactory;
private const string VersionPath = "/app/version";
private const string GiteaServer = "https://git.battlestati.one";
private const string GiteaRepository = "Moonlight-Panel/Moonlight";
[GeneratedRegex(@"^v(?!1(\.|$))\d+\.[A-Za-z0-9]+(\.[A-Za-z0-9]+)*$")]
private static partial Regex RegexFilter();
private readonly IHttpClientFactory HttpClientFactory;
private readonly IOptions<VersionOptions> Options;
public VersionService(
IOptions<VersionOptions> options,
@@ -27,11 +21,14 @@ public partial class VersionService
HttpClientFactory = httpClientFactory;
}
[GeneratedRegex(@"^v(?!1(\.|$))\d+\.[A-Za-z0-9]+(\.[A-Za-z0-9]+)*$")]
private static partial Regex RegexFilter();
public async Task<MoonlightVersion[]> GetVersionsAsync()
{
if (Options.Value.OfflineMode)
return [];
var versions = new List<MoonlightVersion>();
var httpClient = HttpClientFactory.CreateClient();
@@ -42,7 +39,6 @@ public partial class VersionService
var tagsJson = await JsonNode.ParseAsync(tagsJsonStream);
if (tagsJson != null)
{
foreach (var node in tagsJson.AsArray())
{
if (node == null)
@@ -50,8 +46,8 @@ public partial class VersionService
var name = node["name"]?.GetValue<string>() ?? "N/A";
var createdAt = node["createdAt"]?.GetValue<DateTimeOffset>() ?? DateTimeOffset.MinValue;
if(!RegexFilter().IsMatch(name))
if (!RegexFilter().IsMatch(name))
continue;
versions.Add(new MoonlightVersion(
@@ -61,8 +57,7 @@ public partial class VersionService
createdAt
));
}
}
// Branches
const string branchesPath = $"{GiteaServer}/api/v1/repos/{GiteaRepository}/branches";
@@ -70,7 +65,6 @@ public partial class VersionService
var branchesJson = await JsonNode.ParseAsync(branchesJsonStream);
if (branchesJson != null)
{
foreach (var node in branchesJson.AsArray())
{
if (node == null)
@@ -83,8 +77,8 @@ public partial class VersionService
continue;
var createdAt = commit["timestamp"]?.GetValue<DateTimeOffset>() ?? DateTimeOffset.MinValue;
if(!RegexFilter().IsMatch(name))
if (!RegexFilter().IsMatch(name))
continue;
versions.Add(new MoonlightVersion(
@@ -94,7 +88,6 @@ public partial class VersionService
createdAt
));
}
}
return versions.ToArray();
}
@@ -106,7 +99,9 @@ public partial class VersionService
string versionIdentifier;
if (!string.IsNullOrWhiteSpace(Options.Value.CurrentOverride))
{
versionIdentifier = Options.Value.CurrentOverride;
}
else
{
if (File.Exists(VersionPath))

View File

@@ -1,11 +1,9 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Moonlight.Api.Mappers;
using Moonlight.Api.Services;
using Moonlight.Shared;
using Moonlight.Shared.Http.Responses.Admin;
using Moonlight.Shared.Admin.Sys.Versions;
namespace Moonlight.Api.Http.Controllers.Admin;
namespace Moonlight.Api.Admin.Sys.Versions;
[ApiController]
[Route("api/admin/versions")]
@@ -37,8 +35,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,10 +1,9 @@
using System.Diagnostics.CodeAnalysis;
using Moonlight.Api.Database.Entities;
using Moonlight.Shared.Http.Requests.Admin.Roles;
using Moonlight.Shared.Http.Responses.Admin;
using Moonlight.Api.Infrastructure.Database.Entities;
using Moonlight.Shared.Admin.Users.Roles;
using Riok.Mapperly.Abstractions;
namespace Moonlight.Api.Mappers;
namespace Moonlight.Api.Admin.Users.Roles;
[Mapper]
[SuppressMessage("Mapper", "RMG020:Source member is not mapped to any target member")]
@@ -13,6 +12,7 @@ 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,23 +1,23 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Moonlight.Api.Database;
using Moonlight.Api.Database.Entities;
using Moonlight.Api.Mappers;
using Moonlight.Api.Admin.Users.Users;
using Moonlight.Api.Infrastructure.Database;
using Moonlight.Api.Infrastructure.Database.Entities;
using Moonlight.Shared;
using Moonlight.Shared.Http.Responses;
using Moonlight.Shared.Http.Responses.Admin.Users;
using Moonlight.Shared.Admin.Users.Users;
using Moonlight.Shared.Shared;
namespace Moonlight.Api.Http.Controllers.Admin;
namespace Moonlight.Api.Admin.Users.Roles;
[ApiController]
[Authorize(Policy = Permissions.Roles.Members)]
[Route("api/admin/roles/{roleId:int}/members")]
public class RoleMembersController : Controller
{
private readonly DatabaseRepository<User> UsersRepository;
private readonly DatabaseRepository<Role> RolesRepository;
private readonly DatabaseRepository<RoleMember> RoleMembersRepository;
private readonly DatabaseRepository<Role> RolesRepository;
private readonly DatabaseRepository<User> UsersRepository;
public RoleMembersController(
DatabaseRepository<User> usersRepository,
@@ -53,19 +53,16 @@ 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 = query
.OrderBy(x => x.Id)
.Skip(startIndex)
.Take(length)
.ProjectToDto()
var items = UserMapper.ProjectToDto(query
.OrderBy(x => x.Id)
.Skip(startIndex)
.Take(length))
.ToArray();
var totalCount = await query.CountAsync();
@@ -95,19 +92,16 @@ 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 = query
.OrderBy(x => x.Id)
.Skip(startIndex)
.Take(length)
.ProjectToDto()
var items = UserMapper.ProjectToDto(query
.OrderBy(x => x.Id)
.Skip(startIndex)
.Take(length))
.ToArray();
var totalCount = await query.CountAsync();

View File

@@ -1,16 +1,13 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Moonlight.Api.Database;
using Moonlight.Api.Database.Entities;
using Moonlight.Api.Mappers;
using Moonlight.Api.Infrastructure.Database;
using Moonlight.Api.Infrastructure.Database.Entities;
using Moonlight.Shared;
using Moonlight.Shared.Http.Requests;
using Moonlight.Shared.Http.Requests.Admin.Roles;
using Moonlight.Shared.Http.Responses;
using Moonlight.Shared.Http.Responses.Admin;
using Moonlight.Shared.Admin.Users.Roles;
using Moonlight.Shared.Shared;
namespace Moonlight.Api.Http.Controllers.Admin;
namespace Moonlight.Api.Admin.Users.Roles;
[ApiController]
[Route("api/admin/roles")]
@@ -39,15 +36,13 @@ 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) =>
@@ -55,8 +50,6 @@ public class RolesController : Controller
_ => query
};
}
}
// Pagination
var data = await query
@@ -106,7 +99,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

@@ -3,26 +3,24 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Hybrid;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Moonlight.Api.Configuration;
using Moonlight.Api.Database;
using Moonlight.Api.Database.Entities;
using Moonlight.Api.Interfaces;
using Moonlight.Api.Infrastructure.Database;
using Moonlight.Api.Infrastructure.Database.Entities;
using Moonlight.Api.Infrastructure.Hooks;
using Moonlight.Shared;
namespace Moonlight.Api.Services;
namespace Moonlight.Api.Admin.Users.Users;
public class UserAuthService
{
private readonly DatabaseRepository<User> UserRepository;
private readonly ILogger<UserAuthService> Logger;
private readonly IOptions<UserOptions> Options;
private readonly IEnumerable<IUserAuthHook> Hooks;
private readonly HybridCache HybridCache;
private const string UserIdClaim = "UserId";
private const string IssuedAtClaim = "IssuedAt";
public const string CacheKeyPattern = $"Moonlight.{nameof(UserAuthService)}.{nameof(ValidateAsync)}-{{0}}";
private readonly IEnumerable<IUserAuthHook> Hooks;
private readonly HybridCache HybridCache;
private readonly ILogger<UserAuthService> Logger;
private readonly IOptions<UserOptions> Options;
private readonly DatabaseRepository<User> UserRepository;
public UserAuthService(
DatabaseRepository<User> userRepository,
@@ -60,7 +58,7 @@ public class UserAuthService
if (user == null) // Sync user if not already existing in the database
{
user = await UserRepository.AddAsync(new User()
user = await UserRepository.AddAsync(new User
{
Username = username,
Email = email,
@@ -80,11 +78,9 @@ public class UserAuthService
]);
foreach (var hook in Hooks)
{
// Run every hook, and if any returns false, we return false as well
if (!await hook.SyncAsync(principal, user))
return false;
}
return true;
}
@@ -114,9 +110,9 @@ public class UserAuthService
u.InvalidateTimestamp,
u.RoleMemberships.SelectMany(x => x.Role.Permissions).ToArray())
)
.FirstOrDefaultAsync(cancellationToken: ct);
.FirstOrDefaultAsync(ct);
},
new HybridCacheEntryOptions()
new HybridCacheEntryOptions
{
LocalCacheExpiration = Options.Value.ValidationCacheL1Expiry,
Expiration = Options.Value.ValidationCacheL2Expiry
@@ -146,11 +142,9 @@ public class UserAuthService
);
foreach (var hook in Hooks)
{
// Run every hook, and if any returns false we return false as well
if (!await hook.ValidateAsync(principal, userId))
return false;
}
return true;
}

View File

@@ -1,21 +1,19 @@
using System.Collections.Frozen;
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.Api.Infrastructure.Database;
using Moonlight.Api.Infrastructure.Database.Entities;
using Moonlight.Shared;
namespace Moonlight.Api.Http.Controllers.Admin.Users;
namespace Moonlight.Api.Admin.Users.Users;
[ApiController]
[Route("api/admin/users")]
[Authorize(Policy = Permissions.Users.Delete)]
public class UserDeletionController : Controller
{
private readonly UserDeletionService UserDeletionService;
private readonly DatabaseRepository<User> Repository;
private readonly UserDeletionService UserDeletionService;
public UserDeletionController(UserDeletionService userDeletionService, DatabaseRepository<User> repository)
{
@@ -36,10 +34,9 @@ public class UserDeletionController : Controller
var validationResult = await UserDeletionService.ValidateAsync(id);
if (!validationResult.IsValid)
{
return ValidationProblem(
new ValidationProblemDetails(
new Dictionary<string, string[]>()
new Dictionary<string, string[]>
{
{
string.Empty,
@@ -48,8 +45,7 @@ public class UserDeletionController : Controller
}
)
);
}
await UserDeletionService.DeleteAsync(id);
return NoContent();
}

View File

@@ -1,22 +1,22 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Hybrid;
using Moonlight.Api.Database;
using Moonlight.Api.Database.Entities;
using Moonlight.Api.Interfaces;
using Moonlight.Api.Infrastructure.Database;
using Moonlight.Api.Infrastructure.Database.Entities;
using Moonlight.Api.Infrastructure.Hooks;
namespace Moonlight.Api.Services;
namespace Moonlight.Api.Admin.Users.Users;
public class UserDeletionService
{
private readonly DatabaseRepository<User> Repository;
private readonly IEnumerable<IUserDeletionHook> Hooks;
private readonly HybridCache HybridCache;
private readonly DatabaseRepository<User> Repository;
public UserDeletionService(
DatabaseRepository<User> repository,
IEnumerable<IUserDeletionHook> hooks,
HybridCache hybridCache
)
)
{
Repository = repository;
Hooks = hooks;
@@ -28,20 +28,20 @@ public class UserDeletionService
var user = await Repository
.Query()
.FirstOrDefaultAsync(x => x.Id == userId);
if(user == null)
if (user == null)
throw new AggregateException($"User with id {userId} not found");
var errorMessages = new List<string>();
foreach (var hook in Hooks)
{
if (await hook.ValidateAsync(user, errorMessages))
continue;
return new UserDeletionValidationResult(false, errorMessages);
}
return new UserDeletionValidationResult(true, []);
}
@@ -50,13 +50,13 @@ public class UserDeletionService
var user = await Repository
.Query()
.FirstOrDefaultAsync(x => x.Id == userId);
if(user == null)
if (user == null)
throw new AggregateException($"User with id {userId} not found");
foreach (var hook in Hooks)
await hook.ExecuteAsync(user);
await Repository.RemoveAsync(user);
await HybridCache.RemoveAsync(string.Format(UserAuthService.CacheKeyPattern, user.Id));

View File

@@ -1,12 +1,11 @@
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.Api.Infrastructure.Database;
using Moonlight.Api.Infrastructure.Database.Entities;
using Moonlight.Shared;
namespace Moonlight.Api.Http.Controllers.Admin.Users;
namespace Moonlight.Api.Admin.Users.Users;
[ApiController]
[Route("api/admin/users/{id:int}/logout")]

View File

@@ -1,16 +1,16 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Hybrid;
using Moonlight.Api.Database;
using Moonlight.Api.Database.Entities;
using Moonlight.Api.Interfaces;
using Moonlight.Api.Infrastructure.Database;
using Moonlight.Api.Infrastructure.Database.Entities;
using Moonlight.Api.Infrastructure.Hooks;
namespace Moonlight.Api.Services;
namespace Moonlight.Api.Admin.Users.Users;
public class UserLogoutService
{
private readonly DatabaseRepository<User> Repository;
private readonly IEnumerable<IUserLogoutHook> Hooks;
private readonly HybridCache HybridCache;
private readonly DatabaseRepository<User> Repository;
public UserLogoutService(
DatabaseRepository<User> repository,

View File

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

View File

@@ -1,4 +1,4 @@
namespace Moonlight.Api.Configuration;
namespace Moonlight.Api.Admin.Users.Users;
public class UserOptions
{

View File

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

View File

@@ -1,21 +0,0 @@
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,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,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,4 +1,4 @@
namespace Moonlight.Api.Configuration;
namespace Moonlight.Api.Infrastructure.Configuration;
public class CacheOptions
{

View File

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

View File

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

View File

@@ -1,19 +1,11 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using Moonlight.Api.Configuration;
using Moonlight.Api.Database.Entities;
using Moonlight.Api.Infrastructure.Database.Entities;
namespace Moonlight.Api.Database;
namespace Moonlight.Api.Infrastructure.Database;
public class DataContext : DbContext
{
public DbSet<User> Users { get; set; }
public DbSet<SettingsOption> SettingsOptions { get; set; }
public DbSet<Role> Roles { get; set; }
public DbSet<RoleMember> RoleMembers { get; set; }
public DbSet<ApiKey> ApiKeys { get; set; }
public DbSet<Theme> Themes { get; set; }
private readonly IOptions<DatabaseOptions> Options;
public DataContext(IOptions<DatabaseOptions> options)
@@ -21,24 +13,36 @@ public class DataContext : DbContext
Options = options;
}
public DbSet<User> Users { get; set; }
public DbSet<SettingsOption> SettingsOptions { get; set; }
public DbSet<Role> Roles { get; set; }
public DbSet<RoleMember> RoleMembers { get; set; }
public DbSet<ApiKey> ApiKeys { get; set; }
public DbSet<Theme> Themes { get; set; }
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}"
$"Database={Options.Value.Database}",
builder =>
{
builder.MigrationsAssembly(typeof(DataContext).Assembly);
builder.MigrationsHistoryTable("MigrationsHistory", "core");
}
);
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.HasDefaultSchema("core");
base.OnModelCreating(modelBuilder);
}
}

View File

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

View File

@@ -1,7 +1,7 @@
using Microsoft.EntityFrameworkCore;
using Moonlight.Api.Database.Interfaces;
using Moonlight.Api.Infrastructure.Database.Interfaces;
namespace Moonlight.Api.Database;
namespace Moonlight.Api.Infrastructure.Database;
public class DatabaseRepository<T> where T : class
{
@@ -14,7 +14,10 @@ public class DatabaseRepository<T> where T : class
Set = DataContext.Set<T>();
}
public IQueryable<T> Query() => Set;
public IQueryable<T> Query()
{
return Set;
}
public async Task<T> AddAsync(T entity)
{
@@ -23,7 +26,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;
@@ -33,7 +36,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

@@ -2,9 +2,8 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Moonlight.Api.Database;
namespace Moonlight.Api.Services;
namespace Moonlight.Api.Infrastructure.Database;
public class DbMigrationService : IHostedLifecycleService
{
@@ -41,9 +40,28 @@ public class DbMigrationService : IHostedLifecycleService
Logger.LogInformation("Migration complete");
}
public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
public Task StartedAsync(CancellationToken cancellationToken) => Task.CompletedTask;
public Task StoppedAsync(CancellationToken cancellationToken) => Task.CompletedTask;
public Task StoppingAsync(CancellationToken cancellationToken) => Task.CompletedTask;
public Task StartAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
public Task StartedAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
public Task StoppedAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
public Task StoppingAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
}

View File

@@ -1,24 +1,21 @@
using System.ComponentModel.DataAnnotations;
using Moonlight.Api.Database.Interfaces;
using Moonlight.Api.Infrastructure.Database.Interfaces;
namespace Moonlight.Api.Database.Entities;
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; }
[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; }
[MaxLength(32)] public string Key { get; set; }
// Action timestamps
public DateTimeOffset CreatedAt { get; set; }
public DateTimeOffset UpdatedAt { get; set; }

View File

@@ -1,23 +1,21 @@
using System.ComponentModel.DataAnnotations;
using Moonlight.Api.Database.Interfaces;
using Moonlight.Api.Infrastructure.Database.Interfaces;
namespace Moonlight.Api.Database.Entities;
namespace Moonlight.Api.Infrastructure.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.Database.Interfaces;
using Moonlight.Api.Infrastructure.Database.Interfaces;
namespace Moonlight.Api.Database.Entities;
namespace Moonlight.Api.Infrastructure.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

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

View File

@@ -0,0 +1,18 @@
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,25 +1,23 @@
using System.ComponentModel.DataAnnotations;
using Moonlight.Api.Database.Interfaces;
using Moonlight.Api.Infrastructure.Database.Interfaces;
namespace Moonlight.Api.Database.Entities;
namespace Moonlight.Api.Infrastructure.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.Database.Interfaces;
namespace Moonlight.Api.Infrastructure.Database.Interfaces;
internal interface IActionTimestamps
{

View File

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

View File

@@ -2,7 +2,7 @@
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Logging.Console;
namespace Moonlight.Api.Helpers;
namespace Moonlight.Api.Infrastructure.Helpers;
public class AppConsoleFormatter : ConsoleFormatter
{
@@ -58,7 +58,9 @@ 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 Moonlight.Api.Models;
using Moonlight.Api.Admin.Sys.Diagnose;
namespace Moonlight.Api.Interfaces;
namespace Moonlight.Api.Infrastructure.Hooks;
public interface IDiagnoseProvider
{

View File

@@ -1,12 +1,12 @@
using System.Security.Claims;
using Moonlight.Api.Database.Entities;
using Moonlight.Api.Infrastructure.Database.Entities;
namespace Moonlight.Api.Interfaces;
namespace Moonlight.Api.Infrastructure.Hooks;
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.Database.Entities;
using Moonlight.Api.Infrastructure.Database.Entities;
namespace Moonlight.Api.Interfaces;
namespace Moonlight.Api.Infrastructure.Hooks;
public interface IUserDeletionHook
{

View File

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

View File

@@ -1,7 +1,7 @@
using Microsoft.AspNetCore.Authorization;
using Moonlight.Shared;
namespace Moonlight.Api.Implementations;
namespace Moonlight.Api.Infrastructure.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.Implementations;
namespace Moonlight.Api.Infrastructure.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,18 +25,22 @@ public class PermissionPolicyProvider : IAuthorizationPolicyProvider
}
public Task<AuthorizationPolicy> GetDefaultPolicyAsync()
=> FallbackProvider.GetDefaultPolicyAsync();
{
return FallbackProvider.GetDefaultPolicyAsync();
}
public Task<AuthorizationPolicy?> GetFallbackPolicyAsync()
=> FallbackProvider.GetFallbackPolicyAsync();
{
return 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.Interfaces;
using Moonlight.Api.Models;
using Moonlight.Api.Services;
using Moonlight.Api.Admin.Sys;
using Moonlight.Api.Admin.Sys.Diagnose;
using Moonlight.Api.Infrastructure.Hooks;
namespace Moonlight.Api.Implementations;
namespace Moonlight.Api.Infrastructure.Implementations;
public sealed class UpdateDiagnoseProvider : IDiagnoseProvider
{

View File

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

View File

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

View File

@@ -6,7 +6,7 @@
<ImplicitUsings>enable</ImplicitUsings>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
</PropertyGroup>
<PropertyGroup Label="Nuget Package Settings">
<Version>2.1.0</Version>
<Title>Moonlight.Api</Title>
@@ -27,12 +27,12 @@
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="10.0.3"/>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="10.0.3"/>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.3"/>
<PackageReference Include="Microsoft.Extensions.Caching.Hybrid" Version="10.3.0" />
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="10.0.3" />
<PackageReference Include="Microsoft.Extensions.Caching.Hybrid" Version="10.3.0"/>
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="10.0.3"/>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0"/>
<PackageReference Include="Riok.Mapperly" Version="4.3.1"/>
<PackageReference Include="SimplePlugin.Abstractions" Version="1.0.2" />
<PackageReference Include="VYaml" Version="1.2.0" />
<PackageReference Include="SimplePlugin.Abstractions" Version="1.0.2"/>
<PackageReference Include="VYaml" Version="1.2.0"/>
</ItemGroup>
<ItemGroup>
@@ -42,6 +42,6 @@
</ItemGroup>
<ItemGroup>
<Folder Include="Http\Services\ContainerHelper\Responses\" />
<Folder Include="Client\"/>
</ItemGroup>
</Project>

View File

@@ -1,6 +1,4 @@
using System.Reflection;
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Builder;
using SimplePlugin.Abstractions;
namespace Moonlight.Api;
@@ -13,7 +11,7 @@ public abstract class MoonlightPlugin : IPluginModule
{
Plugins = plugins;
}
public virtual void PreBuild(WebApplicationBuilder builder)
{
}

View File

@@ -1,9 +1,9 @@
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Moonlight.Shared.Http.Responses.Admin.Auth;
using Moonlight.Shared.Shared.Auth;
namespace Moonlight.Api.Http.Controllers;
namespace Moonlight.Api.Shared.Auth;
[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,9 +1,8 @@
using System.Diagnostics.CodeAnalysis;
using Moonlight.Api.Models;
using Moonlight.Shared.Http.Responses.Admin.Frontend;
using Moonlight.Shared.Shared.Frontend;
using Riok.Mapperly.Abstractions;
namespace Moonlight.Api.Mappers;
namespace Moonlight.Api.Shared.Frontend;
[Mapper]
[SuppressMessage("Mapper", "RMG020:No members are mapped in an object mapping")]

View File

@@ -1,3 +1,3 @@
namespace Moonlight.Api.Models;
namespace Moonlight.Api.Shared.Frontend;
public record FrontendConfiguration(string Name, string? ThemeCss);

View File

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

View File

@@ -1,24 +1,23 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using Moonlight.Api.Configuration;
using Moonlight.Api.Constants;
using Moonlight.Api.Database;
using Moonlight.Api.Database.Entities;
using Moonlight.Api.Models;
using Moonlight.Api.Admin.Sys.Settings;
using Moonlight.Api.Admin.Sys.Versions;
using Moonlight.Api.Infrastructure.Database;
using Moonlight.Api.Infrastructure.Database.Entities;
namespace Moonlight.Api.Services;
namespace Moonlight.Api.Shared.Frontend;
public class FrontendService
{
private const string CacheKey = $"Moonlight.{nameof(FrontendService)}.{nameof(GetConfigurationAsync)}";
private readonly IMemoryCache Cache;
private readonly DatabaseRepository<Theme> ThemeRepository;
private readonly IOptions<FrontendOptions> Options;
private readonly SettingsService SettingsService;
private const string CacheKey = $"Moonlight.{nameof(FrontendService)}.{nameof(GetConfigurationAsync)}";
private readonly DatabaseRepository<Theme> ThemeRepository;
public FrontendService(IMemoryCache cache, DatabaseRepository<Theme> themeRepository, IOptions<FrontendOptions> options, SettingsService settingsService)
public FrontendService(IMemoryCache cache, DatabaseRepository<Theme> themeRepository,
IOptions<FrontendOptions> options, SettingsService settingsService)
{
Cache = cache;
ThemeRepository = themeRepository;
@@ -29,17 +28,15 @@ public class FrontendService
public async Task<FrontendConfiguration> GetConfigurationAsync()
{
if (Cache.TryGetValue(CacheKey, out FrontendConfiguration? value))
{
if (value != null)
return value;
}
var theme = await ThemeRepository
.Query()
.FirstOrDefaultAsync(x => x.IsEnabled);
var name = await SettingsService.GetValueAsync<string>(FrontendSettingConstants.Name);
var config = new FrontendConfiguration(name ?? "Moonlight", theme?.CssContent);
Cache.Set(CacheKey, config, TimeSpan.FromMinutes(Options.Value.CacheMinutes));

View File

@@ -1,4 +1,4 @@
namespace Moonlight.Api.Constants;
namespace Moonlight.Api.Shared.Frontend;
public class FrontendSettingConstants
{

View File

@@ -1,7 +1,7 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Moonlight.Api.Http.Controllers;
namespace Moonlight.Api.Shared;
[ApiController]
[Route("api/ping")]
@@ -9,5 +9,8 @@ public class PingController : Controller
{
[HttpGet]
[AllowAnonymous]
public IActionResult Get() => Ok("Pong");
public IActionResult Get()
{
return Ok("Pong");
}
}

View File

@@ -6,10 +6,11 @@ using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Moonlight.Api.Configuration;
using Moonlight.Api.Implementations;
using Moonlight.Api.Implementations.ApiKeyScheme;
using Moonlight.Api.Services;
using Moonlight.Api.Admin.Sys.ApiKeys;
using Moonlight.Api.Admin.Sys.ApiKeys.Scheme;
using Moonlight.Api.Admin.Users.Users;
using Moonlight.Api.Infrastructure.Configuration;
using Moonlight.Api.Infrastructure.Implementations;
namespace Moonlight.Api.Startup;
@@ -25,7 +26,7 @@ public partial class Startup
var apiKeyOptions = new ApiOptions();
builder.Configuration.GetSection("Moonlight:Api").Bind(apiKeyOptions);
builder.Services.AddOptions<ApiOptions>().BindConfiguration("Moonlight:Api");
// Session
builder.Services.AddOptions<UserOptions>().BindConfiguration("Moonlight:User");
@@ -67,7 +68,7 @@ public partial class Startup
context.RejectPrincipal();
};
options.Cookie = new CookieBuilder()
options.Cookie = new CookieBuilder
{
Name = "token",
Path = "/",
@@ -109,7 +110,7 @@ public partial class Startup
options.LookupL1CacheTime = apiKeyOptions.LookupCacheL1Expiry;
options.LookupL2CacheTime = apiKeyOptions.LookupCacheL2Expiry;
});
// Authorization
builder.Services.AddAuthorization();
@@ -119,7 +120,7 @@ public partial class Startup
// Custom permission handling using named policies
builder.Services.AddSingleton<IAuthorizationHandler, PermissionAuthorizationHandler>();
builder.Services.AddSingleton<IAuthorizationPolicyProvider, PermissionPolicyProvider>();
builder.Services.AddScoped<UserDeletionService>();
builder.Services.AddScoped<UserLogoutService>();
builder.Services.AddScoped<UserAuthService>();

View File

@@ -5,12 +5,18 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Console;
using Microsoft.Extensions.Options;
using Moonlight.Api.Configuration;
using Moonlight.Shared.Http;
using Moonlight.Api.Helpers;
using Moonlight.Api.Implementations;
using Moonlight.Api.Interfaces;
using Moonlight.Api.Services;
using Moonlight.Api.Admin.Sys;
using Moonlight.Api.Admin.Sys.ContainerHelper;
using Moonlight.Api.Admin.Sys.Diagnose;
using Moonlight.Api.Admin.Sys.Settings;
using Moonlight.Api.Admin.Sys.Versions;
using Moonlight.Api.Admin.Users.Users;
using Moonlight.Api.Infrastructure.Helpers;
using Moonlight.Api.Infrastructure.Hooks;
using Moonlight.Api.Infrastructure.Implementations;
using Moonlight.Api.Shared.Frontend;
using SerializationContext = Moonlight.Shared.SerializationContext;
using VersionService = Moonlight.Api.Admin.Sys.Versions.VersionService;
namespace Moonlight.Api.Startup;

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