diff --git a/Moonlight.ApiServer/Attributes/RequirePermissionAttribute.cs b/Moonlight.ApiServer/Attributes/RequirePermissionAttribute.cs deleted file mode 100644 index 3b128a27..00000000 --- a/Moonlight.ApiServer/Attributes/RequirePermissionAttribute.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Moonlight.ApiServer.Attributes; - -public class RequirePermissionAttribute : Attribute -{ - public string Permission { get; set; } - - public RequirePermissionAttribute(string permission) - { - Permission = permission; - } -} \ No newline at end of file diff --git a/Moonlight.ApiServer/Database/CoreDataContext.cs b/Moonlight.ApiServer/Database/CoreDataContext.cs index 51fa8b70..4a7ea146 100644 --- a/Moonlight.ApiServer/Database/CoreDataContext.cs +++ b/Moonlight.ApiServer/Database/CoreDataContext.cs @@ -9,4 +9,5 @@ public class CoreDataContext : DatabaseContext public override string Prefix { get; } = "Core"; public DbSet Users { get; set; } + public DbSet ApiKeys { get; set; } } \ No newline at end of file diff --git a/Moonlight.ApiServer/Database/Entities/ApiKey.cs b/Moonlight.ApiServer/Database/Entities/ApiKey.cs new file mode 100644 index 00000000..b0e637c2 --- /dev/null +++ b/Moonlight.ApiServer/Database/Entities/ApiKey.cs @@ -0,0 +1,11 @@ +namespace Moonlight.ApiServer.Database.Entities; + +public class ApiKey +{ + public int Id { get; set; } + + public string Secret { get; set; } + public string Description { get; set; } + public string PermissionsJson { get; set; } = "[]"; + public DateTime ExpiresAt { get; set; } +} \ No newline at end of file diff --git a/Moonlight.ApiServer/Database/Migrations/20241029134013_AddedApiKeys.Designer.cs b/Moonlight.ApiServer/Database/Migrations/20241029134013_AddedApiKeys.Designer.cs new file mode 100644 index 00000000..7895b1f1 --- /dev/null +++ b/Moonlight.ApiServer/Database/Migrations/20241029134013_AddedApiKeys.Designer.cs @@ -0,0 +1,102 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Moonlight.ApiServer.Database; + +#nullable disable + +namespace Moonlight.ApiServer.Database.Migrations +{ + [DbContext(typeof(CoreDataContext))] + [Migration("20241029134013_AddedApiKeys")] + partial class AddedApiKeys + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("Core") + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("Moonlight.ApiServer.Database.Entities.ApiKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ExpiresAt") + .HasColumnType("datetime(6)"); + + b.Property("PermissionsJson") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Secret") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("ApiKeys", "Core"); + }); + + modelBuilder.Entity("Moonlight.ApiServer.Database.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AccessToken") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Email") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Password") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("PermissionsJson") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("RefreshTimestamp") + .HasColumnType("datetime(6)"); + + b.Property("RefreshToken") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TokenValidTimestamp") + .HasColumnType("datetime(6)"); + + b.Property("Username") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Users", "Core"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Moonlight.ApiServer/Database/Migrations/20241029134013_AddedApiKeys.cs b/Moonlight.ApiServer/Database/Migrations/20241029134013_AddedApiKeys.cs new file mode 100644 index 00000000..bbc69b7c --- /dev/null +++ b/Moonlight.ApiServer/Database/Migrations/20241029134013_AddedApiKeys.cs @@ -0,0 +1,45 @@ +using System; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Moonlight.ApiServer.Database.Migrations +{ + /// + public partial class AddedApiKeys : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "ApiKeys", + schema: "Core", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + Secret = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Description = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + PermissionsJson = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + ExpiresAt = table.Column(type: "datetime(6)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ApiKeys", x => x.Id); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ApiKeys", + schema: "Core"); + } + } +} diff --git a/Moonlight.ApiServer/Database/Migrations/CoreDataContextModelSnapshot.cs b/Moonlight.ApiServer/Database/Migrations/CoreDataContextModelSnapshot.cs index 45d9f8b5..a6a8a462 100644 --- a/Moonlight.ApiServer/Database/Migrations/CoreDataContextModelSnapshot.cs +++ b/Moonlight.ApiServer/Database/Migrations/CoreDataContextModelSnapshot.cs @@ -23,6 +23,34 @@ namespace Moonlight.ApiServer.Database.Migrations MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + modelBuilder.Entity("Moonlight.ApiServer.Database.Entities.ApiKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ExpiresAt") + .HasColumnType("datetime(6)"); + + b.Property("PermissionsJson") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Secret") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("ApiKeys", "Core"); + }); + modelBuilder.Entity("Moonlight.ApiServer.Database.Entities.User", b => { b.Property("Id") diff --git a/Moonlight.ApiServer/Http/Controllers/Admin/ApiKeys/ApiKeysController.cs b/Moonlight.ApiServer/Http/Controllers/Admin/ApiKeys/ApiKeysController.cs new file mode 100644 index 00000000..ad565abd --- /dev/null +++ b/Moonlight.ApiServer/Http/Controllers/Admin/ApiKeys/ApiKeysController.cs @@ -0,0 +1,64 @@ +using Microsoft.AspNetCore.Mvc; +using MoonCore.Blazor.Tailwind.Attributes; +using MoonCore.Extended.Abstractions; +using MoonCore.Extended.Helpers; +using MoonCore.Helpers; +using MoonCore.Models; +using Moonlight.ApiServer.Database.Entities; +using Moonlight.Shared.Http.Requests.Admin.ApiKeys; +using Moonlight.Shared.Http.Responses.Admin.ApiKeys; + +namespace Moonlight.ApiServer.Http.Controllers.Admin.ApiKeys; + +[ApiController] +[Route("api/admin/apikeys")] +public class ApiKeysController : Controller +{ + private readonly CrudHelper CrudHelper; + private readonly DatabaseRepository ApiKeyRepository; + + public ApiKeysController(CrudHelper crudHelper, DatabaseRepository apiKeyRepository) + { + CrudHelper = crudHelper; + ApiKeyRepository = apiKeyRepository; + } + + [HttpGet] + [RequirePermission("admin.apikeys.read")] + public async Task> Get([FromQuery] int page, [FromQuery] int pageSize = 50) + => await CrudHelper.Get(page, pageSize); + + [HttpGet("{id}")] + [RequirePermission("admin.apikeys.read")] + public async Task GetSingle(int id) + => await CrudHelper.GetSingle(id); + + [HttpPost] + [RequirePermission("admin.apikeys.create")] + public async Task Create([FromBody] CreateApiKeyRequest request) + { + var secret = "api_" + Formatter.GenerateString(32); + + var apiKey = new ApiKey() + { + Description = request.Description, + PermissionsJson = request.PermissionsJson, + ExpiresAt = request.ExpiresAt, + Secret = secret + }; + + var finalApiKey = ApiKeyRepository.Add(apiKey); + + return Mapper.Map(finalApiKey); + } + + [HttpPatch("{id}")] + [RequirePermission("admin.apikeys.update")] + public async Task Update([FromRoute] int id, [FromBody] UpdateApiKeyRequest request) + => await CrudHelper.Update(id, request); + + [HttpDelete("{id}")] + [RequirePermission("admin.apikeys.delete")] + public async Task Delete([FromRoute] int id) + => await CrudHelper.Delete(id); +} \ No newline at end of file diff --git a/Moonlight.ApiServer/Http/Controllers/Admin/Users/UsersController.cs b/Moonlight.ApiServer/Http/Controllers/Admin/Users/UsersController.cs index 507241d0..0527f1e2 100644 --- a/Moonlight.ApiServer/Http/Controllers/Admin/Users/UsersController.cs +++ b/Moonlight.ApiServer/Http/Controllers/Admin/Users/UsersController.cs @@ -1,9 +1,9 @@ using Microsoft.AspNetCore.Mvc; +using MoonCore.Blazor.Tailwind.Attributes; using MoonCore.Exceptions; using MoonCore.Extended.Abstractions; using MoonCore.Extended.Helpers; using MoonCore.Models; -using Moonlight.ApiServer.Attributes; using Moonlight.ApiServer.Database.Entities; using Moonlight.Shared.Http.Requests.Admin.Users; using Moonlight.Shared.Http.Responses.Admin.Users; diff --git a/Moonlight.ApiServer/Http/Controllers/Auth/AuthController.cs b/Moonlight.ApiServer/Http/Controllers/Auth/AuthController.cs index c1cf59b1..58734334 100644 --- a/Moonlight.ApiServer/Http/Controllers/Auth/AuthController.cs +++ b/Moonlight.ApiServer/Http/Controllers/Auth/AuthController.cs @@ -1,13 +1,12 @@ using System.Text.Json; using Microsoft.AspNetCore.Mvc; +using MoonCore.Blazor.Tailwind.Attributes; using MoonCore.Exceptions; using MoonCore.Extended.Abstractions; using MoonCore.Extended.Helpers; using MoonCore.Extended.OAuth2.ApiServer; using MoonCore.Helpers; -using MoonCore.PluginFramework.Services; using MoonCore.Services; -using Moonlight.ApiServer.Attributes; using Moonlight.ApiServer.Configuration; using Moonlight.ApiServer.Database.Entities; using Moonlight.ApiServer.Helpers.Authentication; diff --git a/Moonlight.ApiServer/Http/Middleware/AuthorizationMiddleware.cs b/Moonlight.ApiServer/Http/Middleware/AuthorizationMiddleware.cs index 4159bdcb..cd238b79 100644 --- a/Moonlight.ApiServer/Http/Middleware/AuthorizationMiddleware.cs +++ b/Moonlight.ApiServer/Http/Middleware/AuthorizationMiddleware.cs @@ -1,5 +1,5 @@ using Microsoft.AspNetCore.Mvc.Controllers; -using Moonlight.ApiServer.Attributes; +using MoonCore.Blazor.Tailwind.Attributes; using Moonlight.ApiServer.Exceptions; using Moonlight.ApiServer.Helpers.Authentication; diff --git a/Moonlight.ApiServer/Startup.cs b/Moonlight.ApiServer/Startup.cs index 77669c74..4f22a96e 100644 --- a/Moonlight.ApiServer/Startup.cs +++ b/Moonlight.ApiServer/Startup.cs @@ -41,7 +41,8 @@ public static class Startup var logLevels = new Dictionary { { "Default", "Information" }, - { "Microsoft.AspNetCore", "Warning" } + { "Microsoft.AspNetCore", "Warning" }, + { "System.Net.Http.HttpClient", "Warning" } }; var logLevelsJson = JsonSerializer.Serialize(logLevels); @@ -113,7 +114,7 @@ public static class Startup databaseHelper.AddDbContext(database); builder.Services.AddScoped(database); } - + databaseHelper.GenerateMappings(); builder.Services.AddSingleton(databaseHelper); @@ -163,7 +164,7 @@ public static class Startup }); if (!config.Authentication.UseLocalOAuth2) return Task.CompletedTask; - + logger.LogInformation("Using local oauth2 provider"); builder.Services.AddOAuth2Provider(configuration => diff --git a/Moonlight.Client/Moonlight.Client.csproj b/Moonlight.Client/Moonlight.Client.csproj index c38b3628..4b638f22 100644 --- a/Moonlight.Client/Moonlight.Client.csproj +++ b/Moonlight.Client/Moonlight.Client.csproj @@ -26,7 +26,6 @@ - diff --git a/Moonlight.Client/Program.cs b/Moonlight.Client/Program.cs index 60e66cf3..4a20b3c5 100644 --- a/Moonlight.Client/Program.cs +++ b/Moonlight.Client/Program.cs @@ -16,6 +16,7 @@ using Moonlight.Client.Implementations; using Moonlight.Client.Interfaces; using Moonlight.Client.Services; using Moonlight.Client.UI; +using Moonlight.Client.UI.Forms; using Moonlight.Shared.Http.Requests.Auth; using Moonlight.Shared.Http.Responses.Auth; @@ -104,6 +105,7 @@ builder.Services.AutoAddServices(); FormComponentRepository.Set(); FormComponentRepository.Set(); +FormComponentRepository.Set(); // Interface service builder.Services.AddPlugins(configuration => diff --git a/Moonlight.Client/UI/App.razor b/Moonlight.Client/UI/App.razor index 8b6ed3d4..9b4d90b4 100644 --- a/Moonlight.Client/UI/App.razor +++ b/Moonlight.Client/UI/App.razor @@ -9,7 +9,14 @@ Not found -

