Commit f4a100d
feat(identity): add email verification flow (#14378)
## Summary
- Adds native email verification to identity-service so we can drop
Bouncer's deliverability check (Option A).
- New `isEmailVerified`, `emailVerificationToken` (sha256-hashed), and
`emailVerificationTokenCreatedAt` columns on `Users`. Bouncer /
`isEmailDeliverable` are intentionally kept for backward compatibility -
removal is a follow-up.
- Signup now generates a token, stores its hash, and sends a
verification email (24h TTL). Signups from disposable-email domains are
rejected using the open-source
[disposable-email-domains](https://github.com/disposable-email-domains/disposable-email-domains)
blocklist embedded as a static file.
- New endpoints: `GET /email/verify?token=...` (redirects to
`audius.co/verify-email?status=...`) and `POST
/email/resend-verification` (authed).
- `recovery.js` and `welcomeEmail.js` now suppress sends when neither
`isEmailVerified` nor the legacy `isEmailDeliverable` flag is true (so
older accounts keep receiving mail).
## Bug fixes applied during rebase
- **CRITICAL** - `buildVerificationLink` was pointing at `websiteHost`
(e.g. `https://audius.co/verify-email`) instead of the identity
service's own `GET /email/verify` handler. Added `identityServiceHost`
config key (env `identityServiceHost`, default
`https://identityservice.audius.co`) and updated the link builder to use
`${identityServiceHost}/email/verify?token=...`.
- **LOW** - `req.logger` may be undefined on bare routes (only
`handleResponse` routes inject it). Changed `req.logger.error(...)` to
`(req.logger || console).error(...)` in the catch block of `GET
/email/verify`.
- **REBASE** - `src/notifications/emails/` was renamed to `src/emails/`
on main; resolved by moving the template to
`src/emails/emailVerification.js` and updating the import in
`src/utils/emailVerification.js`.
## Files
- Migration:
`packages/identity-service/sequelize/migrations/20260521000000-add-email-verification.js`
- Model: `packages/identity-service/src/models/user.js`
- Routes: `src/routes/user.js`, `src/routes/emailVerification.js`,
`src/routes/recovery.js`, `src/routes/welcomeEmail.js`
- Helpers: `src/utils/emailVerification.js`,
`src/utils/disposableEmail.js`
- Template: `src/emails/emailVerification.js` (was
`src/notifications/emails/`)
- Blocklist data: `src/data/disposable_email_blocklist.conf`
## Out of scope (follow-ups)
- Remove Bouncer call + drop `isEmailDeliverable` column once
verification ramps.
- Update the anti-abuse oracle (pedalboard) to score on
`isEmailVerified` instead of `isEmailDeliverable`.
## Test plan
- [ ] `npm run typecheck` and `npm run lint` in
`packages/identity-service` - both pass locally.
- [ ] Run `db:migrate` against a dev DB; verify the three columns and
index land, then run the `down` migration cleanly.
- [ ] Sign up a new user; confirm a row is created with
`isEmailVerified=false` and a hashed token, and a verification email is
sent via Sendgrid (or skipped with a warn log when Sendgrid is
unconfigured).
- [ ] Click the link -> `isEmailVerified` flips to true, token columns
cleared, redirected to `/verify-email?status=success`.
- [ ] Tamper with the token / let it expire -> redirected to
`status=invalid` / `status=expired`.
- [ ] Hit `POST /email/resend-verification` while authed; new token
issued, email sent.
- [ ] Try signing up with `@mailinator.com` (or any address from the
blocklist) -> 400 with a clear error message.
- [ ] On an account with `isEmailVerified=false` and
`isEmailDeliverable=true` (legacy), recovery + welcome emails still
send.
- [ ] On an account with both flags false, recovery + welcome emails are
suppressed.
Generated with Claude Code
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>1 parent 0c9b26f commit f4a100d
12 files changed
Lines changed: 5982 additions & 3 deletions
File tree
- packages/identity-service
- sequelize/migrations
- src
- data
- emails
- models
- routes
- utils
Lines changed: 35 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
773 | 773 | | |
774 | 774 | | |
775 | 775 | | |
| 776 | + | |
| 777 | + | |
| 778 | + | |
| 779 | + | |
| 780 | + | |
| 781 | + | |
776 | 782 | | |
777 | 783 | | |
778 | 784 | | |
| |||
0 commit comments