feat(platform): harden access scoping and delivery baseline
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { z } from "zod";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { PermissionKey, parseTaskAction, resolvePermissions } from "@capakraken/shared";
|
||||
import { findUniqueOrThrow } from "../db/helpers.js";
|
||||
import { createTRPCRouter, managerProcedure, protectedProcedure } from "../trpc.js";
|
||||
import {
|
||||
@@ -12,6 +13,7 @@ import {
|
||||
import { createNotification } from "../lib/create-notification.js";
|
||||
import { resolveRecipients } from "../lib/notification-targeting.js";
|
||||
import { sendEmail } from "../lib/email.js";
|
||||
import { getTaskAction } from "../lib/task-actions.js";
|
||||
|
||||
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -260,6 +262,49 @@ export const notificationRouter = createTRPCRouter({
|
||||
};
|
||||
}),
|
||||
|
||||
/** Get one task/approval visible to the current user */
|
||||
getTaskDetail: protectedProcedure
|
||||
.input(z.object({ id: z.string() }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
const userId = await resolveUserId(ctx);
|
||||
|
||||
const task = await ctx.db.notification.findFirst({
|
||||
where: {
|
||||
id: input.id,
|
||||
OR: [{ userId }, { assigneeId: userId }],
|
||||
category: { in: ["TASK", "APPROVAL"] },
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
body: true,
|
||||
type: true,
|
||||
priority: true,
|
||||
category: true,
|
||||
taskStatus: true,
|
||||
taskAction: true,
|
||||
dueDate: true,
|
||||
entityId: true,
|
||||
entityType: true,
|
||||
completedAt: true,
|
||||
completedBy: true,
|
||||
createdAt: true,
|
||||
userId: true,
|
||||
assigneeId: true,
|
||||
sender: { select: { id: true, name: true, email: true } },
|
||||
},
|
||||
});
|
||||
|
||||
if (!task) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Task not found or you do not have permission",
|
||||
});
|
||||
}
|
||||
|
||||
return task;
|
||||
}),
|
||||
|
||||
/** Update task status */
|
||||
updateTaskStatus: protectedProcedure
|
||||
.input(
|
||||
@@ -312,6 +357,101 @@ export const notificationRouter = createTRPCRouter({
|
||||
return updated;
|
||||
}),
|
||||
|
||||
/** Execute the machine-readable action associated with a task */
|
||||
executeTaskAction: protectedProcedure
|
||||
.input(z.object({ id: z.string() }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const userId = await resolveUserId(ctx);
|
||||
const task = await ctx.db.notification.findFirst({
|
||||
where: {
|
||||
id: input.id,
|
||||
OR: [{ userId }, { assigneeId: userId }],
|
||||
category: { in: ["TASK", "APPROVAL"] },
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
userId: true,
|
||||
assigneeId: true,
|
||||
taskAction: true,
|
||||
taskStatus: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!task) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Task not found or you do not have permission",
|
||||
});
|
||||
}
|
||||
if (!task.taskAction) {
|
||||
throw new TRPCError({
|
||||
code: "PRECONDITION_FAILED",
|
||||
message: "This task has no executable action",
|
||||
});
|
||||
}
|
||||
if (task.taskStatus === "DONE") {
|
||||
throw new TRPCError({
|
||||
code: "PRECONDITION_FAILED",
|
||||
message: "This task is already completed",
|
||||
});
|
||||
}
|
||||
|
||||
const parsed = parseTaskAction(task.taskAction);
|
||||
if (!parsed) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: `Invalid taskAction format: ${task.taskAction}`,
|
||||
});
|
||||
}
|
||||
|
||||
const handler = getTaskAction(parsed.action);
|
||||
if (!handler) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: `Unknown action: ${parsed.action}`,
|
||||
});
|
||||
}
|
||||
|
||||
const permissions = resolvePermissions(
|
||||
ctx.dbUser.systemRole as import("@capakraken/shared").SystemRole,
|
||||
ctx.dbUser.permissionOverrides as import("@capakraken/shared").PermissionOverrides | null,
|
||||
ctx.roleDefaults ?? undefined,
|
||||
);
|
||||
if (handler.permission && !permissions.has(handler.permission as PermissionKey)) {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: `Permission denied: you need "${handler.permission}" to perform this action`,
|
||||
});
|
||||
}
|
||||
|
||||
const actionResult = await handler.execute(parsed.entityId, ctx.db, userId);
|
||||
if (!actionResult.success) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: actionResult.message,
|
||||
});
|
||||
}
|
||||
|
||||
const completedTask = await ctx.db.notification.update({
|
||||
where: { id: input.id },
|
||||
data: {
|
||||
taskStatus: "DONE",
|
||||
completedAt: new Date(),
|
||||
completedBy: userId,
|
||||
},
|
||||
});
|
||||
|
||||
emitTaskCompleted(task.userId, task.id);
|
||||
if (task.assigneeId && task.assigneeId !== task.userId) {
|
||||
emitTaskCompleted(task.assigneeId, task.id);
|
||||
}
|
||||
|
||||
return {
|
||||
task: completedTask,
|
||||
actionResult,
|
||||
};
|
||||
}),
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// REMINDERS
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
@@ -542,6 +682,21 @@ export const notificationRouter = createTRPCRouter({
|
||||
});
|
||||
}),
|
||||
|
||||
/** Get one broadcast with sender context */
|
||||
getBroadcastById: managerProcedure
|
||||
.input(z.object({ id: z.string() }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
return findUniqueOrThrow(
|
||||
ctx.db.notificationBroadcast.findUnique({
|
||||
where: { id: input.id },
|
||||
include: {
|
||||
sender: { select: { id: true, name: true, email: true } },
|
||||
},
|
||||
}),
|
||||
"Broadcast",
|
||||
);
|
||||
}),
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// TASK CREATION (Manager+)
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
Reference in New Issue
Block a user