Sorry, there's nothing at this address.

+
+ Not found illustration + +

Page not found

+

+ The page you requested does not exist +

+
\ No newline at end of file diff --git a/Moonlight.Client/UI/Components/StatCard.razor b/Moonlight.Client/UI/Components/StatCard.razor new file mode 100644 index 00000000..cc67aa12 --- /dev/null +++ b/Moonlight.Client/UI/Components/StatCard.razor @@ -0,0 +1,16 @@ +
+
+

+ @Text +

+ +
+

@Title

+
+ +@code +{ + [Parameter] public string Title { get; set; } + [Parameter] public string Text { get; set; } + [Parameter] public string Icon { get; set; } +} diff --git a/Moonlight.Client/UI/Forms/DateComponent.razor b/Moonlight.Client/UI/Forms/DateComponent.razor new file mode 100644 index 00000000..f58ea2fa --- /dev/null +++ b/Moonlight.Client/UI/Forms/DateComponent.razor @@ -0,0 +1,3 @@ +@inherits BaseFormComponent + + \ No newline at end of file diff --git a/Moonlight.Client/UI/Views/Admin/Api/Index.razor b/Moonlight.Client/UI/Views/Admin/Api/Index.razor new file mode 100644 index 00000000..cd7e14aa --- /dev/null +++ b/Moonlight.Client/UI/Views/Admin/Api/Index.razor @@ -0,0 +1,111 @@ +@page "/admin/api" + +@using MoonCore.Blazor.Tailwind.Attributes +@using MoonCore.Helpers +@using MoonCore.Models +@using Moonlight.Shared.Http.Requests.Admin.ApiKeys +@using Moonlight.Shared.Http.Responses.Admin.ApiKeys + +@attribute [RequirePermission("admin.apikeys.read")] + +@inject HttpApiClient HttpApiClient +@inject AlertService AlertService + +
+
+

