feat(identity): add email verification flow#14378
Merged
Merged
Conversation
|
Contributor
🌐 Web preview readyPreview URL: https://audius-web-preview-pr-14378.audius.workers.dev Unique preview for this PR (deployed from this branch). |
Add native email verification to identity-service so we can stop relying on Bouncer's deliverability check. New columns isEmailVerified, emailVerificationToken (sha256-hashed), and emailVerificationTokenCreatedAt are added to the Users table. Signup now sends a verification email with a 24h-expiring token, exposes GET /email/verify and authed POST /email/resend-verification, and rejects signups from disposable-email domains (open-source blocklist embedded as a static file). Recovery and welcome email suppression now honors isEmailVerified, falling back to the legacy isEmailDeliverable flag so existing accounts keep working. Bouncer code is intentionally left in place; removal will follow once verification has rolled out. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
e70e926 to
094371a
Compare
dylanjeffers
added a commit
that referenced
this pull request
Jun 16, 2026
…ers -> .users) (#14482) ## Problem Users report failing/janky signups, with `POST https://identityservice.audius.co/users/update` misbehaving ([Slack thread](https://audius-internal.slack.com/archives/CA80RCL77/p1781637102823139)). `authMiddleware` (which gates `/users/update`, `/record_ip`, and every other authenticated endpoint) backfills `blockchainUserId`/`handle` for any identity `Users` row that lacks them — i.e. **every guest / freshly signed-up user**. It did this via: ```js req.app.get('audiusSdk').full.users.getUserAccount({ ... }) ``` But the `@audius/sdk` instance **has no `.full` namespace** — `users` is a top-level API (`audiusSdk.users.getUserAccount`). So `.full` is `undefined` and `.users` throws a **synchronous `TypeError`** on every new-user auth request. Confirmed in prod logs: ``` TypeError: Cannot read properties of undefined (reading 'users') at authMiddleware (build/src/authMiddleware.js:97:68) msg: "Failed to update blockchainUserId/handle" ``` The surrounding `try/catch` swallowed it and called `next()`, so the request proceeded but the **backfill silently never happened** — new identity rows never got `blockchainUserId`/`handle` set. ## Why it started now The bad accessor came in with the **monorepo import** of identity-service (#14388, 5/22), which rewrote `authMiddleware` to use `@audius/sdk`. It was dormant until #14474 (6/15) shipped `loadAudiusSdk.cjs` into the build, so the SDK actually initialized and this line began firing — matching the regression window. (identity hadn't been deployed in a while; 6/15 was the first monorepo image promoted to prod.) I verified prod is in the *benign* config otherwise: `environment=production` is set in `identity-service-secret`, so the SDK targets prod discovery — the issue is purely the wrong accessor, not SDK misconfig. ## Fix Use the correct accessor `audiusSdk.users.getUserAccount` in both `authMiddleware` and `parameterizedAuthMiddleware`. One-token change per call site. ## Notes - Ray's `record_ip` hunch is a red herring: `/users/update` doesn't call `recordIP`. (Though `/record_ip` is also gated by `authMiddleware`, so it hit the same TypeError — likely the source of the confusion.) - Same 6/15 batch also fixed a related latent crash: #14378 reads `src/data/disposable_email_blocklist.conf` at signup via an unguarded `fs.readFileSync`; that file wasn't copied into the build until #14474 (same one-liner that also added `loadAudiusSdk.cjs`). Worth hardening that read separately. - The batch's endpoint removals (#14458, #14472) are safe — the legacy `packages/libs` methods that hit them aren't called by any current client. ## Verification - Confirmed `getUserAccount` lives on the top-level `UsersApi` (`audiusSdk.users`) and there is no `.full` in the SDK instance shape (`@audius/sdk` `index.d.ts`). - Confirmed response shape `res.data.user` is still correct (`UserAccountResponse.data: Account`, `Account.user: User`). - Confirmed prod `environment=production`. After deploy, the `TypeError ... reading 'users'` log should disappear and `Failed to update blockchainUserId/handle` should drop to near-zero. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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
isEmailVerified,emailVerificationToken(sha256-hashed), andemailVerificationTokenCreatedAtcolumns onUsers. Bouncer /isEmailDeliverableare intentionally kept for backward compatibility - removal is a follow-up.GET /email/verify?token=...(redirects toaudius.co/verify-email?status=...) andPOST /email/resend-verification(authed).recovery.jsandwelcomeEmail.jsnow suppress sends when neitherisEmailVerifiednor the legacyisEmailDeliverableflag is true (so older accounts keep receiving mail).Bug fixes applied during rebase
buildVerificationLinkwas pointing atwebsiteHost(e.g.https://audius.co/verify-email) instead of the identity service's ownGET /email/verifyhandler. AddedidentityServiceHostconfig key (envidentityServiceHost, defaulthttps://identityservice.audius.co) and updated the link builder to use${identityServiceHost}/email/verify?token=....req.loggermay be undefined on bare routes (onlyhandleResponseroutes inject it). Changedreq.logger.error(...)to(req.logger || console).error(...)in the catch block ofGET /email/verify.src/notifications/emails/was renamed tosrc/emails/on main; resolved by moving the template tosrc/emails/emailVerification.jsand updating the import insrc/utils/emailVerification.js.Files
packages/identity-service/sequelize/migrations/20260521000000-add-email-verification.jspackages/identity-service/src/models/user.jssrc/routes/user.js,src/routes/emailVerification.js,src/routes/recovery.js,src/routes/welcomeEmail.jssrc/utils/emailVerification.js,src/utils/disposableEmail.jssrc/emails/emailVerification.js(wassrc/notifications/emails/)src/data/disposable_email_blocklist.confOut of scope (follow-ups)
isEmailDeliverablecolumn once verification ramps.isEmailVerifiedinstead ofisEmailDeliverable.Test plan
npm run typecheckandnpm run lintinpackages/identity-service- both pass locally.db:migrateagainst a dev DB; verify the three columns and index land, then run thedownmigration cleanly.isEmailVerified=falseand a hashed token, and a verification email is sent via Sendgrid (or skipped with a warn log when Sendgrid is unconfigured).isEmailVerifiedflips to true, token columns cleared, redirected to/verify-email?status=success.status=invalid/status=expired.POST /email/resend-verificationwhile authed; new token issued, email sent.@mailinator.com(or any address from the blocklist) -> 400 with a clear error message.isEmailVerified=falseandisEmailDeliverable=true(legacy), recovery + welcome emails still send.Generated with Claude Code