Lucas Berger
63beb74650
docs(19-04): complete PWA login UI + account management surfaces plan
2026-06-17 17:26:07 -04:00
Lucas Berger
eba0bb095d
docs(19-05): complete dev-bypass rework + harness + CI plan (checkpoint)
2026-06-17 17:22:39 -04:00
Lucas Berger
19c45eb069
feat(19-04): AdminPage LOCAL ACCOUNTS + SettingsSheet change-password / link-OIDC
...
- Add hasLocalCredential to AdminMember type (mirrors API extension from plan 19-02)
- Add createMember mutation + Surface 11A inline add-member form in AdminPage
- Add Surface 11B Reset-password button in MemberRow (hasLocalCredential gate)
- Add ResetPasswordSheet component (bottom-sheet, role=dialog, focus-managed, Escape closes)
- Add Surface 12 Change-password row in SettingsSheet (hasLocalCredential gate)
- Add Surface 13 Link-OIDC identity row in SettingsSheet (hasLocalCredential + oidcEnabled gate)
- Add ChangePasswordSheet component (current/new/confirm fields, change-password mutation)
- Add LinkOidcSheet component (confirmation dialog; uses generic OIDC copy per D-06, no provider branding)
- Fix: update InstructionSheet.test.tsx to wrap with QueryClientProvider (Rule 1 - now uses useQuery)
- Fix: remove stale eslint-disable in App.test.tsx (lint --max-warnings 0 would fail)
- All 263 tests pass; typecheck clean; lint clean
2026-06-17 17:21:20 -04:00
Lucas Berger
1f94dc5eb7
feat(19-05): global-setup local_credentials seed + login.spec.ts + CI harness env
...
- global-setup.ts: TRUNCATE local_credentials + seed devuser/devpass (PHC scrypt inline)
- Create login.spec.ts: real-login-form e2e (gate redirect, wrong-password error, correct login)
- ci.yml: add LOCAL_SESSION_SECRET dev value + local_credentials seed step in harness job
- Fix all test mocks: add devSessionCookieMiddleware no-op to vi.mock(devBypass.js) blocks
in admin/setup/push/lists/localAuth/authMode/requireAdmin tests (Rule 1 - Bug: missing export)
- Full API suite: 446/446 tests pass; pnpm typecheck: exit 0
2026-06-17 17:21:05 -04:00
Lucas Berger
32d0408774
feat(19-04): LoginPage (Surfaces 1-10) + App.tsx authModeQuery gate + /login route
...
- Create LoginPage with BrandSlot, username/password form, show/hide toggle
- Four error states: invalid credentials, rate-limit, locked, server (all per UI-SPEC)
- OIDC method divider + 'Login with OIDC' button rendered only when oidcEnabled
- Accessibility: role=main, h1 in BrandSlot, h2 Sign in, aria-live error banner, 44px targets
- Focus management: username autofocus, Enter navigates username→password→submit
- App.tsx: add authModeQuery (queryKey ['authMode'], staleTime 60s)
- App.tsx: add /login standalone route (sibling of /setup, no AppNav/BottomTabBar)
- App.tsx: login gate after setup gate — meQuery error + localEnabled → Navigate /login
- App.tsx: OidcRedirect helper for OIDC-only mode (meQuery error + !localEnabled + oidcEnabled)
- Fix App.test.tsx to include fetchAuthMode mock and hasLocalCredential in user fixture
2026-06-17 17:14:18 -04:00
Lucas Berger
82391874ee
feat(19-05): add break-glass reset-admin CLI (dev-only, .dockerignore'd)
...
- Create apps/api/scripts/reset-admin.ts (AUTH-LOCAL-11, D-13, D-15)
- Dev-only guard as FIRST executable statement (NODE_ENV=production throws)
- Inline scrypt PHC hashPassword (cannot import compiled TS, Pitfall 11)
- Parse --username/--password from argv; never log password value (T-19-26)
- --dry-run validates args + DB connection without writing
- Upserts users (is_admin=true) + local_credentials rows idempotently
- Script excluded from prod image via .dockerignore apps/api/scripts/ (IMG-02)
- dry-run: exit=0, no password in output verified
2026-06-17 17:13:04 -04:00
Lucas Berger
3094df84c8
feat(19-05): Option C — devSessionCookieMiddleware issues real local-session cookie under bypass
...
- Add devSessionCookieMiddleware() to devBypass.ts (production hard-guard FIRST)
- Issues local-session JWT cookie for DEV_USER when no cookie present under bypass
- Pure no-op when NODE_ENV=production, DEV_AUTH_BYPASS!=true, or secret not set
- Mount devSessionCookieMiddleware() after devAuthBypass() in index.ts
- Existing devBypass tests: 3/3 pass; typecheck: exit 0
2026-06-17 17:11:31 -04:00
Lucas Berger
869cdc26c8
feat(19-04): add LoginError, fetchAuthMode/login/logout + auth fetchers, BrandSlot, brand-seam tokens
...
- Add LoginError class (4 codes: invalid/rate-limit/locked/server) mirroring SessionExpiredError shape
- Add hasLocalCredential to MeUser interface
- Add fetchAuthMode, fetchLocalLogin, fetchLocalLogout (pre-auth endpoints)
- Add fetchChangePassword, fetchCreateMember, fetchAdminResetPassword, fetchLinkOidc
- Create BrandSlot component with Phase-17-ready placeholder (48px circle, FS initials, h1, tagline)
- Add --brand-logo-* CSS custom properties to tokens.css (Phase 17 seam)
2026-06-17 17:10:30 -04:00
Lucas Berger
1cf572a2b9
docs(phase-19): update tracking after wave 3
2026-06-17 17:06:30 -04:00
Lucas Berger
b2f3182ef4
chore: merge executor worktree (worktree-agent-a11c9f6d35e8d8721)
2026-06-17 17:01:05 -04:00
Lucas Berger
e0d471a5d5
docs(19-03): complete local-auth middleware wiring plan
2026-06-17 17:00:42 -04:00
Lucas Berger
9b569efeab
feat(19-03): wire /callback link branch, OIDC-guard skip, de-Authelia comments
...
- /callback: reads signed state, extracts linkUserId, calls linkOidcToUser after OIDC session set; OidcLinkConflictError redirects to /?error=oidc-link-conflict
- OIDC guard: oidcAuthMiddleware() factory called once at construction, handler invoked per-request inside skip-when-user-set wrapper (D-03)
- middleware.ts: de-Authelia-ize comments — generic OIDC identity provider language (D-06, AUTH-LOCAL-18)
- localAuthMiddleware.ts: cast to typeof DEV_USER for ContextVariableMap type compatibility
- All 446 tests pass; typecheck clean
2026-06-17 16:59:06 -04:00
Lucas Berger
c437f408bb
feat(19-03): implement POST /api/auth/local/login (rate-limit + lockout) + logout
...
- Rate-limit: per-IP in-memory Map; 5 failures → 429, 10 → 423 (lockedOut)
- Counter increments even on 429 so brute-force accumulates toward lockout
- Timing-safe: DUMMY_HASH ensures verifyPassword runs for unknown usernames (T-19-12)
- noEchoHook: Zod errors never echo submitted values (T-19-14)
- Same 401 body for wrong-password and unknown-username (no enumeration)
- POST+GET /local/logout clear the local-session cookie
2026-06-17 16:49:59 -04:00
Lucas Berger
db66295920
test(19-03): add failing tests for POST /api/auth/local/login + logout
...
- RED: 8 tests for login success, wrong-password 401, unknown-username 401 (no enumeration), rate-limit 429, lockout 423, logout cookie clear, no-echo 400
2026-06-17 16:49:51 -04:00
Lucas Berger
be7a0aec90
feat(19-03): implement localAuthMiddleware, GET /api/auth/mode, and pre-auth route mounts
...
- localAuthMiddleware: cookie→c.set('user') with Pitfall-1 guard (no-set on no-cookie path)
- authMode: GET /api/auth/mode pre-auth endpoint (localEnabled:true, oidcEnabled from env+config)
- localAuth: POST /api/auth/local/login (rate-limit + timing-safe), logout routes
- index.ts: mount authModeRouter + localAuthRouter pre-auth; localAuthMiddleware after devAuthBypass; OIDC guard wrapped skip-when-user-set
2026-06-17 16:48:17 -04:00
Lucas Berger
ac32bd405f
test(19-03): add failing tests for localAuthMiddleware and GET /api/auth/mode
...
- RED: 8 tests failing (modules not yet created)
- localAuthMiddleware: 4 tests for cookie→user shape, no-cookie passthrough, missing user row, devAuthBypass coexistence
- authMode: 3 tests for mode response with no oidc, env oidc, app_config oidc
2026-06-17 16:48:07 -04:00
Lucas Berger
eb090bb57e
docs(phase-19): update tracking after wave 2
2026-06-17 16:42:17 -04:00
Lucas Berger
55cd5cf698
chore: merge executor worktree (worktree-agent-a177cd3aa5422e109)
2026-06-17 16:41:18 -04:00
Lucas Berger
f167031292
docs(19-02): complete admin+me account management plan summary
2026-06-17 16:40:50 -04:00
Lucas Berger
efb80c8c1a
feat(19-02): linkOidcToUser helper + POST /api/me/link-oidc initiation
...
apps/api/src/auth/linkOidc.ts (new):
- OidcLinkConflictError: thrown when iss+sub already belongs to a different user
- linkOidcToUser(userId, iss, sub): preflight SELECT for conflict, then db.transaction
(UPDATE users SET oidc_iss/sub/claimed + DELETE local_credentials); atomic, no email (D-10)
- 88 lines; no email in source (D-10/T-19-08 assertion passes)
apps/api/src/routes/me.ts:
- POST /api/me/link-oidc: resolveUserId (401 if null), sign state JWT
({ linkUserId, nonce, iat, exp } HS256 with LOCAL_SESSION_SECRET, 10-min window)
- Returns { signedState, authorizationUrl } — 19-03 /callback reads linkUserId from state
- authorizationUrl constructed from OIDC env vars when configured, null otherwise
- T-19-09: per-request nonce in state prevents CSRF/replay
2026-06-17 16:38:14 -04:00
Lucas Berger
8ced2d0a20
test(19-02): add failing tests for linkOidcToUser and POST /api/me/link-oidc
...
RED phase for Task 3:
- Test 1: linkOidcToUser updates users.oidc_iss/sub and deletes local_credentials
- Test 2: linkOidcToUser throws OidcLinkConflictError on conflict, no local_cred deletion
- Test 3: POST /api/me/link-oidc returns initiation payload (state / authorizationUrl)
2026-06-17 16:35:19 -04:00
Lucas Berger
c88f7d41e5
feat(19-02): self-change password and hasLocalCredential on GET /api/me
...
- POST /api/me/password: verifyPassword(current) gate before hashPassword(new) update
- 401 on wrong current password, 404 if no local_credentials row, 200 on success
- meNoEchoHook on /password route (T-19-06, never echo submitted password)
- resolveAdminAndSetupStatus extended with hasLocalCredential (AUTH-LOCAL-17)
- GET /api/me response includes hasLocalCredential alongside isAdmin/needsProviderSetup
2026-06-17 16:34:10 -04:00
Lucas Berger
80b5906bb8
test(19-02): add failing tests for self-change password and hasLocalCredential on /api/me
...
RED phase for Task 2:
- Test 1: POST /api/me/password correct current → 200, new hash verifies newPassword
- Test 2: wrong currentPassword → 401, UPDATE not called (hash unchanged)
- Test 3: no local_credentials row → 404
- Test 4 (GET /api/me): hasLocalCredential:true/false based on local_credentials existence
2026-06-17 16:32:48 -04:00
Lucas Berger
6232aa0d68
feat(19-02): admin create-member, reset-password, hasLocalCredential on GET /members
...
- POST /api/admin/members: atomic tx (users + local_credentials), 409 on dup username
- POST /api/admin/members/:id/password: admin reset (no current-pwd required), 404 if no local cred
- GET /api/admin/members: LEFT JOIN local_credentials, hasLocalCredential in each member row
- noEchoHook on both POST routes (T-19-06); requireAdmin via router.use('*') remains first statement
- Dup-entry detection via error message string match (Drizzle wraps mysql2 ER_DUP_ENTRY)
2026-06-17 16:31:37 -04:00
Lucas Berger
b2c7902e9e
test(19-02): add failing tests for admin create-member, reset-password, hasLocalCredential
...
RED phase for Task 1:
- Test 1: POST /api/admin/members creates users row + local_credentials, hash verifies
- Test 2: duplicate username returns 409, transaction rolled back (no orphaned user row)
- Test 3: admin reset password updates hash, old password no longer verifies
- Test 4: non-admin gets 403 on both POST /members and POST /members/:id/password
- Test 5: GET /api/admin/members returns hasLocalCredential:true/false per local cred existence
2026-06-17 16:29:43 -04:00
Lucas Berger
13e3757e88
docs(phase-19): update tracking after wave 1
2026-06-17 16:26:23 -04:00
Lucas Berger
12f5fb5991
chore: merge executor worktree (worktree-agent-a2e0909f9686032ab)
2026-06-17 16:24:55 -04:00
Lucas Berger
d22da015cb
docs(19-01): complete local-auth foundation plan (checkpoint reached at Task 4)
2026-06-17 16:19:25 -04:00
Lucas Berger
96f0991605
feat(19-01): add local_credentials schema, 0003 migration, generate-secrets LOCAL_SESSION_SECRET, .dockerignore D-15
...
- schema.ts: export localCredentials = mysqlTable('local_credentials', {...})
- user_id FK->users(cascade), username, password_hash, createdAt, updatedAt
- UNIQUE(user_id), UNIQUE(username), INDEX(user_id)
- 0003_warm_deathstrike.sql: purely additive CREATE TABLE (no ALTER/DROP/TRUNCATE on existing tables)
- Applied to dev DB: pnpm --filter @familysync/api db:migrate exits 0
- test/setup.ts: add localCredentials to afterEach delete cleanup (FK-safe ordering)
- generate-secrets.mjs: emit LOCAL_SESSION_SECRET (base64 32-byte, >=32 chars, D-05)
- .dockerignore: add apps/api/scripts/ exclusion (D-15/IMG-02) — entire break-glass dir excluded
2026-06-17 16:17:04 -04:00
Lucas Berger
7d61148415
feat(19-01): implement localSession JWT cookie helpers and assertLocalSessionSecretSet boot guard
...
- localSession.ts: issueLocalSessionCookie/verifyLocalSessionCookie/clearLocalSessionCookie
- Jwt namespace import from hono/utils/jwt (Pitfall 8 — not named sign/verify)
- Cookie name: 'local-session' (distinct from 'oidc-auth', Pitfall 4)
- httpOnly, sameSite=Lax, secure in production; try/catch on Jwt.verify (Pitfall 9)
- verifyLocalSessionCookie returns null (never throws) on any error
- bootGuards.ts: assertLocalSessionSecretSet — exit(1) if secret missing/<32 chars
- Exempt when DEV_AUTH_BYPASS=true (bypass doesn't issue local-session cookies)
- index.ts: wire assertLocalSessionSecretSet() after assertNotDevBypassInProduction()
- All 5 unit tests pass; typecheck exits 0
2026-06-17 16:14:48 -04:00
Lucas Berger
0d8f3fa051
test(19-01): add failing tests for localSession JWT cookie helpers and assertLocalSessionSecretSet
2026-06-17 16:13:33 -04:00
Lucas Berger
85b01b5c26
feat(19-01): implement hashPassword/verifyPassword with scrypt + timingSafeEqual
...
- node:crypto scrypt (N=16384, r=8, p=1, 32-byte output) — zero new dependencies (D-08)
- 16-byte random salt per hash; PHC-encoded format: scrypt$N$r$p$salt_b64url$hash_b64url
- timingSafeEqual for constant-time comparison (prevents timing oracle attacks, T-19-01)
- verifyPassword returns false on any error (never throws); passwords never logged
- All 5 unit tests pass (round-trip, wrong-pw, unique-salt, malformed-hash, PHC-shape)
2026-06-17 16:12:59 -04:00
Lucas Berger
7ece96688d
test(19-01): add failing tests for hashPassword/verifyPassword scrypt primitives
2026-06-17 16:12:14 -04:00
Lucas Berger
f96282a767
docs(19): add PATTERNS.md (codebase analog map for planning)
2026-06-17 16:08:36 -04:00
Lucas Berger
cb23603c83
docs(19): record phase planned (5 plans, 4 waves)
2026-06-17 16:04:38 -04:00
Lucas Berger
dc40ba9fb8
docs(19): create local-auth phase plan (5 plans, 4 waves)
2026-06-17 15:35:43 -04:00
Lucas Berger
4b461cbaab
docs(19): add validation strategy
2026-06-17 15:19:08 -04:00
Lucas Berger and Claude Sonnet 4.6
29f4a2e623
docs(19): research phase 19 local auth domain
...
Covers password hashing (node:crypto scrypt), JWT session cookies
(hono/utils/jwt), middleware ordering, local_credentials schema,
OIDC-link flow, dev-bypass rework (option C), and break-glass CLI.
Resolves all five open questions from CONTEXT.md.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com >
2026-06-17 15:17:46 -04:00
Lucas Berger and Claude Opus 4.8
9ef7eaada8
docs(state): record phase 19 UI-SPEC approval; ignore local claude settings
...
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com >
2026-06-16 21:21:25 -04:00
Lucas Berger and Claude Sonnet 4.6
4dd6068dcc
docs(19): mark UI-SPEC approved after checker verification
...
All 6 design dimensions PASS plus Phase 17 brand-slot readiness contract.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com >
2026-06-16 21:19:45 -04:00
Lucas Berger and Claude Sonnet 4.6
71bf21634c
docs(19): UI design contract for local auth login screen and admin additions
...
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com >
2026-06-16 21:18:04 -04:00
Lucas Berger
45fca0ed6b
docs(state): record phase 19 context session
2026-06-16 21:04:57 -04:00
Lucas Berger
64fa4653da
docs(19): capture phase context
2026-06-16 21:04:52 -04:00
luckberg
883ae48f8b
Merge pull request 'Phase 12: Initial Setup Wizard' ( #22 ) from gsd/phase-12-initial-setup-wizard into main
...
Publish / publish (push) Failing after 14m18s
Reviewed-on: #22
2026-06-16 19:10:31 -04:00
Lucas Berger and Claude Opus 4.8
7354f3ec4f
fix(12): unblock CI security + harness jobs
...
CI / changes (pull_request) Successful in 3s
CI / fast-checks (pull_request) Successful in 1m56s
CI / api (pull_request) Successful in 1m44s
CI / harness (pull_request) Successful in 6m28s
CI / security (pull_request) Successful in 1m11s
CI / gate (pull_request) Successful in 0s
security/gitleaks: allowlist apps/api/tests/routes/setup.test.ts — synthetic
VAPID test pair (verified absent from .env), same class as existing fixture
allowlist entries.
security/audit: waive GHSA-88fw-hqm2-52qc (hono CORS) — not exploitable, the
app uses no hono cors() middleware; newly-published vs pinned hono 4.12.23.
harness/e2e: seed app_config.setup_complete='true' + a dev-admin credential in
global-setup so the Phase-12 setup gate no longer redirects every spec to
/setup (was causing all 95 e2e failures) and no onboarding banner renders.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com >
2026-06-16 18:24:10 -04:00
Lucas Berger and Claude Opus 4.8
717c859f3c
fix(12): make api test suite hermetic — provide OIDC env so fallback skips DB
...
CI / changes (pull_request) Successful in 2s
CI / fast-checks (pull_request) Successful in 1m56s
CI / api (pull_request) Successful in 1m27s
CI / security (pull_request) Has been cancelled
CI / gate (pull_request) Has been cancelled
CI / harness (pull_request) Has been cancelled
oidcConfigFallbackMiddleware (Phase 12) reads OIDC config from app_config on
every /api/* request when OIDC_ISSUER/CLIENT_ID/AUTH_EXTERNAL_URL are absent.
CI's api job sets no OIDC env, so events/login tests (which mock db with a
partial query chain) 500'd on every request. Local runs passed only because
ambient .env supplied the vars. Set dummy OIDC config in vitest test.env so the
middleware always takes the env path — hermetic across CI and local.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com >
2026-06-16 17:43:48 -04:00
Lucas Berger and Claude Opus 4.8
a193bc8236
fix(12): satisfy CI fast-checks — lint unused vars, typed contract-test body, prettier
...
CI / changes (pull_request) Successful in 3s
CI / fast-checks (pull_request) Successful in 2m16s
CI / api (pull_request) Failing after 1m37s
CI / harness (pull_request) Failing after 1h3m45s
CI / security (pull_request) Failing after 11s
CI / gate (pull_request) Failing after 1s
- Remove unused 'res'/'container' assignments (no-unused-vars)
- setupClient.contract.test.ts: typed parseSentBody helper + non-async json mock
(no-unsafe-*/require-await)
- Prettier format 7 setup files
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com >
2026-06-16 16:53:14 -04:00
Lucas Berger and Claude Opus 4.8
f485b38324
docs(12): ship phase 12 — PR #22
...
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com >
2026-06-16 16:24:41 -04:00
Lucas Berger and Claude Opus 4.8
e821515d25
docs(12): resolve VERIFICATION human-needed — wizard e2e satisfied via UAT re-verify
...
CI / changes (pull_request) Successful in 4s
CI / fast-checks (pull_request) Failing after 1m4s
CI / api (pull_request) Failing after 1m30s
CI / harness (pull_request) Failing after 1h2m7s
CI / security (pull_request) Failing after 13s
CI / gate (pull_request) Failing after 1s
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com >
2026-06-16 16:23:15 -04:00
Lucas Berger and Claude Opus 4.8
932fcb6e3f
chore(12): mark Phase 12 complete — UAT re-verified, all 6 gaps closed
...
- ROADMAP/STATE advanced to Phase 13 (real-lint-gate-eslint)
- Archived diagnosed UAT marked superseded (historical only)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com >
2026-06-16 16:17:18 -04:00