- Schema: lyraVoiceId stays, new os_version column on user_devices (Migration 20260515) - registerDevice() merge-heuristic: if existing record matches userId + same name + same model + lastSeen < 30 days, update existing instead of inserting new. Fixes iOS IDFV-reset creating phantom devices on Recovery-Restore. - register.post.ts: accepts osVersion in body, maps isCurrent in error-path payload - New util testUser.ts: isTestUser(email) — explicit allowlist for charioanouar@gmail.com plus existing @rebreak.internal suffix Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
71 lines
2.2 KiB
TypeScript
71 lines
2.2 KiB
TypeScript
import { listUserDevices, registerDevice } from "../../db/devices";
|
|
import { getProfile } from "../../db/profile";
|
|
import { getPlanLimits } from "../../utils/plan-features";
|
|
|
|
/**
|
|
* POST /api/devices/register
|
|
*
|
|
* Body: { deviceId: string, platform: string, model?: string, name?: string }
|
|
*
|
|
* Idempotent: gleiche deviceId für gleichen User → updated lastSeenAt + 200.
|
|
* Wenn neues Device + Limit erreicht → 403 mit { error, devices } damit der
|
|
* Frontend-Drawer dem User die Wahl gibt, welches Gerät er freigibt.
|
|
*/
|
|
export default defineEventHandler(async (event) => {
|
|
// Bootstrap: kein Device-Check sonst wäre erstes Register unmöglich (chicken-egg)
|
|
const user = await requireUser(event, { skipDeviceCheck: true });
|
|
const body = await readBody(event);
|
|
const { deviceId, platform, model, name, osVersion } = body as {
|
|
deviceId?: string;
|
|
platform?: string;
|
|
model?: string;
|
|
name?: string;
|
|
osVersion?: string;
|
|
};
|
|
|
|
if (!deviceId || !platform) {
|
|
throw createError({
|
|
statusCode: 400,
|
|
message: "deviceId und platform required",
|
|
});
|
|
}
|
|
if (!["ios", "android", "web"].includes(platform)) {
|
|
throw createError({ statusCode: 400, message: "invalid platform" });
|
|
}
|
|
|
|
const profile = await getProfile(user.id);
|
|
const limits = getPlanLimits(profile?.plan ?? "free");
|
|
|
|
try {
|
|
const { device, created, merged } = await registerDevice({
|
|
userId: user.id,
|
|
deviceId,
|
|
platform,
|
|
model: model ?? null,
|
|
name: name ?? null,
|
|
osVersion: osVersion ?? null,
|
|
maxDevices: limits.maxAppDevices,
|
|
});
|
|
return { device, created, merged: merged ?? false, max: limits.maxAppDevices };
|
|
} catch (err: any) {
|
|
if (err.code === "DEVICE_LIMIT_REACHED") {
|
|
const allDevices = await listUserDevices(user.id);
|
|
const devices = allDevices.map((d) => ({
|
|
...d,
|
|
isCurrent: d.deviceId === deviceId,
|
|
}));
|
|
throw createError({
|
|
statusCode: 403,
|
|
statusMessage: "device_limit_reached",
|
|
data: {
|
|
error: "device_limit_reached",
|
|
max: limits.maxAppDevices,
|
|
plan: profile?.plan ?? "free",
|
|
devices,
|
|
},
|
|
});
|
|
}
|
|
throw err;
|
|
}
|
|
});
|