Adds application design (awaiting approval)
This commit is contained in:
@@ -10,15 +10,16 @@
|
|||||||
- **Purpose**: Authenticate a user and receive JWT tokens
|
- **Purpose**: Authenticate a user and receive JWT tokens
|
||||||
- **Authorization**: Anonymous
|
- **Authorization**: Anonymous
|
||||||
- **Request**: `{ "email": string, "password": string }`
|
- **Request**: `{ "email": string, "password": string }`
|
||||||
- **Response**: `{ "accessToken": string, "refreshToken": string, "expiresAt": datetime, "user": { "id": guid, "email": string, "naam": string, "role": string } }`
|
- **Response**: `{ "accessToken": string, "expiresAt": datetime, "user": { "id": guid, "email": string, "name": string, "role": string, "isActive": bool } }`
|
||||||
|
- **Cookie set**: `refreshToken` (httpOnly, Secure, SameSite=Strict, Path=/api/v1/auth)
|
||||||
|
|
||||||
#### POST /auth/refresh
|
#### POST /auth/refresh
|
||||||
- **Method**: POST
|
- **Method**: POST
|
||||||
- **Path**: `/auth/refresh`
|
- **Path**: `/auth/refresh`
|
||||||
- **Purpose**: Refresh an access token using a valid refresh token
|
- **Purpose**: Refresh an access token using the httpOnly refresh token cookie
|
||||||
- **Authorization**: Anonymous
|
- **Authorization**: Anonymous
|
||||||
- **Request**: `{ "accessToken": string, "refreshToken": string }`
|
- **Request**: (empty body — refresh token read from cookie)
|
||||||
- **Response**: Same as `/auth/login`
|
- **Response**: Same as `/auth/login` (new access token + new cookie)
|
||||||
|
|
||||||
#### POST /auth/revoke
|
#### POST /auth/revoke
|
||||||
- **Method**: POST
|
- **Method**: POST
|
||||||
@@ -110,10 +111,18 @@
|
|||||||
- `expiresAt` — access token expiry datetime
|
- `expiresAt` — access token expiry datetime
|
||||||
- `user` — authenticated user info
|
- `user` — authenticated user info
|
||||||
|
|
||||||
|
### Password Validation Rules (enforced by backend)
|
||||||
|
Configured in `ServiceCollectionExtensions.cs` via ASP.NET Core Identity `PasswordOptions`:
|
||||||
|
- `RequiredLength = 8` — minimum 8 characters
|
||||||
|
- `RequireUppercase = true` — at least 1 uppercase letter
|
||||||
|
- `RequireLowercase = true` — at least 1 lowercase letter
|
||||||
|
- `RequireDigit = true` — at least 1 digit
|
||||||
|
- `RequireNonAlphanumeric = true` — at least 1 non-alphanumeric character (e.g. `!@#$%^&*`)
|
||||||
|
|
||||||
### ApplicationUser (returned in auth responses)
|
### ApplicationUser (returned in auth responses)
|
||||||
- `id` — Guid
|
- `id` — Guid
|
||||||
- `email` — string
|
- `email` — string
|
||||||
- `naam` — string (display name)
|
- `name` — string (display name)
|
||||||
- `role` — string (Owner / Admin / User)
|
- `role` — string (Owner / Admin / User)
|
||||||
- `isActive` — boolean
|
- `isActive` — boolean
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
- **Feature Slug**: cms-frontend
|
- **Feature Slug**: cms-frontend
|
||||||
- **Project Type**: Brownfield
|
- **Project Type**: Brownfield
|
||||||
- **Start Date**: 2026-06-16T20:27:00Z
|
- **Start Date**: 2026-06-16T20:27:00Z
|
||||||
- **Current Stage**: INCEPTION - User Stories (awaiting user approval)
|
- **Current Stage**: INCEPTION - Application Design (awaiting approval)
|
||||||
- **Branch**: unknown
|
- **Branch**: unknown
|
||||||
|
|
||||||
## Workspace State
|
## Workspace State
|
||||||
@@ -29,11 +29,31 @@
|
|||||||
| Security Baseline | Yes | Requirements Analysis |
|
| Security Baseline | Yes | Requirements Analysis |
|
||||||
| Property-Based Testing | No | Requirements Analysis |
|
| Property-Based Testing | No | Requirements Analysis |
|
||||||
|
|
||||||
|
## Execution Plan Summary
|
||||||
|
- **Total Units**: 6
|
||||||
|
- **Stages to Execute**: Application Design, Units Generation, Functional Design (per unit), NFR Requirements (Unit 1), NFR Design (Unit 1), Code Generation (per unit), Build and Test
|
||||||
|
- **Stages to Skip**: Infrastructure Design (no cloud infra; static SPA)
|
||||||
|
|
||||||
## Stage Progress
|
## Stage Progress
|
||||||
|
|
||||||
|
### 🔵 INCEPTION PHASE
|
||||||
- [x] Workspace Detection — Completed 2026-06-16T20:28:00Z
|
- [x] Workspace Detection — Completed 2026-06-16T20:28:00Z
|
||||||
- [x] Reverse Engineering (shared) — Completed 2026-06-16T20:32:00Z — Artifacts: aidlc-docs/_shared/reverse-engineering/
|
- [x] Reverse Engineering (shared) — Completed 2026-06-16T20:32:00Z
|
||||||
- [~] Requirements Analysis — In Progress (requirements.md generated; awaiting approval)
|
- [x] Requirements Analysis — Completed 2026-06-16T21:00:00Z
|
||||||
- [~] User Stories — In Progress (stories.md + personas.md generated; awaiting approval)
|
- [x] User Stories — Completed 2026-06-16T21:05:00Z
|
||||||
- [ ] Workflow Planning — Not Started
|
- [x] Workflow Planning — Completed 2026-06-16T21:15:00Z
|
||||||
- [ ] Construction — Not Started
|
- [~] Application Design — In Progress (all artifacts generated; awaiting approval)
|
||||||
|
- [ ] Units Generation — Not Started
|
||||||
|
|
||||||
|
### 🟢 CONSTRUCTION PHASE (per unit)
|
||||||
|
- [ ] Unit 0 — Backend Prerequisites (CORS + httpOnly cookie) — blocking
|
||||||
|
- [ ] Unit 1 — Project Scaffold & Infrastructure
|
||||||
|
- [ ] Unit 2 — Authentication Pages
|
||||||
|
- [ ] Unit 3 — Layout & Navigation
|
||||||
|
- [ ] Unit 4 — Dashboard
|
||||||
|
- [ ] Unit 5 — User Management
|
||||||
|
- [ ] Unit 6 — Profile, Settings & CMS Placeholder
|
||||||
- [ ] Build and Test — Not Started
|
- [ ] Build and Test — Not Started
|
||||||
|
|
||||||
|
### 🟡 OPERATIONS PHASE
|
||||||
|
- [ ] Operations — Placeholder
|
||||||
|
|||||||
@@ -12,6 +12,18 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## User Stories — Approved
|
||||||
|
|
||||||
|
**Timestamp**: 2026-06-16T21:10:00Z
|
||||||
|
|
||||||
|
**User Input**: "approve & continue"
|
||||||
|
|
||||||
|
**AI Response**: "User Stories approved. Proceeding to Workflow Planning."
|
||||||
|
|
||||||
|
**Context**: User Stories stage completed and approved. Requirements also implicitly approved. Moving to Workflow Planning.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## User Stories — Stage Added
|
## User Stories — Stage Added
|
||||||
|
|
||||||
**Timestamp**: 2026-06-16T20:47:00Z
|
**Timestamp**: 2026-06-16T20:47:00Z
|
||||||
|
|||||||
@@ -0,0 +1,151 @@
|
|||||||
|
# Application Design — CMS Frontend
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This document consolidates the complete application design for the CMS frontend feature. It covers both the backend prerequisite changes (Unit 0) and the full frontend React SPA (Units 1–6).
|
||||||
|
|
||||||
|
**Tech decisions confirmed by user:**
|
||||||
|
- **State management**: React Context (auth) + TanStack Query (server state)
|
||||||
|
- **HTTP client**: TanStack Query (server state) using native fetch via `ApiClient` wrapper
|
||||||
|
- **Form validation**: react-hook-form + zod
|
||||||
|
- **Routing**: TanStack Router — file-based (`src/routes/`)
|
||||||
|
- **Auth token storage**: Access token in-memory (React context), refresh token in httpOnly cookie
|
||||||
|
- **Cookie**: `refreshToken`, path `/api/v1/auth`, HttpOnly, Secure, SameSite=Strict
|
||||||
|
- **CORS origins**: Configured via `appsettings.json → Cors:AllowedOrigins[]` (dotnet-appsettings pattern)
|
||||||
|
- **User data in context**: Full user object `{ id, email, name, role, isActive }`
|
||||||
|
- **Error boundaries**: Global + per-page
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture at a Glance
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
subgraph FE["Frontend (frontend/)"]
|
||||||
|
direction TB
|
||||||
|
Infra["Infrastructure\nApiClient · AuthContext · RouterConfig"]
|
||||||
|
Guards["Guards\nProtectedRoute · RoleGuard · InitGuard"]
|
||||||
|
Layout["Layout\nAppLayout · Sidebar · ThemeProvider"]
|
||||||
|
Hooks["TanStack Query Hooks\nuseAvailability · useUsers · useInvitation · useSetup"]
|
||||||
|
Pages["Pages\nLogin · Setup · InviteComplete\nDashboard · Users · Profile\nSettings · CMS · 403 · 404"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph BE["Backend (src/)"]
|
||||||
|
direction TB
|
||||||
|
CORS["CORS Policy\n(ServiceCollectionExtensions)"]
|
||||||
|
AuthCtrl["AuthController\n(httpOnly cookie)"]
|
||||||
|
AuthSvc["AuthService"]
|
||||||
|
end
|
||||||
|
|
||||||
|
Pages --> Hooks
|
||||||
|
Pages --> Infra
|
||||||
|
Layout --> Infra
|
||||||
|
Guards --> Infra
|
||||||
|
Hooks --> Infra
|
||||||
|
Infra -->|HTTP + cookie| AuthCtrl
|
||||||
|
AuthCtrl --> AuthSvc
|
||||||
|
|
||||||
|
style Infra fill:#FFC107,stroke:#F57F17,color:#000
|
||||||
|
style Guards fill:#FF5722,stroke:#BF360C,color:#fff
|
||||||
|
style Layout fill:#9C27B0,stroke:#4A148C,color:#fff
|
||||||
|
style Hooks fill:#009688,stroke:#004D40,color:#fff
|
||||||
|
style Pages fill:#2196F3,stroke:#0D47A1,color:#fff
|
||||||
|
style CORS fill:#4CAF50,stroke:#2E7D32,color:#fff
|
||||||
|
style AuthCtrl fill:#4CAF50,stroke:#2E7D32,color:#fff
|
||||||
|
style AuthSvc fill:#FFC107,stroke:#F57F17,color:#000
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Unit Decomposition Summary
|
||||||
|
|
||||||
|
| Unit | Name | Primary Components |
|
||||||
|
|------|------|--------------------|
|
||||||
|
| 0 | Backend Prerequisites | CorsPolicy, AuthController (cookie), appsettings updates |
|
||||||
|
| 1 | Project Scaffold | ApiClient, AuthContext, RouterConfig, ThemeProvider, ErrorBoundary, `.env` |
|
||||||
|
| 2 | Auth Pages | LoginPage, SetupPage, InviteCompletePage, ProtectedRoute, RoleGuard, InitGuard |
|
||||||
|
| 3 | Layout & Navigation | AppLayout, Sidebar |
|
||||||
|
| 4 | Dashboard | DashboardPage, AvailabilityStatusBadge, useAvailabilityStatus |
|
||||||
|
| 5 | User Management | UsersPage, InviteUserDialog, useUsers, useInviteUser, useValidateInvitation, useCompleteSetup |
|
||||||
|
| 6 | Remaining Pages | ProfilePage, SettingsPage, CmsPage, NotFoundPage, AccessDeniedPage, README.md |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
frontend/
|
||||||
|
├── src/
|
||||||
|
│ ├── api/
|
||||||
|
│ │ ├── client.ts # ApiClient (fetch wrapper, 401 interceptor)
|
||||||
|
│ │ ├── types.ts # Shared TypeScript interfaces
|
||||||
|
│ │ ├── useAvailability.ts # TanStack Query hooks
|
||||||
|
│ │ ├── useUsers.ts
|
||||||
|
│ │ ├── useInvitation.ts
|
||||||
|
│ │ └── useSetup.ts
|
||||||
|
│ ├── auth/
|
||||||
|
│ │ ├── AuthContext.tsx # AuthContext + AuthProvider
|
||||||
|
│ │ ├── useAuth.ts # useAuth hook
|
||||||
|
│ │ ├── ProtectedRoute.tsx
|
||||||
|
│ │ ├── RoleGuard.tsx
|
||||||
|
│ │ └── InitGuard.tsx
|
||||||
|
│ ├── components/
|
||||||
|
│ │ ├── layout/
|
||||||
|
│ │ │ ├── AppLayout.tsx
|
||||||
|
│ │ │ ├── Sidebar.tsx
|
||||||
|
│ │ │ └── ThemeProvider.tsx
|
||||||
|
│ │ ├── users/
|
||||||
|
│ │ │ └── InviteUserDialog.tsx
|
||||||
|
│ │ ├── shared/
|
||||||
|
│ │ │ └── AvailabilityStatusBadge.tsx
|
||||||
|
│ │ └── error/
|
||||||
|
│ │ └── ErrorBoundary.tsx
|
||||||
|
│ ├── routes/ # TanStack Router file-based routes
|
||||||
|
│ │ ├── __root.tsx # Root route (InitGuard, ErrorBoundary)
|
||||||
|
│ │ ├── index.tsx # / → DashboardPage
|
||||||
|
│ │ ├── login.tsx # /login
|
||||||
|
│ │ ├── setup.tsx # /setup
|
||||||
|
│ │ ├── invite.complete.tsx # /invite/complete
|
||||||
|
│ │ ├── users.tsx # /users (Owner/Admin)
|
||||||
|
│ │ ├── profile.tsx # /profile
|
||||||
|
│ │ ├── settings.tsx # /settings (Owner)
|
||||||
|
│ │ ├── cms.tsx # /cms (Owner)
|
||||||
|
│ │ ├── 403.tsx # /403
|
||||||
|
│ │ └── $404.tsx # catch-all
|
||||||
|
│ ├── routeTree.gen.ts # Generated by TanStack Router CLI
|
||||||
|
│ ├── main.tsx # App entry point
|
||||||
|
│ └── app.tsx # QueryClientProvider + AuthProvider + RouterProvider
|
||||||
|
├── .env.example # Template for VITE_API_BASE_URL
|
||||||
|
├── .env # gitignored — developer local override
|
||||||
|
├── package.json
|
||||||
|
├── pnpm-lock.yaml
|
||||||
|
├── tsconfig.json
|
||||||
|
├── vite.config.ts
|
||||||
|
└── tailwind.config.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Security Design Summary
|
||||||
|
|
||||||
|
| Concern | Design Decision |
|
||||||
|
|---------|----------------|
|
||||||
|
| Access token storage | In-memory only (React state in `AuthContext`) |
|
||||||
|
| Refresh token storage | httpOnly cookie — set/cleared by backend |
|
||||||
|
| CORS | Named policy `"FrontendPolicy"` with specific origins + `AllowCredentials()` |
|
||||||
|
| Role enforcement | `RoleGuard` (routing) + `Sidebar` (UX) + backend (authoritative) |
|
||||||
|
| HTTP calls | Centralised through `ApiClient` only |
|
||||||
|
| Form validation | zod schemas matching backend password rules |
|
||||||
|
| Error handling | Global + per-page `ErrorBoundary`; generic user-facing error messages |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Artifacts Index
|
||||||
|
|
||||||
|
| Artifact | Path |
|
||||||
|
|---------|------|
|
||||||
|
| Component definitions | `inception/application-design/components.md` |
|
||||||
|
| Method signatures | `inception/application-design/component-methods.md` |
|
||||||
|
| Service layer | `inception/application-design/services.md` |
|
||||||
|
| Dependency diagram | `inception/application-design/component-dependency.md` |
|
||||||
|
| This document | `inception/application-design/application-design.md` |
|
||||||
+143
@@ -0,0 +1,143 @@
|
|||||||
|
# Component Dependencies — CMS Frontend
|
||||||
|
|
||||||
|
## Dependency Overview
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
subgraph Backend["Backend (Unit 0)"]
|
||||||
|
CorsPolicy["CorsPolicy\n(ServiceCollectionExtensions)"]
|
||||||
|
AuthCtrl["AuthController\n(updated)"]
|
||||||
|
AuthSvc["AuthService\n(no changes)"]
|
||||||
|
AuthCtrl --> AuthSvc
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph Infrastructure["Frontend Infrastructure Layer"]
|
||||||
|
ApiClient["ApiClient\n(fetch wrapper)"]
|
||||||
|
AuthContext["AuthContext\n(session state)"]
|
||||||
|
Router["RouterConfig\n(TanStack Router)"]
|
||||||
|
AuthContext --> ApiClient
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph Guards["Auth Guards"]
|
||||||
|
ProtectedRoute["ProtectedRoute"]
|
||||||
|
RoleGuard["RoleGuard"]
|
||||||
|
InitGuard["InitGuard"]
|
||||||
|
ProtectedRoute --> AuthContext
|
||||||
|
RoleGuard --> AuthContext
|
||||||
|
InitGuard --> ApiClient
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph Layout["Layout Components"]
|
||||||
|
AppLayout["AppLayout"]
|
||||||
|
Sidebar["Sidebar"]
|
||||||
|
ThemeProvider["ThemeProvider"]
|
||||||
|
AppLayout --> Sidebar
|
||||||
|
Sidebar --> AuthContext
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph QueryHooks["TanStack Query Hooks"]
|
||||||
|
useAvailability["useAvailabilityStatus"]
|
||||||
|
useUsers["useUsers + useInviteUser"]
|
||||||
|
useInvitation["useValidateInvitation\nuseCompleteSetup"]
|
||||||
|
useSetup["useSetupStatus\nuseCreateOwner"]
|
||||||
|
useAvailability --> ApiClient
|
||||||
|
useUsers --> ApiClient
|
||||||
|
useInvitation --> ApiClient
|
||||||
|
useSetup --> ApiClient
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph Pages["Page Components"]
|
||||||
|
LoginPage["LoginPage"]
|
||||||
|
SetupPage["SetupPage"]
|
||||||
|
InviteCompletePage["InviteCompletePage"]
|
||||||
|
DashboardPage["DashboardPage"]
|
||||||
|
UsersPage["UsersPage"]
|
||||||
|
ProfilePage["ProfilePage"]
|
||||||
|
SettingsPage["SettingsPage"]
|
||||||
|
CmsPage["CmsPage"]
|
||||||
|
|
||||||
|
LoginPage --> AuthContext
|
||||||
|
SetupPage --> useSetup
|
||||||
|
InviteCompletePage --> useInvitation
|
||||||
|
DashboardPage --> useAvailability
|
||||||
|
UsersPage --> useUsers
|
||||||
|
ProfilePage --> AuthContext
|
||||||
|
SettingsPage --> useAvailability
|
||||||
|
end
|
||||||
|
|
||||||
|
Router --> Guards
|
||||||
|
Router --> AppLayout
|
||||||
|
AppLayout --> Pages
|
||||||
|
|
||||||
|
style CorsPolicy fill:#4CAF50,stroke:#2E7D32,color:#fff
|
||||||
|
style AuthCtrl fill:#4CAF50,stroke:#2E7D32,color:#fff
|
||||||
|
style AuthSvc fill:#FFC107,stroke:#F57F17,color:#000
|
||||||
|
style ApiClient fill:#FFC107,stroke:#F57F17,color:#000
|
||||||
|
style AuthContext fill:#FFC107,stroke:#F57F17,color:#000
|
||||||
|
style Router fill:#FFC107,stroke:#F57F17,color:#000
|
||||||
|
style ProtectedRoute fill:#FF5722,stroke:#BF360C,color:#fff
|
||||||
|
style RoleGuard fill:#FF5722,stroke:#BF360C,color:#fff
|
||||||
|
style InitGuard fill:#FF5722,stroke:#BF360C,color:#fff
|
||||||
|
style AppLayout fill:#9C27B0,stroke:#4A148C,color:#fff
|
||||||
|
style Sidebar fill:#9C27B0,stroke:#4A148C,color:#fff
|
||||||
|
style ThemeProvider fill:#9C27B0,stroke:#4A148C,color:#fff
|
||||||
|
style useAvailability fill:#009688,stroke:#004D40,color:#fff
|
||||||
|
style useUsers fill:#009688,stroke:#004D40,color:#fff
|
||||||
|
style useInvitation fill:#009688,stroke:#004D40,color:#fff
|
||||||
|
style useSetup fill:#009688,stroke:#004D40,color:#fff
|
||||||
|
style LoginPage fill:#2196F3,stroke:#0D47A1,color:#fff
|
||||||
|
style SetupPage fill:#2196F3,stroke:#0D47A1,color:#fff
|
||||||
|
style InviteCompletePage fill:#2196F3,stroke:#0D47A1,color:#fff
|
||||||
|
style DashboardPage fill:#2196F3,stroke:#0D47A1,color:#fff
|
||||||
|
style UsersPage fill:#2196F3,stroke:#0D47A1,color:#fff
|
||||||
|
style ProfilePage fill:#2196F3,stroke:#0D47A1,color:#fff
|
||||||
|
style SettingsPage fill:#2196F3,stroke:#0D47A1,color:#fff
|
||||||
|
style CmsPage fill:#2196F3,stroke:#0D47A1,color:#fff
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dependency Matrix
|
||||||
|
|
||||||
|
| Component | Depends On | Used By |
|
||||||
|
|-----------|-----------|---------|
|
||||||
|
| **ApiClient** | `import.meta.env.VITE_API_BASE_URL`, `AuthContext.accessToken` | All TanStack Query hooks, `InitGuard` |
|
||||||
|
| **AuthContext** | `ApiClient` (for login/logout/refresh calls) | `ProtectedRoute`, `RoleGuard`, `Sidebar`, `ProfilePage`, `LoginPage` |
|
||||||
|
| **RouterConfig** | `ProtectedRoute`, `RoleGuard`, `InitGuard`, `AppLayout` | App root |
|
||||||
|
| **ProtectedRoute** | `AuthContext` | Router (wraps authenticated routes) |
|
||||||
|
| **RoleGuard** | `AuthContext` | Router (wraps role-restricted routes) |
|
||||||
|
| **InitGuard** | `useSetupStatus` → `ApiClient` | Router (root-level check) |
|
||||||
|
| **AppLayout** | `Sidebar`, `ThemeProvider` | Authenticated routes |
|
||||||
|
| **Sidebar** | `AuthContext` | `AppLayout` |
|
||||||
|
| **useAvailabilityStatus** | `ApiClient` | `DashboardPage`, `SettingsPage` |
|
||||||
|
| **useUsers / useInviteUser** | `ApiClient` | `UsersPage` |
|
||||||
|
| **useValidateInvitation / useCompleteSetup** | `ApiClient` | `InviteCompletePage` |
|
||||||
|
| **useSetupStatus / useCreateOwner** | `ApiClient` | `InitGuard`, `SetupPage` |
|
||||||
|
| **DashboardPage** | `useAvailabilityStatus`, `AuthContext` | Router |
|
||||||
|
| **UsersPage** | `useUsers`, `useInviteUser`, `InviteUserDialog` | Router |
|
||||||
|
| **ProfilePage** | `AuthContext` (user data from context — no API call) | Router |
|
||||||
|
| **SettingsPage** | `useAvailabilityStatus` | Router |
|
||||||
|
| **InviteCompletePage** | `useValidateInvitation`, `useCompleteSetup` | Router |
|
||||||
|
| **LoginPage** | `AuthContext.login()` | Router |
|
||||||
|
| **SetupPage** | `useCreateOwner` | Router |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Communication Patterns
|
||||||
|
|
||||||
|
| Pattern | Where Used |
|
||||||
|
|---------|-----------|
|
||||||
|
| **React Context** | Auth state shared from `AuthContext` to guards, sidebar, profile, login page |
|
||||||
|
| **TanStack Query** | Server state (availability, users, invitation, setup) — cached, auto-refetch |
|
||||||
|
| **Cookie (httpOnly)** | Browser ↔ Backend: refresh token — set by backend, never read by JS |
|
||||||
|
| **In-memory state** | Access token held in `AuthContext` — cleared on page refresh (intentional) |
|
||||||
|
| **LocalStorage** | Theme preference only (`cms-theme`) — no auth data |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Dependency Rules (Security Baseline)
|
||||||
|
|
||||||
|
- `ApiClient` is the **only** module that makes HTTP calls — no direct `fetch` in pages/components
|
||||||
|
- `accessToken` is accessed **only** through `AuthContext` — never passed as prop or stored elsewhere
|
||||||
|
- `refreshToken` is **never accessible to JavaScript** — httpOnly cookie only
|
||||||
|
- Role checks happen in `RoleGuard` (routing) and `Sidebar` (UX) — backend is authoritative
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
# Component Methods — CMS Frontend
|
||||||
|
|
||||||
|
> **Note**: Detailed business rules and logic are defined per-unit in the Functional Design stage (CONSTRUCTION phase). This document captures method signatures, inputs, outputs, and high-level purpose only.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Backend — AuthController (updated)
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// POST /api/v1/auth/login
|
||||||
|
// Body: LoginRequest { Email, Password }
|
||||||
|
// Sets: Set-Cookie: refreshToken=<token>; HttpOnly; Secure; SameSite=Strict; Path=/api/v1/auth
|
||||||
|
// Returns: { accessToken: string, expiresAt: datetime, user: { id, email, name, role, isActive } }
|
||||||
|
Task<IActionResult> Login(LoginRequest request)
|
||||||
|
|
||||||
|
// POST /api/v1/auth/refresh
|
||||||
|
// Reads: Cookie: refreshToken
|
||||||
|
// Sets: Set-Cookie: refreshToken=<newToken>; HttpOnly; Secure; SameSite=Strict; Path=/api/v1/auth
|
||||||
|
// Returns: { accessToken: string, expiresAt: datetime, user: { id, email, name, role, isActive } }
|
||||||
|
Task<IActionResult> Refresh() // no body parameter — reads from cookie
|
||||||
|
|
||||||
|
// POST /api/v1/auth/revoke
|
||||||
|
// Requires: Authorization: Bearer <accessToken>
|
||||||
|
// Reads: Cookie: refreshToken
|
||||||
|
// Clears: Set-Cookie: refreshToken=; Expires=epoch; Path=/api/v1/auth
|
||||||
|
// Returns: 204 No Content
|
||||||
|
Task<IActionResult> Revoke() // no body parameter — reads from cookie
|
||||||
|
```
|
||||||
|
|
||||||
|
## Backend — ServiceCollectionExtensions (updated)
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Registers CORS policy "FrontendPolicy" using Cors:AllowedOrigins from configuration
|
||||||
|
// Called in AddCoreInfrastructure()
|
||||||
|
static void AddCorsFrontendPolicy(this IServiceCollection services, IConfiguration configuration)
|
||||||
|
|
||||||
|
// Adds app.UseCors("FrontendPolicy") to the pipeline
|
||||||
|
// Called in Program.cs after UseExceptionHandler, before UseAuthentication
|
||||||
|
static void UseFrontendCors(this WebApplication app)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Frontend — ApiClient (`frontend/src/api/client.ts`)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Base configured fetch wrapper; sets Authorization header and credentials: 'include'
|
||||||
|
// Retries once on 401 after calling authContext.refresh()
|
||||||
|
function createApiClient(getAccessToken: () => string | null, refresh: () => Promise<void>): ApiClient
|
||||||
|
|
||||||
|
// Typed GET request
|
||||||
|
async function get<T>(path: string, options?: RequestInit): Promise<T>
|
||||||
|
|
||||||
|
// Typed POST request
|
||||||
|
async function post<T>(path: string, body: unknown, options?: RequestInit): Promise<T>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Frontend — AuthContext (`frontend/src/auth/AuthContext.tsx`)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface AuthContextValue {
|
||||||
|
isAuthenticated: boolean
|
||||||
|
isLoading: boolean // true during initial session restore
|
||||||
|
user: User | null // { id, email, name, role, isActive }
|
||||||
|
accessToken: string | null // in-memory only, never in localStorage
|
||||||
|
|
||||||
|
login(email: string, password: string): Promise<void>
|
||||||
|
logout(): Promise<void>
|
||||||
|
refresh(): Promise<void> // called by ApiClient on 401; restores token from cookie
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hook
|
||||||
|
function useAuth(): AuthContextValue
|
||||||
|
```
|
||||||
|
|
||||||
|
## Frontend — Route Guards
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ProtectedRoute: renders children if authenticated, redirects to /login otherwise
|
||||||
|
// Used as a TanStack Router beforeLoad or as a wrapper component
|
||||||
|
function ProtectedRoute({ children }: { children: ReactNode }): JSX.Element
|
||||||
|
|
||||||
|
// RoleGuard: renders children if user.role is in allowedRoles, redirects to /403 otherwise
|
||||||
|
function RoleGuard({ allowedRoles, children }: { allowedRoles: string[], children: ReactNode }): JSX.Element
|
||||||
|
|
||||||
|
// InitGuard: checks GET /setup/status on app load; redirects to /setup if not initialized
|
||||||
|
function InitGuard({ children }: { children: ReactNode }): JSX.Element
|
||||||
|
```
|
||||||
|
|
||||||
|
## Frontend — TanStack Query hooks (`frontend/src/api/`)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Fetch current availability status; staleTime: 30s
|
||||||
|
function useAvailabilityStatus(): UseQueryResult<AvailabilityStatusResponse>
|
||||||
|
|
||||||
|
// Fetch user list; staleTime: 60s
|
||||||
|
function useUsers(): UseQueryResult<User[]>
|
||||||
|
|
||||||
|
// Invite a user; invalidates useUsers on success
|
||||||
|
function useInviteUser(): UseMutationResult<InviteResponse, Error, InviteUserRequest>
|
||||||
|
|
||||||
|
// Complete invitation setup (public, no auth required)
|
||||||
|
function useCompleteSetup(): UseMutationResult<void, Error, CompleteSetupRequest>
|
||||||
|
|
||||||
|
// Validate invitation token (public)
|
||||||
|
function useValidateInvitation(token: string): UseQueryResult<InvitationValidationResponse>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Frontend — Page component signatures
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// All page components are default exports used as TanStack Router route components
|
||||||
|
export default function LoginPage(): JSX.Element
|
||||||
|
export default function SetupPage(): JSX.Element
|
||||||
|
export default function InviteCompletePage(): JSX.Element // reads token from search params
|
||||||
|
export default function DashboardPage(): JSX.Element
|
||||||
|
export default function UsersPage(): JSX.Element
|
||||||
|
export default function ProfilePage(): JSX.Element
|
||||||
|
export default function SettingsPage(): JSX.Element
|
||||||
|
export default function CmsPage(): JSX.Element
|
||||||
|
export default function NotFoundPage(): JSX.Element
|
||||||
|
export default function AccessDeniedPage(): JSX.Element
|
||||||
|
```
|
||||||
|
|
||||||
|
## Frontend — Shared components
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Availability badge with colour-coding
|
||||||
|
function AvailabilityStatusBadge({ status, message }: {
|
||||||
|
status: 'Available' | 'Maintenance' | 'Unavailable'
|
||||||
|
message?: string
|
||||||
|
}): JSX.Element
|
||||||
|
|
||||||
|
// Two-step invite dialog
|
||||||
|
function InviteUserDialog({ open, onClose }: {
|
||||||
|
open: boolean
|
||||||
|
onClose: () => void
|
||||||
|
}): JSX.Element
|
||||||
|
|
||||||
|
// Error boundary — class component wrapping children
|
||||||
|
class ErrorBoundary extends React.Component<{
|
||||||
|
fallback?: ReactNode
|
||||||
|
children: ReactNode
|
||||||
|
}>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## TypeScript Type Definitions (`frontend/src/api/types.ts`)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface User {
|
||||||
|
id: string
|
||||||
|
email: string
|
||||||
|
name: string
|
||||||
|
role: 'Owner' | 'Admin' | 'User'
|
||||||
|
isActive: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AuthResponse {
|
||||||
|
accessToken: string
|
||||||
|
expiresAt: string // ISO datetime
|
||||||
|
user: User
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AvailabilityStatusResponse {
|
||||||
|
status: 'Available' | 'Maintenance' | 'Unavailable'
|
||||||
|
checkedAt: string // ISO datetime
|
||||||
|
message?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface InviteUserRequest {
|
||||||
|
email: string
|
||||||
|
role: 'Admin' | 'User'
|
||||||
|
}
|
||||||
|
|
||||||
|
interface InviteResponse {
|
||||||
|
inviteLink: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CompleteSetupRequest {
|
||||||
|
token: string
|
||||||
|
displayName: string
|
||||||
|
password: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface InvitationValidationResponse {
|
||||||
|
valid: boolean
|
||||||
|
email: string
|
||||||
|
role: string
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,196 @@
|
|||||||
|
# Component Definitions — CMS Frontend
|
||||||
|
|
||||||
|
## Backend Components (Unit 0)
|
||||||
|
|
||||||
|
### CorsPolicy
|
||||||
|
- **Purpose**: Configure Cross-Origin Resource Sharing for the frontend SPA
|
||||||
|
- **Responsibilities**:
|
||||||
|
- Allow the configured frontend origin(s) from `appsettings.json → AllowedOrigins`
|
||||||
|
- Allow credentials (`AllowCredentials()`) so the browser sends the httpOnly cookie
|
||||||
|
- Allow required HTTP methods and headers
|
||||||
|
- **Location**: `src/SlpModularCms.Api/Extensions/ServiceCollectionExtensions.cs` (extended)
|
||||||
|
- **Configuration**: `appsettings.json → Cors:AllowedOrigins[]`, `appsettings.Development.json` (dev origins), `appsettings.local.json` (developer override)
|
||||||
|
|
||||||
|
### AuthController (updated)
|
||||||
|
- **Purpose**: Authentication HTTP endpoints — updated to set/clear httpOnly cookie
|
||||||
|
- **Responsibilities**:
|
||||||
|
- `Login`: Call `IAuthService.AuthenticateAsync()`, set `refreshToken` httpOnly cookie, return access token + user info only in body
|
||||||
|
- `Refresh`: Read `refreshToken` from cookie (not request body), call `IAuthService.RefreshTokenAsync()`, set new cookie, return new access token
|
||||||
|
- `Revoke`: Call `IAuthService.RevokeTokenAsync()` with cookie value, clear the cookie
|
||||||
|
- **Location**: `src/SlpModularCms.Modules.Identity/Controllers/AuthController.cs`
|
||||||
|
|
||||||
|
### AuthService (updated)
|
||||||
|
- **Purpose**: Core authentication business logic — updated for cookie-based refresh flow
|
||||||
|
- **Responsibilities**:
|
||||||
|
- `RefreshTokenAsync(accessToken, refreshToken)` — signature unchanged; cookie reading is the controller's responsibility
|
||||||
|
- No other changes to service layer
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Frontend Components (Units 1–6)
|
||||||
|
|
||||||
|
### Infrastructure Layer
|
||||||
|
|
||||||
|
#### ApiClient
|
||||||
|
- **Purpose**: Centralised HTTP client — all API calls go through this module
|
||||||
|
- **Responsibilities**:
|
||||||
|
- Base URL from `import.meta.env.VITE_API_BASE_URL`
|
||||||
|
- Attach `Authorization: Bearer {accessToken}` header on authenticated requests
|
||||||
|
- Intercept 401 responses — trigger token refresh via `AuthContext.refresh()`, retry original request once
|
||||||
|
- Expose typed request functions used by TanStack Query
|
||||||
|
- Send requests with `credentials: 'include'` so the browser includes the httpOnly cookie
|
||||||
|
- **Location**: `frontend/src/api/client.ts`
|
||||||
|
|
||||||
|
#### AuthContext
|
||||||
|
- **Purpose**: React Context providing authentication state to the entire app
|
||||||
|
- **Responsibilities**:
|
||||||
|
- Store in-memory `accessToken` (string | null) — never persisted to localStorage
|
||||||
|
- Store authenticated user: `{ id, email, name, role, isActive }`
|
||||||
|
- Expose `login(email, password)`, `logout()`, `refresh()` methods
|
||||||
|
- On app mount: call `/auth/refresh` via httpOnly cookie to restore session
|
||||||
|
- Provide `isAuthenticated`, `isLoading`, `user`, `role` derived state
|
||||||
|
- **Location**: `frontend/src/auth/AuthContext.tsx` + `frontend/src/auth/useAuth.ts`
|
||||||
|
|
||||||
|
#### RouterConfig
|
||||||
|
- **Purpose**: TanStack Router file-based route tree with integrated guards
|
||||||
|
- **Responsibilities**:
|
||||||
|
- File-based route structure under `frontend/src/routes/`
|
||||||
|
- Root route checks `InitGuard` (setup status) before rendering any child
|
||||||
|
- Authenticated routes wrapped with `ProtectedRoute` check
|
||||||
|
- Role-specific routes wrapped with `RoleGuard` check
|
||||||
|
- Redirect to `/login` with `?redirect=` param on auth failure
|
||||||
|
- **Location**: `frontend/src/routes/` (file-based), `frontend/src/routeTree.gen.ts` (generated)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Auth & Guard Components
|
||||||
|
|
||||||
|
#### ProtectedRoute
|
||||||
|
- **Purpose**: Redirect unauthenticated users to `/login`
|
||||||
|
- **Responsibilities**:
|
||||||
|
- Read `isAuthenticated` from `AuthContext`
|
||||||
|
- If not authenticated: redirect to `/login?redirect={currentPath}`
|
||||||
|
- If loading (session restore in progress): show loading spinner — no flash of content
|
||||||
|
- If authenticated: render child route
|
||||||
|
- **Location**: `frontend/src/auth/ProtectedRoute.tsx`
|
||||||
|
|
||||||
|
#### RoleGuard
|
||||||
|
- **Purpose**: Restrict routes to specific roles
|
||||||
|
- **Responsibilities**:
|
||||||
|
- Accept `allowedRoles: string[]` prop
|
||||||
|
- Read `user.role` from `AuthContext`
|
||||||
|
- If role not in `allowedRoles`: redirect to `/403`
|
||||||
|
- If role allowed: render child route
|
||||||
|
- **Location**: `frontend/src/auth/RoleGuard.tsx`
|
||||||
|
|
||||||
|
#### InitGuard
|
||||||
|
- **Purpose**: Redirect to `/setup` if the system is not initialized
|
||||||
|
- **Responsibilities**:
|
||||||
|
- On first app render: call `GET /setup/status`
|
||||||
|
- If `initialized: false`: redirect all routes to `/setup`
|
||||||
|
- If `initialized: true`: allow normal routing
|
||||||
|
- Show loading state during check
|
||||||
|
- Handle API failure with error page + retry
|
||||||
|
- **Location**: `frontend/src/auth/InitGuard.tsx`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Layout Components
|
||||||
|
|
||||||
|
#### AppLayout
|
||||||
|
- **Purpose**: Authenticated shell layout wrapping all protected pages
|
||||||
|
- **Responsibilities**:
|
||||||
|
- Render `Sidebar` on the left + `<Outlet />` (page content) on the right
|
||||||
|
- Responsive: full sidebar on desktop, collapsed/hamburger on mobile
|
||||||
|
- **Location**: `frontend/src/components/layout/AppLayout.tsx`
|
||||||
|
|
||||||
|
#### Sidebar
|
||||||
|
- **Purpose**: Navigation sidebar with role-filtered links
|
||||||
|
- **Responsibilities**:
|
||||||
|
- Read `user.role` from `AuthContext` to filter navigation items
|
||||||
|
- Navigation items: Dashboard (all), User Management (Owner/Admin), System Settings (Owner), CMS (Owner), Profile (all)
|
||||||
|
- Logout button: calls `AuthContext.logout()`
|
||||||
|
- Theme toggle: calls `ThemeProvider.toggle()`
|
||||||
|
- Highlight active route
|
||||||
|
- Collapsible on mobile
|
||||||
|
- **Location**: `frontend/src/components/layout/Sidebar.tsx`
|
||||||
|
|
||||||
|
#### ThemeProvider
|
||||||
|
- **Purpose**: Dark/light theme management
|
||||||
|
- **Responsibilities**:
|
||||||
|
- Wrap app with `next-themes` provider
|
||||||
|
- Persist theme preference in `localStorage` under key `cms-theme`
|
||||||
|
- Default to system preference
|
||||||
|
- Expose `useTheme()` hook via next-themes
|
||||||
|
- **Location**: `frontend/src/components/layout/ThemeProvider.tsx`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Page Components
|
||||||
|
|
||||||
|
#### LoginPage (`/login`)
|
||||||
|
- **Purpose**: Unauthenticated login form
|
||||||
|
- **Responsibilities**: Email + password form, react-hook-form + zod validation, calls `AuthContext.login()`
|
||||||
|
- **Location**: `frontend/src/routes/login.tsx`
|
||||||
|
|
||||||
|
#### SetupPage (`/setup`)
|
||||||
|
- **Purpose**: First-time system initialization form
|
||||||
|
- **Responsibilities**: Email + password form (owner creation), calls `POST /setup/owner`, redirects to `/login` on success
|
||||||
|
- **Location**: `frontend/src/routes/setup.tsx`
|
||||||
|
|
||||||
|
#### InviteCompletePage (`/invite/complete`)
|
||||||
|
- **Purpose**: Invitation token completion form
|
||||||
|
- **Responsibilities**: Validate token via `GET /users/validate-invitation`, show display name + password form, call `POST /users/complete-setup`
|
||||||
|
- **Location**: `frontend/src/routes/invite.complete.tsx`
|
||||||
|
|
||||||
|
#### DashboardPage (`/`)
|
||||||
|
- **Purpose**: Authenticated home page
|
||||||
|
- **Responsibilities**: Welcome widget (user name + role), availability status widget (TanStack Query)
|
||||||
|
- **Location**: `frontend/src/routes/index.tsx`
|
||||||
|
|
||||||
|
#### UsersPage (`/users`)
|
||||||
|
- **Purpose**: User management page (Owner/Admin only)
|
||||||
|
- **Responsibilities**: User list (TanStack Query), InviteUserDialog (invite form + share link step)
|
||||||
|
- **Location**: `frontend/src/routes/users.tsx`
|
||||||
|
|
||||||
|
#### ProfilePage (`/profile`)
|
||||||
|
- **Purpose**: Read-only profile page
|
||||||
|
- **Responsibilities**: Show `user.name`, `user.email`, `user.role` from `AuthContext`
|
||||||
|
- **Location**: `frontend/src/routes/profile.tsx`
|
||||||
|
|
||||||
|
#### SettingsPage (`/settings`)
|
||||||
|
- **Purpose**: System settings page (Owner only)
|
||||||
|
- **Responsibilities**: Show availability status (TanStack Query), show initialized state, placeholder sections
|
||||||
|
- **Location**: `frontend/src/routes/settings.tsx`
|
||||||
|
|
||||||
|
#### CmsPage (`/cms`)
|
||||||
|
- **Purpose**: CMS management placeholder (Owner only)
|
||||||
|
- **Responsibilities**: Work-in-progress placeholder with description of future multi-CMS management
|
||||||
|
- **Location**: `frontend/src/routes/cms.tsx`
|
||||||
|
|
||||||
|
#### NotFoundPage (`/404` or catch-all)
|
||||||
|
- **Purpose**: 404 error page
|
||||||
|
- **Location**: `frontend/src/routes/$404.tsx`
|
||||||
|
|
||||||
|
#### AccessDeniedPage (`/403`)
|
||||||
|
- **Purpose**: 403 forbidden page shown when role guard fails
|
||||||
|
- **Location**: `frontend/src/routes/403.tsx`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Shared UI Components
|
||||||
|
|
||||||
|
#### InviteUserDialog
|
||||||
|
- **Purpose**: Modal dialog for inviting a new user
|
||||||
|
- **Responsibilities**: Two-step: (1) invite form with email + role, (2) share invite link with copy button
|
||||||
|
- **Location**: `frontend/src/components/users/InviteUserDialog.tsx`
|
||||||
|
|
||||||
|
#### AvailabilityStatusBadge
|
||||||
|
- **Purpose**: Colour-coded availability status indicator
|
||||||
|
- **Responsibilities**: Display Available (green) / Maintenance (yellow) / Unavailable (red) with reason
|
||||||
|
- **Location**: `frontend/src/components/shared/AvailabilityStatusBadge.tsx`
|
||||||
|
|
||||||
|
#### ErrorBoundary (Global + Per-Page)
|
||||||
|
- **Purpose**: Catch unexpected React errors
|
||||||
|
- **Responsibilities**: Global boundary at app root; per-page boundaries for isolated page failures; show user-friendly fallback UI
|
||||||
|
- **Location**: `frontend/src/components/error/ErrorBoundary.tsx`
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
# Services — CMS Frontend
|
||||||
|
|
||||||
|
## Service Architecture Overview
|
||||||
|
|
||||||
|
The frontend uses **TanStack Query** as the server-state service layer. All API interactions are encapsulated in typed query/mutation hooks. The `ApiClient` handles transport concerns (headers, 401 retry). `AuthContext` manages session state. There is no additional service abstraction layer needed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Backend Services (Unit 0 changes)
|
||||||
|
|
||||||
|
### CORS Service (new)
|
||||||
|
- **Service**: `ServiceCollectionExtensions.AddCorsFrontendPolicy()`
|
||||||
|
- **Purpose**: Register named CORS policy `"FrontendPolicy"` for frontend SPA
|
||||||
|
- **Configuration source**: `IConfiguration` → `Cors:AllowedOrigins` array
|
||||||
|
- **Key settings**:
|
||||||
|
- `WithOrigins(allowedOrigins)` — from config
|
||||||
|
- `AllowAnyHeader()` + `AllowAnyMethod()`
|
||||||
|
- `AllowCredentials()` — required for httpOnly cookie
|
||||||
|
- **dotnet-appsettings pattern**: `AllowedOrigins` stored in `appsettings.json` (production) and `appsettings.Development.json` (dev, e.g. `["http://localhost:5173"]`); `appsettings.local.json` for developer-specific overrides
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Frontend Services (TanStack Query hooks)
|
||||||
|
|
||||||
|
### Auth Service (`frontend/src/auth/`)
|
||||||
|
|
||||||
|
**AuthContext** functions as the auth service — it is the single source of truth for authentication state.
|
||||||
|
|
||||||
|
| Operation | Implementation | Notes |
|
||||||
|
|-----------|----------------|-------|
|
||||||
|
| Login | `POST /api/v1/auth/login` → stores `accessToken` in memory | httpOnly cookie set by server |
|
||||||
|
| Session restore | `POST /api/v1/auth/refresh` on app mount → stores new `accessToken` | Reads cookie automatically |
|
||||||
|
| Logout | `POST /api/v1/auth/revoke` → clears in-memory token | Server clears cookie |
|
||||||
|
| Token refresh (interceptor) | `POST /api/v1/auth/refresh` on 401 → retry original request | Centralised in ApiClient |
|
||||||
|
|
||||||
|
### Availability Service (`frontend/src/api/useAvailability.ts`)
|
||||||
|
|
||||||
|
| Hook | Query key | Endpoint | Stale time |
|
||||||
|
|------|-----------|----------|-----------|
|
||||||
|
| `useAvailabilityStatus()` | `['availability', 'status']` | `GET /api/v1/availability/status` | 30s |
|
||||||
|
|
||||||
|
### Users Service (`frontend/src/api/useUsers.ts`)
|
||||||
|
|
||||||
|
| Hook | Query key | Endpoint | Notes |
|
||||||
|
|------|-----------|----------|-------|
|
||||||
|
| `useUsers()` | `['users']` | `GET /api/v1/users` | Owner/Admin only |
|
||||||
|
| `useInviteUser()` | mutation | `POST /api/v1/users/invite` | Invalidates `['users']` on success |
|
||||||
|
|
||||||
|
### Invitation Service (`frontend/src/api/useInvitation.ts`)
|
||||||
|
|
||||||
|
| Hook | Query key | Endpoint | Notes |
|
||||||
|
|------|-----------|----------|-------|
|
||||||
|
| `useValidateInvitation(token)` | `['invitation', token]` | `GET /api/v1/users/validate-invitation?token=` | Public, no auth |
|
||||||
|
| `useCompleteSetup()` | mutation | `POST /api/v1/users/complete-setup` | Public, no auth |
|
||||||
|
|
||||||
|
### Setup Service (`frontend/src/api/useSetup.ts`)
|
||||||
|
|
||||||
|
| Hook | Query key | Endpoint | Notes |
|
||||||
|
|------|-----------|----------|-------|
|
||||||
|
| `useSetupStatus()` | `['setup', 'status']` | `GET /api/v1/setup/status` | Used by `InitGuard` |
|
||||||
|
| `useCreateOwner()` | mutation | `POST /api/v1/setup/owner` | Used on `/setup` page |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Service Interaction Diagram
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
App["App Entry\n(main.tsx)"]
|
||||||
|
InitGuard["InitGuard\n(useSetupStatus)"]
|
||||||
|
AuthCtx["AuthContext\n(session state)"]
|
||||||
|
ApiClient["ApiClient\n(fetch + 401 retry)"]
|
||||||
|
Router["TanStack Router\n(ProtectedRoute + RoleGuard)"]
|
||||||
|
|
||||||
|
subgraph Pages["Page Components"]
|
||||||
|
Login["LoginPage"]
|
||||||
|
Dashboard["DashboardPage\n(useAvailabilityStatus)"]
|
||||||
|
Users["UsersPage\n(useUsers, useInviteUser)"]
|
||||||
|
Invite["InviteCompletePage\n(useValidateInvitation\nuseCompleteSetup)"]
|
||||||
|
Settings["SettingsPage\n(useAvailabilityStatus)"]
|
||||||
|
Profile["ProfilePage"]
|
||||||
|
end
|
||||||
|
|
||||||
|
App --> InitGuard
|
||||||
|
App --> AuthCtx
|
||||||
|
InitGuard -->|initialized| Router
|
||||||
|
Router --> Login
|
||||||
|
Router --> Dashboard
|
||||||
|
Router --> Users
|
||||||
|
Router --> Invite
|
||||||
|
Router --> Settings
|
||||||
|
Router --> Profile
|
||||||
|
|
||||||
|
AuthCtx -->|getAccessToken| ApiClient
|
||||||
|
AuthCtx -->|refresh| ApiClient
|
||||||
|
Dashboard --> ApiClient
|
||||||
|
Users --> ApiClient
|
||||||
|
Invite --> ApiClient
|
||||||
|
Settings --> ApiClient
|
||||||
|
|
||||||
|
style App fill:#CE93D8,stroke:#6A1B9A,color:#000
|
||||||
|
style InitGuard fill:#FFC107,stroke:#F57F17,color:#000
|
||||||
|
style AuthCtx fill:#FFC107,stroke:#F57F17,color:#000
|
||||||
|
style ApiClient fill:#FFC107,stroke:#F57F17,color:#000
|
||||||
|
style Router fill:#4CAF50,stroke:#2E7D32,color:#fff
|
||||||
|
style Login fill:#2196F3,stroke:#0D47A1,color:#fff
|
||||||
|
style Dashboard fill:#2196F3,stroke:#0D47A1,color:#fff
|
||||||
|
style Users fill:#2196F3,stroke:#0D47A1,color:#fff
|
||||||
|
style Invite fill:#2196F3,stroke:#0D47A1,color:#fff
|
||||||
|
style Settings fill:#2196F3,stroke:#0D47A1,color:#fff
|
||||||
|
style Profile fill:#2196F3,stroke:#0D47A1,color:#fff
|
||||||
|
```
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
# Application Design Plan — CMS Frontend
|
||||||
|
|
||||||
|
## Design Scope
|
||||||
|
This plan covers the high-level component architecture for:
|
||||||
|
1. **Unit 0** — Backend prerequisites (CORS + httpOnly cookie — .NET changes to `SlpModularCms.Api`)
|
||||||
|
2. **Units 1–6** — React SPA frontend
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Design Checklist
|
||||||
|
- [x] Context analyzed (requirements.md + stories.md + backend inspection)
|
||||||
|
- [x] Questions generated
|
||||||
|
- [x] Questions answered
|
||||||
|
- [x] components.md generated
|
||||||
|
- [x] component-methods.md generated
|
||||||
|
- [x] services.md generated
|
||||||
|
- [x] component-dependency.md generated
|
||||||
|
- [x] application-design.md (consolidated) generated
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Identified Components (preliminary — to be validated by answers below)
|
||||||
|
|
||||||
|
### Backend (Unit 0)
|
||||||
|
- `CorsConfiguration` — CORS policy setup in `ServiceCollectionExtensions`
|
||||||
|
- `AuthController` (update) — Add `Set-Cookie` on login/refresh/revoke
|
||||||
|
- `AuthService` (update) — Read refresh token from cookie in `RefreshTokenAsync`
|
||||||
|
|
||||||
|
### Frontend (Units 1–6)
|
||||||
|
**Infrastructure layer**
|
||||||
|
- `ApiClient` — Centralised HTTP client with baseURL, auth headers, 401 interceptor
|
||||||
|
- `AuthContext` — React context holding in-memory access token + user info + session state
|
||||||
|
- `RouterConfig` — TanStack Router route tree with guards
|
||||||
|
|
||||||
|
**Auth & Guard components**
|
||||||
|
- `ProtectedRoute` — Redirects unauthenticated users to `/login`
|
||||||
|
- `RoleGuard` — Redirects users to access-denied if role insufficient
|
||||||
|
- `InitGuard` — Checks `/setup/status` and redirects to `/setup` if uninitialized
|
||||||
|
|
||||||
|
**Layout components**
|
||||||
|
- `AppLayout` — Authenticated shell with sidebar + main content area
|
||||||
|
- `Sidebar` — Role-filtered navigation links + logout + theme toggle
|
||||||
|
- `ThemeProvider` — next-themes wrapper for dark/light mode
|
||||||
|
|
||||||
|
**Page components**
|
||||||
|
- `LoginPage`, `SetupPage`, `InviteCompletePage` — Public/unauthenticated pages
|
||||||
|
- `DashboardPage` — Availability widget + welcome
|
||||||
|
- `UsersPage` — User list + invite dialog + share link dialog
|
||||||
|
- `ProfilePage` — Read-only user info
|
||||||
|
- `SettingsPage` — Owner-only system info
|
||||||
|
- `CmsPage` — Owner-only placeholder
|
||||||
|
- `NotFoundPage`, `AccessDeniedPage` — Error pages
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Clarification Questions
|
||||||
|
|
||||||
|
Please answer the following questions by filling in the letter choice after the `[Answer]:` tag.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Question 1: State management approach
|
||||||
|
How should global application state (auth token, user info, availability status) be managed?
|
||||||
|
|
||||||
|
A) React Context API only — simple, no extra dependencies, sufficient for this app size
|
||||||
|
B) React Context API + Zustand — Context for auth, Zustand for other shared state (e.g. availability)
|
||||||
|
C) React Query / TanStack Query for all server state + Context for auth only
|
||||||
|
D) React Context API + TanStack Query for server state (API calls with caching)
|
||||||
|
X) Other (please describe after [Answer]: tag below)
|
||||||
|
|
||||||
|
[Answer]: D
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Question 2: API client library
|
||||||
|
Which library should be used for HTTP calls to the backend?
|
||||||
|
|
||||||
|
A) Native `fetch` API with a custom wrapper (no extra dependency)
|
||||||
|
B) Axios — popular HTTP client with interceptors, request cancellation
|
||||||
|
C) TanStack Query (React Query) — server state management + caching built-in
|
||||||
|
D) ky — modern fetch-based HTTP client, lightweight
|
||||||
|
X) Other (please describe after [Answer]: tag below)
|
||||||
|
|
||||||
|
[Answer]: C
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Question 3: Form validation library
|
||||||
|
Which library should handle form state and validation (already mentioned react-hook-form in NFR-01, but confirm)?
|
||||||
|
|
||||||
|
A) react-hook-form with zod schema validation — standard modern choice
|
||||||
|
B) react-hook-form with yup schema validation
|
||||||
|
C) react-hook-form only (no schema library — manual validation)
|
||||||
|
X) Other (please describe after [Answer]: tag below)
|
||||||
|
|
||||||
|
[Answer]: A
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Question 4: TanStack Router approach
|
||||||
|
Which routing style should be used with TanStack Router?
|
||||||
|
|
||||||
|
A) Code-based routing — define routes as objects in a central `routes.ts` file
|
||||||
|
B) File-based routing — file structure in `src/routes/` maps to URL structure (TanStack Router convention)
|
||||||
|
X) Other (please describe after [Answer]: tag below)
|
||||||
|
|
||||||
|
[Answer]: B
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Question 5: Backend — refresh token cookie name and path
|
||||||
|
What name and path should the httpOnly cookie use for the refresh token?
|
||||||
|
|
||||||
|
A) `refreshToken` with path `/api/v1/auth` — scoped to the auth endpoints only (more secure)
|
||||||
|
B) `refreshToken` with path `/` — available for all paths
|
||||||
|
C) `cms_refresh_token` with path `/api/v1/auth`
|
||||||
|
X) Other (please describe after [Answer]: tag below)
|
||||||
|
|
||||||
|
[Answer]: A
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Question 6: CORS allowed origins configuration
|
||||||
|
How should the CORS allowed origins be configured in the backend?
|
||||||
|
|
||||||
|
A) Via appsettings.json — e.g. `"AllowedOrigins": ["http://localhost:5173"]` configurable per environment
|
||||||
|
B) Hardcoded in `ServiceCollectionExtensions.cs` for development only
|
||||||
|
C) Via environment variable `CORS_ALLOWED_ORIGINS` read at startup
|
||||||
|
X) Other (please describe after [Answer]: tag below)
|
||||||
|
|
||||||
|
[Answer]: A, let the dotnet-appsettings skill help you manage these settings if needed
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Question 7: User data in auth context
|
||||||
|
What user data should be stored in the auth context after login?
|
||||||
|
|
||||||
|
A) Only `userId`, `email`, `role` (minimum needed for routing/guards)
|
||||||
|
B) Full user object: `id`, `email`, `naam`, `role`, `isActive`
|
||||||
|
C) JWT claims only — extract `userId` and `role` directly from the decoded token
|
||||||
|
X) Other (please describe after [Answer]: tag below)
|
||||||
|
|
||||||
|
[Answer]: B
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Question 8: Error boundary scope
|
||||||
|
Where should React Error Boundaries be placed?
|
||||||
|
|
||||||
|
A) One global error boundary at the app root only
|
||||||
|
B) Global error boundary + per-page boundaries for isolated page failures
|
||||||
|
C) Global error boundary + per-widget boundaries (e.g. availability widget on dashboard)
|
||||||
|
X) Other (please describe after [Answer]: tag below)
|
||||||
|
|
||||||
|
[Answer]: B
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
# Execution Plan — CMS Frontend
|
||||||
|
|
||||||
|
## Detailed Analysis Summary
|
||||||
|
|
||||||
|
### Transformation Scope
|
||||||
|
- **Transformation Type**: New greenfield frontend project within a brownfield workspace
|
||||||
|
- **Primary Changes**: New React SPA (`frontend/`) with auth layer, routing, API client, 8+ pages, role-based access
|
||||||
|
- **Related Components**: SlpModularCms.Api (REST consumer), existing example app (design reference only)
|
||||||
|
|
||||||
|
### Change Impact Assessment
|
||||||
|
- **User-facing changes**: Yes — entire new admin interface
|
||||||
|
- **Structural changes**: Yes — new `frontend/` project added to workspace root
|
||||||
|
- **Data model changes**: No — frontend consumes existing API contracts
|
||||||
|
- **API changes**: Yes — backend requires CORS configuration and httpOnly cookie support for refresh token (Unit 0, blocking)
|
||||||
|
- **NFR impact**: Yes — Security Baseline enabled; auth token strategy, input validation, HTTP headers all addressed
|
||||||
|
|
||||||
|
### Risk Assessment
|
||||||
|
- **Risk Level**: High → Mitigated to Medium after backend fixes
|
||||||
|
- **Rollback Complexity**: Moderate — backend changes (CORS, httpOnly cookie) must be coordinated with frontend
|
||||||
|
- **Testing Complexity**: Moderate — auth flows, role guards, invitation token states require careful testing
|
||||||
|
|
||||||
|
### Backend Blocking Issues Found (pre-flight inspection)
|
||||||
|
1. **🔴 No CORS configuration** — `Program.cs` has no `app.UseCors()` or `builder.Services.AddCors()`. The frontend SPA (different origin) cannot make any API calls without a CORS error.
|
||||||
|
2. **🟠 Refresh token via JSON body** — `AuthService` returns the refresh token in the response body and the `/auth/refresh` endpoint reads it from the request body. No httpOnly cookie support. This conflicts with NFR-04 (Security Baseline SECURITY-12).
|
||||||
|
3. **🟡 JWT expiry: 60 minutes** — Acceptable; no action needed.
|
||||||
|
|
||||||
|
**Resolution**: Backend must be updated (Unit 0) before frontend development begins.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Workflow Visualization
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
Start(["User Request"])
|
||||||
|
|
||||||
|
subgraph INCEPTION["🔵 INCEPTION PHASE"]
|
||||||
|
WD["Workspace Detection\n✅ COMPLETED"]
|
||||||
|
RE["Reverse Engineering\n✅ COMPLETED"]
|
||||||
|
RA["Requirements Analysis\n✅ COMPLETED"]
|
||||||
|
US["User Stories\n✅ COMPLETED"]
|
||||||
|
WP["Workflow Planning\n⚙️ IN PROGRESS"]
|
||||||
|
AD["Application Design\n▶️ EXECUTE"]
|
||||||
|
UG["Units Generation\n▶️ EXECUTE"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph CONSTRUCTION["🟢 CONSTRUCTION PHASE"]
|
||||||
|
FD["Functional Design\n▶️ EXECUTE (per unit)"]
|
||||||
|
NFRA["NFR Requirements\n▶️ EXECUTE (Unit 1 only)"]
|
||||||
|
NFRD["NFR Design\n▶️ EXECUTE (Unit 1 only)"]
|
||||||
|
ID["Infrastructure Design\n⏭️ SKIP"]
|
||||||
|
CG["Code Generation\n▶️ EXECUTE (per unit)"]
|
||||||
|
BT["Build and Test\n▶️ EXECUTE"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph OPERATIONS["🟡 OPERATIONS PHASE"]
|
||||||
|
OPS["Operations\n⏸️ PLACEHOLDER"]
|
||||||
|
end
|
||||||
|
|
||||||
|
Start --> WD --> RE --> RA --> US --> WP
|
||||||
|
WP --> AD --> UG
|
||||||
|
UG --> FD --> NFRA --> NFRD --> CG
|
||||||
|
NFRD -.->|skip infra| CG
|
||||||
|
ID -.->|skipped| CG
|
||||||
|
CG -->|next unit| FD
|
||||||
|
CG --> BT --> OPS --> End(["Complete"])
|
||||||
|
|
||||||
|
style WD fill:#4CAF50,stroke:#1B5E20,stroke-width:3px,color:#fff
|
||||||
|
style RE fill:#4CAF50,stroke:#1B5E20,stroke-width:3px,color:#fff
|
||||||
|
style RA fill:#4CAF50,stroke:#1B5E20,stroke-width:3px,color:#fff
|
||||||
|
style US fill:#4CAF50,stroke:#1B5E20,stroke-width:3px,color:#fff
|
||||||
|
style WP fill:#FFA726,stroke:#E65100,stroke-width:3px,color:#000
|
||||||
|
style AD fill:#FFA726,stroke:#E65100,stroke-width:3px,stroke-dasharray:5 5,color:#000
|
||||||
|
style UG fill:#FFA726,stroke:#E65100,stroke-width:3px,stroke-dasharray:5 5,color:#000
|
||||||
|
style FD fill:#FFA726,stroke:#E65100,stroke-width:3px,stroke-dasharray:5 5,color:#000
|
||||||
|
style NFRA fill:#FFA726,stroke:#E65100,stroke-width:3px,stroke-dasharray:5 5,color:#000
|
||||||
|
style NFRD fill:#FFA726,stroke:#E65100,stroke-width:3px,stroke-dasharray:5 5,color:#000
|
||||||
|
style ID fill:#BDBDBD,stroke:#424242,stroke-width:2px,stroke-dasharray:5 5,color:#000
|
||||||
|
style CG fill:#4CAF50,stroke:#1B5E20,stroke-width:3px,color:#fff
|
||||||
|
style BT fill:#4CAF50,stroke:#1B5E20,stroke-width:3px,color:#fff
|
||||||
|
style OPS fill:#FFF9C4,stroke:#F57F17,stroke-width:2px,stroke-dasharray:5 5,color:#000
|
||||||
|
style Start fill:#CE93D8,stroke:#6A1B9A,stroke-width:3px,color:#000
|
||||||
|
style End fill:#CE93D8,stroke:#6A1B9A,stroke-width:3px,color:#000
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phases to Execute
|
||||||
|
|
||||||
|
### 🔵 INCEPTION PHASE
|
||||||
|
|
||||||
|
- [x] Workspace Detection — COMPLETED
|
||||||
|
- [x] Reverse Engineering (shared) — COMPLETED
|
||||||
|
- [x] Requirements Analysis — COMPLETED
|
||||||
|
- [x] User Stories — COMPLETED
|
||||||
|
- [~] Workflow Planning — IN PROGRESS
|
||||||
|
- [ ] **Application Design — EXECUTE**
|
||||||
|
- **Rationale**: New project with multiple layers (auth context, API client, router, role guards, pages). Component responsibilities and service boundaries need to be defined before code generation.
|
||||||
|
- [ ] **Units Generation — EXECUTE**
|
||||||
|
- **Rationale**: The frontend consists of 6 logical units (scaffold, auth pages, layout, dashboard, user management, remaining pages) that benefit from being designed and implemented one at a time.
|
||||||
|
|
||||||
|
### 🟢 CONSTRUCTION PHASE (per unit)
|
||||||
|
|
||||||
|
- [ ] **Functional Design — EXECUTE (for each unit)**
|
||||||
|
- **Rationale**: Each unit has business logic (auth flows, token refresh, role guards, invitation token validation) that needs to be designed before coding.
|
||||||
|
- [ ] **NFR Requirements — EXECUTE (Unit 1 — Project Scaffold only)**
|
||||||
|
- **Rationale**: Tech stack and security patterns are set in Unit 1. Subsequent units inherit these decisions; no need to re-evaluate NFRs per unit.
|
||||||
|
- [ ] **NFR Design — EXECUTE (Unit 1 only)**
|
||||||
|
- **Rationale**: Auth token storage pattern, API client interceptor, and error boundary design are foundational and should be explicitly designed once.
|
||||||
|
- [ ] **Infrastructure Design — SKIP**
|
||||||
|
- **Rationale**: No cloud infrastructure resources to define. The frontend is a static SPA served from a web server or CDN. Deployment instructions go in README.md per NFR-05.
|
||||||
|
- [ ] **Code Generation — EXECUTE (per unit, always)**
|
||||||
|
- **Rationale**: Implementation of each unit.
|
||||||
|
- [ ] **Build and Test — EXECUTE**
|
||||||
|
- **Rationale**: Build, type-check, lint, and verify all units work together.
|
||||||
|
|
||||||
|
### 🟡 OPERATIONS PHASE
|
||||||
|
- [ ] Operations — PLACEHOLDER (future deployment/monitoring workflows)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Proposed Unit Decomposition
|
||||||
|
|
||||||
|
| Unit | Name | Key Deliverables |
|
||||||
|
|------|------|-----------------|
|
||||||
|
| **Unit 0** | **Backend Prerequisites** | CORS policy (allow frontend origin, credentials), httpOnly cookie for refresh token (Set-Cookie on login/refresh, clear on revoke), update `/auth/refresh` to read token from cookie |
|
||||||
|
| Unit 1 | Project Scaffold & Infrastructure | Vite + React + TypeScript project init, TanStack Router setup, shadcn/ui + Tailwind v4, auth context (in-memory access token), API client with interceptor, `.env` config |
|
||||||
|
| Unit 2 | Authentication Pages | Login page, Setup (initialization) page, Invite Complete page, token refresh on 401 |
|
||||||
|
| Unit 3 | Layout & Navigation | Authenticated layout shell, role-based sidebar, theme toggle (dark/light), ProtectedRoute + RoleGuard |
|
||||||
|
| Unit 4 | Dashboard | Dashboard page with welcome widget and availability status indicator |
|
||||||
|
| Unit 5 | User Management | Users list page, Invite user dialog, share invite link step |
|
||||||
|
| Unit 6 | Profile, System Settings & CMS Placeholder | Profile page, System Settings page (Owner only), CMS placeholder page (Owner only), 404/403 pages, README.md frontend section |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
- **Primary Goal**: Fully functional CMS admin SPA that connects to the existing .NET API
|
||||||
|
- **Key Deliverables**: All 20 user stories implemented, INVEST-compliant acceptance criteria met
|
||||||
|
- **Quality Gates**:
|
||||||
|
- TypeScript compiles without errors
|
||||||
|
- All routes protected with correct role guards
|
||||||
|
- Auth flow (login → refresh → logout) works end-to-end
|
||||||
|
- Password validation matches backend rules
|
||||||
|
- Security baseline compliance maintained
|
||||||
|
- README.md frontend section complete
|
||||||
@@ -34,6 +34,7 @@
|
|||||||
- If the user is not authenticated and tries to access a protected route, redirect to `/login`
|
- If the user is not authenticated and tries to access a protected route, redirect to `/login`
|
||||||
- Role-based guards MUST be applied per page:
|
- Role-based guards MUST be applied per page:
|
||||||
- System Settings page: Owner only
|
- System Settings page: Owner only
|
||||||
|
- CMS Management page: Owner only
|
||||||
- User Management page: Owner and Admin
|
- User Management page: Owner and Admin
|
||||||
- Profile page: Any authenticated user
|
- Profile page: Any authenticated user
|
||||||
- Dashboard: Any authenticated user
|
- Dashboard: Any authenticated user
|
||||||
@@ -75,6 +76,7 @@
|
|||||||
- Dashboard: all authenticated users
|
- Dashboard: all authenticated users
|
||||||
- User Management: Owner and Admin
|
- User Management: Owner and Admin
|
||||||
- System Settings: Owner only
|
- System Settings: Owner only
|
||||||
|
- CMS Management: Owner only (multi-CMS management feature; restricted from the start even though it is a placeholder in v1)
|
||||||
- Profile: all authenticated users
|
- Profile: all authenticated users
|
||||||
- Logout button: all authenticated users
|
- Logout button: all authenticated users
|
||||||
- Mobile-responsive: sidebar collapses on small screens
|
- Mobile-responsive: sidebar collapses on small screens
|
||||||
@@ -85,9 +87,10 @@
|
|||||||
- Default: system preference
|
- Default: system preference
|
||||||
|
|
||||||
### FR-12: CMS Management (Placeholder)
|
### FR-12: CMS Management (Placeholder)
|
||||||
- Page at `/cms` (structure ready, no real data)
|
- Page at `/cms` accessible to **Owner only**
|
||||||
- Shows navigation structure for future CMS content modules
|
- Non-Owner roles attempting to access `/cms` are redirected to an "Access Denied" page or back to `/`
|
||||||
- Clearly marked as "Work in Progress" with a placeholder message
|
- Shows a "Work in Progress" placeholder describing that this section will allow managing multiple client CMS instances
|
||||||
|
- CMS Management is NOT shown in the sidebar for Admin or User roles
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -140,7 +143,14 @@
|
|||||||
|
|
||||||
### NFR-06: Input Validation (SECURITY-05)
|
### NFR-06: Input Validation (SECURITY-05)
|
||||||
- All form inputs are validated client-side using react-hook-form before submission
|
- All form inputs are validated client-side using react-hook-form before submission
|
||||||
- Validation includes: required fields, email format, password minimum length (8+ chars), max lengths
|
- Password fields enforce the **exact backend rules** (verified in `ServiceCollectionExtensions.cs`):
|
||||||
|
- Minimum length: **8 characters**
|
||||||
|
- At least **1 uppercase letter** (`RequireUppercase = true`)
|
||||||
|
- At least **1 lowercase letter** (`RequireLowercase = true`)
|
||||||
|
- At least **1 digit** (`RequireDigit = true`)
|
||||||
|
- At least **1 non-alphanumeric character** (`RequireNonAlphanumeric = true`, e.g. `!@#$%^&*`)
|
||||||
|
- Inline per-rule validation messages indicate which specific rule is not yet met
|
||||||
|
- Validation also includes: required fields, email format, max lengths
|
||||||
- Server-side validation errors are displayed to the user without exposing internal details
|
- Server-side validation errors are displayed to the user without exposing internal details
|
||||||
|
|
||||||
### NFR-07: Access Control (SECURITY-08)
|
### NFR-07: Access Control (SECURITY-08)
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ As an Owner/Admin/User, I want to log in with my email address and password, so
|
|||||||
- [ ] A `/login` page is displayed for unauthenticated users
|
- [ ] A `/login` page is displayed for unauthenticated users
|
||||||
- [ ] The form contains an email field, a password field, and a submit button
|
- [ ] The form contains an email field, a password field, and a submit button
|
||||||
- [ ] Email field validates format before submission (invalid format shows inline error)
|
- [ ] Email field validates format before submission (invalid format shows inline error)
|
||||||
- [ ] Password field has a minimum length client-side hint (8 characters)
|
- [ ] Password field enforces backend rules client-side: minimum 8 characters, at least 1 uppercase letter, 1 lowercase letter, 1 digit, and 1 non-alphanumeric character (e.g. `!@#$%`)
|
||||||
- [ ] On valid credentials: access token is stored in memory, refresh token is set as httpOnly cookie, user is redirected to `/`
|
- [ ] On valid credentials: access token is stored in memory, refresh token is set as httpOnly cookie, user is redirected to `/`
|
||||||
- [ ] On invalid credentials: a generic error message is shown ("Invalid email or password") — no distinction between wrong email and wrong password
|
- [ ] On invalid credentials: a generic error message is shown ("Invalid email or password") — no distinction between wrong email and wrong password
|
||||||
- [ ] On network error: a user-friendly error message is shown ("Unable to connect. Please try again.")
|
- [ ] On network error: a user-friendly error message is shown ("Unable to connect. Please try again.")
|
||||||
@@ -205,7 +205,8 @@ As a person who received an invitation link, I want to complete my account setup
|
|||||||
**Acceptance Criteria**:
|
**Acceptance Criteria**:
|
||||||
- [ ] Navigating to `/invite/complete?token={token}` triggers `GET /users/validate-invitation?token={token}`
|
- [ ] Navigating to `/invite/complete?token={token}` triggers `GET /users/validate-invitation?token={token}`
|
||||||
- [ ] If the token is valid: a form is shown with a display name field and a password field (with confirmation)
|
- [ ] If the token is valid: a form is shown with a display name field and a password field (with confirmation)
|
||||||
- [ ] Password must meet minimum requirements (8+ characters); a strength indicator is shown
|
- [ ] Password must meet the backend requirements: minimum 8 characters, at least 1 uppercase letter, 1 lowercase letter, 1 digit, and 1 non-alphanumeric character — these rules are enforced client-side before submission and validated server-side
|
||||||
|
- [ ] Inline validation messages indicate which specific rule is not yet met (e.g. "Must contain at least 1 uppercase letter")
|
||||||
- [ ] Password confirmation must match; mismatch shows an inline error
|
- [ ] Password confirmation must match; mismatch shows an inline error
|
||||||
- [ ] On submit: `POST /users/complete-setup` is called with the token, display name, and password
|
- [ ] On submit: `POST /users/complete-setup` is called with the token, display name, and password
|
||||||
- [ ] On success: a confirmation message is shown ("Your account is ready. You can now log in.") with a link to `/login`
|
- [ ] On success: a confirmation message is shown ("Your account is ready. You can now log in.") with a link to `/login`
|
||||||
@@ -283,7 +284,7 @@ As an authenticated user, I want to see only the navigation items relevant to my
|
|||||||
- [ ] Dashboard: visible to all authenticated users
|
- [ ] Dashboard: visible to all authenticated users
|
||||||
- [ ] User Management: visible to Owner and Admin only
|
- [ ] User Management: visible to Owner and Admin only
|
||||||
- [ ] System Settings: visible to Owner only
|
- [ ] System Settings: visible to Owner only
|
||||||
- [ ] CMS Management: visible to all authenticated users
|
- [ ] CMS Management: visible to Owner only (feature is for managing other client CMS instances; out of scope for v1 but access is restricted from the start)
|
||||||
- [ ] Profile: visible to all authenticated users
|
- [ ] Profile: visible to all authenticated users
|
||||||
- [ ] Logout: visible to all authenticated users
|
- [ ] Logout: visible to all authenticated users
|
||||||
- [ ] The sidebar is responsive: collapses to an icon-only view or a hamburger menu on small screens
|
- [ ] The sidebar is responsive: collapses to an icon-only view or a hamburger menu on small screens
|
||||||
@@ -309,15 +310,18 @@ As an authenticated user, I want to switch between dark and light themes, so tha
|
|||||||
## Epic: CMS Management (Placeholder)
|
## Epic: CMS Management (Placeholder)
|
||||||
|
|
||||||
### US-20: View CMS management placeholder page
|
### US-20: View CMS management placeholder page
|
||||||
**Persona**: Owner, Admin, User
|
**Persona**: Owner
|
||||||
|
|
||||||
As an authenticated user, I want to navigate to the CMS Management section, so that I know where CMS content features will be available in the future.
|
As an Owner, I want to navigate to the CMS Management section, so that I know where multi-CMS management features will be available in the future.
|
||||||
|
|
||||||
**Acceptance Criteria**:
|
**Acceptance Criteria**:
|
||||||
- [ ] The `/cms` route renders a CMS Management page for all authenticated users
|
- [ ] The `/cms` route renders a CMS Management page for Owner only
|
||||||
|
- [ ] Navigating to `/cms` as Admin or User redirects to an "Access Denied" page or back to `/`
|
||||||
|
- [ ] The CMS Management item is NOT shown in the sidebar for Admin or User roles
|
||||||
- [ ] The page displays a clear "Work in Progress" or "Coming Soon" message
|
- [ ] The page displays a clear "Work in Progress" or "Coming Soon" message
|
||||||
- [ ] A brief description explains that CMS content modules will appear here
|
- [ ] A brief description explains that this section will allow managing multiple client CMS instances
|
||||||
- [ ] The page uses the same layout as other pages (sidebar, header)
|
- [ ] The page uses the same layout as other pages (sidebar, header)
|
||||||
|
- [ ] **Security**: The access restriction is enforced client-side as defence-in-depth; the backend also enforces authorisation
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user