rebreak-monorepo/backend/server/db/protectedDevices.ts
chahinebrini 677b67902b feat(devices): protected device enrollment + mobileconfig generator
Backend:
- ProtectedDevice prisma model + migration add_protected_devices
- DB helpers: list/count/get/create/confirm/revoke
- mobileconfig.ts utility — XML-escape, unique UUIDs per request
- 5 endpoints under /api/devices/* (avoid /api/devices conflict with existing
  Capacitor UserDevice route by using /api/devices/protected for list)

Phase 1: backend ready. DoH-server token-routing comes in phase 2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 04:06:49 +02:00

140 lines
3.3 KiB
TypeScript

import { usePrisma } from "../utils/prisma";
export interface ProtectedDeviceRecord {
id: string;
platform: string;
label: string;
status: string;
installedAt: Date | null;
createdAt: Date;
}
export interface ProtectedDeviceWithToken extends ProtectedDeviceRecord {
dnsToken: string;
userId: string;
}
/** Alle nicht-revoked Devices eines Users, neueste zuerst. */
export async function listProtectedDevices(
userId: string,
): Promise<ProtectedDeviceRecord[]> {
const db = usePrisma();
return db.protectedDevice.findMany({
where: { userId, status: { not: "revoked" } },
orderBy: { createdAt: "desc" },
select: {
id: true,
platform: true,
label: true,
status: true,
installedAt: true,
createdAt: true,
},
});
}
/** Anzahl der aktiven+pending Devices für Limit-Check. */
export async function countActiveProtectedDevices(
userId: string,
): Promise<number> {
const db = usePrisma();
return db.protectedDevice.count({
where: { userId, status: { in: ["active", "pending"] } },
});
}
/** Lookup by id — inkl. dnsToken und userId (für mobileconfig-Generation + ownership-check). */
export async function getProtectedDevice(
id: string,
): Promise<ProtectedDeviceWithToken | null> {
const db = usePrisma();
return db.protectedDevice.findUnique({
where: { id },
select: {
id: true,
userId: true,
dnsToken: true,
platform: true,
label: true,
status: true,
installedAt: true,
createdAt: true,
},
});
}
/** Anlegen eines neuen ProtectedDevice (status=pending). */
export async function createProtectedDevice(opts: {
userId: string;
dnsToken: string;
platform: string;
label: string;
}): Promise<ProtectedDeviceWithToken> {
const db = usePrisma();
return db.protectedDevice.create({
data: {
userId: opts.userId,
dnsToken: opts.dnsToken,
platform: opts.platform,
label: opts.label,
status: "pending",
},
select: {
id: true,
userId: true,
dnsToken: true,
platform: true,
label: true,
status: true,
installedAt: true,
createdAt: true,
},
});
}
/** User bestätigt Installation — setzt installedAt + status=active. */
export async function confirmProtectedDeviceInstalled(
id: string,
userId: string,
): Promise<ProtectedDeviceRecord | null> {
const db = usePrisma();
const device = await db.protectedDevice.findFirst({
where: { id, userId, status: { not: "revoked" } },
});
if (!device) return null;
return db.protectedDevice.update({
where: { id },
data: {
status: "active",
installedAt: new Date(),
},
select: {
id: true,
platform: true,
label: true,
status: true,
installedAt: true,
createdAt: true,
},
});
}
/** Soft-delete: setzt status=revoked + revokedAt. Ownership-check via userId. */
export async function revokeProtectedDevice(
id: string,
userId: string,
): Promise<boolean> {
const db = usePrisma();
const device = await db.protectedDevice.findFirst({
where: { id, userId, status: { not: "revoked" } },
});
if (!device) return false;
await db.protectedDevice.update({
where: { id },
data: { status: "revoked", revokedAt: new Date() },
});
return true;
}