Aufträge: Router-Gerüste und Einbindung in server/index.js und Team-Router

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-07 14:08:37 +00:00
1 parent 4f467fa9c5
commit 8627ed1124
8 files changed
+158

No files matched your search

+10
View File
@@ -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)
+11
View File
@@ -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
+11
View File
@@ -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=<id>) und GET /:projektId.
*/
import { Router } from 'express'
import { requireAppwriteStaff } from '../../middleware/appwriteStaff.js'
const router = Router()
router.use(requireAppwriteStaff)
export default router
+12
View File
@@ -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
+10
View File
@@ -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
+9
View File
@@ -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=<id>).
*/
import { Router } from 'express'
const router = Router()
export default router
+3
View File
@@ -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
+92
View File
@@ -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'"))
})