refactor(manager): replace internal CMS HTTP layer with direct CmsTrackService calls

This commit is contained in:
Daniel Harvey
2026-05-24 20:46:22 -04:00
parent 058e4ca196
commit 428359b241
10 changed files with 301 additions and 509 deletions
@@ -1,10 +1,8 @@
@page "/cms/tracks/{Id:int}"
@using System.Net.Http.Headers
@using System.Net.Http.Json
@using DeepDrftManager.Services
@attribute [HierarchicalRoleAuthorize([SystemRoleConstants.Admin])]
@inject ITrackService TrackService
@inject IHttpClientFactory HttpClientFactory
@inject IAuthSession AuthSession
@inject ICmsTrackService CmsTrackService
@inject ISnackbar Snackbar
@inject IDialogService DialogService
@inject NavigationManager Nav
@@ -126,27 +124,32 @@
_busy = true;
try
{
var http = HttpClientFactory.CreateClient("DeepDrft.API");
await AttachBearerAsync(http);
var payload = new
// Re-fetch under the current scope so we mutate the DB-authoritative entity, not
// the copy loaded at OnInitialized. Metadata-only update — EntryKey is immutable.
var lookup = await TrackService.GetById(Id);
if (!lookup.Success || lookup.Value is null)
{
TrackName = _form.TrackName,
Artist = _form.Artist,
Album = string.IsNullOrWhiteSpace(_form.Album) ? null : _form.Album,
Genre = string.IsNullOrWhiteSpace(_form.Genre) ? null : _form.Genre,
ReleaseDate = _form.ReleaseDate is { } d ? DateOnly.FromDateTime(d) : (DateOnly?)null
};
Snackbar.Add("Save failed — track could not be loaded.", Severity.Error);
return;
}
var response = await http.PutAsJsonAsync($"api/cms/track/{Id}", payload);
if (response.IsSuccessStatusCode)
var track = lookup.Value;
track.TrackName = _form.TrackName;
track.Artist = _form.Artist;
track.Album = string.IsNullOrWhiteSpace(_form.Album) ? null : _form.Album;
track.Genre = string.IsNullOrWhiteSpace(_form.Genre) ? null : _form.Genre;
track.ReleaseDate = _form.ReleaseDate is { } d ? DateOnly.FromDateTime(d) : null;
var updated = await TrackService.Update(track);
if (updated.Success)
{
Snackbar.Add("Track updated.", Severity.Success);
await LoadAsync();
}
else
{
Snackbar.Add($"Save failed: {(int)response.StatusCode} {response.ReasonPhrase}", Severity.Error);
var error = updated.Messages.FirstOrDefault()?.Message ?? "Unknown error";
Snackbar.Add($"Save failed: {error}", Severity.Error);
}
}
catch (Exception ex)
@@ -160,7 +163,6 @@
}
}
// DELETE api/cms/track/{Id} is handled by CmsDeleteController (T3 branch).
private async Task ConfirmDelete()
{
if (_track is null) return;
@@ -176,18 +178,16 @@
_busy = true;
try
{
var http = HttpClientFactory.CreateClient("DeepDrft.API");
await AttachBearerAsync(http);
var response = await http.DeleteAsync($"api/cms/track/{Id}");
if (response.IsSuccessStatusCode)
var result = await CmsTrackService.DeleteTrackAsync(Id);
if (result.Success)
{
Snackbar.Add("Track deleted.", Severity.Success);
Nav.NavigateTo("/cms/tracks");
}
else
{
Snackbar.Add($"Delete failed: {(int)response.StatusCode} {response.ReasonPhrase}", Severity.Error);
var error = result.Messages.FirstOrDefault()?.Message ?? "Unknown error";
Snackbar.Add($"Delete failed: {error}", Severity.Error);
_busy = false;
}
}
@@ -199,15 +199,6 @@
}
}
private async Task AttachBearerAsync(HttpClient http)
{
var token = await AuthSession.GetValidTokenAsync();
if (!string.IsNullOrEmpty(token))
{
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
}
}
private sealed class TrackEditForm
{
public string TrackName { get; set; } = string.Empty;
@@ -1,10 +1,9 @@
@page "/cms/tracks"
@using System.Net
@using System.Net.Http.Headers
@using DeepDrftManager.Services
@attribute [HierarchicalRoleAuthorize([SystemRoleConstants.Admin])]
@inject ITrackService TrackService
@inject IHttpClientFactory HttpClientFactory
@inject IAuthSession AuthSession
@inject ICmsTrackService CmsTrackService
@inject IDialogService DialogService
@inject ISnackbar Snackbar
@inject ILogger<TrackList> Logger
@@ -113,20 +112,16 @@
try
{
var client = HttpClientFactory.CreateClient("DeepDrft.API");
var token = await AuthSession.GetValidTokenAsync();
if (!string.IsNullOrEmpty(token))
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
var response = await client.DeleteAsync($"api/cms/track/{track.Id}");
if (response.IsSuccessStatusCode)
var result = await CmsTrackService.DeleteTrackAsync(track.Id);
if (result.Success)
{
Snackbar.Add($"Deleted '{track.TrackName}'.", Severity.Success);
if (_table is not null) await _table.ReloadServerData();
}
else
{
Snackbar.Add($"Delete failed ({(int)response.StatusCode} {response.ReasonPhrase}).", Severity.Error);
var error = result.Messages.FirstOrDefault()?.Message ?? "Unknown error";
Snackbar.Add($"Delete failed: {error}", Severity.Error);
}
}
catch (Exception ex)
@@ -1,12 +1,13 @@
@page "/cms/tracks/new"
@using System.Net.Http.Headers
@using System.Security.Claims
@using DeepDrftManager.Services
@attribute [HierarchicalRoleAuthorize([SystemRoleConstants.Admin])]
@inject IHttpClientFactory HttpClientFactory
@inject ICmsTrackService CmsTrackService
@inject AuthenticationStateProvider AuthStateProvider
@inject NavigationManager Navigation
@inject ISnackbar Snackbar
@inject ILogger<TrackNew> Logger
@inject IAuthSession AuthSession
<PageTitle>Add Track — DeepDrft CMS</PageTitle>
@@ -61,8 +62,8 @@
</MudContainer>
@code {
// 1 GB ceiling matches the proxy controller's RequestSizeLimit; the actual streaming
// path means the limit caps the request, not in-memory buffering.
// 1 GB ceiling matches DeepDrftContent's per-request limit on api/track/upload; the
// streaming path means the limit caps the request, not in-memory buffering.
private const long MaxUploadBytes = 1_073_741_824L;
private IBrowserFile? _selectedFile;
@@ -115,41 +116,46 @@
return;
}
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
var userIdValue = authState.User.FindFirstValue(ClaimTypes.NameIdentifier);
if (!long.TryParse(userIdValue, out var createdByUserId))
{
// The page is gated by [HierarchicalRoleAuthorize(Admin)], so a missing or
// unparseable id here is a configuration bug, not normal client state.
Logger.LogError("Authenticated user has no parseable NameIdentifier claim: {Value}", userIdValue);
_errorMessage = "Your session is missing a valid identifier. Please sign in again.";
return;
}
_isUploading = true;
try
{
using var multipart = new MultipartFormDataContent();
// OpenReadStream streams chunks from the browser via the SignalR circuit;
// wrapping in StreamContent avoids materialising the whole file in memory
// before the proxy controller receives it.
// OpenReadStream streams chunks from the browser via the SignalR circuit; the
// service wraps it in StreamContent so the whole file is never materialised in
// memory before DeepDrftContent receives it.
await using var fileStream = _selectedFile.OpenReadStream(MaxUploadBytes);
var fileContent = new StreamContent(fileStream);
fileContent.Headers.ContentType = new MediaTypeHeaderValue(
string.IsNullOrWhiteSpace(_selectedFile.ContentType) ? "audio/wav" : _selectedFile.ContentType);
multipart.Add(fileContent, "wav", _selectedFile.Name);
multipart.Add(new StringContent(_trackName), "trackName");
multipart.Add(new StringContent(_artist), "artist");
if (!string.IsNullOrWhiteSpace(_album)) multipart.Add(new StringContent(_album), "album");
if (!string.IsNullOrWhiteSpace(_genre)) multipart.Add(new StringContent(_genre), "genre");
if (!string.IsNullOrWhiteSpace(_releaseDate)) multipart.Add(new StringContent(_releaseDate), "releaseDate");
var client = HttpClientFactory.CreateClient("DeepDrft.API");
await AttachBearerAsync(client);
using var response = await client.PostAsync("api/cms/track", multipart);
var result = await CmsTrackService.UploadTrackAsync(
fileStream,
_selectedFile.Name,
_selectedFile.ContentType,
_trackName,
_artist,
string.IsNullOrWhiteSpace(_album) ? null : _album,
string.IsNullOrWhiteSpace(_genre) ? null : _genre,
string.IsNullOrWhiteSpace(_releaseDate) ? null : _releaseDate,
createdByUserId);
if (response.IsSuccessStatusCode)
if (result.Success)
{
Snackbar.Add($"Uploaded '{_trackName}'.", Severity.Success);
Navigation.NavigateTo("/cms/tracks");
return;
}
var body = await response.Content.ReadAsStringAsync();
_errorMessage = string.IsNullOrWhiteSpace(body)
? $"Upload failed ({(int)response.StatusCode})."
: $"Upload failed ({(int)response.StatusCode}): {body}";
Logger.LogWarning("CMS upload rejected: {Status} {Body}", (int)response.StatusCode, body);
var error = result.Messages.FirstOrDefault()?.Message ?? "Unknown error";
_errorMessage = $"Upload failed: {error}";
Logger.LogWarning("CMS upload rejected: {Error}", error);
}
catch (Exception ex)
{
@@ -167,15 +173,6 @@
Navigation.NavigateTo("/cms/tracks");
}
private async Task AttachBearerAsync(HttpClient http)
{
var token = await AuthSession.GetValidTokenAsync();
if (!string.IsNullOrEmpty(token))
{
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
}
}
private static string FormatBytes(long bytes)
{
const long KB = 1024;
@@ -1,88 +0,0 @@
@using System.Net.Http
@using System.Net.Http.Headers
@using Microsoft.AspNetCore.Components
@inject IHttpClientFactory HttpClientFactory
@inject IAuthSession AuthSession
<MudDialog>
<DialogContent>
<MudText Typo="Typo.body1">
Are you sure you want to delete '@TrackName'? This cannot be undone.
</MudText>
@if (!string.IsNullOrEmpty(_errorMessage))
{
<MudAlert Severity="Severity.Error" Class="mt-3" Dense="true">@_errorMessage</MudAlert>
}
</DialogContent>
<DialogActions>
<MudButton OnClick="Cancel" Disabled="_isDeleting">Cancel</MudButton>
<MudButton Color="Color.Error"
Variant="Variant.Filled"
OnClick="ConfirmAsync"
Disabled="_isDeleting">
@if (_isDeleting)
{
<MudProgressCircular Size="Size.Small" Indeterminate="true" Class="me-2" />
<span>Deleting...</span>
}
else
{
<span>Delete</span>
}
</MudButton>
</DialogActions>
</MudDialog>
@code {
[CascadingParameter] private IMudDialogInstance MudDialog { get; set; } = default!;
[Parameter] public long TrackId { get; set; }
[Parameter] public string TrackName { get; set; } = "";
[Parameter] public EventCallback OnDeleted { get; set; }
private bool _isDeleting;
private string? _errorMessage;
private async Task ConfirmAsync()
{
_isDeleting = true;
_errorMessage = null;
try
{
var client = HttpClientFactory.CreateClient("DeepDrft.API");
var token = await AuthSession.GetValidTokenAsync();
if (!string.IsNullOrEmpty(token))
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
var response = await client.DeleteAsync($"api/cms/track/{TrackId}");
if (response.IsSuccessStatusCode)
{
if (OnDeleted.HasDelegate)
{
await OnDeleted.InvokeAsync();
}
MudDialog.Close(DialogResult.Ok(true));
return;
}
_errorMessage = response.StatusCode switch
{
System.Net.HttpStatusCode.NotFound => "Track not found. It may have already been deleted.",
System.Net.HttpStatusCode.Unauthorized => "You are not authorized to delete this track.",
System.Net.HttpStatusCode.Forbidden => "You are not authorized to delete this track.",
_ => $"Delete failed ({(int)response.StatusCode})."
};
}
catch (Exception ex)
{
_errorMessage = $"Delete failed: {ex.Message}";
}
finally
{
_isDeleting = false;
}
}
private void Cancel() => MudDialog.Cancel();
}