rebreak-monorepo/backend/server/api/magic/profile.mobileconfig.get.ts
chahinebrini a95e66560d feat(magic): Hard-Lock + Geräte-UX (Push, Realtime, Detail-Sheet, Offline-Removal)
Devices/Magic:
- Offline-Profil-Enroll deaktiviert (410) — Lock-PW würde im Klartext im
  Download landen; stationärer Schutz läuft jetzt nur über Rebreak Magic
- Mac-DNS-Template: ProhibitDisablement (Filter nicht abschaltbar)
- Push "Neues Gerät verbunden" an mobile Geräte bei neuer Bindung
- Realtime auf user_devices → Settings aktualisiert Magic-Bindings live
- Geräte-Detail-Sheet (Tap auf Gerät): Status, verbunden-seit, Schutz-Donut

Hard-Lock (server-gehaltenes Removal-PW, User sieht es nie):
- magic_removal_password generiert/gespeichert + in Profil injiziert (Lazy-Backfill)
- Reveal NUR bei Account-Löschung (user/delete) + Kündigung (stripe webhook),
  per Resend-Mail + in-Response
- Signing config-gated (inaktiv ohne Cert; Lock greift auch unsigniert)

Migrations: user_devices-Realtime-Publication + magic_removal_password-Spalten

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 22:26:25 +02:00

95 lines
3.1 KiB
TypeScript

import { randomUUID } from "crypto";
import {
findMagicDeviceByToken,
ensureMagicRemovalPassword,
} from "../../db/devices";
import { MAGIC_PROFILE_TEMPLATE } from "../../utils/magic-profile-template";
import {
buildRemovalPasswordPayload,
generateRemovalPassword,
signProfileIfConfigured,
} from "../../utils/magic-lock";
/**
* GET /api/magic/profile.mobileconfig?token=<dnsToken>
*
* Generiert das personalisierte, GESPERRTE DNS-Config-Profil für macOS/Windows.
* Template: ops/mdm/rebreak-mac-dns-filter.mobileconfig (inlined als TS const).
*
* Hard-Lock-Payloads:
* - DNS-Filter (com.apple.dnsSettings.managed) mit ProhibitDisablement
* - PayloadRemovalDisallowed + PayloadScope=System (im Template)
* - com.apple.profileRemovalPassword mit server-gehaltenem Passwort (hier
* injiziert) — der User sieht es NIE, nur nach Cooldown-Release (Offboarding).
*
* Signing: wenn Cert via runtimeConfig konfiguriert → CMS-signiert (grünes
* „Verifiziert"), sonst unsigniert (Lock greift trotzdem). Siehe magic-lock.ts.
*/
export default defineEventHandler(async (event) => {
const query = getQuery(event);
const token = query.token as string | undefined;
if (!token) {
throw createError({
statusCode: 400,
message: "token query parameter required",
});
}
const device = await findMagicDeviceByToken(token);
if (!device) {
throw createError({
statusCode: 404,
message: "Invalid or revoked DNS token",
});
}
// Removal-Passwort: aus DB oder Lazy-Backfill (Devices vor dem Hard-Lock).
let removalPassword = device.magicRemovalPassword;
if (!removalPassword) {
removalPassword = generateRemovalPassword();
await ensureMagicRemovalPassword(device.id, removalPassword);
}
const deviceSlice = device.deviceId.slice(0, 8);
// Personalisierung: ServerURL → token-spezifisch, UUIDs/Identifier unique.
let personalizedProfile = MAGIC_PROFILE_TEMPLATE.replace(
"https://dns.rebreak.org/dns-query",
`https://dns.rebreak.org/dns-query/${token}`,
)
.replace("7D2E8B1A-C3D4-4E76-8B23-A4B5C6D7E8F0", randomUUID().toUpperCase())
.replace("8C3F9A2B-D4E5-4F87-9A12-B5C6D7E8F901", randomUUID().toUpperCase())
.replace(
"org.rebreak.protection.dns.filter",
`org.rebreak.protection.dns.filter.${deviceSlice}`,
)
.replace(
"org.rebreak.protection.profile",
`org.rebreak.protection.profile.${deviceSlice}`,
);
// Removal-Passwort-Payload in die PayloadContent-Array injizieren.
const removalPayload = buildRemovalPasswordPayload(removalPassword, deviceSlice);
personalizedProfile = personalizedProfile.replace(
" </array>",
`${removalPayload}\n </array>`,
);
// Optional signieren (config-gated; inaktiv ohne Cert).
const config = useRuntimeConfig(event);
const signed = signProfileIfConfigured(
personalizedProfile,
(config as any).magicSigning,
);
setHeader(event, "Content-Type", "application/x-apple-aspen-config");
setHeader(
event,
"Content-Disposition",
`attachment; filename="RebreakMagic-${deviceSlice}.mobileconfig"`,
);
return signed;
});