Add permission-scoped MCP, readiness checks, and management UI improvements

This commit is contained in:
2026-09-11 18:12:48 -04:00
parent 32e56b1c34
commit 815640d919
40 changed files with 845 additions and 57 deletions
@@ -0,0 +1,48 @@
import { expect, test } from "bun:test";
import { and, eq, inArray } from "drizzle-orm";
import { getDb, user, groups, guests, submissions, photos, events, eventMemberships } from "@album/database";
import { groupRouter } from "./api/routers/group";
import { managerRouter } from "./api/routers/manager";
import type { TrpcContext } from "./api/trpc";
test.skipIf(process.env.POLISH_INTEGRATION !== "1")("group ownership and event-scoped submission approval", async () => {
if (!["localhost", "127.0.0.1"].includes(new URL(process.env.DATABASE_URL!).hostname)) throw new Error("Local database required");
const db = getDb();
const id = crypto.randomUUID();
const people = await db.insert(user).values([0, 1].map(n => ({ id: `${id}-${n}`, name: "Workflow test", email: `${id}-${n}@manyangles.test`, emailVerified: true }))).returning();
const ctx = (n: number): TrpcContext => ({ session: { user: people[n]!, session: {} } as TrpcContext["session"], cookies: new Map(), activeGroupId: null, requestOrigin: "http://localhost:3000", clientIdentifier: id, guestTokenForEvent: () => null, setCookies: [], appendSetCookie: () => {} });
let groupId: string | undefined;
try {
const owner = groupRouter.createCaller(ctx(0));
const group = await owner.create({ name: "Workflow test" });
groupId = group.id;
expect((await owner.get({ groupId })).role).toBe("owner");
await owner.rename({ groupId, name: "Renamed group" });
expect((await owner.get({ groupId })).name).toBe("Renamed group");
await expect(groupRouter.createCaller(ctx(1)).rename({ groupId, name: "Denied" })).rejects.toThrow();
const [event] = await db.insert(events).values({ groupId, title: "Workflow test", slug: id }).returning();
await db.insert(eventMemberships).values({ eventId: event!.id, userId: people[0]!.id, role: "owner" });
const [guest] = await db.insert(guests).values({ eventId: event!.id, tokenHash: id }).returning();
const [submission] = await db.insert(submissions).values({ eventId: event!.id, guestId: guest!.id }).returning();
await db.insert(photos).values((["ready", "processing"] as const).map(processingStatus => ({ eventId: event!.id, submissionId: submission!.id, originalKey: `test/${id}/${processingStatus}`, contentType: "image/jpeg", processingStatus })));
const manager = managerRouter.createCaller(ctx(0));
const input = { eventId: event!.id, submissionId: submission!.id, visibility: "public" as const };
await expect(managerRouter.createCaller(ctx(1)).moderateSubmission(input)).rejects.toThrow();
await expect(manager.moderateSubmission({ ...input, eventId: crypto.randomUUID() })).rejects.toThrow();
expect((await manager.moderateSubmission(input)).updatedCount).toBe(2);
expect((await manager.moderateSubmission(input)).updatedCount).toBe(0);
const result = await db.select().from(photos).where(and(eq(photos.eventId, event!.id), eq(photos.submissionId, submission!.id)));
expect(result.every(photo => photo.visibility === "public")).toBe(true);
expect(result.find(photo => photo.processingStatus === "processing")).toBeDefined();
const [privatePhoto] = await db.insert(photos).values({ eventId: event!.id, submissionId: submission!.id, originalKey: `test/${id}/private`, contentType: "image/jpeg", processingStatus: "ready", visibility: "private" }).returning();
await db.insert(eventMemberships).values({ eventId: event!.id, userId: people[1]!.id, role: "moderator" });
const moderator = managerRouter.createCaller(ctx(1));
expect((await moderator.moderateSubmission({ ...input, visibility: "hidden" })).updatedCount).toBe(2);
const [preserved] = await db.select().from(photos).where(and(eq(photos.eventId, event!.id), eq(photos.id, privatePhoto!.id)));
expect(preserved!.visibility).toBe("private");
await expect(moderator.moderateSubmission({ ...input, visibility: "private" })).rejects.toThrow();
} finally {
if (groupId) await db.delete(groups).where(eq(groups.id, groupId));
await db.delete(user).where(inArray(user.id, people.map(person => person.id)));
}
});