diff --git a/PLAN_authblocks_trackmanager.md b/PLAN_authblocks_trackmanager.md new file mode 100644 index 0000000..7559854 --- /dev/null +++ b/PLAN_authblocks_trackmanager.md @@ -0,0 +1,536 @@ +# PLAN — AuthBlocks startup separation + TrackManager / ITrackService cleanup + +Working plan for two connected workstreams. Standalone execution doc; not part of the themed +`PLAN.md` roadmap. When this work lands, the relevant bodies move to `COMPLETED.md` per the +`CONTEXT.md §6` convention, and the `ITrackService` cross-cutting note in `PLAN.md` (line ~192) +should be retired since this plan resolves it. + +**Status:** awaiting Daniel's approval. Engineering is not dispatched until then. + +--- + +## Goal + +Make **DeepDrftAPI** the AuthBlocks API host (own registration, migration/seed, and endpoint +mounting; Manager keeps only web-side auth), and enforce a clean layer boundary on the SQL side: +**`TrackRepository` outputs entities; `ITrackService` / `TrackManager` outputs DTOs** (via +`TrackConverter`). The current `ITrackService` returns `TrackEntity` everywhere — that is the defect. +The interface changes to return `TrackDto` for all query and mutation results, making `TrackConverter` +the single authoritative entity→DTO conversion path and putting it inside the service layer where it +belongs, rather than at scattered call sites. + +--- + +## Scope note — a third consumer the brief did not name + +The brief lists CLI and `UnifiedTrackService` as the non-HTTP `ITrackService` consumers. There is a +**third**: `DeepDrftPublic` consumes `ITrackService` in two places — + +- `DeepDrftPublic/Services/TrackDirectDataService.cs` (server-side SSR prerender; calls `.GetPaged`). +- `DeepDrftPublic/Controllers/TrackController.cs` (the public `api/track/page` endpoint; calls `.GetPaged`). +- Registered in `DeepDrftPublic/Startup.cs` as `AddScoped(sp => sp.GetRequiredService())`. + +Whatever happens to `ITrackService` must keep DeepDrftPublic compiling. This is load-bearing for the +wave plan below: under Daniel's clarified rule (repository → entity, service → DTO), an `ITrackService` +signature change is **no longer optional** — the interface itself flips to DTO return types, so every +consumer is in the blast radius, including all four projects (DeepDrftAPI, DeepDrftManager, DeepDrftCli, +DeepDrftPublic). The public host returns `TrackEntity` to its own WASM client today; whether that wire +format also moves to DTO is now an explicit **decision point** (see Workstream 2, Q2.3) rather than a +settled scope-out — the layer rule applies system-wide, so the default is that Public aligns too. + +--- + +## Workstream 1 — AuthBlocks startup separation + +### Current state + +`DeepDrftManager/Program.cs` does all four AuthBlocks responsibilities: + +1. `builder.Services.AddAuthBlocks(options => { ... })` (lines 35–63) — EF Identity, JWT services, + email sender, admin-seed config. Reads `ConnectionStrings:Auth` + all `AuthBlocks:*` keys. +2. `AuthBlocksWeb.Startup.ConfigureAuthServices(builder.Services, baseUrl)` (line 68) — web-side + cascading auth state + JWT client for the `/account/login` + `/account/logout` Razor pages. +3. `await app.Services.UseAuthBlocksStartupAsync()` (line 119) — EF migrate + seed roles/admin. +4. `app.MapAuthBlocks()` (line 143) — mounts `/api/auth/*`, `/api/users/*`. + +`DeepDrftManager.csproj` references **both** `AuthBlocksLib` and `AuthBlocksWeb`. The +`authblocks.json` secrets file is loaded into Manager's config (lines 23–24). + +`DeepDrftAPI/Program.cs` has **no** AuthBlocks anything. It already loads +`environment/connections.json` (line 46) for `DefaultConnection` (SQL metadata) and has a CORS +policy `ContentApiPolicy` driven by `CorsSettings.AllowedOrigins` from `appsettings.json` +(lines 22–37), applied via `app.UseCors("ContentApiPolicy")` (line 81). + +### Target state + +- **Manager**: keeps only step 2 (`ConfigureAuthServices`). Drops steps 1, 3, 4. Drops the + `AuthBlocksLib` package reference; keeps `AuthBlocksWeb`. No longer loads `authblocks.json` *for + AddAuthBlocks*, but see secrets migration below — it likely still needs the JWT validation subset. +- **DeepDrftAPI**: gains steps 1, 3, 4. Becomes the AuthBlocks API host — auth endpoints live + alongside track endpoints. Gains the `AuthBlocksLib` package reference, an `Auth` connection + string, and the `authblocks.json` secrets. CORS policy widened to allow the Manager origin so the + Manager's browser client can call `/api/auth/*` cross-origin. + +### Files touched + +| File | Change | +|---|---| +| `DeepDrftManager/Program.cs` | Remove `AddAuthBlocks` block (35–63), `UseAuthBlocksStartupAsync` (119), `MapAuthBlocks` (143). Keep `ConfigureAuthServices` (68). Adjust `authblocks.json` load (23–24) to the JWT-validation subset or remove if unused — see open question Q1.1. | +| `DeepDrftManager/DeepDrftManager.csproj` | Remove `AuthBlocksLib` PackageReference. Keep `AuthBlocksWeb`. | +| `DeepDrftManager/CLAUDE.md` | (doc-keeper, post-land) reflect Manager as web-auth-only. *Not this plan's edit.* | +| `DeepDrftAPI/Program.cs` | Add `AddAuthBlocks` block reading `Auth` conn string + `AuthBlocks:*`. Add `await app.Services.UseAuthBlocksStartupAsync()` after `Build()`. Add `app.MapAuthBlocks()`. Add `app.UseAuthentication()`/`UseAuthorization()` if AuthBlocks endpoints require the auth middleware (verify — the ApiKey middleware is separate; see Q1.3). | +| `DeepDrftAPI/DeepDrftAPI.csproj` | Add `AuthBlocksLib` PackageReference (match the version Manager used). | +| `DeepDrftAPI/appsettings.json` | Add Manager's origin to `CorsSettings.AllowedOrigins`. | +| `DeepDrftAPI/environment/connections.json` | Add the `Auth` connection string (gitignored; deployment + local). | +| `DeepDrftAPI/environment/authblocks.json` | New: the JWT/email/admin secrets file (gitignored). Moved from Manager. | +| `DeepDrftManager/environment/authblocks.json` | Remove, or reduce to the JWT-validation subset — see Q1.1. | +| `*.example.json` templates | Update both hosts' example/templates to reflect the new shape (the Manager `Program.cs` header comment at lines 11–15 documents these; keep it accurate). | + +### Migration path for secrets + +`authblocks.json` today (Manager) carries: `AuthBlocks:Jwt:{Secret,Issuer,Audience}`, +`AuthBlocks:Email:{Host,Token}`, `AuthBlocks:Admin:{UserName,Email,Password}`, `AuthBlocks:SupportEmail`. + +1. **DeepDrftAPI gets the full file.** It runs `AddAuthBlocks` + seed, so it needs all of it + (JWT signing secret, email sender creds, admin seed creds). Place at + `DeepDrftAPI/environment/authblocks.json`, loaded via `CredentialTools.ResolvePathOrThrow` + exactly as Manager does today. +2. **The `Auth` connection string moves to DeepDrftAPI's `connections.json`.** DeepDrftAPI already + loads `connections.json` for `DefaultConnection`; add the `Auth` key alongside it. The Auth + schema stays in its own database, separate from `DefaultConnection`, exactly as today. +3. **Manager's residual need** is the open question — see Q1.1. The default position: Manager's + `ConfigureAuthServices` is a *client* of the JWT (validates tokens read from browser + localStorage, points its JWT client at the auth API base URL). If it needs the issuer/audience + for client-side validation, Manager keeps a **reduced** `authblocks.json` with only the JWT + validation subset (no signing secret, no email, no admin creds). If `ConfigureAuthServices` + takes only a base URL (as the current call `ConfigureAuthServices(services, baseUrl)` suggests), + Manager may need **no** `authblocks.json` at all. Engineering confirms against the AuthBlocksWeb + API surface before deleting. + +### Open questions — resolved + +**Q1.1 — Does Manager still need any `authblocks.json`?** +Resolution: **Default to "no signing secret, possibly a JWT-validation subset."** The current +`ConfigureAuthServices(builder.Services, baseUrl)` signature takes only a base URL, which strongly +implies the web side does not need the signing secret. Engineering's first implementation step is +to read the `AuthBlocksWeb.Startup.ConfigureAuthServices` signature and its downstream JWT client +config: if it consumes `AuthBlocks:Jwt:*` from config, Manager keeps a reduced file with +`Issuer`/`Audience` only; if it does not, Manager drops `authblocks.json` entirely. The signing +secret (`Jwt:Secret`), email, and admin creds **never** stay on Manager. Capture the actual finding +in the commit and update the Manager `Program.cs` header comment accordingly. + +**Q1.2 — Where do auth tokens get validated, given the Blazor circuit?** +Manager's existing comment (lines 145–155) documents that JWTs live in browser localStorage and +never reach the Manager server on navigation — page authz is via `AuthorizeRouteView`, and the +Manager API surface is `AllowAnonymous` at the endpoint layer. **This is unchanged by the split.** +The browser holds the token and presents it to whichever API it calls. After the split, the auth +*issuing* API is DeepDrftAPI, so the browser obtains its token from DeepDrftAPI's `/api/auth/*` and +presents it to DeepDrftAPI's protected endpoints. Manager's job is only to host the login UI and +the cascading auth-state provider that reads the stored token. No server-side token validation moves. + +**Q1.3 — Does DeepDrftAPI need `UseAuthentication`/`UseAuthorization` added?** +Resolution: **Likely yes, and it must compose with the existing ApiKey middleware.** DeepDrftAPI +today authenticates track endpoints with a custom `ApiKeyAuthenticationMiddleware` +(`app.UseApiKeyAuthentication`), not ASP.NET auth middleware. `MapAuthBlocks` mounts endpoints that +expect JWT bearer auth, which requires `app.UseAuthentication()` + `app.UseAuthorization()` in the +pipeline. Engineering adds these and verifies ordering: the ApiKey middleware must continue to gate +only `[ApiKeyAuthorize]`-tagged track endpoints, and the JWT bearer scheme must gate the AuthBlocks +endpoints — the two auth mechanisms coexist on one host. **This is the highest-risk integration +point in Workstream 1**; flag for careful review. Confirm `AddAuthBlocks` registers the JWT bearer +scheme (it does in Manager today, which is why Manager calls `UseAuthentication`). + +**Q1.4 — CORS.** DeepDrftAPI's `ContentApiPolicy` already does `AllowAnyMethod/Header/Credentials` +with specific origins. Add `manage.deepdrft.com` (prod) and the Manager's dev origin to +`CorsSettings.AllowedOrigins`. Because the policy already `AllowCredentials()`, the cross-origin +`/api/auth/*` flow (which may set cookies or carry the Authorization header) works without a new +policy. No second CORS policy needed. + +--- + +## Workstream 2 — TrackManager / ITrackService cleanup + +### Current state + +**The defect:** `ITrackService` returns `TrackEntity` everywhere (see `DeepDrftData/ITrackService.cs`), +so the service layer leaks entities to its callers and `TrackConverter` is never reached on the read +path. The layer boundary Daniel wants — repository → entity, service → DTO — is inverted in the +current code: the converter exists but the entity-typed `ITrackService` surface bypasses it. + +`TrackManager : Manager, ITrackService`. +The BlazorBlocks `Manager<>` base (NuGet `Cerebellum.BlazorBlocks.Data` 10.3.30, namespace +`Data.Managers`) exposes a **DTO-shaped** surface using `TrackConverter` (e.g. `GetById → TrackDto`). +`TrackManager` *also* implements the **entity-shaped** `ITrackService`, bypassing the converter — +these entity-typed methods are exactly what the new DTO-typed interface eliminates: + +- `ITrackService.GetById` — explicit impl (signature collides with base `GetById → TrackDto`). +- `GetAll`, `GetPaged`, `Create`, `Update` — direct public methods hitting `Repository` and + returning `TrackEntity`. These **shadow/satisfy** the entity interface and never touch the converter. +- `Delete(long) → Result` — inherited from base; satisfies `ITrackService.Delete` by signature. + +**DeepDrftAPI consumers of `ITrackService` (entity-typed, converter-bypassing):** +- `TrackController` injects `ITrackService _sqlTrackService`; calls `.GetPaged` (GetPage endpoint), + `.GetById` (GetMeta, and inside UpdateMeta), `.Update` (UpdateMeta). Returns raw `TrackEntity` / + `PagedResult` over the wire. +- `UnifiedTrackService` injects `ITrackService`; calls `.Create(TrackEntity)` (upload), + `.GetById(long)` + `.Delete(long)` (delete orchestration). + +**Other consumers (out of DeepDrftAPI, in blast radius):** +- `DeepDrftPublic` — `TrackDirectDataService.GetPaged`, `TrackController.GetPaged`. Returns + `TrackEntity` to its own client. +- `DeepDrftCli` — `CliService` (`Create`, `GetAll`), `GuiService` (`Create`, `Update`, `Delete`, `GetAll`). + +**`CmsTrackService` (Manager, deserialization side)** currently deserializes DeepDrftAPI responses +as `TrackEntity` / `PagedResult`: `ReadFromJsonAsync` (upload at line 96, +get-by-id at 229), `ReadFromJsonAsync>` (page at 180). Its +`ICmsTrackService` public contract returns `TrackEntity` / `PagedResult` to the Blazor +components. + +### Target state + +Daniel's clarified rule (verbatim): *"For workstream 2, ITrackService is where we need to output +DTOs. The repository outputs entities, the Service outputs DTOs."* This is a layer boundary, not a +controller tweak: + +- **`TrackRepository`** → outputs `TrackEntity` (unchanged; it is the data-access layer). +- **`ITrackService` / `TrackManager`** → outputs `TrackDto` for **all** query and mutation results, + invoking `TrackConverter` internally. This is the change. + +The new `ITrackService` shape: + +```csharp +public interface ITrackService +{ + Task> GetById(long id); + Task>> GetAll(); + Task>> GetPaged(int page, int pageSize, string? sortColumn, bool sortDescending, CancellationToken ct = default); + Task> Create(TrackDto newTrack); // was Create(TrackEntity) + Task> Update(TrackDto track); // was Update(TrackEntity) + Task Delete(long id); // unchanged +} +``` + +Consequences that fall out of this single rule: + +- **The controller keeps injecting `ITrackService`** and gets DTOs automatically. No change to *which* + type it injects — only the endpoint response types change from `TrackEntity` to `TrackDto`. This is + *simpler* than the prior plan (which had the controller switch to `IManager`). +- **`UnifiedTrackService`** converts its unpersisted entity to a DTO before `Create`, and its + `UploadAsync` now returns `ResultContainer`. +- **CLI** converts entity → DTO at its `Create`/`Update` call sites (it already builds entities), and + its list/display collections bind `TrackDto`. +- **DeepDrftPublic** — its `TrackDirectDataService` and own controller now receive `PagedResult` + from `ITrackService`, so its wire format to the WASM client changes accordingly. This was previously + scoped out; it is now a **decision point** (Q2.3) — the rule applies system-wide, so the default is + that Public aligns too. +- **`CmsTrackService`** deserializes `TrackDto` / `PagedResult` (unchanged from the prior plan). + +### Resolution of each design question + +**Q2.1 — ITrackService fate: change the interface to DTO return types.** +The direction is settled by Daniel's rule. It is **neither "keep as-is" nor "split into two +interfaces"** — the three-direction exploration in the prior draft is retired. `ITrackService` stays +as a single interface, owned by `DeepDrftData`, but its return types flip from `TrackEntity` to +`TrackDto` (and its `Create`/`Update` *inputs* flip to `TrackDto` as well). This is the layer boundary +Daniel stated: the repository is the entity surface; the service is the DTO surface; `TrackConverter` +lives inside `TrackManager` and is the only place the conversion happens. Every consumer that +previously received entities now receives DTOs and adjusts at its own boundary (controller: serialize +the DTO; orchestration/CLI: convert their locally-built entities to DTOs before calling, and bind DTOs +on the way back). + +**Q2.2 — TrackManager implementation.** +`TrackManager : Manager` — the BlazorBlocks +base very likely already provides DTO-returning `Create`/`Update`/`GetById`/`GetPaged` that run +`TrackConverter`. If so, the **explicit entity-typed implementations currently in `TrackManager.cs` +are removed** in favor of the base-class DTO surface satisfying the new `ITrackService`: + +- `ITrackService.GetById` (the explicit interface impl, lines 37–48) — removed; base `GetById → TrackDto` + now satisfies the interface directly (no signature collision once the interface is DTO-typed). +- entity-typed `GetAll` (50–61), `GetPaged` (63–95), `Create` (97–108), `Update` (118–132) — removed + in favor of the base DTO methods. +- `Delete(long) → Result` — already inherited from the base; still satisfies the interface unchanged. + +**The one piece that must survive is the sort-column→expression mapping** (the switch at +`TrackManager.GetPaged` lines 77–86: `TrackName`/`Artist`/`Album`/`Genre`/`ReleaseDate` → `OrderBy`, +nulls padded to end, default `Id`). This is product behavior, not boilerplate, and the base class may +not carry it. Engineering's first step (B1) confirms against the `Cerebellum.BlazorBlocks.Data` +10.3.30 surface — which is **not readable from this repo** — whether the base paged method accepts a +sort-column string and maps it, or only a pre-built `PagingParameters<>`: + +- If the base supports a sort-column string with a configurable mapping: configure the five columns there. +- Otherwise: keep a DTO-returning `GetPaged` **override** on `TrackManager` that reuses the existing + switch to build `PagingParameters`, calls `Repository.GetPagedAsync`, then converts the + page to `PagedResult` (per-item `TrackConverter.Convert`, or `PagedResult.From<>` — see the + `DeepDrftModels` `PagedResult.From` factory). Either way the switch stays in `TrackManager`. + +This is the single most likely place Workstream 2 retains a hand-written method on `TrackManager`. + +**Q2.3 — Controller response types, and the DeepDrftPublic decision point.** +The controller **keeps injecting `ITrackService`** (no DI change in DeepDrftAPI's `Program.cs` for the +track service). Because the interface is now DTO-typed: + +- `GET api/track/page` returns `PagedResult`. +- `GET api/track/meta/{id}` returns `TrackDto`. +- `PUT api/track/meta/{id}` — the lookup now returns a `TrackDto`; the controller mutates the DTO's + fields and passes the DTO to `Update(TrackDto)`. `EntryKey` stays immutable (not in the request body). +- `POST api/track/upload` — `ActionResult` becomes `ActionResult` (see Q2.5). + +On the **Manager side**, `CmsTrackService` deserializes `TrackDto` / `PagedResult`, +`ICmsTrackService` returns DTOs, and the Blazor components binding those results move from +`TrackEntity` to `TrackDto`. Because `TrackConverter` mirrors the entity field-for-field, **the JSON +payload is identical** — this is a type-level rename, not a payload change, so there is no +serialization risk. Engineering enumerates the affected components by grepping the `ICmsTrackService` +return types. + +**DeepDrftPublic — decision point.** Under the prior plan Public was explicitly *out* of scope. Under +Daniel's system-wide rule it is now *in* by default: `TrackDirectDataService.GetPage` and Public's own +`api/track/page` controller call `ITrackService.GetPaged`, which now returns `PagedResult`, +so Public's `ITrackDataService` contract and its WASM client deserialization move from `TrackEntity` to +`TrackDto`. The payload is again identical (converter mirrors the entity). **Flagged for Daniel:** + +> The brief's clarification names "the Service" generically, which means DeepDrftPublic's consumption +> of `ITrackService` is also affected — its public wire format would move to `TrackDto`. Recommended: +> align Public in the same pass (the change is mechanical and the payload is unchanged), so the layer +> rule holds uniformly and there is no lingering entity-on-the-wire path. If Daniel wants Public's wire +> deferred, the *only* way to keep it on entities is to convert `TrackDto` → `TrackEntity` at Public's +> own boundary (`TrackDirectDataService` and controller) — extra work to preserve the old shape, and a +> conversion in the wrong direction. **Default: align Public.** + +**Q2.4 — UnifiedTrackService.** +`UnifiedTrackService.UploadAsync` builds an unpersisted `TrackEntity` (from +`TrackContentService.AddTrackFromWavAsync`), sets `CreatedByUserId`, then calls `ITrackService.Create`. +Because `Create` now takes a `TrackDto`, it converts first: + +```csharp +unpersisted.CreatedByUserId = createdByUserId; +var dto = TrackConverter.Convert(unpersisted); // entity → DTO +var saveResult = await _sqlTrackService.Create(dto); // returns ResultContainer +// saveResult.Value is a TrackDto with Id populated +``` + +`UploadAsync`'s return type changes from `ResultContainer` to `ResultContainer` +— the controller serializes it as-is, so no second conversion is needed. The orphan-logging path is +unchanged (it reads `EntryKey`, present on the DTO). `DeleteAsync` already only needs `GetById` (now +returns `TrackDto?`, still carries `EntryKey`) and `Delete(long)` (unchanged) — its logic is +untouched beyond the `EntryKey` source being a DTO. + +**Q2.5 — UploadTrack response type.** +With `UnifiedTrackService.UploadAsync` returning `ResultContainer`, the controller's +`UploadTrack` action returns the DTO directly: `ActionResult` → `ActionResult`, +and `return Ok(result.Value)` already serializes the DTO. `CmsTrackService.UploadTrackAsync` +deserializes `TrackDto`; `ICmsTrackService.UploadTrackAsync` returns `ResultContainer`. The +upload caller in the Manager component uses `Id`, `EntryKey`, etc. — all present on the DTO. + +**Q2.6 — CLI change.** +`CliService` and `GuiService` (in `DeepDrftCli`) consume `ITrackService` directly: `.Create(entity)`, +`.Update(entity)`, `.GetAll()`, `.Delete(id)`. After the change: + +- **Create** (`CliService.HandleAddCommand` ~line 168, `GuiService.ValidateAndAddTrackAsync` ~line 654): + both build a `TrackEntity` via `TrackContentService.AddTrackFromWavAsync`, then call `Create`. They + must convert to DTO first — `TrackConverter.Convert(trackEntity)` — and read `result.Value` as a + `TrackDto` (the displayed `Id`, `TrackName`, `Artist`, `Album`, `Genre`, `ReleaseDate`, `EntryKey` + are all on the DTO). +- **Update** (`GuiService.ValidateAndUpdateTrackAsync` ~line 712): builds an updated `TrackEntity`, + calls `Update`. Convert to DTO before the call. Note `GuiService` constructs the entity by hand here; + it could equally construct a `TrackDto` directly — engineering picks whichever is cleaner given the + surrounding code, but `TrackConverter.Convert` keeps the call sites uniform. +- **GetAll / display** (`CliService.HandleListCommand`, `GuiService.RefreshTrackListAsync`, + `_tracks` field, `DeleteTrackAsync`/`ShowEditTrackDialog`/`ShowTrackDetails` selected-track handling): + `GetAll()` now returns `List`, so `GuiService._tracks` becomes `List` and every + display/selection site binds `TrackDto`. Field access is identical (same property names), so this is + a type-declaration change, not a logic change. `TrackConverter` and `TrackDto` are already in + `DeepDrftData` / `DeepDrftModels`, both referenced by the CLI — no new dependency. + +The CLI is a deliberate local entity-space admin tool, but the entity now lives only between +`AddTrackFromWavAsync` and the `TrackConverter.Convert` call; everything it reads back from the service +is a DTO. This is consistent with the layer rule. + +**Q2.7 — PutTrack endpoint.** No change. It writes `AudioBinaryDto` straight to `FileDatabase` and +never touches `ITrackService`/`TrackManager`. Confirmed by reading the controller (lines 352–373). + +### Files touched (Workstream 2) + +| File | Change | +|---|---| +| `DeepDrftData/ITrackService.cs` | **Signature change** — all return types `TrackEntity`→`TrackDto`; `Create`/`Update` inputs `TrackEntity`→`TrackDto`; `Delete(long)` unchanged. Update doc comment to state the layer rule (repository → entity, service → DTO). | +| `DeepDrftData/TrackManager.cs` | Remove the entity-typed `ITrackService` implementations (explicit `GetById`, and `GetAll`/`GetPaged`/`Create`/`Update`) in favor of the base-class DTO surface satisfying the DTO-typed interface. **Preserve the sort-column→expression switch** (lines 77–86): keep a DTO-returning `GetPaged` override that reuses it iff the base can't carry the mapping (B1 decides). Update the class doc comment — the dual-surface note is obsolete once there is one DTO surface. | +| `DeepDrftAPI/Program.cs` | **No change to the track-service registration** — `AddScoped(sp => sp.GetRequiredService())` stays; the controller keeps injecting `ITrackService` and now gets DTOs. | +| `DeepDrftAPI/Controllers/TrackController.cs` | Keep injecting `ITrackService`. GetPage returns `PagedResult`; GetMeta returns `TrackDto`; UpdateMeta mutates the looked-up `TrackDto` and calls `Update(TrackDto)`; UploadTrack returns the DTO from `UnifiedTrackService` — `ActionResult` → `ActionResult`. | +| `DeepDrftAPI/Services/UnifiedTrackService.cs` | `UploadAsync` converts the unpersisted entity via `TrackConverter.Convert` before `Create(dto)`; return type `ResultContainer` → `ResultContainer`. `DeleteAsync` reads `EntryKey` off the `TrackDto?` lookup (logic otherwise unchanged). | +| `DeepDrftManager/Services/CmsTrackService.cs` | Deserialize `TrackDto` / `PagedResult` instead of entity. | +| `DeepDrftManager/Services/ICmsTrackService.cs` | Return types `TrackEntity`→`TrackDto`, `PagedResult`→`PagedResult`. | +| `DeepDrftManager/Components/Pages/**` (Tracks/Cms) | Components binding `ICmsTrackService` results move from `TrackEntity` to `TrackDto`. Enumerate via grep before editing. | +| `DeepDrftCli/Services/CliService.cs` | `Create` call site converts entity → `TrackDto` (`TrackConverter.Convert`); `GetAll`/list display binds `TrackDto`. Field access unchanged. | +| `DeepDrftCli/Services/GuiService.cs` | `Create`/`Update` call sites convert to `TrackDto`; `_tracks` field and all display/selection sites bind `TrackDto`. Field access unchanged. | +| `DeepDrftPublic/Services/TrackDirectDataService.cs`, `DeepDrftPublic/Controllers/TrackController.cs`, Public's `ITrackDataService` + WASM client | **Decision point (Q2.3).** Default: align to `PagedResult` (mechanical; payload identical). If Daniel defers Public, these instead convert `TrackDto` → `TrackEntity` at Public's boundary to preserve the old wire shape. | +| `DeepDrftData/CLAUDE.md`, `DeepDrftAPI/CLAUDE.md`, `DeepDrftCli/CLAUDE.md`, `DeepDrftPublic/CLAUDE.md` | (doc-keeper, post-land) folder docs that describe `ITrackService` / endpoints returning `TrackEntity` become DTO. *Not this plan's edit.* | + +--- + +## Wave plan + +Two workstreams are **independent** and can run in parallel (different files, no shared edits except +both touch `DeepDrftAPI/Program.cs` — sequence those two edits or merge carefully). Within each +workstream, tracks are ordered. + +### Wave A — AuthBlocks separation (Workstream 1) + +- **A1 (sequential, first):** Read `AuthBlocksWeb.Startup.ConfigureAuthServices` + `AddAuthBlocks` + signatures from the `AuthBlocksLib`/`AuthBlocksWeb` packages. Resolve Q1.1 (Manager's residual + `authblocks.json`) and Q1.3 (auth-middleware ordering) against the real API. Output: a one-paragraph + confirmation appended to this section by engineering before code. +- **A2 (after A1):** DeepDrftAPI gains AuthBlocks: csproj reference, `AddAuthBlocks`, secrets files + (`authblocks.json` + `Auth` conn string), `UseAuthBlocksStartupAsync`, `MapAuthBlocks`, + `UseAuthentication`/`UseAuthorization`, CORS origin. Verify ApiKey + JWT middleware coexist. +- **A3 (after A2, can overlap A2 tail):** Manager loses AuthBlocks: remove `AddAuthBlocks`/seed/map, + drop `AuthBlocksLib` csproj ref, reduce/remove `authblocks.json` per A1 finding. Keep + `ConfigureAuthServices`. +- **A4 (after A2 + A3):** End-to-end auth smoke: login from Manager UI obtains a token from + DeepDrftAPI's `/api/auth/*`, protected DeepDrftAPI endpoints accept it, admin seed ran on + DeepDrftAPI first boot. + +### Wave B — TrackManager / ITrackService (Workstream 2) + +- **B1 (sequential, first):** Read the `Cerebellum.BlazorBlocks.Data` 10.3.30 `Manager`/`IManager` + surface. Resolve Q2.2 — does the base provide DTO `Create`/`Update`/`GetById`/`GetPaged`, and does + its paged method carry the sort-column→expression mapping or must `TrackManager` keep a `GetPaged` + override for it? Output: confirmation appended here before code. +- **B2 (after B1):** DeepDrftData — flip `ITrackService` to DTO return/input types; remove the + entity-typed `TrackManager` implementations in favor of the base DTO surface; keep the sort switch + (as a `GetPaged` override if B1 requires it). This is the load-bearing edit — every B-track below + depends on it compiling. +- **B3 (after B2):** DeepDrftAPI — `UnifiedTrackService.UploadAsync` converts to DTO before `Create` + and returns `ResultContainer`; controller endpoints return DTOs; UploadTrack returns + `ActionResult`. **No `Program.cs` DI change** for the track service. +- **B4 (after B2):** DeepDrftCli — convert at `Create`/`Update` call sites; `GetAll`/display bind + `TrackDto`. Independent of B3 (different project), gated only on B2's interface change. +- **B5 (after B3):** Manager — `CmsTrackService` + `ICmsTrackService` + consuming components move to + `TrackDto`. The largest single track by file count. +- **B6 (after B2, decision-gated):** DeepDrftPublic — align `TrackDirectDataService`, controller, + `ITrackDataService`, and the WASM client to `TrackDto` (Q2.3 default), **or** add the + `TrackDto`→`TrackEntity` conversion at Public's boundary if Daniel defers the public wire. Gated on + Daniel's Q2.3 decision before it starts. +- **B7 (after B3 + B5):** CMS smoke: track browser lists, edit saves, upload returns the new row, + delete works — all through the DTO wire. + +### Parallelism + +- **Wave A and Wave B run in parallel** by different engineers. Wave A touches + `DeepDrftAPI/Program.cs` (auth wiring); Wave B no longer touches `Program.cs` for the track service + (the DI registration is unchanged), so the two waves now have **no shared file** — conflict risk is + effectively zero. (Wave B does touch `DeepDrftAPI` in the controller and `UnifiedTrackService`, but + not `Program.cs`.) +- A1 and B1 (the package-surface reads) can both happen up front, in parallel, before any code. +- **Within Wave B, B2 is the gate:** the interface flip must land first; B3/B4/B5/B6 fan out from it + across DeepDrftAPI, DeepDrftCli, DeepDrftManager, and (decision-gated) DeepDrftPublic — they can run + in parallel once B2 compiles, since they are different projects. This is the real cost of the + layer-rule change: it is **not** a single-controller edit but a fan-out to every `ITrackService` + consumer. DeepDrftPublic and CLI are **in scope** now (they were not in the prior draft). + +--- + +## Acceptance criteria + +### Workstream 1 + +- DeepDrftManager builds without referencing `AuthBlocksLib` (only `AuthBlocksWeb`). +- DeepDrftManager's `Program.cs` contains no `AddAuthBlocks`, `UseAuthBlocksStartupAsync`, or + `MapAuthBlocks` call; it retains `ConfigureAuthServices`. +- DeepDrftAPI references `AuthBlocksLib`, runs `AddAuthBlocks` + `UseAuthBlocksStartupAsync` on + startup, and mounts `/api/auth/*` + `/api/users/*` via `MapAuthBlocks`. +- On first boot against an empty Auth database, DeepDrftAPI applies AuthBlocks EF migrations and + seeds roles + the admin user. +- The signing secret, email creds, and admin creds exist only in DeepDrftAPI's `environment/`, + never in Manager's. +- DeepDrftAPI's CORS policy includes the Manager origin; a browser served by Manager can call + DeepDrftAPI `/api/auth/*` without a CORS rejection. +- ApiKey-protected track endpoints on DeepDrftAPI still enforce the ApiKey; AuthBlocks endpoints + enforce JWT — neither auth path interferes with the other. + +### Workstream 2 + +- `ITrackService` returns `TrackDto` for all queries and `Create`/`Update`; `Create`/`Update` take + `TrackDto`; `Delete(long)` is unchanged. `TrackRepository` still returns entities. +- `TrackManager` no longer has entity-typed `ITrackService` methods; the DTO surface (base class + + any retained `GetPaged` override) satisfies the interface, and `TrackConverter` runs inside it. +- `GET api/track/page`, `GET api/track/meta/{id}`, `PUT api/track/meta/{id}`, and + `POST api/track/upload` produce DTOs (no raw `TrackEntity` crosses the wire from DeepDrftAPI). +- `CmsTrackService` deserializes DTOs; `ICmsTrackService` and its consuming components are DTO-typed; + the CMS track browser, edit, upload, and delete flows work unchanged from the user's view. +- `UnifiedTrackService.UploadAsync` returns `ResultContainer` and converts the unpersisted + entity to a DTO via `TrackConverter` before `Create`. +- DeepDrftCli compiles against the DTO interface: add/edit convert at the call site, list/display bind + `TrackDto`, behavior is unchanged from the user's view. +- DeepDrftPublic: per the Q2.3 decision — either its wire format is `PagedResult` (default) + or it converts at its own boundary to keep the existing shape. Either way the public gallery loads + identically. +- `TrackConverter` is the single entity↔DTO conversion path; it lives inside `TrackManager` / + `UnifiedTrackService`, not at scattered controller or page call sites. + +--- + +## Test cases + +### Workstream 1 + +1. **Cold-start seed.** Point DeepDrftAPI at an empty Auth DB, boot. Verify migrations applied, + system roles present, admin user created with the seeded creds. +2. **Idempotent re-boot.** Boot DeepDrftAPI a second time against the seeded DB. No duplicate admin, + no migration error. +3. **Login round-trip.** From Manager's login page, authenticate against DeepDrftAPI `/api/auth/*`; + confirm a JWT is issued and stored client-side. +4. **Protected DeepDrftAPI endpoint with JWT.** Call an AuthBlocks-protected endpoint with the + issued token → 200; without it → 401. +5. **ApiKey endpoints unaffected.** `GET api/track/page` with a valid ApiKey → 200; with none → 401. + Confirm the JWT middleware does not start rejecting ApiKey-only requests. +6. **CORS preflight.** Browser OPTIONS preflight from the Manager origin to DeepDrftAPI `/api/auth/*` + returns the allow headers; a disallowed origin is rejected. +7. **Manager has no secrets.** Grep Manager's deployed `environment/` — no signing secret / admin + password present. + +### Workstream 2 + +1. **Page endpoint shape.** `GET api/track/page` returns `PagedResult` JSON; `Items` carry + the same field values as before (compare a row against its `TrackEntity` pre-change). +2. **Sort preserved.** Each supported `sortColumn` (TrackName, Artist, Album, Genre, ReleaseDate) + and `sortDescending` produce the same ordering as before the change — proves the sort-column→ + expression mapping survived the move to the DTO path. +3. **Get-meta shape.** `GET api/track/meta/{id}` returns `TrackDto`; 404 for a missing id; 500 path + on a forced query error still returns 500. +4. **Update round-trip.** `PUT api/track/meta/{id}` updates fields, clears optionals on null, + leaves `EntryKey` immutable; subsequent get reflects the change. +5. **Upload returns DTO with Id.** `POST api/track/upload` returns `TrackDto` with `Id > 0` and the + correct `EntryKey`; the CMS upload flow reads back the new row. +6. **CMS browser/edit/delete.** Through the Manager UI: list paginates and sorts, edit saves, + delete removes the row and the vault entry, upload adds a row — all against DTO responses. +7. **Public behaves identically.** DeepDrftPublic's gallery loads and paginates the same as before, + whichever Q2.3 branch was taken (DTO wire by default, or boundary conversion if deferred). Payload + values match a pre-change capture. +8. **CLI behavior unchanged.** `DeepDrftCli list` / `add` / GUI add+edit+delete operate the same from + the user's view; the displayed fields (Id, Name, Artist, Album, Genre, ReleaseDate, EntryKey) are + correct, now sourced from `TrackDto`. +9. **Converter is the only path.** Confirm (by inspection/coverage) no DeepDrftAPI metadata response + serializes a `TrackEntity` directly, and the conversion happens inside `TrackManager` / + `UnifiedTrackService`, not in a controller or Razor component. + +--- + +## Risks / trade-offs + +- **Highest risk (W1):** ApiKey + JWT middleware coexistence on one host (Q1.3). If misordered, one + auth scheme can short-circuit the other. Needs deliberate review and test cases 4–5. +- **Highest cost (W2):** the consumer fan-out from B2, not the interface edit itself. Flipping + `ITrackService` to DTO is a small file; the cost is that **every** consumer changes — DeepDrftAPI + controller + `UnifiedTrackService`, DeepDrftCli (two services), DeepDrftManager (`CmsTrackService` + + components), and DeepDrftPublic (decision-gated). Payload is identical field-for-field, so there + is no runtime serialization risk, but the breadth is the real budget. This is a larger blast radius + than the prior draft, which kept CLI and Public out — Daniel's system-wide rule pulls them in. +- **The sort switch is the one piece of real logic at risk (B1/B2).** Everything else is a type + rename; the sort-column→expression mapping is product behavior that must not be lost in the move to + the DTO path. Test case W2-2 exists specifically to guard it. +- **Unknown until B1/A1:** the BlazorBlocks `Manager`/`IManager` surface and the AuthBlocks package + signatures are not readable from this repo. Both waves open with a package-surface read. For W2 the + open question is narrow: does the base DTO `GetPaged` carry a configurable sort mapping, or must + `TrackManager` keep a `GetPaged` override? The plan holds either way. +- **Open decision (W2):** whether DeepDrftPublic's public wire format moves to `TrackDto` (default, + recommended) or stays `TrackEntity` via a boundary conversion (Q2.3). B6 is gated on Daniel's call; + the rest of Wave B does not depend on it.