import { usePrisma } from "../utils/prisma"; export interface ProtectedDeviceRecord { id: string; platform: string; label: string; status: string; installedAt: Date | null; degradedAt: Date | null; createdAt: Date; } export interface ProtectedDeviceWithToken extends ProtectedDeviceRecord { dnsToken: string; userId: string; } const DEVICE_SELECT = { id: true, platform: true, label: true, status: true, installedAt: true, degradedAt: true, createdAt: true, } as const; const DEVICE_SELECT_WITH_TOKEN = { ...DEVICE_SELECT, dnsToken: true, userId: true, } as const; /** Alle nicht-revoked Devices eines Users, neueste zuerst. */ export async function listProtectedDevices( userId: string, ): Promise { const db = usePrisma(); return db.protectedDevice.findMany({ where: { userId, status: { not: "revoked" } }, orderBy: { createdAt: "desc" }, select: DEVICE_SELECT, }); } /** Anzahl der aktiven+pending Devices für Limit-Check (degraded zählt NICHT — Slot freigegeben). */ export async function countActiveProtectedDevices( userId: string, ): Promise { 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 { const db = usePrisma(); return db.protectedDevice.findUnique({ where: { id }, select: DEVICE_SELECT_WITH_TOKEN, }); } /** Lookup by dnsToken — für DoH-Blocklist-Endpoint (Token aus URL). */ export async function getProtectedDeviceByToken( dnsToken: string, ): Promise { const db = usePrisma(); return db.protectedDevice.findUnique({ where: { dnsToken }, select: DEVICE_SELECT_WITH_TOKEN, }); } /** Anlegen eines neuen ProtectedDevice (status=pending). */ export async function createProtectedDevice(opts: { userId: string; dnsToken: string; platform: string; label: string; }): Promise { const db = usePrisma(); return db.protectedDevice.create({ data: { userId: opts.userId, dnsToken: opts.dnsToken, platform: opts.platform, label: opts.label, status: "pending", }, select: DEVICE_SELECT_WITH_TOKEN, }); } /** User bestätigt Installation — setzt installedAt + status=active. */ export async function confirmProtectedDeviceInstalled( id: string, userId: string, ): Promise { 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: DEVICE_SELECT, }); } /** Soft-delete: setzt status=revoked + revokedAt. Ownership-check via userId. */ export async function revokeProtectedDevice( id: string, userId: string, ): Promise { 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; } /** * DoH-Handshake: lookup by dnsToken, flip pending→active, always update lastDnsQueryAt. * * Returns: * { found: false } → Token unbekannt * { found: true, statusChanged: false, ... } → war schon active/degraded, nur lastDnsQueryAt updated * { found: true, statusChanged: true, ... } → war pending, jetzt active * { found: true, revoked: true } → Token existiert aber revoked → ignorieren */ export async function handshakeProtectedDevice(dnsToken: string): Promise< | { found: false } | { found: true; revoked: true } | { found: true; revoked: false; statusChanged: boolean; status: string } > { const db = usePrisma(); const device = await db.protectedDevice.findUnique({ where: { dnsToken } }); if (!device) return { found: false }; if (device.status === "revoked") return { found: true, revoked: true }; const now = new Date(); const updateData: Record = { lastDnsQueryAt: now }; const wasPending = device.status === "pending"; if (wasPending) { updateData.status = "active"; updateData.installedAt = now; } await db.protectedDevice.update({ where: { id: device.id }, data: updateData }); return { found: true, revoked: false, statusChanged: wasPending, status: wasPending ? "active" : device.status }; } /** * Prüft ob ein Token nach der 14-Tage-Grace-Period in Passthrough-Modus ist. * Wird vom DoH-Blocklist-Endpoint aufgerufen. * * Returns: * 'active' → volle Blocklist liefern * 'grace' → volle Blocklist liefern (innerhalb 14-Tage-Grace) * 'passthrough' → nur minimale/leere Liste liefern * 'revoked' → Token unbekannt oder revoked → Passthrough */ export async function getDeviceBlocklistMode( dnsToken: string, ): Promise<"active" | "grace" | "passthrough" | "revoked"> { const device = await getProtectedDeviceByToken(dnsToken); if (!device) return "revoked"; if (device.status === "revoked") return "revoked"; if (device.status === "active" || device.status === "pending") return "active"; if (device.status === "degraded") { const GRACE_MS = 14 * 24 * 60 * 60 * 1000; const gracedAt = device.degradedAt ? device.degradedAt.getTime() + GRACE_MS : 0; if (Date.now() <= gracedAt) return "grace"; return "passthrough"; } return "passthrough"; }