// GitLab Webhook Routes — receives issue lifecycle events from GitLab // Used to create in-app notifications when feedback issues are closed. const express = require('express'); const pool = require('../db'); const GITLAB_WEBHOOK_SECRET = process.env.GITLAB_WEBHOOK_SECRET || ''; function createWebhooksRouter() { const router = express.Router(); /** * 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 { // 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 notification'); return res.status(200).json({ status: 'ignored', reason: 'no submitter in description' }); } const username = submitterMatch[1]; // Verify user exists in database const { rows } = await pool.query( 'SELECT id FROM users WHERE username = $1', [username] ); 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 userId = rows[0].id; // 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 — notification created for ${username}`); 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;