Files
2026-05-22 15:12:02 +02:00

439 lines
23 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
phase: 2
slug: users-authentication
status: approved
shadcn_initialized: false
preset: none
created: 2026-05-22
---
# Phase 2 — UI Design Contract
> Visual and interaction contract for Phase 2: Users & Authentication.
> Generated by gsd-ui-researcher, verified by gsd-ui-checker.
---
## Design System
| Property | Value |
|----------|-------|
| Tool | none — raw Tailwind CSS v3 |
| Preset | not applicable |
| Component library | none |
| Icon library | Inline SVG heroicons (stroke, no fill) — matches existing AppSidebar and DocumentCard patterns |
| Font | System font stack (Tailwind default: ui-sans-serif, system-ui, sans-serif) |
**Source:** Detected from `frontend/package.json` (no icon library installed), `frontend/tailwind.config.js` (empty theme.extend), and existing component audit.
---
## Spacing Scale
Declared values (multiples of 4 only):
| Token | Value | Usage |
|-------|-------|-------|
| xs | 4px | Icon-to-label gaps, badge internal padding, inline list gaps |
| sm | 8px | Input icon padding, compact inline spacing |
| md | 16px | Default field padding, card internal padding, form row gap |
| lg | 24px | Section vertical gap, card-to-card gap |
| xl | 32px | Form block top margin, admin table row height padding |
| 2xl | 48px | Auth page vertical centering padding |
| 3xl | 64px | Auth card max-height clearance from viewport edge |
Exceptions:
- Touch targets (buttons, checkboxes, radio): minimum 44px height — enforced via `min-h-[44px]` on all interactive controls
- TOTP code input cells: 48px wide × 56px tall per digit cell (6 cells). Justification: 48px (scale) is too compact for touch-target comfort on a digit-only input; 64px wastes vertical space — 56px balances touch ergonomics and form compactness.
- Backup code grid cells and text inputs: 12px vertical/horizontal padding (`py-3 px-3` = 12px). Justification: 16px inflates form card height beyond max-w-sm on mobile viewports; 8px collapses touch targets below 44px when combined with text-sm line height — 12px is the ergonomic midpoint. All instances use the same value for consistency.
---
## Typography
| Role | Size | Weight | Line Height | Tailwind Class |
|------|------|--------|-------------|----------------|
| Body | 14px | 400 (regular) | 1.5 | `text-sm` |
| Label | 14px | 600 (semibold) | 1.4 | `text-sm font-semibold` |
| Heading | 20px | 600 (semibold) | 1.3 | `text-xl font-semibold` |
| Display | 24px | 600 (semibold) | 1.2 | `text-2xl font-semibold` |
**Source:** Extracted from `HomeView.vue` (`text-2xl font-bold` page heading, `text-sm` body, `text-lg font-semibold` section headings) and `DocumentCard.vue` (`font-medium text-sm`). Heading reduced from text-lg to text-xl for auth page section hierarchy.
Font weight scale: exactly 2 weights — regular (400) for body text and helper text; semibold (600) for headings, labels, CTAs, and display text. Differentiation between roles relies on SIZE, not weight. Do not use `font-medium` (500) or `font-bold` (700) anywhere in Phase 2 components.
---
## Color
| Role | Value | Tailwind Token | Usage |
|------|-------|----------------|-------|
| Dominant (60%) | #f9fafb | `bg-gray-50` | Page background — all views |
| Secondary (30%) | #ffffff | `bg-white` | Auth cards, form panels, admin table rows, sidebar |
| Accent (10%) | #4f46e5 | `indigo-600` / `indigo-700` | Reserved for: primary CTA buttons only, active nav link text, brand logo text |
| Accent subtle | #eef2ff | `indigo-50` | Active nav link background, icon container backgrounds |
| Destructive | #dc2626 | `red-600` | Deactivate user action, sign-out-all confirmation button, field validation errors |
| Warning | #d97706 | `amber-600` | Password strength: weak/medium indicator only |
| Success | #16a34a | `green-600` | Password strength: strong indicator, TOTP enrollment success |
| Neutral border | #e5e7eb | `gray-200` | Card borders, input default border, table dividers |
| Muted text | #9ca3af | `gray-400` | Helper text, placeholder text, secondary metadata |
**Source:** `style.css` (body bg-gray-50 text-gray-900), `AppSidebar.vue` (indigo-600 brand, indigo-50 active bg, indigo-700 active text), `DocumentCard.vue` (gray-200 border, hover:indigo-300). Destructive and warning added for auth-specific needs.
Accent reserved for: primary CTA buttons, active sidebar nav link text, DocuVault brand/logo text.
Accent is NOT used on: input focus rings (use `ring-indigo-500` only), links within prose, or secondary actions.
Input focus ring: `focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 focus:outline-none`
---
## Form Field States
All text inputs (`<input type="text|email|password">`) follow this contract:
| State | Border | Background | Ring | Text |
|-------|--------|------------|------|------|
| Default | `border-gray-300` | `bg-white` | none | `text-gray-900` |
| Focused | `border-indigo-500` | `bg-white` | `ring-2 ring-indigo-500` | `text-gray-900` |
| Error | `border-red-500` | `bg-white` | `ring-2 ring-red-500` | `text-gray-900` |
| Disabled | `border-gray-200` | `bg-gray-50` | none | `text-gray-400` |
| Read-only | `border-gray-200` | `bg-gray-50` | none | `text-gray-700` |
Base classes (all states share): `block w-full rounded-lg px-3 py-3 text-sm transition-colors`
Error messages appear **inline**, directly below the field, never as a toast. Class: `mt-1 text-xs text-red-600`. Maximum 1 error line per field.
---
## Password Strength Indicator
Displayed below the password input on Register and Password Reset (new password) screens only.
Visual contract:
- 4-segment horizontal bar, full width of the input
- Each segment: 4px tall, rounded, `gap-1` between segments
- Segments light up left-to-right based on score
| Score | Segments lit | Bar color | Label |
|-------|-------------|-----------|-------|
| 0 (empty) | 0 | — | hidden |
| 1 (too short / fails rules) | 1 | `bg-red-500` | "Too weak" |
| 2 (fails 2+ rules) | 2 | `bg-amber-500` | "Weak" |
| 3 (fails 1 rule) | 3 | `bg-amber-400` | "Fair" |
| 4 (all rules pass) | 4 | `bg-green-500` | "Strong" |
Rules checked client-side (≥12 chars, uppercase, lowercase, digit, special char — matches AUTH-01):
- Label: `text-xs font-semibold` at same color as the bar, right-aligned
- Unlit segments: `bg-gray-200`
- Layout: `mt-2 space-y-1`
HaveIBeenPwned breach check runs on blur (not on every keystroke). If the password is pwned, an inline error appears below the strength bar: "This password has appeared in a data breach. Choose a different password." — same `text-xs text-red-600` style. The strength bar is NOT cleared; both messages coexist.
---
## TOTP Enrollment Flow
**Step 1 — QR Code Display**
- QR code: 200px × 200px centered in a white card with `border border-gray-200 rounded-xl p-6`
- Manual secret: `font-mono text-sm text-gray-700 bg-gray-50 px-3 py-2 rounded-md border border-gray-200 break-all`
- Copy secret button: icon-only (clipboard SVG, w-4 h-4), `text-gray-400 hover:text-gray-600`, positioned inline right of the secret block. Must include `aria-label="Copy secret key"` for accessibility.
- Instruction copy: "Open your authenticator app and scan this QR code, or enter the key manually."
**Step 2 — Code Verification**
- Single 6-digit code input: rendered as one `<input type="text" inputmode="numeric" maxlength="6">` field, NOT 6 separate cells
- Width: `w-36` (144px), centered
- Error state on wrong code: field switches to error state + inline message "Incorrect code. Try again." — generic, no hint about time window
- On success: green checkmark icon (w-5 h-5 text-green-600) + "Authenticator connected." transition before proceeding to backup codes
**Step 3 — Backup Codes Display**
Layout: 2-column grid of codes (`grid grid-cols-2 gap-2`)
Each code cell:
- `font-mono text-sm text-gray-800`
- `bg-gray-50 border border-gray-200 rounded-md px-3 py-2 text-center`
- Do not number them (no 1., 2. prefix — prevents users from thinking order matters)
Copy-all button: `"Copy all codes"` — text button with clipboard icon, `text-indigo-600 hover:text-indigo-700 text-sm font-semibold`. On click: copies newline-separated codes to clipboard, button text changes to `"Copied"` for 2 seconds.
Acknowledgment checkbox (required before proceeding):
- Checkbox + label: `"I have saved these codes in a secure place. I understand they will not be shown again."`
- Checkbox class: `rounded border-gray-300 text-indigo-600 focus:ring-indigo-500`
- "Enable two-factor authentication" CTA button: disabled (`opacity-50 cursor-not-allowed`) until checkbox is checked
---
## Login Flow — TOTP Step
Login is a **two-step flow**, not a single form:
**Step 1:** Email + password fields + "Sign in" button
**Step 2 (only for TOTP-enrolled users):** Separate screen/card with:
- Heading: "Two-factor authentication"
- Helper: "Enter the 6-digit code from your authenticator app."
- Single code input (same spec as enrollment verification above)
- Secondary link below: "Use a backup code instead" — `text-sm text-indigo-600 hover:underline`
- Back link: "Back to sign in" — `text-sm text-gray-500 hover:text-gray-700`
Backup code entry: replaces the code input with `<input type="text" placeholder="XXXXXXXX">` and label "Enter a backup code". Same error state on invalid/already-used code, generic message: "Invalid or already used code."
The step transition (password accepted → TOTP screen) is NOT a page navigation. The card content swaps in place (v-if swap). URL stays at `/login`.
---
## Loading States
All async actions must show a loading state. Contract:
| Action | Loading indicator | Button state |
|--------|------------------|--------------|
| Login submit | Spinner (w-4 h-4, inline left of button label) | `disabled`, `opacity-75` |
| Register submit | Spinner inline | `disabled`, `opacity-75` |
| HaveIBeenPwned breach check | Spinner (w-3 h-3) right-aligned inside input field | Input remains editable |
| Password reset email send | Spinner inline | `disabled`, `opacity-75` |
| TOTP code verify | Spinner inline | `disabled`, `opacity-75` |
| Admin: create user | Spinner inline | `disabled`, `opacity-75` |
| Admin: deactivate user | Row-level spinner in action column | Row actions `pointer-events-none` |
| Admin: reset password | Row-level spinner | Row actions `pointer-events-none` |
| Admin: save quota | Spinner inline in edit cell | Save button `disabled` |
| Sign-out-all | Spinner inline | `disabled`, `opacity-75` |
Spinner: `animate-spin rounded-full border-2 border-current border-t-transparent` — inherits button text color. No external spinner library.
---
## Error Message Placement
| Error type | Placement | Style |
|------------|-----------|-------|
| Field validation (required, format, strength) | Inline, below field | `mt-1 text-xs text-red-600` |
| Server field error (email taken, invalid creds) | Inline, below affected field | `mt-1 text-xs text-red-600` |
| Form-level server error (generic 500, network) | Above the submit button, inside the card | `p-3 rounded-lg bg-red-50 border border-red-200 text-sm text-red-700` |
| Session expired (401 on any page) | Toast, top-right, 5s auto-dismiss | `bg-gray-900 text-white text-sm px-4 py-3 rounded-lg shadow-lg` |
| Rate limit hit | Inline form-level block | Same as form-level server error, with copy "Too many attempts. Try again in X minutes." |
| Success (password reset email sent) | Inline, replaces form | `p-4 bg-green-50 border border-green-200 text-sm text-green-800 rounded-lg` |
**Security copy rule:** Error messages MUST NOT confirm whether an email address exists in the system. Login failure: "Incorrect email or password." Password reset: "If an account exists for that email, you will receive a reset link shortly." — identical response for existing and non-existing emails (anti-enumeration).
---
## Auth Page Layout
Auth pages (`/login`, `/register`, `/reset-password`) share a single-column centered layout:
- Page background: `bg-gray-50 min-h-screen flex items-center justify-center`
- Card: `bg-white rounded-2xl shadow-sm border border-gray-200 p-8 w-full max-w-sm`
- Logo/brand above card: `text-xl font-semibold text-indigo-600 tracking-tight` centered, `mb-6`
- Card heading: `text-2xl font-semibold text-gray-900 mb-1`
- Card subheading: `text-sm text-gray-500 mb-6`
- No sidebar on auth pages — full-width layout
Primary focal point of all auth screens is the card heading (Display / 24px / text-gray-900 font-semibold); secondary anchor is the primary CTA button (indigo-600 bg).
Auth pages are NOT wrapped in the main `App.vue` sidebar layout. They use a bare layout with no navigation.
---
## Account View (/account)
Uses the standard sidebar + main content layout (same as HomeView).
Sections (rendered as stacked cards with `space-y-6`):
1. **Account information** — display-only: email, role badge
2. **Two-factor authentication** — TOTP status + enrollment/disable controls
3. **Change password** — current password + new password + strength indicator
4. **Sessions** — "Sign out all devices" with confirmation dialog
Role badge: `inline-flex items-center px-2 py-1 rounded text-xs font-semibold bg-indigo-100 text-indigo-700` for admin; `bg-gray-100 text-gray-600` for user.
**Sign-out-all confirmation dialog:**
- Not a browser `confirm()`. Rendered as an inline confirmation block that replaces the button on click.
- Copy: "This will sign you out of all devices, including this one. You will need to sign in again."
- Two buttons: "Keep signed in" (`text-gray-600 hover:text-gray-800 text-sm`) + "Sign out all devices" (`bg-red-600 hover:bg-red-700 text-white text-sm font-semibold px-4 py-2 rounded-lg`)
- After confirmation: loading state on the destructive button, then redirect to `/login`
---
## Admin View (/admin)
Uses standard sidebar layout. Admin link in `AppSidebar.vue` visible only when `useAuthStore().user?.role === 'admin'`. Admin nav link styled identically to other nav links (no special styling — admin access is not advertised).
**Sub-navigation (within AdminView):**
Horizontal tab strip below the page heading:
- Tab class (default): `px-4 py-2 text-sm font-semibold text-gray-500 hover:text-gray-700 border-b-2 border-transparent`
- Tab class (active): `px-4 py-2 text-sm font-semibold text-indigo-600 border-b-2 border-indigo-600`
- Strip: `flex border-b border-gray-200 mb-6`
- Tabs: "Users" | "Quotas" | "AI Config"
**Users Tab — Table:**
Table structure: full-width, `divide-y divide-gray-200`, inside a `bg-white rounded-xl border border-gray-200 overflow-hidden`
Columns: Email | Role | Status | Created | Actions
| Row state | Background | Status badge |
|-----------|------------|--------------|
| Active user | `bg-white` | `bg-green-100 text-green-700` "Active" |
| Deactivated user | `bg-gray-50` | `bg-gray-100 text-gray-500` "Deactivated" |
| Admin user (active) | `bg-white` | Same as active + role badge |
Row text: `text-sm text-gray-900` for email, `text-sm text-gray-500` for created date.
Actions column (per row): text links, separated by `·`
- Active row: "Reset password" · "Deactivate"
- Deactivated row: "Reactivate" (no reset password on deactivated accounts)
- Deactivate link: `text-red-600 hover:text-red-700 text-sm`
- Other action links: `text-indigo-600 hover:text-indigo-700 text-sm`
**Create User (Users tab):**
"Create user" button: positioned top-right of table, `bg-indigo-600 hover:bg-indigo-700 text-white text-sm font-semibold px-4 py-2 rounded-lg`.
Create user form renders as an inline panel above the table (not a modal), with: email field, temporary password field (pre-generated, read-only with copy button), role selector (dropdown: User / Admin).
**Quotas Tab — Inline Edit:**
Table columns: Email | Used | Limit | Usage % | Actions
Limit field is editable inline. Click "Edit" → limit cell becomes `<input type="number" min="1" step="1">` (in MB) with Save / Cancel buttons. Warning shown inline if new limit < current usage: `text-xs text-amber-600 mt-1 "New limit is below current usage (X MB). Existing documents will not be deleted, but uploads will be blocked."`
**AI Config Tab:**
Table columns: Email | AI Provider | AI Model | Actions
Provider and model: dropdown selectors (not free-text). Populated from the backend's supported provider list. "Save" button per row. No bulk edit.
---
## Sidebar Updates (Phase 2)
`AppSidebar.vue` gains two additions:
1. **Admin link** (bottom section, above Settings): conditionally rendered, `v-if="authStore.user?.role === 'admin'"` — shield SVG icon (w-4 h-4), label "Admin". Same nav-link styling.
2. **User identity footer**: below the settings link at very bottom.
- Layout: `flex items-center gap-3 px-4 py-3 border-t border-gray-100`
- Avatar: initials-based circle, 32px, `bg-indigo-100 text-indigo-700 text-xs font-semibold rounded-full w-8 h-8 flex items-center justify-center shrink-0`
- Email: `text-xs text-gray-600 truncate flex-1`
- Sign-out icon button: arrow-right-on-rectangle SVG (w-4 h-4), `text-gray-400 hover:text-gray-600`, `aria-label="Sign out"`
---
## Router Guard Feedback
When an unauthenticated user is redirected to `/login` from a protected route, the login card shows no banner by default. The router saves the `redirect` query param (`/login?redirect=/account`). After successful login, the router navigates to the saved route.
If the redirect was triggered by a 401 (token expired, not initial page load), show the session-expired toast (defined in Error Message Placement above) before the redirect completes.
---
## Copywriting Contract
| Element | Copy |
|---------|------|
| Register page heading | "Create your account" |
| Register page subheading | "Start managing your documents securely." |
| Register primary CTA | "Create account" |
| Login page heading | "Sign in to DocuVault" |
| Login page subheading | (none) |
| Login primary CTA | "Sign in" |
| Login: redirect to register | "Don't have an account? Create one" |
| Register: redirect to login | "Already have an account? Sign in" |
| Forgot password link | "Forgot your password?" |
| Password reset page heading | "Reset your password" |
| Password reset subheading | "Enter your email and we'll send you a reset link." |
| Password reset CTA | "Send reset link" |
| Password reset success | "If an account exists for that email, you will receive a reset link shortly. Check your inbox." |
| New password page heading | "Set a new password" |
| New password CTA | "Set password" |
| New password success copy | "Password updated. Please sign in." (redirects to /login, not auto-login — per AUTH-05) |
| TOTP step heading | "Two-factor authentication" |
| TOTP step helper | "Enter the 6-digit code from your authenticator app." |
| TOTP verify CTA | "Verify code" |
| TOTP backup code link | "Use a backup code instead" |
| TOTP enrollment heading | "Set up two-factor authentication" |
| TOTP backup codes heading | "Save your backup codes" |
| Backup codes subheading | "Store these codes somewhere safe. Each can only be used once if you lose access to your authenticator app." |
| Backup codes copy-all CTA | "Copy all codes" |
| Backup codes acknowledge checkbox | "I have saved these codes in a secure place. I understand they will not be shown again." |
| TOTP enable CTA | "Enable two-factor authentication" |
| Account page heading | "Account settings" |
| Sign-out-all label | "Sign out all devices" |
| Sign-out-all confirmation copy | "This will sign you out of all devices, including this one. You will need to sign in again." |
| Sign-out-all cancel CTA | "Keep signed in" |
| Sign-out-all destructive CTA | "Sign out all devices" |
| Admin page heading | "Admin panel" |
| Admin: create user CTA | "Create user" |
| Admin: deactivate action | "Deactivate" |
| Admin: reactivate action | "Reactivate" |
| Admin: reset password action | "Reset password" |
| Admin: deactivate confirmation | Inline: "Deactivate [email]? They will lose access immediately. Their data is preserved." + "Deactivate" / "Keep account" |
| Empty state (users table) | Heading: "No users yet" / Body: "Create the first user account to get started." |
| Generic server error | "Something went wrong. Please try again or contact support if the problem persists." |
| Login failure | "Incorrect email or password." |
| Rate limit error | "Too many attempts. Please wait a few minutes and try again." |
| TOTP wrong code | "Incorrect code. Try again." |
| Backup code invalid | "Invalid or already used code." |
| Password breach error | "This password has appeared in a data breach. Choose a different password." |
| Password too weak | "Password must be at least 12 characters and include uppercase, lowercase, a number, and a special character." |
**Security copy rules:**
- Never confirm or deny whether an email exists (password reset and login)
- Never include user email or PII in generic error messages
- Never state the number of remaining TOTP attempts
- Admin actions on user rows include the email in the confirmation to prevent mis-clicks, but error responses from the server must not echo credentials back
---
## Registry Safety
| Registry | Blocks Used | Safety Gate |
|----------|-------------|-------------|
| shadcn official | none | not applicable — shadcn not installed |
| third-party | none | not applicable |
No third-party component registries. All components are handwritten Vue 3 SFC with raw Tailwind CSS, consistent with the existing codebase pattern.
---
## Component Inventory (New — Phase 2)
Components to create, with their file paths:
| Component | Path | Description |
|-----------|------|-------------|
| LoginView | `src/views/auth/LoginView.vue` | Two-step login (password → TOTP) |
| RegisterView | `src/views/auth/RegisterView.vue` | Registration with strength indicator |
| PasswordResetView | `src/views/auth/PasswordResetView.vue` | Reset request form + success state |
| NewPasswordView | `src/views/auth/NewPasswordView.vue` | Token-gated new password form |
| AccountView | `src/views/AccountView.vue` | Account settings, TOTP, sign-out-all |
| AdminView | `src/views/AdminView.vue` | Admin panel with sub-navigation |
| AuthLayout | `src/layouts/AuthLayout.vue` | Bare centered layout (no sidebar) for auth pages |
| PasswordStrengthBar | `src/components/auth/PasswordStrengthBar.vue` | 4-segment strength indicator |
| TotpEnrollment | `src/components/auth/TotpEnrollment.vue` | QR + manual secret + code verify |
| BackupCodesDisplay | `src/components/auth/BackupCodesDisplay.vue` | Grid + copy-all + acknowledge checkbox |
| AppSpinner | `src/components/ui/AppSpinner.vue` | Inline CSS spinner, inherits color |
| ConfirmBlock | `src/components/ui/ConfirmBlock.vue` | Inline confirm/cancel replacement for destructive actions |
| AdminUsersTab | `src/components/admin/AdminUsersTab.vue` | User table + create user form |
| AdminQuotasTab | `src/components/admin/AdminQuotasTab.vue` | Quota inline edit table |
| AdminAiConfigTab | `src/components/admin/AdminAiConfigTab.vue` | AI provider/model dropdowns per user |
---
## Checker Sign-Off
- [x] Dimension 1 Copywriting: PASS
- [x] Dimension 2 Visuals: PASS
- [x] Dimension 3 Color: PASS
- [x] Dimension 4 Typography: PASS
- [x] Dimension 5 Spacing: PASS
- [x] Dimension 6 Registry Safety: PASS
**Approval:** approved — 2026-05-22