feat: add password reset flow
- 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
This commit is contained in:
@@ -1,3 +1,3 @@
|
||||
import { createAuthClient } from "better-auth/react";
|
||||
|
||||
export const authClient = createAuthClient({});
|
||||
export const authClient = createAuthClient();
|
||||
|
||||
@@ -10,8 +10,10 @@
|
||||
|
||||
import { Route as rootRouteImport } from './routes/__root'
|
||||
import { Route as TermsRouteImport } from './routes/terms'
|
||||
import { Route as ResetPasswordRouteImport } from './routes/reset-password'
|
||||
import { Route as PrivacyRouteImport } from './routes/privacy'
|
||||
import { Route as LoginRouteImport } from './routes/login'
|
||||
import { Route as ForgotPasswordRouteImport } from './routes/forgot-password'
|
||||
import { Route as DrinkkaartRouteImport } from './routes/drinkkaart'
|
||||
import { Route as ContactRouteImport } from './routes/contact'
|
||||
import { Route as AccountRouteImport } from './routes/account'
|
||||
@@ -29,6 +31,11 @@ const TermsRoute = TermsRouteImport.update({
|
||||
path: '/terms',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const ResetPasswordRoute = ResetPasswordRouteImport.update({
|
||||
id: '/reset-password',
|
||||
path: '/reset-password',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const PrivacyRoute = PrivacyRouteImport.update({
|
||||
id: '/privacy',
|
||||
path: '/privacy',
|
||||
@@ -39,6 +46,11 @@ const LoginRoute = LoginRouteImport.update({
|
||||
path: '/login',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const ForgotPasswordRoute = ForgotPasswordRouteImport.update({
|
||||
id: '/forgot-password',
|
||||
path: '/forgot-password',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const DrinkkaartRoute = DrinkkaartRouteImport.update({
|
||||
id: '/drinkkaart',
|
||||
path: '/drinkkaart',
|
||||
@@ -100,8 +112,10 @@ export interface FileRoutesByFullPath {
|
||||
'/account': typeof AccountRoute
|
||||
'/contact': typeof ContactRoute
|
||||
'/drinkkaart': typeof DrinkkaartRoute
|
||||
'/forgot-password': typeof ForgotPasswordRoute
|
||||
'/login': typeof LoginRoute
|
||||
'/privacy': typeof PrivacyRoute
|
||||
'/reset-password': typeof ResetPasswordRoute
|
||||
'/terms': typeof TermsRoute
|
||||
'/admin/drinkkaart': typeof AdminDrinkkaartRoute
|
||||
'/manage/$token': typeof ManageTokenRoute
|
||||
@@ -116,8 +130,10 @@ export interface FileRoutesByTo {
|
||||
'/account': typeof AccountRoute
|
||||
'/contact': typeof ContactRoute
|
||||
'/drinkkaart': typeof DrinkkaartRoute
|
||||
'/forgot-password': typeof ForgotPasswordRoute
|
||||
'/login': typeof LoginRoute
|
||||
'/privacy': typeof PrivacyRoute
|
||||
'/reset-password': typeof ResetPasswordRoute
|
||||
'/terms': typeof TermsRoute
|
||||
'/admin/drinkkaart': typeof AdminDrinkkaartRoute
|
||||
'/manage/$token': typeof ManageTokenRoute
|
||||
@@ -133,8 +149,10 @@ export interface FileRoutesById {
|
||||
'/account': typeof AccountRoute
|
||||
'/contact': typeof ContactRoute
|
||||
'/drinkkaart': typeof DrinkkaartRoute
|
||||
'/forgot-password': typeof ForgotPasswordRoute
|
||||
'/login': typeof LoginRoute
|
||||
'/privacy': typeof PrivacyRoute
|
||||
'/reset-password': typeof ResetPasswordRoute
|
||||
'/terms': typeof TermsRoute
|
||||
'/admin/drinkkaart': typeof AdminDrinkkaartRoute
|
||||
'/manage/$token': typeof ManageTokenRoute
|
||||
@@ -151,8 +169,10 @@ export interface FileRouteTypes {
|
||||
| '/account'
|
||||
| '/contact'
|
||||
| '/drinkkaart'
|
||||
| '/forgot-password'
|
||||
| '/login'
|
||||
| '/privacy'
|
||||
| '/reset-password'
|
||||
| '/terms'
|
||||
| '/admin/drinkkaart'
|
||||
| '/manage/$token'
|
||||
@@ -167,8 +187,10 @@ export interface FileRouteTypes {
|
||||
| '/account'
|
||||
| '/contact'
|
||||
| '/drinkkaart'
|
||||
| '/forgot-password'
|
||||
| '/login'
|
||||
| '/privacy'
|
||||
| '/reset-password'
|
||||
| '/terms'
|
||||
| '/admin/drinkkaart'
|
||||
| '/manage/$token'
|
||||
@@ -183,8 +205,10 @@ export interface FileRouteTypes {
|
||||
| '/account'
|
||||
| '/contact'
|
||||
| '/drinkkaart'
|
||||
| '/forgot-password'
|
||||
| '/login'
|
||||
| '/privacy'
|
||||
| '/reset-password'
|
||||
| '/terms'
|
||||
| '/admin/drinkkaart'
|
||||
| '/manage/$token'
|
||||
@@ -200,8 +224,10 @@ export interface RootRouteChildren {
|
||||
AccountRoute: typeof AccountRoute
|
||||
ContactRoute: typeof ContactRoute
|
||||
DrinkkaartRoute: typeof DrinkkaartRoute
|
||||
ForgotPasswordRoute: typeof ForgotPasswordRoute
|
||||
LoginRoute: typeof LoginRoute
|
||||
PrivacyRoute: typeof PrivacyRoute
|
||||
ResetPasswordRoute: typeof ResetPasswordRoute
|
||||
TermsRoute: typeof TermsRoute
|
||||
AdminDrinkkaartRoute: typeof AdminDrinkkaartRoute
|
||||
ManageTokenRoute: typeof ManageTokenRoute
|
||||
@@ -221,6 +247,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof TermsRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/reset-password': {
|
||||
id: '/reset-password'
|
||||
path: '/reset-password'
|
||||
fullPath: '/reset-password'
|
||||
preLoaderRoute: typeof ResetPasswordRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/privacy': {
|
||||
id: '/privacy'
|
||||
path: '/privacy'
|
||||
@@ -235,6 +268,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof LoginRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/forgot-password': {
|
||||
id: '/forgot-password'
|
||||
path: '/forgot-password'
|
||||
fullPath: '/forgot-password'
|
||||
preLoaderRoute: typeof ForgotPasswordRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/drinkkaart': {
|
||||
id: '/drinkkaart'
|
||||
path: '/drinkkaart'
|
||||
@@ -320,8 +360,10 @@ const rootRouteChildren: RootRouteChildren = {
|
||||
AccountRoute: AccountRoute,
|
||||
ContactRoute: ContactRoute,
|
||||
DrinkkaartRoute: DrinkkaartRoute,
|
||||
ForgotPasswordRoute: ForgotPasswordRoute,
|
||||
LoginRoute: LoginRoute,
|
||||
PrivacyRoute: PrivacyRoute,
|
||||
ResetPasswordRoute: ResetPasswordRoute,
|
||||
TermsRoute: TermsRoute,
|
||||
AdminDrinkkaartRoute: AdminDrinkkaartRoute,
|
||||
ManageTokenRoute: ManageTokenRoute,
|
||||
|
||||
118
apps/web/src/routes/forgot-password.tsx
Normal file
118
apps/web/src/routes/forgot-password.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { createFileRoute, Link } from "@tanstack/react-router";
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
|
||||
export const Route = createFileRoute("/forgot-password")({
|
||||
component: ForgotPasswordPage,
|
||||
});
|
||||
|
||||
function ForgotPasswordPage() {
|
||||
const [email, setEmail] = useState("");
|
||||
const [sent, setSent] = useState(false);
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: async (email: string) => {
|
||||
const result = await authClient.requestPasswordReset({
|
||||
email,
|
||||
redirectTo: "/reset-password",
|
||||
});
|
||||
if (result.error) {
|
||||
throw new Error(result.error.message);
|
||||
}
|
||||
return result.data;
|
||||
},
|
||||
onSuccess: () => {
|
||||
setSent(true);
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
mutation.mutate(email);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex min-h-[calc(100dvh-2.75rem)] items-center justify-center overflow-y-auto px-4 py-8 sm:min-h-[calc(100dvh-3.5rem)]">
|
||||
<Card className="my-auto w-full max-w-md border-white/10 bg-white/5">
|
||||
<CardHeader className="text-center">
|
||||
<CardTitle className="font-['Intro',sans-serif] text-3xl text-white">
|
||||
Wachtwoord vergeten
|
||||
</CardTitle>
|
||||
<CardDescription className="text-white/60">
|
||||
{sent
|
||||
? "Controleer je inbox"
|
||||
: "Vul je e-mailadres in en we sturen je een link"}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{sent ? (
|
||||
<div className="space-y-4">
|
||||
<div className="rounded-lg border border-white/10 bg-white/5 p-5 text-center">
|
||||
<p className="text-sm text-white/80 leading-relaxed">
|
||||
Als er een account bestaat voor{" "}
|
||||
<span className="font-medium text-white">{email}</span>, is er
|
||||
een e-mail verstuurd met een link om je wachtwoord opnieuw in
|
||||
te stellen. De link is 1 uur geldig.
|
||||
</p>
|
||||
</div>
|
||||
<Link
|
||||
to="/login"
|
||||
className="block text-center text-sm text-white/60 hover:text-white"
|
||||
>
|
||||
← Terug naar inloggen
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label
|
||||
htmlFor="email"
|
||||
className="mb-2 block text-sm text-white/60"
|
||||
>
|
||||
E-mailadres
|
||||
</label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="jouw@email.be"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
className="border-white/20 bg-white/10 text-white placeholder:text-white/40"
|
||||
/>
|
||||
</div>
|
||||
{mutation.isError && (
|
||||
<p className="text-red-400 text-sm">{mutation.error.message}</p>
|
||||
)}
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={mutation.isPending}
|
||||
className="w-full bg-white text-[#214e51] hover:bg-white/90"
|
||||
>
|
||||
{mutation.isPending ? "Bezig..." : "Stuur reset-link"}
|
||||
</Button>
|
||||
<div className="text-center">
|
||||
<Link
|
||||
to="/login"
|
||||
className="text-sm text-white/60 hover:text-white"
|
||||
>
|
||||
← Terug naar inloggen
|
||||
</Link>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -299,6 +299,16 @@ function LoginPage() {
|
||||
className="border-white/20 bg-white/10 text-white placeholder:text-white/40"
|
||||
/>
|
||||
</div>
|
||||
{!isSignup && (
|
||||
<div className="text-right">
|
||||
<Link
|
||||
to="/forgot-password"
|
||||
className="text-sm text-white/50 hover:text-white"
|
||||
>
|
||||
Wachtwoord vergeten?
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={loginMutation.isPending || signupMutation.isPending}
|
||||
|
||||
154
apps/web/src/routes/reset-password.tsx
Normal file
154
apps/web/src/routes/reset-password.tsx
Normal file
@@ -0,0 +1,154 @@
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { createFileRoute, Link, useNavigate } from "@tanstack/react-router";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
|
||||
export const Route = createFileRoute("/reset-password")({
|
||||
validateSearch: (search: Record<string, unknown>) => {
|
||||
const token = typeof search.token === "string" ? search.token : undefined;
|
||||
return { ...(token !== undefined && { token }) } as { token?: string };
|
||||
},
|
||||
component: ResetPasswordPage,
|
||||
});
|
||||
|
||||
function ResetPasswordPage() {
|
||||
const navigate = useNavigate();
|
||||
const search = Route.useSearch();
|
||||
const token = search.token;
|
||||
|
||||
const [password, setPassword] = useState("");
|
||||
const [confirm, setConfirm] = useState("");
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: async ({
|
||||
token,
|
||||
newPassword,
|
||||
}: {
|
||||
token: string;
|
||||
newPassword: string;
|
||||
}) => {
|
||||
const result = await authClient.resetPassword({ token, newPassword });
|
||||
if (result.error) {
|
||||
throw new Error(result.error.message);
|
||||
}
|
||||
return result.data;
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.success("Wachtwoord succesvol gewijzigd!");
|
||||
navigate({ to: "/login" });
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (password !== confirm) {
|
||||
toast.error("De wachtwoorden komen niet overeen.");
|
||||
return;
|
||||
}
|
||||
if (!token) {
|
||||
toast.error("Ongeldige reset-link. Vraag een nieuwe aan.");
|
||||
return;
|
||||
}
|
||||
mutation.mutate({ token, newPassword: password });
|
||||
};
|
||||
|
||||
if (!token) {
|
||||
return (
|
||||
<div className="flex min-h-[calc(100dvh-2.75rem)] items-center justify-center px-4 sm:min-h-[calc(100dvh-3.5rem)]">
|
||||
<Card className="w-full max-w-md border-white/10 bg-white/5">
|
||||
<CardHeader className="text-center">
|
||||
<CardTitle className="font-['Intro',sans-serif] text-3xl text-white">
|
||||
Ongeldige link
|
||||
</CardTitle>
|
||||
<CardDescription className="text-white/60">
|
||||
Deze reset-link is ongeldig of verlopen.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="text-center">
|
||||
<Link
|
||||
to="/forgot-password"
|
||||
className="text-sm text-white/60 hover:text-white"
|
||||
>
|
||||
Vraag een nieuwe reset-link aan
|
||||
</Link>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex min-h-[calc(100dvh-2.75rem)] items-center justify-center overflow-y-auto px-4 py-8 sm:min-h-[calc(100dvh-3.5rem)]">
|
||||
<Card className="my-auto w-full max-w-md border-white/10 bg-white/5">
|
||||
<CardHeader className="text-center">
|
||||
<CardTitle className="font-['Intro',sans-serif] text-3xl text-white">
|
||||
Nieuw wachtwoord
|
||||
</CardTitle>
|
||||
<CardDescription className="text-white/60">
|
||||
Kies een nieuw wachtwoord voor je account
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label
|
||||
htmlFor="password"
|
||||
className="mb-2 block text-sm text-white/60"
|
||||
>
|
||||
Nieuw wachtwoord
|
||||
</label>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
placeholder="••••••••"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
minLength={8}
|
||||
className="border-white/20 bg-white/10 text-white placeholder:text-white/40"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
htmlFor="confirm"
|
||||
className="mb-2 block text-sm text-white/60"
|
||||
>
|
||||
Bevestig wachtwoord
|
||||
</label>
|
||||
<Input
|
||||
id="confirm"
|
||||
type="password"
|
||||
placeholder="••••••••"
|
||||
value={confirm}
|
||||
onChange={(e) => setConfirm(e.target.value)}
|
||||
required
|
||||
minLength={8}
|
||||
className="border-white/20 bg-white/10 text-white placeholder:text-white/40"
|
||||
/>
|
||||
</div>
|
||||
{mutation.isError && (
|
||||
<p className="text-red-400 text-sm">{mutation.error.message}</p>
|
||||
)}
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={mutation.isPending}
|
||||
className="w-full bg-white text-[#214e51] hover:bg-white/90"
|
||||
>
|
||||
{mutation.isPending ? "Bezig..." : "Stel wachtwoord in"}
|
||||
</Button>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
4
bun.lock
4
bun.lock
@@ -111,10 +111,12 @@
|
||||
"@kk/env": "workspace:*",
|
||||
"better-auth": "catalog:",
|
||||
"dotenv": "catalog:",
|
||||
"nodemailer": "^8.0.2",
|
||||
"zod": "catalog:",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@kk/config": "workspace:*",
|
||||
"@types/nodemailer": "^7.0.11",
|
||||
"typescript": "catalog:",
|
||||
},
|
||||
},
|
||||
@@ -1985,6 +1987,8 @@
|
||||
|
||||
"@jridgewell/remapping/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
||||
|
||||
"@kk/auth/nodemailer": ["nodemailer@8.0.2", "", {}, "sha512-zbj002pZAIkWQFxyAaqoxvn+zoIwRnS40hgjqTXudKOOJkiFFgBeNqjgD3/YCR12sZnrghWYBY+yP1ZucdDRpw=="],
|
||||
|
||||
"@noble/curves/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="],
|
||||
|
||||
"@octokit/plugin-paginate-rest/@octokit/types": ["@octokit/types@13.10.0", "", { "dependencies": { "@octokit/openapi-types": "^24.2.0" } }, "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA=="],
|
||||
|
||||
@@ -15,10 +15,12 @@
|
||||
"@kk/env": "workspace:*",
|
||||
"better-auth": "catalog:",
|
||||
"dotenv": "catalog:",
|
||||
"nodemailer": "^8.0.2",
|
||||
"zod": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@kk/config": "workspace:*",
|
||||
"@types/nodemailer": "^7.0.11",
|
||||
"typescript": "catalog:"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,96 @@ 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, {
|
||||
@@ -35,6 +125,9 @@ export const auth = betterAuth({
|
||||
return keyBuffer.equals(hashBuffer);
|
||||
},
|
||||
},
|
||||
sendResetPassword: async ({ user, url }) => {
|
||||
await sendPasswordResetEmail(user.email, url);
|
||||
},
|
||||
},
|
||||
user: {
|
||||
additionalFields: {
|
||||
|
||||
Reference in New Issue
Block a user