import type { UmamiServiceWidget } from "@/lib/config/schema"; import type { ServiceStat } from "@/lib/types/service-result"; import { serviceBaseUrl } from "./base-url"; // Self-hosted Umami has no static API-key header (that's Cloud-only) - it needs // a username/password login exchanged for a Bearer token. Cache the token per // container and only re-login when a request comes back 401, since the docs // don't document a fixed token lifetime to pre-emptively refresh against. const tokenCache = new Map(); async function login(widget: UmamiServiceWidget, base: string): Promise { const response = await fetch(`${base}/api/auth/login`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username: widget.username, password: widget.password }), signal: AbortSignal.timeout(5_000), }); if (!response.ok) { throw new Error(`Umami login returned ${response.status}`); } const body = (await response.json()) as { token: string }; tokenCache.set(widget.containerName, body.token); return body.token; } export async function collectUmami(widget: UmamiServiceWidget): Promise<{ stats: ServiceStat[] }> { const base = serviceBaseUrl(widget); let token = tokenCache.get(widget.containerName) ?? (await login(widget, base)); let response = await fetch(`${base}/api/websites/${widget.websiteId}/active`, { headers: { Authorization: `Bearer ${token}` }, signal: AbortSignal.timeout(5_000), }); if (response.status === 401) { token = await login(widget, base); response = await fetch(`${base}/api/websites/${widget.websiteId}/active`, { headers: { Authorization: `Bearer ${token}` }, signal: AbortSignal.timeout(5_000), }); } if (!response.ok) { throw new Error(`Umami API returned ${response.status}`); } const body = (await response.json()) as { visitors: number }; return { stats: [{ label: "Active", value: String(body.visitors) }] }; }