Add screenshot uploads to feedback modal, Webex bot DM on issue close
- Feedback modal now supports up to 3 image attachments (PNG/JPG/GIF/WebP, 5MB each) with thumbnail previews. Images are uploaded to GitLab project uploads and embedded as markdown in the issue description. - New webhook endpoint (POST /api/webhooks/gitlab) receives issue close events, parses the submitter from the description, looks up their email, and sends a Webex DM via the Patches O'Houlihan bot. - New helper: backend/helpers/webexBot.js (fire-and-forget DM sender). - Requires WEBEX_BOT_TOKEN and GITLAB_WEBHOOK_SECRET in backend/.env.
This commit is contained in:
73
backend/routes/webhooks.js
Normal file
73
backend/routes/webhooks.js
Normal file
@@ -0,0 +1,73 @@
|
||||
// GitLab Webhook Routes — receives issue lifecycle events from GitLab
|
||||
// Used to notify users via Webex when their 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
|
||||
router.post('/gitlab', express.json(), async (req, res) => {
|
||||
// Always return 200 — webhooks should not retry on app-level failures
|
||||
try {
|
||||
// Validate webhook secret token
|
||||
const token = req.headers['x-gitlab-token'];
|
||||
if (!GITLAB_WEBHOOK_SECRET || token !== GITLAB_WEBHOOK_SECRET) {
|
||||
console.warn('[Webhook] Invalid or missing X-Gitlab-Token');
|
||||
return res.status(200).json({ status: 'ignored', reason: 'invalid token' });
|
||||
}
|
||||
|
||||
const { object_attributes } = req.body || {};
|
||||
|
||||
// Only process issue close events
|
||||
if (!object_attributes || object_attributes.action !== 'close') {
|
||||
return res.status(200).json({ status: 'ignored', reason: 'not a close event' });
|
||||
}
|
||||
|
||||
const issueTitle = object_attributes.title || 'Untitled';
|
||||
const issueNumber = object_attributes.iid;
|
||||
const description = object_attributes.description || '';
|
||||
|
||||
// Parse submitter username from issue description
|
||||
// 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');
|
||||
return res.status(200).json({ status: 'ignored', reason: 'no submitter in description' });
|
||||
}
|
||||
|
||||
const username = submitterMatch[1];
|
||||
|
||||
// Look up user email in database
|
||||
const { rows } = await pool.query(
|
||||
'SELECT email 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' });
|
||||
}
|
||||
|
||||
const email = rows[0].email;
|
||||
|
||||
// Send Webex DM notification
|
||||
const message = `Hey! Your bug report **${issueTitle}** (Issue #${issueNumber}) has been resolved and deployed. — Patches O'Houlihan`;
|
||||
sendDirectMessage(email, message);
|
||||
|
||||
console.log(`[Webhook] Issue #${issueNumber} closed — notified ${username} (${email})`);
|
||||
return res.status(200).json({ status: 'ok', notified: username });
|
||||
} catch (err) {
|
||||
console.error('[Webhook] Error processing GitLab webhook:', err.message);
|
||||
return res.status(200).json({ status: 'error', message: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
module.exports = createWebhooksRouter;
|
||||
Reference in New Issue
Block a user