rebreak-monorepo/backend/server/api/protection/webcontent-domains.get.ts
chahinebrini fe156a5f58 feat(blocker/vip): Freigabe-Button, landabhängige VIP-Liste, Hybrid-Komposition + Add-Check
Blocker-UI:
- FilterTile: Trash-Button → status-aware Freigabe-Button (Freigeben/Erneut/
  in-Prüfung); RemoveDomainSheet entfernt — kein Domain-Entfernen mehr in der UI
- VIP-Liste landabhängig: zeigt die komponierte Endpoint-Liste statt nur
  eigener Customs; Land über Geräte-Region (expo-localization)
- VIP-Realtime: refetch bei Domain-Add/Approve/Reject, pulsierender Ring
  für neue/active/submitted Chips

VIP-Komposition (webcontent-domains):
- Hybrid: Customs auf 30 gekappt, 20 Slots fest für die kuratierte Top-Liste
  reserviert — Customs können die Top-Gambling-Domains nicht verdrängen

Add-Check (custom-domains POST), für web reaktiviert — 3 Fälle gegen
Layer 1 (global) + Layer 2 (kuratierte VIP):
- weder global noch kuratiert → normaler active-Eintrag
- global + kuratiert → alreadyProtected, kein Slot
- global, nicht kuratiert → inGlobalNotVip; per addToVip als status=approved
  speicherbar (kein Slot, nur VIP-Liste)

DE-Gambling-Liste 30→36, nach Relevanz sortiert (erste 20 = reservierte Plätze)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 17:27:10 +02:00

81 lines
3.2 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";
const COUNTRY_KEYS = ["DE", "GB", "FR"] 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);
// 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) {
const globalList: string[] = GLOBAL_LISTS[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,
};
});