166 lines
5.2 KiB
TypeScript

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 now = new Date();
const magicItems = magic.map((d) => {
let releaseAvailableAt: string | null = null;
if (d.releaseRequestedAt) {
releaseAvailableAt = new Date(
d.releaseRequestedAt.getTime() + 24 * 60 * 60 * 1000,
).toISOString();
}
const inCooldown = d.magicCooldownUntil && d.magicCooldownUntil > now;
const status = d.magicRevokedAt
? "revoked"
: inCooldown
? "cooldown"
: d.magicEnrolledAt
? "active"
: "pending";
return {
source: "magic" as const,
deviceId: d.deviceId,
hostname: d.hostname ?? "Unbenanntes Ger\u00e4t",
model: d.model,
osVersion: d.osVersion,
magicEnrolledAt: d.magicEnrolledAt?.toISOString() ?? null,
releaseRequestedAt: d.releaseRequestedAt?.toISOString() ?? null,
releaseAvailableAt,
cooldownUntil: d.magicCooldownUntil?.toISOString() ?? null,
status,
lastSeenAt: d.lastSeenAt?.toISOString() ?? null,
};
});
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,
status: "active" as const,
lastSeenAt: d.lastSeenAt?.toISOString() ?? null,
cooldownUntil: null,
};
});
// 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,
status: "active" as const,
lastSeenAt: null,
cooldownUntil: 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;
}
}