Spaces:
Sleeping
Sleeping
| import { Router } from "express"; | |
| import { AuthRequest, requireAuth } from "../middleware/auth.js"; | |
| import { canAccessTask, User } from "../lib/auth-utils.js"; | |
| import { db } from "@workspace/db"; | |
| import { commentsTable, usersTable, notificationsTable, tasksTable } from "@workspace/db"; | |
| import { eq } from "drizzle-orm"; | |
| import { ListTaskCommentsParams } from "@workspace/api-zod"; | |
| import { logAudit } from "../lib/audit-logger.js"; | |
| import { z } from "zod"; | |
| const router = Router(); | |
| router.get("/tasks/:id/comments", requireAuth, async (req: AuthRequest, res) => { | |
| try { | |
| const user = req.user as User; | |
| if (!user) { res.status(401).json({ error: "Unauthorized" }); return; } | |
| const { id } = ListTaskCommentsParams.parse({ id: Number(req.params.id) }); | |
| const [task] = await db.select().from(tasksTable).where(eq(tasksTable.id, id)); | |
| if (!task) { res.status(404).json({ error: "Task not found" }); return; } | |
| if (!canAccessTask(user, task)) { | |
| res.status(403).json({ error: "Forbidden: You do not have access to this task's comments" }); | |
| return; | |
| } | |
| const comments = await db.select().from(commentsTable) | |
| .where(eq(commentsTable.taskId, id)) | |
| .orderBy(commentsTable.createdAt); | |
| const userIds = [...new Set(comments.map(c => c.userId).filter(Boolean))] as number[]; | |
| const users = userIds.length > 0 | |
| ? await db.select().from(usersTable) | |
| .then(all => all.filter(u => userIds.includes(u.id))) | |
| : []; | |
| const userMap = Object.fromEntries(users.map(u => [u.id, { ...u, createdAt: u.createdAt.toISOString() }])); | |
| res.json(comments.map(c => ({ | |
| ...c, | |
| createdAt: c.createdAt.toISOString(), | |
| user: c.userId ? userMap[c.userId] ?? null : null, | |
| }))); | |
| } catch (err) { | |
| req.log.error({ err }, "Failed to list comments"); | |
| res.status(500).json({ error: "Internal server error" }); | |
| } | |
| }); | |
| const createCommentSchema = z.object({ | |
| taskId: z.coerce.number().int().positive("معرف المهمة غير صالح"), | |
| content: z.string().trim().min(1, "محتوى التعليق مطلوب"), | |
| mentionedUserIds: z.array(z.coerce.number().int().positive("معرف المستخدم غير صالح")).optional(), | |
| }).strict(); | |
| async function handleCreateComment(req: AuthRequest, res: any) { | |
| try { | |
| const requestingUser = req.user as User; | |
| if (!requestingUser) { res.status(401).json({ error: "Unauthorized" }); return; } | |
| const bodyData = req.params.id !== undefined ? { taskId: req.params.id, ...req.body } : req.body; | |
| const parseResult = createCommentSchema.safeParse(bodyData); | |
| if (!parseResult.success) { | |
| res.status(400).json({ error: parseResult.error.errors[0].message }); | |
| return; | |
| } | |
| const { taskId, content, mentionedUserIds } = parseResult.data; | |
| const [task] = await db.select().from(tasksTable).where(eq(tasksTable.id, taskId)); | |
| if (!task) { res.status(404).json({ error: "Task not found" }); return; } | |
| if (!canAccessTask(requestingUser, task)) { | |
| res.status(403).json({ error: "Forbidden: You do not have permission to comment on this task" }); | |
| return; | |
| } | |
| const [comment] = await db.insert(commentsTable).values({ | |
| taskId, | |
| userId: requestingUser.id, | |
| content, | |
| }).returning(); | |
| logAudit({ | |
| userId: requestingUser.id, | |
| action: "comment_created", | |
| entityType: "comment", | |
| entityId: comment.id, | |
| details: { | |
| commentId: comment.id, | |
| taskId: comment.taskId, | |
| content: comment.content, | |
| mentionedUserIds: mentionedUserIds ?? [], | |
| }, | |
| req, | |
| }); | |
| if (task) { | |
| const notifyUserIds = new Set<number>(); | |
| if (task.createdByUserId && task.createdByUserId !== requestingUser.id) notifyUserIds.add(task.createdByUserId); | |
| if (task.assignedUserId && task.assignedUserId !== requestingUser.id) notifyUserIds.add(task.assignedUserId); | |
| if (mentionedUserIds && mentionedUserIds.length > 0) { | |
| mentionedUserIds.forEach(uid => { if (uid !== requestingUser.id) notifyUserIds.add(uid); }); | |
| } | |
| if (notifyUserIds.size > 0) { | |
| await db.insert(notificationsTable).values( | |
| [...notifyUserIds].map(uid => ({ | |
| userId: uid, | |
| taskId, | |
| type: "comment_added", | |
| message: `تعليق جديد على المهمة: "${task.title}"`, | |
| })) | |
| ); | |
| } | |
| } | |
| let commentAuthor = null; | |
| if (comment.userId) { | |
| const [u] = await db.select().from(usersTable).where(eq(usersTable.id, comment.userId)); | |
| if (u) commentAuthor = { ...u, createdAt: u.createdAt.toISOString() }; | |
| } | |
| res.status(201).json({ | |
| ...comment, | |
| createdAt: comment.createdAt.toISOString(), | |
| user: commentAuthor, | |
| }); | |
| } catch (err) { | |
| req.log.error({ err: err instanceof Error ? err.message : String(err) }, "Failed to create comment"); | |
| res.status(500).json({ error: "Internal server error" }); | |
| } | |
| } | |
| router.post("/tasks/:id/comments", requireAuth, handleCreateComment); | |
| router.post("/comments", requireAuth, handleCreateComment); | |
| export default router; | |