Business Rules — Unit 2: Authentication Pages
Setup Form Validation Rules
Field-Level Rules
| Field |
Type |
Rules |
Error Message |
| Name |
string |
Required, 1–255 chars |
"Name is required" |
| Email |
string |
Required, valid email format |
"Enter a valid email address" |
| Password |
string |
Zod schema matching backend (8+ chars, 1 uppercase, 1 digit, 1 special) |
Backend-specific error (e.g., "Password must contain uppercase") |
| Confirm Password |
string |
Required, must match Password field |
"Passwords do not match" |
| Language |
enum |
"en" or "nl" |
(dropdown, always valid) |
Cross-Field Rules
Confirm Password must equal Password (checked on blur and form validation)
Invitation Completion Form Validation Rules
Field-Level Rules
| Field |
Type |
Rules |
Error Message |
| Email |
string |
Read-only (from token validation) |
N/A |
| Name |
string |
Required, 1–255 chars |
"Name is required" |
| Password |
string |
Zod schema matching backend |
Backend-specific error |
| Confirm Password |
string |
Required, must match Password field |
"Passwords do not match" |
Token Validation Rules
- Token comes from query param (
?token=xxx)
- Validated on page mount via
GET /Invitation/validate?token=xxx
- If valid: backend returns
{ valid: true, email: "..." }
- If invalid/expired: backend returns
{ valid: false, error: "..." }
- Loading state shown while validating
- Error state shown if validation fails (no form rendered)
Initialization Status Rules
InitGuard Behavior
| Scenario |
Action |
App loads, GET /Setup/status returns { initialized: true } |
Allow all routes; proceed normally |
App loads, GET /Setup/status returns { initialized: false } |
Redirect to /setup (InitGuard blocks other routes) |
User navigates to /setup directly |
Show setup form (accessible regardless of init status) |
| User navigates to any other route while not initialized |
Redirect to /setup (InitGuard blocks) |
User completes setup (POST /Setup succeeds) |
App redirects to /login, session resets, init status re-fetched on next app load |
| Network error while checking init status |
Treat as "not initialized" and redirect to /setup (fail-safe) |
Caching Strategy
staleTime: Infinity (session-level cache, never re-fetch after first successful load)
- Rationale: Setup operation ends with redirect to
/login, which reloads the app
- No manual re-fetching needed during the session
Role-Based Access Control Rules
Role Definition
- Owner: Created during system initialization (
POST /Setup)
- Admin: Reserved for future use (not used in Unit 2–6)
- User: Created via user invitations (
POST /Invitation/complete)
Route Access Rules
| Route |
Required Role(s) |
Behavior if Denied |
Guard Component |
/setup |
None (public, no auth required) |
N/A |
None |
/login |
None (public, no auth required) |
N/A |
None |
/invite/complete |
None (public, token-authenticated) |
N/A |
None |
/dashboard |
Any authenticated |
N/A |
ProtectedRoute (Unit 1) |
/profile |
Any authenticated |
N/A |
ProtectedRoute (Unit 1) |
/users |
Owner or Admin |
Inline "Access Denied" message |
RoleGuard (this unit) |
/settings |
Owner only |
Inline "Access Denied" message |
RoleGuard (this unit) |
/cms |
Owner only |
Inline "Access Denied" message |
RoleGuard (this unit) |
Access Denied Behavior
- When user lacks required role for a route:
- DO: Show inline "Access Denied" message within the page (e.g., in a styled container)
- DO NOT: Use toast notifications
- DO NOT: Redirect to separate
/403 route
- DO: Make it clear the page exists but access is denied
- Example: "You do not have permission to access this page. Contact your administrator."
Guard Implementation
RoleGuard implemented as TanStack Router beforeLoad hook in route definitions
- Evaluates
user.role from AuthContext at route load time
- Returns redirect-to-self if denied (allows inline error display via page component state)
- OR: page component checks role and displays error inline
Error Handling Rules
Form Submission Error Display
Two-part error handling (combining inline validation + banner):
Part 1: Real-Time Inline Validation
- Zod schema validates as user types/blurs on field
- Show error message below the field in red text
- Example:
<span class="text-red-600 text-sm">{error}</span>
- Clears when user fixes the error
Part 2: Form-Level Banner (After Submit)
- After form submission, if backend returns error:
- Show dismissible banner at the top of the form with full error message
- Banner includes close button
- Banner persists until user dismisses or form is reset
- Consistent with LoginPage pattern (Unit 1)
- If network error (no response):
- Banner: "Network error. Please try again."
Specific Error Scenarios
| Scenario |
Banner Message |
Inline Errors |
| Email already exists on setup |
"Setup failed: This email is already registered" |
None (only from backend) |
| Password too weak |
"Setup failed: [backend message about password rules]" |
Pre-validated by Zod |
| Network timeout on setup |
"Network error. Please try again." |
None |
| Invalid invitation token on load |
Full-page error (InitGuard redirect to setup OR error page) |
N/A |
| Token expired during form fill |
Error shown after submit: "Invitation link expired" |
N/A |
| Invalid form data before submit |
Inline errors only (Zod validation) |
Below each field |
Translation Key Rules
Locales
- English (en) — eager load (fallback language)
- Nederlands (nl) — lazy load (per-route chunk)
Translation Keys Scope
- All Unit 2 keys added to existing
translation.json files (not separate namespace files)
- Organized by feature:
setup, inviteComplete, errors sections
- Used with
i18next pattern: t('setup.title'), t('errors.accessDenied')
Language Preference Storage
- SetupPage collects language preference at account creation
- Stored in backend user profile (for future use)
- Frontend respects this preference on login (future: read from user data)
Password Schema Rules
Location
src/lib/schemas/auth.ts — shared by LoginPage, SetupPage, InviteCompletePage
Validation Rules (Backend-Aligned)
Shared Usage
passwordSchema — used by LoginPage, SetupPage, InviteCompletePage
confirmPasswordSchema — used only when form collects both password fields (setup, invite)
MSW Mock Handler Rules
Handler Organization
- Setup handlers: Extend existing
src/mocks/setup/ → add POST /Setup handler
- Invitation handlers: New
src/mocks/invitation/ → add GET /Invitation/validate + POST /Invitation/complete handlers
Setup Handler Behaviors
GET /Setup/status
- Always returns
{ initialized: true/false }
- Default:
{ initialized: false } on app first load
- After
POST /Setup succeeds: automatically return { initialized: true } for subsequent requests
POST /Setup
- Request:
{ name, email, password, language }
- Validates email not already used (mock: always accepts if password valid)
- Returns
{ status: 201, message: "System initialized", user: { id, name, email, role: "Owner" } }
- Error:
{ status: 400, detail: "Email already in use" } (ProblemDetails format)
Invitation Handler Behaviors
GET /Invitation/validate?token=xxx
- Mock tokens:
valid-token-123, expired-token-456
- Valid token: returns
{ valid: true, email: "invited@example.com" }
- Invalid/expired: returns
{ valid: false, error: "Invalid or expired token" } (ProblemDetails format)
POST /Invitation/complete
- Request:
{ token, name, password }
- Validates token and password
- Returns
{ status: 201, user: { id, name, email, role: "User" }, message: "Account created" }
- Error:
{ status: 400, detail: "Invalid token" } (ProblemDetails format)
Consistency Rules
Form Pattern Consistency
- All forms use
react-hook-form + zod (LoginPage pattern from Unit 1)
- Error display: inline (on blur) + banner (on submit)
- Submission feedback: disable submit button during request
- Success: redirect or success message
Navigation Consistency
- After setup: redirect to
/login (user must log in)
- After invitation completion: redirect to
/login (user must log in)
- After login: redirect to
/dashboard (existing pattern from Unit 1)
Component API Consistency
- All hooks follow React conventions (use* prefix)
- All components accept standard React props (className, etc.)
- All pages rendered in
src/pages/ directory (consistent with LoginPage)
- All routes defined in
src/router.tsx (code-based routing)