Implemented handling of server side issues using the rfc for problem detasils in the frontend

This commit is contained in:
2026-01-29 09:28:50 +01:00
parent 136620f1e6
commit 97a676ccd7
14 changed files with 212 additions and 82 deletions

View File

@@ -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<ProblemDetails>();
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);
}
}
}
}
}

View File

@@ -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.Requests.ApiKeys
@using Moonlight.Shared.Http.Responses
@using ShadcnBlazor.Dialogs @using ShadcnBlazor.Dialogs
@using ShadcnBlazor.Extras.Forms @using ShadcnBlazor.Extras.Forms
@using ShadcnBlazor.Extras.Toasts
@using ShadcnBlazor.Fields @using ShadcnBlazor.Fields
@using ShadcnBlazor.Inputs @using ShadcnBlazor.Inputs
@inherits ShadcnBlazor.Extras.Dialogs.DialogBase @inherits ShadcnBlazor.Extras.Dialogs.DialogBase
@inject HttpClient HttpClient
@inject ToastService ToastService
<DialogHeader> <DialogHeader>
<DialogTitle>Create new API key</DialogTitle> <DialogTitle>Create new API key</DialogTitle>
<DialogDescription> <DialogDescription>
@@ -44,7 +50,7 @@
@code @code
{ {
[Parameter] public Func<CreateApiKeyDto, Task> OnSubmit { get; set; } [Parameter] public Func<Task> OnSubmit { get; set; }
private CreateApiKeyDto Request; private CreateApiKeyDto Request;
@@ -62,7 +68,24 @@
{ {
Request.Permissions = Permissions.ToArray(); Request.Permissions = Permissions.ToArray();
await OnSubmit.Invoke(Request); var response = await HttpClient.PostAsJsonAsync(
"/api/admin/apiKeys",
Request,
Constants.SerializerOptions
);
if (!response.IsSuccessStatusCode)
{
await ProblemDetailsHelper.HandleProblemDetailsAsync(response, Request, validationMessageStore);
return false;
}
await ToastService.SuccessAsync(
"API Key creation",
$"Successfully created API key {Request.Name}"
);
await OnSubmit.Invoke();
await CloseAsync(); await CloseAsync();
return true; return true;

View File

@@ -1,12 +1,17 @@
@using Moonlight.Frontend.Helpers
@using Moonlight.Frontend.UI.Admin.Components @using Moonlight.Frontend.UI.Admin.Components
@using Moonlight.Shared.Http.Requests.Roles @using Moonlight.Shared.Http.Requests.Roles
@using ShadcnBlazor.Dialogs @using ShadcnBlazor.Dialogs
@using ShadcnBlazor.Extras.Forms @using ShadcnBlazor.Extras.Forms
@using ShadcnBlazor.Extras.Toasts
@using ShadcnBlazor.Fields @using ShadcnBlazor.Fields
@using ShadcnBlazor.Inputs @using ShadcnBlazor.Inputs
@inherits ShadcnBlazor.Extras.Dialogs.DialogBase @inherits ShadcnBlazor.Extras.Dialogs.DialogBase
@inject HttpClient HttpClient
@inject ToastService ToastService
<DialogHeader> <DialogHeader>
<DialogTitle> <DialogTitle>
Create new role Create new role
@@ -49,7 +54,7 @@
@code @code
{ {
[Parameter] public Func<CreateRoleDto, Task> OnSubmit { get; set; } [Parameter] public Func<Task> OnSubmit { get; set; }
private CreateRoleDto Request; private CreateRoleDto Request;
private List<string> Permissions; private List<string> Permissions;
@@ -68,7 +73,21 @@
{ {
Request.Permissions = Permissions.ToArray(); Request.Permissions = Permissions.ToArray();
await OnSubmit.Invoke(Request); var response = await HttpClient.PostAsJsonAsync(
"api/admin/roles",
Request,
Constants.SerializerOptions
);
if (!response.IsSuccessStatusCode)
{
await ProblemDetailsHelper.HandleProblemDetailsAsync(response, Request, validationMessageStore);
return false;
}
await ToastService.SuccessAsync("Role creation", $"Role {Request.Name} has been successfully created");
await OnSubmit.Invoke();
await CloseAsync(); await CloseAsync();
return true; return true;

View File

@@ -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.Dialogs
@using ShadcnBlazor.Extras.Forms @using ShadcnBlazor.Extras.Forms
@using ShadcnBlazor.Extras.Toasts
@using ShadcnBlazor.Fields @using ShadcnBlazor.Fields
@using ShadcnBlazor.Inputs @using ShadcnBlazor.Inputs
@inherits ShadcnBlazor.Extras.Dialogs.DialogBase @inherits ShadcnBlazor.Extras.Dialogs.DialogBase
@inject HttpClient HttpClient
@inject ToastService ToastService
<DialogHeader> <DialogHeader>
<DialogTitle> <DialogTitle>
Create new user Create new user
@@ -45,7 +51,7 @@
@code @code
{ {
[Parameter] public Func<CreateUserDto, Task> OnSubmit { get; set; } [Parameter] public Func<Task> OnCompleted { get; set; }
private CreateUserDto Request; private CreateUserDto Request;
@@ -56,7 +62,24 @@
private async Task<bool> OnSubmitAsync(EditContext editContext, ValidationMessageStore validationMessageStore) private async Task<bool> 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(); await CloseAsync();
return true; return true;

View File

@@ -1,14 +1,19 @@
@using Moonlight.Frontend.Mappers @using Moonlight.Frontend.Helpers
@using Moonlight.Frontend.Mappers
@using Moonlight.Frontend.UI.Admin.Components @using Moonlight.Frontend.UI.Admin.Components
@using Moonlight.Shared.Http.Requests.ApiKeys @using Moonlight.Shared.Http.Requests.ApiKeys
@using Moonlight.Shared.Http.Responses.ApiKeys @using Moonlight.Shared.Http.Responses.ApiKeys
@using ShadcnBlazor.Dialogs @using ShadcnBlazor.Dialogs
@using ShadcnBlazor.Extras.Forms @using ShadcnBlazor.Extras.Forms
@using ShadcnBlazor.Extras.Toasts
@using ShadcnBlazor.Fields @using ShadcnBlazor.Fields
@using ShadcnBlazor.Inputs @using ShadcnBlazor.Inputs
@inherits ShadcnBlazor.Extras.Dialogs.DialogBase @inherits ShadcnBlazor.Extras.Dialogs.DialogBase
@inject HttpClient HttpClient
@inject ToastService ToastService
<DialogHeader> <DialogHeader>
<DialogTitle>Update API key</DialogTitle> <DialogTitle>Update API key</DialogTitle>
<DialogDescription> <DialogDescription>
@@ -45,7 +50,7 @@
@code @code
{ {
[Parameter] public Func<UpdateApiKeyDto, Task> OnSubmit { get; set; } [Parameter] public Func<Task> OnSubmit { get; set; }
[Parameter] public ApiKeyDto Key { get; set; } [Parameter] public ApiKeyDto Key { get; set; }
private UpdateApiKeyDto Request; private UpdateApiKeyDto Request;
@@ -60,7 +65,25 @@
private async Task<bool> OnSubmitAsync(EditContext editContext, ValidationMessageStore validationMessageStore) private async Task<bool> OnSubmitAsync(EditContext editContext, ValidationMessageStore validationMessageStore)
{ {
Request.Permissions = Permissions.ToArray(); 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(); await CloseAsync();
return true; return true;

View File

@@ -1,14 +1,19 @@
@using Moonlight.Frontend.Helpers
@using Moonlight.Frontend.Mappers @using Moonlight.Frontend.Mappers
@using Moonlight.Frontend.UI.Admin.Components @using Moonlight.Frontend.UI.Admin.Components
@using Moonlight.Shared.Http.Requests.Roles @using Moonlight.Shared.Http.Requests.Roles
@using Moonlight.Shared.Http.Responses.Admin @using Moonlight.Shared.Http.Responses.Admin
@using ShadcnBlazor.Dialogs @using ShadcnBlazor.Dialogs
@using ShadcnBlazor.Extras.Forms @using ShadcnBlazor.Extras.Forms
@using ShadcnBlazor.Extras.Toasts
@using ShadcnBlazor.Fields @using ShadcnBlazor.Fields
@using ShadcnBlazor.Inputs @using ShadcnBlazor.Inputs
@inherits ShadcnBlazor.Extras.Dialogs.DialogBase @inherits ShadcnBlazor.Extras.Dialogs.DialogBase
@inject HttpClient HttpClient
@inject ToastService ToastService
<DialogHeader> <DialogHeader>
<DialogTitle> <DialogTitle>
Update @Role.Name Update @Role.Name
@@ -51,7 +56,7 @@
@code @code
{ {
[Parameter] public Func<UpdateRoleDto, Task> OnSubmit { get; set; } [Parameter] public Func<Task> OnSubmit { get; set; }
[Parameter] public RoleDto Role { get; set; } [Parameter] public RoleDto Role { get; set; }
private UpdateRoleDto Request; private UpdateRoleDto Request;
@@ -66,7 +71,22 @@
private async Task<bool> OnSubmitAsync(EditContext editContext, ValidationMessageStore validationMessageStore) private async Task<bool> OnSubmitAsync(EditContext editContext, ValidationMessageStore validationMessageStore)
{ {
Request.Permissions = Permissions.ToArray(); 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(); await CloseAsync();
return true; return true;

View File

@@ -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.Requests.Users
@using Moonlight.Shared.Http.Responses
@using Moonlight.Shared.Http.Responses.Users @using Moonlight.Shared.Http.Responses.Users
@using ShadcnBlazor.Dialogs @using ShadcnBlazor.Dialogs
@using ShadcnBlazor.Extras.Forms @using ShadcnBlazor.Extras.Forms
@using ShadcnBlazor.Extras.Toasts
@using ShadcnBlazor.Fields @using ShadcnBlazor.Fields
@using ShadcnBlazor.Inputs @using ShadcnBlazor.Inputs
@inherits ShadcnBlazor.Extras.Dialogs.DialogBase @inherits ShadcnBlazor.Extras.Dialogs.DialogBase
@inject HttpClient HttpClient
@inject ToastService ToastService
<DialogHeader> <DialogHeader>
<DialogTitle> <DialogTitle>
Update @User.Username Update @User.Username
@@ -46,7 +52,7 @@
@code @code
{ {
[Parameter] public Func<UpdateUserDto, Task> OnSubmit { get; set; } [Parameter] public Func<Task> OnCompleted { get; set; }
[Parameter] public UserDto User { get; set; } [Parameter] public UserDto User { get; set; }
private UpdateUserDto Request; private UpdateUserDto Request;
@@ -58,7 +64,23 @@
private async Task<bool> OnSubmitAsync(EditContext editContext, ValidationMessageStore validationMessageStore) private async Task<bool> 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(); await CloseAsync();
return true; return true;

View File

@@ -123,19 +123,8 @@
{ {
await DialogService.LaunchAsync<CreateApiKeyDialog>(parameters => await DialogService.LaunchAsync<CreateApiKeyDialog>(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(); await Grid.RefreshAsync();
}; };
}); });
@@ -146,19 +135,8 @@
await DialogService.LaunchAsync<UpdateApiKeyDialog>(parameters => await DialogService.LaunchAsync<UpdateApiKeyDialog>(parameters =>
{ {
parameters[nameof(UpdateApiKeyDialog.Key)] = key; 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(); await Grid.RefreshAsync();
}; };
}); });

View File

@@ -3,6 +3,7 @@
@using Microsoft.AspNetCore.Authorization @using Microsoft.AspNetCore.Authorization
@using Moonlight.Shared @using Moonlight.Shared
@using LucideBlazor @using LucideBlazor
@using Moonlight.Frontend.Helpers
@using Moonlight.Frontend.Services @using Moonlight.Frontend.Services
@using Moonlight.Shared.Http.Requests.Themes @using Moonlight.Shared.Http.Requests.Themes
@using ShadcnBlazor.Buttons @using ShadcnBlazor.Buttons
@@ -118,12 +119,18 @@
{ {
Request.CssContent = await Editor.GetValueAsync(); Request.CssContent = await Editor.GetValueAsync();
await HttpClient.PostAsJsonAsync( var response = await HttpClient.PostAsJsonAsync(
"/api/admin/themes", "/api/admin/themes",
Request, Request,
Constants.SerializerOptions Constants.SerializerOptions
); );
if (!response.IsSuccessStatusCode)
{
await ProblemDetailsHelper.HandleProblemDetailsAsync(response, Request, validationMessageStore);
return false;
}
await ToastService.SuccessAsync( await ToastService.SuccessAsync(
"Theme creation", "Theme creation",
$"Successfully created theme {Request.Name}" $"Successfully created theme {Request.Name}"

View File

@@ -3,6 +3,7 @@
@using Microsoft.AspNetCore.Authorization @using Microsoft.AspNetCore.Authorization
@using Moonlight.Shared @using Moonlight.Shared
@using LucideBlazor @using LucideBlazor
@using Moonlight.Frontend.Helpers
@using Moonlight.Frontend.Mappers @using Moonlight.Frontend.Mappers
@using Moonlight.Frontend.Services @using Moonlight.Frontend.Services
@using Moonlight.Shared.Http.Requests.Themes @using Moonlight.Shared.Http.Requests.Themes
@@ -132,12 +133,18 @@
{ {
Request.CssContent = await Editor.GetValueAsync(); Request.CssContent = await Editor.GetValueAsync();
await HttpClient.PatchAsJsonAsync( var response = await HttpClient.PatchAsJsonAsync(
$"/api/admin/themes/{Theme.Id}", $"/api/admin/themes/{Theme.Id}",
Request, Request,
Constants.SerializerOptions Constants.SerializerOptions
); );
if (!response.IsSuccessStatusCode)
{
await ProblemDetailsHelper.HandleProblemDetailsAsync(response, Request, validationMessageStore);
return false;
}
await ToastService.SuccessAsync( await ToastService.SuccessAsync(
"Theme update", "Theme update",
$"Successfully updated theme {Request.Name}" $"Successfully updated theme {Request.Name}"

View File

@@ -134,15 +134,8 @@
{ {
await DialogService.LaunchAsync<CreateRoleDialog>(parameters => await DialogService.LaunchAsync<CreateRoleDialog>(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(); await Grid.RefreshAsync();
}; };
}); });
@@ -153,15 +146,8 @@
await DialogService.LaunchAsync<UpdateRoleDialog>(parameters => await DialogService.LaunchAsync<UpdateRoleDialog>(parameters =>
{ {
parameters[nameof(UpdateRoleDialog.Role)] = role; 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(); await Grid.RefreshAsync();
}; };
}); });

View File

@@ -124,19 +124,8 @@
{ {
await DialogService.LaunchAsync<CreateUserDialog>(parameters => await DialogService.LaunchAsync<CreateUserDialog>(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(); await Grid.RefreshAsync();
}; };
}); });
@@ -147,18 +136,8 @@
await DialogService.LaunchAsync<UpdateUserDialog>(parameters => await DialogService.LaunchAsync<UpdateUserDialog>(parameters =>
{ {
parameters[nameof(UpdateUserDialog.User)] = user; 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(); await Grid.RefreshAsync();
}; };
}); });

View File

@@ -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<string, string[]>? Errors { get; set; }
}

View File

@@ -50,6 +50,9 @@ namespace Moonlight.Shared.Http;
// Container Helper // Container Helper
[JsonSerializable(typeof(ContainerHelperStatusDto))] [JsonSerializable(typeof(ContainerHelperStatusDto))]
// Misc
[JsonSerializable(typeof(ProblemDetails))]
public partial class SerializationContext : JsonSerializerContext public partial class SerializationContext : JsonSerializerContext
{ {
} }