Archiv/Rechte: Review-Punkte
- archiviertes Projekt bleibt unter Funktionen sichtbar, solange dort eine Funktion bestellt/eingerichtet/aktiv ist (z. B. Terminbuchungen) - auf archivierten Projekten nichts neu bestellen oder einschalten - Projekttyp Vorlage nur fuer Admins; repo-readme nur fuer bekannte Projekte Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3156bb790a
commit
4fc4c70b7d
5 files changed
+52
-6
No files matched your search
@@ -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 })
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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 = {}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
Reference in new issue
Block a user