Merge origin/main in feature/terminplaner-tp2 (Analyse-Cockpit, Subdomain-Sperre, Projekte)
Konflikte: funktionen.js folgt der Aufteilung aus main (fn-util.js, analyse.js); der Link
'Zu Verträge' der Analyse-Seite kommt jetzt auch in analyse.js aus vertraege-link.js.
Checkout und Team-Ansicht übernehmen assertOwnProject(..., { forNewOrder: true }) und
includeArchived aus main. Test-Pins der absoluten Importe nachgezogen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
af63b69ec1
56 files changed
+4583
-717
No files matched your search
@@ -2,12 +2,13 @@
|
||||
import { Router } from 'express'
|
||||
import { config } from '../../config.js'
|
||||
import { getDocument } from '../../services/appwriteAdmin.js'
|
||||
import { requireAppwriteStaff } from '../../middleware/appwriteStaff.js'
|
||||
import { requireAppwriteTeam } from '../../middleware/appwriteStaff.js'
|
||||
import { activateAnalyse, analyseEligibility, controllerView, deactivateAnalyse, ELIGIBILITY_MESSAGES, validateTextform } from '../../services/analyse.js'
|
||||
import { getFeatureDoc } from '../../services/siteFeatures.js'
|
||||
|
||||
const router = Router()
|
||||
router.use(requireAppwriteStaff)
|
||||
// Projekt-Aktionen: jeder Mitarbeiter (nicht nur Admins)
|
||||
router.use(requireAppwriteTeam)
|
||||
|
||||
function sendError(res, err) {
|
||||
const status = [400, 403, 404, 409, 503].includes(err.status) && !err.type ? err.status : 500
|
||||
|
||||
@@ -121,7 +121,7 @@ async function unterlagenAlsAnhang() {
|
||||
// je Anfrage einmal gelesen (ein gemeinsamer aboMemo fuer alle Projekte des Kunden), portalFeatures nicht erneut.
|
||||
async function adminView(customerId) {
|
||||
const docs = await listCustomerFeatureDocs(customerId)
|
||||
const projects = await listCustomerFeatures(customerId, { docs })
|
||||
const projects = await listCustomerFeatures(customerId, { docs, includeArchived: true })
|
||||
const abo = aboMemo()
|
||||
for (const p of projects) {
|
||||
for (const key of FEATURE_KEYS) {
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { Router } from 'express'
|
||||
import { config } from '../../config.js'
|
||||
import { listDocuments, Query } from '../../services/appwriteAdmin.js'
|
||||
import { syncReposToAppwrite, fetchRepoReadme, importNewRepos } from '../../services/giteaAdmin.js'
|
||||
import { requireAppwriteStaff } from '../../middleware/appwriteStaff.js'
|
||||
import { requireAppwriteTeam } from '../../middleware/appwriteStaff.js'
|
||||
|
||||
const router = Router()
|
||||
|
||||
router.use(requireAppwriteStaff)
|
||||
// Projekt-Aktionen: jeder Mitarbeiter (nicht nur Admins)
|
||||
router.use(requireAppwriteTeam)
|
||||
|
||||
router.post('/sync-repos', async (_req, res) => {
|
||||
try {
|
||||
@@ -35,6 +38,12 @@ router.get('/repo-readme', async (req, res) => {
|
||||
}
|
||||
|
||||
try {
|
||||
// Nur Repos, die ein Projekt sind - der System-Schluessel sieht sonst jedes (auch private) Repo
|
||||
const known = await listDocuments(config.collections.websiteProjects, [
|
||||
Query.equal('repoFullName', repoFullName),
|
||||
Query.limit(1),
|
||||
])
|
||||
if (!known[0]) return res.status(404).json({ error: 'Projekt nicht gefunden' })
|
||||
const readme = await fetchRepoReadme(repoFullName)
|
||||
if (!readme) {
|
||||
return res.json({ ok: true, found: false, repoFullName })
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { Router } from 'express'
|
||||
import { config } from '../../config.js'
|
||||
import { getDocument, updateDocument } from '../../services/appwriteAdmin.js'
|
||||
import { requireAppwriteStaff } from '../../middleware/appwriteStaff.js'
|
||||
import { requireAppwriteTeam } from '../../middleware/appwriteStaff.js'
|
||||
import { envKey, encryptEnv, decryptEnv, envNames, validateEnvChange } from '../../services/previewEnv.js'
|
||||
import { listRunLogs, readRunLog, isRunFile } from '../../services/previewLogs.js'
|
||||
import * as runnerClient from '../../services/previewRunnerClient.js'
|
||||
@@ -11,7 +11,8 @@ import { withLocks } from '../../services/keyedLock.js'
|
||||
import { repoHasDevScript } from '../../services/previewDevScript.js'
|
||||
|
||||
const router = Router()
|
||||
router.use(requireAppwriteStaff)
|
||||
// Projekt-Aktionen: jeder Mitarbeiter (nicht nur Admins)
|
||||
router.use(requireAppwriteTeam)
|
||||
|
||||
const BUSY = 'Server ausgelastet, versuche es gleich noch mal'
|
||||
const MODES = ['auto', 'build', 'dev', 'off']
|
||||
|
||||
@@ -2,11 +2,12 @@ import { Router } from 'express'
|
||||
import { config } from '../../config.js'
|
||||
import { listDocuments, Query } from '../../services/appwriteAdmin.js'
|
||||
import { servicesCatalog } from '../../services/projectKinds.js'
|
||||
import { requireAppwriteStaff } from '../../middleware/appwriteStaff.js'
|
||||
import { requireAppwriteTeam } from '../../middleware/appwriteStaff.js'
|
||||
|
||||
const router = Router()
|
||||
|
||||
router.use(requireAppwriteStaff)
|
||||
// Projekt-Aktionen: jeder Mitarbeiter (nicht nur Admins)
|
||||
router.use(requireAppwriteTeam)
|
||||
|
||||
const CHUNK = 100
|
||||
|
||||
|
||||
@@ -6,14 +6,15 @@ import {
|
||||
} from '../../services/giteaAdmin.js'
|
||||
import { assignProject, reconcileProject } from '../../services/projectAssignment.js'
|
||||
import { completeAssignment } from '../../services/projectKindAssign.js'
|
||||
import { requireAppwriteStaff } from '../../middleware/appwriteStaff.js'
|
||||
import { requireAppwriteTeam } from '../../middleware/appwriteStaff.js'
|
||||
|
||||
const router = Router()
|
||||
|
||||
router.use(requireAppwriteStaff)
|
||||
// Projekt-Aktionen: jeder Mitarbeiter (nicht nur Admins)
|
||||
router.use(requireAppwriteTeam)
|
||||
|
||||
router.post('/create-from-template', async (req, res) => {
|
||||
const { repoName, displayName, subdomain, customerId = '', ticketId = '', owner } = req.body || {}
|
||||
const { repoName, displayName, subdomain, customerId = '', ticketId = '', owner, kind = '' } = req.body || {}
|
||||
|
||||
if (!repoName && !displayName) {
|
||||
return res.status(400).json({ error: 'repoName oder displayName ist erforderlich' })
|
||||
@@ -27,6 +28,8 @@ router.post('/create-from-template', async (req, res) => {
|
||||
customerId,
|
||||
ticketId,
|
||||
owner,
|
||||
kind,
|
||||
staffName: req.appwriteUser?.name || req.appwriteUser?.email || '',
|
||||
})
|
||||
return res.status(201).json({ ok: true, ...result })
|
||||
} catch (err) {
|
||||
@@ -38,6 +41,10 @@ router.post('/create-from-template', async (req, res) => {
|
||||
// Projekt bearbeiten: Name / Subdomain / Typ -> synchronisiert Git + Preview + Appwrite
|
||||
router.patch('/:id', async (req, res) => {
|
||||
const { projectName, subdomain, projectType } = req.body || {}
|
||||
// Vorlagen bleiben Admin-Sache (im Ticketsystem nur fuer Admins sichtbar)
|
||||
if (projectType === 'template' && !req.appwriteUser?.labels?.includes('admin')) {
|
||||
return res.status(403).json({ error: 'Nur Admins können Vorlagen anlegen.' })
|
||||
}
|
||||
try {
|
||||
const result = await updateWebsiteProject(req.params.id, { projectName, subdomain, projectType })
|
||||
return res.json({ ok: true, ...result })
|
||||
|
||||
@@ -292,7 +292,8 @@ router.get('/preview-access', async (req, res) => {
|
||||
|
||||
const projects = await listDocuments(config.collections.websiteProjects, queries)
|
||||
const project = projects[0]
|
||||
if (!project) {
|
||||
// Archiviert: fuer Kunden wie kein Projekt (das Portal blendet es aus)
|
||||
if (!project || (project.archived && !admin)) {
|
||||
return res.json({ allowed: false, reason: 'no_project' })
|
||||
}
|
||||
if (!isPreviewProjectReady(project)) {
|
||||
|
||||
@@ -76,6 +76,7 @@ async function analyseView(customerId, projectId) {
|
||||
avvVersion: config.analyse.avvVersion,
|
||||
legal: legalPaths(config.analyse.avvVersion),
|
||||
controller,
|
||||
activatedAt: doc?.state === 'active' ? doc.activatedAt || null : null,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -258,7 +259,7 @@ router.post('/:key/checkout', requireSession, async (req, res) => {
|
||||
return res.status(400).json({ error: 'Bitte wähle ein Projekt.' })
|
||||
}
|
||||
const customerId = getSessionCustomerId(req)
|
||||
await assertOwnProject(customerId, projectId)
|
||||
await assertOwnProject(customerId, projectId, { forNewOrder: true })
|
||||
if (fortsetzen && (await getFeatureDoc(customerId, projectId, key))?.state !== 'ordered') {
|
||||
throw bestellFehler(400, ZUSTIMMUNG_NOETIG, 'zustimmung_fehlt')
|
||||
}
|
||||
@@ -342,6 +343,7 @@ router.post('/analyse/activate', analyseGuard, async (req, res) => {
|
||||
return res.status(403).json({ error: 'In der Kundenansicht kann nichts geändert werden.' })
|
||||
}
|
||||
const { projectId, accentColor, consent } = req.body || {}
|
||||
await assertOwnProject(getSessionCustomerId(req), analyseProjectId(projectId), { forNewOrder: true })
|
||||
const { doc } = await activateAnalyse({
|
||||
customerId: getSessionCustomerId(req),
|
||||
projectId: analyseProjectId(projectId),
|
||||
|
||||
@@ -14,7 +14,8 @@ const router = Router()
|
||||
async function loadOwnProject(req, res, next) {
|
||||
const customerId = getSessionCustomerId(req)
|
||||
const project = await getDocument(config.collections.websiteProjects, req.params.id).catch(() => null)
|
||||
if (!project || !customerId || project.customerId !== customerId) {
|
||||
// Archivierte Projekte sieht der Kunde nicht mehr (wie in der Liste)
|
||||
if (!project || !customerId || project.customerId !== customerId || project.archived) {
|
||||
return res.status(404).json({ error: 'Projekt nicht gefunden' })
|
||||
}
|
||||
req.project = project
|
||||
@@ -31,9 +32,11 @@ router.get('/', requireSession, async (req, res) => {
|
||||
const projects = await listDocuments(config.collections.websiteProjects, [
|
||||
Query.equal('customerId', customerId),
|
||||
Query.orderDesc('$createdAt'),
|
||||
Query.limit(500), // ohne Limit liefert Appwrite nur 25
|
||||
])
|
||||
|
||||
const sanitized = projects.map((p) => ({
|
||||
// Archivierte Projekte (Vorschau entfernt) blendet das Portal aus (Entscheidung 2026-10-04)
|
||||
const sanitized = projects.filter((p) => !p.archived).map((p) => ({
|
||||
id: p.$id,
|
||||
projectName: p.projectName || '',
|
||||
subdomain: p.subdomain || '',
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import crypto from 'node:crypto'
|
||||
import { Router } from 'express'
|
||||
import { config } from '../../config.js'
|
||||
import { upsertWebsiteProjectByRepo, getDocument } from '../../services/appwriteAdmin.js'
|
||||
import { upsertWebsiteProjectByRepo, getDocument, listDocuments, Query } from '../../services/appwriteAdmin.js'
|
||||
import {
|
||||
fetchPreviewConfig,
|
||||
defaultPreviewConfig,
|
||||
runDeploy,
|
||||
foreignSubdomainBlock,
|
||||
deployKeepsStatus,
|
||||
buildPreviewUrl,
|
||||
} from '../../services/previewDeploy.js'
|
||||
import { ensureRepoPreviewConfig } from '../../services/giteaAdmin.js'
|
||||
import { createGitPushWorksheet } from '../../services/gitWorksheet.js'
|
||||
import { createGitPushWorksheet, isSystemPusher } from '../../services/gitWorksheet.js'
|
||||
import { sendPushTimeMail } from '../../services/pushTimeMail.js'
|
||||
import { ensureProjectTicket } from '../../services/ticketAdmin.js'
|
||||
import { invalidateProjectCache } from '../../services/projectInsights.js'
|
||||
@@ -107,18 +108,24 @@ router.post('/gitea', async (req, res) => {
|
||||
}
|
||||
}
|
||||
const previewConfig = previewConfigFile || defaultPreviewConfig(name)
|
||||
const hasPreview = Boolean(previewConfigFile?.enabled !== false && previewConfigFile)
|
||||
const wantsPreview = Boolean(previewConfigFile?.enabled !== false && previewConfigFile)
|
||||
// Kopiertes Repo mit der .webklar-preview.json eines anderen Projekts: dessen Subdomain weder
|
||||
// beliefern noch uebernehmen - das Projekt bleibt ohne Preview, bis WOMS ihm eine eigene gibt.
|
||||
const blocked = wantsPreview
|
||||
? await foreignSubdomainBlock(name, previewConfig.subdomain || name.split('/').pop())
|
||||
: null
|
||||
const hasPreview = wantsPreview && !blocked
|
||||
// Nur Projekte mit aktivierter Preview bekommen eine Subdomain zugewiesen -
|
||||
// Repo-only-Projekte (enabled:false, keine Subdomain) bleiben ohne Website.
|
||||
const subdomain = hasPreview ? previewConfig.subdomain || name.split('/').pop() : ''
|
||||
const previewUrl = subdomain ? buildPreviewUrl(subdomain) : ''
|
||||
|
||||
let deployResult = { deployed: false, skipped: true }
|
||||
let deployResult = blocked || { deployed: false, skipped: true }
|
||||
const configBranch = previewConfig.branch || 'main'
|
||||
|
||||
if (hasPreview && previewConfig.enabled !== false && branch === configBranch) {
|
||||
try {
|
||||
deployResult = await runDeploy(name, branch, previewConfig, { sha: commitSha })
|
||||
deployResult = await runDeploy(name, branch, previewConfig, { sha: commitSha, subdomainChecked: true })
|
||||
} catch (deployErr) {
|
||||
console.error('[webhook] deploy failed:', deployErr.message)
|
||||
deployResult = { deployed: false, error: deployErr.message }
|
||||
@@ -141,7 +148,8 @@ router.post('/gitea', async (req, res) => {
|
||||
const payloadNameStillCurrent = payloadName.toLowerCase() === current.toLowerCase()
|
||||
|
||||
const projectPayload = {
|
||||
projectName: previewConfig.displayName || (payloadNameStillCurrent ? repo.name : '') || subdomain,
|
||||
// der Anzeigename einer fremden Config gehoert ebenfalls dem Original
|
||||
projectName: (blocked ? '' : previewConfig.displayName) || (payloadNameStillCurrent ? repo.name : '') || subdomain,
|
||||
templateName: previewConfig.templateName || config.gitea.templateRepo,
|
||||
giteaRepoUrl: payloadNameStillCurrent
|
||||
? repo.html_url || `${config.gitea.baseUrl}/${current}`
|
||||
@@ -161,6 +169,20 @@ router.post('/gitea', async (req, res) => {
|
||||
} else if (hasPreview) {
|
||||
projectPayload.status = 'pending'
|
||||
projectPayload.provisioningStatus = deployResult.error ? 'deploy_failed' : 'pending'
|
||||
} else if (blocked) {
|
||||
// eine schon vergebene eigene Preview bleibt, wie sie ist - sonst Projekt ohne Preview;
|
||||
// ein vorhandener Projektname auch (der Repo-Name waere nur der Ersatz fuer neue Projekte)
|
||||
const existing = (await listDocuments(config.collections.websiteProjects, [
|
||||
Query.equal('repoFullName', current),
|
||||
Query.limit(1),
|
||||
]))[0]
|
||||
if (!existing?.subdomain) projectPayload.status = 'synced'
|
||||
else {
|
||||
// sonst stuende im WOMS weiter der alte Bau als aktuell da (Fehlertext nur bei 'failed')
|
||||
projectPayload.previewState = 'failed'
|
||||
projectPayload.previewError = `Neuer Stand nicht veröffentlicht: In .webklar-preview.json steht die Subdomain „${blocked.subdomain}“, die gehört ${blocked.owner}. Projekt im WOMS bearbeiten und speichern, dann stimmt die Datei wieder.`.slice(0, 500)
|
||||
}
|
||||
if (existing?.projectName) delete projectPayload.projectName
|
||||
} else {
|
||||
projectPayload.status = 'synced'
|
||||
}
|
||||
@@ -177,7 +199,10 @@ router.post('/gitea', async (req, res) => {
|
||||
// (derselben wie assignProject/reconcileProject), damit ein gleichzeitiges Zuweisen nicht
|
||||
// dieselbe ticketId-Aufloesung doppelt anstoesst.
|
||||
let gitWorksheet = null
|
||||
if (commitSha && commitSha !== '0000000000000000000000000000000000000000') {
|
||||
// System-Commits (z. B. .webklar-preview.json beim Anlegen aus dem Ticketsystem) bekommen weder
|
||||
// Arbeitsblatt noch Auto-Ticket - sonst entstand beim Anlegen mit Kunde ein zusaetzliches
|
||||
// Ticket "Website <name>" neben dem, das im Zuordnen-Assistenten gewaehlt wird.
|
||||
if (commitSha && commitSha !== '0000000000000000000000000000000000000000' && !isSystemPusher(pusher)) {
|
||||
gitWorksheet = await withLocks(
|
||||
['project:' + project.$id],
|
||||
async () => {
|
||||
|
||||
Reference in new issue
Block a user