PSA/ee/docs/plans/2025-11-24-ninjaone-rmm-integration-plan.md
Hermes 284313f908
Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Initial import of AlgaPSA codebase from PSA server
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz

Source: /opt/alga-psa on psa.joliet.tech
2026-06-22 16:12:17 -05:00

943 lines
38 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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<string>;
private async refreshTokenIfNeeded(): Promise<void>;
// API methods
async getOrganizations(): Promise<NinjaOneOrganization[]>;
async getDevices(params?: DeviceQueryParams): Promise<NinjaOneDevice[]>;
async getDeviceDetails(deviceId: number): Promise<NinjaOneDeviceDetailed>;
async getDeviceAlerts(deviceId: number): Promise<NinjaOneAlert[]>;
async getAlerts(params?: AlertQueryParams): Promise<NinjaOneAlert[]>;
async getDeviceLink(deviceId: number): Promise<string>; // Remote access URL
async resetAlert(alertId: string): Promise<void>;
// Webhook management
async configureWebhook(webhookUrl: string, activities: string[]): Promise<void>;
async removeWebhook(): Promise<void>;
}
```
- [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<RmmConnectionStatus>;
export async function disconnectNinjaOne(integrationId: string): Promise<{ success: boolean }>;
export async function updateNinjaOneSettings(integrationId: string, settings: Partial<RmmIntegration>): Promise<void>;
// Organization mapping
export async function getNinjaOneOrganizations(integrationId: string): Promise<NinjaOneOrganization[]>;
export async function getOrganizationMappings(integrationId: string): Promise<RmmOrganizationMapping[]>;
export async function createOrganizationMapping(data: CreateOrgMappingRequest): Promise<RmmOrganizationMapping>;
export async function updateOrganizationMapping(mappingId: string, data: UpdateOrgMappingRequest): Promise<void>;
export async function deleteOrganizationMapping(mappingId: string): Promise<void>;
// Sync operations
export async function triggerFullSync(integrationId: string): Promise<{ jobId: string }>;
export async function getSyncStatus(integrationId: string): Promise<RmmSyncStatus>;
// Alert management
export async function getActiveAlerts(integrationId: string, filters?: AlertFilters): Promise<RmmAlert[]>;
export async function acknowledgeAlert(alertId: string): Promise<void>;
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<SyncResult>;
async runIncrementalSync(since: Date): Promise<SyncResult>;
async syncDevice(deviceId: number): Promise<Asset>;
async syncOrganization(orgId: number): Promise<SyncResult>;
private async createAsset(device: NinjaOneDevice): Promise<Asset>;
private async updateAsset(asset: Asset, device: NinjaOneDevice): Promise<Asset>;
private async handleDeletedDevice(deviceId: number): Promise<void>;
}
```
- [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<void>;
export async function handleDeviceEvent(payload: DeviceActivityPayload): Promise<void>;
export async function handleAlertEvent(payload: AlertActivityPayload): Promise<void>;
export async function handleSystemEvent(payload: SystemActivityPayload): Promise<void>;
```
#### 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<void>;
export async function removeWebhook(
client: NinjaOneClient
): Promise<void>;
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<RmmAlert>;
export async function evaluateAlertRules(
tenantId: string,
alert: RmmAlert
): Promise<AlertRuleMatch | null>;
export async function executeAlertActions(
alert: RmmAlert,
rule: RmmAlertRule
): Promise<void>;
```
#### 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>;
```
- [ ] 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