feat/user email verification - #49
Merged
Merged
Conversation
…s; add helper functions to generate JWT email verification tokens
…ccess and error states; add Spinner component
…line, show modal if successful
…stead of 'no-reply' to prevent email services from marking as spam
…n email verification token would decode as a session token; fix this by giving session tokens their own aud claim
12 tasks
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
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.
Background
Users can register and log in without verifying their email. Verification is a nudge, not a blocker — unverified users can access the app but see a banner prompting them to verify (banner itself is tracked separately, see Notes).
Changes
Backend
email_verifiedcolumn onUser(Boolean, nullable=False, default=False) with migration — kept separate fromis_active, which belongs to the admin-activation flowapp/core/email_verification.py:generate_verification_token(user_id: int) -> str— signed JWT, 24hexp,aud: "email_verification"claimverify_verification_token(token: str) -> int | None— returnsuser_idif valid,Noneif expired/invalid/wrong audienceapp/services/email_service.py— thin async wrapper around Resend (resend, usingsend_async);send_verification_email(to: str, token: str); sends fromverify@nexus.ethanshih.com(deliberately notnoreply@, to avoid spam-filtering), noreply_toGET /auth/verify-email/?token=— public; validates token, setsemail_verified=True; idempotent; 400 with a clear message on invalid/expired tokensPOST /auth/send-email-verification/— authenticated; 400 if already verified;# todo: add rate limitingRESEND_API_KEYadded toSettings(app/core/config.py) and.envFRONTEND_URLadded/confirmed inSettings— used to build the verification link in the email bodyIntentionally disabled for now: the actual send call in
send-email-verificationis commented out (_send_verification_email(...)), andregister()never calls it either. This is deliberate — Resend's free tier has a sending limit and we don't want every registration burning it during dev/testing. Wiring both call sites back up is a follow-up, not a bug.Frontend
/verify-emailpage — readstokenfrom query params on mount, callsauthApi.verifyEmail(token); loading → success or error state; success shows a "go to dashboard" button when logged in; error surfaces the backend's message (e.g. "Invalid or expired token")VerifyModalshown over the sign-up flow's profile-enrichment step, after step 1 registration: "We sent a verification email to {email}. You can verify later — let's finish your profile."authApi.verifyEmail(token)→GET /auth/verify-email/?token=authApi.sendEmailVerification()→POST /auth/send-email-verification/proxy.ts: visiting/verify-emailwithout atokenparam redirects to/dashboard(logged in) or/(logged out)Testing
/verify-emailsuccess and error states (using a manually-minted token, since sending is currently disabled)feat/signin-and-signupKnown gaps / follow-ups
register()andsend-email-verification) are wired for it but the send is disabled to stay under the Resend free-tier limit during dev. Re-enable both before this feature is user-facing.resendis a plain dependency (resend==2.32.2) — no[async]extra needed;send_asyncis a normal method on the base package.Notes
verify@nexus.ethanshih.comis verified in Resend; SPF/DKIM DNS records are set in Vercel DNS; no MX records, since this is send-onlyaudclaim specifically so a login/session JWT can't be reused as a verification token (see security gap above for the unaddressed reverse case)feat/signin-and-signup— depends on that landing first, or should be rebased ontomainonce it doesdatabase_urldefault flipped from SQLite to Postgres inconfig.py;_find_user_by_idmoved fromusers.pyto sharedcore/users.py; smallModal.tsx/Input.tsxtweaks and a newSpinner.tsxcomponent