import { Router } from "express"; import { AuthRequest, requireAuth, requireAdmin } from "../middleware/auth.js"; import { checkDeadlines } from "../lib/deadline-checker.js"; import { User, isAdmin, isMember } from "../lib/auth-utils.js"; import { db } from "@workspace/db"; import { notificationsTable, usersTable, tasksTable } from "@workspace/db"; import { eq, and } from "drizzle-orm"; import { ListNotificationsQueryParams, MarkNotificationReadParams, MarkAllNotificationsReadBody, } from "@workspace/api-zod"; import { logAudit } from "../lib/audit-logger.js"; import { z } from "zod"; const router = Router(); function serializeNotif(n: typeof notificationsTable.$inferSelect) { return { ...n, createdAt: n.createdAt.toISOString(), }; } router.get("/notifications", requireAuth, async (req: AuthRequest, res) => { try { const params = ListNotificationsQueryParams.parse(req.query); const userId = Number(params.userId); if (req.userId !== userId && req.user?.role !== "admin") { res.status(403).json({ error: "Forbidden: you can only view your own notifications" }); return; } const conditions = [eq(notificationsTable.userId, userId)]; if (params.unreadOnly) { conditions.push(eq(notificationsTable.isRead, false)); } const notifications = await db.select().from(notificationsTable) .where(and(...conditions)) .orderBy(notificationsTable.createdAt); res.json(notifications.map(serializeNotif)); } catch (err) { req.log.error({ err }, "Failed to list notifications"); res.status(500).json({ error: "Internal server error" }); } }); router.patch("/notifications/:id/read", requireAuth, async (req: AuthRequest, res) => { try { const { id } = MarkNotificationReadParams.parse({ id: Number(req.params.id) }); const [existing] = await db.select().from(notificationsTable).where(eq(notificationsTable.id, id)); if (!existing) { res.status(404).json({ error: "Notification not found" }); return; } if (existing.userId !== req.userId && req.user?.role !== "admin") { res.status(403).json({ error: "Forbidden: you can only mark your own notifications as read" }); return; } const [notif] = await db.update(notificationsTable) .set({ isRead: true }) .where(eq(notificationsTable.id, id)) .returning(); res.json(serializeNotif(notif)); } catch (err) { req.log.error({ err }, "Failed to mark notification read"); res.status(500).json({ error: "Internal server error" }); } }); router.patch("/notifications/read-all", requireAuth, async (req: AuthRequest, res) => { try { const { userId } = MarkAllNotificationsReadBody.parse(req.body); if (userId !== req.userId && req.user?.role !== "admin") { res.status(403).json({ error: "Forbidden: you can only mark your own notifications as read" }); return; } await db.update(notificationsTable) .set({ isRead: true }) .where(eq(notificationsTable.userId, userId)); res.json({ success: true }); } catch (err) { req.log.error({ err }, "Failed to mark all notifications read"); res.status(500).json({ error: "Internal server error" }); } }); const createNotificationSchema = z.object({ recipientId: z.coerce.number().int().positive("معرف المستخدم غير صالح"), type: z.string().trim().min(1, "نوع الإشعار مطلوب"), taskId: z.coerce.number().int().positive("معرف المهمة غير صالح").optional(), message: z.string().trim().min(1, "رسالة الإشعار مطلوبة").optional(), }).strict(); router.post("/notifications", requireAuth, async (req: AuthRequest, res) => { try { const user = req.user as User; if (!user) { res.status(401).json({ error: "Unauthorized" }); return; } if (isMember(user)) { res.status(403).json({ error: "Forbidden: members cannot send notifications" }); return; } const parseResult = createNotificationSchema.safeParse(req.body); if (!parseResult.success) { res.status(400).json({ error: parseResult.error.errors[0].message }); return; } const { recipientId, type, taskId, message } = parseResult.data; const [recipient] = await db.select().from(usersTable).where(eq(usersTable.id, recipientId)); if (!recipient) { res.status(400).json({ error: "Recipient user not found" }); return; } if (!isAdmin(user)) { if (taskId) { const [task] = await db.select().from(tasksTable).where(eq(tasksTable.id, taskId)); if (!task || (task.fromTeam !== user.team && task.assignedTeam !== user.team)) { res.status(403).json({ error: "Forbidden: You can only send notifications related to tasks in your team" }); return; } } else { if (recipient.team !== user.team) { res.status(403).json({ error: "Forbidden: You can only send notifications to users in your team" }); return; } } } const [notif] = await db.insert(notificationsTable).values({ userId: recipientId, taskId: taskId ?? null, type, message: message ?? `إشعار جديد: ${type}`, }).returning(); logAudit({ userId: user.id, action: "notification_created", entityType: "notification", entityId: notif.id, details: { notificationId: notif.id, recipientId: notif.userId, taskId: notif.taskId, type: notif.type, message: notif.message, }, req, }); res.status(201).json(serializeNotif(notif)); } catch (err) { req.log.error({ err: err instanceof Error ? err.message : String(err) }, "Failed to create notification"); res.status(500).json({ error: "Internal server error" }); } }); router.post("/notifications/check-deadlines", requireAuth, requireAdmin, async (req: AuthRequest, res) => { try { const sent = await checkDeadlines(); logAudit({ userId: req.userId || 1, action: "deadline_check_manual", entityType: "notification", details: { sentCount: sent, supabaseUid: req.user?.userId }, req }); res.json({ sent }); } catch (err) { req.log.error({ err: err instanceof Error ? err.message : String(err) }, "Failed manual deadline check"); res.status(500).json({ error: "Internal server error" }); } }); export default router;