Replace Webex bot with in-app notification system
Org blocks external Webex bots, so replaced the DM approach with an in-app notification bell. GitLab webhook still fires on issue close, but now writes to a notifications table instead of calling Webex API. - New: notifications table + migration - New: GET/PATCH/POST /api/notifications endpoints - New: NotificationBell component (bell icon + badge + dropdown) - Removed: backend/helpers/webexBot.js (org-blocked) - Removed: WEBEX_BOT_TOKEN from .env
This commit is contained in:
98
backend/routes/notifications.js
Normal file
98
backend/routes/notifications.js
Normal file
@@ -0,0 +1,98 @@
|
||||
// Notifications route — in-app notification management for users
|
||||
// Provides unread notifications, counts, and mark-as-read operations.
|
||||
|
||||
const express = require('express');
|
||||
const pool = require('../db');
|
||||
const { requireAuth } = require('../middleware/auth');
|
||||
|
||||
function createNotificationsRouter() {
|
||||
const router = express.Router();
|
||||
|
||||
// All routes require authentication
|
||||
router.use(requireAuth());
|
||||
|
||||
/**
|
||||
* GET /api/notifications
|
||||
* Returns unread notifications for the current user, ordered by newest first.
|
||||
* Limited to 50 results.
|
||||
*/
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const { rows } = await pool.query(
|
||||
`SELECT id, type, title, message, issue_number, read, created_at
|
||||
FROM notifications
|
||||
WHERE username = $1 AND read = FALSE
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 50`,
|
||||
[req.user.username]
|
||||
);
|
||||
res.json(rows);
|
||||
} catch (err) {
|
||||
console.error('[Notifications] Error fetching notifications:', err.message);
|
||||
res.status(500).json({ error: 'Failed to fetch notifications' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/notifications/count
|
||||
* Returns the unread notification count for the current user (for badge display).
|
||||
*/
|
||||
router.get('/count', async (req, res) => {
|
||||
try {
|
||||
const { rows } = await pool.query(
|
||||
`SELECT COUNT(*)::int AS unread
|
||||
FROM notifications
|
||||
WHERE username = $1 AND read = FALSE`,
|
||||
[req.user.username]
|
||||
);
|
||||
res.json({ unread: rows[0].unread });
|
||||
} catch (err) {
|
||||
console.error('[Notifications] Error fetching count:', err.message);
|
||||
res.status(500).json({ error: 'Failed to fetch notification count' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* PATCH /api/notifications/:id/read
|
||||
* Marks a single notification as read. Only the owning user can mark their own.
|
||||
*/
|
||||
router.patch('/:id/read', async (req, res) => {
|
||||
const { id } = req.params;
|
||||
try {
|
||||
const result = await pool.query(
|
||||
`UPDATE notifications SET read = TRUE
|
||||
WHERE id = $1 AND username = $2`,
|
||||
[id, req.user.username]
|
||||
);
|
||||
if (result.rowCount === 0) {
|
||||
return res.status(404).json({ error: 'Notification not found' });
|
||||
}
|
||||
res.json({ status: 'ok' });
|
||||
} catch (err) {
|
||||
console.error('[Notifications] Error marking read:', err.message);
|
||||
res.status(500).json({ error: 'Failed to mark notification as read' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/notifications/read-all
|
||||
* Marks all notifications as read for the current user.
|
||||
*/
|
||||
router.post('/read-all', async (req, res) => {
|
||||
try {
|
||||
const result = await pool.query(
|
||||
`UPDATE notifications SET read = TRUE
|
||||
WHERE username = $1 AND read = FALSE`,
|
||||
[req.user.username]
|
||||
);
|
||||
res.json({ status: 'ok', marked: result.rowCount });
|
||||
} catch (err) {
|
||||
console.error('[Notifications] Error marking all read:', err.message);
|
||||
res.status(500).json({ error: 'Failed to mark notifications as read' });
|
||||
}
|
||||
});
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
module.exports = createNotificationsRouter;
|
||||
@@ -1,16 +1,32 @@
|
||||
// GitLab Webhook Routes — receives issue lifecycle events from GitLab
|
||||
// Used to notify users via Webex when their feedback issues are closed.
|
||||
// Used to create in-app notifications when feedback issues are closed.
|
||||
|
||||
const express = require('express');
|
||||
const pool = require('../db');
|
||||
const { sendDirectMessage } = require('../helpers/webexBot');
|
||||
|
||||
const GITLAB_WEBHOOK_SECRET = process.env.GITLAB_WEBHOOK_SECRET || '';
|
||||
|
||||
function createWebhooksRouter() {
|
||||
const router = express.Router();
|
||||
|
||||
// POST /api/webhooks/gitlab — GitLab issue webhook receiver
|
||||
/**
|
||||
* POST /api/webhooks/gitlab
|
||||
*
|
||||
* Receives GitLab issue webhook events. When an issue is closed, parses the
|
||||
* submitter username from the issue description and creates an in-app notification.
|
||||
*
|
||||
* Always returns HTTP 200 to prevent GitLab from retrying on app-level failures.
|
||||
*
|
||||
* @header {string} x-gitlab-token - Webhook secret token (must match GITLAB_WEBHOOK_SECRET env var)
|
||||
* @body {object} object_attributes - GitLab issue event payload
|
||||
* @body {string} object_attributes.action - The issue action (only 'close' is processed)
|
||||
* @body {string} object_attributes.title - The issue title
|
||||
* @body {number} object_attributes.iid - The issue number
|
||||
* @body {string} object_attributes.description - The issue description (parsed for "**Submitted by:** username")
|
||||
* @returns {object} 200 - { status: 'ok', notified: username }
|
||||
* @returns {object} 200 - { status: 'ignored', reason: 'invalid token' | 'not a close event' | 'no submitter in description' | 'user not found' }
|
||||
* @returns {object} 200 - { status: 'error', message: string }
|
||||
*/
|
||||
router.post('/gitlab', express.json(), async (req, res) => {
|
||||
// Always return 200 — webhooks should not retry on app-level failures
|
||||
try {
|
||||
@@ -36,30 +52,34 @@ function createWebhooksRouter() {
|
||||
// Format: **Submitted by:** username
|
||||
const submitterMatch = description.match(/\*\*Submitted by:\*\*\s*(\S+)/);
|
||||
if (!submitterMatch) {
|
||||
console.log('[Webhook] No submitter found in issue description — skipping DM');
|
||||
console.log('[Webhook] No submitter found in issue description — skipping notification');
|
||||
return res.status(200).json({ status: 'ignored', reason: 'no submitter in description' });
|
||||
}
|
||||
|
||||
const username = submitterMatch[1];
|
||||
|
||||
// Look up user email in database
|
||||
// Verify user exists in database
|
||||
const { rows } = await pool.query(
|
||||
'SELECT email FROM users WHERE username = $1',
|
||||
'SELECT id FROM users WHERE username = $1',
|
||||
[username]
|
||||
);
|
||||
|
||||
if (!rows || rows.length === 0 || !rows[0].email) {
|
||||
console.log(`[Webhook] No email found for user "${username}" — skipping DM`);
|
||||
return res.status(200).json({ status: 'ignored', reason: 'user email not found' });
|
||||
if (!rows || rows.length === 0) {
|
||||
console.log(`[Webhook] No user found for "${username}" — skipping notification`);
|
||||
return res.status(200).json({ status: 'ignored', reason: 'user not found' });
|
||||
}
|
||||
|
||||
const email = rows[0].email;
|
||||
const userId = rows[0].id;
|
||||
|
||||
// Send Webex DM notification
|
||||
const message = `Hey! Your bug report **${issueTitle}** (Issue #${issueNumber}) has been resolved and deployed. — Patches O'Houlihan`;
|
||||
sendDirectMessage(email, message);
|
||||
// Insert in-app notification
|
||||
const message = `Your bug report **${issueTitle}** (Issue #${issueNumber}) has been resolved and deployed.`;
|
||||
await pool.query(
|
||||
`INSERT INTO notifications (user_id, username, type, title, message, issue_number)
|
||||
VALUES ($1, $2, 'issue_resolved', $3, $4, $5)`,
|
||||
[userId, username, issueTitle, message, issueNumber]
|
||||
);
|
||||
|
||||
console.log(`[Webhook] Issue #${issueNumber} closed — notified ${username} (${email})`);
|
||||
console.log(`[Webhook] Issue #${issueNumber} closed — notification created for ${username}`);
|
||||
return res.status(200).json({ status: 'ok', notified: username });
|
||||
} catch (err) {
|
||||
console.error('[Webhook] Error processing GitLab webhook:', err.message);
|
||||
|
||||
Reference in New Issue
Block a user