feat(auth): add configurable email verification enforcement

Auth:RequireEmailVerification (default off) gates whether local
register requires confirming email before login. OAuth new-user paths
are untouched -- Google/Microsoft already assert a verified email.
Adds verify-email and resend-verification-email endpoints, mirroring
the existing reset-password enumeration-avoidance and rate-limiting
patterns, plus a login-embedded resend affordance and a verify-email
landing page on the frontend.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
cesnimda
2026-07-13 01:22:26 +02:00
parent 0ca2f2b261
commit 904f3a8ec8
9 changed files with 528 additions and 3 deletions
+29
View File
@@ -137,4 +137,33 @@ describe('LoginPage', () => {
expect(await screen.findByRole('alert')).toHaveTextContent('Too many attempts. Please wait a few minutes and try again.');
});
it('offers a resend-verification action when login reports the account is not verified', async () => {
mockedApi.get.mockResolvedValueOnce({
data: { requireAuth: false, googleEnabled: false, microsoftEnabled: false, localEnabled: true, allowRegistration: false, requireEmailVerification: true },
} as any);
mockedApi.post.mockImplementation((url: string) => {
if (url === '/auth/login') {
return Promise.reject({ response: { status: 403, data: { error: 'email_not_verified' } } });
}
if (url === '/auth/resend-verification-email') {
return Promise.resolve({ data: {} } as any);
}
return Promise.resolve({ data: {} } as any);
});
renderLoginPage();
await screen.findByLabelText('Email');
await userEvent.type(screen.getByLabelText('Email'), 'unverified@example.com');
await userEvent.type(screen.getByLabelText('Current password'), 'hunter2');
await userEvent.click(screen.getByRole('button', { name: 'Sign in' }));
expect(await screen.findByText('Please verify your email address before signing in.')).toBeInTheDocument();
await userEvent.click(screen.getByRole('button', { name: 'Resend verification email' }));
await waitFor(() => expect(mockedApi.post).toHaveBeenCalledWith('/auth/resend-verification-email', { email: 'unverified@example.com' }));
await screen.findByText('Verification email sent. Check your inbox.');
});
});