Finanzen: Portal-Client mit deutschen Fehlertexten fuer Proxy-Fehler und abgelaufene Anmeldung

Der Anfrage-Kern liegt jetzt in financeRequest.js (rein, fetch und createJWT werden
injiziert, unter Node testbar). 502/503/504 ohne JSON-Antwort und Netzfehler melden
"Das Kundenportal ist gerade nicht erreichbar."; scheitert createJWT, kommt
"Anmeldung abgelaufen. Bitte neu anmelden." (status 401). Meldungen des Portals bleiben.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-01 15:21:53 +00:00
1 parent e9cfae28ee
commit 1e72faf97c
3 files changed
+197 -35

No files matched your search

+3 -35
View File
@@ -1,47 +1,15 @@
import { account } from './appwrite'
import { createJwtCache, fetchWithRetry } from './serverpulsAuth'
import { createFinanceRequest } from './financeRequest'
/**
* Client fuer die Finanzen-API des Kundenportals (/api/admin/finanzen).
* Authentifiziert per Appwrite-JWT (Bearer); alle Betraege in Cent.
* Authentifiziert per Appwrite-JWT (Bearer); alle Betraege in Cent. Anfrage-Kern und Fehlertexte: financeRequest.js.
*/
const PROJECT_ADMIN_URL =
import.meta.env.VITE_PROJECT_ADMIN_URL || 'https://project.webklar.com'
const BASE = `${PROJECT_ADMIN_URL}/api/admin/finanzen`
// JWT 10 Min. gecacht (Appwrite begrenzt das Erzeugen pro Nutzer)
const jwtCache = createJwtCache({ createJWT: () => account.createJWT() })
// Netzwerkfehler (fetch wirft) bekommen eine deutsche Meldung mit status 0
async function portalFetch(url, init) {
try {
return await fetch(url, init)
} catch {
const err = new Error('Das Kundenportal ist gerade nicht erreichbar.')
err.status = 0
throw err
}
}
async function request(path, { method = 'GET', body } = {}) {
const res = await fetchWithRetry(jwtCache, (jwt) =>
portalFetch(`${BASE}${path}`, {
method,
headers: { Authorization: `Bearer ${jwt}`, 'Content-Type': 'application/json' },
body: body === undefined ? undefined : JSON.stringify(body),
})
)
const data = await res.json().catch(() => ({}))
if (!res.ok) {
const err = new Error(data.error || `API-Fehler ${res.status}`)
err.status = res.status
// Nach dem Festschreiben (502) liegt die Rechnung schon als "offen" vor
if (data.invoiceId) err.invoiceId = data.invoiceId
if (data.invoiceNumber) err.invoiceNumber = data.invoiceNumber
throw err
}
return data
}
const request = createFinanceRequest({ baseUrl: BASE, createJWT: () => account.createJWT() })
const enc = encodeURIComponent
+67
View File
@@ -0,0 +1,67 @@
import { createJwtCache, fetchWithRetry } from './serverpulsAuth'
// Anfrage-Kern der Finanzen-API (Portal). Rein (ohne Appwrite-Import, fetch und createJWT werden injiziert), damit die
// Fehlertexte unter Node testbar sind; financeApi.js verdrahtet ihn mit Appwrite.
export const PORTAL_DOWN_TEXT = 'Das Kundenportal ist gerade nicht erreichbar.'
export const SESSION_EXPIRED_TEXT = 'Anmeldung abgelaufen. Bitte neu anmelden.'
const statusError = (message, status) => Object.assign(new Error(message), { status })
/**
* request(path, { method, body }) -> geparstes JSON oder ein Error mit `status` (und bei 502 nach dem Festschreiben
* `invoiceId` / `invoiceNumber`). Meldungen:
* - fetch wirft (Netz, Portal aus): „Das Kundenportal ist gerade nicht erreichbar.“ (status 0)
* - 502/503/504 ohne JSON-Antwort (Proxy-Fehlerseite): dieselbe Meldung (status = HTTP-Status)
* - createJWT scheitert (Sitzung abgelaufen): „Anmeldung abgelaufen. Bitte neu anmelden.“ (status 401)
* - sonst die Meldung des Portals (`error`), ohne sie „API-Fehler <Status>“
* Das JWT wird 10 Minuten gecacht; bei 401 einmal mit frischem JWT wiederholt.
*/
export function createFinanceRequest({ baseUrl, createJWT, fetchImpl = (...args) => globalThis.fetch(...args), now }) {
// JWT 10 Min. gecacht (Appwrite begrenzt das Erzeugen pro Nutzer)
const jwtCache = createJwtCache({
createJWT: async () => {
try {
return await createJWT()
} catch {
throw statusError(SESSION_EXPIRED_TEXT, 401)
}
},
...(now ? { now } : {}),
})
// Netzwerkfehler (fetch wirft) bekommen eine deutsche Meldung mit status 0
async function portalFetch(url, init) {
try {
return await fetchImpl(url, init)
} catch {
throw statusError(PORTAL_DOWN_TEXT, 0)
}
}
return async function request(path, { method = 'GET', body } = {}) {
const res = await fetchWithRetry(jwtCache, (jwt) =>
portalFetch(`${baseUrl}${path}`, {
method,
headers: { Authorization: `Bearer ${jwt}`, 'Content-Type': 'application/json' },
body: body === undefined ? undefined : JSON.stringify(body),
})
)
let data = null
try {
data = await res.json()
} catch {
data = null // keine JSON-Antwort (Fehlerseite des Proxys, leerer Body)
}
if (!res.ok) {
const gatewayDown = data === null && (res.status === 502 || res.status === 503 || res.status === 504)
const info = data && typeof data === 'object' ? data : {}
const err = statusError(gatewayDown ? PORTAL_DOWN_TEXT : info.error || `API-Fehler ${res.status}`, res.status)
// Nach dem Festschreiben (502) liegt die Rechnung schon als "offen" vor
if (info.invoiceId) err.invoiceId = info.invoiceId
if (info.invoiceNumber) err.invoiceNumber = info.invoiceNumber
throw err
}
return data ?? {}
}
}