- Add /forgot-password and /reset-password routes (Dutch UI) - Wire sendResetPassword into emailAndPassword config (not a plugin) - Send styled HTML email via nodemailer with 1-hour expiry - Add 'Wachtwoord vergeten?' link on login page - Update routeTree.gen.ts with new routes
142 lines
5.1 KiB
TypeScript
142 lines
5.1 KiB
TypeScript
import { randomBytes, scryptSync } from "node:crypto";
|
|
import { db } from "@kk/db";
|
|
import * as schema from "@kk/db/schema/auth";
|
|
import { env } from "@kk/env/server";
|
|
import { betterAuth } from "better-auth";
|
|
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
|
import { tanstackStartCookies } from "better-auth/tanstack-start";
|
|
import nodemailer from "nodemailer";
|
|
|
|
const _smtpFrom = env.SMTP_FROM ?? "Kunstenkamp <info@kunstenkamp.be>";
|
|
|
|
let _transport: nodemailer.Transporter | null | undefined;
|
|
function getTransport(): nodemailer.Transporter | null {
|
|
if (_transport !== undefined) return _transport;
|
|
if (!env.SMTP_HOST || !env.SMTP_USER || !env.SMTP_PASS) {
|
|
_transport = null;
|
|
return null;
|
|
}
|
|
_transport = nodemailer.createTransport({
|
|
host: env.SMTP_HOST,
|
|
port: env.SMTP_PORT,
|
|
secure: env.SMTP_PORT === 465,
|
|
auth: { user: env.SMTP_USER, pass: env.SMTP_PASS },
|
|
});
|
|
return _transport;
|
|
}
|
|
|
|
async function sendPasswordResetEmail(to: string, resetUrl: string) {
|
|
const transport = getTransport();
|
|
if (!transport) {
|
|
console.warn("SMTP not configured — skipping password reset email");
|
|
return;
|
|
}
|
|
const html = `<!DOCTYPE html>
|
|
<html lang="nl">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<title>Wachtwoord opnieuw instellen</title>
|
|
</head>
|
|
<body style="margin:0;padding:0;background:#f4f4f5;font-family:sans-serif;">
|
|
<table width="100%" cellpadding="0" cellspacing="0" style="background:#f4f4f5;padding:40px 0;">
|
|
<tr>
|
|
<td align="center">
|
|
<table width="600" cellpadding="0" cellspacing="0" style="background:#214e51;border-radius:4px;overflow:hidden;">
|
|
<tr>
|
|
<td style="padding:40px 48px 32px;">
|
|
<p style="margin:0;font-size:13px;color:rgba(255,255,255,0.5);letter-spacing:0.08em;text-transform:uppercase;">Kunstenkamp</p>
|
|
<h1 style="margin:12px 0 0;font-size:28px;color:#ffffff;font-weight:700;">Wachtwoord opnieuw instellen</h1>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="padding:0 48px 32px;">
|
|
<p style="margin:0 0 16px;font-size:16px;color:rgba(255,255,255,0.85);line-height:1.6;">
|
|
We hebben een aanvraag ontvangen om het wachtwoord van je Kunstenkamp-account opnieuw in te stellen.
|
|
</p>
|
|
<p style="margin:0 0 24px;font-size:16px;color:rgba(255,255,255,0.85);line-height:1.6;">
|
|
Klik op de knop hieronder om een nieuw wachtwoord in te stellen. Deze link is <strong style="color:#ffffff;">1 uur geldig</strong>.
|
|
</p>
|
|
<table cellpadding="0" cellspacing="0">
|
|
<tr>
|
|
<td style="border-radius:2px;background:#ffffff;">
|
|
<a href="${resetUrl}" style="display:inline-block;padding:14px 32px;font-size:16px;font-weight:600;color:#214e51;text-decoration:none;">
|
|
Stel wachtwoord in
|
|
</a>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
<p style="margin:16px 0 0;font-size:13px;color:rgba(255,255,255,0.4);">
|
|
Of kopieer deze link: <span style="color:rgba(255,255,255,0.6);">${resetUrl}</span>
|
|
</p>
|
|
<p style="margin:24px 0 0;font-size:14px;color:rgba(255,255,255,0.5);line-height:1.6;">
|
|
Heb je dit niet aangevraagd? Dan hoef je niets te doen — je wachtwoord blijft ongewijzigd.
|
|
</p>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="padding:24px 48px;border-top:1px solid rgba(255,255,255,0.1);">
|
|
<p style="margin:0;font-size:13px;color:rgba(255,255,255,0.4);line-height:1.6;">
|
|
Vragen? Mail ons op <a href="mailto:info@kunstenkamp.be" style="color:rgba(255,255,255,0.6);">info@kunstenkamp.be</a><br/>
|
|
Kunstenkamp vzw — Een initiatief voor en door kunstenaars.
|
|
</p>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
</body>
|
|
</html>`;
|
|
await transport.sendMail({
|
|
from: _smtpFrom,
|
|
to,
|
|
subject: "Wachtwoord opnieuw instellen — Kunstenkamp",
|
|
html,
|
|
});
|
|
}
|
|
|
|
export const auth = betterAuth({
|
|
database: drizzleAdapter(db, {
|
|
provider: "sqlite",
|
|
|
|
schema: schema,
|
|
}),
|
|
trustedOrigins: [
|
|
env.CORS_ORIGIN,
|
|
"http://localhost:3000",
|
|
"http://localhost:3001",
|
|
],
|
|
emailAndPassword: {
|
|
enabled: true,
|
|
// Use Cloudflare's native scrypt via node:crypto for better performance
|
|
// This avoids CPU time limit errors on Cloudflare Workers
|
|
password: {
|
|
hash: async (password) => {
|
|
const salt = randomBytes(16).toString("hex");
|
|
const hash = scryptSync(password, salt, 64).toString("hex");
|
|
return `${salt}:${hash}`;
|
|
},
|
|
verify: async ({ hash, password }) => {
|
|
const [salt, key] = hash.split(":");
|
|
if (!salt || !key) return false;
|
|
const keyBuffer = Buffer.from(key, "hex");
|
|
const hashBuffer = scryptSync(password, salt, 64);
|
|
return keyBuffer.equals(hashBuffer);
|
|
},
|
|
},
|
|
sendResetPassword: async ({ user, url }) => {
|
|
await sendPasswordResetEmail(user.email, url);
|
|
},
|
|
},
|
|
user: {
|
|
additionalFields: {
|
|
role: {
|
|
type: "string",
|
|
input: false,
|
|
},
|
|
},
|
|
},
|
|
plugins: [tanstackStartCookies()],
|
|
});
|