File size: 5,127 Bytes
8314cf4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
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;