In de Bijbel staat ongedesemd brood (ook wel{" "}
- matze genoemd) symbool voor
+ matzah genoemd) symbool voor
eenvoud en zuiverheid, zonder de 'ballast' van
desem.
diff --git a/apps/web/src/routeTree.gen.ts b/apps/web/src/routeTree.gen.ts
index 1f3a7ca..a0db2ae 100644
--- a/apps/web/src/routeTree.gen.ts
+++ b/apps/web/src/routeTree.gen.ts
@@ -15,6 +15,7 @@ import { Route as LoginRouteImport } from './routes/login'
import { Route as ContactRouteImport } from './routes/contact'
import { Route as AdminRouteImport } from './routes/admin'
import { Route as IndexRouteImport } from './routes/index'
+import { Route as ManageTokenRouteImport } from './routes/manage.$token'
import { Route as ApiRpcSplatRouteImport } from './routes/api/rpc/$'
import { Route as ApiAuthSplatRouteImport } from './routes/api/auth/$'
@@ -48,6 +49,11 @@ const IndexRoute = IndexRouteImport.update({
path: '/',
getParentRoute: () => rootRouteImport,
} as any)
+const ManageTokenRoute = ManageTokenRouteImport.update({
+ id: '/manage/$token',
+ path: '/manage/$token',
+ getParentRoute: () => rootRouteImport,
+} as any)
const ApiRpcSplatRoute = ApiRpcSplatRouteImport.update({
id: '/api/rpc/$',
path: '/api/rpc/$',
@@ -66,6 +72,7 @@ export interface FileRoutesByFullPath {
'/login': typeof LoginRoute
'/privacy': typeof PrivacyRoute
'/terms': typeof TermsRoute
+ '/manage/$token': typeof ManageTokenRoute
'/api/auth/$': typeof ApiAuthSplatRoute
'/api/rpc/$': typeof ApiRpcSplatRoute
}
@@ -76,6 +83,7 @@ export interface FileRoutesByTo {
'/login': typeof LoginRoute
'/privacy': typeof PrivacyRoute
'/terms': typeof TermsRoute
+ '/manage/$token': typeof ManageTokenRoute
'/api/auth/$': typeof ApiAuthSplatRoute
'/api/rpc/$': typeof ApiRpcSplatRoute
}
@@ -87,6 +95,7 @@ export interface FileRoutesById {
'/login': typeof LoginRoute
'/privacy': typeof PrivacyRoute
'/terms': typeof TermsRoute
+ '/manage/$token': typeof ManageTokenRoute
'/api/auth/$': typeof ApiAuthSplatRoute
'/api/rpc/$': typeof ApiRpcSplatRoute
}
@@ -99,6 +108,7 @@ export interface FileRouteTypes {
| '/login'
| '/privacy'
| '/terms'
+ | '/manage/$token'
| '/api/auth/$'
| '/api/rpc/$'
fileRoutesByTo: FileRoutesByTo
@@ -109,6 +119,7 @@ export interface FileRouteTypes {
| '/login'
| '/privacy'
| '/terms'
+ | '/manage/$token'
| '/api/auth/$'
| '/api/rpc/$'
id:
@@ -119,6 +130,7 @@ export interface FileRouteTypes {
| '/login'
| '/privacy'
| '/terms'
+ | '/manage/$token'
| '/api/auth/$'
| '/api/rpc/$'
fileRoutesById: FileRoutesById
@@ -130,6 +142,7 @@ export interface RootRouteChildren {
LoginRoute: typeof LoginRoute
PrivacyRoute: typeof PrivacyRoute
TermsRoute: typeof TermsRoute
+ ManageTokenRoute: typeof ManageTokenRoute
ApiAuthSplatRoute: typeof ApiAuthSplatRoute
ApiRpcSplatRoute: typeof ApiRpcSplatRoute
}
@@ -178,6 +191,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
+ '/manage/$token': {
+ id: '/manage/$token'
+ path: '/manage/$token'
+ fullPath: '/manage/$token'
+ preLoaderRoute: typeof ManageTokenRouteImport
+ parentRoute: typeof rootRouteImport
+ }
'/api/rpc/$': {
id: '/api/rpc/$'
path: '/api/rpc/$'
@@ -202,6 +222,7 @@ const rootRouteChildren: RootRouteChildren = {
LoginRoute: LoginRoute,
PrivacyRoute: PrivacyRoute,
TermsRoute: TermsRoute,
+ ManageTokenRoute: ManageTokenRoute,
ApiAuthSplatRoute: ApiAuthSplatRoute,
ApiRpcSplatRoute: ApiRpcSplatRoute,
}
diff --git a/apps/web/src/routes/manage.$token.tsx b/apps/web/src/routes/manage.$token.tsx
new file mode 100644
index 0000000..940eaaa
--- /dev/null
+++ b/apps/web/src/routes/manage.$token.tsx
@@ -0,0 +1,430 @@
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
+import { createFileRoute, Link, useParams } from "@tanstack/react-router";
+import { useState } from "react";
+import { toast } from "sonner";
+import { orpc } from "@/utils/orpc";
+
+export const Route = createFileRoute("/manage/$token")({
+ component: ManageRegistrationPage,
+});
+
+function ManageRegistrationPage() {
+ const { token } = useParams({ from: "/manage/$token" });
+ const queryClient = useQueryClient();
+
+ const [isEditing, setIsEditing] = useState(false);
+ const [formData, setFormData] = useState({
+ firstName: "",
+ lastName: "",
+ email: "",
+ phone: "",
+ wantsToPerform: false,
+ artForm: "",
+ experience: "",
+ extraQuestions: "",
+ });
+
+ const { data, isLoading, error } = useQuery({
+ ...orpc.getRegistrationByToken.queryOptions({ input: { token } }),
+ });
+
+ const updateMutation = useMutation({
+ ...orpc.updateRegistration.mutationOptions(),
+ onSuccess: () => {
+ toast.success("Inschrijving bijgewerkt!");
+ queryClient.invalidateQueries({ queryKey: ["getRegistrationByToken"] });
+ setIsEditing(false);
+ },
+ onError: (err) => {
+ toast.error(`Opslaan mislukt: ${err.message}`);
+ },
+ });
+
+ const cancelMutation = useMutation({
+ ...orpc.cancelRegistration.mutationOptions(),
+ onSuccess: () => {
+ toast.success("Inschrijving geannuleerd");
+ queryClient.invalidateQueries({ queryKey: ["getRegistrationByToken"] });
+ },
+ onError: (err) => {
+ toast.error(`Annuleren mislukt: ${err.message}`);
+ },
+ });
+
+ const handleEdit = () => {
+ if (data) {
+ setFormData({
+ firstName: data.firstName,
+ lastName: data.lastName,
+ email: data.email,
+ phone: data.phone || "",
+ wantsToPerform: data.wantsToPerform ?? false,
+ artForm: data.artForm || "",
+ experience: data.experience || "",
+ extraQuestions: data.extraQuestions || "",
+ });
+ setIsEditing(true);
+ }
+ };
+
+ const handleSave = (e: React.FormEvent) => {
+ e.preventDefault();
+ updateMutation.mutate({
+ token,
+ firstName: formData.firstName.trim(),
+ lastName: formData.lastName.trim(),
+ email: formData.email.trim(),
+ phone: formData.phone.trim() || undefined,
+ wantsToPerform: formData.wantsToPerform,
+ artForm: formData.wantsToPerform
+ ? formData.artForm.trim() || undefined
+ : undefined,
+ experience: formData.wantsToPerform
+ ? formData.experience.trim() || undefined
+ : undefined,
+ extraQuestions: formData.extraQuestions.trim() || undefined,
+ });
+ };
+
+ const handleCancel = () => {
+ if (
+ confirm(
+ "Weet je zeker dat je je inschrijving wilt annuleren? Dit kan niet ongedaan worden gemaakt.",
+ )
+ ) {
+ cancelMutation.mutate({ token });
+ }
+ };
+
+ if (isLoading) {
+ return (
+
+ );
+ }
+
+ if (error || !data || data.cancelledAt) {
+ return (
+
+
+
+ ← Terug naar home
+
+
+ Inschrijving niet gevonden
+
+
+ Deze link is ongeldig of de inschrijving is geannuleerd.
+
+
+ Nieuwe inschrijving
+
+
+
+ );
+ }
+
+ if (isEditing) {
+ return (
+
+
+
+ ← Terug naar home
+
+
+ Bewerk inschrijving
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ ← Terug naar home
+
+
+
+ Jouw inschrijving
+
+
+ Open Mic Night — vrijdag 18 april 2026
+
+
+
+
+
+
Voornaam
+
{data.firstName}
+
+
+
Achternaam
+
{data.lastName}
+
+
+
E-mail
+
{data.email}
+
+
+
Telefoon
+
{data.phone || "—"}
+
+
+
+
+
Rol
+
+ {data.wantsToPerform
+ ? `Optreden${data.artForm ? ` — ${data.artForm}` : ""}`
+ : "Toeschouwer"}
+
+ {data.wantsToPerform && data.experience && (
+
{data.experience}
+ )}
+
+
+ {data.extraQuestions && (
+
+
+ Vragen of opmerkingen
+
+
{data.extraQuestions}
+
+ )}
+
+
+
+
+ Bewerken
+
+
+ {cancelMutation.isPending
+ ? "Annuleren..."
+ : "Inschrijving annuleren"}
+
+
+
+
+ Deze link is uniek voor jouw inschrijving. Bewaar deze pagina of de
+ bevestigingsmail om je gegevens later aan te passen.
+
+
+
+ );
+}
diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts
index e55389d..9660292 100644
--- a/apps/web/vite.config.ts
+++ b/apps/web/vite.config.ts
@@ -16,4 +16,5 @@ export default defineConfig({
server: {
port: 3001,
},
+ envDir: "../../packages/env",
});
diff --git a/bun.lock b/bun.lock
index 92fc315..3176a0a 100644
--- a/bun.lock
+++ b/bun.lock
@@ -89,10 +89,12 @@
"@orpc/zod": "catalog:",
"dotenv": "catalog:",
"drizzle-orm": "^0.45.1",
+ "nodemailer": "^8.0.1",
"zod": "catalog:",
},
"devDependencies": {
"@kk/config": "workspace:*",
+ "@types/nodemailer": "^7.0.11",
"typescript": "catalog:",
},
},
@@ -909,6 +911,8 @@
"@types/node": ["@types/node@22.19.11", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w=="],
+ "@types/nodemailer": ["@types/nodemailer@7.0.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-E+U4RzR2dKrx+u3N4DlsmLaDC6mMZOM/TPROxA0UAPiTgI0y4CEFBmZE+coGWTjakDriRsXG368lNk1u9Q0a2g=="],
+
"@types/offscreencanvas": ["@types/offscreencanvas@2019.7.3", "", {}, "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A=="],
"@types/react": ["@types/react@19.2.7", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg=="],
@@ -1481,6 +1485,8 @@
"node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="],
+ "nodemailer": ["nodemailer@8.0.1", "", {}, "sha512-5kcldIXmaEjZcHR6F28IKGSgpmZHaF1IXLWFTG+Xh3S+Cce4MiakLtWY+PlBU69fLbRa8HlaGIrC/QolUpHkhg=="],
+
"normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
"npm-run-path": ["npm-run-path@6.0.0", "", { "dependencies": { "path-key": "^4.0.0", "unicorn-magic": "^0.3.0" } }, "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA=="],
diff --git a/packages/api/package.json b/packages/api/package.json
index 00c04f5..348a15d 100644
--- a/packages/api/package.json
+++ b/packages/api/package.json
@@ -20,10 +20,12 @@
"@orpc/zod": "catalog:",
"dotenv": "catalog:",
"drizzle-orm": "^0.45.1",
+ "nodemailer": "^8.0.1",
"zod": "catalog:"
},
"devDependencies": {
"@kk/config": "workspace:*",
+ "@types/nodemailer": "^7.0.11",
"typescript": "catalog:"
}
}
diff --git a/packages/api/src/email.ts b/packages/api/src/email.ts
new file mode 100644
index 0000000..7700ddd
--- /dev/null
+++ b/packages/api/src/email.ts
@@ -0,0 +1,286 @@
+import { env } from "@kk/env/server";
+import nodemailer from "nodemailer";
+
+function createTransport() {
+ if (!env.SMTP_HOST || !env.SMTP_USER || !env.SMTP_PASS) {
+ return null;
+ }
+ return nodemailer.createTransport({
+ host: env.SMTP_HOST,
+ port: env.SMTP_PORT,
+ secure: env.SMTP_PORT === 465,
+ auth: {
+ user: env.SMTP_USER,
+ pass: env.SMTP_PASS,
+ },
+ });
+}
+
+const from = env.SMTP_FROM ?? "Kunstenkamp
";
+const baseUrl = env.BETTER_AUTH_URL ?? "https://kunstenkamp.be";
+
+function registrationConfirmationHtml(params: {
+ firstName: string;
+ manageUrl: string;
+ wantsToPerform: boolean;
+ artForm?: string | null;
+}) {
+ const role = params.wantsToPerform
+ ? `Optreden${params.artForm ? ` — ${params.artForm}` : ""}`
+ : "Toeschouwer";
+
+ return `
+
+
+
+
+ Bevestiging inschrijving
+
+
+
+
+
+
+
+
+
+ Kunstenkamp
+ Je inschrijving is bevestigd!
+
+
+
+
+
+
+ Hoi ${params.firstName},
+
+
+ We hebben je inschrijving voor Open Mic Night — vrijdag 18 april 2026 in goede orde ontvangen.
+
+
+
+
+
+ Jouw rol
+ ${role}
+
+
+
+
+ Wil je je gegevens later nog aanpassen of je inschrijving annuleren? Gebruik dan de knop hieronder. De link is uniek voor jou — deel hem niet.
+
+
+
+
+ Of kopieer deze link: ${params.manageUrl}
+
+
+
+
+
+
+
+ Vragen? Mail ons op info@kunstenkamp.be
+ Kunstenkamp vzw — Een initiatief voor en door kunstenaars.
+
+
+
+
+
+
+
+
+`;
+}
+
+function updateConfirmationHtml(params: {
+ firstName: string;
+ manageUrl: string;
+ wantsToPerform: boolean;
+ artForm?: string | null;
+}) {
+ const role = params.wantsToPerform
+ ? `Optreden${params.artForm ? ` — ${params.artForm}` : ""}`
+ : "Toeschouwer";
+
+ return `
+
+
+
+ Inschrijving bijgewerkt
+
+
+
+
+
+
+
+
+ Kunstenkamp
+ Inschrijving bijgewerkt
+
+
+
+
+
+ Hoi ${params.firstName},
+
+
+ Je inschrijving voor Open Mic Night — vrijdag 18 april 2026 is succesvol bijgewerkt.
+
+
+
+
+ Jouw rol
+ ${role}
+
+
+
+
+
+
+
+
+
+ Vragen? Mail ons op info@kunstenkamp.be
+
+
+
+
+
+
+
+
+`;
+}
+
+function cancellationHtml(params: { firstName: string }) {
+ return `
+
+
+
+ Inschrijving geannuleerd
+
+
+
+
+
+
+
+
+ Kunstenkamp
+ Inschrijving geannuleerd
+
+
+
+
+
+ Hoi ${params.firstName},
+
+
+ Je inschrijving voor Open Mic Night — vrijdag 18 april 2026 is geannuleerd.
+
+
+ Van gedachten veranderd? Je kunt je altijd opnieuw inschrijven via kunstenkamp.be .
+
+
+
+
+
+
+ Vragen? Mail ons op info@kunstenkamp.be
+
+
+
+
+
+
+
+
+`;
+}
+
+export async function sendConfirmationEmail(params: {
+ to: string;
+ firstName: string;
+ managementToken: string;
+ wantsToPerform: boolean;
+ artForm?: string | null;
+}) {
+ const transport = createTransport();
+ if (!transport) {
+ console.warn("SMTP not configured — skipping confirmation email");
+ return;
+ }
+ const manageUrl = `${baseUrl}/manage/${params.managementToken}`;
+ await transport.sendMail({
+ from,
+ to: params.to,
+ subject: "Bevestiging inschrijving — Open Mic Night",
+ html: registrationConfirmationHtml({
+ firstName: params.firstName,
+ manageUrl,
+ wantsToPerform: params.wantsToPerform,
+ artForm: params.artForm,
+ }),
+ });
+}
+
+export async function sendUpdateEmail(params: {
+ to: string;
+ firstName: string;
+ managementToken: string;
+ wantsToPerform: boolean;
+ artForm?: string | null;
+}) {
+ const transport = createTransport();
+ if (!transport) {
+ console.warn("SMTP not configured — skipping update email");
+ return;
+ }
+ const manageUrl = `${baseUrl}/manage/${params.managementToken}`;
+ await transport.sendMail({
+ from,
+ to: params.to,
+ subject: "Inschrijving bijgewerkt — Open Mic Night",
+ html: updateConfirmationHtml({
+ firstName: params.firstName,
+ manageUrl,
+ wantsToPerform: params.wantsToPerform,
+ artForm: params.artForm,
+ }),
+ });
+}
+
+export async function sendCancellationEmail(params: {
+ to: string;
+ firstName: string;
+}) {
+ const transport = createTransport();
+ if (!transport) {
+ console.warn("SMTP not configured — skipping cancellation email");
+ return;
+ }
+ await transport.sendMail({
+ from,
+ to: params.to,
+ subject: "Inschrijving geannuleerd — Open Mic Night",
+ html: cancellationHtml({ firstName: params.firstName }),
+ });
+}
diff --git a/packages/api/src/routers/index.ts b/packages/api/src/routers/index.ts
index 19ce5d3..08c5e70 100644
--- a/packages/api/src/routers/index.ts
+++ b/packages/api/src/routers/index.ts
@@ -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
diff --git a/packages/db/src/migrations/0001_third_stark_industries.sql b/packages/db/src/migrations/0001_third_stark_industries.sql
new file mode 100644
index 0000000..7b0fa23
--- /dev/null
+++ b/packages/db/src/migrations/0001_third_stark_industries.sql
@@ -0,0 +1,4 @@
+ALTER TABLE `registration` ADD `management_token` text;--> statement-breakpoint
+ALTER TABLE `registration` ADD `cancelled_at` integer;--> statement-breakpoint
+CREATE UNIQUE INDEX `registration_management_token_unique` ON `registration` (`management_token`);--> statement-breakpoint
+CREATE INDEX `registration_managementToken_idx` ON `registration` (`management_token`);
\ No newline at end of file
diff --git a/packages/db/src/migrations/meta/0000_snapshot.json b/packages/db/src/migrations/meta/0000_snapshot.json
index 7fa1ea0..054f172 100644
--- a/packages/db/src/migrations/meta/0000_snapshot.json
+++ b/packages/db/src/migrations/meta/0000_snapshot.json
@@ -1,558 +1,528 @@
{
- "version": "6",
- "dialect": "sqlite",
- "id": "dfeebea6-5c6c-4f28-9ecf-daf1f35f23ea",
- "prevId": "00000000-0000-0000-0000-000000000000",
- "tables": {
- "admin_request": {
- "name": "admin_request",
- "columns": {
- "id": {
- "name": "id",
- "type": "text",
- "primaryKey": true,
- "notNull": true,
- "autoincrement": false
- },
- "user_id": {
- "name": "user_id",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- },
- "status": {
- "name": "status",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false,
- "default": "'pending'"
- },
- "requested_at": {
- "name": "requested_at",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false,
- "default": "(cast(unixepoch('subsecond') * 1000 as integer))"
- },
- "reviewed_at": {
- "name": "reviewed_at",
- "type": "integer",
- "primaryKey": false,
- "notNull": false,
- "autoincrement": false
- },
- "reviewed_by": {
- "name": "reviewed_by",
- "type": "text",
- "primaryKey": false,
- "notNull": false,
- "autoincrement": false
- }
- },
- "indexes": {
- "admin_request_user_id_unique": {
- "name": "admin_request_user_id_unique",
- "columns": [
- "user_id"
- ],
- "isUnique": true
- },
- "admin_request_userId_idx": {
- "name": "admin_request_userId_idx",
- "columns": [
- "user_id"
- ],
- "isUnique": false
- },
- "admin_request_status_idx": {
- "name": "admin_request_status_idx",
- "columns": [
- "status"
- ],
- "isUnique": false
- }
- },
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {},
- "checkConstraints": {}
- },
- "account": {
- "name": "account",
- "columns": {
- "id": {
- "name": "id",
- "type": "text",
- "primaryKey": true,
- "notNull": true,
- "autoincrement": false
- },
- "account_id": {
- "name": "account_id",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- },
- "provider_id": {
- "name": "provider_id",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- },
- "user_id": {
- "name": "user_id",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- },
- "access_token": {
- "name": "access_token",
- "type": "text",
- "primaryKey": false,
- "notNull": false,
- "autoincrement": false
- },
- "refresh_token": {
- "name": "refresh_token",
- "type": "text",
- "primaryKey": false,
- "notNull": false,
- "autoincrement": false
- },
- "id_token": {
- "name": "id_token",
- "type": "text",
- "primaryKey": false,
- "notNull": false,
- "autoincrement": false
- },
- "access_token_expires_at": {
- "name": "access_token_expires_at",
- "type": "integer",
- "primaryKey": false,
- "notNull": false,
- "autoincrement": false
- },
- "refresh_token_expires_at": {
- "name": "refresh_token_expires_at",
- "type": "integer",
- "primaryKey": false,
- "notNull": false,
- "autoincrement": false
- },
- "scope": {
- "name": "scope",
- "type": "text",
- "primaryKey": false,
- "notNull": false,
- "autoincrement": false
- },
- "password": {
- "name": "password",
- "type": "text",
- "primaryKey": false,
- "notNull": false,
- "autoincrement": false
- },
- "created_at": {
- "name": "created_at",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false,
- "default": "(cast(unixepoch('subsecond') * 1000 as integer))"
- },
- "updated_at": {
- "name": "updated_at",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- }
- },
- "indexes": {
- "account_userId_idx": {
- "name": "account_userId_idx",
- "columns": [
- "user_id"
- ],
- "isUnique": false
- }
- },
- "foreignKeys": {
- "account_user_id_user_id_fk": {
- "name": "account_user_id_user_id_fk",
- "tableFrom": "account",
- "tableTo": "user",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {},
- "checkConstraints": {}
- },
- "session": {
- "name": "session",
- "columns": {
- "id": {
- "name": "id",
- "type": "text",
- "primaryKey": true,
- "notNull": true,
- "autoincrement": false
- },
- "expires_at": {
- "name": "expires_at",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- },
- "token": {
- "name": "token",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- },
- "created_at": {
- "name": "created_at",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false,
- "default": "(cast(unixepoch('subsecond') * 1000 as integer))"
- },
- "updated_at": {
- "name": "updated_at",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- },
- "ip_address": {
- "name": "ip_address",
- "type": "text",
- "primaryKey": false,
- "notNull": false,
- "autoincrement": false
- },
- "user_agent": {
- "name": "user_agent",
- "type": "text",
- "primaryKey": false,
- "notNull": false,
- "autoincrement": false
- },
- "user_id": {
- "name": "user_id",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- }
- },
- "indexes": {
- "session_token_unique": {
- "name": "session_token_unique",
- "columns": [
- "token"
- ],
- "isUnique": true
- },
- "session_userId_idx": {
- "name": "session_userId_idx",
- "columns": [
- "user_id"
- ],
- "isUnique": false
- }
- },
- "foreignKeys": {
- "session_user_id_user_id_fk": {
- "name": "session_user_id_user_id_fk",
- "tableFrom": "session",
- "tableTo": "user",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {},
- "checkConstraints": {}
- },
- "user": {
- "name": "user",
- "columns": {
- "id": {
- "name": "id",
- "type": "text",
- "primaryKey": true,
- "notNull": true,
- "autoincrement": false
- },
- "name": {
- "name": "name",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- },
- "email": {
- "name": "email",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- },
- "email_verified": {
- "name": "email_verified",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false,
- "default": false
- },
- "image": {
- "name": "image",
- "type": "text",
- "primaryKey": false,
- "notNull": false,
- "autoincrement": false
- },
- "role": {
- "name": "role",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false,
- "default": "'user'"
- },
- "created_at": {
- "name": "created_at",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false,
- "default": "(cast(unixepoch('subsecond') * 1000 as integer))"
- },
- "updated_at": {
- "name": "updated_at",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false,
- "default": "(cast(unixepoch('subsecond') * 1000 as integer))"
- }
- },
- "indexes": {
- "user_email_unique": {
- "name": "user_email_unique",
- "columns": [
- "email"
- ],
- "isUnique": true
- }
- },
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {},
- "checkConstraints": {}
- },
- "verification": {
- "name": "verification",
- "columns": {
- "id": {
- "name": "id",
- "type": "text",
- "primaryKey": true,
- "notNull": true,
- "autoincrement": false
- },
- "identifier": {
- "name": "identifier",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- },
- "value": {
- "name": "value",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- },
- "expires_at": {
- "name": "expires_at",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- },
- "created_at": {
- "name": "created_at",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false,
- "default": "(cast(unixepoch('subsecond') * 1000 as integer))"
- },
- "updated_at": {
- "name": "updated_at",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false,
- "default": "(cast(unixepoch('subsecond') * 1000 as integer))"
- }
- },
- "indexes": {
- "verification_identifier_idx": {
- "name": "verification_identifier_idx",
- "columns": [
- "identifier"
- ],
- "isUnique": false
- }
- },
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {},
- "checkConstraints": {}
- },
- "registration": {
- "name": "registration",
- "columns": {
- "id": {
- "name": "id",
- "type": "text",
- "primaryKey": true,
- "notNull": true,
- "autoincrement": false
- },
- "first_name": {
- "name": "first_name",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- },
- "last_name": {
- "name": "last_name",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- },
- "email": {
- "name": "email",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- },
- "phone": {
- "name": "phone",
- "type": "text",
- "primaryKey": false,
- "notNull": false,
- "autoincrement": false
- },
- "wants_to_perform": {
- "name": "wants_to_perform",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false,
- "default": false
- },
- "art_form": {
- "name": "art_form",
- "type": "text",
- "primaryKey": false,
- "notNull": false,
- "autoincrement": false
- },
- "experience": {
- "name": "experience",
- "type": "text",
- "primaryKey": false,
- "notNull": false,
- "autoincrement": false
- },
- "extra_questions": {
- "name": "extra_questions",
- "type": "text",
- "primaryKey": false,
- "notNull": false,
- "autoincrement": false
- },
- "created_at": {
- "name": "created_at",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false,
- "default": "(cast(unixepoch('subsecond') * 1000 as integer))"
- }
- },
- "indexes": {
- "registration_email_idx": {
- "name": "registration_email_idx",
- "columns": [
- "email"
- ],
- "isUnique": false
- },
- "registration_artForm_idx": {
- "name": "registration_artForm_idx",
- "columns": [
- "art_form"
- ],
- "isUnique": false
- },
- "registration_createdAt_idx": {
- "name": "registration_createdAt_idx",
- "columns": [
- "created_at"
- ],
- "isUnique": false
- }
- },
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {},
- "checkConstraints": {}
- }
- },
- "views": {},
- "enums": {},
- "_meta": {
- "schemas": {},
- "tables": {},
- "columns": {}
- },
- "internal": {
- "indexes": {}
- }
-}
\ No newline at end of file
+ "version": "6",
+ "dialect": "sqlite",
+ "id": "dfeebea6-5c6c-4f28-9ecf-daf1f35f23ea",
+ "prevId": "00000000-0000-0000-0000-000000000000",
+ "tables": {
+ "admin_request": {
+ "name": "admin_request",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'pending'"
+ },
+ "requested_at": {
+ "name": "requested_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(cast(unixepoch('subsecond') * 1000 as integer))"
+ },
+ "reviewed_at": {
+ "name": "reviewed_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "reviewed_by": {
+ "name": "reviewed_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "admin_request_user_id_unique": {
+ "name": "admin_request_user_id_unique",
+ "columns": ["user_id"],
+ "isUnique": true
+ },
+ "admin_request_userId_idx": {
+ "name": "admin_request_userId_idx",
+ "columns": ["user_id"],
+ "isUnique": false
+ },
+ "admin_request_status_idx": {
+ "name": "admin_request_status_idx",
+ "columns": ["status"],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "account": {
+ "name": "account",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "account_id": {
+ "name": "account_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "provider_id": {
+ "name": "provider_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "access_token": {
+ "name": "access_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "refresh_token": {
+ "name": "refresh_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "id_token": {
+ "name": "id_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "access_token_expires_at": {
+ "name": "access_token_expires_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "refresh_token_expires_at": {
+ "name": "refresh_token_expires_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "scope": {
+ "name": "scope",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "password": {
+ "name": "password",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(cast(unixepoch('subsecond') * 1000 as integer))"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "account_userId_idx": {
+ "name": "account_userId_idx",
+ "columns": ["user_id"],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "account_user_id_user_id_fk": {
+ "name": "account_user_id_user_id_fk",
+ "tableFrom": "account",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "session": {
+ "name": "session",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "token": {
+ "name": "token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(cast(unixepoch('subsecond') * 1000 as integer))"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "ip_address": {
+ "name": "ip_address",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "user_agent": {
+ "name": "user_agent",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "session_token_unique": {
+ "name": "session_token_unique",
+ "columns": ["token"],
+ "isUnique": true
+ },
+ "session_userId_idx": {
+ "name": "session_userId_idx",
+ "columns": ["user_id"],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "session_user_id_user_id_fk": {
+ "name": "session_user_id_user_id_fk",
+ "tableFrom": "session",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "user": {
+ "name": "user",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "email_verified": {
+ "name": "email_verified",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": false
+ },
+ "image": {
+ "name": "image",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "role": {
+ "name": "role",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'user'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(cast(unixepoch('subsecond') * 1000 as integer))"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(cast(unixepoch('subsecond') * 1000 as integer))"
+ }
+ },
+ "indexes": {
+ "user_email_unique": {
+ "name": "user_email_unique",
+ "columns": ["email"],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "verification": {
+ "name": "verification",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "identifier": {
+ "name": "identifier",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "value": {
+ "name": "value",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(cast(unixepoch('subsecond') * 1000 as integer))"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(cast(unixepoch('subsecond') * 1000 as integer))"
+ }
+ },
+ "indexes": {
+ "verification_identifier_idx": {
+ "name": "verification_identifier_idx",
+ "columns": ["identifier"],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "registration": {
+ "name": "registration",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "first_name": {
+ "name": "first_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "last_name": {
+ "name": "last_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "phone": {
+ "name": "phone",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "wants_to_perform": {
+ "name": "wants_to_perform",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": false
+ },
+ "art_form": {
+ "name": "art_form",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "experience": {
+ "name": "experience",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "extra_questions": {
+ "name": "extra_questions",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(cast(unixepoch('subsecond') * 1000 as integer))"
+ }
+ },
+ "indexes": {
+ "registration_email_idx": {
+ "name": "registration_email_idx",
+ "columns": ["email"],
+ "isUnique": false
+ },
+ "registration_artForm_idx": {
+ "name": "registration_artForm_idx",
+ "columns": ["art_form"],
+ "isUnique": false
+ },
+ "registration_createdAt_idx": {
+ "name": "registration_createdAt_idx",
+ "columns": ["created_at"],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ }
+ },
+ "views": {},
+ "enums": {},
+ "_meta": {
+ "schemas": {},
+ "tables": {},
+ "columns": {}
+ },
+ "internal": {
+ "indexes": {}
+ }
+}
diff --git a/packages/db/src/migrations/meta/0001_snapshot.json b/packages/db/src/migrations/meta/0001_snapshot.json
new file mode 100644
index 0000000..2778e64
--- /dev/null
+++ b/packages/db/src/migrations/meta/0001_snapshot.json
@@ -0,0 +1,552 @@
+{
+ "version": "6",
+ "dialect": "sqlite",
+ "id": "d9a4f07e-e6ae-45d0-be82-c919ae7fbe09",
+ "prevId": "dfeebea6-5c6c-4f28-9ecf-daf1f35f23ea",
+ "tables": {
+ "admin_request": {
+ "name": "admin_request",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'pending'"
+ },
+ "requested_at": {
+ "name": "requested_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(cast(unixepoch('subsecond') * 1000 as integer))"
+ },
+ "reviewed_at": {
+ "name": "reviewed_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "reviewed_by": {
+ "name": "reviewed_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "admin_request_user_id_unique": {
+ "name": "admin_request_user_id_unique",
+ "columns": ["user_id"],
+ "isUnique": true
+ },
+ "admin_request_userId_idx": {
+ "name": "admin_request_userId_idx",
+ "columns": ["user_id"],
+ "isUnique": false
+ },
+ "admin_request_status_idx": {
+ "name": "admin_request_status_idx",
+ "columns": ["status"],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "account": {
+ "name": "account",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "account_id": {
+ "name": "account_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "provider_id": {
+ "name": "provider_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "access_token": {
+ "name": "access_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "refresh_token": {
+ "name": "refresh_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "id_token": {
+ "name": "id_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "access_token_expires_at": {
+ "name": "access_token_expires_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "refresh_token_expires_at": {
+ "name": "refresh_token_expires_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "scope": {
+ "name": "scope",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "password": {
+ "name": "password",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(cast(unixepoch('subsecond') * 1000 as integer))"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "account_userId_idx": {
+ "name": "account_userId_idx",
+ "columns": ["user_id"],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "account_user_id_user_id_fk": {
+ "name": "account_user_id_user_id_fk",
+ "tableFrom": "account",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "session": {
+ "name": "session",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "token": {
+ "name": "token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(cast(unixepoch('subsecond') * 1000 as integer))"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "ip_address": {
+ "name": "ip_address",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "user_agent": {
+ "name": "user_agent",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "session_token_unique": {
+ "name": "session_token_unique",
+ "columns": ["token"],
+ "isUnique": true
+ },
+ "session_userId_idx": {
+ "name": "session_userId_idx",
+ "columns": ["user_id"],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "session_user_id_user_id_fk": {
+ "name": "session_user_id_user_id_fk",
+ "tableFrom": "session",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "user": {
+ "name": "user",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "email_verified": {
+ "name": "email_verified",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": false
+ },
+ "image": {
+ "name": "image",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "role": {
+ "name": "role",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'user'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(cast(unixepoch('subsecond') * 1000 as integer))"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(cast(unixepoch('subsecond') * 1000 as integer))"
+ }
+ },
+ "indexes": {
+ "user_email_unique": {
+ "name": "user_email_unique",
+ "columns": ["email"],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "verification": {
+ "name": "verification",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "identifier": {
+ "name": "identifier",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "value": {
+ "name": "value",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(cast(unixepoch('subsecond') * 1000 as integer))"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(cast(unixepoch('subsecond') * 1000 as integer))"
+ }
+ },
+ "indexes": {
+ "verification_identifier_idx": {
+ "name": "verification_identifier_idx",
+ "columns": ["identifier"],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "registration": {
+ "name": "registration",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "first_name": {
+ "name": "first_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "last_name": {
+ "name": "last_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "phone": {
+ "name": "phone",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "wants_to_perform": {
+ "name": "wants_to_perform",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": false
+ },
+ "art_form": {
+ "name": "art_form",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "experience": {
+ "name": "experience",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "extra_questions": {
+ "name": "extra_questions",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "management_token": {
+ "name": "management_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "cancelled_at": {
+ "name": "cancelled_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(cast(unixepoch('subsecond') * 1000 as integer))"
+ }
+ },
+ "indexes": {
+ "registration_management_token_unique": {
+ "name": "registration_management_token_unique",
+ "columns": ["management_token"],
+ "isUnique": true
+ },
+ "registration_email_idx": {
+ "name": "registration_email_idx",
+ "columns": ["email"],
+ "isUnique": false
+ },
+ "registration_artForm_idx": {
+ "name": "registration_artForm_idx",
+ "columns": ["art_form"],
+ "isUnique": false
+ },
+ "registration_createdAt_idx": {
+ "name": "registration_createdAt_idx",
+ "columns": ["created_at"],
+ "isUnique": false
+ },
+ "registration_managementToken_idx": {
+ "name": "registration_managementToken_idx",
+ "columns": ["management_token"],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ }
+ },
+ "views": {},
+ "enums": {},
+ "_meta": {
+ "schemas": {},
+ "tables": {},
+ "columns": {}
+ },
+ "internal": {
+ "indexes": {}
+ }
+}
diff --git a/packages/db/src/migrations/meta/_journal.json b/packages/db/src/migrations/meta/_journal.json
index 0db23c7..042e610 100644
--- a/packages/db/src/migrations/meta/_journal.json
+++ b/packages/db/src/migrations/meta/_journal.json
@@ -1,13 +1,20 @@
{
- "version": "7",
- "dialect": "sqlite",
- "entries": [
- {
- "idx": 0,
- "version": "6",
- "when": 1772480169471,
- "tag": "0000_mean_sunspot",
- "breakpoints": true
- }
- ]
-}
\ No newline at end of file
+ "version": "7",
+ "dialect": "sqlite",
+ "entries": [
+ {
+ "idx": 0,
+ "version": "6",
+ "when": 1772480169471,
+ "tag": "0000_mean_sunspot",
+ "breakpoints": true
+ },
+ {
+ "idx": 1,
+ "version": "6",
+ "when": 1772480778632,
+ "tag": "0001_third_stark_industries",
+ "breakpoints": true
+ }
+ ]
+}
diff --git a/packages/db/src/schema/registrations.ts b/packages/db/src/schema/registrations.ts
index 7f34058..e007fdf 100644
--- a/packages/db/src/schema/registrations.ts
+++ b/packages/db/src/schema/registrations.ts
@@ -15,6 +15,8 @@ export const registration = sqliteTable(
artForm: text("art_form"),
experience: text("experience"),
extraQuestions: text("extra_questions"),
+ managementToken: text("management_token").unique(),
+ cancelledAt: integer("cancelled_at", { mode: "timestamp_ms" }),
createdAt: integer("created_at", { mode: "timestamp_ms" })
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
.notNull(),
@@ -23,5 +25,6 @@ export const registration = sqliteTable(
index("registration_email_idx").on(table.email),
index("registration_artForm_idx").on(table.artForm),
index("registration_createdAt_idx").on(table.createdAt),
+ index("registration_managementToken_idx").on(table.managementToken),
],
);
diff --git a/packages/env/src/server.ts b/packages/env/src/server.ts
index cee1323..9757053 100644
--- a/packages/env/src/server.ts
+++ b/packages/env/src/server.ts
@@ -1,13 +1,26 @@
-import "dotenv/config";
+import { dirname, resolve } from "node:path";
+import { fileURLToPath } from "node:url";
import { createEnv } from "@t3-oss/env-core";
+import { config } from "dotenv";
import { z } from "zod";
+// Only load .env file in development (not in Cloudflare Workers)
+if (process.env.NODE_ENV !== "production") {
+ const __dirname = dirname(fileURLToPath(import.meta.url));
+ config({ path: resolve(__dirname, "../.env") });
+}
+
export const env = createEnv({
server: {
DATABASE_URL: z.string().min(1),
BETTER_AUTH_SECRET: z.string().min(32),
BETTER_AUTH_URL: z.url(),
CORS_ORIGIN: z.url(),
+ SMTP_HOST: z.string().min(1).optional(),
+ SMTP_PORT: z.coerce.number().default(587),
+ SMTP_USER: z.string().min(1).optional(),
+ SMTP_PASS: z.string().min(1).optional(),
+ SMTP_FROM: z.string().min(1).optional(),
NODE_ENV: z
.enum(["development", "production", "test"])
.default("development"),
diff --git a/packages/infra/alchemy.run.ts b/packages/infra/alchemy.run.ts
index e8d60e1..495ac25 100644
--- a/packages/infra/alchemy.run.ts
+++ b/packages/infra/alchemy.run.ts
@@ -3,7 +3,7 @@ import { TanStackStart } from "alchemy/cloudflare";
import { config } from "dotenv";
config({ path: "./.env" });
-config({ path: "../../apps/web/.env" });
+config({ path: "../env/.env" });
const app = await alchemy("kk");
@@ -23,6 +23,11 @@ export const web = await TanStackStart("web", {
CORS_ORIGIN: getEnvVar("CORS_ORIGIN"),
BETTER_AUTH_SECRET: getEnvVar("BETTER_AUTH_SECRET"),
BETTER_AUTH_URL: getEnvVar("BETTER_AUTH_URL"),
+ SMTP_HOST: getEnvVar("SMTP_HOST"),
+ SMTP_PORT: getEnvVar("SMTP_PORT"),
+ SMTP_USER: getEnvVar("SMTP_USER"),
+ SMTP_PASS: getEnvVar("SMTP_PASS"),
+ SMTP_FROM: getEnvVar("SMTP_FROM"),
},
domains: ["kunstenkamp.be"],
});