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:
parent
72cd195d36
commit
9dfccfcbaa
@ -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)/],
|
||||
|
||||
@ -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,
|
||||
};
|
||||
});
|
||||
|
||||
@ -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",
|
||||
@ -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({
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user