You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
POST /auth/signup/email creates the account and logs the browser straight in. Nothing checks that the person signing up controls the address they typed.
api/routes/auth.py — email_signup() validates the field set, the email format (_validate_email), and a password length of 8, rejects an address that already has an account under any provider, then:
So someone@company-i-do-not-work-for.com becomes a real, immediately usable account.
This gap is specific to the email provider. The Google and GitHub paths receive an already-verified address from the identity provider, so they are unaffected.
Why it matters
Accounts can be created on addresses the registrant does not own, in someone else's name.
Every downstream count — the org graph, the analytics dashboard, any waiting-list or quota logic (Add waiting list #63, Users management #10) — treats an unverified address as a real user.
It is the normal precondition for anything sent by email later (password reset, notifications). Building those on unverified addresses means building on sand.
There is no mail infrastructure yet
Nothing in api/ sends email — no SMTP, SendGrid, Mailgun, SES or Resend. Choosing and wiring a provider is part of this issue, and probably the larger part. Whatever is chosen needs to work in local dev without real delivery (log the link, or a console transport) so the signup flow stays testable.
Suggested shape
Persist a verification state on the Identity node alongside password_hash — email_verified: bool plus a hashed, single-use, expiring token. Store the hash, not the token, the same way passwords are handled.
Send on signup. Keep account creation as it is, add the send.
Decide what an unverified account can do — see below.
GET /auth/verify/email?token=… — constant-time compare, single use, expire after e.g. 24h, redirect into the app with a result. Do not leak whether an address exists.
Resend endpoint, rate-limited per address.
UI — app/src/components/modals/AuthModal.tsx, app/src/services/auth.ts, app/src/contexts/AuthContext.tsx: a post-signup "check your inbox" state, a resend control, and a banner while unverified.
Decision needed before implementing
Does an unverified account get a session? Two defensible answers:
Verify-then-use — signup returns 201 with no session; the account is inert until the link is clicked. Strongest, and it changes today's flow, since signup currently logs the user in.
Grace period — log them in as now, but gate the things that cost us (query execution, graph creation) behind verification and show a persistent banner. Gentler on conversion.
There is precedent for a pending state in this file already: establish_browser_session(..., provisioned=…) and _retry_pending_provisioning().
Note on existing work
#330 ("Add login and signup with email") is still open, DIRTY, CHANGES_REQUESTED, last touched 4 January — but email signup has since shipped by another route and is live on staging. That PR should be closed rather than revived; it is not the foundation for this work.
Done when
A new email signup cannot reach a verified state without the link being clicked
Tokens are hashed at rest, single-use, and expire
Resend exists and is rate-limited
Verification responses do not reveal whether an address is registered
Local dev can complete the flow without a real mail provider
The chosen gate (verify-then-use, or grace period) is applied consistently in the API, not only in the UI
Problem
POST /auth/signup/emailcreates the account and logs the browser straight in. Nothing checks that the person signing up controls the address they typed.api/routes/auth.py—email_signup()validates the field set, the email format (_validate_email), and a password length of 8, rejects an address that already has an account under any provider, then:So
someone@company-i-do-not-work-for.combecomes a real, immediately usable account.This gap is specific to the email provider. The Google and GitHub paths receive an already-verified address from the identity provider, so they are unaffected.
Why it matters
There is no mail infrastructure yet
Nothing in
api/sends email — no SMTP, SendGrid, Mailgun, SES or Resend. Choosing and wiring a provider is part of this issue, and probably the larger part. Whatever is chosen needs to work in local dev without real delivery (log the link, or a console transport) so the signup flow stays testable.Suggested shape
password_hash—email_verified: boolplus a hashed, single-use, expiring token. Store the hash, not the token, the same way passwords are handled.GET /auth/verify/email?token=…— constant-time compare, single use, expire after e.g. 24h, redirect into the app with a result. Do not leak whether an address exists.app/src/components/modals/AuthModal.tsx,app/src/services/auth.ts,app/src/contexts/AuthContext.tsx: a post-signup "check your inbox" state, a resend control, and a banner while unverified.Decision needed before implementing
Does an unverified account get a session? Two defensible answers:
There is precedent for a pending state in this file already:
establish_browser_session(..., provisioned=…)and_retry_pending_provisioning().Note on existing work
#330("Add login and signup with email") is still open,DIRTY,CHANGES_REQUESTED, last touched 4 January — but email signup has since shipped by another route and is live onstaging. That PR should be closed rather than revived; it is not the foundation for this work.Done when
References
api/routes/auth.py—email_signup(),_set_mail_hash(),_email_account_exists(),_validate_email(),establish_browser_session()app/src/components/modals/AuthModal.tsx·app/src/services/auth.ts·app/src/contexts/AuthContext.tsxRelated: #330 (close), #63, #10