import { usePrisma } from "../utils/prisma"; import pg from "pg"; import { randomUUID } from "node:crypto"; import { upsertDeviceProtectionState } from "./device-protection"; const { Pool } = pg; let _mdmPool: pg.Pool | null = null; /** * Lazily initialised pg.Pool against the NanoMDM Postgres. * Connection string comes from runtimeConfig.mdmDatabaseUrl. */ function useMdmPool(): pg.Pool { if (_mdmPool) return _mdmPool; const config = useRuntimeConfig(); const connectionString = (config as any).mdmDatabaseUrl; if (!connectionString) { throw new Error("MDM_DATABASE_URL not configured"); } _mdmPool = new Pool({ connectionString, // NanoMDM queries are point lookups — keep pool small. max: 5, connectionTimeoutMillis: 5000, query_timeout: 5000, }); return _mdmPool; } export interface UserDeviceMdmRecord { id: string; userId: string; deviceId: string; platform: string; mdmId: string | null; } const USER_DEVICE_MDM_SELECT = { id: true, userId: true, deviceId: true, platform: true, mdmId: true, } as const; const USER_DEVICE_MDM_HEALTH_SELECT = { id: true, userId: true, deviceId: true, platform: true, mdmId: true, mdmEnrolled: true, mdmSupervised: true, mdmLastSeenAt: true, } as const; /** * Find a user's iOS device by Capacitor deviceId. */ export async function getUserDeviceByDeviceId( userId: string, deviceId: string, platform: string = "ios", ): Promise { const db = usePrisma(); return db.userDevice.findFirst({ where: { userId, deviceId, platform }, select: USER_DEVICE_MDM_SELECT, }); } /** * Persist the NanoMDM UDID for a user's device. */ export async function setUserDeviceMdmId( userId: string, deviceId: string, mdmId: string, ): Promise { const db = usePrisma(); await db.userDevice.updateMany({ where: { userId, deviceId, platform: "ios" }, data: { mdmId }, }); } /** * Clear the stored NanoMDM UDID (e.g. device no longer enrolled). */ export async function clearUserDeviceMdmId( userId: string, deviceId: string, ): Promise { const db = usePrisma(); await db.userDevice.updateMany({ where: { userId, deviceId, platform: "ios" }, data: { mdmId: null }, }); } /** * Load all iOS devices that have a NanoMDM UDID link. */ export async function getLinkedUserDevices(): Promise< UserDeviceMdmHealthRecord[] > { const db = usePrisma(); return db.userDevice.findMany({ where: { platform: "ios", mdmId: { not: null } }, select: USER_DEVICE_MDM_HEALTH_SELECT, }); } export interface MdmDeviceStatus { enrolled: boolean; exists: boolean; company: string | null; supervised: boolean; tokenUpdateAt: Date | null; lastAckAt: Date | null; lastAppPushAt: Date | null; } export interface MdmEnrollmentStatus { enrolled: boolean; supervised: boolean; lastSeenAt: Date | null; } export interface UserDeviceMdmHealthRecord { id: string; userId: string; deviceId: string; platform: string; mdmId: string | null; mdmEnrolled: boolean | null; mdmSupervised: boolean | null; mdmLastSeenAt: Date | null; } /** * Query NanoMDM Postgres for a device by UDID. * * Throws if the MDM DB is unreachable — callers should treat this as an * infra/runtime error and not cache a negative result. */ export async function getMdmStatusByUdid( udid: string, ): Promise { const pool = useMdmPool(); // Defensive: only raw parameters reach the query layer below. const result = await pool.query<{ unlock_token: Buffer | null; token_update_at: Date | null; last_ack: Date | null; last_app_push_at: Date | null; enrolled: boolean; }>( `SELECT d.unlock_token, d.token_update_at, COALESCE(e.enabled = TRUE, FALSE) AS enrolled, (SELECT max(updated_at) FROM command_results WHERE id = d.id) AS last_ack, (SELECT max(r.updated_at) FROM command_results r JOIN commands c ON c.command_uuid = r.command_uuid WHERE r.id = d.id AND c.request_type = 'InstallApplication' AND r.status = 'Acknowledged') AS last_app_push_at FROM devices d LEFT JOIN enrollments e ON e.device_id = d.id WHERE d.id = $1`, [udid], ); const row = result.rows[0]; const exists = row !== undefined; const enrolled = row?.enrolled ?? false; return { enrolled, exists, company: enrolled ? "ReBreak" : null, supervised: exists && row?.unlock_token != null, tokenUpdateAt: row?.token_update_at ?? null, lastAckAt: row?.last_ack ?? null, lastAppPushAt: row?.last_app_push_at ?? null, }; } /** * Bulk-query NanoMDM for enrollment/supervision/last-seen status. * Returns a map keyed by UDID. Missing devices are omitted. * * Throws if the MDM DB is unreachable — callers should treat this as an * infra/runtime error and not cache a negative result. */ export async function getMdmEnrollmentStatusesByUdids( udids: string[], ): Promise> { if (udids.length === 0) { return new Map(); } const pool = useMdmPool(); const result = await pool.query<{ udid: string; enrolled: boolean; supervised: boolean; last_seen_at: Date | null; }>( `SELECT d.id AS udid, COALESCE(e.enabled = TRUE, FALSE) AS enrolled, (d.unlock_token IS NOT NULL) AS supervised, e.last_seen_at FROM devices d LEFT JOIN enrollments e ON e.device_id = d.id WHERE d.id = ANY($1::text[])`, [udids], ); const map = new Map(); for (const row of result.rows) { map.set(row.udid, { enrolled: row.enrolled, supervised: row.supervised, lastSeenAt: row.last_seen_at, }); } return map; } /** * Persist mirrored MDM health status on a UserDevice row. */ export async function updateUserDeviceMdmHealth( id: string, status: MdmEnrollmentStatus, ): Promise { const db = usePrisma(); await db.userDevice.update({ where: { id }, data: { mdmEnrolled: status.enrolled, mdmSupervised: status.supervised, mdmLastSeenAt: status.lastSeenAt, }, }); } // ─── ProfileList-based lock-profile health check ───────────────────────────── const LOCK_PROFILE_ID = "org.rebreak.protection.contentfilter.sideload"; /** * Send a ProfileList command to a device via the NanoMDM HTTP API. * Returns true if the command was accepted by NanoMDM. */ export async function enqueueProfileListCommand(udid: string): Promise { const config = useRuntimeConfig(); const baseUrl = (config as any).mdmApiUrl as string | undefined; const apiKey = (config as any).mdmApiKey as string | undefined; if (!baseUrl || !apiKey) { throw new Error("MDM_API_URL or MDM_API_KEY not configured"); } const commandUuid = randomUUID(); const plist = ` CommandUUID ${commandUuid} Command RequestType ProfileList `; const auth = Buffer.from(`nanomdm:${apiKey}`).toString("base64"); const url = `${baseUrl.replace(/\/$/, "")}/v1/enqueue/${encodeURIComponent(udid)}?push=1`; const res = await fetch(url, { method: "PUT", headers: { Authorization: `Basic ${auth}`, "Content-Type": "application/x-plist", }, body: plist, }); if (!res.ok) { const text = await res.text().catch(() => ""); throw new Error(`NanoMDM enqueue failed: ${res.status} ${res.statusText} ${text}`); } return true; } /** * Read the most recent ProfileList command result for a device. * Returns null if no result exists yet. */ export async function getLatestProfileListResult( udid: string, ): Promise<{ result: string; updatedAt: Date } | null> { const pool = useMdmPool(); const res = await pool.query<{ result: string; updated_at: Date; }>( `SELECT cr.result, cr.updated_at FROM command_results cr JOIN commands c ON c.command_uuid = cr.command_uuid WHERE cr.id = $1 AND c.request_type = 'ProfileList' AND cr.status = 'Acknowledged' ORDER BY cr.updated_at DESC LIMIT 1`, [udid], ); return res.rows[0] ?? null; } /** * Extract all PayloadIdentifier values from a ProfileList response plist * and check whether the ReBreak sideload lock profile is present. */ export function isLockProfileInstalled(xml: string): boolean { const regex = /PayloadIdentifier<\/key>\s*([^<]+)<\/string>/g; let match: RegExpExecArray | null; const identifiers: string[] = []; while ((match = regex.exec(xml)) !== null) { identifiers.push(match[1]); } return identifiers.includes(LOCK_PROFILE_ID); } /** * If the lock profile is missing according to a ProfileList response, * mark the device's nefilter protection state as inactive. */ export async function markNefilterInactiveIfLockProfileMissing( userId: string, deviceId: string, xml: string, lastSeenAt?: Date, ): Promise { if (isLockProfileInstalled(xml)) { return; } await upsertDeviceProtectionState( userId, deviceId, "ios", "nefilter", false, lastSeenAt ?? new Date(), "MDM ProfileList: lock profile not installed", "mdm", ); }