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.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
<DialogHeader>
<DialogTitle>Create new API key</DialogTitle>
<DialogDescription>
@@ -44,7 +50,7 @@
@code
{
[Parameter] public Func<CreateApiKeyDto, Task> OnSubmit { get; set; }
[Parameter] public Func<Task> OnSubmit { get; set; }
private CreateApiKeyDto Request;
@@ -62,7 +68,24 @@
{
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();
return true;

View File

@@ -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
<DialogHeader>
<DialogTitle>
Create new role
@@ -49,7 +54,7 @@
@code
{
[Parameter] public Func<CreateRoleDto, Task> OnSubmit { get; set; }
[Parameter] public Func<Task> OnSubmit { get; set; }
private CreateRoleDto Request;
private List<string> Permissions;
@@ -68,7 +73,21 @@
{
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();
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.Extras.Forms
@using ShadcnBlazor.Extras.Toasts
@using ShadcnBlazor.Fields
@using ShadcnBlazor.Inputs
@inherits ShadcnBlazor.Extras.Dialogs.DialogBase
@inject HttpClient HttpClient
@inject ToastService ToastService
<DialogHeader>
<DialogTitle>
Create new user
@@ -45,7 +51,7 @@
@code
{
[Parameter] public Func<CreateUserDto, Task> OnSubmit { get; set; }
[Parameter] public Func<Task> OnCompleted { get; set; }
private CreateUserDto Request;
@@ -56,7 +62,24 @@
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();
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.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
<DialogHeader>
<DialogTitle>Update API key</DialogTitle>
<DialogDescription>
@@ -45,7 +50,7 @@
@code
{
[Parameter] public Func<UpdateApiKeyDto, Task> OnSubmit { get; set; }
[Parameter] public Func<Task> OnSubmit { get; set; }
[Parameter] public ApiKeyDto Key { get; set; }
private UpdateApiKeyDto Request;
@@ -60,7 +65,25 @@
private async Task<bool> 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;

View File

@@ -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
<DialogHeader>
<DialogTitle>
Update @Role.Name
@@ -51,7 +56,7 @@
@code
{
[Parameter] public Func<UpdateRoleDto, Task> OnSubmit { get; set; }
[Parameter] public Func<Task> OnSubmit { get; set; }
[Parameter] public RoleDto Role { get; set; }
private UpdateRoleDto Request;
@@ -66,7 +71,22 @@
private async Task<bool> 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;

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.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
<DialogHeader>
<DialogTitle>
Update @User.Username
@@ -46,7 +52,7 @@
@code
{
[Parameter] public Func<UpdateUserDto, Task> OnSubmit { get; set; }
[Parameter] public Func<Task> OnCompleted { get; set; }
[Parameter] public UserDto User { get; set; }
private UpdateUserDto Request;
@@ -58,7 +64,23 @@
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();
return true;

View File

@@ -123,19 +123,8 @@
{
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();
};
});
@@ -146,19 +135,8 @@
await DialogService.LaunchAsync<UpdateApiKeyDialog>(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();
};
});

View File

@@ -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,12 +119,18 @@
{
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",
$"Successfully created theme {Request.Name}"

View File

@@ -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,12 +133,18 @@
{
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",
$"Successfully updated theme {Request.Name}"

View File

@@ -134,15 +134,8 @@
{
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();
};
});
@@ -153,15 +146,8 @@
await DialogService.LaunchAsync<UpdateRoleDialog>(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();
};
});

View File

@@ -124,19 +124,8 @@
{
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();
};
});
@@ -147,18 +136,8 @@
await DialogService.LaunchAsync<UpdateUserDialog>(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();
};
});

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
[JsonSerializable(typeof(ContainerHelperStatusDto))]
// Misc
[JsonSerializable(typeof(ProblemDetails))]
public partial class SerializationContext : JsonSerializerContext
{
}