fix: isolate ERP queues and force deletion by account
This commit is contained in:
1 parent
69ea6d2517
commit
d09b3032c0
9 files changed
+677
-97
No files matched your search
+37
-14
@@ -21,10 +21,10 @@ import {
|
||||
import {
|
||||
TaskError,
|
||||
TaskService,
|
||||
canAccessTask,
|
||||
canViewOperationsDashboard,
|
||||
type ParseDecisionInput,
|
||||
type ParseTaskClaim,
|
||||
type TaskBrowserCommand,
|
||||
type TaskContext,
|
||||
type TaskEvent
|
||||
} from './task-service.js';
|
||||
@@ -168,8 +168,19 @@ const listTasksQuerySchema = z.object({
|
||||
z.boolean()
|
||||
).default(true)
|
||||
});
|
||||
const taskEventsQuerySchema = z.object({
|
||||
since: z.coerce.number().int().min(0).default(0),
|
||||
executable_by: z.literal('me').default('me')
|
||||
});
|
||||
const taskIdListSchema = z.array(z.string().trim().min(1).max(200)).min(1).max(100);
|
||||
const taskBulkDeleteSchema = z.object({
|
||||
task_ids: z.array(z.string().trim().min(1).max(200)).min(1).max(100),
|
||||
task_ids: taskIdListSchema
|
||||
}).refine(
|
||||
(body) => new Set(body.task_ids).size === body.task_ids.length,
|
||||
{ message: '任务编号不能重复。', path: ['task_ids'] }
|
||||
);
|
||||
const taskBulkArchiveSchema = z.object({
|
||||
task_ids: taskIdListSchema,
|
||||
reason: z.string().trim().max(500).optional()
|
||||
}).refine(
|
||||
(body) => new Set(body.task_ids).size === body.task_ids.length,
|
||||
@@ -1302,14 +1313,14 @@ export async function buildServer({
|
||||
const body = taskBulkDeleteSchema.parse(request.body);
|
||||
return {
|
||||
ok: true,
|
||||
archived: true,
|
||||
...(await tasks.archiveTasks(contextFor(session, request), body.task_ids, body.reason))
|
||||
deleted: true,
|
||||
...(await tasks.hardDeleteTasks(contextFor(session, request), body.task_ids))
|
||||
};
|
||||
});
|
||||
|
||||
app.post('/api/tasks/bulk-archive', async (request) => {
|
||||
const session = await requireMutationSession(request);
|
||||
const body = taskBulkDeleteSchema.parse(request.body);
|
||||
const body = taskBulkArchiveSchema.parse(request.body);
|
||||
return {
|
||||
ok: true,
|
||||
archived: true,
|
||||
@@ -1320,11 +1331,9 @@ export async function buildServer({
|
||||
app.delete('/api/tasks/:taskId', async (request) => {
|
||||
const session = await requireMutationSession(request);
|
||||
const params = request.params as { taskId: string };
|
||||
const body = taskArchiveSchema.parse(request.body || {});
|
||||
return {
|
||||
ok: true,
|
||||
archived: true,
|
||||
task: await tasks.archiveTask(contextFor(session, request), params.taskId, body.reason)
|
||||
...(await tasks.hardDeleteTask(contextFor(session, request), params.taskId))
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1422,8 +1431,8 @@ export async function buildServer({
|
||||
|
||||
app.get('/api/events', async (request, reply) => {
|
||||
const session = await getSession(request);
|
||||
const query = (request.query || {}) as Record<string, unknown>;
|
||||
const querySince = Number(query.since || 0);
|
||||
const query = taskEventsQuerySchema.parse(request.query || {});
|
||||
const querySince = query.since;
|
||||
const reconnectSince = Number(request.headers['last-event-id'] || 0);
|
||||
const since = Math.max(
|
||||
Number.isFinite(querySince) ? querySince : 0,
|
||||
@@ -1439,9 +1448,10 @@ export async function buildServer({
|
||||
});
|
||||
const send = (event: TaskEvent) => {
|
||||
if (event.organization_id !== session.user.organizationId) return;
|
||||
if (!canAccessTask(contextFor(session, request), {
|
||||
assignedUserId: event.owner_user_id
|
||||
})) return;
|
||||
// This is the executable wake-up feed, not the administrator's read
|
||||
// model. Every role, including admin, receives only its own assigned
|
||||
// task events so visibility can never turn into plugin dispatch.
|
||||
if (event.owner_user_id !== session.user.id) return;
|
||||
const publicEvent = {
|
||||
id: event.id,
|
||||
organization_id: event.organization_id,
|
||||
@@ -1454,13 +1464,26 @@ export async function buildServer({
|
||||
};
|
||||
response.write(`id: ${event.id}\nevent: task\ndata: ${JSON.stringify(publicEvent)}\n\n`);
|
||||
};
|
||||
for (const event of await tasks.eventsSince(session.user.organizationId, since, contextFor(session, request))) send(event);
|
||||
const sendBrowserCommand = (command: TaskBrowserCommand) => {
|
||||
if (command.organization_id !== session.user.organizationId) return;
|
||||
if (command.assigned_user_id !== session.user.id) return;
|
||||
response.write(`event: browser-command\ndata: ${JSON.stringify({
|
||||
action: command.action,
|
||||
task_id: command.task_id,
|
||||
target_user_id: command.assigned_user_id,
|
||||
created_at: command.created_at
|
||||
})}\n\n`);
|
||||
};
|
||||
for (const event of await tasks.eventsSince(session.user.organizationId, session.user.id, since)) send(event);
|
||||
const heartbeat = setInterval(() => response.write(': heartbeat\n\n'), 20_000);
|
||||
const onTask = (event: TaskEvent) => send(event);
|
||||
const onBrowserCommand = (command: TaskBrowserCommand) => sendBrowserCommand(command);
|
||||
tasks.events.on('task', onTask);
|
||||
tasks.events.on('browser-command', onBrowserCommand);
|
||||
request.raw.on('close', () => {
|
||||
clearInterval(heartbeat);
|
||||
tasks.events.off('task', onTask);
|
||||
tasks.events.off('browser-command', onBrowserCommand);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user