Add CREATE TABLE IF NOT EXISTS for rebreak.user_devices to the hardware_id migration so fresh databases can migrate despite the alphabetical ordering mismatch with 20260430_add_user_devices. Also apply Prettier formatting to mdm.ts.
249 lines
5.8 KiB
TypeScript
249 lines
5.8 KiB
TypeScript
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,
|
|
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<UserDeviceMdmRecord | null> {
|
|
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<void> {
|
|
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<void> {
|
|
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;
|
|
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<MdmDeviceStatus> {
|
|
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;
|
|
}>(
|
|
`SELECT
|
|
d.unlock_token,
|
|
d.token_update_at,
|
|
(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
|
|
WHERE d.id = $1`,
|
|
[udid],
|
|
);
|
|
|
|
const row = result.rows[0];
|
|
const enrolled = !!row;
|
|
|
|
return {
|
|
enrolled,
|
|
company: enrolled ? "ReBreak" : null,
|
|
supervised: enrolled && 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<Map<string, MdmEnrollmentStatus>> {
|
|
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<string, MdmEnrollmentStatus>();
|
|
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<void> {
|
|
const db = usePrisma();
|
|
await db.userDevice.update({
|
|
where: { id },
|
|
data: {
|
|
mdmEnrolled: status.enrolled,
|
|
mdmSupervised: status.supervised,
|
|
mdmLastSeenAt: status.lastSeenAt,
|
|
},
|
|
});
|
|
}
|