My-Weekly-ToDo-List/src/app/api/auth/resend-verification/route.ts
mARTin-B78 d3a40e32a0 fix: localise auth emails, repair verify link, recover from expired
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>
2026-05-01 20:09:33 +02:00

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 }
);
}
}