rebreak-monorepo/backend/server/api/protection/webcontent-domains.get.ts
chahinebrini 265859467a feat(vip): Curated-Domain-Vorschläge — Suggest-Backend
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>
2026-05-22 21:09:00 +02:00

101 lines
4.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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,
};
});