Adds application design (awaiting approval)
This commit is contained in:
@@ -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
|
||||
```
|
||||
Reference in New Issue
Block a user