diff --git a/backend/nitro.config.ts b/backend/nitro.config.ts index 38832c4..c873a84 100644 --- a/backend/nitro.config.ts +++ b/backend/nitro.config.ts @@ -12,13 +12,6 @@ export default defineNitroConfig({ { baseURL: "/", dir: "../public", maxAge: 60 * 60 }, ], - // Server-Assets: werden ins Build-Artifact gebundelt und sind via - // useStorage('assets:server') abrufbar. Kein process.cwd()-Trick nötig. - // gambling-domains.json: runtime-updatebare iOS-Layer-2-Domain-Liste. - serverAssets: [ - { baseName: "data", dir: "../data" }, - ], - // Supabase als external dep — nicht bundlen externals: { inline: [/^(?!@supabase\/supabase-js)/], diff --git a/backend/server/api/protection/webcontent-domains.get.ts b/backend/server/api/protection/webcontent-domains.get.ts index 9dfefeb..1320e38 100644 --- a/backend/server/api/protection/webcontent-domains.get.ts +++ b/backend/server/api/protection/webcontent-domains.get.ts @@ -1,55 +1,63 @@ -import { requireUser } from "../../utils/auth"; +import gamblingDomains from "../../data/gambling-domains.json"; -const MAX_DOMAINS_PER_COUNTRY = 50; +const COUNTRY_KEYS = ["DE", "GB", "FR"] as const; +type CountryKey = (typeof COUNTRY_KEYS)[number]; + +const GLOBAL_LISTS = gamblingDomains as Record & { + _meta: (typeof gamblingDomains)["_meta"]; +}; + +const MAX_PER_COUNTRY = 50; /** * GET /api/protection/webcontent-domains * - * Liefert die runtime-updatebare iOS-Layer-2-Gambling-Domain-Liste. - * Datenquelle: backend/data/gambling-domains.json (file-based, v1). - * Im Build über Nitro serverAssets gebundelt → kein process.cwd()-Trick. + * Liefert die VIP-Domain-Liste für den WebKit-webContent-Filter (Layer 2). + * Pro User personalisiert: + * Custom-Web-Domains (aktiv, nicht approved/rejected) + globale Liste + * → dedupliziert → hart auf 50 gekappt (Apple-Limit). * - * Pflege: backend/data/gambling-domains.json editieren, + * Custom-Domains stehen vorne (User-Priorität). + * Response-Shape ist identisch mit der statischen Version — iOS parst das unverändert. + * + * Lade-Mechanismus: direkter JSON-Import (build-time gebundelt via Nitro-Bundler). + * Kein serverAssets/useStorage — kein extra Laufzeit-I/O, kein globales + * backend/data/-Verzeichnis nötig. + * + * Pflege: backend/server/data/gambling-domains.json editieren, * _meta.version hochzählen, _meta.updatedAt setzen, dann neu deployen. - * - * Apple-Hartlimit: max. 50 Domains pro Land-Key. Wird serverseitig - * enforced (slice), damit ein versehentlicher Überschuss in der JSON-Datei - * nie zur App durchdringt. - * - * Auth: identisch zu allen anderen /api/protection/*-Routen (requireUser). */ export default defineEventHandler(async (event) => { - await requireUser(event); + const user = await requireUser(event); - const storage = useStorage("assets:server"); - const raw = await storage.getItem<{ - _meta: { version: number; updatedAt: string; [k: string]: unknown }; - _comment?: string; - [country: string]: unknown; - }>("data:gambling-domains.json"); + // Custom Web-Domains des Users laden — parallel zu allen Country-Listen + const userWebDomains = await getWebCustomDomains(user.id); + const userWebSet = new Set(userWebDomains); - if (!raw) { - throw createError({ - statusCode: 500, - message: "GAMBLING_DOMAINS_UNAVAILABLE", - }); - } + // Pro Country: Custom-Domains vorne, dann globale Auffüllung, dedup, cap 50 + const composed: Record = {} as Record< + CountryKey, + string[] + >; - const { _meta, _comment: _c, ...countryEntries } = raw; + for (const country of COUNTRY_KEYS) { + const globalList: string[] = GLOBAL_LISTS[country] ?? []; - const countries: Record = {}; - for (const [key, value] of Object.entries(countryEntries)) { - if (Array.isArray(value)) { - // Apple-Hartlimit: niemals mehr als 50 Domains pro Land ausliefern - countries[key] = (value as string[]).slice(0, MAX_DOMAINS_PER_COUNTRY); + // Custom-Domains zuerst (bereits dedupliziert da aus DB) + const merged: string[] = [...userWebDomains]; + + // Globale Domains auffüllen — nur wenn noch nicht durch Custom drin + for (const domain of globalList) { + if (!userWebSet.has(domain)) { + merged.push(domain); + } } + + composed[country] = merged.slice(0, MAX_PER_COUNTRY); } return { - _meta: { - version: _meta.version, - updatedAt: _meta.updatedAt, - }, - ...countries, + _meta: gamblingDomains._meta, + ...composed, }; }); diff --git a/backend/data/gambling-domains.json b/backend/server/data/gambling-domains.json similarity index 82% rename from backend/data/gambling-domains.json rename to backend/server/data/gambling-domains.json index 7ab6687..d85c403 100644 --- a/backend/data/gambling-domains.json +++ b/backend/server/data/gambling-domains.json @@ -1,5 +1,4 @@ { - "_comment": "Kuratierte Gambling-Domain-Liste pro Land. APPLE-HARTLIMIT: max. 50 Domains pro Land-Key — NIE überschreiten. Schlüssel = ISO-3166-1-alpha-2 (Locale.current.region). Werte = registrierbare Domains ohne Schema/Subdomain (ManagedSettings WebDomain matched inkl. aller Subdomains). Pflege: Datei editieren, version hochzählen, updatedAt setzen, dann neu deployen.", "_meta": { "version": 1, "updatedAt": "2026-05-21", diff --git a/backend/server/db/domains.ts b/backend/server/db/domains.ts index 83c0bb7..33b6aef 100644 --- a/backend/server/db/domains.ts +++ b/backend/server/db/domains.ts @@ -21,6 +21,27 @@ export const CUSTOM_DOMAIN_TYPES: CustomDomainType[] = [ // ─── Custom Domains ─────────────────────────────────────────────────────────── +/** + * Gibt die aktiven Web-Custom-Domains eines Users zurück (nur type='web'). + * Status 'approved' und 'rejected' werden ausgeschlossen: + * - approved → Domain ist in der globalen Blocklist, kein Slot mehr nötig + * - rejected → Domain wurde abgelehnt und Slot wurde freigegeben + * Wird von GET /api/protection/webcontent-domains für die VIP-Komposition genutzt. + */ +export async function getWebCustomDomains(userId: string): Promise { + const db = usePrisma(); + const rows = await db.userCustomDomain.findMany({ + where: { + userId, + type: "web", + status: { notIn: ["approved", "rejected"] }, + }, + orderBy: { addedAt: "asc" }, + select: { domain: true }, + }); + return rows.map((r) => r.domain); +} + export async function getUserCustomDomains(userId: string) { const db = usePrisma(); const rows = await db.userCustomDomain.findMany({