- Neuer Endpoint /api/magic/me liefert nickname/avatar/plan fuer Hub-Header. Mac-App ruft fetchMe() beim Hub-Load. - DeviceHubView Header zeigt jetzt Avatar (AsyncImage mit Fallback auf Initial-Letter), Nickname + Plan-Badge statt nur 'ReBreak Magic'. - /api/magic/devices erweitert: listet zusaetzlich UserDevice-Rows mit boundToPlan != null (das sind iPhone/iPad aus dem Native-App-Login- Flow, Legend-Device-Lock). source='locked'. - Dedupe: ProtectedDevice wird unterdrueckt wenn bereits ein UserDevice mit aehnlichem Namen + gleicher Plattform existiert (fixt doppelten MacBook im Hub). - Helper prettyPlatform() + Normalisierung (platform-key 'mac'/'ios'/ 'android'/'win') fuer robusten Vergleich.
146 lines
4.6 KiB
TypeScript
146 lines
4.6 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 bound-Devices (Pro/Legend-Lock). Magic-only rows kommen
|
|
// \u00fcber `magic` rein \u2014 wir wollen hier die nicht-magic Lock-Bindings.
|
|
boundToPlan: { not: null },
|
|
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;
|
|
}
|
|
}
|