Adds auth pages
This commit is contained in:
+235
@@ -0,0 +1,235 @@
|
||||
# Business Logic Model — Unit 2: Authentication Pages
|
||||
|
||||
## Overview
|
||||
|
||||
Unit 2 implements five distinct business logic flows. Each is technology-agnostic; implementation details (TanStack Router APIs, React Query, etc.) are resolved in Code Generation.
|
||||
|
||||
---
|
||||
|
||||
## Flow 1: App Initialization — InitGuard
|
||||
|
||||
**Trigger**: Every page load / app mount (runs in the root route)
|
||||
**Purpose**: Ensure the system is initialized before rendering any route
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A([App mounts]) --> B{"Setup status<br/>already cached?"}
|
||||
B -- Yes --> E
|
||||
B -- No --> C["Fetch GET /Setup/status"]
|
||||
C --> D{"Request<br/>outcome"}
|
||||
D -- Network error --> ERR["Unable to reach server"]
|
||||
D -- Success --> E{"initialized?"}
|
||||
E -- false --> F{"On /setup<br/>already?"}
|
||||
F -- Yes --> G([Render /setup page])
|
||||
F -- No --> H([Redirect to /setup])
|
||||
E -- true --> I{"On /setup<br/>already?"}
|
||||
I -- Yes --> J([Redirect to /login])
|
||||
I -- No --> K([Continue to route])
|
||||
|
||||
classDef start fill:#c7f9e9,stroke:#065f46,stroke-width:2px,color:#1a1a1a
|
||||
classDef decision fill:#fef3c7,stroke:#b45309,stroke-width:1px,color:#1a1a1a
|
||||
classDef action fill:#dbeafe,stroke:#1d4ed8,stroke-width:1px,color:#1a1a1a
|
||||
classDef error fill:#fee2e2,stroke:#b91c1c,stroke-width:1px,color:#1a1a1a
|
||||
classDef terminal fill:#f3e8ff,stroke:#7c3aed,stroke-width:2px,color:#1a1a1a
|
||||
|
||||
class A start
|
||||
class B,D,E,F,I decision
|
||||
class C action
|
||||
class ERR error
|
||||
class G,H,J,K terminal
|
||||
```
|
||||
|
||||
Text alternative: On app mount, fetch setup status (once per session). If not initialized → redirect to /setup (except when already on /setup). If initialized → allow navigation (redirect away from /setup to /login).
|
||||
|
||||
**Caching rule**: The fetch result is held in React state at root level. Once loaded, it never re-fetches within the same session (BR-U2-08).
|
||||
|
||||
---
|
||||
|
||||
## Flow 2: Protected Route Access — ProtectedRoute
|
||||
|
||||
**Trigger**: User navigates to any route inside the authenticated layout
|
||||
**Purpose**: Prevent unauthenticated access to protected pages
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A([Route navigation]) --> B{"AuthContext:<br/>user present?"}
|
||||
B -- Yes --> C([Render requested page])
|
||||
B -- No --> D{"Restoring<br/>session?"}
|
||||
D -- Yes --> E([Loading spinner])
|
||||
E --> B
|
||||
D -- No --> F["Redirect to /login<br/>with ?redirect=path"]
|
||||
|
||||
classDef start fill:#c7f9e9,stroke:#065f46,stroke-width:2px,color:#1a1a1a
|
||||
classDef decision fill:#fef3c7,stroke:#b45309,stroke-width:1px,color:#1a1a1a
|
||||
classDef action fill:#dbeafe,stroke:#1d4ed8,stroke-width:1px,color:#1a1a1a
|
||||
classDef terminal fill:#f3e8ff,stroke:#7c3aed,stroke-width:2px,color:#1a1a1a
|
||||
classDef loading fill:#e0e7ff,stroke:#4338ca,stroke-width:1px,color:#1a1a1a
|
||||
|
||||
class A start
|
||||
class B,D decision
|
||||
class F action
|
||||
class C terminal
|
||||
class E loading
|
||||
```
|
||||
|
||||
Text alternative: Check if user is in AuthContext. If restoring session, show spinner. If no user after restore, redirect to /login preserving the original path.
|
||||
|
||||
---
|
||||
|
||||
## Flow 3: Role-Restricted Route Access — RoleGuard
|
||||
|
||||
**Trigger**: Authenticated user navigates to a role-restricted route
|
||||
**Purpose**: Enforce per-route role requirements
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A([Authenticated navigation]) --> B{"Route has<br/>role restriction?"}
|
||||
B -- No restriction --> C([Render page])
|
||||
B -- Owner only --> D{"user.role<br/>= Owner?"}
|
||||
B -- Owner or Admin --> E{"user.role<br/>= Owner or Admin?"}
|
||||
D -- Yes --> C
|
||||
D -- No --> F([Inline Access Denied])
|
||||
E -- Yes --> C
|
||||
E -- No --> F
|
||||
|
||||
classDef start fill:#c7f9e9,stroke:#065f46,stroke-width:2px,color:#1a1a1a
|
||||
classDef decision fill:#fef3c7,stroke:#b45309,stroke-width:1px,color:#1a1a1a
|
||||
classDef terminal fill:#f3e8ff,stroke:#7c3aed,stroke-width:2px,color:#1a1a1a
|
||||
classDef denied fill:#fee2e2,stroke:#b91c1c,stroke-width:1px,color:#1a1a1a
|
||||
|
||||
class A start
|
||||
class B,D,E decision
|
||||
class C terminal
|
||||
class F denied
|
||||
```
|
||||
|
||||
Text alternative: If route has no restriction → render. If Owner-only route → check role, render page or show inline "Access Denied". If Owner/Admin route → same check.
|
||||
|
||||
**Inline Access Denied**: Rendered within the normal page shell (sidebar + layout remain visible). The message identifies the required role. No redirect occurs (BR-U2-19).
|
||||
|
||||
---
|
||||
|
||||
## Flow 4: System Setup — SetupPage
|
||||
|
||||
**Trigger**: User visits `/setup` and system is uninitialized (`initialized: false`)
|
||||
**Purpose**: Create the first Owner account
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A([User opens /setup]) --> B["Render 5-field form<br/>Name · Email · Password<br/>Confirm Password · Locale"]
|
||||
B --> C{"Locale<br/>changed?"}
|
||||
C -- Yes --> D["Apply i18n.changeLanguage"]
|
||||
D --> B
|
||||
C -- No --> E["User submits form"]
|
||||
E --> F{"Zod<br/>validation"}
|
||||
F -- Invalid --> G["Show inline field errors"]
|
||||
G --> B
|
||||
F -- Valid --> H["Disable submit · loading"]
|
||||
H --> I["POST /Setup"]
|
||||
I --> J{"Response"}
|
||||
J -- 200/201 --> K["Show success message"]
|
||||
K --> L([Redirect to /login])
|
||||
J -- 409 Conflict --> M["Banner: already initialized"]
|
||||
J -- 4xx --> N["Banner: API error"]
|
||||
J -- Network error --> O["Banner: Unable to connect"]
|
||||
M --> B
|
||||
N --> B
|
||||
O --> B
|
||||
|
||||
classDef start fill:#c7f9e9,stroke:#065f46,stroke-width:2px,color:#1a1a1a
|
||||
classDef decision fill:#fef3c7,stroke:#b45309,stroke-width:1px,color:#1a1a1a
|
||||
classDef action fill:#dbeafe,stroke:#1d4ed8,stroke-width:1px,color:#1a1a1a
|
||||
classDef error fill:#fee2e2,stroke:#b91c1c,stroke-width:1px,color:#1a1a1a
|
||||
classDef terminal fill:#f3e8ff,stroke:#7c3aed,stroke-width:2px,color:#1a1a1a
|
||||
classDef success fill:#dcfce7,stroke:#15803d,stroke-width:1px,color:#1a1a1a
|
||||
|
||||
class A start
|
||||
class C,F,J decision
|
||||
class B,D,E,H,I action
|
||||
class G,M,N,O error
|
||||
class K success
|
||||
class L terminal
|
||||
```
|
||||
|
||||
Text alternative: Render 5-field form → live locale switching → submit → Zod validation → POST /Setup → success banner + redirect to /login, or error banner on API/network failure.
|
||||
|
||||
---
|
||||
|
||||
## Flow 5: Invitation Completion — InviteCompletePage
|
||||
|
||||
**Trigger**: User clicks an invitation link (`/invite/complete?token=xxx`)
|
||||
**Purpose**: Complete account setup for an invited user
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A([User opens /invite/complete]) --> B{"token in URL?"}
|
||||
B -- No --> C(["Error: invalid link"])
|
||||
B -- Yes --> D(["Loading spinner"])
|
||||
D --> E["GET /Invitation/validate"]
|
||||
E --> F{"Validation<br/>result"}
|
||||
F -- Network error --> G(["Error: unable to connect"])
|
||||
F -- isValid=false --> H(["Error: expired / used / not found"])
|
||||
F -- isValid=true --> I["Show form<br/>email read-only · Name · Password<br/>Confirm Password"]
|
||||
I --> J["User submits form"]
|
||||
J --> K{"Zod<br/>validation"}
|
||||
K -- Invalid --> L["Inline field errors"]
|
||||
L --> I
|
||||
K -- Valid --> M(["Disable submit · loading"])
|
||||
M --> N["POST /Invitation/complete"]
|
||||
N --> O{"Response"}
|
||||
O -- Success --> P["Show success message"]
|
||||
P --> Q([Redirect to /login])
|
||||
O -- 4xx --> R["Banner: API error"]
|
||||
O -- Network error --> S["Banner: Unable to connect"]
|
||||
R --> I
|
||||
S --> I
|
||||
|
||||
classDef start fill:#c7f9e9,stroke:#065f46,stroke-width:2px,color:#1a1a1a
|
||||
classDef decision fill:#fef3c7,stroke:#b45309,stroke-width:1px,color:#1a1a1a
|
||||
classDef action fill:#dbeafe,stroke:#1d4ed8,stroke-width:1px,color:#1a1a1a
|
||||
classDef error fill:#fee2e2,stroke:#b91c1c,stroke-width:1px,color:#1a1a1a
|
||||
classDef terminal fill:#f3e8ff,stroke:#7c3aed,stroke-width:2px,color:#1a1a1a
|
||||
classDef success fill:#dcfce7,stroke:#15803d,stroke-width:1px,color:#1a1a1a
|
||||
classDef loading fill:#e0e7ff,stroke:#4338ca,stroke-width:1px,color:#1a1a1a
|
||||
|
||||
class A start
|
||||
class B,F,K,O decision
|
||||
class E,I,J,N action
|
||||
class C,G,H,R,S error
|
||||
class P success
|
||||
class Q terminal
|
||||
class D,M loading
|
||||
```
|
||||
|
||||
Text alternative: On mount → check token param → validate with API (loading spinner) → invalid token shows error state; valid token shows form → submit → success banner + redirect to /login, or error banner on failure.
|
||||
|
||||
---
|
||||
|
||||
## Flow Interaction Diagram
|
||||
|
||||
The five flows compose within the TanStack Router tree:
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
Root["__root · InitGuard"] --> PublicSetup["/setup · SetupPage"]
|
||||
Root --> PublicLogin["/login · LoginPage"]
|
||||
Root --> PublicInvite["/invite/complete · InviteCompletePage"]
|
||||
Root --> AuthLayout["_authenticated · ProtectedRoute"]
|
||||
AuthLayout --> Dashboard["/ · Dashboard"]
|
||||
AuthLayout --> Profile["/profile · ProfilePage"]
|
||||
AuthLayout --> Users["/users · UsersPage<br/>RoleGuard: Owner or Admin"]
|
||||
AuthLayout --> Settings["/settings · SettingsPage<br/>RoleGuard: Owner"]
|
||||
AuthLayout --> CMS["/cms · CmsPage<br/>RoleGuard: Owner"]
|
||||
|
||||
classDef guard fill:#fef3c7,stroke:#b45309,stroke-width:2px,color:#1a1a1a
|
||||
classDef public fill:#dbeafe,stroke:#1d4ed8,stroke-width:1px,color:#1a1a1a
|
||||
classDef protected fill:#dcfce7,stroke:#15803d,stroke-width:1px,color:#1a1a1a
|
||||
classDef restricted fill:#fee2e2,stroke:#b91c1c,stroke-width:1px,color:#1a1a1a
|
||||
|
||||
class Root guard
|
||||
class PublicSetup,PublicLogin,PublicInvite public
|
||||
class Dashboard,Profile protected
|
||||
class AuthLayout,Users,Settings,CMS restricted
|
||||
```
|
||||
|
||||
Text alternative: Root route runs InitGuard. Public routes (/setup, /login, /invite/complete) are accessible without authentication. The _authenticated layout wraps all protected routes and runs ProtectedRoute. Role-restricted routes (/users, /settings, /cms) additionally run RoleGuard.
|
||||
Reference in New Issue
Block a user