Adds profile and settings pages

This commit is contained in:
2026-06-22 23:59:04 +02:00
parent 6976eb4337
commit 2544e20b3c
49 changed files with 2741 additions and 34 deletions
@@ -0,0 +1,147 @@
# Business Logic Model — Unit 6: Profile, Settings & CMS Placeholder
## 1. Profile Update Flow
```mermaid
sequenceDiagram
box rgba(33,150,243,0.15) Frontend
participant U as User
participant PF as ProfilePage
participant AC as AuthContext
end
box rgba(244,67,54,0.15) Backend
participant API as PUT /Users/me
participant REF as POST /auth/refresh
end
U->>PF: Edit name or email, click Save
PF->>PF: Validate form (name required, email valid)
alt Validation fails
PF-->>U: Show inline field errors
else Validation passes
PF->>API: PUT /api/v1/Users/me with name and email
alt API error
API-->>PF: 400 or 409 (e.g. email taken)
PF-->>U: Show error message
else Success
API-->>PF: 200 Updated user data
PF->>REF: POST /api/v1/Auth/refresh
REF-->>AC: New access token plus updated user object
AC-->>PF: AuthContext updated
PF-->>U: Show success toast, form reset to saved values
end
end
```
Text alternative: User edits name/email on ProfilePage → frontend validates → PUT /Users/me → on success calls /auth/refresh to sync AuthContext → success toast shown.
---
## 2. Change Password Flow
```mermaid
sequenceDiagram
box rgba(33,150,243,0.15) Frontend
participant U as User
participant PF as ProfilePage
participant DL as ChangePasswordDialog
end
box rgba(244,67,54,0.15) Backend
participant API as POST /auth/change-password
end
U->>PF: Click Change Password button
PF->>DL: Open dialog
U->>DL: Enter currentPassword, newPassword, confirmPassword
DL->>DL: Validate (newPassword matches confirm, meets policy)
alt Validation fails
DL-->>U: Show inline errors
else Validation passes
DL->>API: POST /api/v1/Auth/change-password
alt Wrong current password or policy violation
API-->>DL: 400 with error detail
DL-->>U: Show error message
else Success
API-->>DL: 200 OK
DL-->>U: Close dialog, show success toast on ProfilePage
end
end
```
Text alternative: User opens Change Password dialog → validates fields → POST /auth/change-password → success closes dialog and shows toast.
---
## 3. Settings — Availability Update Flow
```mermaid
sequenceDiagram
box rgba(33,150,243,0.15) Frontend
participant U as User
participant SP as SettingsPage
participant QC as QueryClient
end
box rgba(244,67,54,0.15) Backend
participant GET as GET /availability/status
participant PUT as POST /availability/admin/status
end
SP->>GET: Fetch current availability on mount
GET-->>SP: status, message, checkedAt
SP-->>U: Display current mode and message
U->>SP: Select new mode and optional message, click Save
SP->>PUT: POST /api/v1/Availability/admin/status
alt Success
PUT-->>SP: 200 OK
SP->>QC: Invalidate availability query cache
QC->>GET: Re-fetch status
GET-->>SP: Updated status
SP-->>U: Show success toast, updated badge
else Error
PUT-->>SP: 400 or 403
SP-->>U: Show error message
end
```
Text alternative: SettingsPage fetches availability on mount. User selects new mode and saves → POST to admin status endpoint → on success invalidate cache to re-fetch updated status.
---
## 4. Route Guard Flow (403 / 404)
```mermaid
graph TD
Nav["Navigation event"]
ProtectedRoute{"ProtectedRoute check\n(authenticated?)"}
RoleGuard{"RoleGuard check\n(role allowed?)"}
RouteMatch{"Route exists?"}
Page["Render Page"]
P403["Render 403 AccessDeniedPage"]
P404["Render 404 NotFoundPage"]
Login["Redirect to /login"]
Nav --> ProtectedRoute
ProtectedRoute -->|No| Login
ProtectedRoute -->|Yes| RouteMatch
RouteMatch -->|No| P404
RouteMatch -->|Yes, has RoleGuard| RoleGuard
RouteMatch -->|Yes, no RoleGuard| Page
RoleGuard -->|Allowed| Page
RoleGuard -->|Denied| P403
classDef guard fill:#FF9800,stroke:#e65100,color:#000
classDef page fill:#2196F3,stroke:#0d47a1,color:#000
classDef error fill:#F44336,stroke:#b71c1c,color:#000
classDef start fill:#9C27B0,stroke:#4a148c,color:#000
class ProtectedRoute guard
class RoleGuard guard
class RouteMatch guard
class Page page
class P403 error
class P404 error
class Login error
class Nav start
```
Text alternative: Navigation → ProtectedRoute (unauthenticated → /login) → route match (unknown → 404) → RoleGuard (denied → 403, allowed → page renders).
@@ -0,0 +1,112 @@
# Business Rules — Unit 6: Profile, Settings & CMS Placeholder
## Access Control Rules
```mermaid
graph TD
Request["Incoming Route Request"]
IsAuth{"Authenticated?"}
Route{"Which route?"}
IsOwner{"Role = Owner?"}
AccessDenied["403 AccessDeniedPage"]
NotFound["404 NotFoundPage"]
Profile["ProfilePage"]
Settings["SettingsPage"]
Cms["CmsPage"]
Login["Redirect to /login"]
Request --> IsAuth
IsAuth -->|No| Login
IsAuth -->|Yes| Route
Route -->|/profile| Profile
Route -->|/settings| IsOwner
Route -->|/cms| IsOwner
Route -->|unknown path| NotFound
IsOwner -->|Yes| Settings
IsOwner -->|Yes| Cms
IsOwner -->|No| AccessDenied
classDef guard fill:#FF9800,stroke:#e65100,color:#000
classDef page fill:#2196F3,stroke:#0d47a1,color:#000
classDef error fill:#F44336,stroke:#b71c1c,color:#000
classDef start fill:#9C27B0,stroke:#4a148c,color:#000
class IsAuth guard
class IsOwner guard
class Route guard
class Profile page
class Settings page
class Cms page
class AccessDenied error
class NotFound error
class Login error
class Request start
```
Text alternative: All routes require authentication (redirect to /login if not). /settings and /cms additionally require Owner role; non-Owners are shown 403. Unknown paths show 404.
---
## BR-01: Profile Access
- **Rule**: Any authenticated user can access `/profile`
- **Implementation**: ProfilePage is a child of `_authenticated.tsx` (inherits ProtectedRoute); no additional RoleGuard
- **Data**: Profile data is read from AuthContext (no extra API call for display)
## BR-02: Profile — Name and Email are Editable
- **Rule**: The logged-in user may update their own `name` and `email`
- **Validation** (frontend, mirrors backend):
- `name`: required, non-empty
- `email`: required, valid email format
- **Endpoint**: `PUT /api/v1/Users/me`
- **After save**: Call `POST /api/v1/Auth/refresh` to synchronize AuthContext with updated values
## BR-03: Profile — Role is Read-Only
- **Rule**: A user cannot change their own role from the profile page
- **Display**: Role shown as a static badge; no edit controls rendered
## BR-04: Change Password
- **Rule**: The logged-in user may change their own password via a dialog on ProfilePage
- **Validation** (frontend, mirrors backend password policy):
- `currentPassword`: required, non-empty
- `newPassword`: required, min 8 chars, at least 1 uppercase, 1 lowercase, 1 digit, 1 special character
- `confirmPassword` (UI-only field): must match `newPassword`
- **Endpoint**: `POST /api/v1/Auth/change-password`
- **On success**: Close dialog, show success toast; no AuthContext update needed (password change does not affect access token)
## BR-05: Settings — Owner Only
- **Rule**: Only users with role `Owner` may access `/settings`
- **Implementation**: `RoleGuard` with `allowedRoles={["Owner"]}` wraps SettingsPage
- **On violation**: Redirect to `/403`
## BR-06: Settings — Availability Management
- **Rule**: Owner may change the system availability status from SettingsPage
- **Allowed modes**: `Available`, `Maintenance`, `Unavailable`
- **Endpoint**: `POST /api/v1/Availability/admin/status` (OwnerOnly — already enforced by backend)
- **Message field**: Optional free-text reason displayed to end-users
- **On save**: Invalidate `useAvailabilityStatus` query cache to reflect new status immediately
## BR-07: CMS Page — Owner Only
- **Rule**: Only users with role `Owner` may access `/cms`
- **Implementation**: `RoleGuard` with `allowedRoles={["Owner"]}` wraps CmsPage
- **On violation**: Redirect to `/403`
- **Content**: Placeholder only — no functional CMS features in this unit
## BR-08: 403 Access Denied Page
- **Rule**: Rendered when `RoleGuard` rejects a route request
- **Content**: Heading "Access Denied" + explanatory message + "Back to Dashboard" button (navigates to `/`)
- **No authentication required**: 403 is a public route (unauthenticated users hitting protected routes are redirected to `/login` by ProtectedRoute first)
## BR-09: 404 Not Found Page
- **Rule**: Rendered when TanStack Router cannot match any registered route
- **Content**: Heading "Page Not Found" + brief message + "Back to Dashboard" button (navigates to `/`)
- **Implementation**: TanStack Router catch-all route (`$404.tsx`)
## BR-10: Backend — New Endpoints Required
The following new backend endpoints must be added as part of Unit 6:
| Endpoint | Method | Policy | Purpose |
|----------|--------|--------|---------|
| `/api/v1/Users/me` | PUT | Authenticated | Update own name and email |
| `/api/v1/Auth/change-password` | POST | Authenticated | Change own password |
Both endpoints operate on the currently authenticated user (identified via JWT claims).
@@ -0,0 +1,105 @@
# Domain Entities — Unit 6: Profile, Settings & CMS Placeholder
## Overview
Unit 6 introduces profile editing and password management. It reuses the existing `AvailabilityStatus` entity from Unit 4 for the Settings page, and introduces new request/response shapes for profile and password operations.
---
## Entity Diagram
```mermaid
graph TD
AuthUser["AuthUser"]
UpdateProfileRequest["UpdateProfileRequest"]
UpdateProfileResponse["UpdateProfileResponse"]
ChangePasswordRequest["ChangePasswordRequest"]
AvailabilityStatus["AvailabilityStatus"]
UpdateAvailabilityRequest["UpdateAvailabilityRequest"]
AuthUser -->|"provides data for"| UpdateProfileRequest
UpdateProfileRequest -->|"produces"| UpdateProfileResponse
UpdateProfileResponse -->|"refreshed into"| AuthUser
AvailabilityStatus -->|"changed by"| UpdateAvailabilityRequest
classDef user fill:#2196F3,stroke:#0d47a1,color:#000
classDef request fill:#FF9800,stroke:#e65100,color:#000
classDef response fill:#4CAF50,stroke:#2e7d32,color:#000
classDef availability fill:#9C27B0,stroke:#4a148c,color:#000
class AuthUser user
class UpdateProfileRequest,ChangePasswordRequest,UpdateAvailabilityRequest request
class UpdateProfileResponse response
class AvailabilityStatus availability
```
Text alternative: AuthUser provides data for UpdateProfileRequest; saving produces UpdateProfileResponse which refreshes AuthContext. AvailabilityStatus is changed via UpdateAvailabilityRequest. ChangePasswordRequest is standalone.
---
## Entity Descriptions
### AuthUser
Represents the currently logged-in user. Sourced from `AuthContext` (populated at login and after token refresh). Displayed on ProfilePage; updated indirectly via token refresh after a profile save.
| Field | Type | Editable | Source |
|-------|------|----------|--------|
| id | string (GUID) | No | AuthContext |
| email | string | Yes (via PUT /Users/me) | AuthContext |
| name | string | Yes (via PUT /Users/me) | AuthContext |
| role | string | No (read-only) | AuthContext |
| isActive | boolean | No | AuthContext |
---
### UpdateProfileRequest
Sent to `PUT /api/v1/Users/me` when the user saves their profile edits.
| Field | Type | Required | Validation |
|-------|------|----------|------------|
| name | string | Yes | Non-empty, max 100 chars |
| email | string | Yes | Valid email format |
---
### UpdateProfileResponse
Response from `PUT /api/v1/Users/me`. Contains the updated user data. After receiving this, the frontend calls `POST /api/v1/Auth/refresh` to sync AuthContext with the new values.
| Field | Type | Notes |
|-------|------|-------|
| id | string | GUID |
| email | string | Updated value |
| name | string | Updated value |
| role | string | Unchanged |
| isActive | boolean | Unchanged |
---
### ChangePasswordRequest
Sent to `POST /api/v1/Auth/change-password`. Requires current password for verification.
| Field | Type | Required | Validation |
|-------|------|----------|------------|
| currentPassword | string | Yes | Must match current stored password |
| newPassword | string | Yes | Must satisfy backend password policy (≥8 chars, upper, lower, digit, special char) |
---
### AvailabilityStatus (reused from Unit 4)
Retrieved via `GET /api/v1/Availability/status`. Displayed on SettingsPage with controls to change it.
| Field | Type | Notes |
|-------|------|-------|
| status | string | "Available" \| "Maintenance" \| "Unavailable" |
| message | string | Optional custom message |
| checkedAt | datetime | Timestamp of last check |
---
### UpdateAvailabilityRequest
Sent to `POST /api/v1/Availability/admin/status` (OwnerOnly policy — already exists in backend).
| Field | Type | Required | Allowed values |
|-------|------|----------|----------------|
| newStatus | string | Yes | "Available" \| "Maintenance" \| "Unavailable" |
| reason | string | No | Free-form message shown to users |
@@ -0,0 +1,279 @@
# Frontend Components — Unit 6: Profile, Settings & CMS Placeholder
## Component Hierarchy
```mermaid
graph TD
Auth["_authenticated.tsx\n(layout route)"]
Profile["profile.tsx\nProfilePage"]
PIF["ProfileInfoForm\n(name + email edit)"]
CPD["ChangePasswordDialog\n(dialog overlay)"]
RB["RoleDisplay\n(read-only badge)"]
Settings["settings.tsx\nSettingsPage\n[RoleGuard: Owner]"]
AS["AvailabilitySection\n(mode selector + message)"]
ASBI["AvailabilityStatusBadge\n(reused from Unit 4)"]
PM1["PlaceholderCard\nModule Management"]
PM2["PlaceholderCard\nSystem Configuration"]
PM3["PlaceholderCard\nBranding / Theme"]
Cms["cms.tsx\nCmsPage\n[RoleGuard: Owner]"]
P403["403.tsx\nAccessDeniedPage"]
P404["$404.tsx\nNotFoundPage"]
Auth --> Profile
Auth --> Settings
Auth --> Cms
Auth --> P403
Auth --> P404
Profile --> PIF
Profile --> CPD
Profile --> RB
Settings --> AS
AS --> ASBI
Settings --> PM1
Settings --> PM2
Settings --> PM3
classDef layout fill:#4CAF50,stroke:#2e7d32,color:#000
classDef page fill:#2196F3,stroke:#0d47a1,color:#000
classDef component fill:#9C27B0,stroke:#4a148c,color:#000
classDef reused fill:#FF9800,stroke:#e65100,color:#000
classDef error fill:#F44336,stroke:#b71c1c,color:#000
class Auth layout
class Profile page
class Settings page
class Cms page
class PIF component
class CPD component
class RB component
class AS component
class PM1 component
class PM2 component
class PM3 component
class ASBI reused
class P403 error
class P404 error
```
Text alternative: _authenticated layout route contains Profile (with ProfileInfoForm, ChangePasswordDialog, RoleDisplay), Settings (Owner-only: AvailabilitySection with badge + 3 PlaceholderCards), CmsPage (Owner-only), 403, and 404.
---
## Component Specifications
### `profile.tsx` — ProfilePage
**Purpose**: Displays and allows editing of the logged-in user's name and email. Shows role as read-only. Provides Change Password access.
**State**:
- Form state managed by `react-hook-form`
- `isSaving` — boolean (PUT /Users/me in flight)
- `isPasswordDialogOpen` — boolean
**Props**: None (reads from `useAuth()`)
**API integrations**:
- `PUT /api/v1/Users/me` — save profile edits (new hook: `useUpdateProfile`)
- `POST /api/v1/Auth/refresh` — called after successful profile save via `AuthContext.refresh()`
**Sections**:
1. **Profile info card**: Name (text input), Email (text input), Role (read-only badge), Save button
2. **Security card**: "Change Password" button → opens `ChangePasswordDialog`
**Validation** (via `zod`):
```
name: z.string().min(1, "Name is required")
email: z.string().email("Invalid email address")
```
---
### `ProfileInfoForm`
**Purpose**: Form fields for name and email editing. Embedded in ProfilePage.
**Props**:
- `defaultValues: { name: string; email: string }`
- `onSave: (data: UpdateProfileRequest) => Promise<void>`
- `isSaving: boolean`
---
### `ChangePasswordDialog`
**Purpose**: Modal dialog for changing password. Opened from ProfilePage.
**Props**:
- `open: boolean`
- `onClose: () => void`
**State** (internal, via `react-hook-form`):
- `currentPassword`, `newPassword`, `confirmPassword`
**API integration**: `POST /api/v1/Auth/change-password` (new hook: `useChangePassword`)
**Validation** (via `zod`):
```
currentPassword: z.string().min(1, "Required")
newPassword: z.string()
.min(8, "At least 8 characters")
.regex(/[A-Z]/, "At least 1 uppercase letter")
.regex(/[a-z]/, "At least 1 lowercase letter")
.regex(/[0-9]/, "At least 1 digit")
.regex(/[^a-zA-Z0-9]/, "At least 1 special character")
confirmPassword: z.string()
// .refine: confirmPassword === newPassword
```
**On success**: Close dialog, emit success toast.
---
### `RoleDisplay`
**Purpose**: Read-only badge showing the user's current role. Not editable.
**Props**:
- `role: string`
---
### `settings.tsx` — SettingsPage
**Purpose**: Owner-only management page. Contains availability controls and placeholder sections for future settings.
**State**:
- Availability data from `useAvailabilityStatus()` (reused from Unit 4)
- `selectedMode` — controlled select: `"Available" | "Maintenance" | "Unavailable"`
- `reason` — string (optional message input)
- `isSaving` — boolean
**Props**: None
**API integrations**:
- `GET /api/v1/Availability/status` — via `useAvailabilityStatus` (existing hook)
- `POST /api/v1/Availability/admin/status` — via new `useUpdateAvailability` mutation; on success invalidates `useAvailabilityStatus` query
**Sections**:
1. **Availability section** (`AvailabilitySection`): Current status badge + mode selector (radio or select) + optional message text area + Save button
2. **PlaceholderCard** — "Module Management" (locked, coming soon)
3. **PlaceholderCard** — "System Configuration" (locked, coming soon)
4. **PlaceholderCard** — "Branding / Theme" (locked, coming soon)
---
### `AvailabilitySection`
**Purpose**: Embedded in SettingsPage. Shows current availability and provides controls to change it.
**Props**:
- `currentStatus: AvailabilityStatus`
- `onSave: (request: UpdateAvailabilityRequest) => Promise<void>`
- `isSaving: boolean`
**Sub-component**: Renders `<AvailabilityStatusBadge>` (reused from Unit 4, imported from `components/shared/`)
---
### `PlaceholderCard`
**Purpose**: Reusable locked card for future settings sections.
**Props**:
- `title: string`
- `description?: string`
**Visual**: Dimmed card with lock icon and "Coming soon" label.
---
### `cms.tsx` — CmsPage
**Purpose**: Owner-only placeholder for the future CMS feature.
**Props**: None
**Content**: Icon (e.g. `LayoutGrid` from lucide-react) + heading "Content Management System" + brief description: "This is where you will manage your CMS content. This feature is coming soon."
**State**: None (pure presentational)
---
### `403.tsx` — AccessDeniedPage
**Purpose**: Shown when RoleGuard rejects access to a route.
**Props**: None
**Content**:
- Icon: `ShieldOff` or `Lock` (lucide-react)
- Heading: "Access Denied"
- Message: "You don't have permission to view this page."
- Button: "Back to Dashboard" → navigates to `/`
**State**: None (pure presentational)
---
### `$404.tsx` — NotFoundPage
**Purpose**: TanStack Router catch-all for unknown routes.
**Props**: None
**Content**:
- Icon: `FileQuestion` (lucide-react)
- Heading: "Page Not Found"
- Message: "The page you're looking for doesn't exist."
- Button: "Back to Dashboard" → navigates to `/`
**State**: None (pure presentational)
---
## New API Hooks
| Hook | File | Endpoint | Method |
|------|------|----------|--------|
| `useUpdateProfile` | `src/api/useProfile.ts` | `PUT /api/v1/Users/me` | useMutation |
| `useChangePassword` | `src/api/useProfile.ts` | `POST /api/v1/Auth/change-password` | useMutation |
| `useUpdateAvailability` | `src/api/useAvailability.ts` (extend existing) | `POST /api/v1/Availability/admin/status` | useMutation |
## New Backend Endpoints
| Endpoint | Method | Controller | Policy |
|----------|--------|------------|--------|
| `/api/v1/Users/me` | PUT | UsersController | Authenticated |
| `/api/v1/Auth/change-password` | POST | AuthController | Authenticated |
## i18n Keys Required
**Profile page**:
- `profile.title`, `profile.name`, `profile.email`, `profile.role`, `profile.save`, `profile.saving`, `profile.saveSuccess`
- `profile.changePassword`, `profile.changePassword.current`, `profile.changePassword.new`, `profile.changePassword.confirm`, `profile.changePassword.success`
**Settings page**:
- `settings.title`, `settings.availability.title`, `settings.availability.save`, `settings.availability.saveSuccess`
- `settings.availability.mode.available`, `settings.availability.mode.maintenance`, `settings.availability.mode.unavailable`
- `settings.availability.reason`, `settings.placeholder.comingSoon`
- `settings.modules.title`, `settings.systemConfig.title`, `settings.branding.title`
**Error pages**:
- `error.403.title`, `error.403.message`, `error.404.title`, `error.404.message`, `error.backToDashboard`
## Unit Test Scope (Q8: C — all pages)
| Component | Test focus |
|-----------|------------|
| ProfilePage | Renders AuthContext user data; name/email inputs; save triggers PUT; password dialog opens |
| ChangePasswordDialog | Validation (mismatch, policy); success flow; error handling |
| SettingsPage | Fetches and displays availability; mode change + save triggers POST; placeholder sections render |
| CmsPage | Renders heading and description |
| AccessDeniedPage (403) | Renders heading, message, Back to Dashboard navigates to `/` |
| NotFoundPage (404) | Renders heading, message, Back to Dashboard navigates to `/` |