import { db, id, json, now, parseJson, visibleClause } from '../db.js'; import { graphConnectionSchema } from '../forms/schemas.js'; import { camelGraphConnection, camelIntuneDeploymentStatus, camelIntuneDeploymentSyncRun } from './mappers.js'; import { decryptSecret, encryptSecret } from '../services/cryptoStore.js'; function normalizeConnection(body, existing = {}) { const parsed = graphConnectionSchema.parse({ ...existing, ...body }); const cloudDefaults = cloudEndpointDefaults(parsed.cloud); return { ...parsed, graphBaseUrl: body.graphBaseUrl || existing.graphBaseUrl || cloudDefaults.graphBaseUrl, authorityHost: body.authorityHost || existing.authorityHost || cloudDefaults.authorityHost, enabled: Boolean(parsed.enabled), allowWrite: Boolean(parsed.allowWrite), requireApproval: Boolean(parsed.requireApproval), publisherIds: Array.isArray(parsed.publisherIds) ? parsed.publisherIds : [], approverIds: Array.isArray(parsed.approverIds) ? parsed.approverIds : [], groupId: parsed.visibility === 'group' ? parsed.groupId || null : null }; } export function cloudEndpointDefaults(cloud = 'global') { const endpoints = { global: { graphBaseUrl: 'https://graph.microsoft.com', authorityHost: 'https://login.microsoftonline.com' }, gcc: { graphBaseUrl: 'https://graph.microsoft.com', authorityHost: 'https://login.microsoftonline.com' }, gcchigh: { graphBaseUrl: 'https://graph.microsoft.us', authorityHost: 'https://login.microsoftonline.us' }, dod: { graphBaseUrl: 'https://dod-graph.microsoft.us', authorityHost: 'https://login.microsoftonline.us' }, china: { graphBaseUrl: 'https://microsoftgraph.chinacloudapi.cn', authorityHost: 'https://login.chinacloudapi.cn' } }; return endpoints[cloud] || endpoints.global; } export function listGraphConnections(userId) { return db.prepare(` SELECT c.*, g.name AS group_name FROM graph_connections c LEFT JOIN groups g ON g.id = c.group_id WHERE ${visibleClause('c')} ORDER BY c.name `).all(userId, userId).map(camelGraphConnection); } export function getGraphConnection(connectionId, userId, includeSecret = false) { const row = db.prepare(` SELECT c.*, g.name AS group_name FROM graph_connections c LEFT JOIN groups g ON g.id = c.group_id WHERE c.id = ? AND ${visibleClause('c')} `).get(connectionId, userId, userId); if (!row) return null; return includeSecret ? { ...row, clientSecret: decryptSecret(row) } : camelGraphConnection(row); } export function createGraphConnection(body, userId) { const payload = normalizeConnection(body); const encrypted = encryptSecret(payload.clientSecret); const connectionId = id('graph'); db.prepare(` INSERT INTO graph_connections ( id, name, tenant_id, client_id, secret_cipher, secret_iv, secret_tag, cloud, graph_base_url, authority_host, default_api_version, enabled, allow_write, require_approval, publisher_ids, approver_ids, visibility, owner_user_id, group_id, created_by, created_at, updated_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `).run( connectionId, payload.name, payload.tenantId, payload.clientId, encrypted.cipher, encrypted.iv, encrypted.tag, payload.cloud, payload.graphBaseUrl, payload.authorityHost, payload.defaultApiVersion, payload.enabled ? 1 : 0, payload.allowWrite ? 1 : 0, payload.requireApproval ? 1 : 0, json(payload.publisherIds, []), json(payload.approverIds, []), payload.visibility, userId, payload.groupId, userId, now(), now() ); return getGraphConnection(connectionId, userId); } export function updateGraphConnection(connectionId, body, userId) { const existing = getGraphConnection(connectionId, userId, true); if (!existing) return null; const payload = normalizeConnection(body, { name: existing.name, tenantId: existing.tenant_id, clientId: existing.client_id, cloud: existing.cloud, graphBaseUrl: existing.graph_base_url, authorityHost: existing.authority_host, defaultApiVersion: existing.default_api_version, enabled: Boolean(existing.enabled), allowWrite: Boolean(existing.allow_write), requireApproval: Boolean(existing.require_approval), publisherIds: parseJson(existing.publisher_ids, []), approverIds: parseJson(existing.approver_ids, []), visibility: existing.visibility, groupId: existing.group_id }); const encrypted = payload.clientSecret ? encryptSecret(payload.clientSecret) : { cipher: existing.secret_cipher, iv: existing.secret_iv, tag: existing.secret_tag }; db.prepare(` UPDATE graph_connections SET name = ?, tenant_id = ?, client_id = ?, secret_cipher = ?, secret_iv = ?, secret_tag = ?, cloud = ?, graph_base_url = ?, authority_host = ?, default_api_version = ?, enabled = ?, allow_write = ?, require_approval = ?, publisher_ids = ?, approver_ids = ?, visibility = ?, group_id = ?, updated_at = ? WHERE id = ? `).run( payload.name, payload.tenantId, payload.clientId, encrypted.cipher, encrypted.iv, encrypted.tag, payload.cloud, payload.graphBaseUrl, payload.authorityHost, payload.defaultApiVersion, payload.enabled ? 1 : 0, payload.allowWrite ? 1 : 0, payload.requireApproval ? 1 : 0, json(payload.publisherIds, []), json(payload.approverIds, []), payload.visibility, payload.groupId, now(), connectionId ); return getGraphConnection(connectionId, userId); } export function deleteGraphConnection(connectionId, userId) { db.prepare(`DELETE FROM graph_connections WHERE id = ? AND ${visibleClause()}`).run(connectionId, userId, userId); } // Immutable record of every tenant-changing Graph action (publish, assign, // relationship edits). Stores the actor, request payload, and Graph response so // changes are auditable and replayable. export function recordGraphAudit(entry) { const auditId = id('gaud'); db.prepare(` INSERT INTO graph_audit_log ( id, connection_id, deployment_id, actor_user_id, actor_email, action, target_graph_id, status, message, request_json, response_json, created_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `).run( auditId, entry.connectionId || null, entry.deploymentId || null, entry.actorUserId || null, entry.actorEmail || '', entry.action, entry.targetGraphId || '', entry.status, entry.message || '', json(entry.request || {}, {}), json(entry.response || {}, {}), now() ); return auditId; } export function listGraphAudit({ deploymentId, connectionId, limit = 100 } = {}) { const clauses = []; const params = []; if (deploymentId) { clauses.push('deployment_id = ?'); params.push(deploymentId); } if (connectionId) { clauses.push('connection_id = ?'); params.push(connectionId); } const where = clauses.length ? `WHERE ${clauses.join(' AND ')}` : ''; return db.prepare(` SELECT * FROM graph_audit_log ${where} ORDER BY created_at DESC LIMIT ? `).all(...params, Number(limit) || 100).map((row) => ({ id: row.id, connectionId: row.connection_id, deploymentId: row.deployment_id, actorUserId: row.actor_user_id, actorEmail: row.actor_email || '', action: row.action, targetGraphId: row.target_graph_id || '', status: row.status, message: row.message || '', request: parseJson(row.request_json, {}), response: parseJson(row.response_json, {}), createdAt: row.created_at })); } // Read just the governance lists for a connection, without visibility scoping, // so designated approvers (who may not "own" the connection) can be authorized. // Full connection row + decrypted secret without visibility scope, for the // background auto-promote worker (system context). export function getGraphConnectionSystem(connectionId) { const row = db.prepare('SELECT * FROM graph_connections WHERE id = ?').get(connectionId); if (!row) return null; return { ...row, clientSecret: decryptSecret(row) }; } export function getConnectionGovernance(connectionId) { const row = db.prepare('SELECT publisher_ids, approver_ids, require_approval FROM graph_connections WHERE id = ?').get(connectionId); if (!row) return null; return { publisherIds: parseJson(row.publisher_ids, []), approverIds: parseJson(row.approver_ids, []), requireApproval: Boolean(row.require_approval) }; } export function recordGraphConnectionTest(connectionId, status, message) { db.prepare(` UPDATE graph_connections SET last_test_status = ?, last_test_message = ?, last_test_at = ?, updated_at = ? WHERE id = ? `).run(status, message, now(), now(), connectionId); } export function upsertDeploymentStatuses(deploymentId, connectionId, graphAppId, rows = []) { const timestamp = now(); const stmt = db.prepare(` INSERT INTO intune_deployment_statuses ( id, deployment_id, connection_id, graph_app_id, scope, principal_id, principal_name, device_id, device_name, user_id, user_principal_name, install_state, install_state_detail, error_code, error_description, last_sync_datetime, raw_json, created_at, updated_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `); db.prepare('DELETE FROM intune_deployment_statuses WHERE deployment_id = ? AND connection_id = ? AND graph_app_id = ?') .run(deploymentId, connectionId, graphAppId); for (const row of rows) { stmt.run( id('ids'), deploymentId, connectionId, graphAppId, row.scope || 'device', row.principalId || '', row.principalName || '', row.deviceId || '', row.deviceName || '', row.userId || '', row.userPrincipalName || '', row.installState || 'unknown', row.installStateDetail || '', row.errorCode == null ? '' : String(row.errorCode), row.errorDescription || '', row.lastSyncDateTime || '', json(row.raw || {}, {}), timestamp, timestamp ); } } export function createSyncRun(deploymentId, connectionId, graphAppId) { const runId = id('sync'); db.prepare(` INSERT INTO intune_deployment_sync_runs (id, deployment_id, connection_id, graph_app_id, status, started_at, summary) VALUES (?, ?, ?, ?, 'running', ?, '{}') `).run(runId, deploymentId, connectionId, graphAppId, now()); return runId; } export function finishSyncRun(runId, status, message, summary = {}) { db.prepare(` UPDATE intune_deployment_sync_runs SET status = ?, message = ?, summary = ?, ended_at = ? WHERE id = ? `).run(status, message || '', json(summary, {}), now(), runId); } export function listDeploymentStatuses(deploymentId) { return db.prepare(` SELECT * FROM intune_deployment_statuses WHERE deployment_id = ? ORDER BY CASE install_state WHEN 'failed' THEN 0 WHEN 'error' THEN 1 WHEN 'pending' THEN 2 WHEN 'installed' THEN 3 ELSE 4 END, updated_at DESC `).all(deploymentId).map(camelIntuneDeploymentStatus); } // Cross-deployment rollout health for the governance/reporting view. Scoped to // the deployments the operator can see (admins see all). export function intuneReportingOverview(userId, isAdmin = false) { const scope = isAdmin ? '1=1' : visibleClause('d'); const params = isAdmin ? [] : [userId, userId]; const deploymentsByStatus = db.prepare(` SELECT status, COUNT(*) AS count FROM intune_deployments d WHERE ${scope} GROUP BY status `).all(...params); const installStateCounts = db.prepare(` SELECT s.install_state AS state, COUNT(*) AS count FROM intune_deployment_statuses s JOIN intune_deployments d ON d.id = s.deployment_id WHERE ${scope} GROUP BY s.install_state `).all(...params); const topErrors = db.prepare(` SELECT s.error_code AS code, COUNT(*) AS count FROM intune_deployment_statuses s JOIN intune_deployments d ON d.id = s.deployment_id WHERE ${scope} AND s.error_code IS NOT NULL AND s.error_code <> '' GROUP BY s.error_code ORDER BY count DESC LIMIT 10 `).all(...params); const writeActions = db.prepare(` SELECT a.action, a.status, COUNT(*) AS count FROM graph_audit_log a JOIN intune_deployments d ON d.id = a.deployment_id WHERE ${scope} GROUP BY a.action, a.status `).all(...params); // SLA / soak timers: deployments mid-soak, with hours remaining until the // auto-promote window elapses (negative = overdue for promotion). const soakTimers = db.prepare(` SELECT d.id, d.name, d.auto_promote_ring AS ring, d.auto_promote_after_hours AS hours, d.soak_started_at AS soakStartedAt, ROUND(d.auto_promote_after_hours - (julianday('now') - julianday(d.soak_started_at)) * 24, 1) AS hoursRemaining FROM intune_deployments d WHERE d.auto_promote_after_hours > 0 AND d.soak_started_at IS NOT NULL AND d.soak_started_at <> '' AND (d.promoted_at IS NULL OR d.promoted_at = '') AND ${scope} ORDER BY hoursRemaining ASC `).all(...params); // Write-action trend over the last 14 days, by day and outcome. const auditTrend = db.prepare(` SELECT substr(a.created_at, 1, 10) AS day, a.status, COUNT(*) AS count FROM graph_audit_log a JOIN intune_deployments d ON d.id = a.deployment_id WHERE ${scope} AND a.created_at >= date('now', '-14 days') GROUP BY day, a.status ORDER BY day `).all(...params); return { deploymentsByStatus, installStateCounts, topErrors, writeActions, soakTimers, auditTrend }; } export function listDeploymentSyncRuns(deploymentId) { return db.prepare(` SELECT * FROM intune_deployment_sync_runs WHERE deployment_id = ? ORDER BY started_at DESC LIMIT 20 `).all(deploymentId).map(camelIntuneDeploymentSyncRun); }