From c37dd49b05d71720df4140b282e51cc0e473cf6c Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 30 Sep 2026 23:01:17 +0000 Subject: [PATCH] Preview: Bereitschaft aus echtem Zustand statt hasPreview (Standardwert false sperrte Previews) Co-Authored-By: Claude Sonnet 5.5 --- server/routes/admin/previewOpen.js | 8 +--- server/routes/auth.js | 8 +--- server/services/previewReadiness.js | 16 +++++++ test/previewReadiness.test.js | 69 +++++++++++++++++++++++++++++ 4 files changed, 87 insertions(+), 14 deletions(-) create mode 100644 server/services/previewReadiness.js create mode 100644 test/previewReadiness.test.js diff --git a/server/routes/admin/previewOpen.js b/server/routes/admin/previewOpen.js index 47ada10..24286bb 100644 --- a/server/routes/admin/previewOpen.js +++ b/server/routes/admin/previewOpen.js @@ -2,6 +2,7 @@ import { Router } from 'express' import { config } from '../../config.js' import { requireAppwriteStaff } from '../../middleware/appwriteStaff.js' import { listDocuments, Query } from '../../services/appwriteAdmin.js' +import { isPreviewProjectReady } from '../../services/previewReadiness.js' import { createStaffPreviewToken } from '../../services/staffPreviewTokens.js' const router = Router() @@ -10,13 +11,6 @@ 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 || '')) diff --git a/server/routes/auth.js b/server/routes/auth.js index f30924f..12597dd 100644 --- a/server/routes/auth.js +++ b/server/routes/auth.js @@ -26,6 +26,7 @@ import { previewSessionCookieOptions, verifyPreviewSessionToken, } from '../services/previewSession.js' +import { isPreviewProjectReady } from '../services/previewReadiness.js' import { consumeStaffPreviewToken } from '../services/staffPreviewTokens.js' import { resolveStaffAccount } from '../services/staffAccount.js' import { MFA_COOKIE, MFA_TTL_MS, resendStaffMfa, startStaffMfa, verifyStaffMfa } from '../services/staffMfa.js' @@ -120,13 +121,6 @@ async function resolveSessionCustomer(session) { return customer } -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 isAdminSession(session) { return session?.role === 'admin' || (session?.labels || []).includes('admin') } diff --git a/server/services/previewReadiness.js b/server/services/previewReadiness.js new file mode 100644 index 0000000..841ba47 --- /dev/null +++ b/server/services/previewReadiness.js @@ -0,0 +1,16 @@ +/** + * Ist die Vorschau eines Projekts freigabebereit? Wird von /api/auth/preview-access und + * dem Staff-Preview-Open genutzt. + * + * `hasPreview` wird bewusst NICHT mehr ausgewertet: Das Appwrite-Attribut wurde am 22.07.2026 + * mit Standardwert false angelegt und wird seit Commit 617d463 nicht mehr gepflegt, daher + * waere jedes neuere Projekt faelschlich gesperrt. Massgeblich ist der echte Zustand. + */ +export function isPreviewProjectReady(project) { + if (!project) return false + if (!String(project.subdomain || '').trim()) return false + if (project.previewMode === 'off') return false + if (project.projectType === 'repo' || project.projectType === 'project') return false + const status = String(project.provisioningStatus || project.status || '').toLowerCase() + return ['ready', 'deployed'].includes(status) +} diff --git a/test/previewReadiness.test.js b/test/previewReadiness.test.js new file mode 100644 index 0000000..949ad8c --- /dev/null +++ b/test/previewReadiness.test.js @@ -0,0 +1,69 @@ +// Preview-Bereitschaft: echter Zustand statt hasPreview (Standardwert false sperrte Previews) +import { test, before, after } from 'node:test' +import assert from 'node:assert/strict' +import express from 'express' +import cookieParser from 'cookie-parser' +import { startFakeAppwrite } from './helpers/fakeAppwrite.js' +import { isPreviewProjectReady } from '../server/services/previewReadiness.js' + +const base = { subdomain: 'kunde-a', projectType: 'website', provisioningStatus: 'deployed', hasPreview: false } + +test('hasPreview false + deployed/ready + Subdomain => bereit', () => { + assert.equal(isPreviewProjectReady(base), true) + assert.equal(isPreviewProjectReady({ ...base, provisioningStatus: undefined, status: 'Ready' }), true) +}) +test('kein Projekt / keine Subdomain => nicht bereit', () => { + assert.equal(isPreviewProjectReady(null), false) + assert.equal(isPreviewProjectReady({ ...base, subdomain: '' }), false) + assert.equal(isPreviewProjectReady({ ...base, subdomain: ' ' }), false) +}) +test('previewMode off => nicht bereit', () => { + assert.equal(isPreviewProjectReady({ ...base, previewMode: 'off' }), false) + assert.equal(isPreviewProjectReady({ ...base, previewMode: 'dev' }), true) +}) +test('projectType repo/project => nicht bereit', () => { + assert.equal(isPreviewProjectReady({ ...base, projectType: 'repo' }), false) + assert.equal(isPreviewProjectReady({ ...base, projectType: 'project' }), false) +}) +test('Status pending/leer => nicht bereit', () => { + assert.equal(isPreviewProjectReady({ ...base, provisioningStatus: 'pending' }), false) + assert.equal(isPreviewProjectReady({ ...base, provisioningStatus: '', status: '' }), false) +}) + +let aw, server, baseUrl, token +before(async () => { + aw = await startFakeAppwrite({ + seed: { websiteProjects: [{ $id: 'p1', customerId: 'c1', ...base }] }, + }) + Object.assign(process.env, { APPWRITE_ENDPOINT: aw.endpoint, APPWRITE_API_KEY: 'test-key', PREVIEW_SESSION_SECRET: 'x'.repeat(40) }) + const { createPreviewSessionToken } = await import('../server/services/previewSession.js') + token = { + c1: await createPreviewSessionToken({ userId: 'u1', customerId: 'c1', email: 'a@b.de' }), + c2: await createPreviewSessionToken({ userId: 'u2', customerId: 'c2', email: 'c@d.de' }), + } + const { default: auth } = await import('../server/routes/auth.js') + const app = express() + app.use(cookieParser()) + app.use('/api/auth', auth) + server = await new Promise((r) => { const s = app.listen(0, '127.0.0.1', () => r(s)) }) + baseUrl = `http://127.0.0.1:${server.address().port}` +}) +after(async () => { + server.closeAllConnections() + await new Promise((r) => server.close(r)) + await aw.close() +}) + +const access = async (t) => { + const res = await fetch(`${baseUrl}/api/auth/preview-access?subdomain=kunde-a`, { headers: { Cookie: `preview_session=${t}` } }) + return res.json() +} + +test('Route: eigener Kunde, hasPreview false, deployed => allowed', async () => { + assert.equal((await access(token.c1)).allowed, true) +}) +test('Route: anderer Kunde bleibt ausgesperrt', async () => { + const r = await access(token.c2) + assert.equal(r.allowed, false) + assert.equal(r.reason, 'no_project') +})