feat(auth): POST /api/auth/login + me-fields + secondary SPA form#119
Merged
Conversation
Phase B of the login-migration impl. Wires the phase-A verifier into
a real login endpoint, surfaces /api/auth/me's new fields, and adds
the SPA's secondary password form.
Backend:
POST /api/auth/login (new) — accepts { usernameOrEmail, password },
resolves to a Person by slug or email, verifies via the phase-A
verifyLegacyPassword, rotates the credential on success (rehash
if needsRehash; always refresh lastUsedAt), mints a session with
loginMethod: 'legacy_password'. Anti-enumeration timing floor via
dummyVerify on missing-user/credential paths. Uniform 401
invalid_credentials on any failure. Rate-limit covered by the
existing /api/auth/* 10/min/IP bucket.
PrivateStore.putLegacyPassword (new) — write path for rotated
credentials. Threaded through the cross-store transaction's
staged-mutation set so future callers (password-reset confirm in
phase C) get atomic semantics for free.
JWT loginMethod claim — optional on access + refresh tokens.
issueSession takes the value; verifyAccess/verifyRefresh surface
it via the claim object. The refresh route preserves the claim
across token rotation. github-oauth.ts now passes 'github' when
minting on the callback's success paths.
GET /api/auth/me — adds hasGitHubLink (derived from
Person.githubUserId !== null) and lastLoginMethod (pulled from
the access claim; null for anonymous or pre-PR sessions).
middleware.ts SessionContext — adds optional loginMethod surfaced
from the verified claim.
SPA:
apps/web/src/pages/LoginPlaceholder.tsx — adds a collapsed
"🔑 Or sign in with your Code for Philly password" disclosure
below the GitHub CTA. Expanded form posts to api.auth.login with
inline 401/429 error rendering. "Returning member" copy updated
to match the new spec. "Forgot your password?" deferred to
phase C.
apps/web/src/lib/api.ts — adds api.auth.login(usernameOrEmail,
password).
Tests:
apps/api/tests/auth-login.test.ts (new) — 12 cases: SHA-1/argon2
happy paths, email resolution, GitHub-linked user, rehash-on-
login proof, no-rotate-when-current, all four 401 paths
(wrong password, unknown user, unknown email, missing body),
/api/auth/me post-login (legacy_password + hasGitHubLink
false/true), anonymous /api/auth/me.
apps/web/tests/LoginPlaceholder.test.tsx (new) — 4 cases: GitHub
button + collapsed disclosure render, disclosure expands to
reveal fields, submit gated until both fields filled, inline
401 error.
All existing tests still pass (357 API + 58 web + 75 shared expected
on full sweep).
Closes the immediate phase B; phase C (password reset) and phase D
(link-github) follow.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This was referenced May 31, 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
Phase B of the login-migration impl. Wires the phase-A verifier into a real login endpoint, surfaces `/api/auth/me`'s new fields, and adds the SPA's secondary password form.
Backend
SPA
What's NOT in this PR
Test plan
🤖 Generated with Claude Code