chore: aktuellen Live-Stand sichern (Admin-API, Services, Auth-Erweiterungen)
Sichert den bisher uncommitteten Produktionsstand des Kundenbereich-Servers, damit kuenftige Deploys (git reset --hard) nichts mehr verwerfen. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
53
server/routes/admin/customers.js
Normal file
53
server/routes/admin/customers.js
Normal file
@@ -0,0 +1,53 @@
|
||||
import { Router } from 'express'
|
||||
import { requireAppwriteStaff } from '../../middleware/appwriteStaff.js'
|
||||
import {
|
||||
createCustomerWithPortalAccess,
|
||||
deleteCustomerWithPortalAccess,
|
||||
listCustomersForAdmin,
|
||||
updateCustomerWithPortalAccess,
|
||||
} from '../../services/customerAdmin.js'
|
||||
|
||||
const router = Router()
|
||||
|
||||
router.use(requireAppwriteStaff)
|
||||
|
||||
router.get('/', async (_req, res) => {
|
||||
try {
|
||||
const customers = await listCustomersForAdmin()
|
||||
return res.json({ customers })
|
||||
} catch (err) {
|
||||
const status = err.status || 500
|
||||
return res.status(status).json({ error: err.message || 'Kunden konnten nicht geladen werden' })
|
||||
}
|
||||
})
|
||||
|
||||
router.post('/', async (req, res) => {
|
||||
try {
|
||||
const result = await createCustomerWithPortalAccess(req.body || {})
|
||||
return res.status(201).json(result)
|
||||
} catch (err) {
|
||||
const status = err.status || 500
|
||||
return res.status(status).json({ error: err.message || 'Kunde konnte nicht angelegt werden' })
|
||||
}
|
||||
})
|
||||
|
||||
router.patch('/:customerId', async (req, res) => {
|
||||
try {
|
||||
const result = await updateCustomerWithPortalAccess(req.params.customerId, req.body || {})
|
||||
return res.json(result)
|
||||
} catch (err) {
|
||||
const status = err.status || 500
|
||||
return res.status(status).json({ error: err.message || 'Kunde konnte nicht aktualisiert werden' })
|
||||
}
|
||||
})
|
||||
|
||||
router.delete('/:customerId', async (req, res) => {
|
||||
try {
|
||||
return res.json(await deleteCustomerWithPortalAccess(req.params.customerId))
|
||||
} catch (err) {
|
||||
const status = err.status || 500
|
||||
return res.status(status).json({ error: err.message || 'Kunde konnte nicht gelöscht werden' })
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
51
server/routes/admin/employees.js
Normal file
51
server/routes/admin/employees.js
Normal file
@@ -0,0 +1,51 @@
|
||||
import { Router } from 'express'
|
||||
import { requireAppwriteStaff } from '../../middleware/appwriteStaff.js'
|
||||
import {
|
||||
createEmployeeWithLogin,
|
||||
deleteEmployeeWithLogin,
|
||||
listEmployeesForAdmin,
|
||||
updateEmployeeWithLogin,
|
||||
} from '../../services/employeeAdmin.js'
|
||||
|
||||
const router = Router()
|
||||
router.use(requireAppwriteStaff)
|
||||
|
||||
router.get('/', async (_req, res) => {
|
||||
try {
|
||||
return res.json({ employees: await listEmployeesForAdmin() })
|
||||
} catch (error) {
|
||||
return res.status(error.status || 500).json({ error: error.message })
|
||||
}
|
||||
})
|
||||
|
||||
router.post('/', async (req, res) => {
|
||||
try {
|
||||
return res.status(201).json(await createEmployeeWithLogin(req.body || {}))
|
||||
} catch (error) {
|
||||
return res.status(error.status || 500).json({ error: error.message })
|
||||
}
|
||||
})
|
||||
|
||||
router.patch('/:employeeId', async (req, res) => {
|
||||
try {
|
||||
return res.json(await updateEmployeeWithLogin(
|
||||
req.params.employeeId,
|
||||
req.body || {},
|
||||
req.appwriteUser.$id
|
||||
))
|
||||
} catch (error) {
|
||||
return res.status(error.status || 500).json({ error: error.message })
|
||||
}
|
||||
})
|
||||
|
||||
router.delete('/:employeeId', async (req, res) => {
|
||||
try {
|
||||
return res.json(
|
||||
await deleteEmployeeWithLogin(req.params.employeeId, req.appwriteUser.$id)
|
||||
)
|
||||
} catch (error) {
|
||||
return res.status(error.status || 500).json({ error: error.message })
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
37
server/routes/admin/gitea.js
Normal file
37
server/routes/admin/gitea.js
Normal file
@@ -0,0 +1,37 @@
|
||||
import { Router } from 'express'
|
||||
import { syncReposToAppwrite, fetchRepoReadme } from '../../services/giteaAdmin.js'
|
||||
import { requireAppwriteStaff } from '../../middleware/appwriteStaff.js'
|
||||
|
||||
const router = Router()
|
||||
|
||||
router.use(requireAppwriteStaff)
|
||||
|
||||
router.post('/sync-repos', async (_req, res) => {
|
||||
try {
|
||||
const result = await syncReposToAppwrite()
|
||||
return res.json({ ok: true, ...result })
|
||||
} catch (err) {
|
||||
const status = err.status || 500
|
||||
return res.status(status).json({ error: err.message || 'Gitea-Sync fehlgeschlagen' })
|
||||
}
|
||||
})
|
||||
|
||||
router.get('/repo-readme', async (req, res) => {
|
||||
const repoFullName = String(req.query.repoFullName || '').trim()
|
||||
if (!repoFullName) {
|
||||
return res.status(400).json({ error: 'repoFullName ist erforderlich' })
|
||||
}
|
||||
|
||||
try {
|
||||
const readme = await fetchRepoReadme(repoFullName)
|
||||
if (!readme) {
|
||||
return res.json({ ok: true, found: false, repoFullName })
|
||||
}
|
||||
return res.json({ ok: true, found: true, repoFullName, ...readme })
|
||||
} catch (err) {
|
||||
const status = err.status || 500
|
||||
return res.status(status).json({ error: err.message || 'README konnte nicht geladen werden' })
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
19
server/routes/admin/invoiceClients.js
Normal file
19
server/routes/admin/invoiceClients.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Router } from 'express'
|
||||
import { requireAppwriteStaff } from '../../middleware/appwriteStaff.js'
|
||||
import { listInvoiceNinjaClients } from '../../services/invoiceNinja.js'
|
||||
|
||||
const router = Router()
|
||||
|
||||
router.use(requireAppwriteStaff)
|
||||
|
||||
router.get('/', async (_req, res) => {
|
||||
try {
|
||||
const clients = await listInvoiceNinjaClients()
|
||||
return res.json({ clients })
|
||||
} catch (err) {
|
||||
const status = err.status || 500
|
||||
return res.status(status).json({ error: err.message || 'Kunden konnten nicht geladen werden' })
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
91
server/routes/admin/previewOpen.js
Normal file
91
server/routes/admin/previewOpen.js
Normal file
@@ -0,0 +1,91 @@
|
||||
import { Router } from 'express'
|
||||
import { config } from '../../config.js'
|
||||
import { requireAppwriteStaff } from '../../middleware/appwriteStaff.js'
|
||||
import { listDocuments, Query } from '../../services/appwriteAdmin.js'
|
||||
import { createStaffPreviewToken } from '../../services/staffPreviewTokens.js'
|
||||
|
||||
const router = Router()
|
||||
|
||||
function isStaffAdmin(user) {
|
||||
return (user?.labels || []).includes('admin')
|
||||
}
|
||||
|
||||
function isPreviewProjectReady(project) {
|
||||
if (!project) return false
|
||||
if (project.hasPreview === false) return false
|
||||
const status = (project.provisioningStatus || project.status || '').toLowerCase()
|
||||
return ['ready', 'deployed'].includes(status)
|
||||
}
|
||||
|
||||
function safePreviewUrl(url) {
|
||||
try {
|
||||
const parsed = new URL(String(url || ''))
|
||||
if (
|
||||
parsed.hostname.endsWith('.project.webklar.com') ||
|
||||
parsed.hostname === 'project.webklar.com'
|
||||
) {
|
||||
return parsed.toString()
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function previewUrlForSubdomain(subdomain) {
|
||||
const safe = String(subdomain || '')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9-]/g, '')
|
||||
if (!safe) return null
|
||||
return `https://${safe}.${config.preview.baseHost}`
|
||||
}
|
||||
|
||||
router.use(requireAppwriteStaff)
|
||||
|
||||
router.post('/', async (req, res) => {
|
||||
const user = req.appwriteUser
|
||||
if (!isStaffAdmin(user)) {
|
||||
return res.status(403).json({ error: 'Admin-Berechtigung erforderlich' })
|
||||
}
|
||||
|
||||
const subdomain = String(req.body?.subdomain || '')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
const previewUrl = safePreviewUrl(req.body?.previewUrl) || previewUrlForSubdomain(subdomain)
|
||||
|
||||
if (!subdomain || !previewUrl) {
|
||||
return res.status(400).json({ error: 'subdomain oder gueltige previewUrl erforderlich' })
|
||||
}
|
||||
|
||||
try {
|
||||
const projects = await listDocuments(config.collections.websiteProjects, [
|
||||
Query.equal('subdomain', subdomain),
|
||||
Query.limit(1),
|
||||
])
|
||||
const project = projects[0]
|
||||
if (!project) {
|
||||
return res.status(404).json({ error: 'Projekt nicht gefunden' })
|
||||
}
|
||||
if (!isPreviewProjectReady(project)) {
|
||||
return res.status(409).json({ error: 'Projekt-Vorschau ist noch nicht bereit' })
|
||||
}
|
||||
|
||||
const token = createStaffPreviewToken({
|
||||
userId: user.$id,
|
||||
email: user.email || '',
|
||||
name: user.name || user.email || 'Admin',
|
||||
previewUrl,
|
||||
customerId: project.customerId || '',
|
||||
})
|
||||
|
||||
const portalBase = `https://${config.preview.baseHost}`
|
||||
return res.json({
|
||||
openUrl: `${portalBase}/api/auth/staff-preview?token=${encodeURIComponent(token)}`,
|
||||
})
|
||||
} catch (err) {
|
||||
return res.status(err.status || 500).json({ error: err.message || 'Preview-Login fehlgeschlagen' })
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
32
server/routes/admin/websiteProjects.js
Normal file
32
server/routes/admin/websiteProjects.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import { Router } from 'express'
|
||||
import { createProjectFromTemplate } from '../../services/giteaAdmin.js'
|
||||
import { requireAppwriteStaff } from '../../middleware/appwriteStaff.js'
|
||||
|
||||
const router = Router()
|
||||
|
||||
router.use(requireAppwriteStaff)
|
||||
|
||||
router.post('/create-from-template', async (req, res) => {
|
||||
const { repoName, displayName, subdomain, customerId = '', ticketId = '', owner } = req.body || {}
|
||||
|
||||
if (!repoName || !subdomain) {
|
||||
return res.status(400).json({ error: 'repoName und subdomain sind erforderlich' })
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await createProjectFromTemplate({
|
||||
repoName,
|
||||
displayName,
|
||||
subdomain,
|
||||
customerId,
|
||||
ticketId,
|
||||
owner,
|
||||
})
|
||||
return res.status(201).json({ ok: true, ...result })
|
||||
} catch (err) {
|
||||
const status = err.status || 500
|
||||
return res.status(status).json({ error: err.message || 'Projekt konnte nicht angelegt werden' })
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -3,14 +3,25 @@ import { config } from '../config.js'
|
||||
import {
|
||||
getCustomerByAppwriteUserId,
|
||||
getCustomerByEmail,
|
||||
getDocument,
|
||||
getPortalAccessByCustomerId,
|
||||
listDocuments,
|
||||
updateDocument,
|
||||
Query,
|
||||
} from '../services/appwriteAdmin.js'
|
||||
import { loginWithAppwrite, getLoginCooldownRemainingSec } from '../services/appwriteClient.js'
|
||||
import {
|
||||
clearPortalSession,
|
||||
setPortalSession,
|
||||
} from '../middleware/session.js'
|
||||
import {
|
||||
PREVIEW_COOKIE_NAME,
|
||||
clearPreviewSession,
|
||||
createPreviewSessionToken,
|
||||
previewSessionCookieOptions,
|
||||
verifyPreviewSessionToken,
|
||||
} from '../services/previewSession.js'
|
||||
import { consumeStaffPreviewToken } from '../services/staffPreviewTokens.js'
|
||||
|
||||
const router = Router()
|
||||
|
||||
@@ -64,6 +75,62 @@ async function validatePortalAccess(appwriteUserId, email) {
|
||||
return { customer, portalAccess }
|
||||
}
|
||||
|
||||
async function syncCustomerAppwriteUserId(customer, appwriteUserId) {
|
||||
if (!customer?.$id || !appwriteUserId) return customer
|
||||
if (customer.appwriteUserId === appwriteUserId) return customer
|
||||
|
||||
const now = new Date().toISOString()
|
||||
return updateDocument(config.collections.customers, customer.$id, {
|
||||
appwriteUserId,
|
||||
portalAccessEnabled: true,
|
||||
updatedAt: now,
|
||||
})
|
||||
}
|
||||
|
||||
async function resolveSessionCustomer(session) {
|
||||
if (!session?.customerId) return null
|
||||
|
||||
let customer = await getDocument(config.collections.customers, session.customerId).catch(() => null)
|
||||
if (customer) return customer
|
||||
|
||||
if (session.appwriteUserId) {
|
||||
customer = await getCustomerByAppwriteUserId(session.appwriteUserId)
|
||||
if (customer) return customer
|
||||
}
|
||||
|
||||
if (session.email) {
|
||||
customer = await getCustomerByEmail(session.email)
|
||||
}
|
||||
|
||||
return customer
|
||||
}
|
||||
|
||||
function isPreviewProjectReady(project) {
|
||||
if (!project) return false
|
||||
if (project.hasPreview === false) return false
|
||||
const status = (project.provisioningStatus || project.status || '').toLowerCase()
|
||||
return ['ready', 'deployed'].includes(status)
|
||||
}
|
||||
|
||||
function isAdminSession(session) {
|
||||
return session?.role === 'admin' || (session?.labels || []).includes('admin')
|
||||
}
|
||||
|
||||
function safePreviewRedirectUrl(url) {
|
||||
try {
|
||||
const parsed = new URL(String(url || ''))
|
||||
if (
|
||||
parsed.hostname.endsWith('.project.webklar.com') ||
|
||||
parsed.hostname === 'project.webklar.com'
|
||||
) {
|
||||
return parsed.toString()
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
router.get('/login-status', (_req, res) => {
|
||||
const retryAfterSeconds = getLoginCooldownRemainingSec()
|
||||
res.json({
|
||||
@@ -80,7 +147,8 @@ router.post('/login', async (req, res) => {
|
||||
|
||||
try {
|
||||
const user = await loginWithAppwrite(email.trim(), password)
|
||||
const { customer, portalAccess } = await validatePortalAccess(user.$id, email.trim())
|
||||
let { customer, portalAccess } = await validatePortalAccess(user.$id, email.trim())
|
||||
customer = await syncCustomerAppwriteUserId(customer, user.$id)
|
||||
|
||||
setPortalSession(res, {
|
||||
customerId: customer.$id,
|
||||
@@ -89,6 +157,17 @@ router.post('/login', async (req, res) => {
|
||||
email: customer.email || user.email || email,
|
||||
})
|
||||
|
||||
const previewToken = await createPreviewSessionToken({
|
||||
userId: user.$id,
|
||||
customerId: customer.$id,
|
||||
email: customer.email || user.email || email,
|
||||
name: customer.name || user.name || '',
|
||||
role: 'customer',
|
||||
labels: [],
|
||||
authSource: 'kundenbereich',
|
||||
})
|
||||
res.cookie(PREVIEW_COOKIE_NAME, previewToken, previewSessionCookieOptions())
|
||||
|
||||
try {
|
||||
await updateDocument(config.collections.customerPortalAccess, portalAccess.$id, {
|
||||
lastLoginAt: new Date().toISOString(),
|
||||
@@ -120,9 +199,78 @@ router.post('/login', async (req, res) => {
|
||||
|
||||
router.post('/logout', (_req, res) => {
|
||||
clearPortalSession(res)
|
||||
clearPreviewSession(res)
|
||||
res.json({ success: true })
|
||||
})
|
||||
|
||||
router.get('/preview-access', async (req, res) => {
|
||||
const token = req.cookies?.[PREVIEW_COOKIE_NAME]
|
||||
if (!token) {
|
||||
return res.status(401).json({ allowed: false })
|
||||
}
|
||||
|
||||
const session = await verifyPreviewSessionToken(token)
|
||||
const admin = isAdminSession(session)
|
||||
if (!session || (!session.customerId && !admin)) {
|
||||
return res.status(401).json({ allowed: false })
|
||||
}
|
||||
|
||||
const subdomain = String(req.query.subdomain || '')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
if (!subdomain) {
|
||||
return res.status(400).json({ allowed: false, error: 'subdomain required' })
|
||||
}
|
||||
|
||||
try {
|
||||
const queries = [Query.equal('subdomain', subdomain), Query.limit(1)]
|
||||
if (!admin) {
|
||||
queries.splice(1, 0, Query.equal('customerId', session.customerId))
|
||||
}
|
||||
|
||||
const projects = await listDocuments(config.collections.websiteProjects, queries)
|
||||
const project = projects[0]
|
||||
if (!project) {
|
||||
return res.json({ allowed: false, reason: 'no_project' })
|
||||
}
|
||||
if (!isPreviewProjectReady(project)) {
|
||||
return res.json({ allowed: false, reason: 'not_ready', projectId: project.$id })
|
||||
}
|
||||
return res.json({ allowed: true, projectId: project.$id })
|
||||
} catch (err) {
|
||||
return res.status(500).json({ allowed: false, error: err.message || 'Fehler' })
|
||||
}
|
||||
})
|
||||
|
||||
router.get('/staff-preview', async (req, res) => {
|
||||
const tokenValue = String(req.query.token || '').trim()
|
||||
if (!tokenValue) {
|
||||
return res.status(400).send('Token fehlt')
|
||||
}
|
||||
|
||||
const tokenData = consumeStaffPreviewToken(tokenValue)
|
||||
if (!tokenData) {
|
||||
return res.status(410).send('Der Preview-Link ist abgelaufen. Bitte erneut aus dem Ticketsystem oeffnen.')
|
||||
}
|
||||
|
||||
const previewUrl = safePreviewRedirectUrl(tokenData.previewUrl)
|
||||
if (!previewUrl) {
|
||||
return res.status(400).send('Ungueltige Preview-URL')
|
||||
}
|
||||
|
||||
const previewToken = await createPreviewSessionToken({
|
||||
userId: tokenData.userId,
|
||||
customerId: tokenData.customerId || '',
|
||||
email: tokenData.email || '',
|
||||
name: tokenData.name || 'Admin',
|
||||
role: 'admin',
|
||||
labels: ['admin'],
|
||||
authSource: 'ticket-admin',
|
||||
})
|
||||
res.cookie(PREVIEW_COOKIE_NAME, previewToken, previewSessionCookieOptions())
|
||||
return res.redirect(previewUrl)
|
||||
})
|
||||
|
||||
router.get('/me', async (req, res) => {
|
||||
const raw = req.signedCookies?.[config.cookieName]
|
||||
if (!raw) {
|
||||
@@ -135,12 +283,25 @@ router.get('/me', async (req, res) => {
|
||||
return res.json({ authenticated: false })
|
||||
}
|
||||
|
||||
const customer = await getCustomerByAppwriteUserId(session.appwriteUserId)
|
||||
let customer = await resolveSessionCustomer(session)
|
||||
if (!customer) {
|
||||
clearPortalSession(res)
|
||||
clearPreviewSession(res)
|
||||
return res.json({ authenticated: false })
|
||||
}
|
||||
|
||||
try {
|
||||
await validatePortalAccess(session.appwriteUserId, session.email || customer.email)
|
||||
} catch {
|
||||
clearPortalSession(res)
|
||||
clearPreviewSession(res)
|
||||
return res.json({ authenticated: false })
|
||||
}
|
||||
|
||||
if (customer.appwriteUserId !== session.appwriteUserId) {
|
||||
customer = await syncCustomerAppwriteUserId(customer, session.appwriteUserId)
|
||||
}
|
||||
|
||||
return res.json({
|
||||
authenticated: true,
|
||||
customer: sanitizeCustomer(customer),
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { Router } from 'express'
|
||||
import { execFile } from 'node:child_process'
|
||||
import { promisify } from 'node:util'
|
||||
import fs from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
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 execFileAsync = promisify(execFile)
|
||||
const router = Router()
|
||||
|
||||
function verifyWebhookToken(req) {
|
||||
@@ -17,84 +19,6 @@ function verifyWebhookToken(req) {
|
||||
return queryToken === token || headerToken === token
|
||||
}
|
||||
|
||||
async function fetchPreviewConfig(repoFullName, branch) {
|
||||
const [owner, repo] = repoFullName.split('/')
|
||||
if (!owner || !repo || !config.gitea.apiToken) {
|
||||
return null
|
||||
}
|
||||
|
||||
const url = `${config.gitea.baseUrl}/api/v1/repos/${owner}/${repo}/contents/.webklar-preview.json?ref=${encodeURIComponent(branch)}`
|
||||
const response = await fetch(url, {
|
||||
headers: { Authorization: `token ${config.gitea.apiToken}` },
|
||||
})
|
||||
|
||||
if (!response.ok) return null
|
||||
|
||||
const data = await response.json()
|
||||
const content = Buffer.from(data.content || '', 'base64').toString('utf8')
|
||||
return JSON.parse(content)
|
||||
}
|
||||
|
||||
function defaultPreviewConfig(repoFullName) {
|
||||
const repoName = repoFullName.split('/').pop() || 'preview'
|
||||
return {
|
||||
enabled: true,
|
||||
type: 'static',
|
||||
branch: 'main',
|
||||
displayName: repoName,
|
||||
subdomain: repoName,
|
||||
}
|
||||
}
|
||||
|
||||
async function cloneRepo(cloneUrl, targetDir, branch) {
|
||||
await fs.rm(targetDir, { recursive: true, force: true })
|
||||
await fs.mkdir(targetDir, { recursive: true })
|
||||
await execFileAsync('git', [
|
||||
'clone',
|
||||
'--depth',
|
||||
'1',
|
||||
'--branch',
|
||||
branch,
|
||||
cloneUrl,
|
||||
targetDir,
|
||||
])
|
||||
}
|
||||
|
||||
async function deployStatic(sourceDir, targetDir, indexSubdir = '') {
|
||||
const webroot = indexSubdir ? path.join(sourceDir, indexSubdir) : sourceDir
|
||||
await fs.rm(targetDir, { recursive: true, force: true })
|
||||
await fs.mkdir(targetDir, { recursive: true })
|
||||
await execFileAsync('cp', ['-R', `${webroot}/.`, targetDir])
|
||||
}
|
||||
|
||||
async function deployNodeBuild(sourceDir, targetDir) {
|
||||
await execFileAsync('npm', ['ci'], { cwd: sourceDir })
|
||||
await execFileAsync('npm', ['run', 'build'], { cwd: sourceDir })
|
||||
const distDir = path.join(sourceDir, 'dist')
|
||||
await deployStatic(distDir, targetDir)
|
||||
}
|
||||
|
||||
async function runDeploy(repoFullName, branch, previewConfig) {
|
||||
if (!config.preview.deployRoot) {
|
||||
return { deployed: false, reason: 'PREVIEW_DEPLOY_ROOT nicht gesetzt' }
|
||||
}
|
||||
|
||||
const subdomain = previewConfig.subdomain || repoFullName.split('/').pop()
|
||||
const cloneUrl = `${config.gitea.baseUrl}/${repoFullName}.git`
|
||||
const workDir = path.join(process.cwd(), 'preview-data', repoFullName.replace(/\//g, '_'))
|
||||
const targetDir = path.join(config.preview.deployRoot, subdomain)
|
||||
|
||||
await cloneRepo(cloneUrl, workDir, branch)
|
||||
|
||||
if (previewConfig.type === 'node_build') {
|
||||
await deployNodeBuild(workDir, targetDir)
|
||||
} else {
|
||||
await deployStatic(workDir, targetDir, previewConfig.index || '')
|
||||
}
|
||||
|
||||
return { deployed: true, subdomain, targetDir }
|
||||
}
|
||||
|
||||
router.post('/gitea', async (req, res) => {
|
||||
if (!verifyWebhookToken(req)) {
|
||||
return res.status(401).json({ error: 'Unauthorized' })
|
||||
@@ -104,6 +28,9 @@ router.post('/gitea', async (req, res) => {
|
||||
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' })
|
||||
@@ -112,42 +39,58 @@ router.post('/gitea', async (req, res) => {
|
||||
const branch = ref.replace('refs/heads/', '') || 'main'
|
||||
|
||||
try {
|
||||
let previewConfig = await fetchPreviewConfig(repoFullName, branch)
|
||||
if (!previewConfig) {
|
||||
previewConfig = defaultPreviewConfig(repoFullName)
|
||||
}
|
||||
|
||||
if (!previewConfig.enabled) {
|
||||
return res.json({ ok: true, skipped: true, reason: 'preview disabled' })
|
||||
}
|
||||
|
||||
const configBranch = previewConfig.branch || 'main'
|
||||
if (branch !== configBranch) {
|
||||
return res.json({ ok: true, skipped: true, reason: `branch ${branch} != ${configBranch}` })
|
||||
}
|
||||
|
||||
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 = `https://${subdomain}.${config.preview.baseHost}`
|
||||
const previewUrl = subdomain ? buildPreviewUrl(subdomain) : ''
|
||||
|
||||
let deployResult = { deployed: false }
|
||||
try {
|
||||
deployResult = await runDeploy(repoFullName, branch, previewConfig)
|
||||
} catch (deployErr) {
|
||||
console.error('[webhook] deploy failed:', deployErr.message)
|
||||
deployResult = { deployed: false, error: deployErr.message }
|
||||
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 project = await upsertWebsiteProjectByRepo(repoFullName, {
|
||||
projectName: previewConfig.displayName || subdomain,
|
||||
templateName: previewConfig.templateName || 'webklar-preview-template',
|
||||
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,
|
||||
subdomain,
|
||||
previewUrl,
|
||||
status: deployResult.deployed ? 'deployed' : 'pending',
|
||||
provisioningStatus: deployResult.deployed ? 'ready' : 'deploy_failed',
|
||||
})
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -156,6 +99,7 @@ router.post('/gitea', async (req, res) => {
|
||||
previewUrl,
|
||||
deploy: deployResult,
|
||||
projectId: project.$id,
|
||||
gitWorksheet,
|
||||
})
|
||||
} catch (err) {
|
||||
console.error('[webhook]', err)
|
||||
|
||||
Reference in New Issue
Block a user