ea61b6a)Phase B of the Auth.js v5 migration. Completes the full user-facing auth surface: Google provider, transactional email via Resend, password reset (was C1), email verification gate (was C2), and enumeration-safe signup (C3 full fix).
allowDangerousEmailAccountLinking: true — verified-email auto-link rather than block.signIn callback guards profile.email_verified === true; unverified Workspace accounts are refused with /login?error=OAuthEmailNotVerified.GoogleSignInButton component (inline SVG, calls signIn("google", { callbackUrl })). Appears on both /login and /signup.src/lib/auth/mongoose-adapter.ts)userId stays canonical — toAdapterUser() returns id: user.userId (not _id).createUser, getUser, getUserByEmail, getUserByAccount, updateUser, deleteUser, linkAccount, unlinkAccount, createVerificationToken, useVerificationToken.accounts collection (src/lib/models/accountModel.ts) with unique index on { provider, providerAccountId }.emailVerificationTokenModel.ts — 32-byte random, sha256-hashed, 24h TTL, one-time use.issueEmailVerificationToken() internal helper; verifyEmail(token) server action.authorize() throws EmailNotVerified (CredentialsSignin subclass, code: "email_not_verified") if user.emailVerified is null.email_not_verified error code./verify-email?token= page fires verifyEmail() on mount; shows pending/success/error states.emailVerified set automatically from Google's email_verified claim — no token needed.tests/grandfather-email-verified.mjs) — stamped 6 local dev users; run once before or after deploy.passwordResetTokenModel.ts — sha256-hashed, 1h TTL, usedAt single-use guard.requestPasswordReset(email) — rate-limited 3/h per email, always returns generic success (enumeration-safe).resetPassword(token, newPassword) — validates hash, bcrypt-hashes new password, resets failedLoginAttempts/lockoutUntil, bumps sessionsValidAfter = new Date() to revoke all existing sessions./forgot-password — email input, "check your inbox" success state./reset-password?token= — password + confirm form, redirects to /login on success.sessionsValidAfteruserModel.ts (Date | null, default null).session callback in auth.ts does one indexed findOne per request; if jwt.iat < sessionsValidAfter, returns user: undefined to kill the session.signup() in src/lib/actions/auth.ts completely rewritten.{ success: true, awaitingVerification: true } for all four paths:
createUser + issueEmailVerificationTokenaccount-exists-already emailsrc/lib/email/sendTransactional.ts)RESEND_API_KEY is unset (dev/test convenience).email-verify, password-reset, password-reset-no-account, account-exists-already.userModel.ts: added emailVerified: Date | null, image: string | null, sessionsValidAfter: Date | null; password made non-required (OAuth users have no password).accountModel.ts, passwordResetTokenModel.ts, emailVerificationTokenModel.ts, verificationTokenModel.ts./forgot-password, /reset-password, /verify-email added to PUBLIC_AUTH_PATHS in src/lib/auth-routes.ts.tests/seed-e2e-user.mjs — sets emailVerified: new Date() on insert and retroactively patches the E2E user.tests/grandfather-email-verified.mjs — one-shot idempotent migration for existing dev/prod users.userId canonical, avoids _id mapping hacks.sessionsValidAfter: Date field (option b from plan) — simpler than tokenVersion, enables "log out all devices" as a one-liner later.allowDangerousEmailAccountLinking) — changed from "block" in the plan after confirming safe for personal accounts.emailVerified = now on existing users (option a) — keeps E2E seed and dev accounts working without disruption.Phase C: [[Projects/personal-finance-notion/backlog/done/p0-authjs-phase-c-hardening|Phase C · Integration API]]. Phase D: [[Projects/personal-finance-notion/backlog/done/p0-authjs-phase-d-pre-launch-hardening|H1/H2/M2/M5 + E2E]].