Add permission-scoped MCP, readiness checks, and management UI improvements
This commit is contained in:
@@ -81,6 +81,18 @@ async function signedPhotoUrls(photo: {
|
||||
}
|
||||
|
||||
export const managerRouter = createTRPCRouter({
|
||||
stats: protectedProcedure.input(z.object({ eventId: z.string().uuid() })).query(async ({ ctx, input }) => {
|
||||
const { access } = await loadEventAccess(ctx.session.user.id, input.eventId, await getPlatformRole(ctx.session.user.id));
|
||||
requireEventPermission(access.permissions, EVENT_PERMISSIONS.OVERVIEW_READ);
|
||||
const rows = await getDb().execute(sql`select
|
||||
(select count(*)::int from photos where event_id = ${input.eventId}) as photos,
|
||||
(select count(*)::int from photos where event_id = ${input.eventId} and visibility = 'public' and processing_status = 'ready') as approved,
|
||||
(select count(*)::int from photos where event_id = ${input.eventId} and visibility = 'pending') as awaiting_review,
|
||||
(select count(*)::int from photos where event_id = ${input.eventId} and processing_status = 'failed') as failed,
|
||||
(select count(*)::int from guests where event_id = ${input.eventId}) as guests,
|
||||
(select count(*)::int from guests where event_id = ${input.eventId} and note is not null and note <> '') as notes`);
|
||||
return rows[0];
|
||||
}),
|
||||
requestExport: protectedProcedure.input(exportPhotosInputSchema).mutation(async ({ctx,input}) => {
|
||||
const {access} = await loadEventAccess(ctx.session.user.id,input.eventId,await getPlatformRole(ctx.session.user.id));
|
||||
requireEventPermission(access.permissions,EVENT_PERMISSIONS.SETTINGS_MANAGE);
|
||||
@@ -511,7 +523,7 @@ export const managerRouter = createTRPCRouter({
|
||||
const [submission] = await getDb()
|
||||
.select()
|
||||
.from(submissions)
|
||||
.where(eq(submissions.id, input.submissionId))
|
||||
.where(and(eq(submissions.id, input.submissionId), eq(submissions.eventId, input.eventId)))
|
||||
.limit(1);
|
||||
if (!submission) throw new TRPCError({ code: "NOT_FOUND" });
|
||||
const platformRole = await getPlatformRole(ctx.session.user.id);
|
||||
@@ -521,28 +533,34 @@ export const managerRouter = createTRPCRouter({
|
||||
platformRole,
|
||||
);
|
||||
requireEventPermission(access.permissions, EVENT_PERMISSIONS.PHOTOS_MODERATE);
|
||||
const rows = await getDb()
|
||||
.select()
|
||||
.from(photos)
|
||||
.where(eq(photos.submissionId, submission.id));
|
||||
for (const photo of rows) {
|
||||
if (photo.processingStatus !== "ready") continue;
|
||||
if (!canTransitionVisibility(photo.visibility, input.visibility)) continue;
|
||||
await getDb()
|
||||
.update(photos)
|
||||
.set({ visibility: input.visibility, updatedAt: new Date() })
|
||||
.where(eq(photos.id, photo.id));
|
||||
if (input.visibility === "private" && !access.permissions.includes(EVENT_PERMISSIONS.PHOTOS_PRIVATE_READ)) {
|
||||
throw new TRPCError({ code: "FORBIDDEN" });
|
||||
}
|
||||
await writeAudit({
|
||||
groupId: event.groupId,
|
||||
eventId: event.id,
|
||||
actorUserId: ctx.session.user.id,
|
||||
action: "submission.visibility",
|
||||
subjectType: "submission",
|
||||
subjectId: submission.id,
|
||||
metadata: { visibility: input.visibility, count: rows.length },
|
||||
return getDb().transaction(async (tx) => {
|
||||
const rows = await tx
|
||||
.select()
|
||||
.from(photos)
|
||||
.where(and(eq(photos.submissionId, submission.id), eq(photos.eventId, event.id)))
|
||||
.for("update");
|
||||
const eligible = rows.filter(photo =>
|
||||
(photo.visibility !== "private" || access.permissions.includes(EVENT_PERMISSIONS.PHOTOS_PRIVATE_READ)) &&
|
||||
canTransitionVisibility(photo.visibility, input.visibility));
|
||||
if (eligible.length) {
|
||||
await tx.update(photos)
|
||||
.set({ visibility: input.visibility, updatedAt: new Date() })
|
||||
.where(and(eq(photos.eventId, event.id), eq(photos.submissionId, submission.id), inArray(photos.id, eligible.map(photo => photo.id))));
|
||||
}
|
||||
await tx.insert(auditEvents).values({
|
||||
groupId: event.groupId,
|
||||
eventId: event.id,
|
||||
actorUserId: ctx.session.user.id,
|
||||
action: "submission.visibility",
|
||||
subjectType: "submission",
|
||||
subjectId: submission.id,
|
||||
metadata: { visibility: input.visibility, count: eligible.length },
|
||||
});
|
||||
return { ok: true as const, updatedCount: eligible.length };
|
||||
});
|
||||
return { ok: true as const };
|
||||
}),
|
||||
|
||||
deletePhoto: protectedProcedure
|
||||
|
||||
Reference in New Issue
Block a user