feat: account settings, session-based auth, and onboarding extraction - #54
Merged
Conversation
…for email verification, email change, and password resets
…he verification token helpers in core/auth alr do the same job
…ned by admin so a password needs to be set
…ion, email change, password reset, and account setup
…et, and account setup
…t-setup - login, register, and confirm_account_setup now call create_session() and set the raw session token as the cookie, replacing the deleted create_access_token JWT helper - logout now revokes the session server-side instead of only clearing the cookie, so a leaked cookie can't be replayed after logout - chose to read the cookie and call get_active_session directly in the logout route rather than adding a get_current_session_optional dependency, since tolerating a missing/invalid cookie is only needed there
- new migration adds status (active/invited/deactivated/locked), backfills from is_active + hashed_password, and drops is_active in the same migration - migration refuses to guess (raises) if it finds is_active=false with a password set, since no current code path produces that combination - updated every is_active call site: login, get_current_user, create_user, admin_register, confirm_account_setup, request_password_reset, resend_account_setup - AdminUserUpdate and UserSlimResponse schemas now expose status instead of is_active - test fixtures/assertions updated; inactive_user fixture (has password, was is_active=False) mapped to status='deactivated' as a placeholder pending Step 3's deactivate/lock design
- PATCH /admin/users/{id} now calls revoke_all_sessions when status is set to 'locked', so locking cuts off already-logged-in devices immediately instead of only blocking future logins
- reused the existing generic admin-update endpoint rather than adding a dedicated /lock route, since status was already a field on it
- admin recovery feature dropped per discussion (low-likelihood scenario, no clean way to verify identity once email access is lost)
- added test_locking_revokes_existing_session covering the session-invalidation behavior
…entralize auth card layout
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
🚅 Deployed to the nexus-pr-54 environment in nexus
|
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
Adds a full account settings surface (
/settings/account,/settings/security) that was previously just a dead link, replaces stateless JWT auth with a revocable, DB-backed session system, and extracts name/phone/DOB collection out of sign-up into a shared/onboardingstep used by both sign-up and admin-invited account setup.This is a large PR spanning backend and frontend across several work sessions — flagging that up front in case it makes sense to review in chunks by section below rather than as one pass.
Why
What changed
Backend
verification_tokens(centralizes signup-verify, email-change, email-change-revert, password-reset, and account-setup tokens under one polymorphic table/purpose column, replacing a standalone JWT-based signup-verification scheme) andsessions(SHA-256-hashed opaque tokens, fixed 7-day expiry, IP + user-agent tracked for the device list).first_name/last_nameto nullable. Register and account-setup are now credentials-only (see below), so a user can exist with no name until they complete/onboarding. This undoes a NOT NULL constraint added in an earlier migration on main.access_tokencookie now holds a random session token, checked against thesessionstable on every request.python-joseusage removed fromcore/auth.pyentirely.statusfield (active/invited/deactivated/locked) replaces the oldis_activeboolean, which conflated three different situations (normal, pending-invite, and any other reason for being inactive) into one flag.registerand account-setup-confirm are now credentials-only — name/phone moved to onboarding (see below)./forgot-password, which looks up by current email — meaningless once an attacker has already changed it. Now a dedicated token-based revert flow that works whether the change was confirmed or still pending, and forces a new password + full session revocation either way.PATCH /users/me/previously accepted anemailfield, bypassing the verify-before-apply flow entirely (not exploitable through the shipped UI, but the API itself didn't enforce the boundary). Removed from the schema.Frontend
/settings/account— name/phone/DOB (floating save bar), email as its own verify-before-apply flow with a persistent (server-backed, not local-state) pending-change banner + modal./settings/security— password change, session/device list, "log out everywhere else."/onboarding— extracted sign-up's old phase-2 question flow, plus new required (non-skippable) name/phone/DOB steps. A redirect guard sends any authenticated user withis_onboarding_complete=falsehere on every page load, not just right after signup./account-setupshares that same shrunk component, password-only (no email field — the invite link never carries the email in the URL, and there's no route to look an address up from just the token, so there's nothing to render or pre-fill without adding an API call I don't want to build for this)./forgot-password,/reset-password,/confirm-email-change,/revert-email-change.SettingsSection,SettingsPageHeading, fixed-positionSettingsNav) and a shared(auth)layout shell for every pre-login/token-based page.SettingsNavcollapses into a hamburger-triggered drawer below 640px — the fixed-width sidebar was squeezing account/security form fields into unusable slivers on a phone-sized viewport. Desktop layout is unchanged; scoped only to/settingssince the rest of the app isn't mobile-optimized yet.Testing
Automated backend suite plus a manual QA pass across every new page and flow; email sending is mocked at the lowest-level send function throughout so none of this hits real Resend.
Automated (backend):
invited-status gating), email verification, admin user management (list/get/update/delete, role + status changes), self-service deactivate/delete (password required, session revoked, membership rows cascade for a plain member — see Notes for the tournament-owner exception), sessions (device list, second-login creates a separate session, "log out others" keeps current), profile fields via/users/me/(emailsilently ignored, null rejected on required fields, phone normalization, completeness logic).Manual QA, weighted toward the security-relevant paths:
Notes
Tournament.owner_idisNOT NULLwith noondeleterule, so a TD who owns a tournament and hits Delete Account on/settings/accountwill get a DB IntegrityError (500) instead of a clean result. Self-delete works for everyone else (verified in tests via a plain member). Needs a decision before it's fully safe to ship: block deletion with a 409 while the user owns tournaments, or cascade-delete owned tournaments along with the user. Not fixed in this PR.