"use server"; import { revalidatePath } from "next/cache"; import { auth } from "@/lib/auth"; import { prisma } from "@/lib/prisma"; // Column bounds from prisma/schema.prisma (radio_applications): // real_name VARCHAR(255); the rest are TEXT. age is an INT. const NAME_MAX = 255; const TEXT_MAX = 5000; const STYLE_MAX = 5000; function str(form: FormData, key: string, max: number): string { return String(form.get(key) ?? "") .normalize("NFC") .trim() .slice(0, max); } /** * Submit a radio DJ application. * * The applicant (userId) is ALWAYS re-read from the session via auth() and is * never taken from the submitted FormData, so a crafted form cannot file an * application on behalf of another account. radio_applications.user_id is an * UnsignedBigInt, hence the BigInt() coercion. */ export async function applyDj(formData: FormData): Promise { const session = await auth(); const userId = Number(session?.user?.id); if (!Number.isInteger(userId) || userId <= 0) return; const realName = str(formData, "realName", NAME_MAX); const availability = str(formData, "availability", TEXT_MAX); const motivation = str(formData, "motivation", TEXT_MAX); const experience = str(formData, "experience", TEXT_MAX); const musicStyle = str(formData, "musicStyle", STYLE_MAX); const ageRaw = Number(formData.get("age")); const age = Number.isInteger(ageRaw) ? ageRaw : 0; // Required fields per the schema (NOT NULL): real_name, age, availability, // motivation. experience + music_style are nullable. if (!realName || !availability || !motivation || age <= 0) return; const now = new Date(); try { await prisma.radioApplications.create({ data: { userId: BigInt(userId), realName, age, availability, motivation, experience: experience || null, musicStyle: musicStyle || null, status: "pending", createdAt: now, updatedAt: now, }, }); } catch { // DB unavailable or duplicate — fail soft; nothing to persist. return; } revalidatePath("/radio/apply"); }