Backend: - ProtectedDevice prisma model + migration add_protected_devices - DB helpers: list/count/get/create/confirm/revoke - mobileconfig.ts utility — XML-escape, unique UUIDs per request - 5 endpoints under /api/devices/* (avoid /api/devices conflict with existing Capacitor UserDevice route by using /api/devices/protected for list) Phase 1: backend ready. DoH-server token-routing comes in phase 2. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
104 lines
3.4 KiB
TypeScript
104 lines
3.4 KiB
TypeScript
import { randomUUID } from "crypto";
|
|
|
|
/** XML-Escape für User-Input in plist-Strings. */
|
|
function xmlEscape(str: string): string {
|
|
return str
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """)
|
|
.replace(/'/g, "'");
|
|
}
|
|
|
|
/** Slugify label für Content-Disposition filename. */
|
|
export function labelToSlug(label: string): string {
|
|
return label
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9]+/g, "-")
|
|
.replace(/^-+|-+$/g, "")
|
|
.slice(0, 40);
|
|
}
|
|
|
|
/**
|
|
* Generiert ein macOS DNS-Over-HTTPS Konfigurationsprofil (plist XML).
|
|
*
|
|
* Jeder Aufruf erzeugt neue random PayloadUUIDs — damit der Mac mehrere Profile
|
|
* unterscheiden kann falls ein User das Profil neu lädt.
|
|
*
|
|
* Phase 2: DoH-Server routet per dnsToken auf user-spezifische Blocklist.
|
|
*/
|
|
export function generateMacOSDnsProfile(opts: {
|
|
deviceId: string;
|
|
dnsToken: string;
|
|
label: string;
|
|
}): string {
|
|
const { deviceId, dnsToken, label } = opts;
|
|
|
|
// Unique UUIDs pro Generierung (nicht deviceId verwenden — Mac dedupliciert sonst)
|
|
const outerUUID = randomUUID().toUpperCase();
|
|
const innerUUID = randomUUID().toUpperCase();
|
|
|
|
// PayloadIdentifier: stabil pro Device (kein UUID hier — bleibt gleich bei Re-Download)
|
|
const tokenPrefix = dnsToken.slice(0, 8);
|
|
const payloadId = `org.rebreak.protection.dns.${tokenPrefix}`;
|
|
|
|
const displayName = xmlEscape(`ReBreak Schutz — ${label}`);
|
|
const innerDisplayName = xmlEscape("ReBreak DNS-Filter");
|
|
const description = xmlEscape(
|
|
"Leitet DNS-Anfragen über dns.rebreak.org. Glücksspiel-Domains werden blockiert.",
|
|
);
|
|
const outerDescription = xmlEscape(
|
|
"Aktiviert den ReBreak-DNS-Filter auf diesem Mac. Glücksspiel-Domains werden auf System-Ebene blockiert — gilt für alle Browser und alle Apps. Entfernen erfordert Admin-Passwort.",
|
|
);
|
|
const serverURL = `https://dns.rebreak.org/api/dns/${dnsToken}/dns-query`;
|
|
|
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
<plist version="1.0">
|
|
<dict>
|
|
<key>PayloadContent</key>
|
|
<array>
|
|
<dict>
|
|
<key>PayloadDisplayName</key>
|
|
<string>${innerDisplayName}</string>
|
|
<key>PayloadDescription</key>
|
|
<string>${description}</string>
|
|
<key>PayloadIdentifier</key>
|
|
<string>${payloadId}.dns</string>
|
|
<key>PayloadType</key>
|
|
<string>com.apple.dnsSettings.managed</string>
|
|
<key>PayloadUUID</key>
|
|
<string>${innerUUID}</string>
|
|
<key>PayloadVersion</key>
|
|
<integer>1</integer>
|
|
<key>DNSSettings</key>
|
|
<dict>
|
|
<key>DNSProtocol</key>
|
|
<string>HTTPS</string>
|
|
<key>ServerURL</key>
|
|
<string>${serverURL}</string>
|
|
</dict>
|
|
</dict>
|
|
</array>
|
|
<key>PayloadDisplayName</key>
|
|
<string>${displayName}</string>
|
|
<key>PayloadDescription</key>
|
|
<string>${outerDescription}</string>
|
|
<key>PayloadIdentifier</key>
|
|
<string>${payloadId}</string>
|
|
<key>PayloadOrganization</key>
|
|
<string>ReBreak</string>
|
|
<key>PayloadType</key>
|
|
<string>Configuration</string>
|
|
<key>PayloadUUID</key>
|
|
<string>${outerUUID}</string>
|
|
<key>PayloadVersion</key>
|
|
<integer>1</integer>
|
|
<key>PayloadScope</key>
|
|
<string>System</string>
|
|
<key>PayloadRemovalDisallowed</key>
|
|
<true/>
|
|
</dict>
|
|
</plist>`;
|
|
}
|