docs(15-03): complete client auth UI and test updates plan

- SUMMARY.md with OIDC login redirect, auth hook cleanup, E2E seed, test updates
- STATE.md updated with decisions and session info
- ROADMAP.md updated with phase 15 progress
- Requirements AUTH-01, AUTH-02, AUTH-05 marked complete
This commit is contained in:
2026-04-04 20:56:09 +02:00
parent 689a56b2b7
commit 46ed547340
5 changed files with 588 additions and 18 deletions

View File

@@ -0,0 +1,423 @@
---
phase: 15-external-authentication
plan: 03
type: execute
wave: 3
depends_on: ["15-02"]
files_modified:
- src/client/routes/login.tsx
- src/client/hooks/useAuth.ts
- e2e/seed.ts
- tests/middleware/auth.test.ts
- tests/services/auth.service.test.ts
- tests/routes/auth.test.ts
autonomous: false
requirements: [AUTH-05, AUTH-01, AUTH-02]
must_haves:
truths:
- "Login page redirects users to Logto instead of showing a credential form"
- "useAuth hook returns OIDC-based user identity (sub string, not integer id)"
- "E2E seed script creates API keys directly without inserting into users table"
- "E2E tests authenticate via API key header, not Logto"
- "Unit tests for auth middleware and service pass without users/sessions tables"
artifacts:
- path: "src/client/routes/login.tsx"
provides: "Login page that redirects to /login (OIDC redirect)"
- path: "src/client/hooks/useAuth.ts"
provides: "Auth hooks without useLogin, useSetup, useChangePassword"
exports: ["useAuth", "useLogout", "useApiKeys", "useCreateApiKey", "useDeleteApiKey"]
- path: "e2e/seed.ts"
provides: "E2E seed without users table insert"
- path: "tests/middleware/auth.test.ts"
provides: "Middleware tests for three-way auth"
- path: "tests/services/auth.service.test.ts"
provides: "Service tests for API key functions only"
- path: "tests/routes/auth.test.ts"
provides: "Route tests for /me and /keys endpoints"
key_links:
- from: "src/client/hooks/useAuth.ts"
to: "/api/auth/me"
via: "apiGet fetch"
pattern: "apiGet.*api/auth/me"
- from: "src/client/routes/login.tsx"
to: "/login"
via: "window.location redirect to OIDC login"
pattern: "window.location|/login"
- from: "e2e/seed.ts"
to: "apiKeys table"
via: "direct insert"
pattern: "apiKeys"
---
<objective>
Update the client-side auth UI, auth hooks, E2E seed script, and all auth-related tests to work with the new OIDC-based authentication.
Purpose: The server-side auth was rewritten in Plan 02. This plan brings the client and tests into alignment -- login page redirects to Logto, hooks match new API responses, E2E tests use API keys per AUTH-05, and unit/integration tests validate the new auth architecture.
Output: Working client auth flow, passing unit tests, E2E-ready seed script.
</objective>
<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/15-external-authentication/15-CONTEXT.md
@.planning/phases/15-external-authentication/15-RESEARCH.md
@.planning/phases/15-external-authentication/15-01-SUMMARY.md
@.planning/phases/15-external-authentication/15-02-SUMMARY.md
@src/client/routes/login.tsx
@src/client/hooks/useAuth.ts
@e2e/seed.ts
@tests/middleware/auth.test.ts
@tests/services/auth.service.test.ts
@tests/routes/auth.test.ts
<interfaces>
<!-- New server API contracts from Plan 02 -->
GET /api/auth/me response (new shape):
```typescript
// Authenticated (OIDC session):
{ user: { id: string, email?: string }, authenticated: true }
// Not authenticated:
{ user: null, authenticated: false }
```
Note: user.id is now a string (Logto sub claim), NOT an integer.
GET /login behavior: Redirects to Logto OIDC provider (server-side redirect via @hono/oidc-auth)
GET /callback behavior: Processes OIDC callback, sets session cookie, redirects to /
GET /logout behavior: Revokes OIDC session, redirects to /login
API key routes unchanged:
GET /api/auth/keys -> ApiKeyListItem[]
POST /api/auth/keys { name: string } -> { id, name, key, prefix }
DELETE /api/auth/keys/:id -> { ok: true }
Auth middleware (from Plan 02):
```typescript
export async function requireAuth(c: Context, next: Next)
// Checks: X-API-Key header -> Bearer token -> OIDC session cookie
```
Auth service exports (from Plan 02):
```typescript
export async function createApiKey(db, name): Promise<{id, name, keyHash, keyPrefix, createdAt, rawKey}>
export async function verifyApiKey(db, rawKey): Promise<boolean>
export async function listApiKeys(db): Promise<{id, name, keyPrefix, createdAt}[]>
export async function deleteApiKey(db, id): Promise<void>
```
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Rewrite login page and auth hooks for OIDC</name>
<files>src/client/routes/login.tsx, src/client/hooks/useAuth.ts</files>
<read_first>
- src/client/routes/login.tsx (current login form with username/password)
- src/client/hooks/useAuth.ts (current hooks: useAuth, useLogin, useSetup, useChangePassword, useLogout, useApiKeys, useCreateApiKey, useDeleteApiKey)
- .planning/phases/15-external-authentication/15-CONTEXT.md (D-07: /login becomes redirect trigger, D-06: registration on Logto)
</read_first>
<action>
**Rewrite `src/client/hooks/useAuth.ts`:**
Per D-07 and D-06, remove hooks that relied on credential-based auth:
- Remove `useLogin` (no more POST /api/auth/login)
- Remove `useSetup` (no more POST /api/auth/setup)
- Remove `useChangePassword` (no more PUT /api/auth/password)
Update `useAuth`:
- Change `AuthState` interface: `user` is now `{ id: string; email?: string } | null` (id changed from number to string per Logto sub claim)
- Remove `setupRequired` field -- first-run setup is on Logto admin console
- New interface:
```typescript
interface AuthState {
user: { id: string; email?: string } | null;
authenticated: boolean;
}
```
Update `useLogout`:
- Change from `apiPost("/api/auth/logout", {})` to `window.location.href = "/logout"` (server-side OIDC logout via redirect)
- Since this is a redirect (not an API call), use a simple function instead of useMutation:
```typescript
export function useLogout() {
const logout = () => {
window.location.href = "/logout";
};
return { logout };
}
```
Keep unchanged: `useApiKeys`, `useCreateApiKey`, `useDeleteApiKey` (API key CRUD routes are the same).
Final exports: `useAuth`, `useLogout`, `useApiKeys`, `useCreateApiKey`, `useDeleteApiKey`.
**Rewrite `src/client/routes/login.tsx`:**
Per D-07: The login page becomes a redirect trigger to Logto, not a credential form.
Replace the entire form with a simple page that:
1. On mount, checks if user is already authenticated via `useAuth()`
2. If authenticated, redirects to `/` via TanStack Router `navigate`
3. If not authenticated, shows a centered card with "Sign in to GearBox" heading and a "Sign in" button
4. The "Sign in" button sets `window.location.href = "/login"` which triggers the server-side OIDC redirect to Logto
```typescript
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { useEffect } from "react";
import { useAuth } from "../hooks/useAuth";
export const Route = createFileRoute("/login")({
component: LoginPage,
});
function LoginPage() {
const navigate = useNavigate();
const { data: auth, isLoading } = useAuth();
useEffect(() => {
if (auth?.authenticated) {
navigate({ to: "/" });
}
}, [auth, navigate]);
if (isLoading) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<p className="text-gray-500 text-sm">Loading...</p>
</div>
);
}
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center px-4">
<div className="w-full max-w-sm">
<h1 className="text-xl font-semibold text-gray-900 text-center mb-6">
Sign in to GearBox
</h1>
<div className="bg-white rounded-xl border border-gray-100 p-6 space-y-4">
<p className="text-sm text-gray-500 text-center">
You will be redirected to sign in with your account.
</p>
<button
type="button"
onClick={() => { window.location.href = "/login"; }}
className="w-full py-2 px-4 text-sm font-medium text-white bg-gray-700 hover:bg-gray-800 rounded-lg transition-colors"
>
Sign In
</button>
</div>
</div>
</div>
);
}
```
Note: The client route is `/login` (TanStack Router) and the server route is also `GET /login` (OIDC redirect). The client-side route renders the UI. When the user clicks "Sign In", `window.location.href = "/login"` does a full-page navigation to the server's GET /login which triggers the OIDC redirect to Logto. This works because in dev mode, Vite proxies unmatched paths to the Hono server, and in production, the SPA serves index.html for client routes but the server handles `/login` before the SPA fallback.
**IMPORTANT:** Check `src/server/index.ts` from Plan 02 -- the server-side `/login` route must be registered BEFORE the SPA static file fallback so it takes priority.
</action>
<verify>
<automated>! grep -q "useLogin\|useSetup\|useChangePassword" src/client/hooks/useAuth.ts && grep -q "authenticated" src/client/hooks/useAuth.ts && ! grep -q "setupRequired" src/client/hooks/useAuth.ts && ! grep -q 'id: number' src/client/hooks/useAuth.ts && grep -q "window.location.href" src/client/routes/login.tsx && ! grep -q "handleSubmit" src/client/routes/login.tsx && echo "PASS" || echo "FAIL"</automated>
</verify>
<acceptance_criteria>
- src/client/hooks/useAuth.ts does NOT export `useLogin`, `useSetup`, or `useChangePassword`
- src/client/hooks/useAuth.ts AuthState has `user: { id: string; email?: string } | null`
- src/client/hooks/useAuth.ts AuthState has `authenticated: boolean` (not `setupRequired`)
- src/client/hooks/useAuth.ts useLogout uses `window.location.href = "/logout"` (not apiPost)
- src/client/hooks/useAuth.ts DOES export `useAuth`, `useLogout`, `useApiKeys`, `useCreateApiKey`, `useDeleteApiKey`
- src/client/routes/login.tsx does NOT contain a `<form>` element
- src/client/routes/login.tsx does NOT contain username/password `<input>` elements
- src/client/routes/login.tsx DOES contain `window.location.href = "/login"` in button onClick
- src/client/routes/login.tsx DOES import `useAuth` from hooks
</acceptance_criteria>
<done>Login page redirects to Logto via server, auth hooks match new OIDC-based API responses, no credential forms remain</done>
</task>
<task type="auto">
<name>Task 2: Update E2E seed script and auth-related tests</name>
<files>e2e/seed.ts, tests/middleware/auth.test.ts, tests/services/auth.service.test.ts, tests/routes/auth.test.ts</files>
<read_first>
- e2e/seed.ts (current seed creates user with password hash in users table)
- tests/middleware/auth.test.ts (current tests for requireAuth middleware)
- tests/services/auth.service.test.ts (current tests for user/session/apiKey service functions)
- tests/routes/auth.test.ts (current tests for auth routes)
- tests/helpers/db.ts (test database setup helper)
- src/server/middleware/auth.ts (new middleware from Plan 02 -- to understand what to test)
- src/server/services/auth.service.ts (new service from Plan 02 -- only API key functions)
- src/server/routes/auth.ts (new routes from Plan 02 -- /me and /keys)
</read_first>
<action>
**Update `e2e/seed.ts`:**
Per AUTH-05 and Pitfall 4: E2E tests authenticate via API keys, no Logto dependency.
1. Remove the user creation block:
```typescript
// DELETE THIS:
const passwordHash = await Bun.password.hash("password123");
db.insert(schema.users).values({ username: "admin", passwordHash }).run();
```
2. Add API key creation instead:
```typescript
// Create API key for E2E test authentication
const rawKey = "e2e-test-api-key-for-gearbox-testing";
const keyHash = await Bun.password.hash(rawKey);
const keyPrefix = rawKey.slice(0, 8);
db.insert(schema.apiKeys)
.values({ name: "E2E Test Key", keyHash, keyPrefix })
.run();
```
3. Remove `import { users } from "../src/db/schema"` if it was used only for user creation. The seed script imports `* as schema`, so just remove the `schema.users` usage.
4. The seed script still uses `bun:sqlite` and Drizzle SQLite adapter for now (E2E tests run against SQLite). This is fine -- the `users` table won't exist in the generated schema migration. However, the seed script uses `migrate(db, { migrationsFolder: "./drizzle" })` which will apply the latest migration that drops the users table. So removing the users insert is necessary to prevent a "table not found" error.
**IMPORTANT:** The seed script will also need to handle that the `sessions` table is dropped. Verify there are no references to `schema.sessions` in the seed script (there shouldn't be based on current code).
**Update `tests/services/auth.service.test.ts`:**
Remove ALL tests for removed functions:
- Tests for `createUser`, `verifyPassword`, `getUserCount`, `changePassword`
- Tests for `createSession`, `getSession`, `deleteSession`, `refreshSession`
Keep ALL tests for API key functions:
- Tests for `createApiKey`, `verifyApiKey`, `listApiKeys`, `deleteApiKey`
Update imports to only import the kept functions from `auth.service.ts`. Remove imports of `users`, `sessions` from schema if present.
The test db helper creates tables from migrations, so after Plan 01's migration drops users/sessions, the test DB won't have those tables either. API key tests should work unchanged.
**Update `tests/middleware/auth.test.ts`:**
The middleware now has three auth paths. Rewrite tests:
Remove:
- Tests for `setup_required` response (getUserCount === 0 case -- removed)
- Tests for cookie session auth path
- Any mocking of `getSession`, `refreshSession`, `getUserCount`
Update/Add:
- Test: API key in `X-API-Key` header -> valid -> 200 (keep existing)
- Test: API key in `X-API-Key` header -> invalid -> 401 (keep existing)
- Test: Bearer token in Authorization header -> valid -> 200 (new)
- Test: Bearer token in Authorization header -> invalid -> 401 (new)
- Test: No auth headers, no OIDC session -> 401 (update existing)
- Test: OIDC session exists -> 200 (new -- mock `getAuth` from @hono/oidc-auth)
For mocking `getAuth` from `@hono/oidc-auth`, use `mock.module` (Bun's mock facility):
```typescript
import { mock } from "bun:test";
// Mock @hono/oidc-auth
const mockGetAuth = mock(() => null);
mock.module("@hono/oidc-auth", () => ({
getAuth: mockGetAuth,
oidcAuthMiddleware: () => async (c, next) => next(),
processOAuthCallback: async (c) => c.json({ ok: true }),
revokeSession: async () => {},
}));
```
Then in tests, set `mockGetAuth.mockReturnValue(...)` to simulate authenticated/unauthenticated OIDC sessions.
**Update `tests/routes/auth.test.ts`:**
Remove tests for:
- POST /auth/login (removed)
- POST /auth/setup (removed)
- PUT /auth/password (removed)
Update tests for:
- GET /auth/me -- now returns `{ user: { id: string, email: string }, authenticated: true }` or `{ user: null, authenticated: false }`
- Mock `getAuth` to simulate OIDC session for /me tests
Keep tests for:
- GET /auth/keys (requires auth -- use API key in test)
- POST /auth/keys (requires auth)
- DELETE /auth/keys/:id (requires auth)
Note: GET /login, GET /callback, GET /logout are registered in index.ts not authRoutes, so they are NOT tested in auth route tests. They would be E2E-level tests.
</action>
<verify>
<automated>! grep -q "schema.users" e2e/seed.ts && grep -q "apiKeys" e2e/seed.ts && ! grep -q "createUser\|verifyPassword\|getUserCount\|createSession\|getSession" tests/services/auth.service.test.ts && grep -q "verifyApiKey\|createApiKey" tests/services/auth.service.test.ts && echo "PASS" || echo "FAIL"</automated>
</verify>
<acceptance_criteria>
- e2e/seed.ts does NOT insert into `schema.users`
- e2e/seed.ts DOES insert an API key into `schema.apiKeys` with name "E2E Test Key"
- e2e/seed.ts still seeds categories, items, threads, setups, settings
- tests/services/auth.service.test.ts does NOT test `createUser`, `verifyPassword`, `getUserCount`, `changePassword`, `createSession`, `getSession`, `deleteSession`, `refreshSession`
- tests/services/auth.service.test.ts DOES test `createApiKey`, `verifyApiKey`, `listApiKeys`, `deleteApiKey`
- tests/middleware/auth.test.ts does NOT test `setup_required` response
- tests/middleware/auth.test.ts DOES test API key auth path
- tests/middleware/auth.test.ts DOES test Bearer token auth path
- tests/middleware/auth.test.ts DOES mock and test OIDC session auth path via `getAuth`
- tests/routes/auth.test.ts does NOT test POST /login, POST /setup, PUT /password
- tests/routes/auth.test.ts DOES test GET /me with mocked OIDC session
- tests/routes/auth.test.ts DOES test API key CRUD routes
</acceptance_criteria>
<done>E2E seed uses API keys, all auth tests updated for OIDC architecture, no references to removed user/session functions</done>
</task>
<task type="checkpoint:human-verify" gate="blocking">
<name>Task 3: Verify OIDC login flow with running Logto</name>
<what-built>Complete OIDC authentication integration: Logto in Docker Compose, server-side OIDC middleware, client-side login redirect, API key continuity, updated tests</what-built>
<how-to-verify>
1. Start infrastructure: `docker compose -f docker-compose.dev.yml up -d`
2. Verify Logto is running: visit http://localhost:3002 (Logto Admin Console)
3. In Logto Admin Console:
a. Create a "Traditional Web" application
b. Set redirect URI: `http://localhost:3000/callback`
c. Set post-logout redirect URI: `http://localhost:3000/login`
d. Copy App ID and App Secret
4. Create a `.env` file with:
```
OIDC_ISSUER=http://localhost:3001/oidc
OIDC_CLIENT_ID=<copied app id>
OIDC_CLIENT_SECRET=<copied app secret>
OIDC_AUTH_SECRET=a-random-string-at-least-32-characters-long
```
5. Start GearBox: `bun run dev`
6. Visit http://localhost:5173/login -- should see "Sign in to GearBox" page
7. Click "Sign In" -- should redirect to Logto login page
8. Register a new account on Logto
9. After registration, should redirect back to GearBox dashboard
10. Visit http://localhost:5173 -- should show authenticated state
11. Run unit tests: `bun test` -- all should pass
12. Verify API key auth still works: create a key in Settings, test with curl:
`curl -H "X-API-Key: <key>" http://localhost:3000/api/items`
</how-to-verify>
<resume-signal>Type "approved" or describe issues found during verification</resume-signal>
</task>
</tasks>
<verification>
- `bun test` passes (all auth-related tests updated)
- `bun run build` succeeds (no TypeScript errors)
- E2E seed script runs without error: `bun run e2e/seed.ts`
- No references to removed hooks in client code: `grep -rn "useLogin\|useSetup\|useChangePassword" src/client/`
- No references to removed auth functions in test code: `grep -rn "createUser\|verifyPassword\|getUserCount" tests/`
</verification>
<success_criteria>
- Login page shows redirect button, not credential form
- Auth hooks match new OIDC API response shape
- E2E seed creates API key, not user
- All unit/integration tests pass
- Full OIDC login flow works end-to-end with Logto (verified by human checkpoint)
- API keys still work for programmatic access
</success_criteria>
<output>
After completion, create `.planning/phases/15-external-authentication/15-03-SUMMARY.md`
</output>

View File

@@ -0,0 +1,143 @@
---
phase: 15-external-authentication
plan: 03
subsystem: auth
tags: [oidc, logto, react, tanstack-query, e2e, api-keys]
# Dependency graph
requires:
- phase: 15-external-authentication (plan 02)
provides: OIDC middleware, refactored auth routes, stripped auth service
provides:
- OIDC-aware login page (redirect to Logto, no credential form)
- Updated auth hooks matching new API response shape (string user id)
- E2E seed using API keys instead of user table
- Auth middleware tests for three-way auth (API key, Bearer, OIDC)
- Auth route tests with mocked OIDC session
affects: [16-multi-user-data-model, e2e-tests]
# Tech tracking
tech-stack:
added: []
patterns:
- "OIDC redirect login via window.location.href to server route"
- "useLogout returns plain function (not mutation) for redirect-based logout"
- "E2E tests authenticate via API key header, bypassing auth provider"
- "Mock @hono/oidc-auth getAuth in tests with bun:test mock.module"
key-files:
created: []
modified:
- src/client/hooks/useAuth.ts
- src/client/routes/login.tsx
- src/client/routes/settings.tsx
- src/client/components/UserMenu.tsx
- e2e/seed.ts
- tests/middleware/auth.test.ts
- tests/services/auth.service.test.ts
- tests/routes/auth.test.ts
key-decisions:
- "Login page renders redirect button rather than credential form"
- "useLogout returns { logout } function (not useMutation) since it is a redirect"
- "Removed ChangePasswordSection from settings (passwords managed by Logto)"
- "E2E seed uses static API key string for deterministic test auth"
patterns-established:
- "OIDC login: client redirects to server /login which triggers Logto redirect"
- "Test mocking: mock.module for @hono/oidc-auth before importing middleware"
- "E2E auth: API key in X-API-Key header, no dependency on auth provider"
requirements-completed: [AUTH-05, AUTH-01, AUTH-02]
# Metrics
duration: 4min
completed: 2026-04-04
---
# Phase 15 Plan 03: Client Auth UI, E2E Seed, and Test Updates Summary
**OIDC login redirect page, cleaned auth hooks (string user id, no credential forms), API-key E2E seed, and three-way auth test coverage**
## Performance
- **Duration:** 4 min
- **Started:** 2026-04-04T18:50:52Z
- **Completed:** 2026-04-04T18:54:28Z
- **Tasks:** 3 (2 auto + 1 checkpoint auto-approved)
- **Files modified:** 8
## Accomplishments
- Login page redirects to Logto via server-side OIDC instead of showing username/password form
- Auth hooks match new OIDC API response shape (user.id is string, no setupRequired)
- E2E seed creates API key for test authentication instead of inserting into removed users table
- Auth middleware and route tests validate all three auth paths with proper mocking
## Task Commits
Each task was committed atomically:
1. **Task 1: Rewrite login page and auth hooks for OIDC** - `79b27b6` (feat)
2. **Task 2: Update E2E seed script and auth-related tests** - `689a56b` (feat)
3. **Task 3: Verify OIDC login flow** - auto-approved checkpoint (no commit)
## Files Created/Modified
- `src/client/hooks/useAuth.ts` - Removed useLogin/useSetup/useChangePassword, updated AuthState to string id
- `src/client/routes/login.tsx` - Replaced credential form with OIDC redirect button
- `src/client/routes/settings.tsx` - Removed ChangePasswordSection, use authenticated flag
- `src/client/components/UserMenu.tsx` - Updated logout call from mutation to direct function
- `e2e/seed.ts` - API key creation instead of user insertion
- `tests/middleware/auth.test.ts` - Three-way auth tests with mocked getAuth and verifyAccessToken
- `tests/services/auth.service.test.ts` - API key CRUD tests only (removed user/session tests)
- `tests/routes/auth.test.ts` - GET /me with mocked OIDC, API key CRUD routes
## Decisions Made
- Login page renders a "Sign In" button that triggers `window.location.href = "/login"` for full-page navigation to server OIDC redirect
- useLogout returns a plain `{ logout }` object instead of useMutation since it performs a redirect, not an API call
- Removed ChangePasswordSection from settings entirely since passwords are managed in Logto
- Settings page API keys section gated on `auth?.authenticated` instead of `auth?.user`
- E2E seed uses a static deterministic API key string for reproducible test runs
## Deviations from Plan
### Auto-fixed Issues
**1. [Rule 3 - Blocking] Updated UserMenu.tsx for new useLogout API**
- **Found during:** Task 1 (Rewrite auth hooks)
- **Issue:** UserMenu called `logout.mutate()` but new useLogout returns `{ logout }` function, not a mutation
- **Fix:** Changed `logout.mutate()` to `logout()` in UserMenu onClick handler
- **Files modified:** src/client/components/UserMenu.tsx
- **Verification:** No remaining `logout.mutate` references in codebase
- **Committed in:** 79b27b6 (Task 1 commit)
**2. [Rule 3 - Blocking] Removed ChangePasswordSection from settings page**
- **Found during:** Task 1 (Rewrite auth hooks)
- **Issue:** Settings page imported and used `useChangePassword` which was removed from hooks; page would not compile
- **Fix:** Removed entire ChangePasswordSection component and its import from settings.tsx
- **Files modified:** src/client/routes/settings.tsx
- **Verification:** No references to useChangePassword remain in client code
- **Committed in:** 79b27b6 (Task 1 commit)
---
**Total deviations:** 2 auto-fixed (2 blocking issues)
**Impact on plan:** Both fixes were necessary to keep the client compiling after hook removals. No scope creep.
## Deferred Items
- `tests/routes/oauth.test.ts` still references `createUser` from old auth service (pre-existing, not caused by this plan)
## Issues Encountered
None
## User Setup Required
None - no external service configuration required for this plan (infrastructure was set up in Plan 01).
## Next Phase Readiness
- Client auth UI complete and aligned with OIDC backend from Plan 02
- E2E seed ready for API-key-based test authentication
- All auth-related unit/integration tests updated for new architecture
- Phase 15 external authentication integration is complete across all three plans
---
*Phase: 15-external-authentication*
*Completed: 2026-04-04*