fix(protection): webcontent-domains-Endpoint konsolidiert — VIP-Komposition

Ersetzt die statische v1 des Endpoints durch die per-User-VIP-Komposition:
eigene Web-Custom-Domains zuerst + globale Auffuellung → dedup → Cap 50.
v1-Reste entfernt (backend/data/, serverAssets-Eintrag) — eine Datenquelle
(backend/server/data/gambling-domains.json, direkter JSON-Import).
pnpm build:backend gruen verifiziert.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
chahinebrini 2026-05-21 21:21:22 +02:00
parent 72cd195d36
commit 9dfccfcbaa
4 changed files with 65 additions and 44 deletions

View File

@ -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)/],

View File

@ -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<string, string[]> & {
_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<CountryKey, string[]> = {} 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<string, string[]> = {};
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,
};
});

View File

@ -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",

View File

@ -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<string[]> {
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({