diff --git a/server/routes/admin/gitea.js b/server/routes/admin/gitea.js index ad9b83c..b84bb57 100644 --- a/server/routes/admin/gitea.js +++ b/server/routes/admin/gitea.js @@ -1,4 +1,6 @@ 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 { requireAppwriteTeam } from '../../middleware/appwriteStaff.js' @@ -36,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 }) diff --git a/server/routes/admin/websiteProjects.js b/server/routes/admin/websiteProjects.js index c119c98..1930d35 100644 --- a/server/routes/admin/websiteProjects.js +++ b/server/routes/admin/websiteProjects.js @@ -41,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 }) diff --git a/server/routes/funktionen.js b/server/routes/funktionen.js index 604cd06..7c0694a 100644 --- a/server/routes/funktionen.js +++ b/server/routes/funktionen.js @@ -147,7 +147,7 @@ router.post('/news/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 }) // Pruefen, Sitzung anlegen und eintragen als Ganzes je (Kunde, Projekt): zwei schnelle Klicks duerfen keine // zwei Sitzungen erzeugen, sonst ueberschreibt die zweite die ID der ersten und deren Zahlung findet kein Dokument const session = await withLocks([`feature-order:${customerId}:${projectId}`], async () => { @@ -188,6 +188,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), diff --git a/server/services/siteFeatures.js b/server/services/siteFeatures.js index 27f4ec9..b8108c8 100644 --- a/server/services/siteFeatures.js +++ b/server/services/siteFeatures.js @@ -64,7 +64,8 @@ export async function getFeatureDoc(customerId, projectId, key) { } } -export async function assertOwnProject(customerId, projectId) { +/** forNewOrder: Neu bestellen/einschalten geht auf archivierten Projekten nicht mehr (Kunde sieht sie nicht). */ +export async function assertOwnProject(customerId, projectId, { forNewOrder = false } = {}) { let project = null try { project = await getDocument(config.collections.websiteProjects, projectId) @@ -72,6 +73,9 @@ export async function assertOwnProject(customerId, projectId) { if (err.status !== 404) throw err } if (!project || !customerId || project.customerId !== customerId) throw httpError(403, 'Kein Zugriff auf dieses Projekt') + if (forNewOrder && project.archived) { + throw httpError(409, 'Dieses Projekt ist archiviert - dafür kann nichts mehr bestellt oder eingeschaltet werden.') + } return project } @@ -131,15 +135,17 @@ export function listCustomerFeatureDocs(customerId) { /** * docs: bereits geladene Funktionsdokumente des Kunden (listCustomerFeatureDocs), damit sie nicht doppelt gelesen werden. - * includeArchived: nur fuer die Mitarbeiter-Ansicht - der Kunde sieht archivierte Projekte nicht. + * includeArchived: nur fuer die Mitarbeiter-Ansicht. Der Kunde sieht archivierte Projekte nur noch, solange dort + * eine Funktion bestellt/eingerichtet/aktiv ist - sonst kaeme er z. B. nicht mehr an seine Terminbuchungen. */ export async function listCustomerFeatures(customerId, { docs: geladen, includeArchived = false } = {}) { + const docs = geladen ?? (await listCustomerFeatureDocs(customerId)) + const laufend = new Set(docs.filter((d) => ['ordered', 'setup', 'active'].includes(d.state)).map((d) => d.projectId)) const projects = (await listDocuments(config.collections.websiteProjects, [ Query.equal('customerId', customerId), Query.orderDesc('$createdAt'), Query.limit(1000), - ])).filter((p) => includeArchived || !p.archived) - const docs = geladen ?? (await listCustomerFeatureDocs(customerId)) + ])).filter((p) => includeArchived || !p.archived || laufend.has(p.$id)) const result = [] for (const p of projects) { const features = {} diff --git a/test/rechtePrivatArchiv.test.js b/test/rechtePrivatArchiv.test.js index df01400..2d7b030 100644 --- a/test/rechtePrivatArchiv.test.js +++ b/test/rechtePrivatArchiv.test.js @@ -9,14 +9,20 @@ import { useTestEnv } from './support/testEnv.js' const aw = await startAppwriteMock() const st = await startFakeStripe() useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url }) -const { startTestApp, rawGet, get, sessionCookie } = await import('./support/testApp.js') +const { startTestApp, rawGet, get, send, sessionCookie } = await import('./support/testApp.js') const { default: projectsRouter } = await import('../server/routes/projects.js') const { default: servicesRouter } = await import('../server/routes/admin/services.js') const { default: customersRouter } = await import('../server/routes/admin/customers.js') +const { default: websiteProjectsRouter } = await import('../server/routes/admin/websiteProjects.js') +const { default: giteaRouter } = await import('../server/routes/admin/gitea.js') +const { default: funktionenRouter } = await import('../server/routes/funktionen.js') const app = await startTestApp({ '/api/projects': projectsRouter, '/api/admin/services': servicesRouter, '/api/admin/customers': customersRouter, + '/api/admin/website-projects': websiteProjectsRouter, + '/api/admin/gitea': giteaRouter, + '/api/funktionen': funktionenRouter, }) after(() => Promise.all([aw.close(), st.close(), app.close()])) @@ -58,3 +64,24 @@ test('Funktionen: Kunde sieht archivierte Projekte nicht, die Mitarbeiter-Ansich ['p_aktiv', 'p_archiv'] ) }) + +test('Funktionen: archiviertes Projekt mit laufender Funktion bleibt fuer den Kunden sichtbar (Buchungen verwalten)', async () => { + const { listCustomerFeatures } = await import('../server/services/siteFeatures.js') + const docs = [{ customerId: 'c1', projectId: 'p_archiv', featureKey: 'termine', state: 'active' }] + assert.deepEqual((await listCustomerFeatures('c1', { docs })).map((p) => p.projectId).sort(), ['p_aktiv', 'p_archiv']) + const aus = [{ customerId: 'c1', projectId: 'p_archiv', featureKey: 'termine', state: 'off' }] + assert.deepEqual((await listCustomerFeatures('c1', { docs: aus })).map((p) => p.projectId), ['p_aktiv']) +}) + +test('Neu bestellen auf archiviertem Projekt geht nicht', async () => { + const r = await send('POST', '/api/funktionen/news/checkout', { ...kunde, body: { projectId: 'p_archiv', consent: true } }) + assert.equal(r.status, 409) + assert.match(r.body.error, /archiviert/) +}) + +test('Vorlage bleibt Admin-Sache; README nur fuer bekannte Projekte', async () => { + const t = await send('PATCH', '/api/admin/website-projects/p_aktiv', { ...bearer('jwt-staff'), body: { projectType: 'template' } }) + assert.equal(t.status, 403) + const fremd = await rawGet('/api/admin/gitea/repo-readme?repoFullName=JUSN/tickte-system', bearer('jwt-staff')) + assert.equal(fremd.status, 404) +})