import { listMagicDevices } from "../../db/devices"; import { listProtectedDevices } from "../../db/protectedDevices"; import { usePrisma } from "../../utils/prisma"; import { requireUser } from "../../utils/auth"; /** * GET /api/magic/devices * * Vereinigt drei Quellen f\u00fcr "registriertes Ger\u00e4t": * - "magic" \u2192 UserDevice mit magicEnrolledAt (Magic-Mac-App) * - "locked" \u2192 UserDevice mit boundToPlan (Native-App Device-Lock, z.B. iPhone/iPad) * - "protected" \u2192 ProtectedDevice (alter Native-App DNS-Schutz-Flow) * * Dedupe: ProtectedDevice wird unterdr\u00fcckt wenn bereits ein UserDevice * mit \u00e4hnlichem Namen + gleicher Plattform existiert (verhindert MacBook-Doppel). */ export default defineEventHandler(async (event) => { const user = await requireUser(event); const db = usePrisma(); const [magic, lockedDevices, protectedDevices] = await Promise.all([ listMagicDevices(user.id), db.userDevice.findMany({ where: { userId: user.id, // Alle Native-App-Geräte des Users \u2014 KEINE magic-only Rows // (die kommen über `magic`). Lock-Status ist egal: free/legend, alle // Native-App-Devices sollen im Hub erscheinen. magicEnrolledAt: null, }, orderBy: [{ lastSeenAt: "desc" }, { createdAt: "desc" }], select: { id: true, deviceId: true, platform: true, model: true, name: true, osVersion: true, lastSeenAt: true, releaseRequestedAt: true, }, }), listProtectedDevices(user.id), ]); const magicItems = magic.map((d) => { let releaseAvailableAt: string | null = null; if (d.releaseRequestedAt) { releaseAvailableAt = new Date( d.releaseRequestedAt.getTime() + 24 * 60 * 60 * 1000, ).toISOString(); } return { source: "magic" as const, deviceId: d.deviceId, hostname: d.hostname ?? "Unbenanntes Ger\u00e4t", model: d.model, osVersion: d.osVersion, magicEnrolledAt: d.magicEnrolledAt.toISOString(), releaseRequestedAt: d.releaseRequestedAt?.toISOString() ?? null, releaseAvailableAt, }; }); const lockedItems = lockedDevices.map((d) => { let releaseAvailableAt: string | null = null; if (d.releaseRequestedAt) { releaseAvailableAt = new Date( d.releaseRequestedAt.getTime() + 24 * 60 * 60 * 1000, ).toISOString(); } return { source: "locked" as const, deviceId: d.deviceId, hostname: d.name ?? d.model ?? prettyPlatform(d.platform), model: d.model, osVersion: d.osVersion, magicEnrolledAt: d.lastSeenAt.toISOString(), releaseRequestedAt: d.releaseRequestedAt?.toISOString() ?? null, releaseAvailableAt, }; }); // Dedupe Helper \u2014 normalisiere platform + name f\u00fcr Vergleich const norm = (s: string | null | undefined) => (s ?? "").toLowerCase().replace(/[^a-z0-9]/g, ""); const platformKey = (p: string | null | undefined) => { const n = norm(p); if (n.startsWith("mac") || n === "darwin") return "mac"; if (n.startsWith("ios") || n.startsWith("ipad") || n.startsWith("iphone")) return "ios"; if (n.startsWith("android")) return "android"; if (n.startsWith("windows") || n === "win") return "win"; return n; }; const alreadyListed = [...magicItems, ...lockedItems].map((d) => ({ pk: platformKey(d.model ?? d.hostname), nameNorm: norm(d.hostname), })); const protectedItems = protectedDevices .filter((pd) => { const pk = platformKey(pd.platform); const labelNorm = norm(pd.label); const dup = alreadyListed.some((u) => { if (u.pk !== pk) return false; if (!u.nameNorm || !labelNorm) return u.pk === pk; return ( u.nameNorm.includes(labelNorm) || labelNorm.includes(u.nameNorm) ); }); return !dup; }) .map((d) => ({ source: "protected" as const, deviceId: d.id, hostname: d.label, model: d.platform, osVersion: null as string | null, magicEnrolledAt: (d.installedAt ?? d.createdAt).toISOString(), releaseRequestedAt: null as string | null, releaseAvailableAt: null as string | null, })); return { success: true, data: [...magicItems, ...lockedItems, ...protectedItems], }; }); function prettyPlatform(p: string): string { switch (p.toLowerCase()) { case "ios": return "iPhone / iPad"; case "android": return "Android-Ger\u00e4t"; case "mac": case "macos": case "darwin": return "Mac"; default: return p; } }