feat:add registration management with token-based access

Add management tokens to registrations allowing users to view, edit, and
cancel their registration via a unique URL. Implement email
notifications
for confirmations, updates, and cancellations using nodemailer. Simplify
art forms grid from 6 to 4 items and remove trajectory links. Translate
footer links to Dutch and fix matzah spelling in info section.
This commit is contained in:
2026-03-02 22:27:21 +01:00
parent 37d9a415eb
commit 3c439649f9
18 changed files with 2092 additions and 627 deletions

View File

@@ -3,8 +3,13 @@ import { db } from "@kk/db";
import { adminRequest, registration } from "@kk/db/schema";
import { user } from "@kk/db/schema/auth";
import type { RouterClient } from "@orpc/server";
import { and, count, desc, eq, gte, like, lte } from "drizzle-orm";
import { and, count, desc, eq, gte, isNull, like, lte } from "drizzle-orm";
import { z } from "zod";
import {
sendCancellationEmail,
sendConfirmationEmail,
sendUpdateEmail,
} from "../email";
import { adminProcedure, protectedProcedure, publicProcedure } from "../index";
const submitRegistrationSchema = z.object({
@@ -42,7 +47,8 @@ export const appRouter = {
submitRegistration: publicProcedure
.input(submitRegistrationSchema)
.handler(async ({ input }) => {
const result = await db.insert(registration).values({
const managementToken = randomUUID();
await db.insert(registration).values({
id: randomUUID(),
firstName: input.firstName,
lastName: input.lastName,
@@ -52,9 +58,122 @@ export const appRouter = {
artForm: input.artForm || null,
experience: input.experience || null,
extraQuestions: input.extraQuestions || null,
managementToken,
});
return { success: true, id: result.lastInsertRowid };
await sendConfirmationEmail({
to: input.email,
firstName: input.firstName,
managementToken,
wantsToPerform: input.wantsToPerform,
artForm: input.artForm,
}).catch((err) =>
console.error("Failed to send confirmation email:", err),
);
return { success: true, managementToken };
}),
getRegistrationByToken: publicProcedure
.input(z.object({ token: z.string().uuid() }))
.handler(async ({ input }) => {
const rows = await db
.select()
.from(registration)
.where(eq(registration.managementToken, input.token))
.limit(1);
const row = rows[0];
if (!row) throw new Error("Inschrijving niet gevonden");
if (row.cancelledAt) throw new Error("Deze inschrijving is geannuleerd");
return row;
}),
updateRegistration: publicProcedure
.input(
z.object({
token: z.string().uuid(),
firstName: z.string().min(1),
lastName: z.string().min(1),
email: z.string().email(),
phone: z.string().optional(),
wantsToPerform: z.boolean().default(false),
artForm: z.string().optional(),
experience: z.string().optional(),
extraQuestions: z.string().optional(),
}),
)
.handler(async ({ input }) => {
const rows = await db
.select()
.from(registration)
.where(
and(
eq(registration.managementToken, input.token),
isNull(registration.cancelledAt),
),
)
.limit(1);
const row = rows[0];
if (!row) throw new Error("Inschrijving niet gevonden of al geannuleerd");
await db
.update(registration)
.set({
firstName: input.firstName,
lastName: input.lastName,
email: input.email,
phone: input.phone || null,
wantsToPerform: input.wantsToPerform,
artForm: input.artForm || null,
experience: input.experience || null,
extraQuestions: input.extraQuestions || null,
})
.where(eq(registration.managementToken, input.token));
await sendUpdateEmail({
to: input.email,
firstName: input.firstName,
managementToken: input.token,
wantsToPerform: input.wantsToPerform,
artForm: input.artForm,
}).catch((err) => console.error("Failed to send update email:", err));
return { success: true };
}),
cancelRegistration: publicProcedure
.input(z.object({ token: z.string().uuid() }))
.handler(async ({ input }) => {
const rows = await db
.select()
.from(registration)
.where(
and(
eq(registration.managementToken, input.token),
isNull(registration.cancelledAt),
),
)
.limit(1);
const row = rows[0];
if (!row) throw new Error("Inschrijving niet gevonden of al geannuleerd");
await db
.update(registration)
.set({ cancelledAt: new Date() })
.where(eq(registration.managementToken, input.token));
await sendCancellationEmail({
to: row.email,
firstName: row.firstName,
}).catch((err) =>
console.error("Failed to send cancellation email:", err),
);
return { success: true };
}),
getRegistrations: adminProcedure