38 KiB
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 publicapi/track/pageendpoint; calls.GetPaged).- Registered in
DeepDrftPublic/Startup.csasAddScoped<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:
builder.Services.AddAuthBlocks(options => { ... })(lines 35–63) — EF Identity, JWT services, email sender, admin-seed config. ReadsConnectionStrings:Auth+ allAuthBlocks:*keys.AuthBlocksWeb.Startup.ConfigureAuthServices(builder.Services, baseUrl)(line 68) — web-side cascading auth state + JWT client for the/account/login+/account/logoutRazor pages.await app.Services.UseAuthBlocksStartupAsync()(line 119) — EF migrate + seed roles/admin.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 theAuthBlocksLibpackage reference; keepsAuthBlocksWeb. No longer loadsauthblocks.jsonfor 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
AuthBlocksLibpackage reference, anAuthconnection string, and theauthblocks.jsonsecrets. 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.
- 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 atDeepDrftAPI/environment/authblocks.json, loaded viaCredentialTools.ResolvePathOrThrowexactly as Manager does today. - The
Authconnection string moves to DeepDrftAPI'sconnections.json. DeepDrftAPI already loadsconnections.jsonforDefaultConnection; add theAuthkey alongside it. The Auth schema stays in its own database, separate fromDefaultConnection, exactly as today. - Manager's residual need is the open question — see Q1.1. The default position: Manager's
ConfigureAuthServicesis 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 reducedauthblocks.jsonwith only the JWT validation subset (no signing secret, no email, no admin creds). IfConfigureAuthServicestakes only a base URL (as the current callConfigureAuthServices(services, baseUrl)suggests), Manager may need noauthblocks.jsonat 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<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 baseGetById → TrackDto).GetAll,GetPaged,Create,Update— direct public methods hittingRepositoryand returningTrackEntity. These shadow/satisfy the entity interface and never touch the converter.Delete(long) → Result— inherited from base; satisfiesITrackService.Deleteby signature.
DeepDrftAPI consumers of ITrackService (entity-typed, converter-bypassing):
TrackControllerinjectsITrackService _sqlTrackService; calls.GetPaged(GetPage endpoint),.GetById(GetMeta, and inside UpdateMeta),.Update(UpdateMeta). Returns rawTrackEntity/PagedResult<TrackEntity>over the wire.UnifiedTrackServiceinjectsITrackService; calls.Create(TrackEntity)(upload),.GetById(long)+.Delete(long)(delete orchestration).
Other consumers (out of DeepDrftAPI, in blast radius):
DeepDrftPublic—TrackDirectDataService.GetPaged,TrackController.GetPaged. ReturnsTrackEntityto its own client.DeepDrftCli—CliService(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→ outputsTrackEntity(unchanged; it is the data-access layer).ITrackService/TrackManager→ outputsTrackDtofor all query and mutation results, invokingTrackConverterinternally. 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
ITrackServiceand gets DTOs automatically. No change to which type it injects — only the endpoint response types change fromTrackEntitytoTrackDto. This is simpler than the prior plan (which had the controller switch toIManager<TrackEntity,TrackDto>). UnifiedTrackServiceconverts its unpersisted entity to a DTO beforeCreate, and itsUploadAsyncnow returnsResultContainer<TrackDto>.- CLI converts entity → DTO at its
Create/Updatecall sites (it already builds entities), and its list/display collections bindTrackDto. - DeepDrftPublic — its
TrackDirectDataServiceand own controller now receivePagedResult<TrackDto>fromITrackService, 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. CmsTrackServicedeserializesTrackDto/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 37–48) — removed; baseGetById → TrackDtonow 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
GetPagedoverride onTrackManagerthat reuses the existing switch to buildPagingParameters<TrackEntity>, callsRepository.GetPagedAsync, then converts the page toPagedResult<TrackDto>(per-itemTrackConverter.Convert, orPagedResult.From<>— see theDeepDrftModelsPagedResult.From<TOther>factory). Either way the switch stays inTrackManager.
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/pagereturnsPagedResult<TrackDto>.GET api/track/meta/{id}returnsTrackDto.PUT api/track/meta/{id}— the lookup now returns aTrackDto; the controller mutates the DTO's fields and passes the DTO toUpdate(TrackDto).EntryKeystays immutable (not in the request body).POST api/track/upload—ActionResult<TrackEntity>becomesActionResult<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
ITrackServiceis also affected — its public wire format would move toTrackDto. 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 convertTrackDto→TrackEntityat Public's own boundary (TrackDirectDataServiceand 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 aTrackEntityviaTrackContentService.AddTrackFromWavAsync, then callCreate. They must convert to DTO first —TrackConverter.Convert(trackEntity)— and readresult.Valueas aTrackDto(the displayedId,TrackName,Artist,Album,Genre,ReleaseDate,EntryKeyare all on the DTO). - Update (
GuiService.ValidateAndUpdateTrackAsync~line 712): builds an updatedTrackEntity, callsUpdate. Convert to DTO before the call. NoteGuiServiceconstructs the entity by hand here; it could equally construct aTrackDtodirectly — engineering picks whichever is cleaner given the surrounding code, butTrackConverter.Convertkeeps the call sites uniform. - GetAll / display (
CliService.HandleListCommand,GuiService.RefreshTrackListAsync,_tracksfield,DeleteTrackAsync/ShowEditTrackDialog/ShowTrackDetailsselected-track handling):GetAll()now returnsList<TrackDto>, soGuiService._tracksbecomesList<TrackDto>and every display/selection site bindsTrackDto. Field access is identical (same property names), so this is a type-declaration change, not a logic change.TrackConverterandTrackDtoare already inDeepDrftData/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<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 UnifiedTrackService — ActionResult<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 TrackEntity→TrackDto, 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 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+AddAuthBlockssignatures from theAuthBlocksLib/AuthBlocksWebpackages. Resolve Q1.1 (Manager's residualauthblocks.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+Authconn 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, dropAuthBlocksLibcsproj ref, reduce/removeauthblocks.jsonper A1 finding. KeepConfigureAuthServices. - 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.Data10.3.30Manager/IManagersurface. Resolve Q2.2 — does the base provide DTOCreate/Update/GetById/GetPaged, and does its paged method carry the sort-column→expression mapping or mustTrackManagerkeep aGetPagedoverride for it? Output: confirmation appended here before code. - B2 (after B1): DeepDrftData — flip
ITrackServiceto DTO return/input types; remove the entity-typedTrackManagerimplementations in favor of the base DTO surface; keep the sort switch (as aGetPagedoverride if B1 requires it). This is the load-bearing edit — every B-track below depends on it compiling. - B3 (after B2): DeepDrftAPI —
UnifiedTrackService.UploadAsyncconverts to DTO beforeCreateand returnsResultContainer<TrackDto>; controller endpoints return DTOs; UploadTrack returnsActionResult<TrackDto>. NoProgram.csDI change for the track service. - B4 (after B2): DeepDrftCli — convert at
Create/Updatecall sites;GetAll/display bindTrackDto. Independent of B3 (different project), gated only on B2's interface change. - B5 (after B3): Manager —
CmsTrackService+ICmsTrackService+ consuming components move toTrackDto. The largest single track by file count. - B6 (after B2, decision-gated): DeepDrftPublic — align
TrackDirectDataService, controller,ITrackDataService, and the WASM client toTrackDto(Q2.3 default), or add theTrackDto→TrackEntityconversion 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 touchesProgram.csfor 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 touchDeepDrftAPIin the controller andUnifiedTrackService, but notProgram.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
ITrackServiceconsumer. DeepDrftPublic and CLI are in scope now (they were not in the prior draft).
Acceptance criteria
Workstream 1
- DeepDrftManager builds without referencing
AuthBlocksLib(onlyAuthBlocksWeb). - DeepDrftManager's
Program.cscontains noAddAuthBlocks,UseAuthBlocksStartupAsync, orMapAuthBlockscall; it retainsConfigureAuthServices. - DeepDrftAPI references
AuthBlocksLib, runsAddAuthBlocks+UseAuthBlocksStartupAsyncon startup, and mounts/api/auth/*+/api/users/*viaMapAuthBlocks. - 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
ITrackServicereturnsTrackDtofor all queries andCreate/Update;Create/UpdatetakeTrackDto;Delete(long)is unchanged.TrackRepositorystill returns entities.TrackManagerno longer has entity-typedITrackServicemethods; the DTO surface (base class + any retainedGetPagedoverride) satisfies the interface, andTrackConverterruns inside it.GET api/track/page,GET api/track/meta/{id},PUT api/track/meta/{id}, andPOST api/track/uploadproduce DTOs (no rawTrackEntitycrosses the wire from DeepDrftAPI).CmsTrackServicedeserializes DTOs;ICmsTrackServiceand its consuming components are DTO-typed; the CMS track browser, edit, upload, and delete flows work unchanged from the user's view.UnifiedTrackService.UploadAsyncreturnsResultContainer<TrackDto>and converts the unpersisted entity to a DTO viaTrackConverterbeforeCreate.- 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. TrackConverteris the single entity↔DTO conversion path; it lives insideTrackManager/UnifiedTrackService, not at scattered controller or page call sites.
Test cases
Workstream 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.
- Idempotent re-boot. Boot DeepDrftAPI a second time against the seeded DB. No duplicate admin, no migration error.
- Login round-trip. From Manager's login page, authenticate against DeepDrftAPI
/api/auth/*; confirm a JWT is issued and stored client-side. - Protected DeepDrftAPI endpoint with JWT. Call an AuthBlocks-protected endpoint with the issued token → 200; without it → 401.
- ApiKey endpoints unaffected.
GET api/track/pagewith a valid ApiKey → 200; with none → 401. Confirm the JWT middleware does not start rejecting ApiKey-only requests. - CORS preflight. Browser OPTIONS preflight from the Manager origin to DeepDrftAPI
/api/auth/*returns the allow headers; a disallowed origin is rejected. - Manager has no secrets. Grep Manager's deployed
environment/— no signing secret / admin password present.
Workstream 2
- Page endpoint shape.
GET api/track/pagereturnsPagedResult<TrackDto>JSON;Itemscarry the same field values as before (compare a row against itsTrackEntitypre-change). - Sort preserved. Each supported
sortColumn(TrackName, Artist, Album, Genre, ReleaseDate) andsortDescendingproduce the same ordering as before the change — proves the sort-column→ expression mapping survived the move to the DTO path. - Get-meta shape.
GET api/track/meta/{id}returnsTrackDto; 404 for a missing id; 500 path on a forced query error still returns 500. - Update round-trip.
PUT api/track/meta/{id}updates fields, clears optionals on null, leavesEntryKeyimmutable; subsequent get reflects the change. - Upload returns DTO with Id.
POST api/track/uploadreturnsTrackDtowithId > 0and the correctEntryKey; the CMS upload flow reads back the new row. - 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.
- 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.
- 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 fromTrackDto. - Converter is the only path. Confirm (by inspection/coverage) no DeepDrftAPI metadata response
serializes a
TrackEntitydirectly, and the conversion happens insideTrackManager/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
ITrackServiceto 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/IManagersurface 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 DTOGetPagedcarry a configurable sort mapping, or mustTrackManagerkeep aGetPagedoverride? The plan holds either way. - Open decision (W2): whether DeepDrftPublic's public wire format moves to
TrackDto(default, recommended) or staysTrackEntityvia a boundary conversion (Q2.3). B6 is gated on Daniel's call; the rest of Wave B does not depend on it.