- devices: cleanupStaleDevices() purges phantoms >14d not bound; called from GET /api/devices + register limit-check. Deterministic sort (lastSeenAt, createdAt, id) so iPad+iPhone see identical order. Merge-cutoff tightened 30d -> 7d. - stats: rejected aggregates from notifications(type='domain_rejected') via Math.max — admin reject cascade-deletes submission row. - blocker.tsx: useDomainSubmissionRealtime triggers blockerStats.refresh() directly (not stale-check only) so info-sheet shows fresh rejected count.
35 lines
1.2 KiB
TypeScript
35 lines
1.2 KiB
TypeScript
import { listUserDevices, cleanupStaleDevices } from "../../db/devices";
|
|
import { getProfile } from "../../db/profile";
|
|
import { getPlanLimits } from "../../utils/plan-features";
|
|
|
|
/**
|
|
* GET /api/devices
|
|
*
|
|
* Liste aller registrierten Devices des Users + plan-limit + welches Device der
|
|
* aktuelle Caller ist (matched via x-device-id header).
|
|
*
|
|
* Ruft cleanupStaleDevices() VOR dem Listing damit Phantom-Devices (>30d inaktiv,
|
|
* nicht bound) nicht in der UI auftauchen.
|
|
*/
|
|
export default defineEventHandler(async (event) => {
|
|
// skipDeviceCheck: User der gerade vom Geräte-Limit blockt wird, soll trotzdem
|
|
// seine Devices-Liste sehen können um eines freizugeben (Chicken-Egg-Bypass).
|
|
const user = await requireUser(event, { skipDeviceCheck: true });
|
|
const profile = await getProfile(user.id);
|
|
const limits = getPlanLimits(profile?.plan ?? "free");
|
|
|
|
await cleanupStaleDevices(user.id);
|
|
|
|
const currentDeviceId = getHeader(event, "x-device-id") ?? null;
|
|
const devices = await listUserDevices(user.id);
|
|
|
|
return {
|
|
devices: devices.map((d) => ({
|
|
...d,
|
|
isCurrent: !!currentDeviceId && d.deviceId === currentDeviceId,
|
|
})),
|
|
max: limits.maxAppDevices,
|
|
plan: profile?.plan ?? "free",
|
|
};
|
|
});
|