Finishes functional design for unit 0 (back-end changes before front-end work)

This commit is contained in:
2026-06-18 21:40:45 +02:00
parent 9e49489f7e
commit 53a307cdbd
14 changed files with 1065 additions and 4 deletions
@@ -5,7 +5,7 @@
- **Feature Slug**: cms-frontend
- **Project Type**: Brownfield
- **Start Date**: 2026-06-16T20:27:00Z
- **Current Stage**: INCEPTION - Application Design (awaiting approval)
- **Current Stage**: CONSTRUCTION - Unit 0: Backend Prerequisites
- **Branch**: unknown
## Workspace State
@@ -42,11 +42,11 @@
- [x] Requirements Analysis — Completed 2026-06-16T21:00:00Z
- [x] User Stories — Completed 2026-06-16T21:05:00Z
- [x] Workflow Planning — Completed 2026-06-16T21:15:00Z
- [~] Application Design — In Progress (all artifacts generated; awaiting approval)
- [ ] Units Generation — Not Started
- [x] Application Design — Completed 2026-06-16T22:00:00Z
- [x] Units Generation — Completed 2026-06-16T22:20:00Z
### 🟢 CONSTRUCTION PHASE (per unit)
- [ ] Unit 0 — Backend Prerequisites (CORS + httpOnly cookie) — blocking
- [~] Unit 0 — Backend Prerequisites (CORS + httpOnly cookie) — IN PROGRESS
- [ ] Unit 1 — Project Scaffold & Infrastructure
- [ ] Unit 2 — Authentication Pages
- [ ] Unit 3 — Layout & Navigation
+12
View File
@@ -12,6 +12,18 @@
---
## Application Design — Approved
**Timestamp**: 2026-06-16T22:00:00Z
**User Input**: "Approve & Continue"
**AI Response**: "Application Design approved. Proceeding to Units Generation."
**Context**: Application Design completed with all 5 artifacts. naam→name corrected throughout.
---
## User Stories — Approved
**Timestamp**: 2026-06-16T21:10:00Z
@@ -0,0 +1,86 @@
# Functional Design Plan — Unit 0: Backend Prerequisites
## Unit Context
- **Unit**: Unit 0 — Backend Prerequisites
- **Type**: .NET backend changes
- **Stories**: US-02 (partial — cookie support), US-06 (partial), US-07 (partial)
- **Deliverables**:
- CORS policy via `appsettings.json`
- `AuthController` updated for httpOnly cookie (login/refresh/revoke)
- `RefreshTokenRequest.cs` removed
- `TokenResponse` updated with `Name` property
- `appsettings.json` + `appsettings.Development.json` updated
## Plan Checklist
- [x] Unit context analyzed
- [x] Questions generated
- [x] Questions answered
- [x] business-logic-model.md generated
- [x] business-rules.md generated
- [x] domain-entities.md generated
---
## Clarification Questions
Please answer the following questions by filling in the letter choice after the `[Answer]:` tag.
---
### Question 1: Cookie SameSite policy
The refresh token cookie will use `SameSite=Strict`. Is this correct, or should it be `SameSite=Lax`?
- `SameSite=Strict` — the cookie is only sent when the request originates from the exact same site (most secure; may cause issues if the login page is linked from an external source)
- `SameSite=Lax` — the cookie is sent on top-level navigations (e.g. clicking a link) but not on cross-site sub-requests (good balance of security and usability)
A) SameSite=Strict (as currently designed)
B) SameSite=Lax (slightly more permissive, still secure)
X) Other (please describe after [Answer]: tag below)
[Answer]: A
---
### Question 2: Cookie Secure flag in development
In local development (HTTP, not HTTPS), the `Secure` flag on the cookie will cause the browser to reject the cookie. How should this be handled?
A) Use `Secure = true` always — developer must use HTTPS locally (e.g. via `dotnet dev-certs`)
B) Set `Secure` based on environment: `true` in Production, `false` in Development
C) Set `Secure = request.IsHttps` — automatically adapts to the current request protocol
X) Other (please describe after [Answer]: tag below)
[Answer]: B
---
### Question 3: TokenResponse — Name field source
The `TokenResponse` needs a `Name` field for the user's display name. `ApplicationUser` has `UserName` (from IdentityUser) but no dedicated display name field. What should `Name` return?
A) `user.UserName` — the username is used as the display name
B) `user.Email` — the email is used as the display name
C) `user.UserName ?? user.Email` — use UserName if set, fall back to Email
X) Other — add a dedicated `DisplayName` property to `ApplicationUser` (describe below)
[Answer]: C
---
### Question 4: Revoke endpoint — authentication requirement
The current `Revoke` endpoint requires a valid Bearer token (`[Authorize]`). With the cookie-based flow, should this still require authentication?
A) Yes — keep `[Authorize]` requirement (user must have a valid access token to revoke; most secure)
B) No — remove `[Authorize]` (allows revoking even after access token expires; better UX for logout after expiry)
X) Other (please describe after [Answer]: tag below)
[Answer]: A, but make the UX still seemless and make it refresh in the background while logging out. Maybe adding a text along the line of "Securely logging out..." gives some more time.
---
### Question 5: CORS — allowed HTTP methods
Which HTTP methods should the CORS policy allow?
A) Only the methods used by the API: GET, POST, PUT, DELETE, OPTIONS
B) All methods: `AllowAnyMethod()` (simpler, allows future endpoints without CORS changes)
X) Other (please describe after [Answer]: tag below)
[Answer]: A
@@ -0,0 +1,50 @@
# NFR Requirements Plan — Unit 0: Backend Prerequisites
## Context
Unit 0 modifies existing .NET 10 backend endpoints. Most NFRs are inherited from the existing system. Only security-specific NFRs for the cookie/CORS changes require clarification.
## Plan Checklist
- [x] Functional design artifacts analyzed
- [x] Questions generated
- [x] Questions answered
- [x] nfr-requirements.md generated
- [x] tech-stack-decisions.md generated
---
## Clarification Questions
---
### Question 1: Refresh token lifetime
What should the refresh token lifetime be?
A) 7 days (current default — check actual `RefreshToken` expiry in the codebase)
B) 30 days
C) 90 days
X) Other (specify in days after [Answer]: tag)
[Answer]: A
---
### Question 2: Rate limiting on auth endpoints
Should rate limiting be applied to the auth endpoints (login, refresh) to prevent brute-force attacks?
A) Yes — add rate limiting now as part of Unit 0 (e.g. via ASP.NET Core built-in rate limiter)
B) No — rate limiting is out of scope for this unit; document as future work
X) Other (please describe after [Answer]: tag below)
[Answer]: A
---
### Question 3: Error response format on auth failures
What should the error response body look like when auth fails (wrong password, invalid token, missing cookie)?
A) RFC 9457 ProblemDetails (standard ASP.NET format: `type`, `title`, `status`, `detail`)
B) Simple JSON `{ "message": "..." }` (consistent with current API error format)
C) Use the existing GlobalExceptionHandler format from `SlpModularCms.Api/Infrastructure/`
X) Other (please describe after [Answer]: tag below)
[Answer]: A
@@ -0,0 +1,93 @@
# Business Logic Model — Unit 0: Backend Prerequisites
## Overview
Unit 0 modifies the authentication flow to use httpOnly cookies for refresh token delivery and adds CORS support for the React SPA. No new business transactions are introduced — existing auth transactions are made more secure.
---
## Flow 1: Login
```mermaid
sequenceDiagram
participant FE as Frontend
participant AC as AuthController
participant AS as AuthService
participant DB as Database
FE->>AC: POST /auth/login
AC->>AS: AuthenticateAsync()
AS->>DB: FindUser + ValidatePassword
DB-->>AS: User + Role
AS-->>AC: TokenResponse
AC-->>FE: 200 accessToken + Set-Cookie refreshToken
```
**Changes from current behaviour**:
- `RefreshToken` is no longer returned in the response body
- httpOnly cookie is set on the response
- `UserDto` with `Name` (DisplayName ?? Email) is added to response
---
## Flow 2: Refresh Token
```mermaid
sequenceDiagram
participant FE as Frontend
participant AC as AuthController
participant AS as AuthService
participant DB as Database
FE->>AC: POST /auth/refresh (cookie auto-sent)
AC->>AC: Read cookie refreshToken
AC->>AS: RefreshTokenAsync(cookieValue)
AS->>DB: Validate RefreshToken
DB-->>AS: Valid + User
AS-->>AC: new TokenResponse
AC-->>FE: 200 new accessToken + Set-Cookie new refreshToken
```
**Changes from current behaviour**:
- Request body (`RefreshTokenRequest`) is **removed** — token read from cookie only
- New refresh token set in cookie (rotation still applies)
---
## Flow 3: Revoke (Logout)
```mermaid
sequenceDiagram
participant FE as Frontend
participant AC as AuthController
participant AS as AuthService
participant DB as Database
FE->>AC: POST /auth/revoke (no Authorize)
AC->>AC: Read cookie refreshToken
AC->>AS: RevokeTokenAsync(cookieValue)
AS->>DB: Mark token revoked
DB-->>AS: OK
AC->>AC: Clear cookie Expires=epoch
AC-->>FE: 204 No Content
```
**Changes from current behaviour**:
- `[Authorize]` attribute removed — logout works even if access token has expired
- Token read from cookie, not request body
- Cookie explicitly cleared in response
---
## Flow 4: CORS Preflight
```mermaid
sequenceDiagram
participant Browser
participant API as API
Browser->>API: OPTIONS /auth/login preflight
API-->>Browser: 204 Access-Control-Allow headers
Browser->>API: POST /auth/login with credentials
API-->>Browser: 200 + Set-Cookie
```
@@ -0,0 +1,66 @@
# Business Rules — Unit 0: Backend Prerequisites
## BR-U0-01: CORS Origin Validation
- The CORS policy MUST only allow origins listed in `Cors:AllowedOrigins` from configuration
- An empty `AllowedOrigins` array in production means no SPA origin is allowed (safe default)
- `AllowCredentials()` MUST be set — required for the browser to send the httpOnly cookie
- `AllowAnyMethod()` and `AllowAnyHeader()` are used to avoid CORS issues for future endpoints
- The CORS middleware MUST be registered BEFORE `UseAuthentication` and `UseAuthorization` in the pipeline
## BR-U0-02: Refresh Token Cookie — Set Rules
- Cookie name: `refreshToken`
- Cookie path: `/api/v1/auth` — scoped to auth endpoints only; not sent with other API calls
- `HttpOnly = true` — ALWAYS, no exceptions
- `Secure = request.IsHttps` — adapts to the current request protocol (true in production, false in local HTTP)
- `SameSite = Strict` — cookie only sent when request originates from the exact same site
- Cookie is set on BOTH `Login` and `Refresh` responses (token rotation)
- Cookie MUST NOT be set on failed auth attempts
## BR-U0-03: Refresh Token Cookie — Clear Rules
- On `Revoke`: Set-Cookie with the same name/path but `Expires = DateTime.UnixEpoch` (epoch = effectively deleted)
- On failed refresh (invalid/expired token): Clear the cookie in the error response
- Cookie clearing MUST use the exact same `Path` as cookie setting (`/api/v1/auth`)
## BR-U0-04: HTTP Response Body — Login and Refresh
The following fields MUST be returned in the response body:
- `accessToken` (string) — JWT bearer token
- `expiresAt` (ISO datetime) — access token expiry
- `user.id` (Guid)
- `user.email` (string)
- `user.name` (string) — `ApplicationUser.DisplayName ?? ApplicationUser.Email`
- `user.role` (string) — the user's primary role name (Owner / Admin / User)
- `user.isActive` (bool)
The `refreshToken` MUST NOT appear in the response body.
## BR-U0-05: Revoke Endpoint Authorization
- `[Authorize]` is REMOVED from the `Revoke` endpoint
- If the refresh token cookie is present: revoke it and clear the cookie
- If the refresh token cookie is absent: no-op, return `204 No Content` (idempotent logout)
- This allows logout to succeed even when the access token has already expired
## BR-U0-06: Refresh Endpoint — Input
- The `RefreshTokenRequest` body parameter is REMOVED
- The refresh token is read exclusively from `Request.Cookies["refreshToken"]`
- If the cookie is absent: return `401 Unauthorized` with a generic error message
- The existing `accessToken` validation logic in `IAuthService.RefreshTokenAsync` can be relaxed or the signature adapted (see code generation for details)
## BR-U0-07: DisplayName Field
- `ApplicationUser.DisplayName` is nullable (`string?`)
- Maximum length: 100 characters
- Default value: `NULL` for existing users
- `name` in the response is computed as: `user.DisplayName ?? user.Email`
- On new user creation via invitation (`POST /users/complete-setup`): the `DisplayName` is set from the form field
- On owner creation (`POST /setup/owner`): `DisplayName` defaults to `NULL` (falls back to email)
## BR-U0-08: Migration
- A new EF Core Code-First migration is required named e.g. `AddDisplayNameToApplicationUser`
- The migration adds `DisplayName nvarchar(100) NULL` to the `AspNetUsers` table
- All existing users receive `NULL` as their `DisplayName` (they see their email as display name)
## BR-U0-09: Security Baseline Compliance (SECURITY-12)
- httpOnly cookie prevents XSS token theft ✅
- `Secure = request.IsHttps` ensures the cookie is only sent over HTTPS in production ✅
- `SameSite=Strict` prevents CSRF attacks on the refresh endpoint ✅
- No credentials (tokens) in localStorage or response body ✅
- Session invalidated on logout (token revoked + cookie cleared) ✅
@@ -0,0 +1,90 @@
# Domain Entities — Unit 0: Backend Prerequisites
## Modified Entity: ApplicationUser
`ApplicationUser` is extended with a new `DisplayName` property.
```csharp
public class ApplicationUser : IdentityUser<Guid>
{
// Existing properties (unchanged)
public bool IsActive { get; set; }
public DateTime CreatedAt { get; set; }
// NEW: Display name shown in the frontend UI
// Optional — if null/empty, the frontend falls back to the user's email
public string? DisplayName { get; set; }
}
```
**Migration required**: A new EF Core migration must be added to `SlpModularCms.Core/Migrations/` to add the `DisplayName` column to the `AspNetUsers` table.
| Property | Type | Nullable | Max Length | Default |
|----------|------|----------|-----------|---------|
| `DisplayName` | `nvarchar` | Yes | 100 | NULL |
---
## Modified DTO: TokenResponse
`TokenResponse` is extended to carry the user's display name and role for the frontend auth context.
```csharp
public record TokenResponse(
string AccessToken,
string RefreshToken, // Kept in record for internal use; NOT returned in HTTP response body
DateTime ExpiresAt,
UserDto User
);
public record UserDto(
Guid Id,
string Email,
string Name, // DisplayName ?? Email (fallback)
string Role,
bool IsActive
);
```
> **Note**: `RefreshToken` is no longer returned in the HTTP response body. The `AuthController` reads it from the `TokenResponse` record internally to set the httpOnly cookie, then returns only `AccessToken`, `ExpiresAt`, and `User` to the client.
---
## Removed: RefreshTokenRequest
`RefreshTokenRequest.cs` is deleted. The refresh token is now read exclusively from the httpOnly cookie.
```csharp
// DELETED — no longer needed
// public record RefreshTokenRequest(string AccessToken, string RefreshToken);
```
---
## Configuration Entity: CorsSettings
New configuration section read via `IConfiguration` — no database entity.
```json
// appsettings.json (production placeholder)
{
"Cors": {
"AllowedOrigins": []
}
}
// appsettings.Development.json
{
"Cors": {
"AllowedOrigins": ["http://localhost:5173"]
}
}
```
```csharp
// Bound via IConfiguration — not a domain entity
public class CorsSettings
{
public string[] AllowedOrigins { get; set; } = [];
}
```
@@ -0,0 +1,53 @@
# NFR Requirements — Unit 0: Backend Prerequisites
## NFR-U0-01: Security — httpOnly Cookie (SECURITY-12)
- Refresh token MUST be stored in an httpOnly, Secure (request.IsHttps), SameSite=Strict cookie
- Access token MUST NOT be returned in persistent storage; only in response body and in-memory on client
- See `business-rules.md` BR-U0-02, BR-U0-03 for detailed cookie specification
## NFR-U0-02: Security — CORS (SECURITY-08)
- CORS MUST use explicit origin allowlist from configuration — no wildcard origins
- `AllowCredentials()` MUST be set to allow the refresh token cookie to be sent cross-origin
- CORS middleware MUST be registered before `UseAuthentication` in the pipeline
- An empty `AllowedOrigins` array in production configuration is a safe default (no SPA access)
## NFR-U0-03: Security — Rate Limiting (SECURITY-11)
- The `POST /api/v1/auth/login` and `POST /api/v1/auth/refresh` endpoints MUST have rate limiting
- **Implementation**: ASP.NET Core built-in `RateLimiter` middleware (available in .NET 7+)
- **Policy**: Fixed window — max **5 requests per 1 minute per IP address** on login endpoint
- **Policy**: Sliding window — max **20 requests per 1 minute per IP address** on refresh endpoint
- Exceeded rate limit returns `429 Too Many Requests`
- Rate limit headers (`Retry-After`) MUST be included in 429 responses
- Configuration stored in `appsettings.json → RateLimiting` section
## NFR-U0-04: Reliability — Refresh Token Lifetime
- Refresh token lifetime: **7 days** (unchanged from current default)
- After expiry, the user must re-authenticate via the login page
- Refresh token rotation is already implemented — each refresh issues a new token
## NFR-U0-05: Error Response Format (SECURITY-09, SECURITY-15)
- ALL authentication error responses MUST use **RFC 9457 ProblemDetails** format:
```json
{
"type": "https://tools.ietf.org/html/rfc7235#section-3.1",
"title": "Unauthorized",
"status": 401,
"detail": "Invalid credentials."
}
```
- Error messages MUST be generic — do NOT reveal whether email or password was wrong
- Error messages MUST NOT expose internal details (stack traces, exception types, DB details)
- `asp-problem-details` behavior is already partially handled by `GlobalExceptionHandler`; auth-specific errors may need explicit `ProblemDetails` returns in `AuthController`
## NFR-U0-06: Maintainability
- Rate limiting configuration (window size, request limits) stored in `appsettings.json` — configurable without code changes
- CORS origins stored in `appsettings.json` — configurable per environment
- No magic strings for cookie name or policy name — use constants
## NFR-U0-07: Test Coverage
- Unit tests for `AuthController` must be updated to cover:
- Cookie is set on successful login/refresh
- Cookie is cleared on revoke
- 401 returned when cookie is missing on refresh
- Rate limit behavior (mock rate limiter)
- Existing `AuthService` unit tests remain valid (service signature unchanged)
@@ -0,0 +1,105 @@
# Tech Stack Decisions — Unit 0: Backend Prerequisites
## Existing Stack (no changes)
All existing technology choices are retained:
- **.NET 10 / ASP.NET Core 10** — Web API framework
- **Entity Framework Core 10** — ORM + migrations
- **ASP.NET Core Identity** — User/role management, password hashing
- **SQL Server** — Primary database
---
## New/Updated: Rate Limiting
| Decision | Choice | Rationale |
|----------|--------|-----------|
| Rate limiter | ASP.NET Core built-in `RateLimiter` (Microsoft.AspNetCore.RateLimiting) | No extra NuGet package needed — available in .NET 7+; production-ready |
| Login policy | Fixed window (5 req / 1 min / IP) | Predictable; blocks brute-force login attempts |
| Refresh policy | Sliding window (20 req / 1 min / IP) | More lenient for token rotation; prevents abuse |
| Configuration | `appsettings.json → RateLimiting` | Configurable without code recompile |
---
## New/Updated: CORS
| Decision | Choice | Rationale |
|----------|--------|-----------|
| CORS implementation | ASP.NET Core built-in CORS middleware | No extra NuGet package; part of framework |
| Origins configuration | `appsettings.json → Cors:AllowedOrigins[]` | Environment-specific; follows dotnet-appsettings pattern |
| Credential support | `AllowCredentials()` | Required for httpOnly cookie to be sent cross-origin |
| Methods | `AllowAnyMethod()` | Avoids future CORS issues when new endpoints are added |
| Headers | `AllowAnyHeader()` | Standard approach; avoids pre-flight failures for custom headers |
---
## New/Updated: httpOnly Cookie
| Decision | Choice | Rationale |
|----------|--------|-----------|
| Cookie implementation | ASP.NET Core `Response.Cookies.Append()` | Built-in, no extra library |
| Token read | `Request.Cookies["refreshToken"]` | Standard ASP.NET Core cookie reading |
| Secure flag | `request.IsHttps` | Adapts to environment; safe in production, usable in local HTTP dev |
| SameSite | `Strict` | Maximum CSRF protection |
---
## New/Updated: Error Responses
| Decision | Choice | Rationale |
|----------|--------|-----------|
| Error format | RFC 9457 ProblemDetails | .NET standard; consistent with ASP.NET Core defaults; interoperable |
| Implementation | `Microsoft.AspNetCore.Mvc.ProblemDetails` (built-in) | No extra NuGet package needed |
| Global handler | Existing `GlobalExceptionHandler` extended | Avoids duplication; centralises error formatting |
---
## appsettings.json Additions
Following the dotnet-appsettings skill pattern:
```json
// appsettings.json (production defaults — no real values)
{
"Cors": {
"AllowedOrigins": []
},
"RateLimiting": {
"Login": {
"PermitLimit": 5,
"WindowSeconds": 60
},
"Refresh": {
"PermitLimit": 20,
"WindowSeconds": 60
}
}
}
```
```json
// appsettings.Development.json (complete reference for developers)
{
"Cors": {
"AllowedOrigins": ["http://localhost:5173"]
},
"RateLimiting": {
"Login": {
"PermitLimit": 5,
"WindowSeconds": 60
},
"Refresh": {
"PermitLimit": 20,
"WindowSeconds": 60
}
}
}
```
```json
// appsettings.local.json (developer override — gitignored)
{
"Cors": {
"AllowedOrigins": ["http://localhost:5173"]
}
}
```
@@ -0,0 +1,60 @@
# Unit of Work Dependencies — CMS Frontend
## Dependency Matrix
| Unit | Depends On | Blocks |
|------|-----------|--------|
| **Unit 0** — Backend Prerequisites | (none — standalone .NET change) | Unit 1 (CORS must work before frontend can call API) |
| **Unit 1** — Project Scaffold | Unit 0 | Units 2, 3, 4, 5, 6 |
| **Unit 2** — Auth Pages | Unit 1 (ApiClient, AuthContext) | Units 3, 4, 5, 6 (auth layer required) |
| **Unit 3** — Layout & Navigation | Unit 2 (ProtectedRoute, RoleGuard) | Units 4, 5, 6 (authenticated shell) |
| **Unit 4** — Dashboard | Unit 3 (AppLayout) | Unit 5, 6 (patterns established) |
| **Unit 5** — User Management | Unit 3 (RoleGuard), Unit 4 (patterns) | Unit 6 |
| **Unit 6** — Remaining Pages & Docs | Unit 3 (AppLayout, RoleGuard) | (none — final unit) |
## Dependency Diagram
```mermaid
graph TD
U0["Unit 0\nBackend Prerequisites"]
U1["Unit 1\nProject Scaffold"]
U2["Unit 2\nAuth Pages"]
U3["Unit 3\nLayout & Navigation"]
U4["Unit 4\nDashboard"]
U5["Unit 5\nUser Management"]
U6["Unit 6\nRemaining Pages"]
U0 -->|"CORS + cookie\nenabled"| U1
U1 -->|"ApiClient\nAuthContext"| U2
U2 -->|"ProtectedRoute\nRoleGuard\nInitGuard"| U3
U3 -->|"AppLayout\nSidebar"| U4
U3 -->|"AppLayout\nRoleGuard"| U5
U3 -->|"AppLayout\nRoleGuard"| U6
U4 -->|"patterns\nestablished"| U5
style U0 fill:#4CAF50,stroke:#2E7D32,color:#fff
style U1 fill:#FFC107,stroke:#F57F17,color:#000
style U2 fill:#FF5722,stroke:#BF360C,color:#fff
style U3 fill:#9C27B0,stroke:#4A148C,color:#fff
style U4 fill:#2196F3,stroke:#0D47A1,color:#fff
style U5 fill:#009688,stroke:#004D40,color:#fff
style U6 fill:#607D8B,stroke:#263238,color:#fff
```
## Shared Components Across Units
| Component | Created In | Used By |
|-----------|-----------|---------|
| `ApiClient` | Unit 1 | Units 2, 4, 5, 6 |
| `AuthContext` / `useAuth` | Unit 1 | Units 2, 3, 4, 5, 6 |
| `types.ts` (TS interfaces) | Unit 1 | All units |
| `ProtectedRoute` | Unit 2 | Units 3, 4, 5, 6 (via layout route) |
| `RoleGuard` | Unit 2 | Units 3, 5, 6 |
| `InitGuard` | Unit 2 | All authenticated routes (via `__root.tsx`) |
| `AppLayout` | Unit 3 | Units 4, 5, 6 |
| `Sidebar` | Unit 3 | Units 4, 5, 6 |
| `useAvailabilityStatus` | Unit 4 | Unit 6 (`SettingsPage`) |
| `AvailabilityStatusBadge` | Unit 4 | Unit 6 (`SettingsPage`) |
| `useValidateInvitation` | Unit 5 | Unit 2 (`InviteCompletePage`) — note: hook defined in Unit 5, used in Unit 2 route |
> **Note on cross-unit hook usage**: `useValidateInvitation` and `useCompleteSetup` are logically invitation hooks (Unit 5) but are used in `InviteCompletePage` (Unit 2). In practice, these hooks can be created in Unit 2 and moved/refactored in Unit 5, or created directly in Unit 5 and `InviteCompletePage` completed then. Implementation order to follow: create stubs in Unit 2, full implementation in Unit 5.
@@ -0,0 +1,100 @@
# Unit of Work Story Map — CMS Frontend
## Story-to-Unit Mapping
All 20 user stories from `stories.md` are assigned to units. Every story is covered.
| Story | Title | Unit | Rationale |
|-------|-------|------|-----------|
| US-06 | Initialize system as first Owner | Unit 0 + Unit 2 | Backend endpoint already exists; frontend SetupPage in Unit 2 |
| US-07 | Redirect to setup when not initialized | Unit 0 + Unit 2 | Backend returns `initialized: false`; InitGuard in Unit 2 |
| US-01 | Login with email and password | Unit 2 | LoginPage + AuthContext.login() |
| US-02 | Session persistence via refresh token | Unit 2 | AuthContext restore-on-mount + httpOnly cookie (Unit 0 backend) |
| US-03 | Logout | Unit 2 | AuthContext.logout() + AuthController.Revoke() |
| US-04 | Redirect to login when not authenticated | Unit 2 | ProtectedRoute |
| US-05 | Auto token refresh on expiry | Unit 2 | ApiClient 401 interceptor → AuthContext.refresh() |
| US-13 | Complete account setup via invitation link | Unit 2 | InviteCompletePage (stubs for invitation hooks; full hooks in Unit 5) |
| US-14 | Handle expired/invalid invitation token | Unit 2 | InviteCompletePage error states |
| US-18 | Role-appropriate sidebar navigation | Unit 3 | Sidebar with role-filtered nav items |
| US-19 | Toggle dark/light theme | Unit 3 | ThemeProvider + theme toggle in Sidebar |
| US-08 | View dashboard after login | Unit 4 | DashboardPage |
| US-09 | View availability status on dashboard | Unit 4 | AvailabilityStatusBadge + useAvailabilityStatus |
| US-10 | View list of users | Unit 5 | UsersPage + useUsers |
| US-11 | Invite a new user | Unit 5 | InviteUserDialog + useInviteUser |
| US-12 | Share invite link | Unit 5 | InviteUserDialog (step 2 — share link) |
| US-15 | View own profile | Unit 6 | ProfilePage |
| US-16 | View availability status in settings | Unit 6 | SettingsPage + useAvailabilityStatus (reused from Unit 4) |
| US-17 | Access denied to System Settings for non-Owners | Unit 6 | RoleGuard on `/settings` |
| US-20 | View CMS management placeholder | Unit 6 | CmsPage (Owner only) |
---
## Coverage Verification
| Unit | Stories | Count |
|------|---------|-------|
| Unit 0 (Backend) | US-02 (partial), US-06 (partial), US-07 (partial) | 3 |
| Unit 2 (Auth Pages) | US-01, US-02, US-03, US-04, US-05, US-06, US-07, US-13, US-14 | 9 |
| Unit 3 (Layout) | US-18, US-19 | 2 |
| Unit 4 (Dashboard) | US-08, US-09 | 2 |
| Unit 5 (User Management) | US-10, US-11, US-12 | 3 |
| Unit 6 (Remaining Pages) | US-15, US-16, US-17, US-20 | 4 |
**Total: 20/20 stories assigned**
---
## Story Map Visualization
```mermaid
graph TD
subgraph U0["Unit 0 — Backend"]
s06b["US-06 partial\nsetup endpoint"]
s07b["US-07 partial\nstatus endpoint"]
s02b["US-02 partial\ncookie support"]
end
subgraph U2["Unit 2 — Auth Pages"]
s01["US-01 Login"]
s02["US-02 Session restore"]
s03["US-03 Logout"]
s04["US-04 Auth redirect"]
s05["US-05 Token refresh"]
s06["US-06 Setup page"]
s07["US-07 Init redirect"]
s13["US-13 Invite complete"]
s14["US-14 Invalid token"]
end
subgraph U3["Unit 3 — Layout"]
s18["US-18 Sidebar nav"]
s19["US-19 Theme toggle"]
end
subgraph U4["Unit 4 — Dashboard"]
s08["US-08 Dashboard"]
s09["US-09 Availability"]
end
subgraph U5["Unit 5 — User Mgmt"]
s10["US-10 User list"]
s11["US-11 Invite user"]
s12["US-12 Share link"]
end
subgraph U6["Unit 6 — Remaining"]
s15["US-15 Profile"]
s16["US-16 Settings"]
s17["US-17 Access denied"]
s20["US-20 CMS placeholder"]
end
U0 --> U2 --> U3 --> U4 --> U5 --> U6
style U0 fill:#4CAF50,stroke:#2E7D32,color:#fff
style U2 fill:#FF5722,stroke:#BF360C,color:#fff
style U3 fill:#9C27B0,stroke:#4A148C,color:#fff
style U4 fill:#2196F3,stroke:#0D47A1,color:#fff
style U5 fill:#009688,stroke:#004D40,color:#fff
style U6 fill:#607D8B,stroke:#263238,color:#fff
```
@@ -0,0 +1,198 @@
# Units of Work — CMS Frontend
## Overview
The CMS frontend is decomposed into **7 sequential units** (Unit 06). Each unit builds on the previous; dependencies flow strictly forward. All units together produce a single deployable SPA + updated backend API.
```mermaid
graph LR
U0["Unit 0\nBackend Prerequisites"]
U1["Unit 1\nProject Scaffold"]
U2["Unit 2\nAuth Pages"]
U3["Unit 3\nLayout & Nav"]
U4["Unit 4\nDashboard"]
U5["Unit 5\nUser Management"]
U6["Unit 6\nRemaining Pages"]
U0 --> U1 --> U2 --> U3 --> U4 --> U5 --> U6
style U0 fill:#4CAF50,stroke:#2E7D32,color:#fff
style U1 fill:#FFC107,stroke:#F57F17,color:#000
style U2 fill:#FF5722,stroke:#BF360C,color:#fff
style U3 fill:#9C27B0,stroke:#4A148C,color:#fff
style U4 fill:#2196F3,stroke:#0D47A1,color:#fff
style U5 fill:#009688,stroke:#004D40,color:#fff
style U6 fill:#607D8B,stroke:#263238,color:#fff
```
---
## Unit 0 — Backend Prerequisites
**Type**: Backend (.NET) — blocking prerequisite for all frontend units
**Priority**: Must complete before Unit 1
### Deliverables
- `ServiceCollectionExtensions.cs` — add `AddCorsFrontendPolicy()` reading `Cors:AllowedOrigins[]` from config
- `Program.cs` — add `app.UseCors("FrontendPolicy")` before `UseAuthentication`
- `AuthController.cs` — update `Login`, `Refresh`, `Revoke` to use httpOnly cookie
- `RefreshTokenRequest.cs`**remove** (no backward compatibility needed; refresh now uses httpOnly cookie)
- `TokenResponse.cs` — add `Name` property (display name from `UserName` or `Email`)
- `appsettings.json` — add `Cors` section (production placeholder)
- `appsettings.Development.json` — add `Cors:AllowedOrigins: ["http://localhost:5173"]`
- Tests — update `AuthController` tests for cookie-based flow
### Key Decisions
- Cookie name: `refreshToken`, Path: `/api/v1/auth`, HttpOnly, Secure, SameSite=Strict
- CORS: `AllowCredentials()` required for cookie flow
- `name` field in `TokenResponse` maps from `user.UserName ?? user.Email`
---
## Unit 1 — Project Scaffold & Infrastructure
**Type**: Frontend — foundation for all subsequent units
**Depends on**: Unit 0 (API must be reachable with CORS)
### Deliverables
```
frontend/
├── src/
│ ├── api/
│ │ ├── client.ts # ApiClient: fetch + auth header + 401 interceptor
│ │ └── types.ts # Shared TS interfaces (User, AuthResponse, etc.)
│ ├── auth/
│ │ ├── AuthContext.tsx # AuthProvider: in-memory token + user state
│ │ └── useAuth.ts # useAuth hook
│ ├── components/
│ │ ├── layout/
│ │ │ └── ThemeProvider.tsx
│ │ └── error/
│ │ └── ErrorBoundary.tsx
│ ├── main.tsx # Entry: QueryClientProvider + AuthProvider + RouterProvider
│ └── app.tsx # App root
├── .env.example # VITE_API_BASE_URL=http://localhost:5000
├── .env # gitignored
├── vite.config.ts
├── tailwind.config.ts # Primary color #ac0000
├── tsconfig.json
└── package.json # pnpm workspace
```
### Key Decisions
- TanStack Router file-based routing initialized (empty route tree)
- shadcn/ui configured with `#ac0000` primary
- `ApiClient` created; `AuthContext` wired up; `QueryClient` configured
---
## Unit 2 — Authentication Pages
**Type**: Frontend — public + session management
**Depends on**: Unit 1 (ApiClient, AuthContext)
### Deliverables
```
frontend/src/
├── auth/
│ ├── ProtectedRoute.tsx # Redirects unauthenticated → /login
│ ├── RoleGuard.tsx # Redirects wrong role → /403
│ └── InitGuard.tsx # Redirects uninitialized → /setup
├── api/
│ └── useSetup.ts # useSetupStatus, useCreateOwner
└── routes/
├── __root.tsx # Root route: InitGuard + global ErrorBoundary
├── login.tsx # LoginPage
├── setup.tsx # SetupPage
└── invite.complete.tsx # InviteCompletePage
```
### Key Decisions
- `react-hook-form` + `zod` for all forms; password schema enforces exact backend rules
- `InitGuard` in `__root.tsx` blocks all routes until setup status confirmed
- `ProtectedRoute` uses `beforeLoad` in TanStack Router
---
## Unit 3 — Layout & Navigation
**Type**: Frontend — authenticated shell
**Depends on**: Unit 2 (ProtectedRoute, RoleGuard, AuthContext)
### Deliverables
```
frontend/src/
├── components/layout/
│ ├── AppLayout.tsx # Shell: Sidebar + <Outlet />
│ └── Sidebar.tsx # Role-filtered nav, logout, theme toggle
└── routes/
└── _authenticated.tsx # Authenticated layout route wrapping all protected pages
```
### Key Decisions
- `_authenticated.tsx` is a TanStack Router layout route — all child routes inherit `ProtectedRoute`
- Sidebar filters nav items by `user.role` from `AuthContext`
- Responsive: hamburger menu on mobile
---
## Unit 4 — Dashboard
**Type**: Frontend — first authenticated page
**Depends on**: Unit 3 (AppLayout, AuthContext)
### Deliverables
```
frontend/src/
├── api/
│ └── useAvailability.ts # useAvailabilityStatus (staleTime: 30s)
├── components/shared/
│ └── AvailabilityStatusBadge.tsx
└── routes/
└── index.tsx # DashboardPage: welcome + availability widget
```
---
## Unit 5 — User Management
**Type**: Frontend — Owner/Admin feature
**Depends on**: Unit 3 (AppLayout, RoleGuard), Unit 4 (patterns established)
### Deliverables
```
frontend/src/
├── api/
│ ├── useUsers.ts # useUsers, useInviteUser
│ └── useInvitation.ts # useValidateInvitation, useCompleteSetup
├── components/users/
│ └── InviteUserDialog.tsx # Two-step: invite form → share link
└── routes/
└── users.tsx # UsersPage: user table + invite dialog
```
---
## Unit 6 — Remaining Pages & Documentation
**Type**: Frontend + Documentation
**Depends on**: Unit 3 (AppLayout, RoleGuard)
### Deliverables
```
frontend/src/routes/
├── profile.tsx # ProfilePage: read-only user info
├── settings.tsx # SettingsPage: Owner-only, availability + placeholders
├── cms.tsx # CmsPage: Owner-only placeholder
├── 403.tsx # AccessDeniedPage
└── $404.tsx # NotFoundPage
```
**Documentation**:
- `README.md` — add **Frontend Development** section:
- Prerequisites (Node.js, pnpm)
- `pnpm install` in `frontend/`
- `.env` setup (copy `.env.example`)
- `pnpm dev` to start dev server
- `pnpm build` for production build
- Security headers note for production deployment
@@ -0,0 +1,27 @@
# Unit of Work Plan — CMS Frontend
## Context
Unit decomposition is derived directly from the Workflow Planning and Application Design stages. No additional questions are needed — all decomposition decisions were already made:
- **Decomposition approach**: Sequential units ordered by dependency (infrastructure → auth → layout → features)
- **Team alignment**: Single developer — no cross-team boundaries needed
- **Technical considerations**: All units deploy as one SPA + one backend API
- **Code organization**: Monorepo-style, `frontend/` at solution root
## Planning Checklist
- [x] Context analyzed (execution-plan.md + application-design.md)
- [x] Unit boundaries confirmed by user (via Workflow Planning approval)
- [x] Story-to-unit mapping defined
- [x] unit-of-work.md generated
- [x] unit-of-work-dependency.md generated
- [x] unit-of-work-story-map.md generated
## Unit Definitions
| Unit | Name | Scope |
|------|------|-------|
| 0 | Backend Prerequisites | .NET backend changes: CORS, httpOnly cookie, TokenResponse DTO with `name` |
| 1 | Project Scaffold & Infrastructure | Vite init, TanStack Router, shadcn/ui, Tailwind v4, ApiClient, AuthContext, ThemeProvider, ErrorBoundary, .env |
| 2 | Authentication Pages | LoginPage, SetupPage, InviteCompletePage, ProtectedRoute, RoleGuard, InitGuard |
| 3 | Layout & Navigation | AppLayout, Sidebar (role-filtered), responsive layout |
| 4 | Dashboard | DashboardPage, AvailabilityStatusBadge, useAvailabilityStatus |
| 5 | User Management | UsersPage, InviteUserDialog, useUsers, useInviteUser, useValidateInvitation, useCompleteSetup |
| 6 | Remaining Pages & Docs | ProfilePage, SettingsPage, CmsPage, NotFoundPage, AccessDeniedPage, README.md frontend section |
@@ -0,0 +1,121 @@
# Gap Report: Completion Messages Missing AI Summary
**Gap ID**: gap-002
**Reported**: 2026-06-16
**Reporter**: User (via cms-frontend Units Generation completion)
**Skill affected**: `aidlc-workflow`
**Rule files affected**:
- `.aidlc-rule-details/inception/units-generation.md` — Step 16
- `.aidlc-rule-details/inception/requirements-analysis.md` — Step 9
- `.aidlc-rule-details/inception/application-design.md` — Step 12
- `.aidlc-rule-details/inception/user-stories.md` — Step 20
- `.aidlc-rule-details/inception/workflow-planning.md` — Step 9
- (potentially all other stage completion messages)
---
## Problem Description
All stage completion messages in the aidlc-workflow skill follow a 3-part structure:
1. **Completion Announcement** (mandatory)
2. **AI Summary** — described as "optional" in the rule files
3. **Formatted Workflow Message** with REVIEW REQUIRED + WHAT'S NEXT (mandatory)
The AI Summary part is described as:
> "AI Summary (optional): Provide structured bullet-point summary of..."
Because the word **"optional"** appears in the rule file, the AI model frequently omits the AI Summary section entirely. This results in completion messages that consist only of a heading and two action options, with no information about:
- What was created or changed
- Which files to review and why
- Key decisions that were made
- Counts, scopes, or metrics of the work done
### Observed Behavior
```markdown
## 🔧 Units Generation Complete
> **📋 REVIEW REQUIRED:**
> ...
> **🚀 WHAT'S NEXT?**
> 🔧 Request Changes
> ✅ Approve & Continue
```
### Expected Behavior
```markdown
## 🔧 Units Generation Complete
Units generation has decomposed the CMS frontend into 7 units:
- **Unit 0** (Backend Prerequisites): CORS policy, httpOnly cookie support in AuthController
- **Unit 1** (Project Scaffold): Vite + TanStack Router + shadcn/ui + ApiClient + AuthContext
- ...
- **20/20 stories** assigned across all units
- **3 artifacts** generated: unit-of-work.md, unit-of-work-dependency.md, unit-of-work-story-map.md
> **📋 REVIEW REQUIRED:**
> ...
```
---
## Root Cause
The word **"optional"** in the AI Summary description is being treated as a literal skip instruction by the model. The intent is that it is "optional" in format (not mandatory to follow a rigid template), but the **presence of a summary is mandatory** for usability.
---
## Impact
- Users cannot determine what was done without opening multiple files
- The review step becomes ineffective because users don't know which files changed or why
- The workflow feels abrupt and opaque after each stage
- User trust in the workflow decreases when completions seem empty
---
## Suggested Fix
Replace the word "optional" in all AI Summary descriptions with language that makes the summary **required but flexible in format**. For example:
**Current** (all affected rule files):
```markdown
**AI Summary** (optional): Provide structured bullet-point summary of...
```
**Proposed fix**:
```markdown
**AI Summary** (mandatory, flexible format): Provide a structured bullet-point summary of what was created, changed, or decided during this stage. Include:
- Key artifacts created (names, not full paths)
- Counts or metrics (e.g. "20/20 stories assigned", "7 units defined")
- Key decisions made
- Anything the user needs to know to do a meaningful review
DO NOT include workflow instructions, file paths, or "please review" language here.
```
---
## Affected Rule Files (all need the same fix)
| Rule File | Section |
|-----------|---------|
| `inception/units-generation.md` | Step 16 — AI Summary |
| `inception/requirements-analysis.md` | Step 9 — AI Summary |
| `inception/application-design.md` | Step 12 — AI Summary |
| `inception/user-stories.md` | Step 20 — AI Summary |
| `inception/workflow-planning.md` | Step 9 — AI Summary |
| `construction/functional-design.md` | Completion message — AI Summary |
| `construction/nfr-requirements.md` | Completion message — AI Summary |
| `construction/nfr-design.md` | Completion message — AI Summary |
| `construction/code-generation.md` | Completion message — AI Summary |
---
## Acceptance Criteria for Fix
- [ ] The word "optional" is removed from AI Summary descriptions in all affected rule files
- [ ] The AI Summary is explicitly required, with flexible format
- [ ] The AI Summary must include at minimum: artifacts created, key decisions, counts/metrics
- [ ] The AI Summary must NOT include workflow navigation instructions (those belong in the WHAT'S NEXT block)
- [ ] After the fix, completion messages consistently include a meaningful summary even when the model is context-constrained