import { usePrisma } from "../utils/prisma"; import pg from "pg"; 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, queryTimeout: 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; /** * 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 }, }); } export interface MdmDeviceStatus { enrolled: boolean; company: string | null; tokenUpdateAt: Date | null; lastAckAt: Date | null; lastAppPushAt: 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<{ enrolled: string; token_update_at: Date | null; last_ack: Date | null; last_app_push_at: Date | null; }>( `SELECT (SELECT count(*)::text FROM devices WHERE id = $1) AS enrolled, (SELECT token_update_at FROM devices WHERE id = $1) AS token_update_at, (SELECT max(updated_at) FROM command_results WHERE id = $1) 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 = $1 AND c.request_type = 'InstallApplication' AND r.status = 'Acknowledged') AS last_app_push_at`, [udid], ); const row = result.rows[0]; const enrolled = row ? row.enrolled !== "0" : false; return { enrolled, company: enrolled ? "ReBreak" : null, tokenUpdateAt: row?.token_update_at ?? null, lastAckAt: row?.last_ack ?? null, lastAppPushAt: row?.last_app_push_at ?? null, }; }