feat(unifi): implement comprehensive UniFi API client with controllers, protect, access, account, managers, resources, HTTP client, interfaces, logging, plugins, and tests
This commit is contained in:
377
ts/classes.unifi-controller.ts
Normal file
377
ts/classes.unifi-controller.ts
Normal file
@@ -0,0 +1,377 @@
|
||||
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<void> {
|
||||
// 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<void> {
|
||||
// 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<T>(
|
||||
method: THttpMethod,
|
||||
endpoint: string,
|
||||
data?: unknown
|
||||
): Promise<T> {
|
||||
if (!this.authenticated) {
|
||||
throw new Error('Not authenticated. Call login() first or provide API key.');
|
||||
}
|
||||
|
||||
return this.http.request<T>(method, endpoint, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* List all sites on this controller
|
||||
*/
|
||||
public async listSites(): Promise<INetworkSite[]> {
|
||||
logger.log('debug', 'Fetching sites');
|
||||
|
||||
const response = await this.request<IUnifiApiResponse<INetworkSite>>(
|
||||
'GET',
|
||||
'/api/self/sites'
|
||||
);
|
||||
|
||||
return response.data || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get system info
|
||||
*/
|
||||
public async getSystemInfo(): Promise<unknown> {
|
||||
return this.request('GET', '/api/s/default/stat/sysinfo');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get controller health
|
||||
*/
|
||||
public async getHealth(siteId: string = 'default'): Promise<unknown> {
|
||||
return this.request('GET', `/api/s/${siteId}/stat/health`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get active alerts
|
||||
*/
|
||||
public async getAlerts(siteId: string = 'default'): Promise<unknown> {
|
||||
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<unknown> {
|
||||
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<unknown> {
|
||||
return this.request('GET', `/api/s/${siteId}/rest/wlanconf`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get network configurations
|
||||
*/
|
||||
public async getNetworks(siteId: string = 'default'): Promise<unknown> {
|
||||
return this.request('GET', `/api/s/${siteId}/rest/networkconf`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get port forward rules
|
||||
*/
|
||||
public async getPortForwards(siteId: string = 'default'): Promise<unknown> {
|
||||
return this.request('GET', `/api/s/${siteId}/rest/portforward`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get firewall rules
|
||||
*/
|
||||
public async getFirewallRules(siteId: string = 'default'): Promise<unknown> {
|
||||
return this.request('GET', `/api/s/${siteId}/rest/firewallrule`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get DPI stats
|
||||
*/
|
||||
public async getDpiStats(siteId: string = 'default'): Promise<unknown> {
|
||||
return this.request('GET', `/api/s/${siteId}/stat/dpi`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Backup the controller configuration
|
||||
*/
|
||||
public async createBackup(siteId: string = 'default'): Promise<unknown> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user