rebreak-monorepo/backend/server/db/curatedDomains.ts
chahinebrini 8f2ef2cc98 feat(mdm,vip): MDM-VPN-Pivot + Layer-2-Country-Curated + Custom-Domain-Refactor
MDM-VPN-Pivot (Phase F.2 done):
- ops/mdm/profiles/rebreak-iphone-protection.mobileconfig auf v5 mit
  com.apple.vpn.managed Payload + OnDemandUserOverrideDisabled. iPhone-User
  kann ReBreak-VPN-Profile nicht entfernen und "Bedarf verbinden"-Toggle
  ist disabled. allowEnablingRestrictions empirisch widerlegt für FC-Toggle-
  Lock — out.
- DEV-removable Variante als Test-Profile dazu.
- Bootstrap-Tool (rebreak-supervise.sh) + Supervision-Identity-Setup-Doc.
- PHASES.md updated mit empirischen Befunden.

App-side MDM-Detect (Pfad-a Banner-Logic):
- modules/rebreak-protection: getDeviceState() returnt mdmManaged via
  Heuristik NETunnelProviderManager.count > 1 (App selbst kann nur einen
  eigenen erstellen, MDM-Push fügt einen zweiten hinzu).
- DeviceLayers.mdmManaged?: boolean Type.
- blocker.tsx: lockedIn-Bedingung erweitert um mdmManaged. Bei MDM-managed
  iPhones wird der App-Lock-Card (FC-Authorization-Toggle UI) ausgeblendet
  weil der per-App FC-Toggle nicht lockbar ist und durch den MDM-VPN-Layer
  redundant.

Layer-2-Country-Curated-Pivot:
- backend: vip-swap.post.ts raus, suggest.post.ts rein. Curated-domains
  durch admin (separate Tabelle/Pfad), getrennt von User-Custom-Domains.
- Admin-APIs für curated-domain Pflege (index.get + [id].patch).
- seed-country-blocklists Script für initiale Curated-Domain-Liste.
- protection/webcontent-domains.get refactored für Country-Curated-Pfad.
- Migration drop_vip_swap_fields.sql + schema.prisma adjusted.
- docs/concepts/layer2-country-pivot.md mit Architektur + Decision-Trail.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 07:11:47 +02:00

108 lines
2.8 KiB
TypeScript

import { usePrisma } from "../utils/prisma";
export type CuratedDomainStatus = "suggested" | "approved" | "rejected";
/**
* User schlägt eine Domain für die Country-Curated-Layer-2-Liste vor.
* Wirft 409 wenn domain+country bereits existiert (egal welcher Status).
*/
export async function suggestCuratedDomain(
suggestedByUserId: string,
domain: string,
country: string,
) {
const db = usePrisma();
// Existiert bereits? Statusabhängige Antwort
const existing = await db.curatedDomain.findUnique({
where: { country_domain: { country, domain } },
select: { id: true, status: true },
});
if (existing) {
// Bereits approved → User muss wissen dass es schon aktiv ist
if (existing.status === "approved") {
return { id: existing.id, domain, country, alreadyApproved: true };
}
// Bereits suggested oder rejected → idempotent zurückgeben
return { id: existing.id, domain, country, alreadySuggested: true };
}
const row = await db.curatedDomain.create({
data: {
domain,
country,
status: "suggested",
suggestedByUserId,
},
select: { id: true, domain: true, country: true, status: true, createdAt: true },
});
return row;
}
/**
* Holt alle CuratedDomain-Einträge für die Admin-Inbox.
* Ohne status-Filter: alle. Mit status="suggested" → nur offene Vorschläge.
*/
export async function getCuratedDomains(
filters: { status?: CuratedDomainStatus; country?: string } = {},
) {
const db = usePrisma();
return db.curatedDomain.findMany({
where: {
...(filters.status ? { status: filters.status } : {}),
...(filters.country ? { country: filters.country } : {}),
},
orderBy: [{ status: "asc" }, { createdAt: "asc" }],
select: {
id: true,
domain: true,
country: true,
status: true,
suggestedByUserId: true,
createdAt: true,
reviewedAt: true,
},
});
}
/**
* Admin: Domain-Vorschlag genehmigen oder ablehnen.
* reviewNote ist optional (für Reject-Begründung).
*/
export async function decideCuratedDomain(
id: string,
decision: "approved" | "rejected",
reviewNote?: string,
) {
const db = usePrisma();
const existing = await db.curatedDomain.findUnique({
where: { id },
select: { id: true, status: true, domain: true, country: true },
});
if (!existing) {
throw Object.assign(new Error("CuratedDomain not found"), { code: "NOT_FOUND" });
}
if (existing.status !== "suggested") {
throw Object.assign(
new Error("Domain already decided"),
{ code: "ALREADY_DECIDED", currentStatus: existing.status },
);
}
const updated = await db.curatedDomain.update({
where: { id },
data: {
status: decision,
reviewedAt: new Date(),
},
select: { id: true, domain: true, country: true, status: true, reviewedAt: true },
});
return updated;
}