+ API Documentation +

+ +
Open
+
+ +
+
+
+ +
+

+ Learn about the api usage +

+ +
Open
+
+ +
+
+
+ +
+

+ Open API Specification +

+ +
Open
+
+ +
+
+
+
+ + + + + + + + + + + +@code +{ + private void OnConfigure(CrudOptions crudOptions) + { + crudOptions.ItemName = "API Key"; + + crudOptions.ItemLoader = async (page, pageSize) + => await HttpApiClient.GetJson>($"api/admin/apikeys?page={page}&pageSize={pageSize}"); + + crudOptions.SingleItemLoader = async id + => await HttpApiClient.GetJson($"api/admin/apikeys/{id}"); + + crudOptions.QueryIdentifier = response => response.Id.ToString(); + + crudOptions.OnCreate = async request => + { + var response = await HttpApiClient.PostJson("api/admin/apikeys", request); + + await AlertService.Success( + "API Key successfully created", + $"Copy the following secret. It wont be shown again. '{response.Secret}'" + ); + }; + + crudOptions.OnUpdate = async (item, request) + => await HttpApiClient.Patch($"api/admin/apikeys/{item.Id}", request); + + crudOptions.OnDelete = async item + => await HttpApiClient.Delete($"api/admin/apikeys/{item.Id}"); + + crudOptions.OnConfigureCreate = configuration => + { + configuration.WithField(x => x.Description); + configuration.WithField(x => x.PermissionsJson); + configuration.WithField(x => x.ExpiresAt, fieldConfiguration => { fieldConfiguration.DefaultValue = DateTime.UtcNow.AddMonths(1); }); + }; + + crudOptions.OnConfigureUpdate = (_, configuration) => + { + configuration.WithField(x => x.Description); + configuration.WithField(x => x.PermissionsJson); + configuration.WithField(x => x.ExpiresAt); + }; + } +} \ No newline at end of file diff --git a/Moonlight.Client/UI/Views/Admin/Sys/Index.razor b/Moonlight.Client/UI/Views/Admin/Sys/Index.razor new file mode 100644 index 00000000..be694ea7 --- /dev/null +++ b/Moonlight.Client/UI/Views/Admin/Sys/Index.razor @@ -0,0 +1,35 @@ +@page "/admin/system" + +@using MoonCore.Blazor.Tailwind.Attributes +@using Moonlight.Client.UI.Components + +@attribute [RequirePermission("admin.system.read")] + +
+ + + + + +
+
+
+
+ +
+
+
+ +
+ Restart +
+
+
+ +@code +{ + private async Task Restart(WButton _) + { + + } +} diff --git a/Moonlight.Client/UI/Views/Admin/Users/Index.razor b/Moonlight.Client/UI/Views/Admin/Users/Index.razor index 6ea127aa..6652fce9 100644 --- a/Moonlight.Client/UI/Views/Admin/Users/Index.razor +++ b/Moonlight.Client/UI/Views/Admin/Users/Index.razor @@ -34,7 +34,7 @@ crudOptions.SingleItemLoader = async id => await HttpApiClient.GetJson($"api/admin/users/{id}"); - crudOptions.QueryIdentifier = item => item.Id.ToString(); //TODO: Make this default + crudOptions.QueryIdentifier = response => response.Id.ToString(); crudOptions.OnCreate = async request => await HttpApiClient.Post("api/admin/users", request); diff --git a/Moonlight.Client/wwwroot/svg/notfound.svg b/Moonlight.Client/wwwroot/svg/notfound.svg new file mode 100644 index 00000000..8174fed0 --- /dev/null +++ b/Moonlight.Client/wwwroot/svg/notfound.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Moonlight.Shared/Http/Requests/Admin/ApiKeys/CreateApiKeyRequest.cs b/Moonlight.Shared/Http/Requests/Admin/ApiKeys/CreateApiKeyRequest.cs new file mode 100644 index 00000000..6b399149 --- /dev/null +++ b/Moonlight.Shared/Http/Requests/Admin/ApiKeys/CreateApiKeyRequest.cs @@ -0,0 +1,15 @@ +using System.ComponentModel.DataAnnotations; + +namespace Moonlight.Shared.Http.Requests.Admin.ApiKeys; + +public class CreateApiKeyRequest +{ + [Required(ErrorMessage = "You need to specify a description")] + public string Description { get; set; } + + [Required(ErrorMessage = "You need to specify permissions for the api key")] + public string PermissionsJson { get; set; } = "[]"; + + [Required(ErrorMessage = "You need to specify an expire date")] + public DateTime ExpiresAt { get; set; } +} \ No newline at end of file diff --git a/Moonlight.Shared/Http/Requests/Admin/ApiKeys/UpdateApiKeyRequest.cs b/Moonlight.Shared/Http/Requests/Admin/ApiKeys/UpdateApiKeyRequest.cs new file mode 100644 index 00000000..3dfd206a --- /dev/null +++ b/Moonlight.Shared/Http/Requests/Admin/ApiKeys/UpdateApiKeyRequest.cs @@ -0,0 +1,15 @@ +using System.ComponentModel.DataAnnotations; + +namespace Moonlight.Shared.Http.Requests.Admin.ApiKeys; + +public class UpdateApiKeyRequest +{ + [Required(ErrorMessage = "You need to specify a description")] + public string Description { get; set; } + + [Required(ErrorMessage = "You need to specify permissions for the api key")] + public string PermissionsJson { get; set; } = "[]"; + + [Required(ErrorMessage = "You need to specify an expire date")] + public DateTime ExpiresAt { get; set; } +} \ No newline at end of file diff --git a/Moonlight.Shared/Http/Responses/Admin/ApiKeys/ApiKeyDetailResponse.cs b/Moonlight.Shared/Http/Responses/Admin/ApiKeys/ApiKeyDetailResponse.cs new file mode 100644 index 00000000..0f6c1aa3 --- /dev/null +++ b/Moonlight.Shared/Http/Responses/Admin/ApiKeys/ApiKeyDetailResponse.cs @@ -0,0 +1,9 @@ +namespace Moonlight.Shared.Http.Responses.Admin.ApiKeys; + +public class ApiKeyDetailResponse +{ + public int Id { get; set; } + public string Description { get; set; } + public string PermissionsJson { get; set; } = "[]"; + public DateTime ExpiresAt { get; set; } +} \ No newline at end of file diff --git a/Moonlight.Shared/Http/Responses/Admin/ApiKeys/CreateApiKeyResponse.cs b/Moonlight.Shared/Http/Responses/Admin/ApiKeys/CreateApiKeyResponse.cs new file mode 100644 index 00000000..f8c865ff --- /dev/null +++ b/Moonlight.Shared/Http/Responses/Admin/ApiKeys/CreateApiKeyResponse.cs @@ -0,0 +1,10 @@ +namespace Moonlight.Shared.Http.Responses.Admin.ApiKeys; + +public class CreateApiKeyResponse +{ + public int Id { get; set; } + public string Secret { get; set; } + public string Description { get; set; } + public string PermissionsJson { get; set; } = "[]"; + public DateTime ExpiresAt { get; set; } +} \ No newline at end of file