Technical Specification — Login & Sign‑up (Vue)
0) Context & Goal
Success depends on high-coverage unit tests and adherence to clean-code practices.We are building a marketplace (similar to Sharetribe) where businesses list services and customers book them. This document specifies only the Login and Sign‑up pages for the Vue 3 SPA, with emphasis on security, validation, and developer‑friendly structure.
1) Tech Stack & Standards
- Framework: Vue 3 (Composition API +
<script setup>) - Router: Vue Router 4
- State: Pinia
- HTTP: Axios with interceptors
- Styling: Bootstrap CSS + Headless UI/Radix Vue
- Icons: Heroicons
- Forms/Validation: Vee‑Validate + Zod (examples use Zod)
- Testing: Vitest (unit), Playwright (E2E)
- Lint/Format: ESLint + Prettier
- Types: TypeScript (strict)
- i18n: Vue I18n (lazy locales)
2) Routes & Navigation
/login— login page/signup— sign‑up page (role selection)/auth/callback/:provider— optional social callback
- Authenticated visitors to
/loginor/signup→ redirect to/dashboardornext. - Support
?next=/listing/newdeep‑link redirection.
3) UI/UX Requirements
- Centered card, logo, dark/light support, responsive.
- WCAG 2.2 AA: labels, focus states, aria‑live for errors.
- Loading: disable submit + spinner during request.
- Errors: field‑level + global banner; no backend internals.
- Password visibility toggle; links to “Forgot password?” and cross‑nav between pages.
- Social buttons (Google/Apple) behind feature flag.
4) Forms & Validation
4.1 Login
- Fields:
email,password,rememberMe - Email must be RFC‑valid; password non‑empty.
4.2 Sign‑up
User types: customer, business (controls conditional fields).
- Common: firstName, lastName, email, password, confirmPassword, acceptTos, marketingOptIn
- Business only: businessName, website (URL), phone (E.164)
- Password policy hint: ≥8 chars; 3 of 4 classes (upper/lower/digit/symbol)
- Anti‑abuse: CAPTCHA (feature flag), triggered after N failures or risk score.
5) API Contracts (proposed)
Base URL: ${VITE_API_BASE}/api/v1
5.1 POST /auth/login
{
"email": "user@example.com",
"password": "••••••••",
"remember": true,
"captchaToken": "optional"
}
// 200
{
"user": {"id":"u_123","role":"customer","email":"user@example.com","firstName":"Ada","lastName":"Lovelace","emailVerified": true},
"tokens": {"access":"jwtAccessToken","refresh":"jwtRefreshToken"}
}
// 400/401
{"error":"INVALID_CREDENTIALS","message":"Email or password is incorrect"}
5.2 POST /auth/register
{
"role": "customer",
"firstName": "Ada",
"lastName": "Lovelace",
"email": "ada@example.com",
"password": "S3cure!Pass",
"business": {"businessName": "Ada Services", "website": "https://ada.services", "phone": "+12125551212"},
"acceptTos": true,
"marketingOptIn": false,
"captchaToken": "optional"
}
// 201
{
"user": {"id":"u_124","role":"customer","email":"ada@example.com","firstName":"Ada","lastName":"Lovelace","emailVerified": false},
"tokens": {"access":"jwtAccessToken","refresh":"jwtRefreshToken"},
"requiresEmailVerification": true
}
// Errors: EMAIL_IN_USE, WEAK_PASSWORD, VALIDATION_ERROR, CAPTCHA_REQUIRED
5.3 POST /auth/refresh
{ "refresh": "jwtRefreshToken" } → { "access": "newAccessToken" }
5.4 POST /auth/logout
{} → 204 No Content
5.5 POST /auth/forgot-password
{ "email": "user@example.com" } → 204 No Content (no enumeration)
5.6 POST /auth/reset-password
{ "token": "resetToken", "password": "NewPass!23" } → 204 No Content
Note: Prefer httpOnly cookies for refresh; JSON mode supported via flag.
6) Security & Privacy (Frontend)
- Access token in memory only; never localStorage.
- Refresh via httpOnly Secure cookie (SameSite=Lax) when possible.
- CSRF header if cookies used.
- Generic errors; no email enumeration.
- Respect
429and show retry countdown. - Do not log PII; mask/harden analytics.
7) State & Interceptors
- Pinia
useAuthStore()storesuser,isAuthenticated. - Axios request: attach
Authorization: Bearer <access>when present. - Response 401: one automatic refresh then retry; on fail →
logout(). - Optional
rememberMe: persist minimal user snapshot insessionStorage(no tokens).
8) Component Structure
/src
/features/auth
components/
AuthCard.vue
EmailInput.vue
PasswordInput.vue
RoleSelector.vue
TosCheckbox.vue
pages/
LoginPage.vue
SignupPage.vue
stores/
auth.store.ts
schemas/
login.schema.ts
signup.schema.ts
services/
auth.api.ts
9) Visual / Interaction Details
- Business fields expand/collapse (200ms) when role is
business. - Password strength meter (zxcvbn if allowed); guidance text.
- Error summary with anchors at top of form.
- Submit buttons full‑width on mobile; spinner during pending.
- Disable submit until form valid and ToS checked.
10) Analytics & Telemetry
- Provider: GA4 or PostHog (behind consent).
- Events:
auth_login_submit|success|error,auth_signup_submit|success|error - Props:
{ role, hasBusinessFields, errorCode?, source: query.next? } - Never send raw email; hash if needed.
11) i18n
- All strings via
$t(); English default. - Keys like
auth.email,auth.password,auth.role.business, etc. - RTL ready.
12) Accessibility Checklist
- Labels bound via
for/id. - Visible focus ring; keyboard navigable.
- Errors announced via
aria-live="polite". - Contrast ≥ 4.5:1 (text) and 3:1 (large).
13) Error Handling Matrix
| Scenario | UX Behavior |
|---|---|
| Wrong email/password | Generic banner: “Email or password is incorrect.” |
| Network down | Inline message with retry; keep inputs. |
| 429 throttling | Disable submit for Retry‑After seconds; show countdown. |
| Email in use | Field error on email + link to login. |
| Weak password | Field error + strength guidance. |
| Server 5xx | Toast + show request ID if available. |
14) Testing
Unit (Vitest)
- Schemas: happy + edge cases
- Store actions:
login,logout,refresh - Components render a11y attributes & states
E2E (Playwright)
- Login success/failure
- Signup (customer + business) happy paths
- Redirect with
next - Rate‑limit banner (mocked 429)
- Axe scan: no serious violations
15) DevOps & Config
- Vite env:
VITE_API_BASE,VITE_ENABLE_SOCIAL_LOGIN,VITE_ENABLE_CAPTCHA,VITE_ANALYTICS_KEY - Feature flags via config module.
- Error tracking: Sentry; scrub PII.
16) Acceptance Criteria (Definition of Done)
- Responsive pages; axe checks pass.
- Validation prevents invalid submits with clear messaging.
- Auth store populated; token refresh works.
nextdeep‑link works.- No tokens in localStorage.
- Unit coverage ≥ 80% (schemas + store); E2E happy paths green in CI.
17) Out of Scope (now)
- Actual OAuth for social login (UI may be present but disabled)
- Email verification screens
- Forgot/Reset password pages (API referenced only)
- SSO (SAML/OIDC)
18) Example Type Definitions (TypeScript)
// schemas/login.schema.ts
import { z } from 'zod';
export const LoginSchema = z.object({
email: z.string().email(),
password: z.string().min(1),
remember: z.boolean().optional().default(false)
});
export type LoginInput = z.infer<typeof LoginSchema>;
// schemas/signup.schema.ts
export const SignupSchema = z.object({
role: z.enum(['customer', 'business']).default('customer'),
firstName: z.string().min(1).max(80),
lastName: z.string().min(1).max(80),
email: z.string().email(),
password: z.string().min(8),
confirmPassword: z.string().min(8),
acceptTos: z.literal(true),
marketingOptIn: z.boolean().optional(),
business: z.object({
businessName: z.string().min(2).max(120),
website: z.string().url().optional(),
phone: z.string().optional()
}).optional()
}).refine((d) => d.password === d.confirmPassword, {
message: 'Passwords must match',
path: ['confirmPassword']
});
export type SignupInput = z.infer<typeof SignupSchema>;
19) Skeleton Components (Vue)
<!-- pages/LoginPage.vue -->
<template>
<AuthCard title="Sign in">
<VForm @submit="onSubmit" :validation-schema="schema">
<EmailInput name="email" />
<PasswordInput name="password" />
<div class="flex items-center justify-between">
<VCheckbox name="remember">Remember me</VCheckbox>
<RouterLink to="/forgot">Forgot password?</RouterLink>
</div>
<VButton :loading="pending" type="submit" class="w-full">Sign in</VButton>
<p class="mt-4 text-sm">No account? <RouterLink to="/signup">Create one</RouterLink></p>
</VForm>
</AuthCard>
</template>
<script setup lang="ts">
import { LoginSchema } from '@/features/auth/schemas/login.schema';
import { useAuthStore } from '@/features/auth/stores/auth.store';
import { useRoute, useRouter } from 'vue-router';
const router = useRouter();
const route = useRoute();
const schema = LoginSchema;
const auth = useAuthStore();
const pending = ref(false);
async function onSubmit(values: any) {
pending.value = true;
const ok = await auth.login(values);
pending.value = false;
if (ok) router.replace((route.query.next as string) || '/dashboard');
}
</script>
20) Open Questions
- Token transport: httpOnly cookie vs JSON refresh?
- CAPTCHA provider & thresholds?
- Analytics provider choice + consent policy?
- Email verification required before dashboard?
- Default post‑login route: global vs role‑specific?