From 46ea9ef823c1072cc125b5e3e6e0106274f62459 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 25 Apr 2026 16:21:44 +0000 Subject: [PATCH] docs: generate implementation plan for decoupled signup and family onboarding Agent-Logs-Url: https://github.com/sullyTheDev/choreography/sessions/c4b55ba2-24c0-4371-b0b0-e5d32c62eeda Co-authored-by: alteclansing110 <12752445+alteclansing110@users.noreply.github.com> --- .github/copilot-instructions.md | 17 +- .../contracts/auth-contracts.md | 129 ++++++-------- specs/005-auth-system-refactor/data-model.md | 135 +++++++-------- specs/005-auth-system-refactor/plan.md | 162 +++++++----------- specs/005-auth-system-refactor/quickstart.md | 116 ++++--------- specs/005-auth-system-refactor/research.md | 75 ++++---- 6 files changed, 244 insertions(+), 390 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 432d602..b9ca0c7 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,6 +1,6 @@ -# choreography Development Guidelines +# choreography Development Guidelines -Auto-generated from all feature plans. Last updated: 2026-04-17 +Auto-generated from all feature plans. Last updated: 2026-04-25 ## UI System (Constitution Principle VI — enforced) All new components, interactive elements, and UX patterns MUST use **Skeleton v4** (`@skeletonlabs/skeleton-svelte`) + **Tailwind CSS v4** first. Custom HTML/CSS is only acceptable when Skeleton provides no equivalent. Do NOT use Flowbite, DaisyUI, or similar alongside Skeleton. @@ -15,6 +15,8 @@ All new components, interactive elements, and UX patterns MUST use **Skeleton v4 - TypeScript 5.x / Node.js 20 LTS + SvelteKit 2, Svelte 5 Runes, Drizzle ORM, `@skeletonlabs/skeleton-svelte` v4, Tailwind CSS v4, `@lucide/svelte` (004-add-redemption-dashboard) - TypeScript 5.x / Node.js 20 LTS + SvelteKit 2, Svelte 5 Runes, Drizzle ORM, `@skeletonlabs/skeleton-svelte` v4, Tailwind CSS v4, `@lucide/svelte`, `better-auth` (004-add-redemption-dashboard) - SQLite via Drizzle ORM + libsql (`drizzle-orm/libsql`) using existing `DATABASE_URL` (004-add-redemption-dashboard) +- TypeScript 5.x on Node.js 20+ with SvelteKit 2 / Svelte 5 + `@sveltejs/kit`, `better-auth`, `drizzle-orm`, `@libsql/client`, `@skeletonlabs/skeleton(-svelte)`, `pino` (copilot/break-out-user-signup-family-creation) +- SQLite/libsql via Drizzle schema (`user`, `families`, `family_members`, related domain tables) (copilot/break-out-user-signup-family-creation) - TypeScript 5.x / Node.js 20 LTS (001-choreography-mvp) @@ -35,15 +37,10 @@ npm test; npm run lint TypeScript 5.x / Node.js 20 LTS: Follow standard conventions ## Recent Changes -<<<<<<< HEAD +- 005-auth-system-refactor: Added TypeScript 5.x on Node.js 20+ with SvelteKit 2 / Svelte 5 + `@sveltejs/kit`, `better-auth`, `drizzle-orm`, `@libsql/client`, `@skeletonlabs/skeleton(-svelte)`, `pino` - 004-add-redemption-dashboard: Added TypeScript 5.x / Node.js 20 LTS + SvelteKit 2, Svelte 5 Runes, Drizzle ORM, `@skeletonlabs/skeleton-svelte` v4, Tailwind CSS v4, `@lucide/svelte`, `better-auth` -- 004-add-redemption-dashboard: Added TypeScript 5.x / Node.js 20 LTS + SvelteKit 2, Svelte 5 Runes, Drizzle ORM, `@skeletonlabs/skeleton-svelte` v4, Tailwind CSS v4, `@lucide/svelte` -- 003-family-unification: Added TypeScript 5.x / Node.js 20 LTS + SvelteKit 2, Svelte 5 Runes, Drizzle ORM, @skeletonlabs/skeleton-svelte v4, Tailwind CSS v4 -======= -- 004-add-redemption-dashboard: Added TypeScript 5.x / Node.js 20 LTS + SvelteKit 2, Svelte 5 Runes, Drizzle ORM, `@skeletonlabs/skeleton-svelte` v4, Tailwind CSS v4, `@lucide/svelte` -- 003-family-unification: Added TypeScript 5.x / Node.js 20 LTS + SvelteKit 2, Svelte 5 Runes, Drizzle ORM, @skeletonlabs/skeleton-svelte v4, Tailwind CSS v4 -- 002-skeleton-ui-migration: Added TypeScript 5.x / Node.js 20 LTS + SvelteKit 2, Svelte 5 Runes, Tailwind CSS v4, @skeletonlabs/skeleton v4, @skeletonlabs/skeleton-svelte v4, @tailwindcss/forms, @lucide/svelte ->>>>>>> 0387394a03b5892b571414cd8d2e86e1b7297175 +- 003-family-unification: Added TypeScript 5.x / Node.js 20 LTS + SvelteKit 2, Svelte 5 Runes, Drizzle ORM, `@skeletonlabs/skeleton-svelte` v4, Tailwind CSS v4 +- 002-skeleton-ui-migration: Added TypeScript 5.x / Node.js 20 LTS + SvelteKit 2, Svelte 5 Runes, Tailwind CSS v4, Skeleton v4 diff --git a/specs/005-auth-system-refactor/contracts/auth-contracts.md b/specs/005-auth-system-refactor/contracts/auth-contracts.md index f06a51f..80be01f 100644 --- a/specs/005-auth-system-refactor/contracts/auth-contracts.md +++ b/specs/005-auth-system-refactor/contracts/auth-contracts.md @@ -1,108 +1,89 @@ -# Contracts: Authentication Interfaces +# Contracts: Signup / Onboarding / Family-Creation Interfaces ## Scope -Defines runtime interface expectations for login UX, auth endpoints, session behavior, and failure handling for the better-auth migration. +Defines contract behavior for decoupled account signup, onboarding routing, and independent family creation after signup. -## 1. Environment Configuration Contract +## 1. Signup Contract (`/signup`) -### Inputs +### Request (form POST) -- `AUTH_MODE`: `local` | `oidc` | `both` (default `local`) -- `OIDC_ISSUER`: URL string -- `OIDC_ACCOUNT_CLAIM`: claim key string (default `email`) -- `OIDC_CLIENT_ID`: string -- `OIDC_CLIENT_SECRET`: string -- `OIDC_ISSUER_LABEL`: string (default `Single Sign-On`) -- `OIDC_ZERO_MATCH_POLICY`: `deny` | `provision` (default `deny`) -- `BETTER_AUTH_SECRET`: 32+ character secret +- Required inputs: + - `email` + - `password` + - `displayName` +- Optional inputs: + - `avatarEmoji` + - `pin` (if current kiosk/member flow still uses it for admin profile) +- Not accepted in decoupled path: + - `familyName` as required field during account creation -### Required combinations +### Success behavior -- `AUTH_MODE=local`: only `BETTER_AUTH_SECRET` required for auth engine operation. -- `AUTH_MODE=oidc`: all OIDC vars required. -- `AUTH_MODE=both`: local remains available if OIDC vars are invalid/missing. -- `OIDC_ZERO_MATCH_POLICY=deny`: no local match on first OIDC sign-in denies login with guidance. -- `OIDC_ZERO_MATCH_POLICY=provision`: no local match on first OIDC sign-in provisions auth user/account and allows login. +- Creates auth user + credential account. +- Creates authenticated session. +- Does **not** create `families` or `family_members` records. +- Redirects to `/onboarding` for first-run family setup. -## 2. Login UI Contract (`/login`) +### Error behavior -### Local mode (`AUTH_MODE=local`) +- Duplicate email -> `409` with user-facing duplicate-account message. +- Validation failures -> `400` with field-level/summary guidance. -- Must render local form fields (email + password). -- Must not render OIDC sign-in button. +## 2. Onboarding Routing Contract -### OIDC mode (`AUTH_MODE=oidc`) +### Guard rules -- Must render single OIDC sign-in button labeled with `OIDC_ISSUER_LABEL`. -- Must not render local password form. -- If OIDC config invalid, sign-in must be blocked with guidance message. +- If unauthenticated user accesses onboarding-required pages -> redirect to `/login`. +- If authenticated user has zero memberships -> redirect to `/onboarding` when attempting app routes requiring family context. +- If authenticated user has at least one membership and visits `/onboarding` root -> redirect to default app destination. -### Both mode (`AUTH_MODE=both`) +## 3. Family Creation Contract (Onboarding Action/API) -- Must render OIDC button first, then divider, then local form. -- If OIDC config invalid/missing, OIDC entry is hidden/disabled per UX decision and local form remains usable. +### Request -## 3. Auth API Route Contract +- Authenticated user submits: + - `familyName` (required) + - optional family setup metadata if later needed -### Route +### Transactional behavior -- `GET/POST /api/auth/[...all]` +- Create `families` row. +- Create `family_members` row linking current user as `admin`. +- Commit atomically (no partial family without membership). -### Behavior +### Response -- All auth lifecycle traffic (signin/signup/callback/signout/session) is handled by `better-auth` route handler. -- Existing app routes must rely on resolved auth session state from this single source. +- Success -> redirect to `/admin/family?new=1` (or equivalent first-admin landing page). +- Failure -> remain on onboarding with actionable error. -## 4. OIDC Account Linking Contract +## 4. Session Shape and Compatibility Contract -### Inputs +- Session identity remains sourced from `better-auth`. +- Family context fields must be treated as nullable/derivable until membership exists. +- Any route requiring `familyId` must resolve membership first and redirect if absent. -- OIDC callback payload including configured `OIDC_ACCOUNT_CLAIM`. +## 5. UI Contract -### Normalization - -- Claim comparison for local linking must apply: - - trim surrounding whitespace - - case-insensitive comparison - -### Outcomes - -- Exactly one local match: link identity to existing user. -- Zero local matches: apply `OIDC_ZERO_MATCH_POLICY` (`deny` -> block with guidance, `provision` -> create auth user/account mapping). -- No claim present: deny login with missing-claim configuration error. -- Multiple local matches: deny login with admin-action-required error. -- In all deny cases above: no account creation and no linking side effects. - -## 5. Session and Guard Contract - -### Session source - -- Authenticated session is provided by `better-auth` only. -- Legacy cookie-token table/session validation logic is removed. - -### Route protection - -- Authenticated route groups continue to redirect unauthenticated users to `/login`. -- Admin-only routes continue to enforce role checks. +- `/signup` presents account-creation-only copy and controls. +- `/onboarding` presents explicit next step to create/join family. +- All components remain Skeleton v4 + Tailwind based. ## 6. Observability Contract -### Required structured log events +### Required structured events -- `auth_oidc_config_invalid` -- `auth_oidc_claim_missing` -- `auth_oidc_link_ambiguous` +- `onboarding_required` +- `family_created_from_onboarding` +- `family_create_failed` -### Required fields (non-secret) +### Required metadata -- `authMode` -- `issuer` (if configured) -- `claimKey` +- `userId` - `path` -- `requestId` (or equivalent correlation key if available) -- `reason` +- `requestId` (or equivalent) +- `reason` (for failures) ### Security rules -- Never log client secret, access tokens, refresh tokens, or raw ID tokens. +- Never log passwords, secrets, or token material. diff --git a/specs/005-auth-system-refactor/data-model.md b/specs/005-auth-system-refactor/data-model.md index c45446d..70882c6 100644 --- a/specs/005-auth-system-refactor/data-model.md +++ b/specs/005-auth-system-refactor/data-model.md @@ -1,109 +1,98 @@ -# Data Model: Authentication System Refactor +# Data Model: Decoupled Signup, Onboarding, and Family Creation ## Overview -This feature introduces a dedicated authentication layer using `better-auth` while preserving existing family-domain entities. The model adds canonical auth entities and defines how they relate to existing household members. +This model keeps identity/auth entities in `better-auth` and shifts family creation to an explicit onboarding step. A user can exist in the system without a family membership immediately after signup. ## Entities -### 1. Auth User +### 1. Auth User (`user`) -- Purpose: Canonical identity record managed by `better-auth` for an application user. +- Purpose: Canonical signed-in identity. - Key fields: - - `id` (string, primary key) - - `email` (string, nullable for non-email identities depending on provider) - - `emailVerified` (boolean) - - `name` (string, optional display label) - - `image` (string, optional) - - `createdAt`, `updatedAt` (timestamps) + - `id` (PK) + - `email` (normalized, unique within credential provider behavior) + - `name` + - `avatarEmoji` (existing schema field for emoji/avatar representation) + - `isActive` + - `createdAt`, `updatedAt` - Validation rules: - - Email, when present, should be normalized to lowercase and trimmed. - - IDs are immutable. + - Email must be trimmed + lowercase before persistence checks. + - `name` required for local signup flow. - Relationships: - - One-to-many with Auth Account - - One-to-many with Auth Session - - One-to-one/optional mapping to existing Member for continuity in current app domain. + - 1:N Auth Account + - 1:N Auth Session + - 0:N Family Membership -### 2. Auth Session +### 2. Family (`families`) -- Purpose: Persistent authenticated session managed by `better-auth`. +- Purpose: Household container for shared chores/prizes/settings. - Key fields: - - `id` (string, primary key) - - `userId` (foreign key -> Auth User) - - `expiresAt` (timestamp) - - `ipAddress`, `userAgent` (optional metadata) - - `createdAt`, `updatedAt` (timestamps) + - `id` (ULID) + - `name` + - `leaderboardResetDay` + - `createdAt` - Validation rules: - - Session must reference an existing Auth User. - - Expired sessions are not accepted for authorization. + - Name required and non-empty. - Relationships: - - Many-to-one with Auth User. + - 1:N Family Membership + - 1:N Chores/Prizes/Activity domain data -### 3. Auth Account +### 3. Family Membership (`family_members`) -- Purpose: External or credential-based login identity linked to an Auth User. +- Purpose: Join table linking users to families with role. - Key fields: - - `id` (string, primary key) - - `userId` (foreign key -> Auth User) - - `providerId` (string; e.g., `oidc` or local-credential provider id) - - `accountId` (string; provider subject/claim identity) - - `accessToken`, `refreshToken`, `idToken` (optional, if stored by provider flow) - - `createdAt`, `updatedAt` (timestamps) + - `memberId` -> `user.id` + - `familyId` -> `families.id` + - `role` (`admin` | `member`) + - `joinedAt` - Validation rules: - - (`providerId`, `accountId`) must be unique. - - Account must reference an existing Auth User. + - Unique pair (`memberId`, `familyId`). + - First family creator from onboarding is assigned `admin`. - Relationships: - - Many-to-one with Auth User. + - N:1 Auth User + - N:1 Family -### 4. Verification/Credential Support Entities (if required by better-auth) +### 4. Onboarding Status (Derived) -- Purpose: Support password/email verification and recovery semantics depending on enabled plugins. -- Expected fields: token/value + expiry + identifier references. +- Purpose: Runtime state used for routing. +- Derived fields: + - `hasFamilyMembership` (boolean) + - `onboardingRequired` = authenticated AND `hasFamilyMembership=false` - Validation rules: - - Tokens must expire and be single-use where applicable. -- Relationships: - - Reference Auth User or identifier fields based on plugin requirements. - -### 5. Member (Existing Domain Entity) - -- Purpose: Existing family-domain user profile used by chores, prizes, assignments, and role logic. -- Key fields currently include: - - `id`, `displayName`, `email`, `passwordHash`, `pin`, `isActive`, `createdAt` -- Feature-specific role in migration: - - Remains source of family membership and app-domain behavior. - - Local email identities are matched for first-time OIDC account linking by normalized claim value. -- Relationship updates: - - Add deterministic mapping approach between Auth User and Member (direct FK or stable lookup mapping) during implementation design. + - Determined from `family_members` existence; no separate persistence required. ## Relationship Summary -- Auth User 1 -> N Auth Session - Auth User 1 -> N Auth Account -- Auth User 1 -> 0..1 Member mapping (implementation detail chosen in code/migration) -- Member N -> N Families (existing `family_members` join remains unchanged) +- Auth User 1 -> N Auth Session +- Auth User 1 -> 0..N Family Membership +- Family 1 -> N Family Membership ## State Transitions -### Sign-In State +### Account and Onboarding Lifecycle -1. Unauthenticated -2. Authenticated session created (local or OIDC) -3. Session refreshed/continued until expiry -4. Signed out or expired -> returns to unauthenticated +1. Anonymous user submits signup form. +2. Auth User (+ auth account/session) is created. +3. Membership check: + - No family membership -> redirect to onboarding. + - Has family membership -> proceed to app routes. +4. Onboarding family creation: + - Create family row. + - Create family_members row with role `admin`. + - Mark onboarding complete via derived membership state. +5. User lands on normal app home/admin destination. -### OIDC First-Login Linking State +### Existing User Sign-In -1. OIDC callback received with configured claim -2. Claim normalized (trim + case-fold) -3. Match local identifier: - - Exactly one match -> create/link Auth Account to existing user mapping - - Zero matches -> create new auth user per policy (if allowed by implementation scope) - - Multiple matches -> deny login and emit admin-action-required error -4. Session creation only after successful link decision +1. User signs in. +2. Membership check runs during route/session guard. +3. If membership missing, redirect to onboarding; otherwise continue normally. ## Data Integrity Rules -- No duplicate (`providerId`, `accountId`) auth accounts. -- No auth session without a valid auth user. -- Missing configured OIDC claim always hard-fails sign-in (no partial account creation). -- Ambiguous local matches always hard-fail sign-in (no auto-linking). +- No family should exist without at least one admin membership after successful onboarding completion transaction. +- No duplicate membership pairs (`memberId`, `familyId`). +- Signup must not create `families` rows directly. +- App routes that require `familyId` must gate or redirect before dereferencing family-dependent data. diff --git a/specs/005-auth-system-refactor/plan.md b/specs/005-auth-system-refactor/plan.md index 3f43448..52be97a 100644 --- a/specs/005-auth-system-refactor/plan.md +++ b/specs/005-auth-system-refactor/plan.md @@ -1,36 +1,63 @@ -# Implementation Plan: Authentication System Refactor (Local + Generic OIDC) +# Implementation Plan: Signup/Family Decoupling + Onboarding Routing -**Branch**: `005-auth-system-refactor` | **Date**: 2026-04-17 | **Spec**: [spec.md](spec.md) -**Input**: Feature specification from `specs/005-auth-system-refactor/spec.md` +**Branch**: `copilot/break-out-user-signup-family-creation` | **Date**: 2026-04-25 | **Spec**: `specs/005-auth-system-refactor/spec.md` +**Input**: Existing feature context for splitting user signup from family creation and routing newly created users into onboarding. ## Summary -Replace the in-house password/PIN plus cookie-session engine with `better-auth` as the single authentication layer while preserving self-hosted SvelteKit architecture and existing SQLite/Drizzle database. Deliver three runtime-driven login modes (`local`, `oidc`, `both`), generic OIDC support, deterministic local-account linking for first-time OIDC sign-in, and explicit operator-focused logging for OIDC misconfiguration scenarios. +Decouple account signup from family creation so users can register independently, create or join a family later, and be routed into onboarding immediately after first account creation/sign-in when no family membership exists. The implementation reuses the current SvelteKit + better-auth + Drizzle stack, introduces onboarding-aware session/route behavior, and preserves existing family workflows after membership is established. ## Technical Context -**Language/Version**: TypeScript 5.x / Node.js 20 LTS -**Primary Dependencies**: SvelteKit 2, Svelte 5 Runes, Drizzle ORM, `@skeletonlabs/skeleton-svelte` v4, Tailwind CSS v4, `@lucide/svelte`, `better-auth` -**Storage**: SQLite via Drizzle ORM + libsql (`drizzle-orm/libsql`) using existing `DATABASE_URL` -**Testing**: Vitest integration + Playwright e2e + SvelteKit type/lint checks (`npm test`, `npm run test:e2e`, `npm run lint`) -**Target Platform**: Self-hosted SvelteKit SSR web app (Node runtime, Docker optional) -**Project Type**: Full-stack web application (single SvelteKit project) -**Performance Goals**: Maintain login UX completion target from spec (`<=60s` in user tests), keep auth route/server action p95 under 300ms at family-scale workloads -**Constraints**: Must fully remove legacy custom auth/session flow, must use generic OIDC only (no vendor SDKs), must keep local fallback behavior in `AUTH_MODE=both` when OIDC is misconfigured, must produce structured DevOps logs for configuration/claim failures, must enforce `OIDC_ZERO_MATCH_POLICY` (`deny` or `provision`, default `deny`), must preserve Skeleton v4-first UI policy -**Scale/Scope**: Single-tenant family instances; expected active sessions per family in low hundreds; auth migration applies across all authenticated routes and admin/member role checks +**Language/Version**: TypeScript 5.x on Node.js 20+ with SvelteKit 2 / Svelte 5 +**Primary Dependencies**: `@sveltejs/kit`, `better-auth`, `drizzle-orm`, `@libsql/client`, `@skeletonlabs/skeleton(-svelte)`, `pino` +**Storage**: SQLite/libsql via Drizzle schema (`user`, `families`, `family_members`, related domain tables) +**Testing**: Vitest integration tests + Playwright e2e tests + `svelte-check` +**Target Platform**: Self-hosted Linux/containerized Node runtime +**Project Type**: Single SvelteKit web application +**Performance Goals**: Maintain current interactive UX expectations (auth/onboarding route loads in normal local-dev timings; no added blocking network round-trips beyond existing server actions) +**Constraints**: Preserve better-auth as canonical auth engine; preserve self-hostability; keep Skeleton v4 UI compliance; maintain backward compatibility for existing family-backed sessions +**Scale/Scope**: Single feature slice across auth/onboarding routes, DB associations, and tests for first-run onboarding + independent family creation flows -## Constitution Check +## Constitution Check (Pre-Design Gate) *GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.* -| Principle | Status | Notes | -|-----------|--------|-------| -| I. Family-Centered Product Slices | PASS | Login and account-linking behavior directly supports parent/member family workflows | -| II. Self-Hostable and Open by Default | PASS | Keeps SvelteKit + SQLite self-host stack; OIDC remains optional and generic | -| III. Privacy and Parent Control First | PASS | No new analytics/data export paths; identity handling remains first-party and least-data | -| IV. Test-First, Correct, Accessible Delivery | PASS | Plan includes integration/e2e acceptance coverage for all auth modes and failure scenarios | -| V. Observable Simplicity | PASS | Consolidates auth into one engine (`better-auth`) and adds explicit structured error logging | -| VI. Skeleton UI System First | PASS | Login page updates continue to use Skeleton + Tailwind patterns | +- **I. Family-Centered Product Slices**: PASS — user outcome is clearer account creation and smoother family setup flow. +- **II. Self-Hostable and Open by Default**: PASS — no hosted dependency added; current runtime/env model remains. +- **III. Privacy and Parent Control First**: PASS — no new personal-data collection; only existing signup/profile fields. +- **IV. Test-First, Correct, Accessible Delivery**: PASS (planning) — plan includes integration/e2e updates for decoupled signup/onboarding behavior. +- **V. Observable Simplicity**: PASS — changes stay in existing app boundaries; structured logging will cover onboarding/family creation failures. +- **VI. Skeleton UI System First**: PASS — onboarding/signup UI updates remain within Skeleton + Tailwind conventions. + +No constitutional violations identified; complexity tracking not required. + +## Phase 0: Research Output Summary + +Research completed in `research.md` and resolves all planning unknowns: +- Session semantics when user has no family membership +- Decoupled signup write-path and transaction boundaries +- Onboarding routing contract for new users +- Family creation API/action ownership and idempotency expectations +- Test strategy for split-signup and post-signup onboarding paths + +## Phase 1: Design & Contracts Output Summary + +- Data model defined in `data-model.md` for account-first onboarding and family membership lifecycle. +- Interface contracts defined in `contracts/auth-contracts.md` for signup, onboarding routing, and independent family creation. +- Operator/developer execution path documented in `quickstart.md`. +- Agent context refresh executed via `.specify/scripts/powershell/update-agent-context.ps1 -AgentType copilot`. + +## Constitution Check (Post-Design Re-Check) + +- **I. Family-Centered Product Slices**: PASS — design keeps onboarding and family setup directly tied to family admin workflow. +- **II. Self-Hostable and Open by Default**: PASS — no external managed service introduced. +- **III. Privacy and Parent Control First**: PASS — no expansion of data capture or third-party transmission. +- **IV. Test-First, Correct, Accessible Delivery**: PASS — contracts and quickstart explicitly require integration/e2e and accessibility checks for onboarding paths. +- **V. Observable Simplicity**: PASS — minimal schema/routing evolution with explicit logging points. +- **VI. Skeleton UI System First**: PASS — UI contract remains Skeleton-first. + +Post-design gates remain clear with no unresolved constitutional risks. ## Project Structure @@ -44,98 +71,29 @@ specs/005-auth-system-refactor/ ├── quickstart.md ├── contracts/ │ └── auth-contracts.md -└── tasks.md # Created later by /speckit.tasks +└── tasks.md ``` ### Source Code (repository root) ```text src/ -├── app.d.ts # UPDATE: App.Locals typing for better-auth session/user shape -├── hooks.server.ts # UPDATE: replace legacy validateSession flow with better-auth handler/session loading ├── lib/ -│ ├── auth.ts # NEW: betterAuth() server configuration -│ ├── auth-client.ts # NEW: Svelte client auth helper -│ └── server/ -│ ├── auth.ts # REMOVE/REWRITE: legacy hashing/session helpers no longer auth authority -│ └── db/ -│ ├── index.ts # REUSE existing DB connection for better-auth adapter -│ └── schema.ts # UPDATE: add/align users, sessions, accounts (and supporting auth tables) +│ ├── auth.ts +│ └── server/db/schema.ts ├── routes/ -│ ├── api/ -│ │ └── auth/ -│ │ └── [...all]/+server.ts # NEW: better-auth route handler │ ├── (auth)/ -│ │ └── login/ -│ │ ├── +page.server.ts # UPDATE: runtime auth-mode config + error reporting inputs -│ │ └── +page.svelte # UPDATE: mode-aware local/oidc/both UI rendering -│ └── logout/+page.server.ts # UPDATE: sign out via better-auth session API - -drizzle/ -├── 00xx_better_auth_refactor.sql # NEW migration(s): auth table alignment and data backfill/linking support -└── meta/_journal.json # UPDATE: migration journal entry +│ │ ├── login/ +│ │ └── signup/ +│ ├── (app)/ +│ │ ├── +layout.server.ts +│ │ └── admin/family/ +│ └── onboarding/ # planned onboarding route entry for no-family users +└── hooks.server.ts tests/ ├── integration/ -│ ├── auth-local.test.ts # NEW: local auth behavior -│ ├── auth-oidc.test.ts # NEW: oidc config/claim/linking behavior -│ └── auth-session-guards.test.ts # NEW/UPDATED: route guard behavior from better-auth session └── e2e/ - └── login-modes.test.ts # NEW: local/oidc/both rendering and fallback behavior ``` -**Structure Decision**: Keep a single SvelteKit application and replace existing auth internals in place, introducing only `better-auth` server/client configuration modules and a dedicated auth API catch-all route. - -## Phase 0: Research - -Research output: [research.md](research.md) - -Resolved technical decisions: - -1. Reuse existing Drizzle/libsql database connection (`src/lib/server/db/index.ts`) as the storage layer for `better-auth`; do not create a second auth database. -2. Implement `better-auth` as the sole session authority by removing cookie-token validation in `hooks.server.ts` and replacing with `better-auth`-driven session extraction. -3. Model OIDC account linking as deterministic claim matching after trim + case-fold normalization, with hard-fail behavior for duplicate matches and missing claims as required by spec clarifications. -4. Support first-time OIDC zero-match behavior via `OIDC_ZERO_MATCH_POLICY`: deny with guidance or provision a new auth user/account mapping. -5. Handle `AUTH_MODE=both` misconfiguration by suppressing OIDC UI affordance while preserving local sign-in, and emit structured operator logs with actionable fields. -6. Keep generic OIDC integration only; no provider-specific SDK adapters. - -## Phase 1: Design and Contracts - -Design outputs: - -1. Data model: [data-model.md](data-model.md) -2. Contracts: [contracts/auth-contracts.md](contracts/auth-contracts.md) -3. Developer quickstart: [quickstart.md](quickstart.md) - -Design highlights: - -1. Introduce/align `users`, `sessions`, and `accounts` tables for `better-auth` while preserving existing member/family domain data. -2. Add explicit migration/backfill path for linking pre-existing local users to new auth records without duplicate account creation. -3. Define endpoint/UI contracts for mode-driven login rendering, zero-match policy behavior, and auth callbacks. -4. Specify structured logging contract for OIDC misconfiguration, missing claim, and ambiguous-link failures. - -## Phase 2: Implementation Planning Approach - -Planned execution slices (for /speckit.tasks generation): - -1. Install/configure `better-auth` and wire server/client modules plus auth API route. -2. Refactor DB schema/migrations for `better-auth` tables and data-linking rules. -3. Replace legacy hook/session/login/logout flows with `better-auth` session and sign-in flows. -4. Implement `AUTH_MODE`-driven login UI behavior (`local`, `oidc`, `both`) including misconfiguration fallback and messaging. -5. Implement OIDC claim extraction, normalization, linking, zero-match policy handling, and failure handling rules from spec clarifications. -6. Add integration and e2e coverage for auth modes, linking, and error observability. - -## Constitution Check (Post-Design Re-check) - -| Principle | Status | Notes | -|-----------|--------|-------| -| I. Family-Centered Product Slices | PASS | Stories remain independently testable (local, oidc, hybrid/linking) | -| II. Self-Hostable and Open by Default | PASS | No hosted-only dependency required; OIDC optional | -| III. Privacy and Parent Control First | PASS | Identity data use remains minimal (issuer + account claim mapping) | -| IV. Test-First, Correct, Accessible Delivery | PASS | Contracts and quickstart define acceptance-focused test gates | -| V. Observable Simplicity | PASS | Single auth engine reduces bespoke complexity and adds structured logging hooks | -| VI. Skeleton UI System First | PASS | Login UX contract uses existing Skeleton/Tailwind components and patterns | - -## Complexity Tracking - -No constitution violations identified; complexity exemptions not required. +**Structure Decision**: Keep the existing single-project SvelteKit structure and add onboarding flow behavior in current auth/app route groups, avoiding new services or packages. diff --git a/specs/005-auth-system-refactor/quickstart.md b/specs/005-auth-system-refactor/quickstart.md index 99ca69a..d8719f0 100644 --- a/specs/005-auth-system-refactor/quickstart.md +++ b/specs/005-auth-system-refactor/quickstart.md @@ -1,96 +1,58 @@ -# Quickstart: Authentication System Refactor (better-auth) +# Quickstart: Decoupled Signup + Onboarding Family Creation ## Goal -Run the refactored authentication stack locally using existing SvelteKit + Drizzle + SQLite infrastructure, with local auth and optional generic OIDC. +Validate end-to-end behavior where account signup is independent from family creation, and new users are routed into onboarding before entering family-scoped app routes. -## 1. Install dependencies +## 1. Install and prepare ```bash npm install -npm install better-auth -``` - -## 2. Configure environment - -Start from existing environment file: - -```bash cp .env.example .env ``` -Required/updated auth settings: - -- `BETTER_AUTH_SECRET` (required, 32+ chars) -- `AUTH_MODE` (`local`, `oidc`, `both`; default `local`) -- `OIDC_ISSUER` (required when OIDC is enabled) -- `OIDC_ACCOUNT_CLAIM` (default `email`) -- `OIDC_CLIENT_ID` (required when OIDC is enabled) -- `OIDC_CLIENT_SECRET` (required when OIDC is enabled) -- `OIDC_ISSUER_LABEL` (default `Single Sign-On`) -- `OIDC_ZERO_MATCH_POLICY` (`deny` or `provision`; default `deny`) - -Keep existing values: - -- `DATABASE_URL` (reuse existing DB) +Set at minimum: +- `BETTER_AUTH_SECRET` +- `DATABASE_URL` - `ORIGIN` -- `PORT` -- `LOG_LEVEL` - -## 3. Create auth modules and route - -Implementation adds: -- `src/lib/auth.ts` (server-side `betterAuth()` setup) -- `src/lib/auth-client.ts` (Svelte client integration via `better-auth/svelte`) -- `src/routes/api/auth/[...all]/+server.ts` (auth endpoint handler) - -## 4. Apply auth migrations - -Use project migration flow, then run better-auth migration as required by feature design: +## 2. Apply migrations and run app ```bash npm run db:migrate -npx auth migrate -``` - -Notes: - -- Do not initialize a new database. Use existing `DATABASE_URL`. -- Ensure migration metadata/journal entries are updated when adding SQL migration files. - -## 5. Run the app - -```bash npm run dev ``` -Open `/login` and verify mode rendering: - -- `AUTH_MODE=local`: local email/password form only -- `AUTH_MODE=oidc`: OIDC button only -- `AUTH_MODE=both`: OIDC button first, divider, then local form +## 3. Validate decoupled signup flow -## 6. Validate critical behavior +1. Visit `/signup`. +2. Create a new account using email/password (+ display name). +3. Confirm signup success redirects to `/onboarding`. +4. Confirm no family-scoped page is rendered before onboarding is completed. -### Local +## 4. Validate independent family creation -- Valid local credentials sign in successfully. -- Invalid local credentials stay on login with clear error. +1. From `/onboarding`, submit family creation form. +2. Confirm family is created and user is inserted into `family_members` with `admin` role. +3. Confirm redirect to primary admin experience (for example `/admin/family?new=1`). -### OIDC +## 5. Validate routing guards -- Valid OIDC configuration allows provider sign-in. -- Missing claim denies login with missing-claim error. -- Ambiguous local matches deny login with admin-action-required error. -- Zero local match follows `OIDC_ZERO_MATCH_POLICY` (`deny` blocks with guidance, `provision` creates auth user/account mapping). +- Authenticated user with no family: + - Accessing `/admin/*` or `/member/*` redirects to `/onboarding`. +- Authenticated user with family: + - Accessing `/onboarding` redirects to normal app destination. +- Unauthenticated user: + - Accessing onboarding or app routes redirects to `/login`. -### Mixed mode (`both`) +## 6. Validate observability -- If OIDC config invalid, OIDC entry is suppressed and local login remains available. -- Structured config error logs are emitted for DevOps troubleshooting. +Ensure structured logs emit: +- `onboarding_required` +- `family_created_from_onboarding` +- `family_create_failed` (by forcing an invalid family creation request) -## 7. Run test suites +## 7. Run verification suites ```bash npm test @@ -98,22 +60,6 @@ npm run test:e2e npm run lint ``` -## 8. UI compliance - -All login UI components use **Skeleton v4** (`@skeletonlabs/skeleton-svelte`) + **Tailwind CSS v4**. -No Flowbite, DaisyUI, or custom CSS is used for auth-related UI elements. +## 8. UI compliance check -## 9. Integration test summary (post-refactor) - -All 90 integration tests pass after migration to better-auth: - -- `auth-local.test.ts`: 7 tests — local login load, actions, and AUTH_MODE fallback (T013/T042) -- `auth-oidc.test.ts`: 13 tests — OIDC mode flags, claim matching, error surfacing (T020/T022/T028-T030/T034/T038/T044/T047/T050) -- `auth-session-guards.test.ts`: 6 tests — session guard regressions for unauthenticated and role-based access (T039) -- All pre-existing integration tests: 64 tests — unchanged - -Run the full integration suite: - -```bash -npx vitest run tests/integration/ -``` +Signup and onboarding UI must use Skeleton v4 components/styles with Tailwind utilities only. diff --git a/specs/005-auth-system-refactor/research.md b/specs/005-auth-system-refactor/research.md index 6f6527e..a9e5159 100644 --- a/specs/005-auth-system-refactor/research.md +++ b/specs/005-auth-system-refactor/research.md @@ -1,64 +1,47 @@ -# Research: Authentication System Refactor (Local + Generic OIDC) +# Research: Signup/Family Decoupling and Onboarding Routing -## Decision 1: Use better-auth as the single auth/session engine +## Decision 1: Make signup account-only (no implicit family creation) -- Decision: Replace custom auth/session logic with `better-auth` as the only authentication authority for sign-in, callback handling, and session lifecycle. -- Rationale: The current stack spreads auth concerns across custom hashing/session helpers, route actions, cookie handling, and hook-based validation. Consolidating into one engine reduces drift, improves maintainability, and aligns with the feature requirement to fully remove legacy session middleware. +- Decision: Convert signup into pure user-account creation and authentication bootstrap, removing direct family row creation from the signup transaction. +- Rationale: This cleanly separates identity creation from household setup, enabling independent signup and future join/create flows. - Alternatives considered: - - Keep legacy session management and add OIDC beside it. Rejected because it creates dual session truth and violates FR-014. - - Build a new in-house auth manager around current code. Rejected because it repeats solved auth-engine concerns and increases long-term risk. + - Keep signup coupled to family creation with optional skip. Rejected because it preserves mixed responsibilities and increases edge-case branching. + - Add a second signup endpoint while retaining legacy path. Rejected due to duplicate UX and maintenance overhead. -## Decision 2: Reuse existing Drizzle/libsql database connection +## Decision 2: Route authenticated users without family membership to onboarding -- Decision: Configure `better-auth` against the existing Drizzle/libsql database (`DATABASE_URL`) and existing migration workflow. -- Rationale: This preserves current deployment assumptions, keeps self-hosting simple, avoids split persistence, and satisfies the explicit requirement not to stand up a new database. +- Decision: Add a guard path that redirects newly authenticated users with no `family_members` row to `/onboarding` until they create or join a family. +- Rationale: Centralized onboarding routing prevents broken assumptions in app layouts that currently expect `session.familyId` to exist. - Alternatives considered: - - Separate auth database. Rejected because it adds operational burden and synchronization complexity. - - In-memory auth/session state. Rejected because persistent sessions and multi-process deployment require durable storage. + - Scatter null-family checks across all app pages. Rejected because it is brittle and error-prone. + - Redirect to `/admin/family` directly. Rejected because that route currently assumes existing family context and is not an onboarding shell. -## Decision 3: Add better-auth tables via Drizzle migration and keep domain tables +## Decision 3: Introduce explicit onboarding state derived from membership -- Decision: Introduce/align `users`, `sessions`, and `accounts` auth tables (plus any required supporting auth tables), while retaining current family/chore/prize domain tables. -- Rationale: `better-auth` needs canonical auth tables, but family workflow data should remain stable. A migration/backfill path can preserve existing user continuity and avoid duplicate records. +- Decision: Model onboarding completion as a derived state: `hasFamilyMembership` = true when user has at least one family membership. +- Rationale: Avoids new persistence unless needed; membership is the source of truth and keeps schema changes minimal. - Alternatives considered: - - Replace existing domain `members` table with auth-native shape immediately. Rejected for this feature because it broadens risk and touches unrelated domain logic. - - Keep existing `sessions` table unchanged and map it directly. Rejected due to mismatch with better-auth lifecycle expectations. + - Persist separate onboarding status table. Rejected for added complexity and risk of state drift. + - Infer completion from first admin action. Rejected because family setup should be the explicit completion boundary. -## Decision 4: Mode-driven login behavior from environment configuration +## Decision 4: Keep family creation as authenticated onboarding action -- Decision: Implement runtime UI/flow behavior controlled by `AUTH_MODE` with defaults and supporting OIDC env vars (`OIDC_ISSUER`, `OIDC_ACCOUNT_CLAIM`, `OIDC_CLIENT_ID`, `OIDC_CLIENT_SECRET`, `OIDC_ISSUER_LABEL`). -- Rationale: This enables self-hosters to choose local-only, OIDC-only, or mixed operation without code edits and directly satisfies FR-001 through FR-008. +- Decision: Move family creation responsibility to onboarding action/API reachable only by authenticated users, then attach creator as admin in `family_members`. +- Rationale: Maintains existing security and ownership semantics while supporting independent family creation timing. - Alternatives considered: - - Build-time mode toggles. Rejected because self-hosted operators need runtime configurability. - - Hardcode provider naming and claim. Rejected because the feature requires generic/white-label OIDC support. + - Allow anonymous family creation before auth. Rejected due to abuse risk and orphaned data potential. + - Background auto-create default family post-signup. Rejected because it violates independent creation goal. -## Decision 5: Deterministic account linking and failure policy +## Decision 5: Preserve better-auth as canonical identity/session engine -- Decision: For first-time OIDC sign-in, normalize configured claim values by trim + case-folding before matching local user identifiers. Link only when there is exactly one match. Deny login when claim is missing or ambiguous. -- Rationale: This directly applies approved clarifications and prevents accidental account takeover or duplicate user creation. +- Decision: Continue using `better-auth` for signup/login/session and only adjust domain-layer linkage behavior. +- Rationale: Aligns with constitutional auth constraint and avoids reworking stable auth internals. - Alternatives considered: - - Fuzzy matching or alias expansion. Rejected because behavior becomes non-deterministic and increases collision risk. - - Auto-link to oldest match in ambiguous cases. Rejected because it is unsafe and non-transparent. + - Custom temporary pre-family session system. Rejected because it duplicates existing auth/session capabilities. -## Decision 6: Misconfiguration handling differs by auth mode +## Decision 6: Add observability around onboarding and family provisioning -- Decision: In `AUTH_MODE=oidc`, block sign-in with guidance when required OIDC settings are missing. In `AUTH_MODE=both`, suppress OIDC entry and keep local sign-in available. -- Rationale: This follows clarified requirements and prevents lockouts in mixed-mode deployments while preserving secure fail-closed behavior for OIDC-only deployments. +- Decision: Emit structured events for onboarding redirect decisions and family-creation outcomes (`onboarding_required`, `family_created_from_onboarding`, `family_create_failed`). +- Rationale: Family creation is a high-impact flow and requires operator visibility per Constitution Principle V. - Alternatives considered: - - Disable all login in both mode if OIDC is broken. Rejected because it unnecessarily blocks local access. - - Silently fallback to local without logs. Rejected because operators lose visibility into broken OIDC setup. - -## Decision 7: Structured observability for auth configuration and linking failures - -- Decision: Emit structured logs for OIDC misconfiguration, missing claim, and ambiguous claim matches with operator-useful context (mode, issuer, claim key, failure type, request correlation fields) while avoiding secrets. -- Rationale: Constitution Principle V requires observability for risky flows; auth misconfiguration has high operational impact. -- Alternatives considered: - - User-only error messaging without logs. Rejected because DevOps cannot diagnose deployment issues quickly. - - Verbose logs including credentials/tokens. Rejected for security reasons. - -## Decision 8: Keep generic OIDC only - -- Decision: Use only the generic OIDC capability of `better-auth`; do not add Google/GitHub/Auth0-specific SDKs. -- Rationale: Required by feature scope and keeps dependency surface minimal for self-hosters. -- Alternatives considered: - - Provider-specific SDK packages. Rejected as out of scope. + - No dedicated logs. Rejected due to weak diagnosis for failed first-run experiences.