Adds auth pages

This commit is contained in:
2026-06-21 00:15:28 +02:00
parent 7dfc3a9692
commit ab93a5c7d1
28 changed files with 2785 additions and 45 deletions
@@ -0,0 +1,320 @@
# Code Generation Plan — Unit 2: Authentication Pages
**Status**: ✅ Complete
## Unit Context
- **Unit**: Unit 2 — Authentication Pages
- **Application code root**: `frontend/` (Vite + React 19 + TypeScript)
- **Depends on**: Unit 1 (ApiClient, AuthContext, router.tsx, shadcn primitives, MSW)
- **Stories**: US-01, US-02, US-03, US-04, US-05, US-06, US-07, US-13, US-14
## Key Architectural Decisions
- **TanStack Query is NOT installed** — hooks use `useState`/`useEffect` with a module-level session cache
- **InitGuard strategy**: `async beforeLoad` on rootRoute + module-level `fetchSetupStatus()` singleton (fetches once, caches for the session); uses TanStack Router's `pendingComponent` for the loading state
- **RoleGuard strategy**: React wrapper component rendering inline "Access Denied" (no redirect, no /403 route); applied to route `component` wrappers in router.tsx
- **UserRole note**: Backend uses `'Administrator'` not `'Admin'` (confirmed from `src/api/types.ts`)
- **Password schema**: Moved from LoginPage inline to `src/lib/schemas/auth.ts` (shared)
- **Error handling pattern**: Zod inline field errors on blur (`mode: 'onTouched'`) + dismissible banner for API/network errors
## Unit 1 Deviations Carried Forward
- Code-based routing in `src/router.tsx` (not `src/routes/`)
- Pages in `src/pages/` (not `src/routes/`)
- 4-space indentation throughout
## Stories Covered
| Story | Description | Steps |
|---|---|---|
| US-01 | Login with email and password | 2, 14 |
| US-02 | Session persistence via refresh token | (already in Unit 1; confirmed by ProtectedRoute) |
| US-03 | Logout | (already in Unit 1; AuthContext.logout()) |
| US-04 | Redirect to login when not authenticated | 5, 13 |
| US-05 | Auto token refresh on expiry | (already in Unit 1; ApiClient 401 interceptor) |
| US-06 | Initialize system as first Owner | 1, 2, 6, 8, 11, 12, 13, 15 |
| US-07 | Redirect to setup when not initialized | 6, 13 |
| US-13 | Complete account setup via invitation link | 1, 2, 7, 9, 10, 11, 12, 16 |
| US-14 | Handle expired/invalid invitation token | 7, 9, 16 |
---
## Steps
### Step 1 — Extend `src/api/types.ts`
- [x] Add `SetupRequest` interface: `{ name: string; email: string; password: string; }`
- [x] Add `InvitationValidation` interface: `{ email: string; name: string | null; isValid: boolean; errorCode: 'EXPIRED' | 'USED' | 'NOT_FOUND' | null; }`
- [x] Add `InviteCompleteRequest` interface: `{ token: string; name: string; password: string; }`
---
### Step 2 — Create `src/lib/schemas/auth.ts`
- [x] Create file with shared Zod schemas:
- `passwordSchema``.min(8)` + 4 regex rules (uppercase, lowercase, digit, non-alphanumeric) matching backend `IdentityOptions` (BR-U2-0105)
- `loginSchema``{ email: z.string().email(), password: z.string().min(1) }` (extracted from LoginPage — see Step 14)
- `setupSchema``{ name, email, password, confirmPassword, locale }` with `.superRefine()` for password match (BR-U2-06); `locale` is `z.enum(['en', 'nl'])`
- `inviteCompleteSchema``{ name, password, confirmPassword }` with password match refine
---
### Step 3 — Create `src/components/ui/PasswordField.tsx`
- [x] Wrapper around shadcn `Input` with internal `showPassword: boolean` state
- [ ] Show/hide toggle button using lucide-react `Eye`/`EyeOff` icons
- [ ] Props: `id: string`, `placeholder?: string`, `autoComplete: string`, `...register` (spread from react-hook-form)
- [ ] `data-testid` on input: `{id}-input`; on toggle: `{id}-toggle`
- [ ] `aria-label` on toggle button for accessibility
---
### Step 4 — Create `src/components/ui/FormBannerError.tsx`
- [x] Props: `error: { message: string } | null`, `onDismiss: () => void`
- [ ] Returns `null` when `error` is `null`
- [ ] Uses shadcn Card or a `div` with `role="alert"`, `data-testid="form-error-banner"`, destructive color classes
- [ ] Dismiss `×` button calls `onDismiss`
---
### Step 5 — Create `src/components/auth/RoleGuard.tsx`
- [x] Props: `allowedRoles: UserRole[]`, `children: ReactNode`
- [ ] Reads `user` from `useAuth()`
- [ ] If `user.role` is in `allowedRoles` → render `children`
- [ ] Otherwise → render inline `AccessDeniedMessage` (heading + body naming the required roles + back-to-dashboard link)
- [ ] `data-testid="access-denied-message"` on the fallback element
---
### Step 6 — Create `src/api/useSetup.ts`
- [x] Module-level cache: `let _cachedStatus: SetupStatus | null = null;`
- [ ] `useSetupStatus()` hook:
- State: `{ status: SetupStatus | null; isLoading: boolean; error: Error | null }`
- Fetches `GET /Setup/status` on first call; subsequent calls return `_cachedStatus` synchronously
- [ ] `useCreateOwner()` hook — returns `{ mutate, isLoading, error }`:
- `mutate(data: SetupRequest): Promise<void>` calls `POST /Setup`
- On success: sets `_cachedStatus = { initialized: true }` (updates session cache)
- Throws on API/network error (caller handles display)
---
### Step 7 — Create `src/api/useInvitation.ts` (stub for Unit 2)
- [x] `useValidateInvitation(token: string | undefined)` hook:
- Fetches `GET /Invitation/validate?token={token}` when token is defined
- State: `{ data: InvitationValidation | null; isLoading: boolean; error: Error | null }`
- [ ] `useCompleteSetup()` hook — returns `{ mutate, isLoading, error }`:
- `mutate(data: InviteCompleteRequest): Promise<void>` calls `POST /Invitation/complete`
- Throws on error
---
### Step 8 — Extend `src/mocks/setup/handlers.ts`
- [x] Keep existing `GET /Setup/status` handler (returns `{ initialized: true }`)
- [ ] Add `POST /Setup` — success scenario: returns `201 Created` with empty body
- [ ] Add `POST /Setup` — already-initialized scenario: export as `setupAlreadyInitializedHandlers` (or a named variant) returning `409 Conflict` with ProblemDetails
---
### Step 9 — Create `src/mocks/invitation/handlers.ts`
- [x] `GET /Invitation/validate?token=valid-token``{ isValid: true, email: "invited@example.com", name: null }`
- [ ] `GET /Invitation/validate?token=expired-token``{ isValid: false, errorCode: "EXPIRED", email: null, name: null }`
- [ ] `GET /Invitation/validate?token=used-token``{ isValid: false, errorCode: "USED", email: null, name: null }`
- [ ] `POST /Invitation/complete` — success: `200 OK`
- [ ] Export as `invitationHandlers`
---
### Step 10 — Update `src/mocks/index.ts`
- [x] Import `invitationHandlers` from `./invitation/handlers`
- [ ] Add to the combined `handlers` array
- [ ] Add re-export line for `invitationHandlers`
---
### Step 11 — Update `src/i18n/locales/en/translation.json`
- [x] Add `setup` section:
```json
"setup": {
"title": "System Setup",
"subtitle": "Create the first Owner account to get started.",
"fields": {
"name": "Full name",
"email": "Email",
"password": "Password",
"confirmPassword": "Confirm password",
"locale": "Language"
},
"localeOptions": { "en": "English", "nl": "Dutch" },
"submit": "Create account",
"submitting": "Creating account…",
"success": "Account created. You can now sign in.",
"errors": {
"alreadyInitialized": "This system has already been set up.",
"generic": "Something went wrong. Please try again."
}
}
```
- [ ] Add `inviteComplete` section:
```json
"inviteComplete": {
"title": "Complete your account",
"subtitle": "You have been invited to {{appName}}.",
"loading": "Validating your invitation…",
"fields": {
"email": "Email",
"name": "Full name",
"password": "Password",
"confirmPassword": "Confirm password"
},
"submit": "Complete setup",
"submitting": "Completing setup…",
"success": "Account setup complete. You can now sign in.",
"errors": {
"expired": "This invitation link has expired. Please request a new one.",
"used": "This invitation link has already been used.",
"notFound": "This invitation link is not valid.",
"noToken": "Invalid invitation link.",
"generic": "Something went wrong. Please try again."
}
}
```
- [ ] Add to `nav`: `"profile": "Profile"`, `"settings": "Settings"`
- [ ] Add to `errors`: `"accessDenied": "You do not have permission to view this page."`, `"sessionExpired": "Your session has expired. Please sign in again."`
---
### Step 12 — Update `src/i18n/locales/nl/translation.json`
- [x] Mirror all keys from Step 11 with Dutch translations:
- `setup.title`: "Systeeminstallatie"
- `setup.subtitle`: "Maak het eerste Owner-account aan om te beginnen."
- `setup.fields.name`: "Volledige naam" / `email`: "E-mail" / `password`: "Wachtwoord" / `confirmPassword`: "Wachtwoord bevestigen" / `locale`: "Taal"
- `setup.localeOptions`: `{ "en": "Engels", "nl": "Nederlands" }`
- `setup.submit`: "Account aanmaken" / `submitting`: "Account aanmaken…" / `success`: "Account aangemaakt. Je kunt nu inloggen."
- `inviteComplete.title`: "Account voltooien" / `subtitle`: "Je bent uitgenodigd voor {{appName}}." / `loading`: "Uitnodiging valideren…"
- `inviteComplete.submit`: "Setup voltooien" / `success`: "Account-setup voltooid. Je kunt nu inloggen."
- All error messages in Dutch
- `nav.profile`: "Profiel" / `nav.settings`: "Instellingen"
- `errors.accessDenied`: "Je hebt geen toestemming om deze pagina te bekijken."
- `errors.sessionExpired`: "Je sessie is verlopen. Meld je opnieuw aan."
---
### Step 13 — Update `src/router.tsx`
- [x] Add module-level setup status cache and fetch function above `rootRoute`:
```ts
let _setupStatusPromise: Promise<SetupStatus> | null = null;
let _setupStatusCache: SetupStatus | null = null;
async function fetchSetupStatus(): Promise<SetupStatus> {
if (_setupStatusCache !== null) return _setupStatusCache;
if (_setupStatusPromise === null) {
_setupStatusPromise = apiClient
.get<SetupStatus>('/Setup/status')
.then((s) => { _setupStatusCache = s; return s; });
}
return _setupStatusPromise;
}
```
- [ ] Update `rootRoute` with `async beforeLoad` (InitGuard logic) and `pendingComponent`:
- On fetch success: if `!initialized && pathname !== '/setup'` → `throw redirect({ to: '/setup' })`
- On fetch success: if `initialized && pathname === '/setup'` → `throw redirect({ to: '/login' })`
- On fetch error: log warning, allow navigation (API calls will also fail)
- `pendingComponent: BootstrapSplash` (reuse existing component from main.tsx — import or duplicate inline)
- `pendingMs: 0` (show spinner immediately)
- [ ] Add `/invite/complete` route (public, under rootRoute):
- `path: '/invite/complete'`
- `validateSearch`: extract `token?: string`
- `component`: lazy `InviteCompletePage`
- [ ] Wrap `usersRoute` component with `RoleGuard` (allowedRoles: `['Owner', 'Administrator']`)
- [ ] Wrap `cmsRoute` component with `RoleGuard` (allowedRoles: `['Owner']`)
- [ ] Add `inviteCompleteRoute` to `routeTree` (alongside `setupRoute` and `loginRoute`)
- [ ] Import `RoleGuard` from `@/components/auth/RoleGuard`
- [ ] Import `apiClient` from `@/lib/api-client`
---
### Step 14 — Update `src/pages/LoginPage.tsx`
- [x] Remove inline `schema` definition; import `loginSchema` from `@/lib/schemas/auth`
- [ ] Change `useForm` mode to `mode: 'onTouched'` (inline validation after first blur; re-validates on change)
- [ ] Keep `reValidateMode: 'onChange'` (default) so errors clear as user types after the first blur
- [ ] No other structural changes; existing banner, submit button, and testids stay
---
### Step 15 — Replace `src/pages/SetupPage.tsx`
- [x] Full implementation replacing the "Coming soon" stub
- [ ] Import `setupSchema` from `@/lib/schemas/auth`
- [ ] Import `PasswordField` and `FormBannerError`
- [ ] Import `useCreateOwner` from `@/api/useSetup`
- [ ] Import `useTranslation` and `i18n` for locale live-switch
- [ ] Form fields: name, email, password (PasswordField), confirmPassword (PasswordField), locale (select)
- [ ] Locale `select` defaults to `i18n.language` if supported (`'en'` | `'nl'`), falls back to `'en'`
- [ ] `onChange` on locale select calls `i18n.changeLanguage(value)` immediately (BR-U2-23)
- [ ] Form mode: `onTouched` (BR-U2-32/33)
- [ ] On submit: call `useCreateOwner().mutate(...)`, handle success/error
- [ ] On success: show `t('setup.success')` in a success alert, then `navigate({ to: '/login', replace: true })` after 1500ms
- [ ] On error: set banner error with API message or network fallback
- [ ] `data-testid` attributes: `setup-name-input`, `setup-email-input`, `setup-locale-select`, `setup-submit-button`, `setup-success`, `setup-error-banner`
- [ ] `autoComplete="off"` on name; `"email"` on email; `"new-password"` on password fields
---
### Step 16 — Create `src/pages/InviteCompletePage.tsx`
- [x] Import `inviteCompleteSchema` from `@/lib/schemas/auth`
- [ ] Import `PasswordField` and `FormBannerError`
- [ ] Import `useValidateInvitation`, `useCompleteSetup` from `@/api/useInvitation`
- [ ] Extract `token` from search params using `useSearch({ strict: false })`
- [ ] Call `useValidateInvitation(token)` on mount; show loading spinner while `isLoading`
- [ ] If `!token` or `data.isValid === false`: show error state with `data.errorCode` mapped to i18n key + link to `/login`
- [ ] If `data.isValid === true`: render form with `email` pre-filled and read-only
- [ ] `name` field pre-filled from `data.name` if not null
- [ ] Form mode: `onTouched`
- [ ] On submit: call `useCompleteSetup().mutate(...)`, handle success/error
- [ ] On success: show `t('inviteComplete.success')`, navigate to `/login` after 1500ms
- [ ] `data-testid` attributes: `invite-loading`, `invite-error`, `invite-name-input`, `invite-email-input`, `invite-submit-button`, `invite-success`
---
### Step 17 — Create `src/pages/SetupPage.test.tsx`
- [x] Render SetupPage with MSW `setupHandlers` + `POST /Setup` success handler
- [ ] Test: all 5 fields render
- [ ] Test: submit with empty fields shows inline errors
- [ ] Test: weak password shows inline error
- [ ] Test: mismatched confirm password shows inline error
- [ ] Test: valid submission shows success message
- [ ] Test: 409 response shows "already initialized" banner
- [ ] Test: locale change updates visible text language
---
### Step 18 — Create `src/pages/InviteCompletePage.test.tsx`
- [x] Render InviteCompletePage with MSW `invitationHandlers`
- [ ] Test: loading state shown on mount
- [ ] Test: invalid token (`?token=expired-token`) shows error state, no form
- [ ] Test: missing token shows error state immediately
- [ ] Test: valid token (`?token=valid-token`) shows form with email read-only
- [ ] Test: successful submission shows success message
---
### Step 19 — Update `src/test/RouteGuard.test.tsx`
- [x] Add test: when `initialized: false` MSW returns `GET /Setup/status` with `{ initialized: false }` → navigating to `/login` or `/dashboard` redirects to `/setup`
- [ ] Add test: when `initialized: true` and visiting `/setup` → redirects to `/login`
- [ ] Add test: `RoleGuard` with `allowedRoles: ['Owner']` renders children when `user.role === 'Owner'`
- [ ] Add test: `RoleGuard` renders inline "Access Denied" when `user.role === 'User'`
---
### Step 20 — Create code generation summary
- [x] Create `aidlc-docs/features/cms-frontend/construction/unit-2/code/code-generation-summary.md`
- [ ] List all created and modified files
- [ ] Document any deviations from the plan
- [ ] Record story coverage
---
## Deviations from Functional Design (pre-noted)
| Design spec | Actual implementation | Reason |
|---|---|---|
| `useSetupStatus()` with `staleTime: Infinity` (TanStack Query style) | Module-level cache + `useState`/`useEffect` | TanStack Query not installed; equivalent session-cache behavior |
| `UserRole = 'Owner' \| 'Admin' \| 'User'` | `UserRole = 'Owner' \| 'Administrator' \| 'User'` | Matches existing `src/api/types.ts` and backend |
| InitGuard as component with `useEffect` | `async beforeLoad` on rootRoute | More idiomatic for TanStack Router; avoids render flash |
@@ -0,0 +1,230 @@
# Functional Design Plan — Unit 2: Authentication Pages
**Status**: ✅ Complete — awaiting approval
## Plan Context
- **Unit**: Unit 2 — Authentication Pages
- **Depends on**: Unit 1 (ApiClient, AuthContext, router.tsx, shadcn primitives, MSW handlers)
- **Stories Covered**: US-01, US-02, US-03, US-04, US-05, US-06, US-07, US-13, US-14
- **Stages to Execute**: Functional Design → Code Generation (NFR + Infrastructure skipped for this unit)
## Unit 1 Context (Already Delivered)
Unit 1 already provided the following that Unit 2 builds on:
- `src/contexts/AuthProvider.tsx` + `src/contexts/auth-context.ts``user`, `accessToken` (memory), `login`, `logout`, `refresh`
- `src/lib/api-client.ts` — fetch wrapper with 401 intercept + refresh retry
- `src/router.tsx``_authenticated` layout route with `beforeLoad` guard (redirects to `/login`); public routes: `/login`, `/setup`; protected routes: `/dashboard`, `/users`, `/cms`
- `src/pages/LoginPage.tsx` — fully implemented with react-hook-form + zod, MSW-tested, `data-testid` attributes
- `src/mocks/``authHandlers` (login, refresh, revoke), `setupHandlers` (setup status at `/Setup/status`), `userHandlers`
- Tailwind v4 + shadcn primitives (Button, Input, Label, Card, DropdownMenu, Toaster)
- Deviations: code-based routing in `router.tsx`; pages in `src/pages/`; locales in `src/i18n/locales/`
## Functional Design Steps
- [x] Step 1: Analyze unit stories and responsibilities
- [x] Step 2: Identify open questions (this document)
- [x] Step 3: Collect answers from user
- [x] Step 4: Generate functional design artifacts
---
## Questions
Answer each question by filling in the `[Answer]:` tag below it.
---
### Q1 — SetupPage: Fields to collect
The SetupPage is shown when the system is not yet initialized (`initialized: false` from `GET /Setup/status`).
The backend `POST /Setup` endpoint creates the first Owner account.
Which fields should the SetupPage form collect?
A) Email + Password + Confirm Password (name comes from the email prefix or can be set later in Profile)
B) Name + Email + Password + Confirm Password
C) Name + Email + Password + Confirm Password + a "language/locale" preference
D) Other
[Answer]: C
---
### Q2 — SetupPage: Action after successful setup
When `POST /Setup` succeeds and the first Owner account is created, what should happen?
A) Automatically log in the user (call `AuthContext.login()`) and redirect to `/dashboard`
B) Show a success message and redirect to `/login` so the user can log in manually
C) Show a success message on the same page with a "Go to login" button
D) Other
[Answer]: B
---
### Q3 — InviteCompletePage: Fields to collect
The InviteCompletePage is reached via a link like `/invite/complete?token=xxx`. The backend validates the token and completes account setup.
Which fields should the form collect?
A) Name + Password + Confirm Password (email is known from the invitation and shown read-only)
B) Password + Confirm Password only (name can be set later in Profile)
C) Name + Password + Confirm Password + email shown read-only as informational
D) Other
[Answer]: C
---
### Q4 — InviteCompletePage: Token validation timing
When should the invitation token be validated?
A) On page mount (GET request to validate token), then show form only if valid — show error page directly if invalid/expired
B) On form submit only — show validation errors after the user tries to submit
C) On page mount with a loading state, then switch to form (valid) or error state (invalid/expired)
D) Other
[Answer]: C
---
### Q5 — InviteCompletePage: Action after successful completion
When `POST /Invitation/complete` succeeds, what should happen?
A) Automatically log in the user and redirect to `/dashboard`
B) Show a success message and redirect to `/login`
C) Show a success message with a "Go to login" button
D) Other
[Answer]: B
---
### Q6 — InitGuard: Caching strategy
The `InitGuard` checks `GET /Setup/status` and redirects to `/setup` if `initialized: false`. It must run before every route renders (in the root route).
How should setup status be cached?
A) Cache for the session (once loaded, never re-fetch — a setup operation redirects back to login anyway)
B) Refresh every time the app is loaded/refreshed (staleTime: 0 — always re-fetch on mount)
C) Short stale time, e.g., 60 seconds (balance between freshness and extra requests)
D) Other
[Answer]: A
---
### Q7 — InitGuard: Routes that bypass the guard
Which routes should bypass the `InitGuard` (i.e., be accessible even when `initialized: false`)?
A) Only `/setup` bypasses the guard (all other routes redirect to `/setup` if not initialized)
B) `/setup` and `/invite/complete` bypass the guard
C) `/setup`, `/invite/complete`, and `/login` bypass the guard
D) Other
[Answer]: A
---
### Q8 — RoleGuard: Role model
The application has user roles. The backend returns a `role` field on the authenticated user.
What roles exist in the system?
A) Two roles: `Owner` and `User`
B) Three roles: `Owner`, `Admin`, and `User`
C) The roles match what's already defined in the backend (check `ApplicationUser` or `UserRole` enum in the existing code)
D) Other
[Answer]: D, This should already be defined in the back-end, but it should be `Owner`, `Admin` and `User`
---
### Q9 — RoleGuard: Which routes are role-restricted?
Which routes require a specific minimum role?
A) Only `/settings` and `/cms` require `Owner` role; `/users` requires `Owner` or `Admin`; `/dashboard` and `/profile` are accessible to all authenticated users
B) `/users`, `/settings`, and `/cms` all require `Owner` role; `/dashboard` and `/profile` for all roles
C) `/cms` requires `Owner`; `/settings` requires `Owner`; `/users` is open to all authenticated users
D) Other
[Answer]: A
---
### Q10 — RoleGuard: Unauthorized behavior
When an authenticated user tries to access a route they don't have the role for, what should happen?
A) Redirect to `/403` (Access Denied page — already in Unit 6 scope but can be a simple inline message for now)
B) Redirect to `/dashboard` with a toast notification
C) Show an inline "Access Denied" message on the page itself (no separate route needed)
D) Other
[Answer]: C
---
### Q11 — Password validation rules
The backend enforces specific password rules. Unit 1 already has Zod schemas.
Where should the shared password Zod schema live?
A) In `src/lib/validation.ts` — a shared module imported by both LoginPage and the new pages
B) In each page file separately (duplicated for isolation)
C) In `src/lib/schemas/auth.ts` — a dedicated auth schemas file
D) Other
[Answer]: C
---
### Q12 — i18n: Translation keys for Unit 2
Unit 1 already added `login` namespace keys. For Unit 2 pages, how should translation keys be organized?
A) Add `setup` and `inviteComplete` keys to the existing `translation.json` files (single namespace per locale)
B) Separate namespace files: `setup.json` and `inviteComplete.json` lazy-loaded per page
C) Extend existing `translation.json` with a `setup` section and `inviteComplete` section
D) Other
[Answer]: C
---
### Q13 — MSW handler scope for Unit 2
Unit 1 has `setupHandlers` for `GET /Setup/status`. Unit 2 needs handlers for `POST /Setup` (create owner) and invitation endpoints.
Where should the new handlers be added?
A) Extend existing `setupHandlers` in `src/mocks/setup/` with `POST /Setup` and add `invitationHandlers` in `src/mocks/invitation/`
B) Add everything to the existing `setupHandlers` file
C) New file `src/mocks/invitation/` for invitation handlers; `POST /Setup` goes into existing setup handlers
D) Other
[Answer]: A
---
### Q14 — Error handling in forms
For form submission errors (network errors, validation errors from the backend), what is the preferred pattern?
A) Show errors in a dismissible banner above the form (consistent with LoginPage's existing pattern)
B) Show errors inline next to the relevant field
C) Show errors in a toast notification (using Sonner, already installed)
D) Other
[Answer]: D, a combination of A and B. Isn't that also used on the login page. Inline for real live checking, but after submission a banner can be shown. I am not seeing that on the login page now. It should be consistent throughout the app though