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>
108 lines
2.8 KiB
TypeScript
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;
|
|
}
|