chahinebrini c1edef8abd feat(magic): RebreakMagic device-binding + DNS profile
- backend: /api/magic/{register,devices,profile,release} + AdGuard provisioning + 24h cooldown
- prisma: magic_binding_fields migration (additive on UserDevice)
- mac-app: Phase 2 - Login + MacRegistration + Profile install
- marketing: landing section + /download/rebreakmagic + DMG
- lyra: forbidden phrases + RebreakMagic coach guidance
2026-06-02 09:15:19 +02:00

139 lines
3.8 KiB
TypeScript

import { randomBytes } from 'crypto';
/**
* 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 (48 char base64url-safe)
const dnsToken = randomBytes(36).toString('base64url');
// 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,
},
};
});