Files
deepdrft/PLAN_authblocks_trackmanager.md
T

38 KiB
Raw Blame History

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<ITrackService>(sp => sp.GetRequiredService<TrackManager>()).

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 3563) — 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 2324).

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 2237), 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 (3563), UseAuthBlocksStartupAsync (119), MapAuthBlocks (143). Keep ConfigureAuthServices (68). Adjust authblocks.json load (2324) 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 1115 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 145155) 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<TrackEntity, TrackDto, TrackRepository, TrackConverter>, 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<TrackEntity> over the wire.
  • UnifiedTrackService injects ITrackService; calls .Create(TrackEntity) (upload), .GetById(long) + .Delete(long) (delete orchestration).

Other consumers (out of DeepDrftAPI, in blast radius):

  • DeepDrftPublicTrackDirectDataService.GetPaged, TrackController.GetPaged. Returns TrackEntity to its own client.
  • DeepDrftCliCliService (Create, GetAll), GuiService (Create, Update, Delete, GetAll).

CmsTrackService (Manager, deserialization side) currently deserializes DeepDrftAPI responses as TrackEntity / PagedResult<TrackEntity>: ReadFromJsonAsync<TrackEntity> (upload at line 96, get-by-id at 229), ReadFromJsonAsync<PagedResult<TrackEntity>> (page at 180). Its ICmsTrackService public contract returns TrackEntity / PagedResult<TrackEntity> 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:

public interface ITrackService
{
    Task<ResultContainer<TrackDto?>>             GetById(long id);
    Task<ResultContainer<List<TrackDto>>>        GetAll();
    Task<ResultContainer<PagedResult<TrackDto>>> GetPaged(int page, int pageSize, string? sortColumn, bool sortDescending, CancellationToken ct = default);
    Task<ResultContainer<TrackDto>>              Create(TrackDto newTrack);   // was Create(TrackEntity)
    Task<ResultContainer<TrackDto>>              Update(TrackDto track);      // was Update(TrackEntity)
    Task<Result>                                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<TrackEntity,TrackDto>).
  • UnifiedTrackService converts its unpersisted entity to a DTO before Create, and its UploadAsync now returns ResultContainer<TrackDto>.
  • 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<TrackDto> 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<TrackDto> (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<TrackEntity, TrackDto, TrackRepository, TrackConverter> — 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 3748) — removed; base GetById → TrackDto now satisfies the interface directly (no signature collision once the interface is DTO-typed).
  • entity-typed GetAll (5061), GetPaged (6395), Create (97108), Update (118132) — 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 7786: TrackName/Artist/Album/Genre/ReleaseDateOrderBy, 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<TrackEntity>, calls Repository.GetPagedAsync, then converts the page to PagedResult<TrackDto> (per-item TrackConverter.Convert, or PagedResult.From<> — see the DeepDrftModels PagedResult.From<TOther> 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<TrackDto>.
  • 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/uploadActionResult<TrackEntity> becomes ActionResult<TrackDto> (see Q2.5).

On the Manager side, CmsTrackService deserializes TrackDto / PagedResult<TrackDto>, 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<TrackDto>, 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 TrackDtoTrackEntity 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:

unpersisted.CreatedByUserId = createdByUserId;
var dto = TrackConverter.Convert(unpersisted);          // entity → DTO
var saveResult = await _sqlTrackService.Create(dto);    // returns ResultContainer<TrackDto>
// saveResult.Value is a TrackDto with Id populated

UploadAsync's return type changes from ResultContainer<TrackEntity> to ResultContainer<TrackDto> — 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<TrackDto>, the controller's UploadTrack action returns the DTO directly: ActionResult<TrackEntity>ActionResult<TrackDto>, and return Ok(result.Value) already serializes the DTO. CmsTrackService.UploadTrackAsync deserializes TrackDto; ICmsTrackService.UploadTrackAsync returns ResultContainer<TrackDto>. 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<TrackDto>, so GuiService._tracks becomes List<TrackDto> 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 352373).

Files touched (Workstream 2)

File Change
DeepDrftData/ITrackService.cs Signature change — all return types TrackEntityTrackDto; Create/Update inputs TrackEntityTrackDto; 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 7786): 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 registrationAddScoped<ITrackService>(sp => sp.GetRequiredService<TrackManager>()) stays; the controller keeps injecting ITrackService and now gets DTOs.
DeepDrftAPI/Controllers/TrackController.cs Keep injecting ITrackService. GetPage returns PagedResult<TrackDto>; GetMeta returns TrackDto; UpdateMeta mutates the looked-up TrackDto and calls Update(TrackDto); UploadTrack returns the DTO from UnifiedTrackServiceActionResult<TrackEntity>ActionResult<TrackDto>.
DeepDrftAPI/Services/UnifiedTrackService.cs UploadAsync converts the unpersisted entity via TrackConverter.Convert before Create(dto); return type ResultContainer<TrackEntity>ResultContainer<TrackDto>. DeleteAsync reads EntryKey off the TrackDto? lookup (logic otherwise unchanged).
DeepDrftManager/Services/CmsTrackService.cs Deserialize TrackDto / PagedResult<TrackDto> instead of entity.
DeepDrftManager/Services/ICmsTrackService.cs Return types TrackEntityTrackDto, PagedResult<TrackEntity>PagedResult<TrackDto>.
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<TrackDto> (mechanical; payload identical). If Daniel defers Public, these instead convert TrackDtoTrackEntity 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<TrackDto>; controller endpoints return DTOs; UploadTrack returns ActionResult<TrackDto>. 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 TrackDtoTrackEntity 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<TrackDto> 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<TrackDto> (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<TrackDto> 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 45.
  • 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.