e5f2c64e14
Add activity logging to all write API routes, notification system with real-time delivery via Socket.IO, @mention parsing in comments, activity feed sidebar on board page, per-card activity in card modal, and notification bell with unread badge in the navbar. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
107 lines
3.2 KiB
TypeScript
107 lines
3.2 KiB
TypeScript
import { error } from '@sveltejs/kit';
|
|
import type { RequestHandler } from './$types.js';
|
|
import { withTenant } from '$lib/server/rls.js';
|
|
import { requireAuth } from '$lib/server/guards.js';
|
|
import { canViewBoard } from '$lib/server/permissions.js';
|
|
import { readFile, unlink } from 'fs/promises';
|
|
import { json } from '@sveltejs/kit';
|
|
import { broadcast } from '$lib/server/realtime.js';
|
|
import { logActivity } from '$lib/server/activity.js';
|
|
|
|
export const GET: RequestHandler = async ({ params, locals }) => {
|
|
const tenant = locals.tenant;
|
|
if (!tenant) throw error(400, 'Unknown tenant');
|
|
|
|
const attachment = await withTenant(tenant.id, async (tx) => {
|
|
const att = await tx.attachment.findFirst({
|
|
where: { id: params.attachmentId },
|
|
include: {
|
|
card: {
|
|
include: {
|
|
column: {
|
|
include: {
|
|
board: {
|
|
select: { tenantId: true, visibility: true, members: true }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
if (!att) throw error(404, 'Attachment not found');
|
|
if (att.card.column.board.tenantId !== tenant.id) throw error(404, 'Attachment not found');
|
|
|
|
if (!canViewBoard(locals.user, att.card.column.board.visibility, att.card.column.board.members)) {
|
|
if (!locals.user) throw error(401, 'Not authenticated');
|
|
throw error(403, 'Access denied');
|
|
}
|
|
|
|
return att;
|
|
});
|
|
|
|
const buffer = await readFile(attachment.filepath);
|
|
return new Response(buffer, {
|
|
headers: {
|
|
'Content-Type': attachment.mimetype,
|
|
'Content-Disposition': `attachment; filename="${attachment.filename}"`,
|
|
'Content-Length': String(attachment.size)
|
|
}
|
|
});
|
|
};
|
|
|
|
export const DELETE: RequestHandler = async ({ params, request, locals }) => {
|
|
const tenant = locals.tenant;
|
|
if (!tenant) throw error(400, 'Unknown tenant');
|
|
requireAuth(locals.user);
|
|
|
|
const deleted = await withTenant(tenant.id, async (tx) => {
|
|
const att = await tx.attachment.findFirst({
|
|
where: { id: params.attachmentId },
|
|
include: {
|
|
card: {
|
|
include: {
|
|
column: {
|
|
include: {
|
|
board: { select: { id: true, tenantId: true, members: true } }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
if (!att) throw error(404, 'Attachment not found');
|
|
if (att.card.column.board.tenantId !== tenant.id) throw error(404, 'Attachment not found');
|
|
|
|
// Check board editor permission
|
|
const { canEditBoard } = await import('$lib/server/permissions.js');
|
|
if (!canEditBoard(locals.user, att.card.column.board.members)) {
|
|
throw error(403, 'Access denied');
|
|
}
|
|
|
|
await logActivity(tx, {
|
|
boardId: att.card.column.board.id,
|
|
userId: locals.user!.id,
|
|
action: 'attachment.deleted',
|
|
cardId: att.cardId,
|
|
metadata: { filename: att.filename }
|
|
});
|
|
|
|
await tx.attachment.delete({ where: { id: params.attachmentId } });
|
|
|
|
try {
|
|
await unlink(att.filepath);
|
|
} catch {
|
|
// File may already be gone
|
|
}
|
|
|
|
return { boardId: att.card.column.board.id, cardId: att.cardId, columnId: att.card.columnId };
|
|
});
|
|
|
|
const socketId = request.headers.get('X-Socket-ID');
|
|
broadcast(deleted.boardId, 'card:updated', { cardId: deleted.cardId, columnId: deleted.columnId, socketId });
|
|
broadcast(deleted.boardId, 'activity:new', { boardId: deleted.boardId, cardId: deleted.cardId, socketId });
|
|
|
|
return json({ ok: true });
|
|
};
|