chahinebrini 6962e09403 feat(devices): Windows 11 DoH protection — reg-file endpoint + tests
- Add server/utils/regfile.ts: generateWindowsDohRegFile() producing
  UTF-16 LE + BOM .reg content for DohWellKnownServers registry path.
  label and dohTemplate values are properly escape'd (\, ", \n, \r, \t).
- Add GET /api/devices/:id/profile.reg — public, windows-platform-gated,
  returns octet-stream with Content-Disposition attachment.
- Update enroll.post.ts: downloadUrl is now platform-aware
  (windows → .reg, all others → .mobileconfig).
- Add tests/devices/regfile.test.ts: 13 tests covering BOM, CRLF,
  token embed, subkey naming, AutoUpgradeFlag, label escaping (", \, \n),
  and labelToSlug edge cases. All 111 tests pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 04:48:51 +02:00

81 lines
2.4 KiB
TypeScript

import { randomBytes } from "crypto";
import { getProfile } from "../../db/profile";
import {
countActiveProtectedDevices,
createProtectedDevice,
} from "../../db/protectedDevices";
/**
* POST /api/devices/enroll
*
* Legend-only. User klickt "Mac hinzufügen" in der App.
* Legt ein ProtectedDevice (status=pending) an und gibt die Download-URL
* für das mobileconfig-Profil zurück.
*
* Body: { platform: "mac" | "windows" | "ios" | "android", label: string }
* Response: { deviceId, dnsToken, downloadUrl }
*/
export default defineEventHandler(async (event) => {
const user = await requireUser(event);
const profile = await getProfile(user.id);
if (profile?.plan !== "legend") {
throw createError({
statusCode: 403,
data: { error: "LEGEND_REQUIRED" },
});
}
const body = await readBody(event);
const platform = body?.platform as string | undefined;
const label = body?.label as string | undefined;
const VALID_PLATFORMS = ["mac", "windows", "ios", "android"];
if (!platform || !VALID_PLATFORMS.includes(platform)) {
throw createError({
statusCode: 400,
data: { error: "INVALID_PLATFORM", validValues: VALID_PLATFORMS },
});
}
if (!label || typeof label !== "string" || label.trim().length === 0) {
throw createError({ statusCode: 400, data: { error: "LABEL_REQUIRED" } });
}
const trimmedLabel = label.trim().slice(0, 100);
// Limit: max 3 active+pending Devices
const activeCount = await countActiveProtectedDevices(user.id);
if (activeCount >= 3) {
throw createError({
statusCode: 409,
data: { error: "DEVICE_LIMIT_REACHED", max: 3, current: activeCount },
});
}
// 32-char hex token — kryptografisch sicher
const dnsToken = randomBytes(16).toString("hex");
const device = await createProtectedDevice({
userId: user.id,
dnsToken,
platform,
label: trimmedLabel,
});
const config = useRuntimeConfig(event);
const apiBase =
(config.public as any)?.apiBase ?? "https://api.rebreak.org";
// Platform-aware download URL: Windows gets .reg, everything else .mobileconfig
const profileExt = platform === "windows" ? "reg" : "mobileconfig";
const downloadUrl = `${apiBase}/api/devices/${device.id}/profile.${profileExt}`;
return {
success: true,
data: {
deviceId: device.id,
dnsToken: device.dnsToken,
downloadUrl,
},
};
});