Adds application design (awaiting approval)

This commit is contained in:
2026-06-17 20:15:54 +02:00
parent c7154e288f
commit 9e49489f7e
12 changed files with 1171 additions and 22 deletions
@@ -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 16).
**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` |
@@ -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 16)
### 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 16** — 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 16)
**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`
- Role-based guards MUST be applied per page:
- System Settings page: Owner only
- CMS Management page: Owner only
- User Management page: Owner and Admin
- Profile page: Any authenticated user
- Dashboard: Any authenticated user
@@ -75,6 +76,7 @@
- Dashboard: all authenticated users
- User Management: Owner and Admin
- 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
- Logout button: all authenticated users
- Mobile-responsive: sidebar collapses on small screens
@@ -85,9 +87,10 @@
- Default: system preference
### FR-12: CMS Management (Placeholder)
- Page at `/cms` (structure ready, no real data)
- Shows navigation structure for future CMS content modules
- Clearly marked as "Work in Progress" with a placeholder message
- Page at `/cms` accessible to **Owner only**
- Non-Owner roles attempting to access `/cms` are redirected to an "Access Denied" page or back to `/`
- 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)
- 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
### 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
- [ ] The form contains an email field, a password field, and a submit button
- [ ] 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 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.")
@@ -205,7 +205,8 @@ As a person who received an invitation link, I want to complete my account setup
**Acceptance Criteria**:
- [ ] 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)
- [ ] 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
- [ ] 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`
@@ -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
- [ ] User Management: visible to Owner and Admin 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
- [ ] Logout: visible to all authenticated users
- [ ] 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)
### 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**:
- [ ] 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
- [ ] 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)
- [ ] **Security**: The access restriction is enforced client-side as defence-in-depth; the backend also enforces authorisation
---