From 1b3e2478c7f7601327bda21639f1b395abecd6ef Mon Sep 17 00:00:00 2001 From: Daniel Harvey Date: Sun, 17 May 2026 21:41:51 -0400 Subject: [PATCH] Collapse CMS-PLAN auth section against AuthBlocks source --- CMS-PLAN.md | 136 ++++++++++++++++++++++++++++++++-------------------- 1 file changed, 85 insertions(+), 51 deletions(-) diff --git a/CMS-PLAN.md b/CMS-PLAN.md index 539e231..19806a7 100644 --- a/CMS-PLAN.md +++ b/CMS-PLAN.md @@ -4,7 +4,7 @@ Forward-looking plan for the in-site Blazor CMS that replaces `DeepDrftCli`. Sit This document **supersedes `PLAN.md §2.4` (Web-side track upload)** and its open question on authentication. The CMS is the home for that capability; once auth and the CMS surface land here, `PLAN.md §2.4` should be archived to `COMPLETED.md` with a forward-pointer to this file. -The plan is intentionally written before all open questions are resolved. Sections that depend on a Daniel decision are marked `[open question]` or `[TBD pending Daniel's input]`. The questions themselves are gathered in the section at the bottom — answer those and the marked sections collapse to commitments. +§3 (Authentication model) commits to `Cerebellum.AuthBlocks` as the substrate, based on a read of the library at `C:\Development\AuthBlocks\`. The remaining open questions (§9) are decisions only Daniel can make — render mode, URL prefix, dual-write transport, CLI retirement timing, soak duration, and the Postgres-vs-unify database call surfaced by the AuthBlocks reading. --- @@ -28,7 +28,7 @@ Replace `DeepDrftCli` with a browser-based admin surface (the **CMS**) living in ## 2. Solution structure (new projects) -### 2.1 New RCL: `DeepDrftCms` `[open question — name and exact split]` +### 2.1 New RCL: `DeepDrftCms` `[open question — exact name]` Daniel said "if possible we will keep the CMS code in an RCL, with mountable pages for the CMS functionality." Recommended shape: @@ -36,7 +36,8 @@ Daniel said "if possible we will keep the CMS code in an RCL, with mountable pag DeepDrftCms Razor Class Library (RCL). CMS pages, components, view models, page-route registration. References: DeepDrftModels, DeepDrftWeb.Services, - DeepDrftContent.Services, Cerebellum.AuthBlocks, MudBlazor. + DeepDrftContent.Services, Cerebellum.AuthBlocks.Web, + Cerebellum.AuthBlocks.Models, MudBlazor. Mounted into DeepDrftWeb via project reference + route discovery. ``` @@ -44,67 +45,105 @@ DeepDrftCms Razor Class Library (RCL). - Pages (`.razor`) under a `Pages/Cms/` folder, all routed under a single base path (recommended `/cms`, see open question). - View models that compose `TrackEntity` editing state. -- A `CmsStartup` (or equivalent extension method) that the host calls to register CMS services, auth policies, and route fallbacks. Mirrors the existing `Startup.ConfigureDomainServices` pattern. +- A `CmsStartup` (or equivalent extension method) that the host calls to register CMS-specific services, view models, and route fallbacks. Mirrors the existing `Startup.ConfigureDomainServices` pattern. **Auth wiring lives in `DeepDrftWeb` (see §2.2)** — the RCL itself does not call `AddAuthBlocks` because that brings in the `AuthDbContext`, EF migrations, and JWT middleware, all of which are host concerns. - CMS-specific components (track-edit form, upload dropzone, confirmation dialogs). Reuse `TrackCard` and other public-side components where they fit. +- A `[HierarchicalRoleAuthorize("Admin")]` attribute (from `AuthBlocksWeb.HierarchicalAuthorize`) on every CMS page component, so `Admin` and any descendant role are admitted by the bundled hierarchical role handler. **What stays in `DeepDrftWeb`:** -- The new upload controller endpoint (`POST api/cms/track`, see §5). Controllers are host-owned per the existing convention. -- Auth middleware wiring, cookie config, login challenge redirects. -- Reference to the `DeepDrftCms` RCL plus the `app.MapRazorComponents().AddAdditionalAssemblies(typeof(DeepDrftCms._Imports).Assembly)` registration. +- The new upload controller endpoint (`POST api/cms/track`, see §5). Controllers are host-owned per the existing convention. Protected by `[Authorize(Roles = "Admin")]` — the JWT bearer middleware AuthBlocks installs validates the access token on each request. +- The `AddAuthBlocks(...)` call in `Program.cs` and the matching `await app.Services.UseAuthBlocksStartupAsync()` post-build hook. This installs JWT bearer middleware, the hierarchical role authorization handler, the `AuthDbContext`, the EF migrations, and seeds system roles plus the configured admin user on first boot. +- The `app.MapAuthBlocks()` call that registers `/api/auth/*`, `/api/users/*`, `/api/roles/*`, `/api/user-roles/*`, and `/api/pending-registrations/*` minimal-API endpoints. The CMS UI uses `/api/auth/login`, `/api/auth/logout`, `/api/auth/refresh`, and `/api/auth/me`; the rest are available if Wave 3 account-management ever lands. +- Reference to the `DeepDrftCms` RCL plus the `app.MapRazorComponents().AddAdditionalAssemblies(typeof(DeepDrftCms._Imports).Assembly)` registration. The `AuthBlocksWeb` login/logout/register pages are picked up the same way once that assembly is added to `AddAdditionalAssemblies`, exposing `/account/login` and `/account/logout` for free. **Why an RCL rather than a folder in `DeepDrftWeb.Client`:** -- Isolation of the auth-gated surface from the public client. The public WASM bundle does not need to ship CMS pages, components, or the AuthBlocks dependency. Smaller download for listeners. -- Reusability. If a future deployment wanted the CMS as a standalone admin host (different port, different process, different auth posture), the RCL is already self-contained. +- Isolation of the auth-gated surface from the public client. The public WASM bundle does not need to ship CMS pages, components, or the AuthBlocks UI dependency. Smaller download for listeners. +- Reusability. If a future deployment wanted the CMS as a standalone admin host (different port, different process), the RCL is already self-contained. - The mountable-page model gives a clean URL prefix without coupling CMS routing to the public site's `Pages.cs` nav source-of-truth. -**Render mode `[open question]`:** The CMS pages probably want `InteractiveServer` rather than `InteractiveAuto`/`InteractiveWebAssembly`. Reasons: (a) auth cookies and `HttpContext` are simpler server-side; (b) the CMS pages call services that already live server-side; (c) file uploads via `InputFile` are natively server-side. WASM CMS pages would have to call the new `POST api/cms/track` over HTTP with the auth cookie attached — workable, but no benefit over server-side rendering. Recommend **Server** for CMS pages; confirm. +**Render mode `[open question]`:** AuthBlocks's bundled UI (`AuthBlocksWeb` pages) is server-rendered MudBlazor with `JwtAuthenticationStateProvider` reading tokens from browser `localStorage` via JS interop. CMS pages can render in any mode that supports `AuthorizeView` / `[HierarchicalRoleAuthorize]`, but `InteractiveServer` is the cleanest fit: (a) it matches what the bundled login UI uses, (b) `InputFile` uploads are natively server-side, (c) CMS endpoints already live in the `DeepDrftWeb` process so no extra HTTP hop. Recommend **InteractiveServer** for CMS pages; confirm. ### 2.2 Solution changes - Add `DeepDrftCms` (RCL) to `DeepDrftHome.sln`. - `DeepDrftWeb` adds a project reference to `DeepDrftCms`. -- `DeepDrftWeb` adds a NuGet (or absolute-path, NetBlocks-style) reference to `Cerebellum.AuthBlocks`. `DeepDrftCms` may also need it directly if it surfaces login UI components. +- `DeepDrftWeb` references three AuthBlocks packages (NuGet, published as `Cerebellum.AuthBlocks*` at version 10.3.16+): + - `Cerebellum.AuthBlocks` — the `AddAuthBlocks`/`UseAuthBlocksStartupAsync`/`MapAuthBlocks` integration surface, JWT services, hierarchical role authorization handler. + - `Cerebellum.AuthBlocks.Web` — the bundled MudBlazor login/logout/register pages, `JwtAuthenticationStateProvider`, `TokenService` (localStorage), and the `HierarchicalRoleAuthorizeAttribute` / `HierarchicalRoleAuthorizeView` used by the RCL. + - `Cerebellum.AuthBlocks.Models` — `ApplicationUser`, `ApplicationRole`, `SystemRole` constants. Transitively pulled by the other two; reference explicitly if `DeepDrftCms` or `DeepDrftWeb.Services` need the entity types. +- `DeepDrftWeb.Client` references `Cerebellum.AuthBlocks.Web` and calls `AuthBlocksWeb.Client.Startup.ConfigureServices(builder.Services)` to register `AddAuthorizationCore()`, `AddCascadingAuthenticationState()`, and `AddAuthenticationStateDeserialization()`. This is the prerender → WASM bridge for auth state, equivalent to what `DarkModeSettings` does today (see `CONTEXT.md §3.6`). +- `DeepDrftCms` references `Cerebellum.AuthBlocks.Web` (for the authorize attribute / view) and `Cerebellum.AuthBlocks.Models` (for `SystemRoleConstants.Admin`). +- A new `DeepDrftWeb/environment/authblocks.json` (or appsettings section) holds the JWT secret, issuer, audience, Mailtrap email connection, admin seed credentials, and Postgres connection string. Follows the same pattern as `apikey.json` — not in repo. +- **New infrastructure dependency: PostgreSQL.** AuthBlocks is PG-only (see §3.5). The existing SQLite metadata DB (`../Database/deepdrft.db`) is unaffected; the auth DB is a separate context. Local dev gains a `docker-compose.yml` Postgres service or assumes a local PG install. - `DeepDrftCli` and its `obj/` legacy `.NET 9` artefacts: removed in §8 (retirement). --- -## 3. Authentication model `[TBD pending Daniel's input on AuthBlocks]` +## 3. Authentication model -Daniel specified `Cerebellum.AuthBlocks` and "local login." The library is not on public NuGet and was not discoverable on GitHub; treat its surface as something Daniel must describe before this section commits. The plan below names the shape of the auth surface in terms the eventual integration must satisfy, regardless of which AuthBlocks primitives back it. +DeepDrft adopts `Cerebellum.AuthBlocks` (v10.3.16, source at `C:\Development\AuthBlocks\`, published as Cerebellum-prefixed NuGets) as a plug-and-play account system. AuthBlocks is built on ASP.NET Core Identity, hands us a complete user/role/registration substrate, and ships its own MudBlazor login UI. The CMS is the first consumer of this substrate in DeepDrftHome. -### 3.1 What "local login" must provide (functional requirements) +### 3.1 What AuthBlocks provides -- A login page (likely `/cms/login` or `/login`) that accepts credentials and establishes a session. -- A session mechanism that survives the prerender → WASM boundary (cookie-based is the obvious match — already how dark mode persists; see `CONTEXT.md §3.6`). -- An authorization gate on every CMS page and every CMS-only API endpoint. Unauthenticated requests redirect to login (UI) or 401 (API). -- A logout affordance. -- A way to seed the first admin account at deploy time (config file, environment variable, or a `dotnet user-jwts`-style command). Required because there is no public signup. +Concretely, from reading the library source: -### 3.2 Account model `[open question]` +- **User store.** Bundled. `ApplicationUser : IdentityUser` and `ApplicationRole : IdentityRole` (the long-keyed Identity entities) live in `AuthBlocksModels`. The `AuthDbContext` extends `IdentityDbContext<...>` with the full eight-table layout (`users`, `roles`, `user_roles`, `user_claims`, `role_claims`, `user_logins`, `user_tokens`, plus AuthBlocks's own `pending_registrations` and `refresh_tokens`) under an `auth` schema. EF migrations ship in the package and run automatically on `UseAuthBlocksStartupAsync()`. We bring nothing — we just call the extension methods. +- **Login / UI.** Bundled. `Cerebellum.AuthBlocks.Web` ships server-rendered MudBlazor pages: `/account/login`, `/account/logout`, `/account/register` (registration-code flow), `/account/super-register` (admin-creates-user), plus admin pages under `/user-admin/*` for user, role, registration, and permission management. A `RedirectToLogin` component handles unauthenticated → login redirects with `returnUrl` preservation, and a `LogoutButton` is exposed for menus. +- **Session mechanism.** JWT bearer, not cookie. Access tokens (default 60 min) plus refresh tokens (default 7 days) issued by AuthBlocks's minimal-API routes; stored in browser `localStorage` by `TokenService`; refreshed automatically by `JwtAuthenticationStateProvider`. Server-side, the standard `[Authorize]` / `[Authorize(Roles=...)]` attributes work because AuthBlocks configures `JwtBearerDefaults.AuthenticationScheme` as the default authenticate/challenge scheme. +- **Identity relationship.** AuthBlocks **sits on top of** ASP.NET Core Identity (`AddIdentityCore().AddRoles<...>().AddEntityFrameworkStores()`), but does **not** install `SignInManager` (no cookie-based sign-in) and replaces the default role authorization handler with `HierarchicalRolesAuthorizationHandler`. Password hashing is the standard ASP.NET Core Identity `IPasswordHasher` — provided, not BYO. Default password policy is relaxed (length 6, no required character classes); we can tighten via the standard Identity options if needed. +- **Hierarchical roles.** `SystemRole.Admin` (id 1) is the parent of `SystemRole.UserAdmin` (id 2). Hierarchy is seeded on startup. `[HierarchicalRoleAuthorize("UserAdmin")]` admits any user assigned `Admin` or `UserAdmin`. The hierarchy is extensible by editing `SystemRole.cs` upstream — for v1 we use what's there. +- **Admin seeding.** A single `AdminUserSettings { UserName, Email, Password }` on the options object causes `UseAuthBlocksStartupAsync` to create (or repair) one admin user on first boot, assigned the `Admin` system role. This is exactly the "seed the first admin at deploy time" capability the CMS needs. +- **Email.** AuthBlocks's registration flow requires an outbound email provider (Mailtrap; `EmailConnection.Host` + `EmailConnection.Token` are required options). For v1 we wire this even though the CMS does not exercise the `/account/register` invitation flow — the options validator throws on startup if it is missing. Wave 3 account-management is when this matters; for Wave 1 we point it at a Mailtrap sandbox. -Three plausible shapes — pick one: +### 3.2 Account model -1. **Shared admin password.** One credential, possibly stored in `environment/cms.json` next to `apikey.json`. Lowest ceremony; matches the CLI's "trusted operator" model. Audit trail is per-action with no "who did it" attribution. -2. **Individual accounts per collective member.** Real user table (hashed passwords), real `UserId` on mutations. Enables attribution, per-member permissions later, and is the precondition for any future audience-side identity work. Higher initial cost. -3. **Hybrid: single admin role today, designed for accounts tomorrow.** Schema and middleware assume `UserId`; v1 has one seeded admin and no signup flow; account-management UI is deferred. (`feedback_design_for_adaptability` — defer the feature, design the seam.) +**Committed: hierarchical-role accounts via AuthBlocks, seeded with one `Admin` user from config.** This is the option-3 shape from the prior draft and it happens to be exactly what AuthBlocks gives us out of the box: -**Recommendation: option 3.** Capture `UserId` on every CMS-side mutation (a nullable `CreatedByUserId` on `TrackEntity` and equivalent on any new entities) from day one, even if v1 has exactly one user. This avoids the backfill cliff if option 2 is ever revisited. The user table is small (one row) but real; the login flow is "username + password," not "shared password against a config string." This is the option the rest of the plan assumes. +- Real per-user accounts (`ApplicationUser` table). No shared password. +- One seeded admin on first boot via `AdminUserSettings`. Username, email, password come from `DeepDrftWeb/environment/authblocks.json` (gitignored, same pattern as `apikey.json`). +- No public signup in Wave 1. The `/account/register` page that AuthBlocks bundles requires a registration code (generated by an admin via `/api/pending-registrations`). We do not surface `/account/register` in any nav until Wave 3 account management lands; the route exists but is uninteresting until then. +- **Mutation attribution.** `TrackEntity` gains a nullable `CreatedByUserId : long?` column in the W1.2 migration. Populated on every CMS-originated mutation; null for historical CLI-added rows and for any pre-CMS data. Captures attribution from day one even though Wave 1 has exactly one user (`feedback_design_for_adaptability`). +- **Role gate.** Every CMS page and every `api/cms/*` endpoint requires the `Admin` system role. We use `Admin` rather than introducing a new `CmsAdmin` role because the collective is small and the existing hierarchy already covers the case; if Wave 3 ever needs finer grain (e.g. a `ContentEditor` role that can edit but not delete), that is a `SystemRole.cs` edit upstream, not a redesign here. -If Daniel prefers option 1 for speed, the rest of this plan still holds — just drop the `UserId` column and the user table. +### 3.3 Session and prerender bridge -### 3.3 What we learn from AuthBlocks before committing +AuthBlocks's JWT-in-localStorage posture interacts with Blazor's prerender → WASM handoff: -Concrete questions Daniel needs to answer (or point at docs for): +- **Server prerender** of a CMS page asks `AuthenticationStateProvider` for state. Server-side, that is satisfied by the JWT bearer middleware reading the `Authorization` header — which is not present on the initial page navigation. The bundled `JwtAuthenticationStateProvider` runs *client-side* (it needs `IJSRuntime` to read localStorage), so during prerender, the user appears anonymous and `[Authorize]` pages redirect to `/account/login`. +- **Solution:** CMS pages render as `InteractiveServer` (no prerender bypass needed since the same circuit handles auth). For the public site's `InteractiveAuto` pages, AuthBlocks's `AddAuthenticationStateDeserialization()` (called in `DeepDrftWeb.Client.Startup`) is the bridge — it carries serialized auth state from prerender into the WASM render and back. This is the same shape as the dark-mode `PersistentComponentState` bridge described in `CONTEXT.md §3.6`. +- A "Sign in" link in the public-site nav points at `/account/login`; AuthBlocks's login page returns the user to `ReturnUrl` on success. The CMS landing page is the natural return target after CMS login. -- Does AuthBlocks bring its own user store (EF Core entities, schema, migrations) or does it expect us to provide one? -- Does it bring a login UI / Razor components, or only the policy/middleware primitives? -- Cookie-based, JWT-based, or both? If both, which is the recommended posture for a server-rendered Blazor surface? -- Does it integrate with ASP.NET Core Identity, or is it a parallel/replacement system? -- Password hashing — provided by the library, or BYO? +### 3.4 Authorization wiring (concrete) -Once answered, the auth section commits and the §4 page list inherits its specifics (login form fields, logout button placement, "current user" display). +- **Razor pages in the RCL:** `@attribute [HierarchicalRoleAuthorize("Admin")]` at the top of every `/cms/*` page. The bundled handler walks the role hierarchy. +- **API endpoints in DeepDrftWeb:** `[Authorize(Roles = "Admin")]` on the new `api/cms/track` controller. The hierarchical handler is registered globally so the standard `[Authorize(Roles=...)]` attribute participates in hierarchy walks too. +- **Anonymous public surface:** unaffected. `GET api/track/page` and `GET api/track/{id}` remain unauthenticated. The public gallery, player, and home page do not require login. Auth state on the public side is "anonymous or signed-in admin"; signed-in state surfaces only as a "CMS" link in the nav. +- **Login UI:** consume the bundled pages at `/account/login`, `/account/logout`. Do not author CMS-specific login pages. `RedirectToLogin` handles the "I tried to visit `/cms/tracks` while anonymous" case. +- **First-run experience:** Daniel runs the app, AuthBlocks applies migrations against the Postgres `auth` schema, seeds `Admin` + `UserAdmin` roles, creates the admin user from `authblocks.json`. Daniel visits `/account/login`, authenticates, lands on `/cms/tracks`. + +### 3.5 Database conflict — AuthBlocks is Postgres-only + +**This is the load-bearing surprise from the AuthBlocks reading.** AuthBlocks's data layer hard-codes `UseNpgsql(...)` in `AddAuthBlocksDataForWebApi`, the bundled migrations are PostgreSQL-specific (`NOW()`, `timestamp with time zone`, identity columns via `Npgsql:ValueGenerationStrategy`), and the DbContext sets `HasDefaultSchema("auth")`. DeepDrft's existing metadata DB is SQLite. There is no clean "use SQLite for everything" path. + +Three options, in order of preference: + +1. **Run Postgres alongside SQLite (recommended).** Add a `docker-compose.yml` Postgres service for local dev; production deploys against a managed PG. SQLite continues to back `DeepDrftContext` (track metadata); Postgres backs `AuthDbContext` (identity). The two DbContexts never share a transaction — track mutations and audit attribution are recorded in SQLite with a `CreatedByUserId : long?` foreign-key-in-name-only to the Postgres `auth.users.Id`. Cost: a new infra dependency. Benefit: zero forks of AuthBlocks. +2. **Migrate `DeepDrftContext` to Postgres too.** Unify on a single DB engine. Requires rewriting EF migrations, re-seeding from `deepdrft.db`, and accepting Postgres in every dev environment. Larger lift; cleaner end-state. +3. **Fork AuthBlocks to add SQLite support.** Replace `UseNpgsql` with a provider-agnostic registration, rewrite migrations, maintain the fork. Highest cost, perpetual maintenance burden. Not recommended. + +**Recommendation: option 1 for Wave 1.** It minimises change to DeepDrft and avoids touching `DeepDrftContext`. Capture option 2 as a deferred consideration if Postgres-for-auth-only feels operationally annoying after a few months of running it. Option 3 is a non-starter unless AuthBlocks gains upstream SQLite support. + +`[open question]` Daniel to confirm option 1 vs option 2. Option 1 is the path of least resistance for shipping the CMS; option 2 is the cleaner long-term posture. + +### 3.6 What this section commits + +- `Cerebellum.AuthBlocks` (+ `.Web`, + `.Models`) is the auth substrate. We do not write our own user store, password hashing, login pages, or JWT plumbing. +- The CMS is gated by the `Admin` system role via `[HierarchicalRoleAuthorize("Admin")]` on pages and `[Authorize(Roles="Admin")]` on API endpoints. +- The login UI lives at `/account/login` (bundled). Logout at `/account/logout`. The CMS does not author its own. +- One admin user is seeded from `DeepDrftWeb/environment/authblocks.json` on first boot. +- `TrackEntity.CreatedByUserId : long?` is added in W1.2 for attribution. +- Postgres becomes a runtime dependency for the auth context, alongside the existing SQLite track-metadata context (pending §3.5 confirmation). --- @@ -207,7 +246,7 @@ Themes, not dates. The order between waves is sequential (each depends on its pr **Goal:** A logged-in collective member can do everything the CLI does today, from a browser. - **W1.1 `DeepDrftCms` RCL skeleton.** Project created, added to solution, referenced from `DeepDrftWeb`. Empty `Pages/Cms/Index.razor` mounted at `/cms` returning a "CMS — under construction" placeholder, proving the mount works. -- **W1.2 AuthBlocks integration + login.** `Cerebellum.AuthBlocks` referenced, configured, seeded with one admin account (config-driven). `/cms/login` page, `/cms/logout`, session cookie. `[Authorize]`-style gate on `/cms/*`. Auth schema columns (`CreatedByUserId`) added to `TrackEntity` as a nullable migration even though they will not be populated by historical CLI-added rows. +- **W1.2 AuthBlocks integration + login.** Reference `Cerebellum.AuthBlocks`, `Cerebellum.AuthBlocks.Web`, `Cerebellum.AuthBlocks.Models` from `DeepDrftWeb`; reference `Cerebellum.AuthBlocks.Web` from `DeepDrftWeb.Client`. Call `AddAuthBlocks(...)` in `Program.cs` with JWT secret/issuer/audience, Mailtrap email connection, Postgres connection string, and `AdminUserSettings` from `environment/authblocks.json`. Call `await app.Services.UseAuthBlocksStartupAsync()` post-build. Call `app.MapAuthBlocks()` to mount `/api/auth/*` routes. Add the `AuthBlocksWeb` assembly to `AddAdditionalAssemblies` so the bundled `/account/login` and `/account/logout` pages resolve. In `DeepDrftWeb.Client.Startup`, call `AuthBlocksWeb.Client.Startup.ConfigureServices(builder.Services)` for the prerender→WASM auth-state bridge. Add `CreatedByUserId : long?` column to `TrackEntity` via a nullable migration. Provision local Postgres (docker-compose) and document the dev setup. Verify: anonymous visit to `/cms/anything` redirects to `/account/login`; authenticated `Admin` lands successfully. - **W1.3 CMS track list.** `/cms/tracks` consuming the same `GET api/track/page` endpoint as the public gallery. Different rendering (table with admin affordances), same VM. No new SQL endpoint. - **W1.4 CMS upload endpoint + add page.** New `POST api/cms/track` on `DeepDrftWeb` (auth-gated, see §5 for the transport decision). `/cms/tracks/new` page wires `InputFile` to the endpoint. - **W1.5 CMS delete endpoint + delete UI.** New `DELETE api/cms/track/{id}` on `DeepDrftWeb`. Removes the SQL row and the vault entry; logs orphans if vault delete fails after SQL delete succeeds. Delete button + confirmation in the list and detail pages. @@ -238,7 +277,7 @@ Things this plan must honour without re-deciding them. - **`feedback_no_direct_db_from_network_clients`.** The CMS runs server-side in `DeepDrftWeb` and is therefore in the trusted process. The browser still does not reach the database — it talks to CMS endpoints, which call the existing services, which call the databases. The architectural rule is preserved. - **`user_one_source_multiple_views`.** The CMS list is a *different rendering* of the same `PagedResult` the public gallery uses. Do not introduce a parallel `GetCmsTrackPage` endpoint or a parallel VM. If the CMS needs additional fields, extend the existing VM, don't fork it. - **`feedback_design_for_adaptability`.** Capture `CreatedByUserId` on mutations from day one, even with one user. Do not introduce schema columns later as a retrofit. -- **`CONTEXT.md §3.6` (dark-mode prerender bridge).** Auth cookie reads in server prerender follow the same pattern as the dark-mode cookie. If AuthBlocks needs to read auth state server-side and carry it into WASM, it should integrate with `PersistentComponentState` the same way `DarkModeSettings` does. (Likely irrelevant if CMS pages are server-rendered.) +- **`CONTEXT.md §3.6` (dark-mode prerender bridge).** AuthBlocks's `AddAuthenticationStateDeserialization()` (called in `DeepDrftWeb.Client.Startup` per §2.2) is the analogue of the dark-mode `PersistentComponentState` bridge — it carries serialized auth state across the prerender → WASM boundary. For pure-`InteractiveServer` CMS pages this is unused; it matters for the public-site `InteractiveAuto` pages that need to render "Sign in" vs "CMS" links consistently. - **`PLAN.md §0` (audit baseline).** The streaming substrate is stable; the CMS does not touch it. Anything the CMS reads from `DeepDrftContent` goes through the existing `GET api/track/{id}` path. - **Supersession of `PLAN.md §2.4`.** When W1.4 lands, doc-keeper archives `PLAN.md §2.4` to `COMPLETED.md` with a note "subsumed by CMS-PLAN.md Wave 1." This document is the authoritative roadmap for the upload capability. @@ -265,21 +304,16 @@ The CLI does not get deleted on day one. Sequence: ## 9. Open questions for Daniel -These are blockers on specific sections of the plan. Numbered for terse reply. +These are blockers on specific sections of the plan. Numbered for terse reply. The §3 AuthBlocks questions from the prior draft are resolved — the library was read and §3 now commits. -1. **`Cerebellum.AuthBlocks` shape.** What does this library actually provide? Specifically: - - User store (EF entities + migrations) — bundled, or BYO? - - Login UI components — bundled, or build our own? - - Cookie-based, JWT, or both? Default posture for server-rendered Blazor? - - Relationship to ASP.NET Core Identity — sits on top, replaces, or parallel? - - Where do I read the docs / source? (It is not on public NuGet or discoverable on GitHub.) -2. **Account model.** Option 1 (shared admin password), option 2 (per-member accounts), or option 3 (one user today, schema designed for many tomorrow — recommended)? -3. **RCL project name and URL prefix.** Project name `DeepDrftCms` (recommended) or something else? Route prefix `/cms` (recommended), `/admin`, `/manage`, or other? -4. **Render mode for CMS pages.** `InteractiveServer` (recommended for auth + uploads) or `InteractiveAuto`/`InteractiveWebAssembly` (consistency with the public client)? -5. **Dual-write transport.** Option A (in-process direct calls, recommended), option B (HTTP through `DeepDrftContent`), or option C (A plus dead-letter log in Wave 1)? If the two hosts will ever deploy to separate machines, the answer is B. -6. **CMS scope confirmation.** Wave 1 = parity (add, list, edit, delete). Wave 2 = image upload, replace audio, bulk delete, dead-letter view, search/filter. Anything missing? Anything to demote out of Wave 1? -7. **CLI retirement.** Drop Terminal.Gui mode entirely (recommended), keep `DeepDrftCli` indefinitely as a secondary path, or extract Terminal.Gui as a separate tool? -8. **Soak duration.** How long does the CMS run alongside the CLI before the CLI is removed? Time-based, release-based, or "I'll tell you when"? +1. **Postgres-for-auth-only vs. unify on Postgres (§3.5).** Option 1: run Postgres alongside SQLite, AuthBlocks gets PG, `DeepDrftContext` stays on SQLite (recommended, minimum-change). Option 2: migrate `DeepDrftContext` to Postgres too, unify on one engine (cleaner end-state, larger lift). Option 3 (fork AuthBlocks for SQLite) is not recommended. +2. **RCL project name and URL prefix.** Project name `DeepDrftCms` (recommended) or something else? Route prefix `/cms` (recommended), `/admin`, `/manage`, or other? Note: login/logout live at `/account/login` and `/account/logout` (bundled, not negotiable without forking AuthBlocks). +3. **Render mode for CMS pages.** `InteractiveServer` (recommended — matches bundled AuthBlocks UI, native `InputFile`) or `InteractiveAuto`/`InteractiveWebAssembly` (consistency with the public client)? +4. **Dual-write transport.** Option A (in-process direct calls, recommended), option B (HTTP through `DeepDrftContent`), or option C (A plus dead-letter log in Wave 1)? If the two hosts will ever deploy to separate machines, the answer is B. +5. **CMS scope confirmation.** Wave 1 = parity (add, list, edit, delete). Wave 2 = image upload, replace audio, bulk delete, dead-letter view, search/filter. Anything missing? Anything to demote out of Wave 1? +6. **CLI retirement.** Drop Terminal.Gui mode entirely (recommended), keep `DeepDrftCli` indefinitely as a secondary path, or extract Terminal.Gui as a separate tool? +7. **Soak duration.** How long does the CMS run alongside the CLI before the CLI is removed? Time-based, release-based, or "I'll tell you when"? +8. **Mailtrap (or alternative) for the AuthBlocks email channel.** AuthBlocks's options validator requires an `EmailConnection.Host` + `Token` on startup even though Wave 1 does not use the registration-email flow. Acceptable to point at a Mailtrap sandbox (free tier) and defer real email until Wave 3, or do you want a different SMTP provider wired from day one? Answer these in any order. Each unblocks the corresponding section. @@ -288,5 +322,5 @@ Answer these in any order. Each unblocks the corresponding section. ## 10. Working with this file - **Same conventions as `PLAN.md`.** Items move to `COMPLETED.md` when they land; do not delete. Original "What / Why / Shape" body preserved. Open questions belong in the item that raises them — they expire when the item does. The exception is §9, which is a single rendezvous point for all blocking decisions; entries there are removed as Daniel answers them (and the marked-up sections elsewhere collapse to commitments at the same time). -- **Markers.** `[open question]` = a decision point inside an otherwise-committed item. `[TBD pending Daniel's input]` = an entire section that cannot commit until §9 is answered. `[speculative]` = direction inferred, not committed. +- **Markers.** `[open question]` = a decision point inside an otherwise-committed item. `[speculative]` = direction inferred, not committed. (Previous draft used `[TBD pending Daniel's input]` for whole sections that could not commit; the §3 auth section that carried it has since been resolved against the AuthBlocks source.) - **Relationship to `PLAN.md`.** When the CMS work touches an item in `PLAN.md` (notably §2.1 image vault, §2.3 search/filter, §2.4 web upload, §4.3 dead-letter), this document is the place to coordinate the joint landing. Cross-reference rather than duplicating the item's body.