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