import { eq } from 'drizzle-orm'; import type { FastifyBaseLogger } from 'fastify'; import { db, schema } from '../db'; import type { User } from '../db/schema'; import { env } from '../env'; import { randomToken, sha256 } from '../lib/crypto'; import { sendMail, simpleEmail } from '../lib/mail'; const VERIFY_HOURS = 24; /** * Emails a confirmation link for `email` (their current address after sign-up, * or a new one they're switching to). Replaces any earlier link. */ export async function sendVerificationEmail(user: User, email: string, log: FastifyBaseLogger) { await db.delete(schema.emailVerifications).where(eq(schema.emailVerifications.userId, user.id)); const token = randomToken(); await db.insert(schema.emailVerifications).values({ id: sha256(token), userId: user.id, email, expiresAt: new Date(Date.now() + VERIFY_HOURS * 3600_000), }); const changing = email !== user.email; const url = `${env.WEB_URL.replace(/\/$/, '')}/verify-email?token=${encodeURIComponent(token)}`; const mail = simpleEmail({ lines: changing ? [`Hi ${user.displayName ?? user.username},`, `Confirm ${email} as the new email for your musicshare account.`] : [`Welcome to musicshare, ${user.displayName ?? user.username}!`, 'Confirm your email so you can reset your password if you ever forget it.'], button: { label: changing ? 'Confirm new email' : 'Confirm my email', url }, footer: `This link expires in ${VERIFY_HOURS} hours. If you didn't ask for this, you can ignore it.`, }); sendMail({ to: email, subject: changing ? 'Confirm your new musicshare email' : 'Confirm your musicshare email', ...mail }).catch( (err) => log.error({ err }, 'verification email failed'), ); } /** Tells the old address an email change was requested, so a hijacker can't do it quietly. */ export function notifyEmailChange(user: User, newEmail: string, log: FastifyBaseLogger) { const mail = simpleEmail({ lines: [ `Hi ${user.displayName ?? user.username},`, `Someone asked to change your musicshare email to ${newEmail}. It only changes once that address is confirmed.`, "If this wasn't you, reset your password now.", ], button: { label: 'Reset my password', url: `${env.WEB_URL.replace(/\/$/, '')}/forgot-password` }, footer: 'You can ignore this email if you made the change.', }); sendMail({ to: user.email, subject: 'Your musicshare email is being changed', ...mail }).catch((err) => log.error({ err }, 'email change notice failed'), ); }