From 531115b6555390912208a5b4a73c88ccea5a8d93 Mon Sep 17 00:00:00 2001 From: Daniel Harvey Date: Mon, 18 May 2026 15:13:48 -0400 Subject: [PATCH] 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);