Skip to content

feat: legacy credentials importer + two-column /login layout#122

Merged
themightychris merged 3 commits into
mainfrom
feat/laddr-creds-import-and-login-layout
Jun 1, 2026
Merged

feat: legacy credentials importer + two-column /login layout#122
themightychris merged 3 commits into
mainfrom
feat/laddr-creds-import-and-login-layout

Conversation

@themightychris
Copy link
Copy Markdown
Member

Summary

Two related changes uncovered while debugging "my login for chris isn't working" on sandbox.

1. Credentials importer (feat(scripts): import-laddr-credentials)

The login-migration impl track (phases A–D, PRs #118#121) shipped runtime support for password sign-in, but there was no tool to get the passwords there. The existing `script:import-laddr` deliberately handles only public data (laddr's public JSON API doesn't expose emails or password hashes). This PR adds the private-side counterpart.

  • New script: `apps/api/scripts/import-laddr-credentials.ts` (+ npm `script:import-laddr-credentials` entry).
  • Input: CSV from laddr MySQL with header `Username,Email,Password` (one row per active user).
  • Reads Person records from a bare clone of `codeforphilly-data` to join `slug → personId`.
  • Output: `profiles.jsonl` + `legacy-passwords.jsonl` in a local directory.
  • Deployment to the runtime backend (PVC for sandbox, S3-compat bucket / GCS for prod) is a separate manual step — keeps the script storage-agnostic.
  • Validation via the runtime Zod schemas; bad emails skipped + counted; warnings for malformed hashes.
  • Full-replace semantics: each run overwrites the local output files.

Sanity-checked on the real 31,869-row laddr export: 30,360 written (95% match rate), 869 skipped no-email, 640 no-person-match. Deployed to sandbox and verified `chris`'s sign-in works end-to-end against his bcrypt hash.

New operator doc: docs/operations/legacy-credentials-import.md — CSV format, run command, deploy paths for both sandbox (`kubectl cp` + rollout restart) and prod (`gsutil cp` / `aws s3 cp` + restart). Note the deploy nuance: `POST /api/_internal/reload-data` only reloads public + FTS, so private-store updates require a full pod restart.

`cutover.md` updated — its T-1 section was incorrectly steering operators to the deferred account-claim flow for credentials; now links to the credentials-import doc.

2. Two-column /login layout (feat(web): two-column /login)

Cutover-time users arriving at `/login` are overwhelmingly legacy users; the previous "GitHub button primary, password form hidden behind a disclosure" layout made the path most users actually need invisible by default.

  • New layout: two-column grid (`max-w-5xl`), "Returning member" card on the left with the password form fully expanded, "New here?" card on the right with the GitHub button + "Why GitHub?" explainer.
  • Stacks on mobile via `grid-cols-1 md:grid-cols-2`.
  • Forgot-password link stays on the password card.
  • Test updates: dropped disclosure-expansion tests, added "both columns visible" assertion.

Test plan

  • `npm run -w apps/api type-check` clean
  • `npm run -w apps/web type-check` clean
  • `npm run lint` clean
  • `npm run -w apps/web test -- tests/LoginPlaceholder.test.tsx` — 3/3
  • Importer end-to-end against real CSV (31,869 rows in, 30,360 records out, expected skip counts)
  • Sandbox-deployed creds + verified sign-in works for `chris`
  • Smoke-test the two-column /login layout in a browser after the new image lands

🤖 Generated with Claude Code

themightychris and others added 3 commits May 31, 2026 21:14
Adds the missing private-side cutover tool. Reads a CSV exported from
laddr's MySQL (Username,Email,Password) and a bare clone of the
codeforphilly-data published branch, joins by slug, and emits
profiles.jsonl + legacy-passwords.jsonl ready for upload to the
runtime private store (PVC for sandbox, S3-compat bucket including
GCS for prod).

The public import-laddr script deliberately doesn't touch private
data because laddr's public JSON API doesn't expose emails or
password hashes; this new script consumes a MySQL extract directly,
which fills the gap. Full-replace semantics: each run overwrites
the local output files. Cutover-time use only — re-running after
in-app rehash-on-login would clobber argon2id rotations with the
SHA-1 originals.

Schema validation routes invalid emails / malformed hashes through
the same Zod schemas the runtime uses, so the output is guaranteed
to load cleanly into the in-memory private store. Reports counts
and warnings; defaults to producing files under .scratch/ so PII
stays out of git.

Operator doc at docs/operations/legacy-credentials-import.md
covers the CSV format, run command, deploy paths for both sandbox
(kubectl cp + rollout restart, since reload-data only covers public
state) and prod (gsutil/aws s3 cp + restart). Linked from
cutover.md's T-1 section, which previously incorrectly steered
operators toward the deferred account-claim flow for credentials.

.gitignore picks up .claude/scheduled_tasks.lock (per-session
ScheduleWakeup artifact, never want it tracked).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the single-card layout (GitHub button primary, password form
hidden behind a disclosure) with a two-column grid: password on the
left, GitHub on the right, both fully expanded. Cutover-time users
arriving at /login are now overwhelmingly legacy users; the disclosure
made the path they need invisible by default.

Layout adjusts to max-w-5xl with a centered header; columns stack on
mobile via grid-cols-1 md:grid-cols-2. Forgot-password link stays on
the password card. WhyGitHub explainer moves under the GitHub card
since it's the long-form explainer for that column.

Test updates: drops the disclosure-expansion tests (no longer
applicable); adds an "both columns visible" assertion as the entry-
point check. Other tests (gated submit, inline 401 error) stay as-is
since the field selectors haven't changed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The legacy password sign-in flow called navigate() after a successful
POST /api/auth/login but never reloaded the AuthProvider's snapshot,
so the navbar (and every other useAuth consumer) stayed in its
anonymous state until the next hard refresh. The session cookies were
set on the server, the user was technically signed in, the UI just
hadn't noticed.

GitHub OAuth doesn't have this bug because the flow is a full-page
navigation through /api/auth/github/start → callback → return path,
which re-mounts AuthProvider and re-fetches /api/auth/me on its own.
The password-reset confirm flow already awaits reload() before
navigating; the legacy login path had drifted from that pattern.

Same fix: await reload() in LoginPlaceholder's onSuccess before
navigating. Widens the onSuccess type to allow a Promise return.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@themightychris themightychris merged commit b877e6e into main Jun 1, 2026
1 check passed
@themightychris themightychris deleted the feat/laddr-creds-import-and-login-layout branch June 1, 2026 02:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant