diff --git a/Moonlight.ApiServer/Http/Controllers/Admin/ApiKeys/ApiKeysController.cs b/Moonlight.ApiServer/Http/Controllers/Admin/ApiKeys/ApiKeysController.cs index 8cedfb93..821df47d 100644 --- a/Moonlight.ApiServer/Http/Controllers/Admin/ApiKeys/ApiKeysController.cs +++ b/Moonlight.ApiServer/Http/Controllers/Admin/ApiKeys/ApiKeysController.cs @@ -1,12 +1,10 @@ -using System.ComponentModel.DataAnnotations; -using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; -using MoonCore.Exceptions; using MoonCore.Extended.Abstractions; -using MoonCore.Extended.Models; using MoonCore.Models; using Moonlight.ApiServer.Database.Entities; +using Moonlight.ApiServer.Mappers; using Moonlight.ApiServer.Services; using Moonlight.Shared.Http.Requests.Admin.ApiKeys; using Moonlight.Shared.Http.Responses.Admin.ApiKeys; @@ -28,69 +26,82 @@ public class ApiKeysController : Controller [HttpGet] [Authorize(Policy = "permissions:admin.apikeys.get")] - public async Task> Get([FromQuery] PagedOptions options) + public async Task>> Get( + [FromQuery] int startIndex, + [FromQuery] int count, + [FromQuery] string? orderBy, + [FromQuery] string? filter, + [FromQuery] string orderByDir = "asc" + ) { - var count = await ApiKeyRepository.Get().CountAsync(); + if (count > 100) + return Problem("You cannot fetch more items than 100 at a time", statusCode: 400); + + IQueryable query = ApiKeyRepository.Get(); - var apiKeys = await ApiKeyRepository - .Get() - .OrderBy(x => x.Id) - .Skip(options.Page * options.PageSize) - .Take(options.PageSize) + query = orderBy switch + { + nameof(ApiKey.Id) => orderByDir == "desc" + ? query.OrderByDescending(x => x.Id) + : query.OrderBy(x => x.Id), + + nameof(ApiKey.ExpiresAt) => orderByDir == "desc" + ? query.OrderByDescending(x => x.ExpiresAt) + : query.OrderBy(x => x.ExpiresAt), + + nameof(ApiKey.CreatedAt) => orderByDir == "desc" + ? query.OrderByDescending(x => x.CreatedAt) + : query.OrderBy(x => x.CreatedAt), + + _ => query.OrderBy(x => x.Id) + }; + + if (!string.IsNullOrEmpty(filter)) + { + query = query.Where(x => + EF.Functions.ILike(x.Description, $"%{filter}%") + ); + } + + var totalCount = await query.CountAsync(); + + var items = await query + .Skip(startIndex) + .Take(count) + .AsNoTracking() + .ProjectToResponse() .ToArrayAsync(); - var mappedApiKey = apiKeys - .Select(x => new ApiKeyResponse() - { - Id = x.Id, - Permissions = x.Permissions, - Description = x.Description, - ExpiresAt = x.ExpiresAt - }) - .ToArray(); - - return new PagedData() + return new CountedData() { - CurrentPage = options.Page, - Items = mappedApiKey, - PageSize = options.PageSize, - TotalItems = count, - TotalPages = count == 0 ? 0 : (count - 1) / options.PageSize + Items = items, + TotalCount = totalCount }; } - [HttpGet("{id}")] + [HttpGet("{id:int}")] [Authorize(Policy = "permissions:admin.apikeys.get")] - public async Task GetSingle(int id) + public async Task> GetSingle(int id) { var apiKey = await ApiKeyRepository .Get() + .AsNoTracking() + .ProjectToResponse() .FirstOrDefaultAsync(x => x.Id == id); if (apiKey == null) - throw new HttpApiException("No api key with that id found", 404); + return Problem("No api key with that id found", statusCode: 404); - return new ApiKeyResponse() - { - Id = apiKey.Id, - Permissions = apiKey.Permissions, - Description = apiKey.Description, - ExpiresAt = apiKey.ExpiresAt - }; + return apiKey; } [HttpPost] [Authorize(Policy = "permissions:admin.apikeys.create")] public async Task Create([FromBody] CreateApiKeyRequest request) { - var apiKey = new ApiKey() - { - Description = request.Description, - Permissions = request.Permissions, - ExpiresAt = request.ExpiresAt - }; - - var finalApiKey = await ApiKeyRepository.Add(apiKey); + var apiKey = ApiKeyMapper.ToApiKey(request); + + var finalApiKey = await ApiKeyRepository.AddAsync(apiKey); var response = new CreateApiKeyResponse { @@ -104,41 +115,36 @@ public class ApiKeysController : Controller return response; } - [HttpPatch("{id}")] + [HttpPatch("{id:int}")] [Authorize(Policy = "permissions:admin.apikeys.update")] - public async Task Update([FromRoute] int id, [FromBody] UpdateApiKeyRequest request) + public async Task> Update([FromRoute] int id, [FromBody] UpdateApiKeyRequest request) { var apiKey = await ApiKeyRepository .Get() .FirstOrDefaultAsync(x => x.Id == id); if (apiKey == null) - throw new HttpApiException("No api key with that id found", 404); + return Problem("No api key with that id found", statusCode: 404); - apiKey.Description = request.Description; + ApiKeyMapper.Merge(apiKey, request); - await ApiKeyRepository.Update(apiKey); + await ApiKeyRepository.UpdateAsync(apiKey); - return new ApiKeyResponse() - { - Id = apiKey.Id, - Description = apiKey.Description, - Permissions = apiKey.Permissions, - ExpiresAt = apiKey.ExpiresAt - }; + return ApiKeyMapper.ToResponse(apiKey); } - [HttpDelete("{id}")] + [HttpDelete("{id:int}")] [Authorize(Policy = "permissions:admin.apikeys.delete")] - public async Task Delete([FromRoute] int id) + public async Task Delete([FromRoute] int id) { var apiKey = await ApiKeyRepository .Get() .FirstOrDefaultAsync(x => x.Id == id); if (apiKey == null) - throw new HttpApiException("No api key with that id found", 404); + return Problem("No api key with that id found", statusCode: 404); - await ApiKeyRepository.Remove(apiKey); + await ApiKeyRepository.RemoveAsync(apiKey); + return NoContent(); } } \ No newline at end of file diff --git a/Moonlight.ApiServer/Http/Controllers/Admin/Sys/Customisation/ThemesController.cs b/Moonlight.ApiServer/Http/Controllers/Admin/Sys/Customisation/ThemesController.cs index 73867116..bcd112c7 100644 --- a/Moonlight.ApiServer/Http/Controllers/Admin/Sys/Customisation/ThemesController.cs +++ b/Moonlight.ApiServer/Http/Controllers/Admin/Sys/Customisation/ThemesController.cs @@ -26,65 +26,96 @@ public class ThemesController : Controller [HttpGet] [Authorize(Policy = "permissions:admin.system.customisation.themes.read")] - public async Task> Get([FromQuery] PagedOptions options) + public async Task>> Get( + [FromQuery] int startIndex, + [FromQuery] int count, + [FromQuery] string? orderBy, + [FromQuery] string? filter, + [FromQuery] string orderByDir = "asc" + ) { - var count = await ThemeRepository.Get().CountAsync(); + if (count > 100) + return Problem("You cannot fetch more items than 100 at a time", statusCode: 400); - var items = await ThemeRepository - .Get() - .Skip(options.Page * options.PageSize) - .Take(options.PageSize) - .ToArrayAsync(); - - var mappedItems = items - .Select(ThemeMapper.ToResponse) - .ToArray(); - - return new PagedData() + IQueryable query = ThemeRepository.Get(); + + query = orderBy switch { - CurrentPage = options.Page, - Items = mappedItems, - PageSize = options.PageSize, - TotalItems = count, - TotalPages = count == 0 ? 0 : (count - 1) / options.PageSize + nameof(Theme.Id) => orderByDir == "desc" + ? query.OrderByDescending(x => x.Id) + : query.OrderBy(x => x.Id), + + nameof(Theme.Name) => orderByDir == "desc" + ? query.OrderByDescending(x => x.Name) + : query.OrderBy(x => x.Name), + + nameof(Theme.Version) => orderByDir == "desc" + ? query.OrderByDescending(x => x.Version) + : query.OrderBy(x => x.Version), + + _ => query.OrderBy(x => x.Id) + }; + + if (!string.IsNullOrEmpty(filter)) + { + query = query.Where(x => + EF.Functions.ILike(x.Name, $"%{filter}%") + ); + } + + var totalCount = await query.CountAsync(); + + var items = await query + .Skip(startIndex) + .Take(count) + .AsNoTracking() + .ProjectToResponse() + .ToArrayAsync(); + + return new CountedData() + { + Items = items, + TotalCount = totalCount }; } [HttpGet("{id:int}")] [Authorize(Policy = "permissions:admin.system.customisation.themes.read")] - public async Task GetSingle([FromRoute] int id) + public async Task> GetSingle([FromRoute] int id) { var theme = await ThemeRepository .Get() + .AsNoTracking() + .ProjectToResponse() .FirstOrDefaultAsync(t => t.Id == id); if (theme == null) - throw new HttpApiException("Theme with this id not found", 404); - - return ThemeMapper.ToResponse(theme); + return Problem("Theme with this id not found", statusCode: 404); + + return theme; } [HttpPost] [Authorize(Policy = "permissions:admin.system.customisation.themes.write")] - public async Task Create([FromBody] CreateThemeRequest request) + public async Task> Create([FromBody] CreateThemeRequest request) { var theme = ThemeMapper.ToTheme(request); - - var finalTheme = await ThemeRepository.Add(theme); - + + var finalTheme = await ThemeRepository.AddAsync(theme); + return ThemeMapper.ToResponse(finalTheme); } [HttpPatch("{id:int}")] [Authorize(Policy = "permissions:admin.system.customisation.themes.write")] - public async Task Update([FromRoute] int id, [FromBody] UpdateThemeRequest request) + public async Task> Update([FromRoute] int id, [FromBody] UpdateThemeRequest request) { var theme = await ThemeRepository .Get() .FirstOrDefaultAsync(t => t.Id == id); - + if (theme == null) - throw new HttpApiException("Theme with this id not found", 404); + return Problem("Theme with this id not found", statusCode: 404); // Disable all other enabled themes if we are enabling the current theme. // This ensures only one theme is enabled at the time @@ -98,29 +129,28 @@ public class ThemesController : Controller foreach (var otherTheme in otherThemes) otherTheme.IsEnabled = false; - await ThemeRepository.RunTransaction(set => - { - set.UpdateRange(otherThemes); - }); + await ThemeRepository.RunTransactionAsync(set => { set.UpdateRange(otherThemes); }); } - + ThemeMapper.Merge(theme, request); - await ThemeRepository.Update(theme); + await ThemeRepository.UpdateAsync(theme); + return ThemeMapper.ToResponse(theme); } [HttpDelete("{id:int}")] [Authorize(Policy = "permissions:admin.system.customisation.themes.write")] - public async Task Delete([FromRoute] int id) + public async Task Delete([FromRoute] int id) { var theme = await ThemeRepository .Get() .FirstOrDefaultAsync(x => x.Id == id); - + if (theme == null) - throw new HttpApiException("Theme with this id not found", 404); - - await ThemeRepository.Remove(theme); + return Problem("Theme with this id not found", statusCode: 404); + + await ThemeRepository.RemoveAsync(theme); + return NoContent(); } } \ 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 247617bc..47e40c53 100644 --- a/Moonlight.ApiServer/Http/Controllers/Admin/Users/UsersController.cs +++ b/Moonlight.ApiServer/Http/Controllers/Admin/Users/UsersController.cs @@ -1,4 +1,3 @@ -using System.ComponentModel.DataAnnotations; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; @@ -6,10 +5,10 @@ using Microsoft.Extensions.DependencyInjection; using MoonCore.Exceptions; using MoonCore.Extended.Abstractions; using MoonCore.Extended.Helpers; -using MoonCore.Extended.Models; using MoonCore.Models; using Moonlight.ApiServer.Database.Entities; using Moonlight.ApiServer.Services; +using Moonlight.ApiServer.Mappers; using Moonlight.Shared.Http.Requests.Admin.Users; using Moonlight.Shared.Http.Responses.Admin.Users; @@ -28,60 +27,78 @@ public class UsersController : Controller [HttpGet] [Authorize(Policy = "permissions:admin.users.get")] - public async Task> Get([FromQuery] PagedOptions options) + public async Task>> Get( + [FromQuery] int startIndex, + [FromQuery] int count, + [FromQuery] string? orderBy, + [FromQuery] string? filter, + [FromQuery] string orderByDir = "asc" + ) { - var count = await UserRepository.Get().CountAsync(); + if (count > 100) + return Problem("You cannot fetch more items than 100 at a time", statusCode: 400); + + IQueryable query = UserRepository.Get(); - var users = await UserRepository - .Get() - .OrderBy(x => x.Id) - .Skip(options.Page * options.PageSize) - .Take(options.PageSize) + query = orderBy switch + { + nameof(Database.Entities.User.Id) => orderByDir == "desc" + ? query.OrderByDescending(x => x.Id) + : query.OrderBy(x => x.Id), + + nameof(Database.Entities.User.Username) => orderByDir == "desc" + ? query.OrderByDescending(x => x.Username) + : query.OrderBy(x => x.Username), + + nameof(Database.Entities.User.Email) => orderByDir == "desc" + ? query.OrderByDescending(x => x.Email) + : query.OrderBy(x => x.Email), + + _ => query.OrderBy(x => x.Id) + }; + + if (!string.IsNullOrEmpty(filter)) + { + query = query.Where(x => + EF.Functions.ILike(x.Username, $"%{filter}%") || + EF.Functions.ILike(x.Email, $"%{filter}%") + ); + } + + var totalCount = await query.CountAsync(); + + var items = await query + .Skip(startIndex) + .Take(count) + .AsNoTracking() + .ProjectToResponse() .ToArrayAsync(); - var mappedUsers = users - .Select(x => new UserResponse() - { - Id = x.Id, - Email = x.Email, - Username = x.Username, - Permissions = x.Permissions - }) - .ToArray(); - - return new PagedData() + return new CountedData() { - CurrentPage = options.Page, - Items = mappedUsers, - PageSize = options.PageSize, - TotalItems = count, - TotalPages = count == 0 ? 0 : (count - 1) / options.PageSize + Items = items, + TotalCount = totalCount }; } [HttpGet("{id}")] [Authorize(Policy = "permissions:admin.users.get")] - public async Task GetSingle(int id) + public async Task> GetSingle(int id) { var user = await UserRepository .Get() + .ProjectToResponse() .FirstOrDefaultAsync(x => x.Id == id); if (user == null) - throw new HttpApiException("No user with that id found", 404); + return Problem("No user with that id found", statusCode: 404); - return new UserResponse() - { - Id = user.Id, - Email = user.Email, - Username = user.Username, - Permissions = user.Permissions - }; + return user; } [HttpPost] [Authorize(Policy = "permissions:admin.users.create")] - public async Task Create([FromBody] CreateUserRequest request) + public async Task> Create([FromBody] CreateUserRequest request) { // Reformat values request.Username = request.Username.ToLower().Trim(); @@ -89,10 +106,10 @@ public class UsersController : Controller // Check for users with the same values if (UserRepository.Get().Any(x => x.Username == request.Username)) - throw new HttpApiException("A user with that username already exists", 400); + return Problem("A user with that username already exists", statusCode: 400); if (UserRepository.Get().Any(x => x.Email == request.Email)) - throw new HttpApiException("A user with that email address already exists", 400); + return Problem("A user with that email address already exists", statusCode: 400); var hashedPassword = HashHelper.Hash(request.Password); @@ -104,27 +121,21 @@ public class UsersController : Controller Permissions = request.Permissions }; - var finalUser = await UserRepository.Add(user); + var finalUser = await UserRepository.AddAsync(user); - return new UserResponse() - { - Id = finalUser.Id, - Email = finalUser.Email, - Username = finalUser.Username, - Permissions = finalUser.Permissions - }; + return UserMapper.ToResponse(finalUser); } [HttpPatch("{id}")] [Authorize(Policy = "permissions:admin.users.update")] - public async Task Update([FromRoute] int id, [FromBody] UpdateUserRequest request) + public async Task> Update([FromRoute] int id, [FromBody] UpdateUserRequest request) { var user = await UserRepository .Get() .FirstOrDefaultAsync(x => x.Id == id); if (user == null) - throw new HttpApiException("No user with that id found", 404); + return Problem("No user with that id found", statusCode: 404); // Reformat values request.Username = request.Username.ToLower().Trim(); @@ -132,10 +143,10 @@ public class UsersController : Controller // Check for users with the same values if (UserRepository.Get().Any(x => x.Username == request.Username && x.Id != user.Id)) - throw new HttpApiException("A user with that username already exists", 400); + return Problem("Another user with that username already exists", statusCode: 400); if (UserRepository.Get().Any(x => x.Email == request.Email && x.Id != user.Id)) - throw new HttpApiException("A user with that email address already exists", 400); + return Problem("Another user with that email address already exists", statusCode: 400); // Perform hashing the password if required if (!string.IsNullOrEmpty(request.Password)) @@ -153,27 +164,21 @@ public class UsersController : Controller user.Email = request.Email; user.Username = request.Username; - await UserRepository.Update(user); + await UserRepository.UpdateAsync(user); - return new UserResponse() - { - Id = user.Id, - Email = user.Email, - Username = user.Username, - Permissions = user.Permissions - }; + return UserMapper.ToResponse(user); } [HttpDelete("{id}")] [Authorize(Policy = "permissions:admin.users.delete")] - public async Task Delete([FromRoute] int id, [FromQuery] bool force = false) + public async Task Delete([FromRoute] int id, [FromQuery] bool force = false) { var user = await UserRepository .Get() .FirstOrDefaultAsync(x => x.Id == id); if (user == null) - throw new HttpApiException("No user with that id found", 404); + return Problem("No user with that id found", statusCode: 404); var deletionService = HttpContext.RequestServices.GetRequiredService(); @@ -182,9 +187,10 @@ public class UsersController : Controller var validationResult = await deletionService.Validate(user); if (!validationResult.IsAllowed) - throw new HttpApiException($"Unable to delete user", 400, validationResult.Reason); + return Problem("Unable to delete user", statusCode: 400, title: validationResult.Reason); } await deletionService.Delete(user, force); + return NoContent(); } } \ No newline at end of file diff --git a/Moonlight.ApiServer/Http/Controllers/LocalAuth/LocalAuthController.cs b/Moonlight.ApiServer/Http/Controllers/LocalAuth/LocalAuthController.cs index 68b90a75..37b8e87e 100644 --- a/Moonlight.ApiServer/Http/Controllers/LocalAuth/LocalAuthController.cs +++ b/Moonlight.ApiServer/Http/Controllers/LocalAuth/LocalAuthController.cs @@ -180,7 +180,7 @@ public class LocalAuthController : Controller Permissions = permissions }; - var finalUser = await UserRepository.Add(user); + var finalUser = await UserRepository.AddAsync(user); return finalUser; } diff --git a/Moonlight.ApiServer/Mappers/ApiKeyMapper.cs b/Moonlight.ApiServer/Mappers/ApiKeyMapper.cs new file mode 100644 index 00000000..2425cda2 --- /dev/null +++ b/Moonlight.ApiServer/Mappers/ApiKeyMapper.cs @@ -0,0 +1,19 @@ +using Moonlight.ApiServer.Database.Entities; +using Moonlight.Shared.Http.Requests.Admin.ApiKeys; +using Moonlight.Shared.Http.Responses.Admin.ApiKeys; +using Riok.Mapperly.Abstractions; + +namespace Moonlight.ApiServer.Mappers; + +[Mapper] +public static partial class ApiKeyMapper +{ + // Mappers + public static partial ApiKeyResponse ToResponse(ApiKey apiKey); + public static partial ApiKey ToApiKey(CreateApiKeyRequest request); + public static partial void Merge([MappingTarget] ApiKey apiKey, UpdateApiKeyRequest request); + + // EF Relations + + public static partial IQueryable ProjectToResponse(this IQueryable apiKeys); +} \ No newline at end of file diff --git a/Moonlight.ApiServer/Mappers/ThemeMapper.cs b/Moonlight.ApiServer/Mappers/ThemeMapper.cs index 3523f9db..4b0e7659 100644 --- a/Moonlight.ApiServer/Mappers/ThemeMapper.cs +++ b/Moonlight.ApiServer/Mappers/ThemeMapper.cs @@ -8,7 +8,12 @@ namespace Moonlight.ApiServer.Mappers; [Mapper] public static partial class ThemeMapper { + // Mappers public static partial ThemeResponse ToResponse(Theme theme); public static partial Theme ToTheme(CreateThemeRequest request); public static partial void Merge([MappingTarget] Theme theme, UpdateThemeRequest request); + + // EF Relations + + public static partial IQueryable ProjectToResponse(this IQueryable themes); } \ No newline at end of file diff --git a/Moonlight.ApiServer/Mappers/UserMapper.cs b/Moonlight.ApiServer/Mappers/UserMapper.cs new file mode 100644 index 00000000..35069d31 --- /dev/null +++ b/Moonlight.ApiServer/Mappers/UserMapper.cs @@ -0,0 +1,15 @@ +using Moonlight.ApiServer.Database.Entities; +using Moonlight.Shared.Http.Responses.Admin.Users; +using Riok.Mapperly.Abstractions; + +namespace Moonlight.ApiServer.Mappers; + +[Mapper] +public static partial class UserMapper +{ + // Mappers + public static partial UserResponse ToResponse(User user); + + // EF Relations + public static partial IQueryable ProjectToResponse(this IQueryable users); +} \ No newline at end of file diff --git a/Moonlight.ApiServer/Moonlight.ApiServer.csproj b/Moonlight.ApiServer/Moonlight.ApiServer.csproj index 6bc2b9e7..254be0dd 100644 --- a/Moonlight.ApiServer/Moonlight.ApiServer.csproj +++ b/Moonlight.ApiServer/Moonlight.ApiServer.csproj @@ -25,9 +25,10 @@ + - - + + diff --git a/Moonlight.ApiServer/Services/UserAuthService.cs b/Moonlight.ApiServer/Services/UserAuthService.cs index 18893e08..ef0e6dc3 100644 --- a/Moonlight.ApiServer/Services/UserAuthService.cs +++ b/Moonlight.ApiServer/Services/UserAuthService.cs @@ -80,7 +80,7 @@ public class UserAuthService permissions = ["*"]; } - user = await UserRepository.Add(new User() + user = await UserRepository.AddAsync(new User() { Email = email, TokenValidTimestamp = DateTimeOffset.UtcNow.AddMinutes(-1), @@ -94,7 +94,7 @@ public class UserAuthService if (user.Username != username) { user.Username = username; - await UserRepository.Update(user); + await UserRepository.UpdateAsync(user); } // Enrich claims with required metadata diff --git a/Moonlight.ApiServer/Services/UserDeletionService.cs b/Moonlight.ApiServer/Services/UserDeletionService.cs index a58324d5..8420a064 100644 --- a/Moonlight.ApiServer/Services/UserDeletionService.cs +++ b/Moonlight.ApiServer/Services/UserDeletionService.cs @@ -35,8 +35,8 @@ public class UserDeletionService public async Task Delete(User user, bool force) { foreach (var handler in Handlers) - await Delete(user, force); + await handler.Delete(user, force); - await UserRepository.Remove(user); + await UserRepository.RemoveAsync(user); } } \ No newline at end of file diff --git a/Moonlight.Client/Moonlight.Client.csproj b/Moonlight.Client/Moonlight.Client.csproj index 2dc84988..be31cbdf 100644 --- a/Moonlight.Client/Moonlight.Client.csproj +++ b/Moonlight.Client/Moonlight.Client.csproj @@ -24,9 +24,9 @@ - + - + diff --git a/Moonlight.Client/Services/ThemeService.cs b/Moonlight.Client/Services/ThemeService.cs index a6628fbc..638f743b 100644 --- a/Moonlight.Client/Services/ThemeService.cs +++ b/Moonlight.Client/Services/ThemeService.cs @@ -17,21 +17,14 @@ public class ThemeService ApiClient = apiClient; } - public async Task> Get(int page, int pageSize) - { - return await ApiClient.GetJson>( - $"api/admin/system/customisation/themes?page={page}&pageSize={pageSize}" - ); - } - - public async Task Get(int id) + public async Task GetAsync(int id) { return await ApiClient.GetJson( $"api/admin/system/customisation/themes/{id}" ); } - public async Task Create(CreateThemeRequest request) + public async Task CreateAsync(CreateThemeRequest request) { return await ApiClient.PostJson( "api/admin/system/customisation/themes", @@ -39,7 +32,7 @@ public class ThemeService ); } - public async Task Update(int id, UpdateThemeRequest request) + public async Task UpdateAsync(int id, UpdateThemeRequest request) { return await ApiClient.PatchJson( $"api/admin/system/customisation/themes/{id}", @@ -47,7 +40,7 @@ public class ThemeService ); } - public async Task Delete(int id) + public async Task DeleteAsync(int id) { await ApiClient.Delete( $"api/admin/system/customisation/themes/{id}" diff --git a/Moonlight.Client/Styles/MoonCore.Blazor.FlyonUi/mappings/mooncore.map b/Moonlight.Client/Styles/MoonCore.Blazor.FlyonUi/mappings/mooncore.map deleted file mode 100755 index 44e291f2..00000000 --- a/Moonlight.Client/Styles/MoonCore.Blazor.FlyonUi/mappings/mooncore.map +++ /dev/null @@ -1,518 +0,0 @@ -!bg-base-100 -!border-base-content/40 -!border-none -!flex -!font-medium -!font-semibold -!h-2.5 -!justify-between -!me-1.5 -!ms-auto -!px-2.5 -!rounded-full -!text-sm -!w-2.5 -*:[grid-area:1/1] -*:first:rounded-tl-lg -*:last:rounded-tr-lg --left-4 --ml-4 --translate-x-full --translate-y-1/2 -absolute -accordion -accordion-bordered -accordion-toggle -active -active-tab:bg-primary -active-tab:text-base-content -advance-select-menu -advance-select-option -advance-select-tag -advance-select-toggle -alert -alert-error -alert-outline -alert-soft -align-bottom -align-middle -animate-bounce -animate-ping -aria-[current='page']:text-bg-soft-primary -avatar -avatar-away-bottom -avatar-away-top -avatar-busy-bottom -avatar-busy-top -avatar-offline-bottom -avatar-offline-top -avatar-online-bottom -avatar-online-top -avatar-placeholder -badge -badge-error -badge-info -badge-outline -badge-primary -badge-soft -badge-success -bg-background -bg-background/60 -bg-base-100 -bg-base-150 -bg-base-200 -bg-base-200/50 -bg-base-300 -bg-base-300/45 -bg-base-300/50 -bg-base-300/60 -bg-error -bg-info -bg-primary -bg-primary/5 -bg-success -bg-transparent -bg-warning -block -blur -border -border-0 -border-2 -border-b -border-base-content -border-base-content/20 -border-base-content/25 -border-base-content/40 -border-base-content/5 -border-dashed -border-t -border-transparent -bottom-0 -bottom-full -break-words -btn -btn-accent -btn-active -btn-circle -btn-disabled -btn-error -btn-info -btn-outline -btn-primary -btn-secondary -btn-sm -btn-soft -btn-square -btn-success -btn-text -btn-warning -card -card-alert -card-body -card-border -card-footer -card-header -card-title -carousel -carousel-body -carousel-next -carousel-prev -carousel-slide -chat -chat-avatar -chat-bubble -chat-footer -chat-header -chat-receiver -chat-sender -checkbox -checkbox-primary -checkbox-xs -col-span-1 -collapse -combo-box-selected:block -combo-box-selected:dropdown-active -complete -container -contents -cursor-default -cursor-not-allowed -cursor-pointer -diff -disabled -divide-base-150/60 -divide-y -drop-shadow -dropdown -dropdown-disabled -dropdown-item -dropdown-menu -dropdown-open:opacity-100 -dropdown-open:rotate-180 -dropdown-toggle -duration-300 -duration-500 -ease-in-out -ease-linear -end-3 -file-upload-complete:progress-success -fill-black -filter -filter-reset -fixed -flex -flex-1 -flex-col -flex-grow -flex-nowrap -flex-row -flex-shrink-0 -flex-wrap -focus-visible:outline-none -focus-within:border-primary -focus:border-primary -focus:outline-1 -focus:outline-none -focus:outline-primary -focus:ring-0 -font-bold -font-inter -font-medium -font-normal -font-semibold -gap-0.5 -gap-1 -gap-1.5 -gap-2 -gap-3 -gap-4 -gap-5 -gap-6 -gap-x-1 -gap-x-2 -gap-x-3 -gap-y-1 -gap-y-3 -grid -grid-cols-1 -grid-flow-col -grow -grow-0 -h-12 -h-2 -h-32 -h-64 -h-8 -h-auto -h-full -h-screen -helper-text -hidden -hover:bg-primary/5 -hover:bg-transparent -hover:text-base-content -hover:text-base-content/60 -hover:text-primary -image-full -inline -inline-block -inline-flex -inline-grid -input -input-floating -input-floating-label -input-lg -input-md -input-sm -input-xl -inset-0 -inset-y-0 -inset-y-2 -invisible -is-invalid -is-valid -isolate -italic -items-center -items-end -items-start -join -join-item -justify-between -justify-center -justify-end -justify-start -justify-stretch -label-text -leading-3 -leading-3.5 -leading-6 -left-0 -lg:bg-base-100/20 -lg:flex -lg:gap-y-0 -lg:grid-cols-2 -lg:hidden -lg:justify-end -lg:justify-start -lg:min-w-0 -lg:p-10 -lg:pb-5 -lg:pl-64 -lg:pr-3.5 -lg:pt-5 -lg:ring-1 -lg:ring-base-content/10 -lg:rounded-lg -lg:shadow-xs -list-disc -list-inside -loading -loading-lg -loading-sm -loading-spinner -loading-xl -loading-xs -lowercase -m-10 -mask -max-h-52 -max-lg:flex-col -max-lg:hidden -max-w-7xl -max-w-80 -max-w-full -max-w-lg -max-w-sm -max-w-xl -mb-0.5 -mb-1 -mb-2 -mb-3 -mb-4 -mb-5 -md:table-cell -md:text-3xl -me-1 -me-1.5 -me-2 -me-2.5 -me-5 -menu -menu-active -menu-disabled -menu-dropdown -menu-dropdown-show -menu-focus -menu-horizontal -menu-title -min-h-0 -min-h-svh -min-w-0 -min-w-28 -min-w-48 -min-w-60 -min-w-[100px] -ml-3 -ml-4 -modal -modal-content -modal-dialog -modal-middle -modal-title -mr-4 -ms-1 -ms-2 -ms-3 -mt-1 -mt-1.5 -mt-10 -mt-12 -mt-2 -mt-2.5 -mt-3 -mt-4 -mt-5 -mt-8 -mx-1 -mx-auto -my-3 -my-auto -opacity-0 -opacity-100 -open -origin-top-left -outline -outline-0 -overflow-hidden -overflow-x-auto -overflow-y-auto -overlay-open:duration-50 -overlay-open:opacity-100 -p-0.5 -p-1 -p-2 -p-3 -p-4 -p-5 -p-6 -p-8 -pin-input -pin-input-underline -placeholder-base-content/60 -pointer-events-auto -pointer-events-none -progress -progress-bar -progress-indeterminate -progress-primary -pt-0 -pt-0.5 -pt-3 -px-1.5 -px-2 -px-2.5 -px-3 -px-4 -px-5 -py-0.5 -py-1.5 -py-2 -py-2.5 -py-6 -radio -range -relative -resize -ring-0 -ring-1 -ring-white/10 -rounded-box -rounded-field -rounded-full -rounded-lg -rounded-md -rounded-t-lg -row-active -row-hover -rtl:!mr-0 -select -select-disabled:opacity-40 -select-disabled:pointer-events-none -select-floating -select-floating-label -selected -selected:select-active -shadow-base-300/20 -shadow-lg -shadow-xs -shrink-0 -size-10 -size-4 -size-5 -size-8 -skeleton -skeleton-animated -sm:auto-cols-max -sm:flex -sm:items-center -sm:items-end -sm:justify-between -sm:justify-end -sm:max-w-2xl -sm:max-w-3xl -sm:max-w-4xl -sm:max-w-5xl -sm:max-w-6xl -sm:max-w-7xl -sm:max-w-lg -sm:max-w-md -sm:max-w-xl -sm:mb-0 -sm:mt-5 -sm:mt-6 -sm:p-6 -sm:py-2 -sm:text-sm/5 -space-x-1 -space-y-1 -space-y-4 -sr-only -static -status -status-error -sticky -switch -tab -tab-active -table -table-pin-cols -table-pin-rows -tabs -tabs-bordered -tabs-lg -tabs-lifted -tabs-md -tabs-sm -tabs-xl -tabs-xs -text-2xl -text-4xl -text-accent -text-base -text-base-content -text-base-content/40 -text-base-content/50 -text-base-content/60 -text-base-content/70 -text-base-content/80 -text-base/6 -text-center -text-error -text-error-content -text-gray-400 -text-info -text-info-content -text-left -text-lg -text-primary -text-primary-content -text-sm -text-sm/5 -text-success -text-success-content -text-warning -text-warning-content -text-xl -text-xs -text-xs/5 -textarea -textarea-floating -textarea-floating-label -theme-controller -tooltip -tooltip-content -top-0 -top-1/2 -top-full -transform -transition -transition-all -transition-opacity -translate-x-0 -truncate -underline -uppercase -validate -w-0 -w-0.5 -w-12 -w-4 -w-56 -w-64 -w-fit -w-full -whitespace-nowrap -z-10 -z-40 -z-50 \ No newline at end of file diff --git a/Moonlight.Client/Styles/MoonCore.Blazor.FlyonUi/mooncore.map b/Moonlight.Client/Styles/MoonCore.Blazor.FlyonUi/mooncore.map index 67f69d38..78d6b417 100755 --- a/Moonlight.Client/Styles/MoonCore.Blazor.FlyonUi/mooncore.map +++ b/Moonlight.Client/Styles/MoonCore.Blazor.FlyonUi/mooncore.map @@ -44,6 +44,7 @@ animate-bounce animate-ping aria-[current='page']:text-bg-soft-primary avatar +avatar-placeholder badge badge-error badge-info @@ -73,16 +74,23 @@ block blur border border-0 +border-1 border-2 border-b +border-b-2 +border-b-base-content/20 border-base-content border-base-content/20 border-base-content/25 border-base-content/40 border-base-content/5 border-dashed +border-error/30 +border-info/30 +border-success/30 border-t border-transparent +border-warning/30 bottom-0 bottom-full break-words @@ -93,7 +101,6 @@ btn-circle btn-disabled btn-error btn-info -btn-outline btn-primary btn-secondary btn-sm @@ -200,7 +207,7 @@ grid-cols-4 grid-flow-col grow grow-0 -h-12 +h-10 h-2 h-3 h-32 @@ -245,11 +252,9 @@ justify-between justify-center justify-end justify-start -justify-stretch label-text leading-3 leading-3.5 -leading-6 leading-none left-0 lg:bg-base-100/20 @@ -287,6 +292,9 @@ mask max-h-52 max-lg:flex-col max-lg:hidden +max-md:flex-wrap +max-md:justify-center +max-sm:hidden max-w-7xl max-w-80 max-w-full @@ -323,6 +331,7 @@ min-w-28 min-w-48 min-w-60 min-w-[100px] +min-w-full min-w-sm ml-3 ml-4 @@ -330,10 +339,10 @@ modal modal-content modal-dialog modal-middle -modal-title mr-4 ms-0.5 ms-1 +ms-1.5 ms-2 ms-3 ms-auto @@ -368,11 +377,13 @@ p-0.5 p-1 p-1.5 p-2 +p-2.5 p-3 p-4 p-5 p-6 p-8 +pb-1 pin-input placeholder-base-content/60 pointer-events-auto @@ -383,7 +394,9 @@ progress-indeterminate progress-primary pt-0 pt-0.5 +pt-2 pt-3 +px-0.5 px-1.5 px-2 px-2.5 @@ -418,6 +431,7 @@ select-disabled:opacity-40 select-disabled:pointer-events-none select-floating select-floating-label +select-sm selected selected:select-active shadow-base-300/20 @@ -426,6 +440,7 @@ shadow-md shadow-xs shrink-0 size-10 +size-12 size-4 size-5 size-8 @@ -448,11 +463,11 @@ sm:max-w-md sm:max-w-xl sm:mb-0 sm:mt-5 -sm:mt-6 sm:p-6 sm:py-2 sm:text-sm/5 space-x-1 +space-x-2.5 space-y-1 space-y-4 sr-only @@ -495,6 +510,7 @@ text-left text-lg text-primary text-primary-content +text-right text-sm text-sm/5 text-success @@ -525,6 +541,7 @@ validate w-0 w-0.5 w-12 +w-13 w-4 w-56 w-64 diff --git a/Moonlight.Client/UI/Views/Admin/Api/Index.razor b/Moonlight.Client/UI/Views/Admin/Api/Index.razor index 852dc382..b82b523e 100644 --- a/Moonlight.Client/UI/Views/Admin/Api/Index.razor +++ b/Moonlight.Client/UI/Views/Admin/Api/Index.razor @@ -3,7 +3,9 @@ @using MoonCore.Helpers @using MoonCore.Models @using Moonlight.Shared.Http.Responses.Admin.ApiKeys -@using MoonCore.Blazor.FlyonUi.DataTables +@using MoonCore.Blazor.FlyonUi.Grid +@using MoonCore.Blazor.FlyonUi.Grid.Columns +@using MoonCore.Blazor.FlyonUi.Grid.ToolbarItems @inject HttpApiClient ApiClient @inject AlertService AlertService @@ -47,48 +49,72 @@ - + + + + + + @Formatter.FormatDate(context.ExpiresAt.UtcDateTime) + + + + + @Formatter.FormatDate(context.CreatedAt.UtcDateTime) + + + + +
+ + + - - - - - - - - - @(Formatter.FormatDate(context.ExpiresAt.UtcDateTime)) - - - - -
- - - - - - - -
-
-
-
-
+ + + +
+ +
+ + + + Create + + +
@code { - private DataTable Table; + private DataGrid Grid; - private async Task> LoadData(PaginationOptions options) - => await ApiClient.GetJson>($"api/admin/apikeys?page={options.Page}&pageSize={options.PerPage}"); + private async Task> ItemsProvider(DataGridItemRequest request) + { + var query = $"?startIndex={request.StartIndex}&count={request.Count}"; - private async Task Delete(ApiKeyResponse apiKeyResponse) + if (!string.IsNullOrEmpty(request.SortColumn)) + { + var dir = request.SortDirection == SortState.Descending ? "desc" : "asc"; + query += $"&orderBy={request.SortColumn}&orderByDir={dir}"; + } + + if (!string.IsNullOrEmpty(request.Filter)) + query += $"&filter={request.Filter}"; + + var data = await ApiClient.GetJson>($"api/admin/apikeys{query}"); + + return new() + { + Items = data.Items, + TotalCount = data.TotalCount + }; + } + + private async Task DeleteAsync(ApiKeyResponse apiKeyResponse) { await AlertService.ConfirmDanger( "API Key deletion", @@ -98,7 +124,7 @@ await ApiClient.Delete($"api/admin/apikeys/{apiKeyResponse.Id}"); await ToastService.Success("Successfully deleted api key"); - await Table.Refresh(); + await Grid.RefreshAsync(); } ); } diff --git a/Moonlight.Client/UI/Views/Admin/Sys/Customisation/Index.razor b/Moonlight.Client/UI/Views/Admin/Sys/Customisation/Index.razor index f2544cbb..8925988b 100644 --- a/Moonlight.Client/UI/Views/Admin/Sys/Customisation/Index.razor +++ b/Moonlight.Client/UI/Views/Admin/Sys/Customisation/Index.razor @@ -2,7 +2,9 @@ @using System.Text.Json @using Microsoft.AspNetCore.Authorization -@using MoonCore.Blazor.FlyonUi.DataTables +@using MoonCore.Blazor.FlyonUi.Grid +@using MoonCore.Blazor.FlyonUi.Grid.Columns +@using MoonCore.Blazor.FlyonUi.Grid.ToolbarItems @using MoonCore.Blazor.FlyonUi.Helpers @using MoonCore.Helpers @using MoonCore.Models @@ -16,6 +18,7 @@ @inject ThemeService ThemeService @inject AlertService AlertService @inject ToastService ToastService +@inject HttpApiClient ApiClient @inject DownloadService DownloadService @inject ILogger Logger @@ -25,72 +28,72 @@ Themes -
-
- -
-
+
+ + + + + +
+ @context.Name -
- - - - - -
- @context.Name - - @if (context.IsEnabled) - { - - } -
-
-
- - - - -
- @if (!string.IsNullOrEmpty(context.DonateUrl)) - { - - - Donate - - } - - @if (!string.IsNullOrEmpty(context.UpdateUrl)) - { - - - Update - - } - - - - Export - - - - + @if (context.IsEnabled) + { + + } +
+ + + + + + + + -
-
- -
-
+ } + + + + Export + + + + + + + + + +
+ + + + + + +
@@ -100,12 +103,31 @@ @code { - private DataTable Table; + private DataGrid Grid; - private async Task> LoadItems(PaginationOptions options) - => await ThemeService.Get(options.Page, options.PerPage); + private async Task> ItemsProvider(DataGridItemRequest request) + { + var query = $"?startIndex={request.StartIndex}&count={request.Count}"; + + if (!string.IsNullOrEmpty(request.SortColumn)) + { + var dir = request.SortDirection == SortState.Descending ? "desc" : "asc"; + query += $"&orderBy={request.SortColumn}&orderByDir={dir}"; + } + + if (!string.IsNullOrEmpty(request.Filter)) + query += $"&filter={request.Filter}"; + + var data = await ApiClient.GetJson>($"api/admin/system/customisation/themes{query}"); + + return new() + { + Items = data.Items, + TotalCount = data.TotalCount + }; + } - private async Task Import(InputFileChangeEventArgs eventArgs) + private async Task ImportAsync(InputFileChangeEventArgs eventArgs) { if(eventArgs.FileCount < 1) return; @@ -141,7 +163,7 @@ continue; } - var theme = await ThemeService.Create(new CreateThemeRequest() + var theme = await ThemeService.CreateAsync(new CreateThemeRequest() { Name = themeTransfer.Name, Author = themeTransfer.Author, @@ -153,7 +175,7 @@ await ToastService.Success("Successfully imported theme", theme.Name); - await Table.Refresh(); + await Grid.RefreshAsync(); } catch (Exception e) { @@ -162,7 +184,7 @@ } } - private async Task Export(ThemeResponse theme) + private async Task ExportAsync(ThemeResponse theme) { var transfer = new ThemeTransferModel() { @@ -184,17 +206,17 @@ await DownloadService.Download(fileName, json); } - private async Task Delete(ThemeResponse response) + private async Task DeleteAsync(ThemeResponse response) { await AlertService.ConfirmDanger( "Theme deletion", $"Do you really want to delete the theme: {response.Name}", async () => { - await ThemeService.Delete(response.Id); + await ThemeService.DeleteAsync(response.Id); await ToastService.Success("Successfully deleted theme"); - await Table.Refresh(); + await Grid.RefreshAsync(); } ); } diff --git a/Moonlight.Client/UI/Views/Admin/Sys/Customisation/Themes/Create.razor b/Moonlight.Client/UI/Views/Admin/Sys/Customisation/Themes/Create.razor index a4131705..73e21879 100644 --- a/Moonlight.Client/UI/Views/Admin/Sys/Customisation/Themes/Create.razor +++ b/Moonlight.Client/UI/Views/Admin/Sys/Customisation/Themes/Create.razor @@ -58,7 +58,7 @@ private async Task OnValidSubmit() { - await ThemeService.Create(Request); + await ThemeService.CreateAsync(Request); await ToastService.Success("Successfully created theme"); NavigationManager.NavigateTo("/admin/system/customisation"); diff --git a/Moonlight.Client/UI/Views/Admin/Sys/Customisation/Themes/Update.razor b/Moonlight.Client/UI/Views/Admin/Sys/Customisation/Themes/Update.razor index faa9c860..74671a73 100644 --- a/Moonlight.Client/UI/Views/Admin/Sys/Customisation/Themes/Update.razor +++ b/Moonlight.Client/UI/Views/Admin/Sys/Customisation/Themes/Update.razor @@ -82,7 +82,7 @@ private async Task Load(LazyLoader _) { - Response = await ThemeService.Get(Id); + Response = await ThemeService.GetAsync(Id); Request = new() { @@ -98,7 +98,7 @@ private async Task OnValidSubmit() { - await ThemeService.Update(Id, Request); + await ThemeService.UpdateAsync(Id, Request); await ToastService.Success("Successfully updated theme"); Navigation.NavigateTo("/admin/system/customisation"); diff --git a/Moonlight.Client/UI/Views/Admin/Sys/Diagnose.razor b/Moonlight.Client/UI/Views/Admin/Sys/Diagnose.razor index 911f7037..919a33c9 100644 --- a/Moonlight.Client/UI/Views/Admin/Sys/Diagnose.razor +++ b/Moonlight.Client/UI/Views/Admin/Sys/Diagnose.razor @@ -96,7 +96,7 @@ .ToDictionary(x => x, _ => true); } - private async Task GenerateDiagnose(WButton _) + private async Task GenerateDiagnose(WButton button) { var request = new GenerateDiagnoseRequest(); diff --git a/Moonlight.Client/UI/Views/Admin/Users/Index.razor b/Moonlight.Client/UI/Views/Admin/Users/Index.razor index 11b13a2e..95afbb8d 100644 --- a/Moonlight.Client/UI/Views/Admin/Users/Index.razor +++ b/Moonlight.Client/UI/Views/Admin/Users/Index.razor @@ -3,13 +3,14 @@ @using MoonCore.Helpers @using MoonCore.Models @using Moonlight.Shared.Http.Responses.Admin.Users -@using MoonCore.Blazor.FlyonUi.DataTables +@using MoonCore.Blazor.FlyonUi.Grid +@using MoonCore.Blazor.FlyonUi.Grid.Columns @inject HttpApiClient ApiClient @inject AlertService AlertService @inject ToastService ToastService -
+ - - - - - - - - - -
- - - + + + + + + + + - - - - + + + +
+ + + @code { - private DataTable Table; + private DataGrid Grid; - private async Task> LoadData(PaginationOptions options) - => await ApiClient.GetJson>($"api/admin/users?page={options.Page}&pageSize={options.PerPage}"); + private async Task> ItemsProvider(DataGridItemRequest request) + { + var query = $"?startIndex={request.StartIndex}&count={request.Count}"; - private async Task Delete(UserResponse response) + if (!string.IsNullOrEmpty(request.SortColumn)) + { + var dir = request.SortDirection == SortState.Descending ? "desc" : "asc"; + query += $"&orderBy={request.SortColumn}&orderByDir={dir}"; + } + + if (!string.IsNullOrEmpty(request.Filter)) + query += $"&filter={request.Filter}"; + + var data = await ApiClient.GetJson>($"api/admin/users{query}"); + + return new() + { + Items = data.Items, + TotalCount = data.TotalCount + }; + } + + private async Task DeleteAsync(UserResponse response) { await AlertService.ConfirmDanger( "User deletion", @@ -57,7 +78,7 @@ await ApiClient.Delete($"api/admin/users/{response.Id}"); await ToastService.Success("Successfully deleted user"); - await Table.Refresh(); + await Grid.RefreshAsync(); } ); } diff --git a/Moonlight.Shared/Http/Responses/Admin/ApiKeys/ApiKeyResponse.cs b/Moonlight.Shared/Http/Responses/Admin/ApiKeys/ApiKeyResponse.cs index 28fe84a5..f30ae182 100644 --- a/Moonlight.Shared/Http/Responses/Admin/ApiKeys/ApiKeyResponse.cs +++ b/Moonlight.Shared/Http/Responses/Admin/ApiKeys/ApiKeyResponse.cs @@ -6,4 +6,5 @@ public class ApiKeyResponse public string Description { get; set; } public string[] Permissions { get; set; } = []; public DateTimeOffset ExpiresAt { get; set; } + public DateTimeOffset CreatedAt { get; set; } } \ No newline at end of file