User können länderspezifische Glücksspielseiten für die kuratierte VIP-Layer-2-Liste vorschlagen — wichtig für Länder mit kurzer Starter-Liste (z.B. TN). - Schema: CuratedDomain (domain, country, status, suggestedByUserId); Migration 20260522_curated_domains - webcontent-domains.get.ts komponiert jetzt JSON-Basis + DB-approved Curated-Domains pro Land - POST /api/curated-domains/suggest legt einen suggested-Eintrag an Admin-Approve (Endpoint + Admin-App-View) folgt als nächster Block. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
101 lines
4.0 KiB
TypeScript
101 lines
4.0 KiB
TypeScript
import gamblingDomains from "../../data/gambling-domains.json";
|
||
import { getWebCustomDomains } from "../../db/domains";
|
||
import { usePrisma } from "../../utils/prisma";
|
||
|
||
const COUNTRY_KEYS = ["DE", "GB", "FR", "TN"] as const;
|
||
type CountryKey = (typeof COUNTRY_KEYS)[number];
|
||
|
||
const GLOBAL_LISTS = gamblingDomains as unknown as Record<string, string[]>;
|
||
|
||
const MAX_PER_COUNTRY = 50;
|
||
|
||
// Hybrid-Reservierung: die Top-N kuratierten Gambling-Domains pro Land sind
|
||
// FEST garantiert — ein User kann sie nicht mit eigenen Custom-Domains aus
|
||
// seinem Layer-2-Zweitschutz verdrängen. Custom-Domains werden daher hart auf
|
||
// (50 − RESERVED_CURATED) gekappt. Voraussetzung: gambling-domains.json ist
|
||
// nach Relevanz sortiert (die ersten RESERVED_CURATED = die wichtigsten).
|
||
const RESERVED_CURATED = 20;
|
||
const MAX_CUSTOM = MAX_PER_COUNTRY - RESERVED_CURATED; // 30
|
||
|
||
/**
|
||
* GET /api/protection/webcontent-domains
|
||
*
|
||
* Liefert die VIP-Domain-Liste für den WebKit-webContent-Filter (Layer 2).
|
||
* Pro User personalisiert, Hybrid-Komposition pro Land:
|
||
* 1. Custom-Web-Domains (pending zuerst, dann approved) — gekappt auf 30
|
||
* 2. kuratierte Gambling-Liste — füllt den Rest bis 50 auf
|
||
* → dedupliziert → hart auf 50 gekappt (Apple-Limit).
|
||
*
|
||
* Damit sind immer ≥ 20 kuratierte Top-Domains im Zweitschutz garantiert,
|
||
* egal wie viele Custom-Domains der User angesammelt hat.
|
||
* 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.
|
||
*/
|
||
export default defineEventHandler(async (event) => {
|
||
const user = await requireUser(event);
|
||
|
||
// Custom Web-Domains des Users laden — parallel zu allen Country-Listen
|
||
const userWebDomains = await getWebCustomDomains(user.id);
|
||
|
||
// Custom-Domains hart auf 30 kappen — die ersten 30 sind die höchst-
|
||
// priorisierten (getWebCustomDomains liefert pending zuerst, dann approved
|
||
// neueste-zuerst). Die restlichen 20 Slots bleiben für die kuratierte Liste.
|
||
const cappedCustom = userWebDomains.slice(0, MAX_CUSTOM);
|
||
// Dedup-Set NUR über die gekappten Customs — eine kuratierte Domain, die
|
||
// einer aus dem 30-Cap GEFLOGENEN Custom-Domain entspricht, soll über die
|
||
// kuratierte Auffüllung wieder reinkommen (sie ist ja eine Top-Domain).
|
||
const cappedCustomSet = new Set(cappedCustom);
|
||
|
||
// DB-approved Curated-Domains (User-Vorschläge, admin-freigegeben) ergänzen
|
||
// die statische gambling-domains.json pro Land — wichtig für Länder mit
|
||
// kurzer Starter-Liste (z.B. TN).
|
||
const db = usePrisma();
|
||
const approvedCurated = await db.curatedDomain.findMany({
|
||
where: { status: "approved" },
|
||
select: { domain: true, country: true },
|
||
});
|
||
const curatedByCountry: Record<string, string[]> = {};
|
||
for (const c of approvedCurated) {
|
||
(curatedByCountry[c.country] ??= []).push(c.domain);
|
||
}
|
||
|
||
// Pro Country: Custom-Domains vorne, dann globale Auffüllung, dedup, cap 50
|
||
const composed: Record<CountryKey, string[]> = {} as Record<
|
||
CountryKey,
|
||
string[]
|
||
>;
|
||
|
||
for (const country of COUNTRY_KEYS) {
|
||
// statische JSON-Liste + DB-approved Curated des Landes, dedupliziert
|
||
const globalList: string[] = [
|
||
...new Set([
|
||
...(GLOBAL_LISTS[country] ?? []),
|
||
...(curatedByCountry[country] ?? []),
|
||
]),
|
||
];
|
||
|
||
// Gekappte Custom-Domains zuerst (bereits dedupliziert da aus DB)
|
||
const merged: string[] = [...cappedCustom];
|
||
|
||
// Kuratierte Domains auffüllen — nur wenn noch nicht durch Custom drin
|
||
for (const domain of globalList) {
|
||
if (!cappedCustomSet.has(domain)) {
|
||
merged.push(domain);
|
||
}
|
||
}
|
||
|
||
composed[country] = merged.slice(0, MAX_PER_COUNTRY);
|
||
}
|
||
|
||
return {
|
||
_meta: gamblingDomains._meta,
|
||
...composed,
|
||
};
|
||
});
|