diff --git a/backend/server/db/mdm.ts b/backend/server/db/mdm.ts index 34e7b24..e32f11b 100644 --- a/backend/server/db/mdm.ts +++ b/backend/server/db/mdm.ts @@ -24,7 +24,7 @@ function useMdmPool(): pg.Pool { // NanoMDM queries are point lookups — keep pool small. max: 5, connectionTimeoutMillis: 5000, - queryTimeout: 5000, + query_timeout: 5000, }); return _mdmPool; @@ -46,6 +46,17 @@ const USER_DEVICE_MDM_SELECT = { 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. */ @@ -90,6 +101,17 @@ export async function clearUserDeviceMdmId( }); } +/** + * Load all iOS devices that have a NanoMDM UDID link. + */ +export async function getLinkedUserDevices(): Promise { + const db = usePrisma(); + return db.userDevice.findMany({ + where: { platform: "ios", mdmId: { not: null } }, + select: USER_DEVICE_MDM_HEALTH_SELECT, + }); +} + export interface MdmDeviceStatus { enrolled: boolean; company: string | null; @@ -99,6 +121,23 @@ export interface MdmDeviceStatus { 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. * @@ -144,3 +183,64 @@ export async function getMdmStatusByUdid( 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, + }, + }); +}