147 lines
3.8 KiB
TypeScript
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 } });
|
|
}
|