Skip to content

Fix email verification dead ends, sync seeded account passwords#18

Merged
haphantran merged 1 commit into
mainfrom
fix/email-verification-flow
Jul 14, 2026
Merged

Fix email verification dead ends, sync seeded account passwords#18
haphantran merged 1 commit into
mainfrom
fix/email-verification-flow

Conversation

@haphantran

Copy link
Copy Markdown
Collaborator

Problem

  • The verify-email screen had no email input; the email rode along in hidden React state. If that state was lost (refresh, new tab), the user hit "Email is required" with no way to provide it.
  • resetPassword never set emailVerified, so an unverified account stayed locked out with 403 "Email verification required" even after a successful password reset, and each login attempt mailed yet another code.
  • The seed only synced role and verified-flag for existing accounts, so a pre-registered demo account kept its manually-set password instead of the documented DEMO_PASSWORD.

Changes

  • Render an editable, prefilled Email Address field on the verify screen; it is now the single source of truth for verify and resend (removed pendingVerificationEmail state).
  • Completing a password reset also sets emailVerified: true (the emailed link proves mailbox ownership).
  • Seed updates the stored password when it does not match the configured one for demo/admin accounts.

Testing

  • Backend: 562/562 tests pass, including a new assertion that reset marks the email verified.
  • Frontend: 152/152 tests pass, including 5 new verify-mode tests (prefill, edit, submit, cleared-email recovery, resend).
  • tsc --noEmit clean on both packages.

- Show an editable, prefilled email field on the verify screen instead
  of relying on hidden state, so users can always supply the email the
  verification code was sent to
- Mark the account email as verified when a password reset completes,
  since following the emailed reset link proves mailbox ownership;
  previously an unverified user stayed locked out (403) after a reset
- Make the seed sync an existing demo/admin account password to the
  configured DEMO_PASSWORD/ADMIN_PASSWORD so documented credentials
  always work
Copilot AI review requested due to automatic review settings July 14, 2026 05:00
@haphantran haphantran merged commit 625db70 into main Jul 14, 2026
4 checks passed
@haphantran haphantran deleted the fix/email-verification-flow branch July 14, 2026 05:01

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR addresses “dead end” states in the email verification flow, ensures password reset unblocks previously-unverified accounts, and makes seeded demo/admin accounts’ credentials consistent with configured values.

Changes:

  • Frontend: In verify mode, render an editable, prefilled Email Address field and remove reliance on hidden “pending verification email” state.
  • Backend: Mark emailVerified: true when completing a password reset.
  • Ops/Seed: When seeding an existing demo/admin user, sync the stored password to the configured password when it differs.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
frontend/src/components/auth/LoginPage.tsx Removes hidden verification-email state; adds visible email field in verify mode and updates verify/resend logic.
frontend/src/tests/components/LoginPage.test.tsx Adds verify-mode tests covering prefill/edit/submit/cleared-email recovery/resend behavior.
backend/src/services/auth.service.ts Updates password-reset flow to also mark the user as email-verified.
backend/src/tests/services/auth.service.test.ts Adds assertion ensuring reset sets emailVerified: true.
backend/prisma/seed.ts Extends seed reconciliation to optionally update existing seeded users’ passwords when mismatched.
Comments suppressed due to low confidence (2)

frontend/src/components/auth/LoginPage.tsx:51

  • The registration-success effect no longer references email, but it’s still included in the dependency array. This causes unnecessary effect re-evaluations and makes the dependency list misleading now that pendingVerificationEmail has been removed.
  useEffect(() => {
    if (registrationSuccess) {
      setSuccessMessage('Account created. Enter the verification code sent to your email.');
      setMode('verify');
      setPassword('');
      setConfirmPassword('');
      setVerificationCode('');
      clearRegistrationSuccess();
    }
  }, [registrationSuccess, clearRegistrationSuccess, email]);

frontend/src/components/auth/LoginPage.tsx:104

  • When an unverified user attempts to log in, the UI switches to verify mode but leaves the password in component state. If the user navigates back to login/register, the password field can be repopulated with the prior value, which is unnecessary and increases the chance of accidental disclosure (e.g., shared screen).
      if (mode === 'login' && err?.message === 'Email verification required') {
        clearError();
        setVerificationCode('');
        setSuccessMessage('Enter the verification code sent to your email.');
        setMode('verify');
      }

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 195 to 201
<Typography variant="body2" color="text.secondary">
{mode === 'login'
? 'Sign in to start working on your models.'
: mode === 'register'
? 'Use your email and password to get started.'
: `Enter the 6-digit code sent to ${pendingVerificationEmail || email}.`}
: `Enter the 6-digit code sent to ${email.trim() || 'your email'}.`}
</Typography>
Comment thread backend/prisma/seed.ts
Comment on lines 25 to 34
if (existing) {
const updates: { role?: UserRole; emailVerified?: boolean } = {};
const updates: { role?: UserRole; emailVerified?: boolean; password?: string } = {};
if (existing.role !== role) updates.role = role;
if (!existing.emailVerified) updates.emailVerified = true;
// Keep the stored password in sync with the configured one, so the
// documented demo/admin credentials always work even if the account
// predates the seed or its password was changed in the app.
const passwordMatches = await bcrypt.compare(password, existing.password);
if (!passwordMatches) updates.password = await bcrypt.hash(password, 12);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants