Spaces:
Sleeping
Sleeping
| 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; | |