CI / check (push) Canceled after 0s
- 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
53 lines
2.5 KiB
TypeScript
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'),
|
|
);
|
|
}
|