172 lines
5.3 KiB
TypeScript
172 lines
5.3 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,
|
|
hardwareId: true,
|
|
platform: true,
|
|
model: true,
|
|
name: true,
|
|
osVersion: true,
|
|
mdmId: 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,
|
|
hardwareId: d.hardwareId,
|
|
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,
|
|
mdmId: d.mdmId,
|
|
};
|
|
});
|
|
|
|
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,
|
|
hardwareId: d.hardwareId,
|
|
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,
|
|
mdmId: d.mdmId,
|
|
};
|
|
});
|
|
|
|
// 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;
|
|
}
|
|
}
|