fix: fehlende Dateien aus vorherigem Commit nachreichen
CSRF-Schutz, DHL-Versandmarken, Entra-ID-SSO, ApiCore-Sync und die Harvest-isPublished-Migration waren gestaged aber nicht im letzten Commit enthalten — Gitea-Actions-Build brach mit "Module not found" fuer genau diese Dateien ab. Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
1 parent
4d3100e9ba
commit
b6db9254a9
10 files changed
+1205
No files matched your search
@@ -0,0 +1,134 @@
|
||||
import { z } from "zod";
|
||||
|
||||
/**
|
||||
* HTTP-Client fuer die ApiCore-Backend-API (separates Projekt, lokal unter
|
||||
* .../ApiCore/ApiCore-Backend — ein NestJS-Server mit eigener Postgres-DB),
|
||||
* die die "Ernten"-Rohdaten fuer den Admin-Sync bereitstellt
|
||||
* (admin/actions/apicore-sync.ts).
|
||||
*
|
||||
* Auth-Modell von ApiCore: E-Mail/Passwort-Login -> kurzlebiges JWT
|
||||
* (POST /auth/login), Endpunkte pruefen zusaetzlich granulare Berechtigungen
|
||||
* ("harvests:read" fuer GET /harvests). Es gibt dort AKTUELL KEINEN
|
||||
* API-Key-/Service-Account-Mechanismus fuer maschinelle Zugriffe — nur den
|
||||
* interaktiven Login-Flow, optional mit einem zweiten MFA-Schritt
|
||||
* (POST /auth/mfa/verify), siehe ApiCore-Backend/src/auth/. Fuer diesen
|
||||
* automatisierten Sync wird dringend ein eigenes, MFA-freies Service-Konto
|
||||
* mit ausschliesslich den Berechtigungen "harvests:read" (und optional
|
||||
* "apiaries:read", fuer Standortnamen) empfohlen statt eines echten
|
||||
* Admin-Zugangs.
|
||||
*
|
||||
* Benoetigte Umgebungsvariablen (siehe .env.example):
|
||||
* APICORE_API_URL z.B. "http://localhost:3001/api" (globaler Prefix
|
||||
* "api" laut ApiCore-Backend/src/app.config.ts)
|
||||
* APICORE_SYNC_EMAIL E-Mail des Service-Kontos
|
||||
* APICORE_SYNC_PASSWORD Passwort des Service-Kontos
|
||||
*
|
||||
* Ohne diese drei Variablen wirft fetchApiCoreHarvests() sofort einen
|
||||
* ApiCoreConfigError — der Aufrufer (apicore-sync.ts) zeigt dessen Message
|
||||
* dem Admin an, statt die Aktion stillschweigend nichts tun zu lassen.
|
||||
*/
|
||||
|
||||
export class ApiCoreConfigError extends Error {}
|
||||
export class ApiCoreRequestError extends Error {}
|
||||
|
||||
const loginResponseSchema = z.union([
|
||||
z.object({ accessToken: z.string().min(1) }),
|
||||
z.object({ mfaRequired: z.literal(true), mfaToken: z.string() }),
|
||||
]);
|
||||
|
||||
const apiarySchema = z.object({ id: z.string(), name: z.string() });
|
||||
|
||||
const harvestSchema = z.object({
|
||||
id: z.string(),
|
||||
apiaryId: z.string(),
|
||||
date: z.string(),
|
||||
honeyType: z.string().nullable().optional(),
|
||||
honeySuperCount: z.number().nullable().optional(),
|
||||
grossWeight: z.number().nullable().optional(),
|
||||
netWeight: z.number().nullable().optional(),
|
||||
notes: z.string().nullable().optional(),
|
||||
});
|
||||
|
||||
export type ApiCoreHarvest = z.infer<typeof harvestSchema>;
|
||||
|
||||
export interface ApiCoreHarvestWithApiaryName extends ApiCoreHarvest {
|
||||
apiaryName: string | null;
|
||||
}
|
||||
|
||||
function getConfig(): { baseUrl: string; email: string; password: string } {
|
||||
const baseUrl = process.env.APICORE_API_URL;
|
||||
const email = process.env.APICORE_SYNC_EMAIL;
|
||||
const password = process.env.APICORE_SYNC_PASSWORD;
|
||||
if (!baseUrl || !email || !password) {
|
||||
throw new ApiCoreConfigError(
|
||||
"ApiCore-Synchronisierung ist nicht konfiguriert. Bitte APICORE_API_URL, APICORE_SYNC_EMAIL und APICORE_SYNC_PASSWORD setzen (siehe .env.example)."
|
||||
);
|
||||
}
|
||||
return { baseUrl: baseUrl.replace(/\/$/, ""), email, password };
|
||||
}
|
||||
|
||||
async function login(baseUrl: string, email: string, password: string): Promise<string> {
|
||||
let res: Response;
|
||||
try {
|
||||
res = await fetch(`${baseUrl}/auth/login`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ email, password }),
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
const message = error instanceof Error ? error.message : "Unbekannter Fehler";
|
||||
throw new ApiCoreRequestError(`ApiCore unter ${baseUrl} nicht erreichbar: ${message}`);
|
||||
}
|
||||
if (!res.ok) {
|
||||
throw new ApiCoreRequestError(`ApiCore-Login fehlgeschlagen (HTTP ${res.status}).`);
|
||||
}
|
||||
const parsed = loginResponseSchema.safeParse(await res.json());
|
||||
if (!parsed.success) {
|
||||
throw new ApiCoreRequestError("ApiCore-Login: unerwartete Antwortstruktur.");
|
||||
}
|
||||
if ("mfaRequired" in parsed.data) {
|
||||
throw new ApiCoreConfigError(
|
||||
"Das konfigurierte ApiCore-Sync-Konto hat MFA aktiviert. Fuer den automatisierten Sync wird ein MFA-freies Service-Konto benoetigt (siehe Kommentar in apicore-client.ts)."
|
||||
);
|
||||
}
|
||||
return parsed.data.accessToken;
|
||||
}
|
||||
|
||||
/** Best effort: Standortnamen sind eine Komfort-Anreicherung, kein kritischer Pfad. */
|
||||
async function fetchApiaryNames(baseUrl: string, token: string): Promise<Map<string, string>> {
|
||||
try {
|
||||
const res = await fetch(`${baseUrl}/apiaries`, { headers: { Authorization: `Bearer ${token}` } });
|
||||
if (!res.ok) return new Map();
|
||||
const parsed = z.array(apiarySchema).safeParse(await res.json());
|
||||
if (!parsed.success) return new Map();
|
||||
return new Map(parsed.data.map((a) => [a.id, a.name]));
|
||||
} catch {
|
||||
return new Map();
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchApiCoreHarvests(): Promise<ApiCoreHarvestWithApiaryName[]> {
|
||||
const { baseUrl, email, password } = getConfig();
|
||||
const token = await login(baseUrl, email, password);
|
||||
|
||||
let res: Response;
|
||||
try {
|
||||
res = await fetch(`${baseUrl}/harvests`, { headers: { Authorization: `Bearer ${token}` } });
|
||||
} catch (error: unknown) {
|
||||
const message = error instanceof Error ? error.message : "Unbekannter Fehler";
|
||||
throw new ApiCoreRequestError(`ApiCore unter ${baseUrl} nicht erreichbar: ${message}`);
|
||||
}
|
||||
if (!res.ok) {
|
||||
throw new ApiCoreRequestError(`ApiCore: Ernten konnten nicht geladen werden (HTTP ${res.status}).`);
|
||||
}
|
||||
const parsed = z.array(harvestSchema).safeParse(await res.json());
|
||||
if (!parsed.success) {
|
||||
throw new ApiCoreRequestError("ApiCore: unerwartete Antwortstruktur fuer Ernten.");
|
||||
}
|
||||
|
||||
const apiaryNames = await fetchApiaryNames(baseUrl, token);
|
||||
return parsed.data.map((harvest) => ({
|
||||
...harvest,
|
||||
apiaryName: apiaryNames.get(harvest.apiaryId) ?? null,
|
||||
}));
|
||||
}
|
||||
Reference in new issue
Block a user