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 },
|
{ 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
|
// Supabase als external dep — nicht bundlen
|
||||||
externals: {
|
externals: {
|
||||||
inline: [/^(?!@supabase\/supabase-js)/],
|
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
|
* GET /api/protection/webcontent-domains
|
||||||
*
|
*
|
||||||
* Liefert die runtime-updatebare iOS-Layer-2-Gambling-Domain-Liste.
|
* Liefert die VIP-Domain-Liste für den WebKit-webContent-Filter (Layer 2).
|
||||||
* Datenquelle: backend/data/gambling-domains.json (file-based, v1).
|
* Pro User personalisiert:
|
||||||
* Im Build über Nitro serverAssets gebundelt → kein process.cwd()-Trick.
|
* 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.
|
* _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) => {
|
export default defineEventHandler(async (event) => {
|
||||||
await requireUser(event);
|
const user = await requireUser(event);
|
||||||
|
|
||||||
const storage = useStorage("assets:server");
|
// Custom Web-Domains des Users laden — parallel zu allen Country-Listen
|
||||||
const raw = await storage.getItem<{
|
const userWebDomains = await getWebCustomDomains(user.id);
|
||||||
_meta: { version: number; updatedAt: string; [k: string]: unknown };
|
const userWebSet = new Set(userWebDomains);
|
||||||
_comment?: string;
|
|
||||||
[country: string]: unknown;
|
|
||||||
}>("data:gambling-domains.json");
|
|
||||||
|
|
||||||
if (!raw) {
|
// Pro Country: Custom-Domains vorne, dann globale Auffüllung, dedup, cap 50
|
||||||
throw createError({
|
const composed: Record<CountryKey, string[]> = {} as Record<
|
||||||
statusCode: 500,
|
CountryKey,
|
||||||
message: "GAMBLING_DOMAINS_UNAVAILABLE",
|
string[]
|
||||||
});
|
>;
|
||||||
|
|
||||||
|
for (const country of COUNTRY_KEYS) {
|
||||||
|
const globalList: string[] = GLOBAL_LISTS[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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { _meta, _comment: _c, ...countryEntries } = raw;
|
composed[country] = merged.slice(0, MAX_PER_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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
_meta: {
|
_meta: gamblingDomains._meta,
|
||||||
version: _meta.version,
|
...composed,
|
||||||
updatedAt: _meta.updatedAt,
|
|
||||||
},
|
|
||||||
...countries,
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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": {
|
"_meta": {
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"updatedAt": "2026-05-21",
|
"updatedAt": "2026-05-21",
|
||||||
@ -21,6 +21,27 @@ export const CUSTOM_DOMAIN_TYPES: CustomDomainType[] = [
|
|||||||
|
|
||||||
// ─── Custom Domains ───────────────────────────────────────────────────────────
|
// ─── 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) {
|
export async function getUserCustomDomains(userId: string) {
|
||||||
const db = usePrisma();
|
const db = usePrisma();
|
||||||
const rows = await db.userCustomDomain.findMany({
|
const rows = await db.userCustomDomain.findMany({
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user