# NinjaOne RMM Integration Plan ## Overview This plan details the implementation of a comprehensive NinjaOne RMM integration for Alga PSA, enabling MSPs to synchronize device assets, receive real-time alerts, create tickets from RMM events, and initiate remote access sessions directly from the PSA interface. ### Core Features - **Device Synchronization**: Bidirectional sync of devices/assets between NinjaOne and Alga PSA - **Webhook Callbacks**: Real-time notifications when changes occur in NinjaOne - **Asset-to-Ticket Linking**: Attach assets to tickets to track which device a ticket is for - **Alert-to-Ticket Automation**: Receive alerts and automatically create tickets based on configurable rules - **Remote Access**: Launch remote sessions to devices through NinjaOne from within Alga PSA - **Patch Compliance Tracking**: Monitor and display patch status for managed devices - **Software Inventory**: Sync and display installed software from RMM-managed devices ### NinjaOne API Capabilities (Reference) | Category | Key Endpoints | Integration Use | |----------|--------------|-----------------| | **Devices** | `GET /devices`, `GET /device/{id}` | Core asset sync | | **Alerts** | `GET /alerts`, `GET /device/{id}/alerts` | Ticket creation | | **Webhooks** | `PUT /webhook` | Real-time sync | | **Organizations** | `GET /organizations` | Client mapping | | **Device Link** | `GET /device/{id}/dashboard-url` | Remote access | | **Queries** | Health report, Software, Patches | Enhanced data | **Webhook Event Types**: `NODE_CREATED`, `NODE_UPDATED`, `NODE_DELETED`, `TRIGGERED` (alerts), `RESET`, plus 200+ activity types including hardware changes, software changes, patch events, and antivirus alerts. --- ## Asset System Readiness Assessment ### Already Supported (No Changes Needed) - Multi-tenant architecture with RLS policies - Extension tables for asset types (workstation, server, network device, mobile, printer) - Asset-to-ticket associations (`asset_ticket_associations`) - External entity mapping table (`tenant_external_entity_mappings`) - Asset history/audit trail - Maintenance scheduling system ### Requires Enhancement - Agent status tracking (online/offline, last seen) - RMM alert storage and lifecycle management - Remote access URL storage/retrieval - Patch compliance fields - Asset event types in EventBus --- ## Integration Settings UI Redesign As integrations grow (QBO, Xero, Google Calendar, Microsoft Calendar, Email, and now NinjaOne), the current flat list within the Integrations tab becomes unwieldy. This plan includes a reorganization. ### Current Structure (SettingsPage.tsx) ``` Settings > Integrations Tab ├── QboIntegrationSettings (Card) ├── XeroIntegrationSettings (Card) ├── Inbound Email Integration (Card) └── Calendar Integrations (Card) ``` ### Proposed Structure ``` Settings > Integrations Tab ├── Integration Categories (Accordion or Sub-tabs) │ ├── Accounting │ │ ├── QuickBooks Online │ │ └── Xero │ ├── RMM & Endpoint Management │ │ └── NinjaOne (NEW) │ ├── Email & Communication │ │ ├── Inbound Email │ │ └── (Future: Outbound SMTP) │ └── Calendar & Scheduling │ ├── Google Calendar │ └── Microsoft Calendar └── (Future: PSA-to-PSA migrations) ``` ### UI Implementation Tasks - [x] Create `IntegrationCategory.tsx` component with collapsible sections - [x] Create `IntegrationCard.tsx` reusable component extracting common patterns from QBO/Xero - [x] Refactor `SettingsPage.tsx` Integrations tab to use category-based layout - [x] Add category icons (accounting, RMM, email, calendar) - [ ] Ensure responsive layout for mobile/tablet views --- ## Phased Implementation Plan ### Phase 0 – Database Schema & Foundation #### Schema: RMM Integration Configuration - [x] Create migration `YYYYMMDDHHMMSS_create_rmm_integration_tables.cjs` - [x] Create `rmm_integrations` table: ```sql CREATE TABLE rmm_integrations ( tenant UUID NOT NULL, integration_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), integration_type TEXT NOT NULL DEFAULT 'ninjaone', display_name TEXT NOT NULL, api_instance TEXT NOT NULL, -- 'app' | 'eu' | 'oc' | 'ca' (region) client_id TEXT NOT NULL, is_active BOOLEAN DEFAULT true, sync_enabled BOOLEAN DEFAULT true, sync_interval_minutes INTEGER DEFAULT 60, last_full_sync_at TIMESTAMPTZ, last_incremental_sync_at TIMESTAMPTZ, webhook_secret TEXT, webhook_registered_at TIMESTAMPTZ, settings JSONB DEFAULT '{}', -- Additional config options created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), CONSTRAINT fk_tenant FOREIGN KEY (tenant) REFERENCES tenants(tenant) ); CREATE INDEX idx_rmm_integrations_tenant ON rmm_integrations(tenant); ``` - [x] Create `rmm_organization_mappings` table for NinjaOne org → Alga client mapping: ```sql CREATE TABLE rmm_organization_mappings ( tenant UUID NOT NULL, mapping_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), integration_id UUID NOT NULL REFERENCES rmm_integrations(integration_id) ON DELETE CASCADE, external_org_id TEXT NOT NULL, -- NinjaOne organization ID external_org_name TEXT, client_id UUID REFERENCES companies(company_id), -- Alga client auto_sync_devices BOOLEAN DEFAULT true, last_synced_at TIMESTAMPTZ, created_at TIMESTAMPTZ DEFAULT NOW(), UNIQUE(tenant, integration_id, external_org_id) ); ``` - [x] Create `rmm_alerts` table: ```sql CREATE TABLE rmm_alerts ( tenant UUID NOT NULL, alert_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), integration_id UUID REFERENCES rmm_integrations(integration_id) ON DELETE CASCADE, external_alert_id TEXT NOT NULL, external_device_id TEXT NOT NULL, asset_id UUID REFERENCES assets(asset_id) ON DELETE SET NULL, severity TEXT NOT NULL, -- NONE, MINOR, MODERATE, MAJOR, CRITICAL priority TEXT, -- NONE, LOW, MEDIUM, HIGH activity_type TEXT NOT NULL, status_code TEXT NOT NULL, message TEXT, source_data JSONB, -- Full webhook payload triggered_at TIMESTAMPTZ NOT NULL, acknowledged_at TIMESTAMPTZ, acknowledged_by UUID REFERENCES users(user_id), resolved_at TIMESTAMPTZ, resolved_by UUID REFERENCES users(user_id), ticket_id UUID REFERENCES tickets(ticket_id) ON DELETE SET NULL, auto_ticket_created BOOLEAN DEFAULT false, created_at TIMESTAMPTZ DEFAULT NOW(), UNIQUE(tenant, integration_id, external_alert_id) ); CREATE INDEX idx_rmm_alerts_asset ON rmm_alerts(asset_id); CREATE INDEX idx_rmm_alerts_ticket ON rmm_alerts(ticket_id); CREATE INDEX idx_rmm_alerts_status ON rmm_alerts(tenant, status_code, triggered_at DESC); ``` - [x] Create `rmm_alert_rules` table for alert-to-ticket automation: ```sql CREATE TABLE rmm_alert_rules ( tenant UUID NOT NULL, rule_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), integration_id UUID REFERENCES rmm_integrations(integration_id) ON DELETE CASCADE, name TEXT NOT NULL, is_active BOOLEAN DEFAULT true, priority_order INTEGER DEFAULT 0, conditions JSONB NOT NULL, -- { severity: [...], activityTypes: [...], orgIds: [...] } actions JSONB NOT NULL, -- { createTicket: true, ticketPriority: 'high', assignToChannel: '...' } created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); ``` #### Schema: Asset Table Enhancements - [x] Add RMM-specific columns to `assets` table: ```sql ALTER TABLE assets ADD COLUMN IF NOT EXISTS rmm_integration_id UUID REFERENCES rmm_integrations(integration_id) ON DELETE SET NULL; ALTER TABLE assets ADD COLUMN IF NOT EXISTS rmm_device_id TEXT; ALTER TABLE assets ADD COLUMN IF NOT EXISTS agent_status TEXT DEFAULT 'unknown'; -- online, offline, unknown ALTER TABLE assets ADD COLUMN IF NOT EXISTS last_seen_at TIMESTAMPTZ; ALTER TABLE assets ADD COLUMN IF NOT EXISTS remote_access_url TEXT; ALTER TABLE assets ADD COLUMN IF NOT EXISTS rmm_sync_status TEXT DEFAULT 'pending'; -- synced, pending, error ALTER TABLE assets ADD COLUMN IF NOT EXISTS rmm_last_synced_at TIMESTAMPTZ; CREATE INDEX idx_assets_rmm_device ON assets(rmm_integration_id, rmm_device_id); ``` - [x] Add patch/compliance columns to workstation and server extension tables: ```sql ALTER TABLE workstation_assets ADD COLUMN IF NOT EXISTS antivirus_status TEXT; ALTER TABLE workstation_assets ADD COLUMN IF NOT EXISTS antivirus_product TEXT; ALTER TABLE workstation_assets ADD COLUMN IF NOT EXISTS pending_os_patches INTEGER DEFAULT 0; ALTER TABLE workstation_assets ADD COLUMN IF NOT EXISTS pending_software_patches INTEGER DEFAULT 0; ALTER TABLE workstation_assets ADD COLUMN IF NOT EXISTS failed_patches INTEGER DEFAULT 0; ALTER TABLE workstation_assets ADD COLUMN IF NOT EXISTS last_patch_scan_at TIMESTAMPTZ; ALTER TABLE server_assets ADD COLUMN IF NOT EXISTS antivirus_status TEXT; ALTER TABLE server_assets ADD COLUMN IF NOT EXISTS antivirus_product TEXT; ALTER TABLE server_assets ADD COLUMN IF NOT EXISTS pending_os_patches INTEGER DEFAULT 0; ALTER TABLE server_assets ADD COLUMN IF NOT EXISTS pending_software_patches INTEGER DEFAULT 0; ALTER TABLE server_assets ADD COLUMN IF NOT EXISTS failed_patches INTEGER DEFAULT 0; ALTER TABLE server_assets ADD COLUMN IF NOT EXISTS last_patch_scan_at TIMESTAMPTZ; ``` #### Schema: Update External Entity Mappings Usage - [x] Document usage of existing `tenant_external_entity_mappings` for device ID mapping: - `integration_type`: `'ninjaone'` - `alga_entity_type`: `'asset'` - `alga_entity_id`: Alga asset UUID - `external_entity_id`: NinjaOne device ID (string) - `external_realm_id`: NinjaOne organization ID (for scoping) #### TypeScript Interfaces - [x] Create `server/src/interfaces/rmm.interfaces.ts`: - `RmmIntegration`, `RmmOrganizationMapping`, `RmmAlert`, `RmmAlertRule` - `NinjaOneDevice`, `NinjaOneOrganization`, `NinjaOneAlert` - `RmmConnectionStatus`, `RmmSyncStatus` - [x] Update `server/src/interfaces/asset.interfaces.tsx` with RMM fields #### Event Bus Extensions - [x] Add RMM event types to `server/src/lib/eventBus/events.ts`: ```typescript // RMM Integration Events RMM_DEVICE_SYNCED = 'rmm.device.synced', RMM_DEVICE_CREATED = 'rmm.device.created', RMM_DEVICE_UPDATED = 'rmm.device.updated', RMM_DEVICE_DELETED = 'rmm.device.deleted', RMM_ALERT_RECEIVED = 'rmm.alert.received', RMM_ALERT_ACKNOWLEDGED = 'rmm.alert.acknowledged', RMM_ALERT_RESOLVED = 'rmm.alert.resolved', RMM_SYNC_STARTED = 'rmm.sync.started', RMM_SYNC_COMPLETED = 'rmm.sync.completed', RMM_SYNC_FAILED = 'rmm.sync.failed', ``` --- ### Phase 1 – NinjaOne API Client & OAuth #### OAuth Flow Implementation - [x] Create `server/src/app/api/integrations/ninjaone/connect/route.ts`: - Generate CSRF token using `crypto.randomBytes(16).toString('hex')` - Create state payload with tenant ID and CSRF token - Encode state as base64url - Determine correct NinjaOne instance URL based on region selection - Redirect to NinjaOne OAuth authorization endpoint - OAuth scopes: `monitoring`, `management`, `control` (as needed) - [x] Create `server/src/app/api/integrations/ninjaone/callback/route.ts`: - Validate state parameter (decode, verify tenant, check CSRF) - Exchange authorization code for access/refresh tokens - Store tokens securely in tenant secrets via `secretProvider.setTenantSecret()` - Create or update `rmm_integrations` record - Redirect to settings page with success/error status params - [x] Implement token refresh logic in API client #### API Client - [x] Create `server/src/lib/integrations/ninjaone/client.ts`: ```typescript class NinjaOneClient { constructor(tenantId: string, integrationId: string); // Token management private async getAccessToken(): Promise; private async refreshTokenIfNeeded(): Promise; // API methods async getOrganizations(): Promise; async getDevices(params?: DeviceQueryParams): Promise; async getDeviceDetails(deviceId: number): Promise; async getDeviceAlerts(deviceId: number): Promise; async getAlerts(params?: AlertQueryParams): Promise; async getDeviceLink(deviceId: number): Promise; // Remote access URL async resetAlert(alertId: string): Promise; // Webhook management async configureWebhook(webhookUrl: string, activities: string[]): Promise; async removeWebhook(): Promise; } ``` - [x] Create `server/src/lib/integrations/ninjaone/types.ts`: - NinjaOne API response types - Device, Organization, Alert, Activity types - Webhook payload types - [x] Create `server/src/lib/integrations/ninjaone/endpoints.ts`: - API endpoint URL builders - Region-specific base URLs (app, eu, oc, ca) - [x] Create `server/src/lib/integrations/ninjaone/errors.ts`: - Custom error classes for API errors - Rate limiting handling - Token expiration handling #### Server Actions - [x] Create `server/src/lib/actions/integrations/ninjaoneActions.ts`: ```typescript // Connection management export async function getNinjaOneConnectionStatus(): Promise; export async function disconnectNinjaOne(integrationId: string): Promise<{ success: boolean }>; export async function updateNinjaOneSettings(integrationId: string, settings: Partial): Promise; // Organization mapping export async function getNinjaOneOrganizations(integrationId: string): Promise; export async function getOrganizationMappings(integrationId: string): Promise; export async function createOrganizationMapping(data: CreateOrgMappingRequest): Promise; export async function updateOrganizationMapping(mappingId: string, data: UpdateOrgMappingRequest): Promise; export async function deleteOrganizationMapping(mappingId: string): Promise; // Sync operations export async function triggerFullSync(integrationId: string): Promise<{ jobId: string }>; export async function getSyncStatus(integrationId: string): Promise; // Alert management export async function getActiveAlerts(integrationId: string, filters?: AlertFilters): Promise; export async function acknowledgeAlert(alertId: string): Promise; export async function createTicketFromAlert(alertId: string, options?: CreateTicketOptions): Promise<{ ticketId: string }>; // Remote access export async function getRemoteAccessUrl(assetId: string): Promise<{ url: string; expiresAt: string }>; ``` - [x] Implement RBAC checks using existing `hasPermission()` pattern - [x] Use `system_settings` or new `rmm_settings` resource for permissions #### Credential Storage - [x] Store credentials in tenant secrets: - Key: `ninjaone_credentials` - Value: JSON with `{ [integrationId]: { accessToken, refreshToken, expiresAt, instanceUrl } }` - [x] Implement credential retrieval with automatic token refresh - [x] Add credential validation on connection status check --- ### Phase 2 – Integration Settings UI #### Reusable Integration Components - [x] Create `server/src/components/settings/integrations/IntegrationCard.tsx`: - Reusable card component with status badge, connect/disconnect buttons - Props: `title`, `description`, `status`, `onConnect`, `onDisconnect`, `children` - Extract common patterns from `QboIntegrationSettings.tsx` - [x] Create `server/src/components/settings/integrations/IntegrationCategory.tsx`: - Collapsible category container with icon and title - Props: `title`, `icon`, `defaultOpen`, `children` - [x] Create `server/src/components/settings/integrations/ConnectionStatusBadge.tsx`: - Unified status badge component - States: connected, disconnected, error, syncing, expired #### NinjaOne Settings Component - [x] Create `server/src/components/settings/integrations/NinjaOneIntegrationSettings.tsx`: - Connection status display with last sync time - Connect button with region selector (North America, EMEA, APAC, Canada) - Disconnect confirmation modal - Sync settings (interval, auto-sync toggle) - Link to organization mapping - Link to alert rules configuration - [x] Create `server/src/components/settings/integrations/NinjaOneDisconnectModal.tsx`: - Confirmation dialog following `QboDisconnectConfirmModal` pattern - Warning about data that will be affected - [x] Create `server/src/components/settings/integrations/NinjaOneRegionSelector.tsx`: - Dropdown for selecting NinjaOne instance region - Display region-specific information #### Organization Mapping UI - [x] Create `server/src/components/settings/integrations/ninjaone/OrganizationMappingManager.tsx`: - List of NinjaOne organizations with mapping status - Dropdown to select corresponding Alga client for each org - Auto-sync toggle per organization - Bulk mapping actions - [x] Create `server/src/components/settings/integrations/ninjaone/OrganizationMappingRow.tsx`: - Individual row component for org mapping - Status indicators (mapped, unmapped, sync error) #### Alert Rules UI - [x] Create `server/src/components/settings/integrations/ninjaone/AlertRulesManager.tsx`: - List of configured alert-to-ticket rules - Add/Edit/Delete rule actions - Rule priority ordering (drag-and-drop) - [x] Create `server/src/components/settings/integrations/ninjaone/AlertRuleForm.tsx`: - Rule name and active toggle - Condition builder: - Severity filter (multi-select) - Activity type filter (multi-select with search) - Organization filter (optional) - Action configuration: - Create ticket toggle - Ticket priority mapping - Channel/team assignment - Notification settings #### Settings Page Integration - [x] Refactor `server/src/components/settings/general/SettingsPage.tsx`: - Implement category-based layout using `IntegrationCategory` - Add "RMM & Endpoint Management" category - Include `NinjaOneIntegrationSettings` in RMM category - Reorganize existing integrations into categories --- ### Phase 3 – Device Synchronization #### Device Mapper - [x] Create `server/src/lib/integrations/ninjaone/mappers/deviceMapper.ts`: ```typescript export function mapNinjaOneDeviceToAsset( device: NinjaOneDeviceDetailed, existingAsset?: Asset ): CreateAssetRequest | UpdateAssetRequest; export function determineAssetType(device: NinjaOneDevice): AssetType; // nodeClass mapping: WINDOWS_WORKSTATION → workstation, WINDOWS_SERVER → server, etc. export function mapDeviceHardware(device: NinjaOneDeviceDetailed): WorkstationExtension | ServerExtension; export function mapNetworkInterfaces(interfaces: NinjaOneNetworkInterface[]): NetworkInterfaceData[]; ``` - [ ] Field mapping reference: | NinjaOne Field | Alga Asset Field | |----------------|------------------| | `id` | `rmm_device_id` | | `systemName` | `name` | | `nodeClass` | `asset_type` (mapped) | | `organizationId` | `client_id` (via org mapping) | | `offline` | `agent_status` | | `lastContact` | `last_seen_at` | | `system.manufacturer` | `attributes.manufacturer` | | `system.model` | `attributes.model` | | `os.name` + `os.version` | `os_type`, `os_version` | | `processors[0]` | `cpu_model`, `cpu_cores` | | `memory.capacity` | `ram_gb` | | `volumes[].capacity` | `storage_capacity` (sum) | #### Sync Engine - [x] Create `server/src/lib/integrations/ninjaone/sync/syncEngine.ts`: ```typescript export class NinjaOneSyncEngine { constructor(tenantId: string, integrationId: string); async runFullSync(): Promise; async runIncrementalSync(since: Date): Promise; async syncDevice(deviceId: number): Promise; async syncOrganization(orgId: number): Promise; private async createAsset(device: NinjaOneDevice): Promise; private async updateAsset(asset: Asset, device: NinjaOneDevice): Promise; private async handleDeletedDevice(deviceId: number): Promise; } ``` - [x] Create `server/src/lib/integrations/ninjaone/sync/syncJob.ts`: - Background job for scheduled sync - Progress tracking and reporting - Error handling and retry logic - Emit events for sync lifecycle - [x] Implement conflict resolution: - Last-write-wins for most fields - Preserve manual Alga edits for certain fields (notes, custom attributes) - Log conflicts for review #### Initial Sync Flow - [x] Create sync initiation endpoint/action - [x] Implement pagination for large device sets (NinjaOne uses cursor-based pagination) - [x] Track sync progress in `rmm_integrations.settings` JSONB - [x] Create sync history/log table or use existing job tracking #### Sync Scheduling - [x] Implement configurable sync interval (default: 60 minutes) - [x] Create cron job or Temporal workflow for scheduled sync - [x] Add manual "Sync Now" button in UI - [x] Implement sync locking to prevent concurrent syncs --- ### Phase 4 – Webhook Handler & Real-Time Sync #### Webhook Endpoint - [x] Create `server/src/app/api/webhooks/ninjaone/route.ts`: ```typescript export async function POST(request: Request) { // 1. Verify webhook signature (HMAC) // 2. Parse webhook payload // 3. Identify tenant from integration lookup // 4. Route to appropriate handler based on activityType // 5. Return 200 quickly, process async } ``` - [x] Implement webhook signature verification: - NinjaOne sends signature in header - Verify using stored webhook secret - [x] Create `server/src/lib/integrations/ninjaone/webhooks/webhookHandler.ts`: ```typescript export async function handleNinjaOneWebhook(payload: NinjaOneWebhookPayload): Promise; export async function handleDeviceEvent(payload: DeviceActivityPayload): Promise; export async function handleAlertEvent(payload: AlertActivityPayload): Promise; export async function handleSystemEvent(payload: SystemActivityPayload): Promise; ``` #### Webhook Event Handlers - [x] Device lifecycle events: - `NODE_CREATED`: Create new asset in Alga - `NODE_UPDATED`: Update existing asset - `NODE_DELETED`: Mark asset as inactive or delete - `NODE_MANUALLY_APPROVED`: Activate newly approved device - [x] Hardware change events: - `CPU_ADDED`, `CPU_REMOVED` - `MEMORY_ADDED`, `MEMORY_REMOVED` - `DISK_DRIVE_ADDED`, `DISK_DRIVE_REMOVED` - Update extension table fields - [x] Status events: - `SYSTEM_REBOOTED`: Update last_seen, log event - `USER_LOGGED_IN`, `USER_LOGGED_OUT`: Update last_login in extension - [x] Alert events (CONDITION type with TRIGGERED/RESET status): - Create `rmm_alerts` record - Evaluate alert rules for auto-ticket creation - Emit `RMM_ALERT_RECEIVED` event #### Webhook Registration - [x] Add webhook configuration to connection flow - [x] Create webhook URL with tenant-specific path or token - [x] Register webhook with NinjaOne API on connection - [x] Remove webhook on disconnect - [x] Handle webhook secret rotation #### Async Processing - [x] Queue webhook payloads for async processing (avoid timeout) - [x] Implement idempotency using external_alert_id/activity id - [x] Add retry logic for transient failures - [x] Create webhook processing log for debugging --- ### Phase 4.5 – Webhook Auto-Registration NinjaOne supports programmatic webhook configuration via the API. This allows us to automatically register the Alga webhook endpoint after OAuth authentication, eliminating manual configuration in the NinjaOne UI. #### API Reference - **`PUT /v2/webhook`** - Configure webhook endpoint and activity filters - **`DELETE /v2/webhook`** - Remove webhook configuration #### Webhook Registration Service - [ ] Create `ee/server/src/lib/integrations/ninjaone/webhooks/webhookRegistration.ts`: ```typescript export interface WebhookConfig { url: string; activities: { statusCode?: string[]; // Filter by status codes activityType?: string[]; // Filter by activity types }; expand?: string[]; // References to expand in payloads (device, organization) headers?: Array<{ name: string; value: string }>; // Custom auth headers organizationIds?: number[]; // Optional org filter } export async function registerWebhook( client: NinjaOneClient, config: WebhookConfig ): Promise; export async function removeWebhook( client: NinjaOneClient ): Promise; export function getDefaultWebhookConfig( baseUrl: string, webhookSecret: string ): WebhookConfig; ``` #### Default Activity Subscriptions Configure webhook to receive the most useful events: - **Device Lifecycle**: `NODE_CREATED`, `NODE_UPDATED`, `NODE_DELETED`, `NODE_MANUALLY_APPROVED`, `NODE_AUTOMATICALLY_APPROVED` - **Alerts/Conditions**: `TRIGGERED`, `RESET`, `ACKNOWLEDGED` - **System Events**: `SYSTEM_REBOOTED`, `USER_LOGGED_IN`, `USER_LOGGED_OUT` - **Hardware Changes**: `CPU_ADDED`, `CPU_REMOVED`, `MEMORY_ADDED`, `MEMORY_REMOVED`, `DISK_DRIVE_ADDED`, `DISK_DRIVE_REMOVED`, `ADAPTER_ADDED`, `ADAPTER_REMOVED` - **Patch Management**: `PATCH_MANAGEMENT_SCAN_STARTED`, `PATCH_MANAGEMENT_SCAN_COMPLETED`, `PATCH_MANAGEMENT_INSTALLED`, `PATCH_MANAGEMENT_INSTALL_FAILED` - **Software Changes**: `SOFTWARE_ADDED`, `SOFTWARE_REMOVED` - **Antivirus**: Severity-based alerts from supported AV products #### Integration with OAuth Flow - [ ] Update `ee/server/src/app/api/integrations/ninjaone/callback/route.ts`: - After successful token exchange and storage - Generate webhook secret using `crypto.randomBytes(32).toString('hex')` - Store webhook secret in rmm_integrations settings - Call `registerWebhook()` with tenant-specific callback URL - Handle registration failures gracefully (log, retry later) - Update `rmm_integrations.settings` with `webhookRegisteredAt` timestamp #### Integration with Disconnect Flow - [ ] Update `disconnectNinjaOneIntegration` action: - Call `removeWebhook()` before clearing credentials - Handle removal failures gracefully (webhook may already be removed) - Clear webhook secret from settings #### Webhook URL Configuration - [ ] Add environment variable `NINJAONE_WEBHOOK_BASE_URL` for production URL - [ ] Generate tenant-scoped webhook URL: - Format: `{baseUrl}/api/webhooks/ninjaone?tenant={tenantId}` - Or use signed token approach for security - [ ] Update webhook handler to validate custom auth header #### Error Handling & Retry - [ ] Handle rate limiting (429 responses) - [ ] Implement retry with exponential backoff - [ ] Log registration failures for admin visibility - [ ] Add "Re-register Webhook" button in settings UI for manual recovery #### Testing - [ ] Unit test webhook config generation - [ ] Integration test webhook registration flow - [ ] Test disconnect cleans up webhook --- ### Phase 5 – Alert Integration & Ticket Creation #### Alert Processing - [x] Create `server/src/lib/integrations/ninjaone/alerts/alertProcessor.ts`: ```typescript export async function processAlert( tenantId: string, integrationId: string, alertPayload: NinjaOneAlertPayload ): Promise; export async function evaluateAlertRules( tenantId: string, alert: RmmAlert ): Promise; export async function executeAlertActions( alert: RmmAlert, rule: RmmAlertRule ): Promise; ``` #### Ticket Creation from Alerts - [x] Create `server/src/lib/integrations/ninjaone/alerts/ticketCreator.ts`: ```typescript export async function createTicketFromAlert( alert: RmmAlert, options: CreateTicketFromAlertOptions ): Promise; ``` - [ ] Ticket content generation: - Title: `[NinjaOne Alert] {activityType} on {deviceName}` - Description: Alert details, device info, severity - Priority mapping: CRITICAL → urgent, MAJOR → high, MODERATE → medium, MINOR → low - Auto-link to asset via `asset_ticket_associations` - Include device context (client, location, IP, last user) - [x] Update `rmm_alerts` with ticket reference #### Alert Management UI - [ ] Create `server/src/components/alerts/RmmAlertsPanel.tsx`: - List of active RMM alerts - Filter by severity, status, device - Acknowledge/resolve actions - Create ticket action - [ ] Add alerts indicator to asset detail drawer - [ ] Create alert detail modal/drawer #### Alert Acknowledgment & Resolution - [ ] Implement acknowledge action (updates local record) - [ ] Implement resolve action (optionally resets in NinjaOne via API) - [ ] Track who acknowledged/resolved - [ ] Sync acknowledgment state bidirectionally (optional) --- ### Phase 6 – Remote Access Integration #### Remote Access URL Retrieval - [x] Implement `getRemoteAccessUrl` in NinjaOne client: - Call `GET /device/{id}/dashboard-url` endpoint - Parse and return the remote access URL - Handle cases where remote access is not available - [ ] Cache URLs with short TTL (5-10 minutes) - [x] Handle URL expiration gracefully #### Remote Access UI - [x] Add "Remote Connect" button to `AssetDetailDrawer.tsx`: - Only show for RMM-managed assets - Show loading state while fetching URL - Open URL in new tab/window - Handle errors (device offline, no remote access configured) - [x] Create `server/src/components/assets/RemoteAccessButton.tsx`: - Props: `assetId`, `disabled`, `variant` - Handle click to fetch and open URL - Show tooltip with device status - [x] Add remote access to asset actions menu - [x] Log remote access attempts for audit trail #### Remote Access from Ticket Context - [x] Add remote access button to ticket detail when asset is linked - [x] Show linked asset's remote access status - [ ] Quick action in ticket actions menu --- ### Phase 7 – Enhanced Asset Display #### Asset Card Enhancements - [x] Update `AssetCard.tsx` to show RMM status: - Agent status indicator (online/offline badge) - Last seen timestamp - Sync status indicator - Patch compliance badge (if applicable) - [x] Add RMM source indicator for synced assets - [ ] Show alert count badge when active alerts exist #### Asset Detail Drawer Enhancements - [x] Add "RMM Status" section to detail drawer: - Agent status with last contact time - Sync status and last sync time - Link to NinjaOne dashboard - Remote access button - [x] Add "Active Alerts" section: - List of unresolved alerts for this device - Quick actions (acknowledge, create ticket) - [x] Add "Patch Status" section: - Pending patches count - Failed patches count - Last scan time - Link to detailed patch report - [x] Add "Software Inventory" section: - List of installed software from RMM - Version information - Search functionality #### Asset List Filters - [x] Add RMM-related filters to asset list: - Agent status (online, offline, unknown) - RMM managed (yes, no) - [ ] Has active alerts (future) - [ ] Patch compliance status (future) - [ ] Add bulk actions for RMM assets: - Trigger sync for selected - View in NinjaOne (bulk open) --- ### Phase 8 – Patch & Software Inventory Sync #### Patch Status Sync - [x] Create `ee/server/src/lib/integrations/ninjaone/sync/patchSync.ts`: - Fetch pending/failed patches from NinjaOne - Update extension table fields - Track patch scan timestamps - [x] Add server actions for patch sync (`triggerPatchStatusSync`) - [ ] Add patch status to device sync flow (auto-sync during device sync) - [ ] Create scheduled patch status refresh (less frequent than device sync) #### Software Inventory Sync - [x] Create `ee/server/src/lib/integrations/ninjaone/sync/softwareSync.ts`: - Fetch installed software list - Store in extension table `installed_software` JSONB field - Track software changes in asset history - [x] Implement software inventory UI component (`AssetSoftwareInventory.tsx`) - [x] Add software search across assets (`searchSoftwareAcrossAssets`, `searchSoftware` action) #### Compliance Dashboard - [x] Create compliance summary widget for dashboard: - Devices online/offline count - Patches pending/failed count - Active alerts count - `NinjaOneComplianceDashboard.tsx` component - [x] Add `getRmmComplianceSummary` server action - [ ] Add compliance reporting (future) #### Asset List RMM Filters - [x] Add agent status filter (Online, Offline, Unknown) - [x] Add RMM managed filter (Managed, Not Managed) - [x] Display RMM filter pills in active filters bar --- ### Phase 9 – Testing & Documentation #### Unit Tests - [ ] Test NinjaOne API client methods - [ ] Test device mapper transformations - [ ] Test webhook signature verification - [ ] Test alert rule evaluation - [ ] Test ticket creation from alerts #### Integration Tests - [ ] Test OAuth flow (mock NinjaOne OAuth server) - [ ] Test webhook endpoint with sample payloads - [ ] Test full sync workflow - [ ] Test alert-to-ticket flow #### E2E Tests - [ ] Test connection flow in UI - [ ] Test organization mapping UI - [ ] Test alert rules configuration - [ ] Test remote access button functionality #### Documentation - [ ] Create user documentation for NinjaOne setup - [ ] Document organization mapping best practices - [ ] Document alert rule configuration - [ ] Create troubleshooting guide - [ ] Add API documentation for webhook endpoint --- ## File Structure Summary ``` server/src/ ├── app/ │ └── api/ │ ├── integrations/ │ │ └── ninjaone/ │ │ ├── connect/route.ts │ │ └── callback/route.ts │ └── webhooks/ │ └── ninjaone/ │ └── route.ts ├── components/ │ └── settings/ │ └── integrations/ │ ├── IntegrationCard.tsx │ ├── IntegrationCategory.tsx │ ├── ConnectionStatusBadge.tsx │ ├── NinjaOneIntegrationSettings.tsx │ ├── NinjaOneDisconnectModal.tsx │ ├── NinjaOneRegionSelector.tsx │ └── ninjaone/ │ ├── OrganizationMappingManager.tsx │ ├── OrganizationMappingRow.tsx │ ├── AlertRulesManager.tsx │ └── AlertRuleForm.tsx ├── lib/ │ ├── actions/ │ │ └── integrations/ │ │ └── ninjaoneActions.ts │ └── integrations/ │ └── ninjaone/ │ ├── client.ts │ ├── types.ts │ ├── endpoints.ts │ ├── errors.ts │ ├── mappers/ │ │ └── deviceMapper.ts │ ├── sync/ │ │ ├── syncEngine.ts │ │ ├── syncJob.ts │ │ ├── patchSync.ts │ │ └── softwareSync.ts │ ├── webhooks/ │ │ └── webhookHandler.ts │ └── alerts/ │ ├── alertProcessor.ts │ └── ticketCreator.ts └── interfaces/ └── rmm.interfaces.ts server/migrations/ └── YYYYMMDDHHMMSS_create_rmm_integration_tables.cjs ``` --- ## Dependencies & Prerequisites ### External - NinjaOne API application registration (OAuth client ID/secret) - Webhook endpoint accessible from NinjaOne servers (public URL or tunnel for dev) - NinjaOne account with API access enabled ### Internal - Existing `tenant_external_entity_mappings` table - Existing `ISecretProvider` for credential storage - Existing asset management system - Existing ticket system - EventBus for event publishing --- ## Acceptance Criteria ### Phase 1-2 (Connection & UI) - [x] User can connect NinjaOne account via OAuth from settings - [x] Connection status displays correctly (connected, disconnected, error) - [x] User can disconnect NinjaOne integration - [x] Settings page shows integrations in organized categories ### Phase 3 (Device Sync) - [x] Initial sync imports all devices from mapped organizations - [x] Devices are created with correct asset type based on nodeClass - [x] Hardware details (CPU, RAM, storage) are populated in extension tables - [x] Scheduled sync runs at configured interval - [x] Manual "Sync Now" works correctly ### Phase 4 (Webhooks) - [x] Webhook endpoint receives and validates NinjaOne callbacks - [x] Device changes in NinjaOne reflect in Alga within seconds - [x] New devices are created automatically - [x] Deleted devices are handled appropriately ### Phase 5 (Alerts & Tickets) - [x] Alerts from NinjaOne are stored in rmm_alerts table - [x] Alert rules can be configured via UI - [x] Tickets are auto-created based on matching rules - [x] Tickets include device context and are linked to asset ### Phase 6 (Remote Access) - [x] "Remote Connect" button appears on RMM-synced assets - [x] Clicking button opens NinjaOne remote session in new tab - [ ] Remote access works from asset detail and ticket context ### Phase 7-8 (Enhanced Display & Compliance) - [x] Asset cards show agent status and alert indicators - [x] Asset detail shows comprehensive RMM status - [x] Patch compliance status is displayed (AssetPatchStatusSection) - [x] Software inventory is viewable (AssetSoftwareInventory) - [x] Compliance dashboard widget shows fleet health - [x] Asset list supports RMM filters (agent status, managed/unmanaged) --- ## Future Considerations ### Additional RMM Platforms This architecture is designed to support multiple RMM platforms. Future integrations could include: - Datto RMM - ConnectWise Automate - Syncro - Atera The `rmm_integrations` table and `integration_type` field support this extensibility. ### Bidirectional Sync Enhancements - Push Alga asset changes to NinjaOne custom fields - Sync maintenance windows bidirectionally - Push ticket status updates to NinjaOne ### Advanced Features - Script execution from Alga PSA - Patch deployment initiation - Software deployment - Device group management