Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion src/app/(app)/m/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand All @@ -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),
});
Expand Down
9 changes: 6 additions & 3 deletions src/app/(app)/m/new/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,14 @@ export default async function NewMachinePage(): Promise<React.JSX.Element> {

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 (
<main className="min-h-screen bg-surface">
{/* Header */}
Expand Down
50 changes: 32 additions & 18 deletions src/app/(app)/m/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -35,6 +35,15 @@ export default async function MachinesPage(): Promise<React.JSX.Element> {
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),
Expand Down Expand Up @@ -84,15 +93,17 @@ export default async function MachinesPage(): Promise<React.JSX.Element> {
Manage pinball machines and view their status
</p>
</div>
<Button
asChild
className="bg-primary text-on-primary hover:bg-primary/90"
>
<Link href="/m/new">
<Plus className="mr-2 size-4" />
Add Machine
</Link>
</Button>
{isAdmin && (
<Button
asChild
className="bg-primary text-on-primary hover:bg-primary/90"
>
<Link href="/m/new">
<Plus className="mr-2 size-4" />
Add Machine
</Link>
</Button>
)}
</div>
</div>
</div>
Expand All @@ -104,14 +115,17 @@ export default async function MachinesPage(): Promise<React.JSX.Element> {
<Card className="border-outline-variant">
<CardContent className="py-12 text-center">
<p className="text-lg text-on-surface-variant mb-4">
No machines yet
No machines yet.
{isAdmin ? " Add the first one!" : ""}
</p>
<Link href="/m/new">
<Button className="bg-primary text-on-primary hover:bg-primary/90">
<Plus className="mr-2 size-4" />
Add Your First Machine
</Button>
</Link>
{isAdmin && (
<Link href="/m/new">
<Button className="bg-primary text-on-primary hover:bg-primary/90">
<Plus className="mr-2 size-4" />
Add Your First Machine
</Button>
</Link>
)}
</CardContent>
</Card>
) : (
Expand Down
2 changes: 1 addition & 1 deletion src/test/unit/machine-actions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading