EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
notifications.js
Go to the documentation of this file.
1/**
2 * @file routes/notifications.js
3 * @module routes/notifications
4 * @description Notification management API for in-app and Slack notifications. Handles user-specific
5 * and global notifications with multi-tenant isolation. Supports Slack webhook integration for
6 * real-time team alerts.
7 *
8 * **Notification Types:**
9 * - User-specific (user_id set): Targeted to individual user
10 * - Global (user_id NULL): Visible to all users in tenant
11 * - System-wide (root MSP only): Visible across all tenants
12 *
13 * **Common Notification Types:**
14 * - stock: Low stock alerts (product inventory)
15 * - ticket: Ticket updates and assignments
16 * - invoice: Invoice generation and payment alerts
17 * - system: System maintenance and updates
18 * - alert: Critical system alerts
19 * - info: Informational messages
20 *
21 * **Multi-Tenant Behavior:**
22 * - Root MSP: Sees all notifications (system + their user-specific)
23 * - Regular tenants: See only notifications for their tenant (global + user-specific)
24 * - User-specific notifications filtered by user_id from JWT
25 *
26 * **Slack Integration:**
27 * - Automatic webhook notification when Slack integration active
28 * - Configured per tenant in integrations table
29 * - Non-blocking (failures don't prevent notification creation)
30 * - Message format: Bold title + plain text message
31 * - Optional channel targeting via Slack config
32 *
33 * **Use Cases:**
34 * - Low stock alerts (from invoices.js stock management)
35 * - Ticket assignment notifications
36 * - System maintenance announcements
37 * - Invoice generation alerts
38 * - Agent heartbeat failures
39 * - Critical system alerts
40 *
41 * **Related Models:**
42 * - notifications table (notification_id, user_id, customer_id, tenant_id, title, message, type, is_read)
43 * - integrations table (Slack webhook configuration)
44 * - users table (notification recipients)
45 * @requires express
46 * @requires ../services/db
47 * @requires ../middleware/auth
48 * @requires ../middleware/tenant
49 * @author IBG MSP Development Team
50 * @date 2024-03-11
51 * @since 1.0.0
52 * @see {@link module:routes/integrations} for Slack integration configuration
53 * @see {@link module:routes/users} for user management
54 */
55
56const express = require('express');
57const router = express.Router();
58const pool = require('../services/db');
59const authenticateToken = require('../middleware/auth');
60
61const { getTenantFilter } = require('../middleware/tenant');
62
63/**
64 * @api {get} /notifications List Notifications
65 * @apiName GetNotifications
66 * @apiGroup Notifications
67 * @apiDescription Retrieves notifications for authenticated user. Returns user-specific notifications
68 * plus global notifications visible to entire tenant. Root MSP users see system-wide notifications.
69 * Ordered by creation date (newest first), limited to 100 most recent.
70 *
71 * **Notification Visibility:**
72 * - User-specific (user_id = current user): Always visible
73 * - Global (user_id NULL, tenant_id = user's tenant): Visible to all in tenant
74 * - System-wide (user_id NULL, tenant_id NULL): Root MSP only
75 *
76 * **Root MSP Behavior:**
77 * Root MSP tenant sees:
78 * - All their personal notifications (user_id = current user)
79 * - All system-wide global notifications (user_id NULL)
80 *
81 * **Regular Tenant Behavior:**
82 * Regular tenants see:
83 * - Their personal notifications (user_id = current user)
84 * - Tenant-wide global notifications (user_id NULL AND tenant_id = user's tenant)
85 *
86 * **Ordering & Limits:**
87 * - Sorted by created_at DESC (newest first)
88 * - Maximum 100 notifications returned
89 * - Use is_read flag for client-side filtering of unread
90 * @apiHeader {string} Authorization Bearer JWT token
91 * @apiSuccess {object[]} notifications Array of notification objects (max 100)
92 * @apiSuccess {number} notifications.notification_id Unique notification ID
93 * @apiSuccess {number} notifications.user_id Target user ID (NULL for global)
94 * @apiSuccess {number} notifications.customer_id Associated customer ID (if applicable)
95 * @apiSuccess {number} notifications.tenant_id Associated tenant ID (NULL for system-wide)
96 * @apiSuccess {string} notifications.title Notification title/subject
97 * @apiSuccess {string} notifications.message Notification body/details
98 * @apiSuccess {string} notifications.type Notification type (stock, ticket, invoice, system, alert, info)
99 * @apiSuccess {boolean} notifications.is_read Whether user has read notification
100 * @apiSuccess {string} notifications.created_at Creation timestamp
101 * @apiError {number} 500 Server error during notification retrieval
102 * @apiExample {curl} Example Request:
103 * curl -X GET \
104 * -H "Authorization: Bearer eyJhbGc..." \
105 * "https://api.everydaytech.au/notifications"
106 * @apiExample {json} Success Response:
107 * HTTP/1.1 200 OK
108 * [
109 * {
110 * "notification_id": 42,
111 * "user_id": 12,
112 * "customer_id": null,
113 * "tenant_id": 5,
114 * "title": "Ticket Assigned",
115 * "message": "Ticket #892 has been assigned to you",
116 * "type": "ticket",
117 * "is_read": false,
118 * "created_at": "2024-03-11T17:45:00.000Z"
119 * },
120 * {
121 * "notification_id": 38,
122 * "user_id": null,
123 * "customer_id": null,
124 * "tenant_id": 5,
125 * "title": "Low stock: Dell Latitude 5420 Laptop",
126 * "message": "Stock for product Dell Latitude 5420 Laptop is low (8 < 10)",
127 * "type": "stock",
128 * "is_read": false,
129 * "created_at": "2024-03-11T16:30:00.000Z"
130 * }
131 * ]
132 * @since 1.0.0
133 * @see {@link module:routes/notifications.createNotification} for creating notifications
134 * @see {@link module:routes/notifications.markNotificationRead} for marking as read
135 */
136router.get('/', authenticateToken, async (req, res) => {
137 const userId = req.user && req.user.user_id;
138 const tenantId = req.user?.tenantId || req.user?.tenant_id;
139 const isRootTenant = req.tenant?.isMsp || req.user?.is_msp;
140
141 const { clause: tenantClause, params: tenantParams } = getTenantFilter(req, 'n');
142 try {
143 let query;
144 let params;
145
146 if (isRootTenant) {
147 // Root tenant sees all notifications (system + user-specific)
148 query = `SELECT * FROM notifications n WHERE (user_id IS NULL OR user_id = $1)`;
149 params = [userId];
150 } else {
151 // Regular tenants only see notifications for their tenant
152 query = `SELECT * FROM notifications n WHERE (user_id = $1 OR (user_id IS NULL AND tenant_id = $2))`;
153 params = [userId, tenantId];
154 }
155
156 if (tenantClause && !isRootTenant) {
157 query += ` AND ${tenantClause}`;
158 params = [...params, ...tenantParams];
159 }
160
161 query += ' ORDER BY created_at DESC LIMIT 100';
162 const result = await pool.query(query, params);
163 res.json(result.rows);
164 } catch (err) {
165 console.error('Error fetching notifications:', err);
166 res.status(500).send('Server error');
167 }
168});
169
170/**
171 * @api {post} /notifications Create Notification
172 * @apiName CreateNotification
173 * @apiGroup Notifications
174 * @apiDescription Creates a new notification with optional Slack webhook integration. Supports
175 * user-specific and global notifications. Automatically sends to Slack if integration active
176 * for tenant.
177 *
178 * **Notification Targeting:**
179 * - User-specific: Set user_id (notification visible only to that user)
180 * - Global: Omit user_id or set NULL (notification visible to all users in tenant)
181 * - Customer-related: Set customer_id to associate with customer record
182 *
183 * **Tenant Context:**
184 * - Application determines tenant_id from req.tenant context (JWT)
185 * - Can override with explicit tenant_id parameter (for impersonation)
186 * - Falls back to req.tenant.id if not provided
187 *
188 * **Slack Integration:**
189 * If Slack integration configured and active for tenant:
190 * 1. Query integrations table for tenant's Slack config
191 * 2. Extract webhookUrl and optional channel from config JSON
192 * 3. POST to Slack webhook with formatted message
193 * 4. Message format: Bold title + plain text message
194 * 5. Failure non-blocking (notification still created)
195 *
196 * **Type Best Practices:**
197 * - stock: Inventory/reorder alerts
198 * - ticket: Ticket assignments, updates, closures
199 * - invoice: Invoice generation, payment, overdue
200 * - system: Maintenance, updates, configuration changes
201 * - alert: Critical issues requiring immediate attention
202 * - info: General informational messages
203 * @apiHeader {string} Authorization Bearer JWT token
204 * @apiHeader {string} Content-Type application/json
205 * @apiParam {number} [user_id] Target user ID (NULL for global notification)
206 * @apiParam {number} [customer_id] Related customer ID (optional)
207 * @apiParam {string} title Notification title/subject (required)
208 * @apiParam {string} message Notification body/details (required)
209 * @apiParam {string} type Notification type (stock, ticket, invoice, system, alert, info)
210 * @apiParam {number} [tenant_id] Override tenant ID (for impersonation, defaults to req.tenant.id)
211 * @apiSuccess {number} notification_id Generated notification ID
212 * @apiSuccess {number} user_id Target user ID
213 * @apiSuccess {number} customer_id Associated customer ID
214 * @apiSuccess {number} tenant_id Associated tenant ID
215 * @apiSuccess {string} title Notification title
216 * @apiSuccess {string} message Notification message
217 * @apiSuccess {string} type Notification type
218 * @apiSuccess {boolean} is_read Always false for new notifications
219 * @apiSuccess {string} created_at Creation timestamp
220 *
221 * @apiError {Number} 500 Failed to create notification
222 *
223 * @apiExample {curl} Example Request (User-Specific):
224 * curl -X POST \
225 * -H "Authorization: Bearer eyJhbGc..." \
226 * -H "Content-Type: application/json" \
227 * -d '{
228 * "user_id": 12,
229 * "title": "Ticket Assigned",
230 * "message": "Ticket #892 has been assigned to you",
231 * "type": "ticket"
232 * }' \
233 * "https://api.everydaytech.au/notifications"
234 *
235 * @apiExample {curl} Example Request (Global):
236 * curl -X POST \
237 * -H "Authorization: Bearer eyJhbGc..." \
238 * -H "Content-Type: application/json" \
239 * -d '{
240 * "title": "System Maintenance",
241 * "message": "Platform maintenance scheduled for March 15, 2024 at 2:00 AM UTC",
242 * "type": "system"
243 * }' \
244 * "https://api.everydaytech.au/notifications"
245 *
246 * @apiExample {json} Success Response:
247 * HTTP/1.1 201 Created
248 * {
249 * "notification_id": 42,
250 * "user_id": 12,
251 * "customer_id": null,
252 * "tenant_id": 5,
253 * "title": "Ticket Assigned",
254 * "message": "Ticket #892 has been assigned to you",
255 * "type": "ticket",
256 * "is_read": false,
257 * "created_at": "2024-03-11T17:45:00.000Z"
258 * }
259 *
260 * @since 1.0.0
261 * @see {@link module:routes/notifications.getNotifications} for retrieving notifications
262 * @see {@link module:routes/integrations} for Slack integration setup
263 */
264router.post('/', authenticateToken, async (req, res) => {
265 const { user_id, customer_id, title, message, type, tenant_id } = req.body;
266 let targetTenantId = tenant_id;
267 if (!targetTenantId && req.tenant && req.tenant.id) targetTenantId = req.tenant.id;
268 try {
269 const result = await pool.query(
270 `INSERT INTO notifications (user_id, customer_id, title, message, type, tenant_id) VALUES ($1,$2,$3,$4,$5,$6) RETURNING *`,
271 [user_id || null, customer_id || null, title, message, type, targetTenantId]
272 );
273 // Slack integration: send notification if enabled and configured
274 try {
275 const slackRes = await pool.query(
276 `SELECT config FROM integrations WHERE tenant_id = $1 AND integration_type = 'slack' AND is_active = true LIMIT 1`,
277 [targetTenantId]
278 );
279 if (slackRes.rows.length) {
280 const slackCfg = slackRes.rows[0].config;
281 const webhookUrl = slackCfg.webhookUrl;
282 const channel = slackCfg.channel || '';
283 if (webhookUrl) {
284 const payload = {
285 text: `*${title}*\n${message}`,
286 channel: channel || undefined
287 };
288 await fetch(webhookUrl, {
289 method: 'POST',
290 headers: { 'Content-Type': 'application/json' },
291 body: JSON.stringify(payload)
292 });
293 }
294 }
295 } catch (e) {
296 console.error('Slack notification error:', e);
297 }
298 res.status(201).json(result.rows[0]);
299 } catch (err) {
300 console.error('Error creating notification:', err);
301 res.status(500).send('Failed to create notification');
302 }
303});
304
305/**
306 * @api {put} /notifications/:id/mark-read Mark Notification as Read
307 * @apiName MarkNotificationRead
308 * @apiGroup Notifications
309 * @apiDescription Marks a notification as read by setting is_read flag to true. Used when user
310 * views notification in UI. Idempotent operation - calling multiple times has same effect.
311 *
312 * **UI Integration:**
313 * - Call when user clicks/views notification
314 * - Update notification badge counts in real-time
315 * - Use for "mark all as read" features (call for each notification)
316 *
317 * **No Ownership Validation:**
318 * Current implementation does not validate notification belongs to authenticated user.
319 * Consider adding WHERE user_id = $1 clause for security.
320 * @apiHeader {string} Authorization Bearer JWT token
321 * @apiParam {number} id Notification ID (in URL path)
322 * @apiSuccess {number} notification_id Notification ID
323 * @apiSuccess {boolean} is_read Always true after successful update
324 * @apiSuccess {string} updated_at Update timestamp (if column exists)
325 * @apiError {number} 500 Failed to update notification
326 * @apiExample {curl} Example Request:
327 * curl -X PUT \
328 * -H "Authorization: Bearer eyJhbGc..." \
329 * "https://api.everydaytech.au/notifications/42/mark-read"
330 * @apiExample {json} Success Response:
331 * HTTP/1.1 200 OK
332 * {
333 * "notification_id": 42,
334 * "user_id": 12,
335 * "customer_id": null,
336 * "tenant_id": 5,
337 * "title": "Ticket Assigned",
338 * "message": "Ticket #892 has been assigned to you",
339 * "type": "ticket",
340 * "is_read": true,
341 * "created_at": "2024-03-11T17:45:00.000Z"
342 * }
343 * @since 1.0.0
344 * @see {@link module:routes/notifications.getNotifications} for retrieving notifications
345 */
346router.put('/:id/mark-read', authenticateToken, async (req, res) => {
347 const id = req.params.id;
348 try {
349 const result = await pool.query('UPDATE notifications SET is_read = TRUE WHERE notification_id = $1 RETURNING *', [id]);
350 res.json(result.rows[0]);
351 } catch (err) {
352 console.error('Error marking notification read:', err);
353 res.status(500).send('Failed to update notification');
354 }
355});
356
357module.exports = router;