AdGuard validates client IDs as DNS labels: 'invalid clientid: bad hostname label rune'. base64url alphabet contains '_' which fails. Switch to hex (a-f, 0-9) — 32 bytes hex = 64 chars, 256-bit entropy, DNS-safe.
143 lines
4.1 KiB
TypeScript
143 lines
4.1 KiB
TypeScript
import { randomBytes } from "crypto";
|
|
import { countActiveMagicBindings, listMagicDevices, MAGIC_DEVICE_LIMIT } from "../../db/devices";
|
|
import { requireUser } from "../../utils/auth";
|
|
import { createAdGuardClient } from "../../utils/adguard";
|
|
|
|
/**
|
|
* POST /api/magic/register
|
|
*
|
|
* Body: { deviceId: string, hostname: string, model?: string, osVersion?: string }
|
|
*
|
|
* Mac-App ruft nach Login auf. Registriert das Device als Magic-Client,
|
|
* generiert DNS-Token und provisioniert AdGuard Persistent Client.
|
|
*
|
|
* Idempotent: wenn bereits gebunden → return existing token.
|
|
* Wenn Limit erreicht → 409 mit activeBindings-Liste.
|
|
*/
|
|
export default defineEventHandler(async (event) => {
|
|
const user = await requireUser(event);
|
|
const body = await readBody(event);
|
|
const { deviceId, hostname, model, osVersion } = body as {
|
|
deviceId?: string;
|
|
hostname?: string;
|
|
model?: string;
|
|
osVersion?: string;
|
|
};
|
|
|
|
if (!deviceId || !hostname) {
|
|
throw createError({
|
|
statusCode: 400,
|
|
message: "deviceId und hostname required",
|
|
});
|
|
}
|
|
|
|
const db = usePrisma();
|
|
|
|
// 1. Prüfe ob Device bereits als Magic-Client gebunden ist (idempotent)
|
|
const existing = await db.userDevice.findUnique({
|
|
where: { userId_deviceId: { userId: user.id, deviceId } },
|
|
select: {
|
|
id: true,
|
|
userId: true,
|
|
magicDnsToken: true,
|
|
magicEnrolledAt: true,
|
|
magicRevokedAt: true,
|
|
},
|
|
});
|
|
|
|
// Wenn Token existiert und nicht revoked → return existing
|
|
if (
|
|
existing?.magicDnsToken &&
|
|
existing.magicEnrolledAt &&
|
|
!existing.magicRevokedAt
|
|
) {
|
|
return {
|
|
success: true,
|
|
data: {
|
|
deviceId,
|
|
dnsToken: existing.magicDnsToken,
|
|
profileUrl: `/api/magic/profile.mobileconfig?token=${existing.magicDnsToken}`,
|
|
existing: true,
|
|
},
|
|
};
|
|
}
|
|
|
|
// 2. Limit-Check (nur wenn kein vorheriges Binding existiert)
|
|
if (!existing || !existing.magicEnrolledAt) {
|
|
const activeCount = await countActiveMagicBindings(user.id);
|
|
if (activeCount >= MAGIC_DEVICE_LIMIT) {
|
|
const activeBindings = await listMagicDevices(user.id);
|
|
throw createError({
|
|
statusCode: 409,
|
|
message: `Magic-Device-Limit erreicht (max ${MAGIC_DEVICE_LIMIT})`,
|
|
data: {
|
|
code: "limit_reached",
|
|
activeBindings,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
// 3. Generiere DNS-Token (64 char hex)
|
|
// WICHTIG: hex statt base64url — AdGuard's clientid muss DNS-Label-konform sein,
|
|
// verbietet `_` (das base64url als Ersatz für `/` generiert) → 400 "bad hostname label rune".
|
|
const dnsToken = randomBytes(32).toString("hex");
|
|
|
|
// 4. Provisioniere AdGuard Client
|
|
const adguardClientName = `magic_${deviceId}`;
|
|
try {
|
|
await createAdGuardClient(adguardClientName, dnsToken, {
|
|
use_global_settings: false,
|
|
filtering_enabled: true,
|
|
parental_enabled: false,
|
|
safebrowsing_enabled: true,
|
|
blocked_services: [], // TODO: Gambling-Filter via AdGuard Blocked-Services
|
|
});
|
|
} catch (err: any) {
|
|
console.error("[Magic] AdGuard provisioning failed:", err);
|
|
throw createError({
|
|
statusCode: 502,
|
|
message: "DNS-Provisioning fehlgeschlagen",
|
|
});
|
|
}
|
|
|
|
// 5. Upsert UserDevice (platform="macos")
|
|
const device = await db.userDevice.upsert({
|
|
where: { userId_deviceId: { userId: user.id, deviceId } },
|
|
create: {
|
|
userId: user.id,
|
|
deviceId,
|
|
platform: "macos",
|
|
model: model ?? null,
|
|
name: hostname,
|
|
osVersion: osVersion ?? null,
|
|
magicDnsToken: dnsToken,
|
|
magicEnrolledAt: new Date(),
|
|
magicHostname: hostname,
|
|
},
|
|
update: {
|
|
magicDnsToken: dnsToken,
|
|
magicEnrolledAt: new Date(),
|
|
magicRevokedAt: null, // Clear falls vorher revoked
|
|
magicHostname: hostname,
|
|
model: model ?? undefined,
|
|
osVersion: osVersion ?? undefined,
|
|
lastSeenAt: new Date(),
|
|
},
|
|
select: {
|
|
deviceId: true,
|
|
magicDnsToken: true,
|
|
},
|
|
});
|
|
|
|
return {
|
|
success: true,
|
|
data: {
|
|
deviceId: device.deviceId,
|
|
dnsToken: device.magicDnsToken,
|
|
profileUrl: `/api/magic/profile.mobileconfig?token=${device.magicDnsToken}`,
|
|
existing: false,
|
|
},
|
|
};
|
|
});
|