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:
2026-07-11 16:24:53 +00:00
parent cff72eb528
commit cf4b3ce4d1
24 changed files with 1883 additions and 123 deletions

View 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

View 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

View 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

View 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

View 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

View 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