feat: verschlüsselte Integrationseinstellungen, Haushaltsmitglieder mit Ersteller, Backup-Download

This commit is contained in:
opencode committed 2026-10-18 14:19:03 +02:00
1 parent a1195223e0
commit 267f4fcf70
21 files changed
+724 -30

No files matched your search

+14 -10
View File
@@ -1,3 +1,5 @@
import { getLogoSettings } from "./integration-settings";
export type LogoCandidate = { name: string; domain: string; logoUrl: string | null };
const DOMAIN_RE = /^[a-z0-9.-]+\.[a-z]{2,}$/i;
@@ -10,11 +12,11 @@ export function normalizeDomain(domain: string | null | undefined): string | nul
}
/** Baut die öffentliche Logo-URL für eine gespeicherte Domain. Key bleibt serverseitig. */
export function logoImageUrl(domain: string | null | undefined): string | null {
export async function logoImageUrl(domain: string | null | undefined): Promise<string | null> {
const d = normalizeDomain(domain);
const token = process.env.LOGO_DEV_PUBLISHABLE_KEY;
if (!d || !token) return null;
return `https://img.logo.dev/${d}?token=${token}&size=128&format=png`;
const { publishableKey } = await getLogoSettings();
if (!d || !publishableKey) return null;
return `https://img.logo.dev/${d}?token=${publishableKey}&size=128&format=png`;
}
/** Kürzel aus dem Anbieternamen als Fallback, z. B. "Therme Erding" → "TE". */
@@ -26,18 +28,20 @@ export function monogram(name: string): string {
}
/** Reichert ein Voucher-Objekt um die öffentliche logoUrl an (serverseitig). */
export function withLogoUrl<T extends { providerDomain: string | null }>(v: T): T & { logoUrl: string | null } {
return { ...v, logoUrl: logoImageUrl(v.providerDomain) };
export async function withLogoUrl<T extends { providerDomain: string | null }>(
v: T
): Promise<T & { logoUrl: string | null }> {
return { ...v, logoUrl: await logoImageUrl(v.providerDomain) };
}
/** Sucht Anbieter über Logo.dev (serverseitig, Secret-Key). Gibt max. 8 Treffer zurück. */
export async function searchLogos(query: string): Promise<LogoCandidate[]> {
const key = process.env.LOGO_DEV_SECRET_KEY;
if (!key) throw new Error("LOGO_DEV_SECRET_KEY fehlt");
const { secretKey } = await getLogoSettings();
if (!secretKey) throw new Error("LOGO_DEV_SECRET_KEY fehlt");
const q = query.trim();
if (!q) return [];
const res = await fetch(`https://api.logo.dev/search?q=${encodeURIComponent(q)}`, {
headers: { Authorization: `Bearer ${key}` },
headers: { Authorization: `Bearer ${secretKey}` },
});
if (!res.ok) throw new Error(`Logosuche ${res.status}`);
const data: unknown = await res.json();
@@ -49,7 +53,7 @@ export async function searchLogos(query: string): Promise<LogoCandidate[]> {
const domain = normalizeDomain(typeof rec.domain === "string" ? rec.domain : null);
if (!domain) continue;
const name = typeof rec.name === "string" && rec.name.trim() ? rec.name.trim() : domain;
out.push({ name, domain, logoUrl: logoImageUrl(domain) });
out.push({ name, domain, logoUrl: await logoImageUrl(domain) });
if (out.length >= 8) break;
}
return out;