Files
musicshare/apps/api/src/services/emailVerification.ts
T
chrisandClaude Opus 5.5 85e1643c29
CI / check (push) Canceled after 0s
Email confirmation, change email/password, show/hide passwords
- Sign-up emails a confirmation link (24h); unconfirmed accounts see a
  banner on the web with "send it again", and status in Settings.
- Change email (needs the password): the new address gets a confirmation
  link and only replaces the old one once clicked; the old address gets a
  heads-up with a reset-password link. Pending changes can be cancelled.
- Change password (needs the current one): signs out other sessions.
- Password fields have Show/Hide on web and mobile.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MzFywujuH9TiuXbr9vjoX8
2026-10-06 13:05:26 -04:00

53 lines
2.5 KiB
TypeScript

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