import { CollectionConfig } from "payload"; import type { User } from "@/payload-types"; import hasRoles from "@/utils/access-control/hasRoles"; import { isSuperuser } from "@/utils/access-control/hasPermission"; export const VoiceSubmissions: CollectionConfig = { slug: "voice-submissions", admin: { group: "Voiceover", useAsTitle: "id", defaultColumns: ["script", "user", "status", "createdAt"], }, access: { read: async ({ req }) => { const user = req.user as User | null; if (!user) return false; // Elevation bypasses the owner scope so reviewers see every submission; // regular users only ever see their own. if (await isSuperuser(req.payload, user)) return true; if (hasRoles(["admin", "developer"], user)) return true; return { user: { equals: user.id } }; }, create: ({ req, data }) => { const user = req.user as User | null; if (!user) return false; if (hasRoles(["admin", "developer"], user)) return true; // Submitters may only create their own submissions. Server actions that // write on behalf of users run with overrideAccess and set `user` themselves. const submittedUser = typeof data?.user === "object" && data?.user !== null ? data.user.id : data?.user; return submittedUser === user.id; }, // Status changes are review actions — submitters must not edit their own docs. update: ({ req }) => hasRoles(["admin", "developer"], req.user), delete: ({ req }) => hasRoles(["developer"], req.user), }, fields: [ { name: "script", type: "relationship", relationTo: "voice-scripts", required: true, index: true, }, { name: "user", type: "relationship", relationTo: "users", required: true, index: true, admin: { description: "Submitter", }, }, { name: "description", type: "textarea", maxLength: 300, admin: { description: "Optional short note from the submitter about this take.", }, }, { name: "status", type: "select", required: true, defaultValue: "pending", options: [ { label: "Pending", value: "pending" }, { label: "Accepted", value: "accepted" }, { label: "Rejected", value: "rejected" }, ], }, { name: "reviewNote", type: "textarea", admin: { description: "Optional reviewer feedback. Shown to the submitter alongside their take.", }, }, ], // No `upload.mimeTypes` allowlist on purpose: Payload content-sniffs uploads // (file-type) and reports WebM recordings as `video/webm`, so an audio/* MIME // allowlist rejects browser-recorded takes. The submit action is the gate // instead (audio/* base type + size cap); restricted file types (executables, // scripts, …) stay blocked by Payload's built-in checks. upload: true, timestamps: true, };