chahinebrini 677b67902b feat(devices): protected device enrollment + mobileconfig generator
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>
2026-05-11 04:06:49 +02:00

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, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&apos;");
}
/** 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>`;
}