147 lines
3.8 KiB
TypeScript

import { usePrisma } from "../utils/prisma";
/**
* Device-Binding pro User. Free=1, Pro=1, Legend=3 (siehe plan-features.maxDevices).
* deviceId kommt vom Frontend via Capacitor Device.getId() (persistent UUID).
*/
export interface DeviceRecord {
id: string;
deviceId: string;
platform: string;
model: string | null;
name: string | null;
lastSeenAt: Date;
createdAt: Date;
}
/** Liste aller Devices eines Users, aktuellstes zuerst. */
export async function listUserDevices(userId: string): Promise<DeviceRecord[]> {
const db = usePrisma();
return db.userDevice.findMany({
where: { userId },
orderBy: { lastSeenAt: "desc" },
select: {
id: true,
deviceId: true,
platform: true,
model: true,
name: true,
lastSeenAt: true,
createdAt: true,
},
});
}
/** Gibt das Device zurück wenn registriert; sonst null. */
export async function findUserDevice(
userId: string,
deviceId: string,
): Promise<DeviceRecord | null> {
const db = usePrisma();
return db.userDevice.findUnique({
where: { userId_deviceId: { userId, deviceId } },
select: {
id: true,
deviceId: true,
platform: true,
model: true,
name: true,
lastSeenAt: true,
createdAt: true,
},
});
}
/**
* Idempotente Registrierung. Wenn Device bereits existiert: Touch lastSeenAt.
* Wenn nicht existiert UND Limit erreicht: throw mit Liste der existierenden Devices.
*/
export async function registerDevice(opts: {
userId: string;
deviceId: string;
platform: string;
model?: string | null;
name?: string | null;
maxDevices: number;
}): Promise<{
device: DeviceRecord;
created: boolean;
}> {
const db = usePrisma();
// Idempotent: existiert das Device schon?
const existing = await findUserDevice(opts.userId, opts.deviceId);
if (existing) {
// model/name beim Re-Register aktualisieren — User-Agent oder OS-Version
// kann sich geändert haben (App-Update, OS-Upgrade, iPad-Detection-Fix).
const updated = await db.userDevice.update({
where: { id: existing.id },
data: {
lastSeenAt: new Date(),
...(opts.model !== undefined && { model: opts.model }),
...(opts.name !== undefined && { name: opts.name }),
},
select: {
id: true,
deviceId: true,
platform: true,
model: true,
name: true,
lastSeenAt: true,
createdAt: true,
},
});
return { device: updated, created: false };
}
// Neues Device — Limit prüfen
const count = await db.userDevice.count({ where: { userId: opts.userId } });
if (count >= opts.maxDevices) {
throw Object.assign(new Error("device_limit_reached"), {
code: "DEVICE_LIMIT_REACHED",
currentCount: count,
max: opts.maxDevices,
});
}
const created = await db.userDevice.create({
data: {
userId: opts.userId,
deviceId: opts.deviceId,
platform: opts.platform,
model: opts.model ?? null,
name: opts.name ?? null,
},
select: {
id: true,
deviceId: true,
platform: true,
model: true,
name: true,
lastSeenAt: true,
createdAt: true,
},
});
return { device: created, created: true };
}
/** Touch lastSeenAt — wird in der Auth-Middleware bei jedem Request aufgerufen. */
export async function touchDevice(userId: string, deviceId: string): Promise<void> {
const db = usePrisma();
await db.userDevice
.updateMany({
where: { userId, deviceId },
data: { lastSeenAt: new Date() },
})
.catch(() => {
/* race-safe: wenn Device gerade gelöscht wurde */
});
}
/** User entfernt ein eigenes Device — gibt Slot frei. */
export async function deleteUserDevice(userId: string, id: string): Promise<void> {
const db = usePrisma();
await db.userDevice.deleteMany({ where: { id, userId } });
}