feat: Website-Projekte bearbeiten (Name/Subdomain/Typ) + Archivierung

- updateWebsiteProject: synct Name/Subdomain/Typ nach Gitea (.webklar-preview.json,
  Repo-Beschreibung), Preview-Deploy (Redeploy+Teardown+TLS) und Appwrite
- Projekt-Typen: preview (Login), website (oeffentlich ohne Login), project (kein Hosting)
- syncPreviewTls erzeugt fuer isPublic-Projekte Router ohne preview-auth
- archiveRepo: Gitea archived + Appwrite-Flag + Preview-Teardown; Reaktivieren redeployt
- syncReposToAppwrite uebernimmt repo.archived
- Routen: PATCH /:id, POST /:id/archive, POST /:id/unarchive

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Claude
2026-07-12 00:21:50 +00:00
parent 8880b10b28
commit b474c4aa48
3 changed files with 305 additions and 4 deletions

View File

@@ -3,9 +3,29 @@ import { promisify } from 'node:util'
import fs from 'node:fs/promises'
import path from 'node:path'
import { config } from '../config.js'
import { listDocuments, Query } from './appwriteAdmin.js'
const execFileAsync = promisify(execFile)
/**
* Subdomains, die als "oeffentliche Website" (ohne Login) laufen sollen.
* Diese bekommen in syncPreviewTls einen Router OHNE preview-auth-ForwardAuth.
* Faellt bei Appwrite-Fehlern bewusst auf ein leeres Set zurueck -> alle Seiten
* bleiben dann login-geschuetzt (sicherer Default).
*/
async function getPublicSubdomains() {
try {
const docs = await listDocuments(config.collections.websiteProjects, [
Query.equal('isPublic', true),
Query.limit(500),
])
return new Set(docs.map((d) => d.subdomain).filter(Boolean))
} catch (err) {
console.error('[previewDeploy] getPublicSubdomains fehlgeschlagen:', err.message)
return new Set()
}
}
export async function fetchPreviewConfig(repoFullName, branch) {
const [owner, repo] = repoFullName.split('/')
if (!owner || !repo || !config.gitea.apiToken) {
@@ -93,17 +113,24 @@ export async function syncPreviewTls() {
.map((entry) => entry.name)
.sort()
const publicSet = await getPublicSubdomains()
const blocks = slugs.map((name) => {
const host = `${name}.${baseHost}`
const id = `preview-${name}`
const isPublic = publicSet.has(name)
// Oeffentliche Website: HTTPS-Router OHNE preview-auth -> frei zugaenglich.
// Preview (Standard): mit preview-chain (ForwardAuth) -> login-geschuetzt.
const httpsMiddlewareLines = isPublic
? []
: [' middlewares:', ' - preview-chain@docker']
return [
` ${id}-https:`,
` rule: "Host(\`${host}\`)"`,
' entryPoints:',
' - websecure',
' service: preview-nginx@docker',
' middlewares:',
' - preview-chain@docker',
...httpsMiddlewareLines,
' priority: 100',
' tls:',
' certResolver: letsencrypt',
@@ -160,3 +187,23 @@ export async function runDeploy(repoFullName, branch, previewConfig) {
return { deployed: true, subdomain, targetDir, tls }
}
/**
* Entfernt das Deploy-Verzeichnis einer Subdomain und synchronisiert die
* Traefik-TLS-Router neu (z.B. beim Aendern der Subdomain oder Archivieren).
*/
export async function removeDeploy(subdomain) {
if (!subdomain || !config.preview.deployRoot) return { removed: false }
if (!/^[a-z0-9-]+$/.test(subdomain)) return { removed: false, reason: 'ungueltige Subdomain' }
const targetDir = path.join(config.preview.deployRoot, subdomain)
await fs.rm(targetDir, { recursive: true, force: true })
let tls = { synced: false }
try {
tls = await syncPreviewTls()
} catch (err) {
console.error('[previewDeploy] TLS-Sync nach removeDeploy fehlgeschlagen:', err.message)
tls = { synced: false, error: err.message }
}
return { removed: true, subdomain, tls }
}