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>
This commit is contained in:
parent
f555c5e4d8
commit
265859467a
@ -0,0 +1,20 @@
|
|||||||
|
-- Migration: curated_domains
|
||||||
|
-- Länderspezifische Curated-Domain-Vorschläge für die VIP-Layer-2-Liste.
|
||||||
|
-- User schlagen Glücksspielseiten ihres Landes vor; status='approved'-Einträge
|
||||||
|
-- werden vom webcontent-domains-Endpoint zusätzlich zur statischen
|
||||||
|
-- gambling-domains.json komponiert. Approve/Reject erfolgt admin-seitig.
|
||||||
|
|
||||||
|
CREATE TABLE "rebreak"."curated_domains" (
|
||||||
|
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
|
||||||
|
"domain" TEXT NOT NULL,
|
||||||
|
"country" TEXT NOT NULL,
|
||||||
|
"status" TEXT NOT NULL DEFAULT 'suggested',
|
||||||
|
"suggested_by_user_id" UUID,
|
||||||
|
"created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
"reviewed_at" TIMESTAMPTZ,
|
||||||
|
|
||||||
|
CONSTRAINT "curated_domains_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX "curated_domains_country_domain_key" ON "rebreak"."curated_domains"("country", "domain");
|
||||||
|
CREATE INDEX "curated_domains_country_status_idx" ON "rebreak"."curated_domains"("country", "status");
|
||||||
@ -454,6 +454,25 @@ model UserCustomDomain {
|
|||||||
@@schema("rebreak")
|
@@schema("rebreak")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Länderspezifische Curated-Domain-Vorschläge für die VIP-Layer-2-Liste.
|
||||||
|
// User schlagen Glücksspielseiten ihres Landes vor; `approved`-Einträge
|
||||||
|
// ergänzen im webcontent-domains-Endpoint die statische gambling-domains.json.
|
||||||
|
model CuratedDomain {
|
||||||
|
id String @id @default(uuid()) @db.Uuid
|
||||||
|
domain String
|
||||||
|
country String
|
||||||
|
// "suggested" | "approved" | "rejected"
|
||||||
|
status String @default("suggested")
|
||||||
|
suggestedByUserId String? @map("suggested_by_user_id") @db.Uuid
|
||||||
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
|
reviewedAt DateTime? @map("reviewed_at")
|
||||||
|
|
||||||
|
@@unique([country, domain])
|
||||||
|
@@index([country, status])
|
||||||
|
@@map("curated_domains")
|
||||||
|
@@schema("rebreak")
|
||||||
|
}
|
||||||
|
|
||||||
model DomainSubmission {
|
model DomainSubmission {
|
||||||
id String @id @default(uuid()) @db.Uuid
|
id String @id @default(uuid()) @db.Uuid
|
||||||
userId String @map("user_id") @db.Uuid
|
userId String @map("user_id") @db.Uuid
|
||||||
|
|||||||
55
backend/server/api/curated-domains/suggest.post.ts
Normal file
55
backend/server/api/curated-domains/suggest.post.ts
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import { usePrisma } from "../../utils/prisma";
|
||||||
|
|
||||||
|
// Domain muss mindestens eine TLD haben (z.B. "mbet216.com").
|
||||||
|
const DOMAIN_RE = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$/;
|
||||||
|
// Unterstützte VIP-Länder — muss zu COUNTRY_KEYS in webcontent-domains.get.ts passen.
|
||||||
|
const VALID_COUNTRIES = ["DE", "GB", "FR", "TN"];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/curated-domains/suggest
|
||||||
|
*
|
||||||
|
* Ein User schlägt eine länderspezifische Glücksspielseite für die kuratierte
|
||||||
|
* VIP-Layer-2-Liste seines Landes vor. Der Eintrag landet als `suggested` in
|
||||||
|
* der curated_domains-Tabelle; ein ReBreak-Admin gibt ihn frei (`approved`) —
|
||||||
|
* erst dann komponiert ihn der webcontent-domains-Endpoint in die Länderliste.
|
||||||
|
*
|
||||||
|
* Body: { domain: string, country: string (ISO-2) }
|
||||||
|
*/
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const user = await requireUser(event);
|
||||||
|
const body = await readBody(event);
|
||||||
|
|
||||||
|
// Domain normalisieren — lowercase, Schema/Pfad/www. strippen.
|
||||||
|
const domain = (typeof body?.domain === "string" ? body.domain : "")
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/^https?:\/\//, "")
|
||||||
|
.replace(/\/.*$/, "")
|
||||||
|
.replace(/^www\./, "");
|
||||||
|
const country =
|
||||||
|
typeof body?.country === "string" ? body.country.toUpperCase() : "";
|
||||||
|
|
||||||
|
if (!domain || domain.length > 253 || !DOMAIN_RE.test(domain)) {
|
||||||
|
throw createError({ statusCode: 400, data: { error: "INVALID_DOMAIN" } });
|
||||||
|
}
|
||||||
|
if (!VALID_COUNTRIES.includes(country)) {
|
||||||
|
throw createError({ statusCode: 400, data: { error: "INVALID_COUNTRY" } });
|
||||||
|
}
|
||||||
|
|
||||||
|
const db = usePrisma();
|
||||||
|
|
||||||
|
// Schon vorhanden (egal welcher Status)? Dann nicht doppelt anlegen.
|
||||||
|
const existing = await db.curatedDomain.findUnique({
|
||||||
|
where: { country_domain: { country, domain } },
|
||||||
|
select: { status: true },
|
||||||
|
});
|
||||||
|
if (existing) {
|
||||||
|
return { ok: false, alreadyExists: true, status: existing.status };
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.curatedDomain.create({
|
||||||
|
data: { domain, country, status: "suggested", suggestedByUserId: user.id },
|
||||||
|
});
|
||||||
|
|
||||||
|
return { ok: true };
|
||||||
|
});
|
||||||
@ -1,5 +1,6 @@
|
|||||||
import gamblingDomains from "../../data/gambling-domains.json";
|
import gamblingDomains from "../../data/gambling-domains.json";
|
||||||
import { getWebCustomDomains } from "../../db/domains";
|
import { getWebCustomDomains } from "../../db/domains";
|
||||||
|
import { usePrisma } from "../../utils/prisma";
|
||||||
|
|
||||||
const COUNTRY_KEYS = ["DE", "GB", "FR", "TN"] as const;
|
const COUNTRY_KEYS = ["DE", "GB", "FR", "TN"] as const;
|
||||||
type CountryKey = (typeof COUNTRY_KEYS)[number];
|
type CountryKey = (typeof COUNTRY_KEYS)[number];
|
||||||
@ -51,6 +52,19 @@ export default defineEventHandler(async (event) => {
|
|||||||
// kuratierte Auffüllung wieder reinkommen (sie ist ja eine Top-Domain).
|
// kuratierte Auffüllung wieder reinkommen (sie ist ja eine Top-Domain).
|
||||||
const cappedCustomSet = new Set(cappedCustom);
|
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
|
// Pro Country: Custom-Domains vorne, dann globale Auffüllung, dedup, cap 50
|
||||||
const composed: Record<CountryKey, string[]> = {} as Record<
|
const composed: Record<CountryKey, string[]> = {} as Record<
|
||||||
CountryKey,
|
CountryKey,
|
||||||
@ -58,7 +72,13 @@ export default defineEventHandler(async (event) => {
|
|||||||
>;
|
>;
|
||||||
|
|
||||||
for (const country of COUNTRY_KEYS) {
|
for (const country of COUNTRY_KEYS) {
|
||||||
const globalList: string[] = GLOBAL_LISTS[country] ?? [];
|
// 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)
|
// Gekappte Custom-Domains zuerst (bereits dedupliziert da aus DB)
|
||||||
const merged: string[] = [...cappedCustom];
|
const merged: string[] = [...cappedCustom];
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user