From 97a676ccd7e9f9ad55766ec06018abe8bdbd07f8 Mon Sep 17 00:00:00 2001 From: ChiaraBm Date: Thu, 29 Jan 2026 09:28:50 +0100 Subject: [PATCH] Implemented handling of server side issues using the rfc for problem detasils in the frontend --- .../Helpers/ProblemDetailsHelper.cs | 30 ++++++++++++++++++ .../UI/Admin/Modals/CreateApiKeyDialog.razor | 29 +++++++++++++++-- .../UI/Admin/Modals/CreateRoleDialog.razor | 23 ++++++++++++-- .../UI/Admin/Modals/CreateUserDialog.razor | 31 ++++++++++++++++--- .../UI/Admin/Modals/UpdateApiKeyDialog.razor | 29 +++++++++++++++-- .../UI/Admin/Modals/UpdateRoleDialog.razor | 24 ++++++++++++-- .../UI/Admin/Modals/UpdateUserDialog.razor | 28 +++++++++++++++-- .../UI/Admin/Views/Sys/ApiKeys.razor | 26 ++-------------- .../UI/Admin/Views/Sys/Themes/Create.razor | 9 +++++- .../UI/Admin/Views/Sys/Themes/Update.razor | 9 +++++- .../UI/Admin/Views/Users/Roles.razor | 18 ++--------- .../UI/Admin/Views/Users/Users.razor | 25 ++------------- .../Http/Responses/ProblemDetails.cs | 10 ++++++ Moonlight.Shared/Http/SerializationContext.cs | 3 ++ 14 files changed, 212 insertions(+), 82 deletions(-) create mode 100644 Moonlight.Frontend/Helpers/ProblemDetailsHelper.cs create mode 100644 Moonlight.Shared/Http/Responses/ProblemDetails.cs diff --git a/Moonlight.Frontend/Helpers/ProblemDetailsHelper.cs b/Moonlight.Frontend/Helpers/ProblemDetailsHelper.cs new file mode 100644 index 00000000..0f04d8ba --- /dev/null +++ b/Moonlight.Frontend/Helpers/ProblemDetailsHelper.cs @@ -0,0 +1,30 @@ +using System.Net.Http.Json; +using Microsoft.AspNetCore.Components.Forms; +using Moonlight.Shared.Http.Responses; + +namespace Moonlight.Frontend.Helpers; + +public static class ProblemDetailsHelper +{ + public static async Task HandleProblemDetailsAsync(HttpResponseMessage response, object model, ValidationMessageStore validationMessageStore) + { + var problemDetails = await response.Content.ReadFromJsonAsync(); + + if (problemDetails == null) + response.EnsureSuccessStatusCode(); // Trigger exception when unable to parse + else + { + if(!string.IsNullOrEmpty(problemDetails.Detail)) + validationMessageStore.Add(new FieldIdentifier(model, string.Empty), problemDetails.Detail); + + if (problemDetails.Errors != null) + { + foreach (var error in problemDetails.Errors) + { + foreach (var message in error.Value) + validationMessageStore.Add(new FieldIdentifier(model, error.Key), message); + } + } + } + } +} \ No newline at end of file diff --git a/Moonlight.Frontend/UI/Admin/Modals/CreateApiKeyDialog.razor b/Moonlight.Frontend/UI/Admin/Modals/CreateApiKeyDialog.razor index 5fe70954..a0e0e899 100644 --- a/Moonlight.Frontend/UI/Admin/Modals/CreateApiKeyDialog.razor +++ b/Moonlight.Frontend/UI/Admin/Modals/CreateApiKeyDialog.razor @@ -1,12 +1,18 @@ -@using Moonlight.Frontend.UI.Admin.Components +@using Moonlight.Frontend.Helpers +@using Moonlight.Frontend.UI.Admin.Components @using Moonlight.Shared.Http.Requests.ApiKeys +@using Moonlight.Shared.Http.Responses @using ShadcnBlazor.Dialogs @using ShadcnBlazor.Extras.Forms +@using ShadcnBlazor.Extras.Toasts @using ShadcnBlazor.Fields @using ShadcnBlazor.Inputs @inherits ShadcnBlazor.Extras.Dialogs.DialogBase +@inject HttpClient HttpClient +@inject ToastService ToastService + Create new API key @@ -44,7 +50,7 @@ @code { - [Parameter] public Func OnSubmit { get; set; } + [Parameter] public Func OnSubmit { get; set; } private CreateApiKeyDto Request; @@ -61,8 +67,25 @@ private async Task OnSubmitAsync(EditContext editContext, ValidationMessageStore validationMessageStore) { Request.Permissions = Permissions.ToArray(); + + var response = await HttpClient.PostAsJsonAsync( + "/api/admin/apiKeys", + Request, + Constants.SerializerOptions + ); + + if (!response.IsSuccessStatusCode) + { + await ProblemDetailsHelper.HandleProblemDetailsAsync(response, Request, validationMessageStore); + return false; + } - await OnSubmit.Invoke(Request); + await ToastService.SuccessAsync( + "API Key creation", + $"Successfully created API key {Request.Name}" + ); + + await OnSubmit.Invoke(); await CloseAsync(); return true; diff --git a/Moonlight.Frontend/UI/Admin/Modals/CreateRoleDialog.razor b/Moonlight.Frontend/UI/Admin/Modals/CreateRoleDialog.razor index ce98ea80..671e8317 100644 --- a/Moonlight.Frontend/UI/Admin/Modals/CreateRoleDialog.razor +++ b/Moonlight.Frontend/UI/Admin/Modals/CreateRoleDialog.razor @@ -1,12 +1,17 @@ +@using Moonlight.Frontend.Helpers @using Moonlight.Frontend.UI.Admin.Components @using Moonlight.Shared.Http.Requests.Roles @using ShadcnBlazor.Dialogs @using ShadcnBlazor.Extras.Forms +@using ShadcnBlazor.Extras.Toasts @using ShadcnBlazor.Fields @using ShadcnBlazor.Inputs @inherits ShadcnBlazor.Extras.Dialogs.DialogBase +@inject HttpClient HttpClient +@inject ToastService ToastService + Create new role @@ -49,7 +54,7 @@ @code { - [Parameter] public Func OnSubmit { get; set; } + [Parameter] public Func OnSubmit { get; set; } private CreateRoleDto Request; private List Permissions; @@ -67,8 +72,22 @@ private async Task OnSubmitAsync(EditContext editContext, ValidationMessageStore validationMessageStore) { Request.Permissions = Permissions.ToArray(); + + var response = await HttpClient.PostAsJsonAsync( + "api/admin/roles", + Request, + Constants.SerializerOptions + ); + + if (!response.IsSuccessStatusCode) + { + await ProblemDetailsHelper.HandleProblemDetailsAsync(response, Request, validationMessageStore); + return false; + } - await OnSubmit.Invoke(Request); + await ToastService.SuccessAsync("Role creation", $"Role {Request.Name} has been successfully created"); + + await OnSubmit.Invoke(); await CloseAsync(); return true; diff --git a/Moonlight.Frontend/UI/Admin/Modals/CreateUserDialog.razor b/Moonlight.Frontend/UI/Admin/Modals/CreateUserDialog.razor index ebf8e442..55c1f566 100644 --- a/Moonlight.Frontend/UI/Admin/Modals/CreateUserDialog.razor +++ b/Moonlight.Frontend/UI/Admin/Modals/CreateUserDialog.razor @@ -1,11 +1,17 @@ -@using Moonlight.Shared.Http.Requests.Users +@using Moonlight.Frontend.Helpers +@using Moonlight.Shared.Http.Requests.Users +@using Moonlight.Shared.Http.Responses @using ShadcnBlazor.Dialogs @using ShadcnBlazor.Extras.Forms +@using ShadcnBlazor.Extras.Toasts @using ShadcnBlazor.Fields @using ShadcnBlazor.Inputs @inherits ShadcnBlazor.Extras.Dialogs.DialogBase +@inject HttpClient HttpClient +@inject ToastService ToastService + Create new user @@ -19,7 +25,7 @@ - +
@@ -45,7 +51,7 @@ @code { - [Parameter] public Func OnSubmit { get; set; } + [Parameter] public Func OnCompleted { get; set; } private CreateUserDto Request; @@ -56,7 +62,24 @@ private async Task OnSubmitAsync(EditContext editContext, ValidationMessageStore validationMessageStore) { - await OnSubmit.Invoke(Request); + var response = await HttpClient.PostAsJsonAsync( + "/api/admin/users", + Request, + Constants.SerializerOptions + ); + + if (!response.IsSuccessStatusCode) + { + await ProblemDetailsHelper.HandleProblemDetailsAsync(response, Request, validationMessageStore); + return false; + } + + await ToastService.SuccessAsync( + "User creation", + $"Successfully created user {Request.Username}" + ); + + await OnCompleted.Invoke(); await CloseAsync(); return true; diff --git a/Moonlight.Frontend/UI/Admin/Modals/UpdateApiKeyDialog.razor b/Moonlight.Frontend/UI/Admin/Modals/UpdateApiKeyDialog.razor index 3f2f62e9..b6a34e44 100644 --- a/Moonlight.Frontend/UI/Admin/Modals/UpdateApiKeyDialog.razor +++ b/Moonlight.Frontend/UI/Admin/Modals/UpdateApiKeyDialog.razor @@ -1,14 +1,19 @@ -@using Moonlight.Frontend.Mappers +@using Moonlight.Frontend.Helpers +@using Moonlight.Frontend.Mappers @using Moonlight.Frontend.UI.Admin.Components @using Moonlight.Shared.Http.Requests.ApiKeys @using Moonlight.Shared.Http.Responses.ApiKeys @using ShadcnBlazor.Dialogs @using ShadcnBlazor.Extras.Forms +@using ShadcnBlazor.Extras.Toasts @using ShadcnBlazor.Fields @using ShadcnBlazor.Inputs @inherits ShadcnBlazor.Extras.Dialogs.DialogBase +@inject HttpClient HttpClient +@inject ToastService ToastService + Update API key @@ -45,7 +50,7 @@ @code { - [Parameter] public Func OnSubmit { get; set; } + [Parameter] public Func OnSubmit { get; set; } [Parameter] public ApiKeyDto Key { get; set; } private UpdateApiKeyDto Request; @@ -60,7 +65,25 @@ private async Task OnSubmitAsync(EditContext editContext, ValidationMessageStore validationMessageStore) { Request.Permissions = Permissions.ToArray(); - await OnSubmit.Invoke(Request); + + var response = await HttpClient.PatchAsJsonAsync( + $"/api/admin/apiKeys/{Key.Id}", + Request, + Constants.SerializerOptions + ); + + if (!response.IsSuccessStatusCode) + { + await ProblemDetailsHelper.HandleProblemDetailsAsync(response, Request, validationMessageStore); + return false; + } + + await ToastService.SuccessAsync( + "API Key update", + $"Successfully updated API key {Request.Name}" + ); + + await OnSubmit.Invoke(); await CloseAsync(); return true; diff --git a/Moonlight.Frontend/UI/Admin/Modals/UpdateRoleDialog.razor b/Moonlight.Frontend/UI/Admin/Modals/UpdateRoleDialog.razor index 1c5870cb..22130a11 100644 --- a/Moonlight.Frontend/UI/Admin/Modals/UpdateRoleDialog.razor +++ b/Moonlight.Frontend/UI/Admin/Modals/UpdateRoleDialog.razor @@ -1,14 +1,19 @@ +@using Moonlight.Frontend.Helpers @using Moonlight.Frontend.Mappers @using Moonlight.Frontend.UI.Admin.Components @using Moonlight.Shared.Http.Requests.Roles @using Moonlight.Shared.Http.Responses.Admin @using ShadcnBlazor.Dialogs @using ShadcnBlazor.Extras.Forms +@using ShadcnBlazor.Extras.Toasts @using ShadcnBlazor.Fields @using ShadcnBlazor.Inputs @inherits ShadcnBlazor.Extras.Dialogs.DialogBase +@inject HttpClient HttpClient +@inject ToastService ToastService + Update @Role.Name @@ -51,7 +56,7 @@ @code { - [Parameter] public Func OnSubmit { get; set; } + [Parameter] public Func OnSubmit { get; set; } [Parameter] public RoleDto Role { get; set; } private UpdateRoleDto Request; @@ -66,7 +71,22 @@ private async Task OnSubmitAsync(EditContext editContext, ValidationMessageStore validationMessageStore) { Request.Permissions = Permissions.ToArray(); - await OnSubmit.Invoke(Request); + + var response = await HttpClient.PatchAsJsonAsync( + $"api/admin/roles/{Role.Id}", + Request, + Constants.SerializerOptions + ); + + if (!response.IsSuccessStatusCode) + { + await ProblemDetailsHelper.HandleProblemDetailsAsync(response, Request, validationMessageStore); + return false; + } + + await ToastService.SuccessAsync("Role update", $"Role {Request.Name} has been successfully updated"); + + await OnSubmit.Invoke(); await CloseAsync(); return true; diff --git a/Moonlight.Frontend/UI/Admin/Modals/UpdateUserDialog.razor b/Moonlight.Frontend/UI/Admin/Modals/UpdateUserDialog.razor index d564202a..145fa3cc 100644 --- a/Moonlight.Frontend/UI/Admin/Modals/UpdateUserDialog.razor +++ b/Moonlight.Frontend/UI/Admin/Modals/UpdateUserDialog.razor @@ -1,13 +1,19 @@ -@using Moonlight.Frontend.Mappers +@using Moonlight.Frontend.Helpers +@using Moonlight.Frontend.Mappers @using Moonlight.Shared.Http.Requests.Users +@using Moonlight.Shared.Http.Responses @using Moonlight.Shared.Http.Responses.Users @using ShadcnBlazor.Dialogs @using ShadcnBlazor.Extras.Forms +@using ShadcnBlazor.Extras.Toasts @using ShadcnBlazor.Fields @using ShadcnBlazor.Inputs @inherits ShadcnBlazor.Extras.Dialogs.DialogBase +@inject HttpClient HttpClient +@inject ToastService ToastService + Update @User.Username @@ -46,7 +52,7 @@ @code { - [Parameter] public Func OnSubmit { get; set; } + [Parameter] public Func OnCompleted { get; set; } [Parameter] public UserDto User { get; set; } private UpdateUserDto Request; @@ -58,7 +64,23 @@ private async Task OnSubmitAsync(EditContext editContext, ValidationMessageStore validationMessageStore) { - await OnSubmit.Invoke(Request); + var response = await HttpClient.PatchAsJsonAsync( + $"/api/admin/users/{User.Id}", + Request + ); + + if (!response.IsSuccessStatusCode) + { + await ProblemDetailsHelper.HandleProblemDetailsAsync(response, Request, validationMessageStore); + return false; + } + + await ToastService.SuccessAsync( + "User update", + $"Successfully updated user {Request.Username}" + ); + + await OnCompleted.Invoke(); await CloseAsync(); return true; diff --git a/Moonlight.Frontend/UI/Admin/Views/Sys/ApiKeys.razor b/Moonlight.Frontend/UI/Admin/Views/Sys/ApiKeys.razor index db99b5b2..ff344fd1 100644 --- a/Moonlight.Frontend/UI/Admin/Views/Sys/ApiKeys.razor +++ b/Moonlight.Frontend/UI/Admin/Views/Sys/ApiKeys.razor @@ -123,19 +123,8 @@ { await DialogService.LaunchAsync(parameters => { - parameters[nameof(CreateApiKeyDialog.OnSubmit)] = async (CreateApiKeyDto dto) => + parameters[nameof(CreateApiKeyDialog.OnSubmit)] = async () => { - await HttpClient.PostAsJsonAsync( - "/api/admin/apiKeys", - dto, - Constants.SerializerOptions - ); - - await ToastService.SuccessAsync( - "API Key creation", - $"Successfully created API key {dto.Name}" - ); - await Grid.RefreshAsync(); }; }); @@ -146,19 +135,8 @@ await DialogService.LaunchAsync(parameters => { parameters[nameof(UpdateApiKeyDialog.Key)] = key; - parameters[nameof(UpdateApiKeyDialog.OnSubmit)] = async (UpdateApiKeyDto dto) => + parameters[nameof(UpdateApiKeyDialog.OnSubmit)] = async () => { - await HttpClient.PatchAsJsonAsync( - $"/api/admin/apiKeys/{key.Id}", - dto, - Constants.SerializerOptions - ); - - await ToastService.SuccessAsync( - "API Key update", - $"Successfully updated API key {dto.Name}" - ); - await Grid.RefreshAsync(); }; }); diff --git a/Moonlight.Frontend/UI/Admin/Views/Sys/Themes/Create.razor b/Moonlight.Frontend/UI/Admin/Views/Sys/Themes/Create.razor index 1c5fa9e5..759eaa5d 100644 --- a/Moonlight.Frontend/UI/Admin/Views/Sys/Themes/Create.razor +++ b/Moonlight.Frontend/UI/Admin/Views/Sys/Themes/Create.razor @@ -3,6 +3,7 @@ @using Microsoft.AspNetCore.Authorization @using Moonlight.Shared @using LucideBlazor +@using Moonlight.Frontend.Helpers @using Moonlight.Frontend.Services @using Moonlight.Shared.Http.Requests.Themes @using ShadcnBlazor.Buttons @@ -118,11 +119,17 @@ { Request.CssContent = await Editor.GetValueAsync(); - await HttpClient.PostAsJsonAsync( + var response = await HttpClient.PostAsJsonAsync( "/api/admin/themes", Request, Constants.SerializerOptions ); + + if (!response.IsSuccessStatusCode) + { + await ProblemDetailsHelper.HandleProblemDetailsAsync(response, Request, validationMessageStore); + return false; + } await ToastService.SuccessAsync( "Theme creation", diff --git a/Moonlight.Frontend/UI/Admin/Views/Sys/Themes/Update.razor b/Moonlight.Frontend/UI/Admin/Views/Sys/Themes/Update.razor index eed40f9e..bab7f725 100644 --- a/Moonlight.Frontend/UI/Admin/Views/Sys/Themes/Update.razor +++ b/Moonlight.Frontend/UI/Admin/Views/Sys/Themes/Update.razor @@ -3,6 +3,7 @@ @using Microsoft.AspNetCore.Authorization @using Moonlight.Shared @using LucideBlazor +@using Moonlight.Frontend.Helpers @using Moonlight.Frontend.Mappers @using Moonlight.Frontend.Services @using Moonlight.Shared.Http.Requests.Themes @@ -132,11 +133,17 @@ { Request.CssContent = await Editor.GetValueAsync(); - await HttpClient.PatchAsJsonAsync( + var response = await HttpClient.PatchAsJsonAsync( $"/api/admin/themes/{Theme.Id}", Request, Constants.SerializerOptions ); + + if (!response.IsSuccessStatusCode) + { + await ProblemDetailsHelper.HandleProblemDetailsAsync(response, Request, validationMessageStore); + return false; + } await ToastService.SuccessAsync( "Theme update", diff --git a/Moonlight.Frontend/UI/Admin/Views/Users/Roles.razor b/Moonlight.Frontend/UI/Admin/Views/Users/Roles.razor index 7d7753ce..195ebae1 100644 --- a/Moonlight.Frontend/UI/Admin/Views/Users/Roles.razor +++ b/Moonlight.Frontend/UI/Admin/Views/Users/Roles.razor @@ -134,15 +134,8 @@ { await DialogService.LaunchAsync(parameters => { - parameters[nameof(CreateRoleDialog.OnSubmit)] = async Task (CreateRoleDto request) => + parameters[nameof(CreateRoleDialog.OnSubmit)] = async Task () => { - await HttpClient.PostAsJsonAsync( - "api/admin/roles", - request, - Constants.SerializerOptions - ); - - await ToastService.SuccessAsync("Role creation", $"Role {request.Name} has been successfully created"); await Grid.RefreshAsync(); }; }); @@ -153,15 +146,8 @@ await DialogService.LaunchAsync(parameters => { parameters[nameof(UpdateRoleDialog.Role)] = role; - parameters[nameof(UpdateRoleDialog.OnSubmit)] = async Task (UpdateRoleDto request) => + parameters[nameof(UpdateRoleDialog.OnSubmit)] = async Task () => { - await HttpClient.PatchAsJsonAsync( - $"api/admin/roles/{role.Id}", - request, - Constants.SerializerOptions - ); - - await ToastService.SuccessAsync("Role update", $"Role {request.Name} has been successfully updated"); await Grid.RefreshAsync(); }; }); diff --git a/Moonlight.Frontend/UI/Admin/Views/Users/Users.razor b/Moonlight.Frontend/UI/Admin/Views/Users/Users.razor index 55f49383..d7778005 100644 --- a/Moonlight.Frontend/UI/Admin/Views/Users/Users.razor +++ b/Moonlight.Frontend/UI/Admin/Views/Users/Users.razor @@ -124,19 +124,8 @@ { await DialogService.LaunchAsync(parameters => { - parameters[nameof(CreateUserDialog.OnSubmit)] = async (CreateUserDto dto) => + parameters[nameof(CreateUserDialog.OnCompleted)] = async () => { - await HttpClient.PostAsJsonAsync( - "/api/admin/users", - dto, - Constants.SerializerOptions - ); - - await ToastService.SuccessAsync( - "User creation", - $"Successfully created user {dto.Username}" - ); - await Grid.RefreshAsync(); }; }); @@ -147,18 +136,8 @@ await DialogService.LaunchAsync(parameters => { parameters[nameof(UpdateUserDialog.User)] = user; - parameters[nameof(CreateUserDialog.OnSubmit)] = async (UpdateUserDto dto) => + parameters[nameof(UpdateUserDialog.OnCompleted)] = async () => { - await HttpClient.PatchAsJsonAsync( - $"/api/admin/users/{user.Id}", - dto - ); - - await ToastService.SuccessAsync( - "User update", - $"Successfully updated user {dto.Username}" - ); - await Grid.RefreshAsync(); }; }); diff --git a/Moonlight.Shared/Http/Responses/ProblemDetails.cs b/Moonlight.Shared/Http/Responses/ProblemDetails.cs new file mode 100644 index 00000000..0af3bc50 --- /dev/null +++ b/Moonlight.Shared/Http/Responses/ProblemDetails.cs @@ -0,0 +1,10 @@ +namespace Moonlight.Shared.Http.Responses; + +public class ProblemDetails +{ + public string Type { get; set; } + public string Title { get; set; } + public int Status { get; set; } + public string? Detail { get; set; } + public Dictionary? Errors { get; set; } +} \ No newline at end of file diff --git a/Moonlight.Shared/Http/SerializationContext.cs b/Moonlight.Shared/Http/SerializationContext.cs index 3f22bc9e..d070f7ff 100644 --- a/Moonlight.Shared/Http/SerializationContext.cs +++ b/Moonlight.Shared/Http/SerializationContext.cs @@ -50,6 +50,9 @@ namespace Moonlight.Shared.Http; // Container Helper [JsonSerializable(typeof(ContainerHelperStatusDto))] + +// Misc +[JsonSerializable(typeof(ProblemDetails))] public partial class SerializationContext : JsonSerializerContext { } \ No newline at end of file