jiraCloud/routes/webexOAuth.js
jmcqueen efc64a227c Add Webex inbound OAuth flow and room-to-Jira job request pipeline.
Introduces integration-based webhook registration, message parsing, dry-run
monitoring, JSM ticket creation, and OAuth token refresh for DC Ops spaces.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-28 08:27:52 -04:00

139 lines
5.1 KiB
JavaScript

const pendingStates = new Map();
const STATE_TTL_MS = 10 * 60 * 1000;
function cleanupStates() {
const now = Date.now();
for (const [state, createdAt] of pendingStates.entries()) {
if (now - createdAt > STATE_TTL_MS) {
pendingStates.delete(state);
}
}
}
function renderHtml(title, body) {
return `<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title>${title}</title></head>
<body>
<h1>${title}</h1>
${body}
</body>
</html>`;
}
export function createWebexOAuthRoutes({ config, webexOAuth, webexWebhookManager, webexRooms, log }) {
const { logger, logError } = log;
const oauthStartUrl = config.webex.integration.redirectUri.replace('/oauth/callback', '/oauth/start');
function registerRoutes(app) {
app.get('/webex/oauth/start', (req, res) => {
if (!config.webex.integration.clientId || !config.webex.integration.clientSecret) {
res.status(503).send(renderHtml(
'Webex OAuth Not Configured',
'<p>Set WEBEX_INTEGRATION_CLIENT_ID and WEBEX_INTEGRATION_CLIENT_SECRET in .env.</p>'
));
return;
}
cleanupStates();
const state = webexOAuth.createOAuthState();
pendingStates.set(state, Date.now());
res.redirect(webexOAuth.buildAuthorizeUrl(state));
});
app.get('/webex/oauth/callback', async (req, res) => {
const { code, state, error, error_description: errorDescription } = req.query;
if (error) {
res.status(400).send(renderHtml(
'Webex OAuth Failed',
`<p>${errorDescription || error}</p>`
));
return;
}
cleanupStates();
if (!state || !pendingStates.has(state)) {
res.status(400).send(renderHtml(
'Webex OAuth Failed',
'<p>Invalid or expired OAuth state. <a href="/webex/oauth/start">Try again</a>.</p>'
));
return;
}
pendingStates.delete(state);
if (!code) {
res.status(400).send(renderHtml(
'Webex OAuth Failed',
'<p>Missing authorization code.</p>'
));
return;
}
try {
await webexOAuth.exchangeCode(code);
const accessToken = await webexOAuth.getAccessToken();
const webhookResults = await webexWebhookManager.ensureWebhooks({
accessToken,
webexRooms,
config,
});
logger('webexOAuth', `OAuth complete; webhooks ensured for ${webhookResults.length} room(s)`);
const webhookList = webhookResults.map(result =>
`<li>${result.configKey}: ${result.action} (${result.webhookId})</li>`
).join('');
res.status(200).send(renderHtml(
'Webex OAuth Complete',
`<p>Authentication succeeded and webhooks were registered.</p>
<ul>${webhookList}</ul>
<p><a href="/webex/oauth/status">View status</a></p>`
));
} catch (err) {
logError('webexOAuth', 'OAuth callback failed', err);
res.status(500).send(renderHtml(
'Webex OAuth Failed',
`<p>${err.message}</p><p><a href="/webex/oauth/start">Try again</a></p>`
));
}
});
app.get('/webex/oauth/status', (req, res) => {
const status = webexOAuth.getStatus();
const wantsJson = req.accepts(['json', 'html']) === 'json' || req.query.format === 'json';
if (wantsJson) {
res.json({
...status,
oauthStartUrl,
inboundEnabled: config.webex.inboundEnabled,
inboundDryRun: config.webex.inboundDryRun,
webhookTargetUrl: config.webex.webhookTargetUrl,
});
return;
}
const webhookItems = Object.entries(status.webhookIds)
.map(([roomId, webhookId]) => `<li>${roomId}: ${webhookId}</li>`)
.join('') || '<li>None registered yet</li>';
res.status(200).send(renderHtml(
'Webex OAuth Status',
`<p>Authenticated: <strong>${status.authenticated}</strong></p>
<p>Access token expires: ${status.expiresAt || 'n/a'}</p>
<p>Refresh token expires: ${status.refreshExpiresAt || 'n/a'}</p>
<p>Inbound enabled: ${config.webex.inboundEnabled}</p>
<p>Dry run: ${config.webex.inboundDryRun}</p>
<p>Webhook target: ${config.webex.webhookTargetUrl}</p>
<h2>Registered webhooks</h2>
<ul>${webhookItems}</ul>
<p><a href="${oauthStartUrl}">Re-authenticate</a></p>`
));
});
}
return { registerRoutes };
}