fix: 403-Schutz + Selbstheilung fuer geloeschte Preview-Config
- runDeploy schreibt Platzhalter-index.html, wenn das Deploy-Verzeichnis keine index.html enthaelt (leergeraeumtes Repo/Fehlbuild) -> nie mehr nginx 403 Forbidden, Subdomain bleibt erreichbar. - deployStatic: Fallback auf Repo-Wurzel bei fehlendem index-Unterordner, .git wird nicht mehr mit ausgeliefert. - Webhook: fehlt .webklar-preview.json im Repo, wird die Hosting-Config aus dem Appwrite-Projektstand rekonstruiert, ins Repo zurueckgeschrieben und normal deployt (ensureRepoPreviewConfig). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,7 @@ import {
|
|||||||
runDeploy,
|
runDeploy,
|
||||||
buildPreviewUrl,
|
buildPreviewUrl,
|
||||||
} from '../../services/previewDeploy.js'
|
} from '../../services/previewDeploy.js'
|
||||||
|
import { ensureRepoPreviewConfig } from '../../services/giteaAdmin.js'
|
||||||
import { createGitPushWorksheet } from '../../services/gitWorksheet.js'
|
import { createGitPushWorksheet } from '../../services/gitWorksheet.js'
|
||||||
import { ensureProjectTicket } from '../../services/ticketAdmin.js'
|
import { ensureProjectTicket } from '../../services/ticketAdmin.js'
|
||||||
import { invalidateProjectCache } from '../../services/projectInsights.js'
|
import { invalidateProjectCache } from '../../services/projectInsights.js'
|
||||||
@@ -55,7 +56,19 @@ router.post('/gitea', async (req, res) => {
|
|||||||
const branch = ref.replace('refs/heads/', '') || 'main'
|
const branch = ref.replace('refs/heads/', '') || 'main'
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const previewConfigFile = await fetchPreviewConfig(repoFullName, branch)
|
let previewConfigFile = await fetchPreviewConfig(repoFullName, branch)
|
||||||
|
// Selbstheilung: Wurde .webklar-preview.json im Repo geloescht (z.B. beim
|
||||||
|
// Leerraeumen/Ueberarbeiten der Seite), aber das Projekt ist in Appwrite als
|
||||||
|
// gehostet bekannt, dann Config aus dem gespeicherten Stand wiederherstellen
|
||||||
|
// (Datei ins Repo zurueckschreiben) und normal weiter-deployen.
|
||||||
|
if (!previewConfigFile) {
|
||||||
|
try {
|
||||||
|
const restored = await ensureRepoPreviewConfig(repoFullName, branch)
|
||||||
|
if (restored) previewConfigFile = restored
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[webhook] preview-config-Wiederherstellung fehlgeschlagen:', err.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
const previewConfig = previewConfigFile || defaultPreviewConfig(repoFullName)
|
const previewConfig = previewConfigFile || defaultPreviewConfig(repoFullName)
|
||||||
const hasPreview = Boolean(previewConfigFile?.enabled !== false && previewConfigFile)
|
const hasPreview = Boolean(previewConfigFile?.enabled !== false && previewConfigFile)
|
||||||
// Nur Projekte mit aktivierter Preview bekommen eine Subdomain zugewiesen -
|
// Nur Projekte mit aktivierter Preview bekommen eine Subdomain zugewiesen -
|
||||||
|
|||||||
@@ -231,6 +231,47 @@ async function updatePreviewConfigFile(repoFullName, previewConfig, branch = 'ma
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Selbstheilung fuer eine geloeschte/fehlende .webklar-preview.json:
|
||||||
|
* Rekonstruiert die Hosting-Config aus dem in Appwrite gespeicherten
|
||||||
|
* Projektstand (Subdomain, Typ, Name), schreibt sie ins Repo zurueck und liefert
|
||||||
|
* sie fuer einen sofortigen Deploy zurueck. Gibt null zurueck, wenn zu dem Repo
|
||||||
|
* kein gehostetes, aktives Projekt bekannt ist (dann bleibt es Repo-only).
|
||||||
|
*/
|
||||||
|
export async function ensureRepoPreviewConfig(repoFullName, branch = 'main') {
|
||||||
|
if (!repoFullName) return null
|
||||||
|
const docs = await listDocuments(config.collections.websiteProjects, [
|
||||||
|
Query.equal('repoFullName', repoFullName),
|
||||||
|
Query.limit(1),
|
||||||
|
])
|
||||||
|
const doc = docs[0]
|
||||||
|
if (!doc || doc.archived) return null
|
||||||
|
|
||||||
|
const type = deriveProjectType(doc)
|
||||||
|
const hasWebsite = type === 'preview' || type === 'website' || type === 'template'
|
||||||
|
const subdomain = doc.subdomain || ''
|
||||||
|
if (!hasWebsite || !subdomain) return null
|
||||||
|
|
||||||
|
const previewConfig = {
|
||||||
|
enabled: true,
|
||||||
|
type: 'static',
|
||||||
|
branch,
|
||||||
|
displayName: doc.projectName || repoFullName.split('/').pop(),
|
||||||
|
templateName: doc.templateName || config.gitea.templateRepo,
|
||||||
|
projectType: type,
|
||||||
|
public: type === 'website',
|
||||||
|
template: type === 'template',
|
||||||
|
subdomain,
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await updatePreviewConfigFile(repoFullName, previewConfig, branch)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[ensureRepoPreviewConfig] .webklar-preview.json zurueckschreiben fehlgeschlagen:', err.message)
|
||||||
|
}
|
||||||
|
return previewConfig
|
||||||
|
}
|
||||||
|
|
||||||
export async function createProjectFromTemplate({
|
export async function createProjectFromTemplate({
|
||||||
repoName,
|
repoName,
|
||||||
displayName,
|
displayName,
|
||||||
|
|||||||
@@ -70,10 +70,74 @@ async function cloneRepo(cloneUrl, targetDir, branch) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function deployStatic(sourceDir, targetDir, indexSubdir = '') {
|
async function deployStatic(sourceDir, targetDir, indexSubdir = '') {
|
||||||
const webroot = indexSubdir ? path.join(sourceDir, indexSubdir) : sourceDir
|
let webroot = indexSubdir ? path.join(sourceDir, indexSubdir) : sourceDir
|
||||||
|
// Falls der in .webklar-preview.json angegebene index-Unterordner nicht (mehr)
|
||||||
|
// existiert, auf die Repo-Wurzel zurueckfallen statt den cp scheitern zu lassen.
|
||||||
|
try {
|
||||||
|
await fs.access(webroot)
|
||||||
|
} catch {
|
||||||
|
webroot = sourceDir
|
||||||
|
}
|
||||||
await fs.rm(targetDir, { recursive: true, force: true })
|
await fs.rm(targetDir, { recursive: true, force: true })
|
||||||
await fs.mkdir(targetDir, { recursive: true })
|
await fs.mkdir(targetDir, { recursive: true })
|
||||||
await execFileAsync('cp', ['-R', `${webroot}/.`, targetDir])
|
await execFileAsync('cp', ['-R', `${webroot}/.`, targetDir])
|
||||||
|
// Git-Metadaten nie mit ausliefern (verhindert Zugriff auf /.git/ im Preview).
|
||||||
|
await fs.rm(path.join(targetDir, '.git'), { recursive: true, force: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Platzhalter-Seite, die statt eines nackten nginx-403 ausgeliefert wird, wenn
|
||||||
|
* ein Deploy-Verzeichnis keine index.html enthaelt (z.B. leergeraeumtes Repo,
|
||||||
|
* fehlerhafter Build oder falscher index-Unterordner). So bleibt die Subdomain
|
||||||
|
* immer erreichbar und zeigt eine verstaendliche Meldung.
|
||||||
|
*/
|
||||||
|
function placeholderHtml(displayName) {
|
||||||
|
const name = String(displayName || 'Diese Website')
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
return `<!doctype html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta name="robots" content="noindex">
|
||||||
|
<title>${name} – wird vorbereitet</title>
|
||||||
|
<style>
|
||||||
|
html,body{height:100%;margin:0}
|
||||||
|
body{display:flex;align-items:center;justify-content:center;
|
||||||
|
font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif;
|
||||||
|
background:#0f172a;color:#e2e8f0}
|
||||||
|
.box{max-width:32rem;padding:2rem;text-align:center}
|
||||||
|
h1{font-size:1.4rem;margin:0 0 .75rem}
|
||||||
|
p{margin:0;color:#94a3b8;line-height:1.5}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="box">
|
||||||
|
<h1>${name}</h1>
|
||||||
|
<p>Diese Seite wird gerade vorbereitet oder aktualisiert.<br>Bitte in Kürze erneut laden.</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stellt sicher, dass ein Deploy-Verzeichnis eine index.html besitzt. Fehlt sie,
|
||||||
|
* wird ein Platzhalter geschrieben, damit nginx (autoindex off) kein 403 liefert.
|
||||||
|
*/
|
||||||
|
async function ensureIndexHtml(targetDir, displayName) {
|
||||||
|
const indexPath = path.join(targetDir, 'index.html')
|
||||||
|
try {
|
||||||
|
await fs.access(indexPath)
|
||||||
|
return { placeholder: false }
|
||||||
|
} catch {
|
||||||
|
/* keine index.html vorhanden -> Platzhalter schreiben */
|
||||||
|
}
|
||||||
|
await fs.mkdir(targetDir, { recursive: true })
|
||||||
|
await fs.writeFile(indexPath, placeholderHtml(displayName), 'utf8')
|
||||||
|
return { placeholder: true }
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deployNodeBuild(sourceDir, targetDir) {
|
async function deployNodeBuild(sourceDir, targetDir) {
|
||||||
@@ -177,6 +241,10 @@ export async function runDeploy(repoFullName, branch, previewConfig) {
|
|||||||
await deployStatic(workDir, targetDir, previewConfig.index || '')
|
await deployStatic(workDir, targetDir, previewConfig.index || '')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Universeller 403-Schutz: ohne index.html liefert nginx (autoindex off) sonst
|
||||||
|
// "403 Forbidden". Ein Platzhalter haelt die Subdomain immer erreichbar.
|
||||||
|
const index = await ensureIndexHtml(targetDir, previewConfig.displayName || subdomain)
|
||||||
|
|
||||||
let tls = { synced: false }
|
let tls = { synced: false }
|
||||||
try {
|
try {
|
||||||
tls = await syncPreviewTls()
|
tls = await syncPreviewTls()
|
||||||
@@ -185,7 +253,7 @@ export async function runDeploy(repoFullName, branch, previewConfig) {
|
|||||||
tls = { synced: false, error: err.message }
|
tls = { synced: false, error: err.message }
|
||||||
}
|
}
|
||||||
|
|
||||||
return { deployed: true, subdomain, targetDir, tls }
|
return { deployed: true, subdomain, targetDir, tls, placeholder: index.placeholder }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user