From df16f01fb2bb8ce69d309d825361806ddfa3f98c Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 29 Jan 2026 03:13:53 +0000 Subject: [PATCH] feat(access-control): Restrict machine creation to admins Restricts the ability to create new pinball machines to users with the 'admin' role. This is implemented by: - Adding a server-side check in the `createMachineAction` to ensure the user is an admin. - Conditionally rendering the "Add Machine" button on the machines list page based on the user's role. - Adding a server-side redirect on the new machine page to prevent non-admins from accessing it directly. Co-authored-by: timothyfroehlich <5819722+timothyfroehlich@users.noreply.github.com> --- src/app/(app)/m/actions.ts | 13 ++++++- src/app/(app)/m/new/page.tsx | 9 +++-- src/app/(app)/m/page.tsx | 50 +++++++++++++++++---------- src/test/unit/machine-actions.test.ts | 2 +- 4 files changed, 51 insertions(+), 23 deletions(-) diff --git a/src/app/(app)/m/actions.ts b/src/app/(app)/m/actions.ts index 188600ce..c29df5a7 100644 --- a/src/app/(app)/m/actions.ts +++ b/src/app/(app)/m/actions.ts @@ -93,6 +93,15 @@ export async function createMachineAction( return err("UNAUTHORIZED", "User profile not found."); } + // Access control: only admins can create machines + if (profile.role !== "admin") { + log.warn( + { userId: user.id, action: "createMachineAction" }, + "Non-admin user attempted to create a machine" + ); + return err("UNAUTHORIZED", "You must be an admin to create a machine."); + } + // Extract form data const rawData = { name: formData.get("name"), @@ -117,7 +126,9 @@ export async function createMachineAction( let finalOwnerId: string | undefined = undefined; let finalInvitedOwnerId: string | undefined = undefined; - if (profile.role === "admin" && ownerId) { + // At this point, user is guaranteed to be an admin. + // If an owner is specified, we resolve them. Otherwise, the admin is the owner. + if (ownerId) { const isActive = await db.query.userProfiles.findFirst({ where: eq(userProfiles.id, ownerId), }); diff --git a/src/app/(app)/m/new/page.tsx b/src/app/(app)/m/new/page.tsx index f55c78a8..543f7537 100644 --- a/src/app/(app)/m/new/page.tsx +++ b/src/app/(app)/m/new/page.tsx @@ -38,11 +38,14 @@ export default async function NewMachinePage(): Promise { const isAdmin = currentUserProfile?.role === "admin"; - let allUsers: UnifiedUser[] = []; - if (isAdmin) { - allUsers = await getUnifiedUsers({ includeEmails: false }); + if (!isAdmin) { + redirect("/m"); } + const allUsers: UnifiedUser[] = await getUnifiedUsers({ + includeEmails: false, + }); + return (
{/* Header */} diff --git a/src/app/(app)/m/page.tsx b/src/app/(app)/m/page.tsx index be39a10c..5f5fe9e5 100644 --- a/src/app/(app)/m/page.tsx +++ b/src/app/(app)/m/page.tsx @@ -3,8 +3,8 @@ import Link from "next/link"; import { redirect } from "next/navigation"; import { createClient } from "~/lib/supabase/server"; import { db } from "~/server/db"; -import { machines } from "~/server/db/schema"; -import { desc } from "drizzle-orm"; +import { machines, userProfiles } from "~/server/db/schema"; +import { desc, eq } from "drizzle-orm"; import { deriveMachineStatus, getMachineStatusLabel, @@ -35,6 +35,15 @@ export default async function MachinesPage(): Promise { redirect("/login?next=%2Fm"); } + const userProfile = await db.query.userProfiles.findFirst({ + where: eq(userProfiles.id, user.id), + columns: { + role: true, + }, + }); + + const isAdmin = userProfile?.role === "admin"; + // Query machines with their open issues (direct Drizzle query - no DAL) const allMachines = await db.query.machines.findMany({ orderBy: desc(machines.name), @@ -84,15 +93,17 @@ export default async function MachinesPage(): Promise { Manage pinball machines and view their status

- + {isAdmin && ( + + )} @@ -104,14 +115,17 @@ export default async function MachinesPage(): Promise {

- No machines yet + No machines yet. + {isAdmin ? " Add the first one!" : ""}

- - - + {isAdmin && ( + + + + )}
) : ( diff --git a/src/test/unit/machine-actions.test.ts b/src/test/unit/machine-actions.test.ts index d259bdf3..97e498ef 100644 --- a/src/test/unit/machine-actions.test.ts +++ b/src/test/unit/machine-actions.test.ts @@ -95,7 +95,7 @@ describe("createMachineAction", () => { it("should successfully create a machine", async () => { // Mock profile found vi.mocked(db.query.userProfiles.findFirst).mockResolvedValue({ - role: "member", + role: "admin", } as any); // Mock successful insert