Slice 2: Login + role-aware dashboard happy path#6
Merged
Merged
Conversation
…ields New tests verify that fields named `authorization`, `jwt`, and `password_hash` are replaced with `[REDACTED]` including in nested structures. These fields carry token material and hash material that must never appear in log output. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ord_hash Adds three field names to the redaction set so that Bearer tokens, raw JWTs, and bcrypt hashes never appear in log output regardless of where they are nested in the log context. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…malformed Unit tests covering all JWT verification outcomes needed by requireAuth: happy path (sign then verify), expired token, tampered signature, token signed with wrong secret, and malformed token (wrong part count). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pure Node.js implementation with no external dependencies: base64url encode/decode, HMAC-SHA256 signing, constant-time signature comparison, exp claim check. Exported types: JwtPayload, JwtVerifyResult. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… chars Four cases: missing secret exits with code 1, too-short secret exits, exactly-32-char secret does not exit, longer-than-32-char secret does not exit. Satisfies AC: API process refuses to boot with weak secret. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
checkEnv() validates JWT_SECRET at startup (exits with code 1 if missing or < 32 chars). loadConfig() validates full env schema with Zod. index.ts wires all deps together before binding the HTTP server. Permissions allowlist added to .claude/settings.json for agent loop. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…lid/invalid token Integration tests against a real Postgres database covering: - POST /auth/login → 200+JWT for valid credentials - POST /auth/login → 401 for wrong password - POST /auth/login → 401 for unknown email (identical body) - POST /auth/login → 422 for missing required field - POST /auth/login → 415 for non-JSON Content-Type - GET /auth/me → 200+profile with valid Bearer token - GET /auth/me → 401 without Authorization header - GET /auth/me → 401 with malformed Authorization header - GET /auth/me → 401 with invalid token Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements the full login+me happy path: - DB schema: users/patients/doctors tables + user_role enum (Drizzle) - Domain types: UserProfile, UserRole, AppVariables (typed Hono env) - Infrastructure: Drizzle db factory - Use-cases: login (bcrypt+JWT), getUserProfile (loads role-specific row) - Routes: POST /auth/login, GET /auth/me - Middleware: requireAuth (Bearer token), requireRole (RBAC) - Contracts: full auth contract with Zod schemas (login+me) - JWT payload narrowed explicitly to avoid unsafe JSON.parse cast - api-client body serialized with JSON.stringify to avoid unsafe cast - Config: .env auto-load in vitest.config.ts and drizzle.config.ts - bcryptjs added to api deps Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rows Integration tests covering: - First run inserts correct number of users, patients, and doctors rows - Second run (idempotency) does not duplicate rows - Hashes produced with saltRounds=12 match the $2b$12$ prefix pattern - Stored hash verifies against the original plaintext password - Role-specific profile (patients/doctors) rows are created Also extends vitest include to cover apps/api/scripts/**/*.test.ts. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extracts seeding logic into exported seedAccounts(db, accounts, saltRounds) so integration tests can import and call it directly. The CLI entry point is guarded by import.meta.url === process.argv[1] so importing the module for testing does not execute the CLI code or call process.exit. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…role
Two new cases: a token missing the exp claim returns { reason: 'invalid' };
a token with role='admin' (not doctor|patient) returns { reason: 'invalid' }.
These cover the JWT payload validation needed to prevent role escalation.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…alues Narrows the parsed payload to confirm sub is string, iat/exp are numbers, and role is exactly 'doctor' or 'patient'. Tokens carrying any other role value (e.g. 'admin') are now rejected with reason 'invalid' before they reach the route layer. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ient Seven e2e scenarios: - Doctor logs in, lands on /doctor with welcome message and appointments section - Patient logs in, lands on /patient with welcome message and appointments section - Wrong credentials shows error, stays on /login - Logged-in doctor logs out and is returned to /login - Session survives a page refresh (token in localStorage re-authenticates via /auth/me) - Unauthenticated visitor hitting /patient is redirected to /login - Doctor visiting /patient is redirected to /doctor (role guard) - Patient visiting /doctor is redirected to /patient (role guard) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…storage Full web auth flow: - auth-token.ts: localStorage wrapper (getToken/setToken/clearToken) - queries.ts: fetchCurrentUser + currentUserQueryOptions for ['auth', 'me'] key - login.tsx: useMutation-based login form; on success stores token then fetchQuery(['auth', 'me']) to get role and navigate to the right dashboard - patient.tsx / doctor.tsx: beforeLoad route guard (redirects to /login or correct role dashboard), Welcome+firstName header, Upcoming appointments placeholder, logout button - main.tsx: wires all routes into TanStack Router with QueryClient context - GLOSSARY.md: extends currentUser shape to include firstName/lastName Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…n /me - user-repository.ts: DB queries for findUserByEmail, findUserById, findPatientProfile, findDoctorProfile (infrastructure layer boundary) - login.ts + get-user-profile.ts: delegate to user-repository instead of querying db/schema directly (fixes layer boundary — use-cases must not import from db/schema) - get-user-profile.ts: adds db_error to result union; me-route.ts handles 503 path - logger.ts: splits redact into redactValue/redactContext to eliminate the unsafe `as LogContext` downcast identified in code review Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
15 tasks
|
Claude finished @itlamb's task in 5m 29s —— View job PR Review — Slice 2: Login + role-aware dashboard
Verdict: APPROVE ✅ (with non-blocking gaps)The slice is genuinely vertical (schema → API → contracts → UI → tests), TDD red→green→refactor is visible in the commit history, and the auth surface is correct and reasonably hardened. A few PRD-listed acceptance criteria are not implemented and a couple of minor security/quality polish items are worth tracking, but nothing blocks merge. Spec compliance
Architecture findings
Security findings
Test quality
Suggestions (non-blocking)
What's solid
· branch: |
This was referenced May 7, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
POST /auth/login(JWT HS256, 24h),GET /auth/me(loads patient/doctor profile)users,patients,doctors,user_roleenum via Drizzle ORM/login(useMutation form),/patient+/doctordashboards withbeforeLoadguards, logoutrequireAuth()(Bearer token),requireRole()(RBAC-ready)authorization,jwt,password_hashTest plan
pnpm db:push && pnpm db:seed— apply schema + seed 10 demo accountspnpm test— 30 unit+integration tests green (jwt, env-guard, logger, auth routes, health, seed)pnpm typecheck— 3 packages cleandr.kowalski@clinic.pl/Pass1234!→ lands on/doctorwith "Welcome, Marek"p.zielinski@mail.pl/Pass1234!→ lands on/patientwith "Welcome, Jan"/login/auth/merehydrates)/login/patientunauthenticated → redirected to/loginpnpm test:e2e— full Playwright e2e suite (doctor+patient login/logout/refresh/guards)Closes #2
🤖 Generated with Claude Code