Wave-3 user feedback (points 1–3): - Email templates: extract de/en/fr/es/it copy into emailTemplates.ts and pick the user's language. Signup forwards navigator.language and the API also respects Accept-Language; resend/reset reuse user.language. - Verify link: the email button now points at the client page instead of the API GET handler, so corporate inbox scanners (M365 Safe Links etc.) can no longer pre-fetch and silently consume the one-shot token. The client page requires a real click before calling PATCH /api/auth/ verify-email. The legacy GET handler now just redirects to the client page so old in-flight emails keep working. - Code expired UX: when users land via an expired-link redirect we no longer leave them with a blank email field — the page exposes an email input so the resend button has something to act on. The verify-email redirect also forwards the email param. Misc: tsconfig migrated to moduleResolution=bundler and dropped downlevelIteration to clear the TS 7.0 deprecation errors. v1.101.0 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
79 lines
2.6 KiB
TypeScript
79 lines
2.6 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server';
|
|
export const dynamic = 'force-dynamic';
|
|
import { prisma } from '@/lib/prisma';
|
|
import { sendVerificationEmail, generateVerificationCode, generateVerificationToken } from '@/lib/email';
|
|
|
|
export async function POST(request: NextRequest) {
|
|
try {
|
|
const { email } = await request.json();
|
|
|
|
if (!email) {
|
|
return NextResponse.json(
|
|
{ error: 'Email is required' },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
const user = await prisma.user.findUnique({
|
|
where: { email },
|
|
select: {
|
|
id: true,
|
|
emailVerified: true,
|
|
emailVerificationExpires: true,
|
|
language: true,
|
|
},
|
|
});
|
|
|
|
if (!user) {
|
|
// Don't reveal whether user exists
|
|
return NextResponse.json({ message: 'If an account exists, a new code has been sent.' });
|
|
}
|
|
|
|
if (user.emailVerified) {
|
|
return NextResponse.json({ message: 'Email is already verified.' });
|
|
}
|
|
|
|
// Rate limit: don't resend if last code was sent less than 60 seconds ago
|
|
if (user.emailVerificationExpires) {
|
|
const lastSent = new Date(user.emailVerificationExpires.getTime() - 15 * 60 * 1000);
|
|
if (Date.now() - lastSent.getTime() < 60 * 1000) {
|
|
return NextResponse.json(
|
|
{ error: 'Please wait before requesting a new code.' },
|
|
{ status: 429 }
|
|
);
|
|
}
|
|
}
|
|
|
|
const code = generateVerificationCode();
|
|
const token = generateVerificationToken();
|
|
const expires = new Date(Date.now() + 15 * 60 * 1000);
|
|
|
|
await prisma.user.update({
|
|
where: { id: user.id },
|
|
data: {
|
|
emailVerificationCode: code,
|
|
emailVerificationToken: token,
|
|
emailVerificationExpires: expires,
|
|
},
|
|
});
|
|
|
|
try {
|
|
await sendVerificationEmail(email, code, token, user.language);
|
|
} catch (emailError) {
|
|
console.error('Failed to resend verification email:', emailError);
|
|
return NextResponse.json(
|
|
{ error: 'Failed to send email. Please try again later.' },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
|
|
return NextResponse.json({ message: 'A new verification code has been sent.' });
|
|
} catch (error) {
|
|
console.error('Resend verification error:', error);
|
|
return NextResponse.json(
|
|
{ error: 'Something went wrong' },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
}
|