import type { FastifyInstance } from "fastify"; import { z } from "zod"; import type { Db } from "./db.js"; import type { AppConfig } from "./config.js"; import type { BaseAdapter } from "./integration/baseAdapter.js"; import { ExportBlocked, ExportRejected, ExportUncertain } from "./integration/baseAdapter.js"; import { getAppSetting, getBaseDefaults, setAppSetting } from "./domain/settings.js"; const baselinkerSettingsSchema = z .object({ apiToken: z.string().trim().min(1).max(200).optional(), inventoryId: z.number().int().positive().optional(), priceGroupId: z.number().int().positive().optional(), warehouseId: z.string().trim().min(1).max(40).optional(), }) .strict(); /** * Ustawienia integracji zewnętrznych. Token Base nigdy nie jest zwracany * do klienta — GET raportuje wyłącznie, czy i skąd token jest ustawiony. * Zapis wymaga roli admin (zarządzanie sekretem); test połączenia to * odczyt zewnętrzny — approver/admin. */ export function registerSettingsRoutes(app: FastifyInstance, db: Db, cfg: AppConfig, adapter: BaseAdapter): void { app.register(async (scope) => { scope.addHook("preHandler", async (req, reply) => { reply.header("Cache-Control", "private, no-store"); if (!req.user) return reply.code(401).send({ error: "Wymagane zalogowanie." }); if (req.method !== "GET") { const origin = req.headers.origin, site = req.headers["sec-fetch-site"]; let matchingOrigin = !origin; if (origin) { try { matchingOrigin = new URL(origin).host === req.headers.host; } catch { matchingOrigin = false; } } if (site === "cross-site" || (site !== "same-origin" && !matchingOrigin)) return reply.code(403).send({ error: "Niedozwolone źródło żądania." }); } }); scope.setErrorHandler((err, _req, reply) => { if (err instanceof z.ZodError) return reply.code(400).send({ error: "Nieprawidłowe ustawienia. Sprawdź wymagane pola." }); return reply.code(500).send({ error: "Nie udało się obsłużyć ustawień." }); }); scope.get("/api/settings", async () => { const dbToken = getAppSetting(db, "baselinker.api_token"); const defaults = getBaseDefaults(db); return { baselinker: { tokenConfigured: Boolean(dbToken ?? cfg.baseApiToken), tokenSource: dbToken ? "panel" : cfg.baseApiToken ? "env" : null, apiUrl: cfg.baseApiUrl, inventoryId: defaults.inventoryId, priceGroupId: defaults.priceGroupId, warehouseId: defaults.warehouseId, }, }; }); scope.put("/api/settings/baselinker", { bodyLimit: 8192 }, async (req, reply) => { if (req.user!.role !== "admin") return reply.code(403).send({ error: "Zmiana ustawień integracji wymaga roli admin." }); const body = baselinkerSettingsSchema.parse(req.body ?? {}); if (body.apiToken !== undefined) setAppSetting(db, "baselinker.api_token", body.apiToken, req.user!.id); if (body.inventoryId !== undefined) setAppSetting(db, "base.inventory_id", String(body.inventoryId), req.user!.id); if (body.priceGroupId !== undefined) setAppSetting(db, "base.price_group_id", String(body.priceGroupId), req.user!.id); if (body.warehouseId !== undefined) setAppSetting(db, "base.warehouse_id", body.warehouseId, req.user!.id); const dbToken = getAppSetting(db, "baselinker.api_token"); const defaults = getBaseDefaults(db); return { baselinker: { tokenConfigured: Boolean(dbToken ?? cfg.baseApiToken), tokenSource: dbToken ? "panel" : cfg.baseApiToken ? "env" : null, apiUrl: cfg.baseApiUrl, inventoryId: defaults.inventoryId, priceGroupId: defaults.priceGroupId, warehouseId: defaults.warehouseId, }, }; }); scope.post("/api/settings/baselinker/test", async (req, reply) => { if (!["admin", "approver"].includes(req.user!.role)) return reply.code(403).send({ error: "Test połączenia wymaga roli approver/admin." }); try { const inventories = await adapter.getInventories(); return { ok: true, inventories }; } catch (err) { if (err instanceof ExportBlocked) return { ok: false, error: "Brak skonfigurowanego tokena Base." }; if (err instanceof ExportRejected) return { ok: false, error: `Base odrzuciło token: ${err.apiMessage ?? err.message}` }; if (err instanceof ExportUncertain) return { ok: false, error: "Brak odpowiedzi Base — sprawdź sieć i spróbuj ponownie." }; throw err; } }); }); }