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

@@ -44,7 +44,7 @@ function renderProjectCard(project, features) {
<h2>${escapeHtml(project.projectName || project.subdomain || 'Projekt')}</h2>
<dl>
<dt>Subdomain</dt><dd>${escapeHtml(project.subdomain || '')}</dd>
<dt>Vorschau</dt><dd>${project.previewUrl ? `<a href="${escapeAttr(project.previewUrl)}" target="_blank" rel="noopener">${escapeHtml(project.previewUrl)}</a>` : ''}</dd>
<dt>Vorschau</dt><dd>${project.previewUrl ? `<a class="btn btn-green" href="${escapeAttr(project.previewUrl)}">Projekt-Webseite öffnen</a>` : ''}</dd>
<dt>Live-Domain</dt><dd>${project.liveDomain ? escapeHtml(project.liveDomain) : ''}</dd>
<dt>Status</dt><dd>${escapeHtml(project.status || '')}</dd>
<dt>Bereitstellung</dt><dd>${escapeHtml(project.provisioningStatus || '')}</dd>
@@ -66,11 +66,84 @@ function escapeAttr(str) {
return escapeHtml(str).replace(/'/g, '&#39;')
}
function getQueryParam(name) {
return new URLSearchParams(window.location.search).get(name) || ''
}
function safeRedirectUrl(url) {
try {
const u = new URL(url)
if (u.hostname.endsWith('.project.webklar.com') || u.hostname === 'project.webklar.com') {
return u.toString()
}
} catch {
/* ignore invalid URL */
}
return null
}
function isProjectPreviewReady(project) {
const status = (project?.provisioningStatus || project?.status || '').toLowerCase()
return ['ready', 'deployed'].includes(status) && Boolean(project?.previewUrl)
}
function previewSubdomainFromUrl(url) {
try {
const host = new URL(url).hostname.toLowerCase()
const match = host.match(/^([a-z0-9-]+)\.project\.webklar\.com$/)
return match ? match[1] : ''
} catch {
return ''
}
}
async function canAccessPreviewUrl(url) {
const subdomain = previewSubdomainFromUrl(url)
if (!subdomain) return false
try {
const access = await fetch(
`/api/auth/preview-access?subdomain=${encodeURIComponent(subdomain)}`,
{ credentials: 'same-origin' }
).then((r) => r.json())
return Boolean(access.allowed)
} catch {
return false
}
}
async function resolvePostLoginTarget() {
const next = safeRedirectUrl(getQueryParam('next'))
if (next && (await canAccessPreviewUrl(next))) {
return next
}
try {
const { projects } = await api('/api/projects')
const readyProjects = (projects || []).filter(isProjectPreviewReady)
if (readyProjects.length === 1) {
return readyProjects[0].previewUrl
}
} catch {
/* dashboard fallback */
}
return '/dashboard.html'
}
async function postLoginRedirect() {
window.location.href = await resolvePostLoginTarget()
}
async function initLoginPage() {
const form = document.getElementById('login-form')
const errorEl = document.getElementById('login-error')
const btn = document.getElementById('login-btn')
const errorFromQuery = getQueryParam('error')
if (errorFromQuery) {
showError(errorEl, errorFromQuery)
}
async function applyCooldown(seconds) {
if (seconds <= 0) return
showError(
@@ -106,7 +179,7 @@ async function initLoginPage() {
try {
const me = await api('/api/auth/me')
if (me.authenticated && me.customer) {
window.location.href = '/dashboard.html'
await postLoginRedirect()
return
}
} catch {
@@ -129,7 +202,7 @@ async function initLoginPage() {
password: document.getElementById('password').value,
}),
})
window.location.href = '/dashboard.html'
await postLoginRedirect()
return
} catch (err) {
showError(errorEl, err.message)