import * as plugins from './plugins.js'; import { logger } from './unifi.logger.js'; import { UnifiHttp } from './classes.unifihttp.js'; import { DeviceManager } from './classes.devicemanager.js'; import { ClientManager } from './classes.clientmanager.js'; import type { IUnifiControllerOptions, INetworkAuthResponse, INetworkSite, IUnifiApiResponse, THttpMethod, } from './interfaces/index.js'; /** * UniFi Controller - Entry point for Network Controller API * * This class provides access to the UniFi Network Controller API. * Supports two authentication methods: * 1. API Key (preferred) - Set X-API-Key header, no login required * 2. Session auth - Username/password login with session cookies * * @example * ```typescript * // Using API key (no login required) * const controller = new UnifiController({ * host: '192.168.1.1', * apiKey: 'your-api-key', * }); * const devices = await controller.deviceManager.listDevices(); * * // Using session auth (requires login) * const controller = new UnifiController({ * host: '192.168.1.1', * username: 'admin', * password: 'password', * }); * await controller.login(); * const devices = await controller.deviceManager.listDevices(); * await controller.logout(); * ``` */ export class UnifiController { /** Controller host */ private host: string; /** API key for authentication */ private apiKey?: string; /** Username for session authentication */ private username?: string; /** Password for session authentication */ private password?: string; /** Controller type determines API paths */ private controllerType: 'unifi-os' | 'udm-pro' | 'standalone'; /** Whether to verify SSL certificates */ private verifySsl: boolean; /** HTTP client */ private http: UnifiHttp; /** Whether currently authenticated */ private authenticated: boolean = false; /** CSRF token (for UniFi OS session auth) */ private csrfToken?: string; /** Device manager instance */ public deviceManager: DeviceManager; /** Client manager instance */ public clientManager: ClientManager; constructor(options: IUnifiControllerOptions) { this.host = options.host.replace(/\/$/, ''); this.apiKey = options.apiKey; this.username = options.username; this.password = options.password; this.controllerType = options.controllerType || 'unifi-os'; this.verifySsl = options.verifySsl ?? false; // Build base URL based on controller type const baseUrl = this.getBaseUrl(); this.http = new UnifiHttp(baseUrl, this.verifySsl); // If API key provided, set it and mark as authenticated if (this.apiKey) { this.http.setHeader('X-API-Key', this.apiKey); this.authenticated = true; } // Initialize managers this.deviceManager = new DeviceManager(this); this.clientManager = new ClientManager(this); logger.log('info', `UnifiController initialized for ${this.host} (${this.controllerType})`); } /** * Get the base URL for API requests based on controller type */ private getBaseUrl(): string { // Add https:// if not present const host = this.host.startsWith('http') ? this.host : `https://${this.host}`; switch (this.controllerType) { case 'unifi-os': case 'udm-pro': // UniFi OS consoles (UDM, UDM Pro, Cloud Key Gen2+) use /proxy/network prefix return `${host}/proxy/network`; case 'standalone': // Standalone controllers (software controller) return host; default: return host; } } /** * Get the login endpoint based on controller type */ private getLoginEndpoint(): string { switch (this.controllerType) { case 'unifi-os': case 'udm-pro': // UniFi OS uses the root /api/auth/login return '/api/auth/login'; case 'standalone': return '/api/login'; default: return '/api/login'; } } /** * Get the logout endpoint based on controller type */ private getLogoutEndpoint(): string { switch (this.controllerType) { case 'unifi-os': case 'udm-pro': return '/api/auth/logout'; case 'standalone': return '/api/logout'; default: return '/api/logout'; } } /** * Login to the controller (only needed for session auth, not API key) */ public async login(): Promise { // If using API key, already authenticated if (this.apiKey) { logger.log('info', 'Using API key authentication, no login required'); this.authenticated = true; return; } if (!this.username || !this.password) { throw new Error('Username and password required for session authentication'); } logger.log('info', `Logging in to UniFi Controller at ${this.host}`); // For UniFi OS, login happens at the console level (not through /proxy/network) let loginUrl: string; if (this.controllerType === 'unifi-os' || this.controllerType === 'udm-pro') { const host = this.host.startsWith('http') ? this.host : `https://${this.host}`; loginUrl = `${host}${this.getLoginEndpoint()}`; } else { loginUrl = `${this.getBaseUrl()}${this.getLoginEndpoint()}`; } // Create a separate HTTP client for login (different base URL for UniFi OS) const loginHttp = new UnifiHttp( loginUrl.replace(this.getLoginEndpoint(), ''), this.verifySsl ); const response = await loginHttp.rawRequest('POST', this.getLoginEndpoint(), { username: this.username, password: this.password, }); if (!response.ok) { const errorText = await response.text().catch(() => 'Unknown error'); throw new Error(`Login failed: HTTP ${response.status} - ${errorText}`); } // Store cookies from response const setCookieHeaders = response.headers?.['set-cookie']; if (setCookieHeaders && Array.isArray(setCookieHeaders)) { this.http.setCookies(setCookieHeaders); } // Extract CSRF token from response (UniFi OS) const csrfHeader = response.headers?.['x-csrf-token']; if (csrfHeader) { this.csrfToken = Array.isArray(csrfHeader) ? csrfHeader[0] : csrfHeader; this.http.setHeader('X-CSRF-Token', this.csrfToken); } this.authenticated = true; logger.log('info', 'Successfully logged in to UniFi Controller'); } /** * Logout from the controller (only for session auth) */ public async logout(): Promise { // If using API key, nothing to logout if (this.apiKey) { return; } if (!this.authenticated) { return; } logger.log('info', 'Logging out from UniFi Controller'); try { // For UniFi OS, logout happens at the console level if (this.controllerType === 'unifi-os' || this.controllerType === 'udm-pro') { const host = this.host.startsWith('http') ? this.host : `https://${this.host}`; const logoutHttp = new UnifiHttp(host, this.verifySsl); logoutHttp.setCookies([this.http.getCookieHeader()]); if (this.csrfToken) { logoutHttp.setHeader('X-CSRF-Token', this.csrfToken); } await logoutHttp.rawRequest('POST', this.getLogoutEndpoint()); } else { await this.http.rawRequest('POST', this.getLogoutEndpoint()); } } catch (error) { logger.log('warn', `Logout error (may be expected): ${error}`); } this.authenticated = false; this.csrfToken = undefined; } /** * Check if logged in */ public isAuthenticated(): boolean { return this.authenticated; } /** * Make a request to the controller API */ public async request( method: THttpMethod, endpoint: string, data?: unknown ): Promise { if (!this.authenticated) { throw new Error('Not authenticated. Call login() first or provide API key.'); } return this.http.request(method, endpoint, data); } /** * List all sites on this controller */ public async listSites(): Promise { logger.log('debug', 'Fetching sites'); const response = await this.request>( 'GET', '/api/self/sites' ); return response.data || []; } /** * Get system info */ public async getSystemInfo(): Promise { return this.request('GET', '/api/s/default/stat/sysinfo'); } /** * Get controller health */ public async getHealth(siteId: string = 'default'): Promise { return this.request('GET', `/api/s/${siteId}/stat/health`); } /** * Get active alerts */ public async getAlerts(siteId: string = 'default'): Promise { return this.request('GET', `/api/s/${siteId}/stat/alarm`); } /** * Get events */ public async getEvents( siteId: string = 'default', options: { start?: number; end?: number; limit?: number } = {} ): Promise { const params = new URLSearchParams(); if (options.start) params.append('start', options.start.toString()); if (options.end) params.append('end', options.end.toString()); if (options.limit) params.append('_limit', options.limit.toString()); const queryString = params.toString() ? `?${params.toString()}` : ''; return this.request('GET', `/api/s/${siteId}/stat/event${queryString}`); } /** * Get WLAN configurations */ public async getWlans(siteId: string = 'default'): Promise { return this.request('GET', `/api/s/${siteId}/rest/wlanconf`); } /** * Get network configurations */ public async getNetworks(siteId: string = 'default'): Promise { return this.request('GET', `/api/s/${siteId}/rest/networkconf`); } /** * Get port forward rules */ public async getPortForwards(siteId: string = 'default'): Promise { return this.request('GET', `/api/s/${siteId}/rest/portforward`); } /** * Get firewall rules */ public async getFirewallRules(siteId: string = 'default'): Promise { return this.request('GET', `/api/s/${siteId}/rest/firewallrule`); } /** * Get DPI stats */ public async getDpiStats(siteId: string = 'default'): Promise { return this.request('GET', `/api/s/${siteId}/stat/dpi`); } /** * Backup the controller configuration */ public async createBackup(siteId: string = 'default'): Promise { return this.request('POST', `/api/s/${siteId}/cmd/backup`, { cmd: 'backup', }); } /** * Get devices (convenience method) */ public async getDevices(siteId: string = 'default') { return this.deviceManager.listDevices(siteId); } /** * Get active clients (convenience method) */ public async getClients(siteId: string = 'default') { return this.clientManager.listActiveClients(siteId); } }