import crypto from 'node:crypto' import { Router } from 'express' import { config } from '../../config.js' import { upsertWebsiteProjectByRepo } from '../../services/appwriteAdmin.js' import { fetchPreviewConfig, defaultPreviewConfig, runDeploy, buildPreviewUrl, } from '../../services/previewDeploy.js' import { createGitPushWorksheet } from '../../services/gitWorksheet.js' const router = Router() function verifyWebhookToken(req) { const token = config.gitea.webhookToken if (!token) return false const queryToken = req.query.token const headerToken = req.get('X-Gitea-Token') if (queryToken === token || headerToken === token) return true // Gitea sendet das Webhook-Secret nicht im Klartext, sondern als // HMAC-SHA256-Signatur des Request-Bodys (X-Gitea-Signature). const signature = req.get('X-Gitea-Signature') || req.get('X-Gogs-Signature') if (signature && req.rawBody) { const expected = crypto.createHmac('sha256', token).update(req.rawBody).digest('hex') try { return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected)) } catch { return false } } return false } router.post('/gitea', async (req, res) => { if (!verifyWebhookToken(req)) { return res.status(401).json({ error: 'Unauthorized' }) } const payload = req.body || {} const ref = payload.ref || '' const repo = payload.repository || {} const repoFullName = repo.full_name || '' const commits = payload.commits || [] const pusher = payload.pusher || {} const commitSha = payload.after || commits[commits.length - 1]?.id || '' if (!repoFullName) { return res.status(400).json({ error: 'repository.full_name fehlt' }) } const branch = ref.replace('refs/heads/', '') || 'main' try { const previewConfigFile = await fetchPreviewConfig(repoFullName, branch) const previewConfig = previewConfigFile || defaultPreviewConfig(repoFullName) const hasPreview = Boolean(previewConfigFile?.enabled !== false && previewConfigFile) const subdomain = previewConfig.subdomain || repoFullName.split('/').pop() const previewUrl = subdomain ? buildPreviewUrl(subdomain) : '' let deployResult = { deployed: false, skipped: true } const configBranch = previewConfig.branch || 'main' if (hasPreview && previewConfig.enabled !== false && branch === configBranch) { try { deployResult = await runDeploy(repoFullName, branch, previewConfig) } catch (deployErr) { console.error('[webhook] deploy failed:', deployErr.message) deployResult = { deployed: false, error: deployErr.message } } } const projectPayload = { projectName: previewConfig.displayName || repo.name || subdomain, templateName: previewConfig.templateName || config.gitea.templateRepo, giteaRepoUrl: repo.html_url || `${config.gitea.baseUrl}/${repoFullName}`, giteaRepoName: repo.name || subdomain, repoFullName, } if (subdomain) projectPayload.subdomain = subdomain if (previewUrl) projectPayload.previewUrl = previewUrl if (deployResult.deployed) { projectPayload.status = 'deployed' projectPayload.provisioningStatus = 'ready' } else if (hasPreview) { projectPayload.status = 'pending' projectPayload.provisioningStatus = deployResult.error ? 'deploy_failed' : 'pending' } else { projectPayload.status = 'synced' } const project = await upsertWebsiteProjectByRepo(repoFullName, projectPayload) let gitWorksheet = null if (project.ticketId && commitSha && commitSha !== '0000000000000000000000000000000000000000') { gitWorksheet = await createGitPushWorksheet({ ticketId: project.ticketId, repoFullName, branch, commits, pusher, commitSha, }) } return res.json({ ok: true, repoFullName, subdomain, previewUrl, deploy: deployResult, projectId: project.$id, gitWorksheet, }) } catch (err) { console.error('[webhook]', err) return res.status(500).json({ error: err.message || 'Webhook-Verarbeitung fehlgeschlagen' }) } }) export default router