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>
140 lines
3.3 KiB
TypeScript
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;
|
|
}
|