Files
Webklar-Kundenbereich/server/routes/webhook/gitea.js
Kenso Grimm d4bd510dcd feat: Kundenportal-Ausbau - Tabs, Projekt-Einblicke, Git-Zeiterfassung, Rechnungen, Chat
Kunden-Dashboard:
- Tab-Navigation: Projekte | Was wurde gemacht | Rechnungen | Chat
- Projekt-Details pro Projekt: letzte Git-Commits (Titel+Beschreibung),
  Projektgroesse, Datei-Uebersicht (Top-Level aggregiert), Ticket-Arbeiten
- Rechnungen: gestylte Liste mit Status-Pillen, Ansehen/Zahlen-Link,
  PDF-Download ueber Server-Proxy (IDOR-geschuetzt)
- Chat mit dem Webklar-Team (Polling, Ungelesen-Badge, viewAs blockiert)

Admin-Dashboard:
- Chat-Tab: Konversationsliste + Thread + Antwortfeld, Ungelesen-Badge

Backend:
- giteaAdmin: getRepoInfo, listRepoCommits, getRepoTreeSummary
- projectInsights: 5-min-Cache, Invalidierung per Gitea-Webhook
- /api/projects/:id/git|files|work mit Ownership-Check (404)
- /api/chat/* (Kunde) + /api/portal-admin/chats/* (Admin), portalMessages-Collection
- mailer: E-Mail-Benachrichtigung an Admins bei Kundennachricht (15-min-Throttle)
- gitWorksheet: dd.mm.yyyy, voller Commit-Body, startTime leer (Zeit-Nachtrag),
  Auto-Webpage-Ticket bei Push auf Projekt ohne Ticket (ensureProjectTicket)
- customerActivity: Git-Push-Eintraege fuer Kunden sichtbar

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 18:01:24 +00:00

139 lines
4.6 KiB
JavaScript

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'
import { ensureProjectTicket } from '../../services/ticketAdmin.js'
import { invalidateProjectCache } from '../../services/projectInsights.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)
invalidateProjectCache(repoFullName)
let gitWorksheet = null
if (commitSha && commitSha !== '0000000000000000000000000000000000000000') {
// Projekt ohne Ticket: automatisch ein Webpage-Ticket anlegen (nur mit Kunde)
let ticketId = project.ticketId
if (!ticketId) {
try {
ticketId = await ensureProjectTicket(project, { repoFullName })
} catch (err) {
console.error('[webhook] Auto-Ticket fehlgeschlagen:', err.message)
}
}
if (ticketId) {
gitWorksheet = await createGitPushWorksheet({
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