From 531115b6555390912208a5b4a73c88ccea5a8d93 Mon Sep 17 00:00:00 2001 From: Daniel Harvey Date: Mon, 18 May 2026 15:13:48 -0400 Subject: [PATCH 1/2] W3 T4: PUT api/cms/track/{id} + /cms/tracks/{id} edit page (metadata-only, Admin-gated) --- DeepDrftCms/DeepDrftCms.csproj | 1 + DeepDrftCms/Pages/Tracks/TrackEdit.razor | 230 +++++++++++++++++++ DeepDrftWeb/Controllers/CmsEditController.cs | 58 +++++ 3 files changed, 289 insertions(+) create mode 100644 DeepDrftCms/Pages/Tracks/TrackEdit.razor create mode 100644 DeepDrftWeb/Controllers/CmsEditController.cs diff --git a/DeepDrftCms/DeepDrftCms.csproj b/DeepDrftCms/DeepDrftCms.csproj index 65b0747..c484fdc 100644 --- a/DeepDrftCms/DeepDrftCms.csproj +++ b/DeepDrftCms/DeepDrftCms.csproj @@ -21,6 +21,7 @@ + diff --git a/DeepDrftCms/Pages/Tracks/TrackEdit.razor b/DeepDrftCms/Pages/Tracks/TrackEdit.razor new file mode 100644 index 0000000..1757f37 --- /dev/null +++ b/DeepDrftCms/Pages/Tracks/TrackEdit.razor @@ -0,0 +1,230 @@ +@page "/cms/tracks/{Id:int}" +@rendermode InteractiveServer +@using AuthBlocksWeb.HierarchicalAuthorize +@using AuthBlocksWeb.Services +@using DeepDrftWeb.Services +@using System.Net.Http.Headers +@using System.Net.Http.Json +@attribute [HierarchicalRoleAuthorize("Admin")] +@inject ITrackService TrackService +@inject IHttpClientFactory HttpClientFactory +@inject ITokenService TokenService +@inject ISnackbar Snackbar +@inject IDialogService DialogService +@inject NavigationManager Nav + +Edit Track — DeepDrft CMS + + + + Back to tracks + + + @if (_loading) + { + + } + else if (_track is null) + { + + Track not found. + + } + else + { + Edit Track + + + + + @_track.EntryKey + + Vault reference — not editable. + + + + + + + + + + + + + + + + Delete + + + + Save Changes + + + + + } + + +@code { + [Parameter] public int Id { get; set; } + + private TrackEntity? _track; + private TrackEditForm _form = new(); + private bool _loading = true; + private bool _busy; + + private bool CanSave => + !string.IsNullOrWhiteSpace(_form.TrackName) + && !string.IsNullOrWhiteSpace(_form.Artist); + + protected override async Task OnInitializedAsync() + { + await LoadAsync(); + } + + private async Task LoadAsync() + { + _loading = true; + var result = await TrackService.GetById(Id); + _track = result.Success ? result.Value : null; + if (_track is not null) + { + _form = TrackEditForm.From(_track); + } + _loading = false; + } + + private async Task SaveAsync() + { + if (_track is null || !CanSave) return; + + _busy = true; + try + { + var http = HttpClientFactory.CreateClient("DeepDrft.API"); + await AttachBearerAsync(http); + + var payload = new + { + 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 + }; + + var response = await http.PutAsJsonAsync($"api/cms/track/{Id}", payload); + if (response.IsSuccessStatusCode) + { + Snackbar.Add("Track updated.", Severity.Success); + await LoadAsync(); + } + else + { + Snackbar.Add($"Save failed: {(int)response.StatusCode} {response.ReasonPhrase}", Severity.Error); + } + } + catch (Exception ex) + { + Snackbar.Add($"Save failed: {ex.Message}", Severity.Error); + } + finally + { + _busy = false; + } + } + + private async Task ConfirmDelete() + { + if (_track is null) return; + + var confirmed = await DialogService.ShowMessageBox( + "Delete track", + $"Permanently delete \"{_track.TrackName}\" by {_track.Artist}? This cannot be undone.", + yesText: "Delete", + cancelText: "Cancel"); + + if (confirmed != true) return; + + _busy = true; + try + { + var http = HttpClientFactory.CreateClient("DeepDrft.API"); + await AttachBearerAsync(http); + + var response = await http.DeleteAsync($"api/cms/track/{Id}"); + if (response.IsSuccessStatusCode) + { + Snackbar.Add("Track deleted.", Severity.Success); + Nav.NavigateTo("/cms/tracks"); + } + else + { + Snackbar.Add($"Delete failed: {(int)response.StatusCode} {response.ReasonPhrase}", Severity.Error); + _busy = false; + } + } + catch (Exception ex) + { + Snackbar.Add($"Delete failed: {ex.Message}", Severity.Error); + _busy = false; + } + } + + private async Task AttachBearerAsync(HttpClient http) + { + var token = await TokenService.GetAccessTokenAsync(); + if (!string.IsNullOrEmpty(token)) + { + http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + } + } + + private sealed class TrackEditForm + { + public string TrackName { get; set; } = string.Empty; + public string Artist { get; set; } = string.Empty; + public string? Album { get; set; } + public string? Genre { get; set; } + public DateTime? ReleaseDate { get; set; } + + public static TrackEditForm From(TrackEntity track) => new() + { + TrackName = track.TrackName, + Artist = track.Artist, + Album = track.Album, + Genre = track.Genre, + ReleaseDate = track.ReleaseDate is { } d + ? d.ToDateTime(TimeOnly.MinValue) + : null + }; + } +} diff --git a/DeepDrftWeb/Controllers/CmsEditController.cs b/DeepDrftWeb/Controllers/CmsEditController.cs new file mode 100644 index 0000000..d6f2ec2 --- /dev/null +++ b/DeepDrftWeb/Controllers/CmsEditController.cs @@ -0,0 +1,58 @@ +using DeepDrftModels.Entities; +using DeepDrftWeb.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using NetBlocks.Models; + +namespace DeepDrftWeb.Controllers; + +[ApiController] +[Authorize(Roles = "Admin")] +[Route("api/cms/track")] +public class CmsEditController : ControllerBase +{ + private readonly ITrackService _trackService; + + public CmsEditController(ITrackService trackService) + { + _trackService = trackService; + } + + // Metadata-only update. EntryKey is immutable in Wave 1 — audio replacement + // is a separate Wave 2 operation that touches the vault. + [HttpPut("{id:int}")] + public async Task>> Update(int id, [FromBody] CmsTrackUpdateRequest request) + { + var existing = await _trackService.GetById(id); + if (!existing.Success) + { + var failure = ApiResult.CreateFailResult(existing.GetMessage()); + return StatusCode(500, new ApiResultDto(failure)); + } + + if (existing.Value is null) + { + return NotFound(); + } + + var track = existing.Value; + track.TrackName = request.TrackName; + track.Artist = request.Artist; + track.Album = request.Album; + track.Genre = request.Genre; + track.ReleaseDate = request.ReleaseDate; + + var updated = await _trackService.Update(track); + var apiResult = ApiResult.From(updated); + var dto = new ApiResultDto(apiResult); + + return updated.Success ? Ok(dto) : StatusCode(500, dto); + } +} + +public record CmsTrackUpdateRequest( + string TrackName, + string Artist, + string? Album, + string? Genre, + DateOnly? ReleaseDate); From 7b20694a311c7107cf3e31702b4825cb61c81915 Mon Sep 17 00:00:00 2001 From: Daniel Harvey Date: Mon, 18 May 2026 15:43:00 -0400 Subject: [PATCH 2/2] Fix W3-T4 review: log+sanitize catch messages, add validation attrs to CmsTrackUpdateRequest, document T3 delete dependency --- DeepDrftCms/Pages/Tracks/TrackEdit.razor | 9 +++++++-- DeepDrftCms/_Imports.razor | 1 + DeepDrftWeb/Controllers/CmsEditController.cs | 9 +++++---- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/DeepDrftCms/Pages/Tracks/TrackEdit.razor b/DeepDrftCms/Pages/Tracks/TrackEdit.razor index 1757f37..72344cc 100644 --- a/DeepDrftCms/Pages/Tracks/TrackEdit.razor +++ b/DeepDrftCms/Pages/Tracks/TrackEdit.razor @@ -1,4 +1,5 @@ @page "/cms/tracks/{Id:int}" +@* InteractiveServer: page injects ITrackService in-process; ITokenService reads localStorage via JS interop over the circuit. *@ @rendermode InteractiveServer @using AuthBlocksWeb.HierarchicalAuthorize @using AuthBlocksWeb.Services @@ -12,6 +13,7 @@ @inject ISnackbar Snackbar @inject IDialogService DialogService @inject NavigationManager Nav +@inject ILogger Logger Edit Track — DeepDrft CMS @@ -154,7 +156,8 @@ } catch (Exception ex) { - Snackbar.Add($"Save failed: {ex.Message}", Severity.Error); + Logger.LogError(ex, "Save failed for track {TrackId}", Id); + Snackbar.Add("Save failed — please try again.", Severity.Error); } finally { @@ -162,6 +165,7 @@ } } + // DELETE api/cms/track/{Id} is handled by CmsDeleteController (T3 branch). private async Task ConfirmDelete() { if (_track is null) return; @@ -194,7 +198,8 @@ } catch (Exception ex) { - Snackbar.Add($"Delete failed: {ex.Message}", Severity.Error); + Logger.LogError(ex, "Delete failed for track {TrackId}", Id); + Snackbar.Add("Delete failed — please try again.", Severity.Error); _busy = false; } } diff --git a/DeepDrftCms/_Imports.razor b/DeepDrftCms/_Imports.razor index cf3d836..5a87bee 100644 --- a/DeepDrftCms/_Imports.razor +++ b/DeepDrftCms/_Imports.razor @@ -5,6 +5,7 @@ @using Microsoft.AspNetCore.Components.Web @using Microsoft.AspNetCore.Components.Web.Virtualization @using static Microsoft.AspNetCore.Components.Web.RenderMode +@using Microsoft.Extensions.Logging @using Microsoft.JSInterop @using DeepDrftCms @using DeepDrftModels.Entities diff --git a/DeepDrftWeb/Controllers/CmsEditController.cs b/DeepDrftWeb/Controllers/CmsEditController.cs index d6f2ec2..d9d088b 100644 --- a/DeepDrftWeb/Controllers/CmsEditController.cs +++ b/DeepDrftWeb/Controllers/CmsEditController.cs @@ -1,3 +1,4 @@ +using System.ComponentModel.DataAnnotations; using DeepDrftModels.Entities; using DeepDrftWeb.Services; using Microsoft.AspNetCore.Authorization; @@ -51,8 +52,8 @@ public class CmsEditController : ControllerBase } public record CmsTrackUpdateRequest( - string TrackName, - string Artist, - string? Album, - string? Genre, + [Required, MaxLength(200)] string TrackName, + [Required, MaxLength(200)] string Artist, + [MaxLength(200)] string? Album, + [MaxLength(100)] string? Genre, DateOnly? ReleaseDate);