From 8627ed112450fa2d383a59141a08b4a46323fd6c Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 7 Oct 2026 14:08:37 +0000 Subject: [PATCH] =?UTF-8?q?Auftr=C3=A4ge:=20Router-Ger=C3=BCste=20und=20Ei?= =?UTF-8?q?nbindung=20in=20server/index.js=20und=20Team-Router?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- server/index.js | 10 +++ server/routes/admin/auftraege.js | 11 ++++ server/routes/admin/projektkonten.js | 11 ++++ server/routes/auftraegeKunde.js | 12 ++++ server/routes/auftrag.js | 10 +++ server/routes/team/arbeitsblaetter.js | 9 +++ server/routes/team/index.js | 3 + test/auftraegeGeruest.test.js | 92 +++++++++++++++++++++++++++ 8 files changed, 158 insertions(+) create mode 100644 server/routes/admin/auftraege.js create mode 100644 server/routes/admin/projektkonten.js create mode 100644 server/routes/auftraegeKunde.js create mode 100644 server/routes/auftrag.js create mode 100644 server/routes/team/arbeitsblaetter.js create mode 100644 test/auftraegeGeruest.test.js diff --git a/server/index.js b/server/index.js index 49a6335..907e7d9 100644 --- a/server/index.js +++ b/server/index.js @@ -21,6 +21,8 @@ import shopRoutes from './routes/shop.js' import shopWebhookRoutes from './routes/shopWebhook.js' import { shopSeiteRouter } from './routes/shopSeite.js' import zeitenRoutes from './routes/zeiten.js' +import publicAuftragRoutes from './routes/auftrag.js' +import kundeAuftraegeRoutes from './routes/auftraegeKunde.js' import takeoverCreditRoutes from './routes/takeoverCredits.js' import { purgeExpiredUploads } from './services/takeoverCredits.js' import { purgePendingChatUploads } from './services/chatAttachments.js' @@ -48,6 +50,8 @@ import publicZeitenRoutes from './routes/publicZeiten.js' import adminAnalyseRoutes from './routes/admin/analyse.js' import { legalRouter } from './services/legalDocs.js' import adminFinanzenRoutes from './routes/admin/finanzen.js' +import adminProjektkontenRoutes from './routes/admin/projektkonten.js' +import adminAuftraegeRoutes from './routes/admin/auftraege.js' import teamRoutes from './routes/team/index.js' import legacyAdminRedirect from './routes/legacyAdminRedirect.js' import { corsMiddleware } from './middleware/cors.js' @@ -94,6 +98,9 @@ app.use('/api/termine', termineRoutes) app.use('/api/shop/stripe-webhook', shopWebhookRoutes) app.use('/api/shop', shopRoutes) app.use('/api/zeiten', zeitenRoutes) +// Auftragsbestaetigung: oeffentliche Bestaetigung per Token (ohne Sitzung) und die Auftraege des angemeldeten Kunden +app.use('/api/auftrag', publicAuftragRoutes) +app.use('/api/auftraege', kundeAuftraegeRoutes) app.use('/api/team', teamRoutes) app.use('/api/admin/gitea', adminGiteaRoutes) app.use('/api/admin/website-projects', adminPreviewRoutes) @@ -103,6 +110,9 @@ app.use('/api/admin/customers', adminCustomersRoutes) app.use('/api/admin/employees', adminEmployeesRoutes) app.use('/api/admin/invoice-clients', adminInvoiceClientsRoutes) app.use('/api/admin/analyse', adminAnalyseRoutes) +// Projektkonten und Auftraege (Admin): vor dem Finanzen-Router, dessen requireAppwriteStaff sonst jede Anfrage darunter zuerst prueft +app.use('/api/admin/finanzen/projektkonten', adminProjektkontenRoutes) +app.use('/api/admin/finanzen/auftraege', adminAuftraegeRoutes) app.use('/api/admin/finanzen', adminFinanzenRoutes) app.use('/webhook', giteaWebhookRoutes) // Push-Zeitmail: Zeit eines Git-Pushes per Link aus der Mail eintragen (ohne Login, signierter Token) diff --git a/server/routes/admin/auftraege.js b/server/routes/admin/auftraege.js new file mode 100644 index 0000000..6ba6e83 --- /dev/null +++ b/server/routes/admin/auftraege.js @@ -0,0 +1,11 @@ +/** + * Gerüst von Strang L0: Auftragsbestätigungen (Admin). Mount: /api/admin/finanzen/auftraege (vor dem Finanzen-Router). + * L3 füllt: Liste, Anlegen, Ändern, Löschen, PDF, Projekt zuordnen, Senden, Erinnern, Neue Fassung, Stornieren. + */ +import { Router } from 'express' +import { requireAppwriteStaff } from '../../middleware/appwriteStaff.js' + +const router = Router() +router.use(requireAppwriteStaff) + +export default router diff --git a/server/routes/admin/projektkonten.js b/server/routes/admin/projektkonten.js new file mode 100644 index 0000000..2c51ca8 --- /dev/null +++ b/server/routes/admin/projektkonten.js @@ -0,0 +1,11 @@ +/** + * Gerüst von Strang L0: Projektkonten (Admin). Mount: /api/admin/finanzen/projektkonten (vor dem Finanzen-Router). + * L1 ergänzt GET / (?kunde=) und GET /:projektId. + */ +import { Router } from 'express' +import { requireAppwriteStaff } from '../../middleware/appwriteStaff.js' + +const router = Router() +router.use(requireAppwriteStaff) + +export default router diff --git a/server/routes/auftraegeKunde.js b/server/routes/auftraegeKunde.js new file mode 100644 index 0000000..11c7852 --- /dev/null +++ b/server/routes/auftraegeKunde.js @@ -0,0 +1,12 @@ +/** + * Gerüst von Strang L0: Aufträge des angemeldeten Kunden. Mount: /api/auftraege. requireSession schützt jede später + * ergänzte Route (fail-closed). L5 ergänzt GET /, GET /:id/pdf, POST /:id/bestaetigen; das Bestätigen sperrt der + * Wächter der Kundenansicht von selbst, es kommt kein Eintrag in KUNDENANSICHT_ALLOWED. + */ +import { Router } from 'express' +import { requireSession } from '../middleware/session.js' + +const router = Router() +router.use(requireSession) + +export default router diff --git a/server/routes/auftrag.js b/server/routes/auftrag.js new file mode 100644 index 0000000..68bcadc --- /dev/null +++ b/server/routes/auftrag.js @@ -0,0 +1,10 @@ +/** + * Gerüst von Strang L0: öffentliche Bestätigung einer Auftragsbestätigung per Token (ohne Anmeldung, das Token steht + * in der Adresse). Mount: /api/auftrag. Kein Sitzungs- und kein Wächter-Eintrag nötig. L5 ergänzt die Ratenbegrenzung + * im Arbeitsspeicher (keine IP-Adressen speichern) und GET /:token, GET /:token/pdf, POST /:token/bestaetigen. + */ +import { Router } from 'express' + +const router = Router() + +export default router diff --git a/server/routes/team/arbeitsblaetter.js b/server/routes/team/arbeitsblaetter.js new file mode 100644 index 0000000..675a291 --- /dev/null +++ b/server/routes/team/arbeitsblaetter.js @@ -0,0 +1,9 @@ +/** + * Gerüst von Strang L0: Arbeitsblätter fürs Ticketsystem. Mount: /api/team/arbeitsblaetter, in team/index.js hinter + * requireStaffJwt (jeder Mitarbeiter, daher kein eigener Schutz). L1 ergänzt GET /abgerechnet (?ticketId=). + */ +import { Router } from 'express' + +const router = Router() + +export default router diff --git a/server/routes/team/index.js b/server/routes/team/index.js index 5c803fc..b82d7dc 100644 --- a/server/routes/team/index.js +++ b/server/routes/team/index.js @@ -5,6 +5,7 @@ import { funktionenAdminRouter } from '../admin/funktionenAdmin.js' import { offersAdminRouter } from '../offers.js' import { storageTasksAdminRouter } from '../storageTasks.js' import { takeoverAdminRouter } from '../takeoverCredits.js' +import arbeitsblaetterTeamRoutes from './arbeitsblaetter.js' import customersRoutes from './customers.js' import filesRoutes from './files.js' import previewRoutes from './preview.js' @@ -28,5 +29,7 @@ router.use('/funktionen', funktionenAdminRouter) router.use('/termine', termineTeamRoutes) // Webshop: Stand je Projekt und Entscheidung ueber die Anfrage router.use('/shop', shopTeamRoutes) +// Abgerechnete Arbeitsblaetter (Abzeichen am Ticket, alle Mitarbeiter) +router.use('/arbeitsblaetter', arbeitsblaetterTeamRoutes) export default router diff --git a/test/auftraegeGeruest.test.js b/test/auftraegeGeruest.test.js new file mode 100644 index 0000000..c320917 --- /dev/null +++ b/test/auftraegeGeruest.test.js @@ -0,0 +1,92 @@ +// test/auftraegeGeruest.test.js +import { test, after } from 'node:test' +import assert from 'node:assert/strict' +import { readFileSync } from 'node:fs' +import { startAppwriteMock } from './support/appwriteMock.js' +import { useTestEnv } from './support/testEnv.js' + +// Gerüste der Router (L0): geprüft wird nur, was auch nach dem Füllen durch L1, L3 und L5 gilt: Zugangsschutz, unbekannte +// Tiefenpfade (…/a/b/c/d) und die Reihenfolge der Einbindung im Quelltext. +const aw = await startAppwriteMock() +useTestEnv({ appwriteUrl: aw.url, stripeUrl: 'http://127.0.0.1:1' }) +const { startTestApp, rawGet } = await import('./support/testApp.js') +const { seedFixtures, customerCookie } = await import('./support/fixtures.js') +const { default: projektkonten } = await import('../server/routes/admin/projektkonten.js') +const { default: auftraegeAdmin } = await import('../server/routes/admin/auftraege.js') +const { default: auftragOeffentlich } = await import('../server/routes/auftrag.js') +const { default: auftraegeKunde } = await import('../server/routes/auftraegeKunde.js') +const { default: teamRoutes } = await import('../server/routes/team/index.js') + +const app = await startTestApp({ + '/api/admin/finanzen/projektkonten': projektkonten, + '/api/admin/finanzen/auftraege': auftraegeAdmin, + '/api/team': teamRoutes, + '/api/auftrag': auftragOeffentlich, + '/api/auftraege': auftraegeKunde, +}) +after(() => Promise.all([app.close(), aw.close()])) +seedFixtures(aw) + +const bearer = (jwt) => ({ headers: { authorization: `Bearer ${jwt}` } }) +const quelle = (pfad) => readFileSync(new URL(`../${pfad}`, import.meta.url), 'utf8') + +test('Admin-Router: ohne JWT 401, Mitarbeiter ohne Admin-Label und Kunden 403, Admin kommt durch', async () => { + for (const pfad of ['/api/admin/finanzen/projektkonten/a/b/c/d', '/api/admin/finanzen/auftraege/a/b/c/d']) { + assert.equal((await rawGet(pfad)).status, 401, pfad) + assert.equal((await rawGet(pfad, bearer('jwt-staff'))).status, 403, `${pfad} jwt-staff`) + assert.equal((await rawGet(pfad, bearer('jwt-kunde'))).status, 403, `${pfad} jwt-kunde`) + assert.equal((await rawGet(pfad, bearer('jwt-admin'))).status, 404, `${pfad} jwt-admin: unbekannter Pfad`) + } +}) + +test('Team-Router: /arbeitsblaetter liegt hinter requireStaffJwt (jeder Mitarbeiter darf, Kunden nicht)', async () => { + const pfad = '/api/team/arbeitsblaetter/a/b/c/d' + assert.equal((await rawGet(pfad)).status, 401) + assert.equal((await rawGet(pfad, bearer('jwt-kunde'))).status, 403) + assert.equal((await rawGet(pfad, bearer('jwt-staff'))).status, 404, 'Mitarbeiter ohne Admin-Label kommen durch') + assert.equal((await rawGet('/api/team/arbeitsblaetter/abgerechnet?ticketId=t1')).status, 401) +}) + +test('Kunden-Router verlangt eine Anmeldung, der öffentliche Router nicht', async () => { + const ohne = await rawGet('/api/auftraege/a/b/c/d') + assert.equal(ohne.status, 401) + assert.deepEqual(ohne.body, { error: 'Nicht angemeldet' }) + assert.equal((await rawGet('/api/auftraege/a/b/c/d', { cookie: customerCookie('c1') })).status, 404) + for (const pfad of ['/api/auftrag/a/b/c/d', '/api/auftrag/a/b/c/d/pdf']) assert.equal((await rawGet(pfad)).status, 404, pfad) +}) + +test('server/index.js: Admin-Router vor dem Finanzen-Router, Kunden- und öffentlicher Router hinter Wächter und Parser', () => { + const index = quelle('server/index.js') + const stelle = (zeile) => { + const pos = index.indexOf(zeile) + assert.ok(pos >= 0, `${zeile} fehlt`) + assert.equal(index.indexOf(zeile, pos + 1), -1, `${zeile} steht mehrfach`) + return pos + } + const projekt = stelle("app.use('/api/admin/finanzen/projektkonten', adminProjektkontenRoutes)") + const auftraege = stelle("app.use('/api/admin/finanzen/auftraege', adminAuftraegeRoutes)") + const finanzen = stelle("app.use('/api/admin/finanzen', adminFinanzenRoutes)") + assert.ok(projekt < finanzen && auftraege < finanzen, 'sonst prüft der Finanzen-Router zuerst') + const waechter = stelle('app.use(kundenansichtGuard)') + const parser = stelle("app.use(express.json({ limit: '2mb'") + const oeffentlich = stelle("app.use('/api/auftrag', publicAuftragRoutes)") + const kunde = stelle("app.use('/api/auftraege', kundeAuftraegeRoutes)") + assert.ok(waechter < oeffentlich && waechter < kunde && parser < oeffentlich && parser < kunde) + for (const [name, datei] of [ + ['adminProjektkontenRoutes', 'admin/projektkonten'], + ['adminAuftraegeRoutes', 'admin/auftraege'], + ['publicAuftragRoutes', 'auftrag'], + ['kundeAuftraegeRoutes', 'auftraegeKunde'], + ]) { + assert.ok(index.includes(`import ${name} from './routes/${datei}.js'`), name) + } +}) + +test('server/routes/team/index.js: /arbeitsblaetter hängt hinter requireStaffJwt', () => { + const team = quelle('server/routes/team/index.js') + const jwt = team.indexOf('router.use(requireStaffJwt)') + const mount = team.indexOf("router.use('/arbeitsblaetter', arbeitsblaetterTeamRoutes)") + assert.ok(jwt >= 0 && mount > jwt) + assert.equal(team.indexOf("router.use('/arbeitsblaetter'", mount + 1), -1) + assert.ok(team.includes("import arbeitsblaetterTeamRoutes from './arbeitsblaetter.js'")) +})