Files
Webklar-Kundenbereich/server/services/previewDeploy.js
Kenso Grimm cc57cf887a feat: HTTPS-TLS-Sync nach Deploy, Projekte ohne Subdomain, portalPassword-Feld
- previewDeploy.syncPreviewTls(): schreibt Traefik-Router (Host-Regeln) fuer alle
  Preview-Subdomains nach preview-hosts.yml, damit Let's Encrypt Zertifikate
  ausstellt. Laeuft nach jedem Deploy und beim Serverstart (ersetzt das nie
  aufgerufene bash-Script sync-preview-tls.sh).
- createProjectFromTemplate: Subdomain optional. Ohne Subdomain wird nur das
  Gitea-Repo angelegt (projectType 'repo', Status 'created'), kein Deploy.
- customerAdmin: Portal-Passwort liegt jetzt im Feld customers.portalPassword
  statt als __portalPassword__-Prefix in den Notizen; Legacy-Notizen werden
  beim naechsten Update bereinigt. CRM-Stufe (lead/customer) wird beim Setzen
  des Portal-Passworts nicht mehr auf 'active' ueberschrieben.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 16:34:23 +00:00

163 lines
5.0 KiB
JavaScript

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'
const execFileAsync = promisify(execFile)
export 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)
}
export function defaultPreviewConfig(repoFullName) {
const repoName = repoFullName.split('/').pop() || 'preview'
return {
enabled: false,
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)
}
export function buildPreviewUrl(subdomain) {
return `https://${subdomain}.${config.preview.baseHost}`
}
/**
* Schreibt pro deployter Preview-Subdomain einen Traefik-Router mit Host()-Regel
* nach preview-hosts.yml, damit Let's Encrypt (HTTP-01) gueltige Zertifikate
* ausstellt. HostRegexp allein liefert nur das Default-Zertifikat ("Nicht sicher").
* Ersetzt traefik/scripts/sync-preview-tls.sh (bash fehlt im Alpine-Container).
*/
export async function syncPreviewTls() {
const previewRoot = config.preview.deployRoot
const outDir = config.preview.traefikDynamicDir
if (!previewRoot || !outDir) {
return { synced: false, reason: 'PREVIEW_DEPLOY_ROOT oder TRAEFIK_DYNAMIC_DIR nicht gesetzt' }
}
const baseHost = config.preview.baseHost
let entries = []
try {
entries = await fs.readdir(previewRoot, { withFileTypes: true })
} catch {
entries = []
}
const slugs = entries
.filter((entry) => entry.isDirectory() && /^[a-z0-9-]+$/.test(entry.name))
.map((entry) => entry.name)
.sort()
const blocks = slugs.map((name) => {
const host = `${name}.${baseHost}`
const id = `preview-${name}`
return [
` ${id}-https:`,
` rule: "Host(\`${host}\`)"`,
' entryPoints:',
' - websecure',
' service: preview-nginx@docker',
' middlewares:',
' - preview-chain@docker',
' priority: 100',
' tls:',
' certResolver: letsencrypt',
'',
` ${id}-http:`,
` rule: "Host(\`${host}\`)"`,
' entryPoints:',
' - web',
' service: preview-nginx@docker',
' middlewares:',
' - preview-sites-redirect@docker',
' priority: 100',
].join('\n')
})
const content = [
'# Auto-generated by previewDeploy.syncPreviewTls - do not edit manually',
'http:',
' routers:',
...blocks,
'',
].join('\n')
await fs.mkdir(outDir, { recursive: true })
await fs.writeFile(path.join(outDir, 'preview-hosts.yml'), content, 'utf8')
return { synced: true, hosts: slugs.length }
}
export 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 || '')
}
let tls = { synced: false }
try {
tls = await syncPreviewTls()
} catch (err) {
console.error('[previewDeploy] TLS-Sync fehlgeschlagen:', err.message)
tls = { synced: false, error: err.message }
}
return { deployed: true, subdomain, targetDir, tls }
}