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>
81 lines
3.2 KiB
TypeScript
81 lines
3.2 KiB
TypeScript
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,
|
||
};
|
||
});
|