Specifies form field states, password strength indicator, TOTP enrollment and backup codes patterns, loading states, error placement, admin table row states, copywriting (anti-enumeration copy), and full component inventory for Phase 2 frontend work. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
22 KiB
phase, slug, status, shadcn_initialized, preset, created
| phase | slug | status | shadcn_initialized | preset | created |
|---|---|---|---|---|---|
| 2 | users-authentication | draft | false | none | 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)
- Backup code grid cells: 8px horizontal padding, 12px vertical padding (between xs and sm)
Typography
| Role | Size | Weight | Line Height | Tailwind Class |
|---|---|---|---|---|
| Body | 14px | 400 (regular) | 1.5 | text-sm |
| Label | 14px | 500 (medium) | 1.4 | text-sm font-medium |
| Heading | 20px | 600 (semibold) | 1.3 | text-xl font-semibold |
| Display | 24px | 700 (bold) | 1.2 | text-2xl font-bold |
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 in components (regular 400 + semibold/bold 600/700). Labels use 500 (medium) as the only addition for form ergonomics.
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-2.5 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-1between 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-mediumat 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 - 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-800bg-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-medium. 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-lg font-bold text-indigo-600 tracking-tightcentered,mb-6 - Card heading:
text-2xl font-bold text-gray-900 mb-1 - Card subheading:
text-sm text-gray-500 mb-6 - No sidebar on auth pages — full-width layout
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):
- Account information — display-only: email, role badge
- Two-factor authentication — TOTP status + enrollment/disable controls
- Change password — current password + new password + strength indicator
- Sessions — "Sign out all devices" with confirmation dialog
Role badge: inline-flex items-center px-2 py-0.5 rounded text-xs font-medium 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: "Cancel" (
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-medium 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-medium text-gray-500 hover:text-gray-700 border-b-2 border-transparent - Tab class (active):
px-4 py-2 text-sm font-medium 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-medium 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:
-
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. -
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
- Layout:
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" |
| 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 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" / "Cancel" |
| 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
- Dimension 1 Copywriting: PASS
- Dimension 2 Visuals: PASS
- Dimension 3 Color: PASS
- Dimension 4 Typography: PASS
- Dimension 5 Spacing: PASS
- Dimension 6 Registry Safety: PASS
Approval: pending