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:
339
ts/classes.unifi-access.ts
Normal file
339
ts/classes.unifi-access.ts
Normal file
@@ -0,0 +1,339 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import { logger } from './unifi.logger.js';
|
||||
import { UnifiHttp } from './classes.unifihttp.js';
|
||||
import { DoorManager } from './classes.doormanager.js';
|
||||
import type {
|
||||
IUnifiAccessOptions,
|
||||
IAccessDevice,
|
||||
IAccessUser,
|
||||
IAccessPolicy,
|
||||
IAccessLocation,
|
||||
IAccessEvent,
|
||||
IAccessApiResponse,
|
||||
THttpMethod,
|
||||
} from './interfaces/index.js';
|
||||
|
||||
/**
|
||||
* UniFi Access - Entry point for Access Controller API
|
||||
*
|
||||
* This class provides access to the UniFi Access API for managing doors,
|
||||
* users, credentials, and access events. It uses bearer token authentication.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const access = new UnifiAccess({
|
||||
* host: '192.168.1.1',
|
||||
* token: 'your-bearer-token',
|
||||
* });
|
||||
*
|
||||
* const doors = await access.doorManager.listDoors();
|
||||
* const users = await access.getUsers();
|
||||
*
|
||||
* // Unlock a door
|
||||
* await access.unlockDoor('door-id');
|
||||
* ```
|
||||
*/
|
||||
export class UnifiAccess {
|
||||
/** Access API port */
|
||||
private static readonly API_PORT = 12445;
|
||||
|
||||
/** Access host */
|
||||
private host: string;
|
||||
|
||||
/** Bearer token for authentication */
|
||||
private token: string;
|
||||
|
||||
/** Whether to verify SSL certificates */
|
||||
private verifySsl: boolean;
|
||||
|
||||
/** HTTP client */
|
||||
private http: UnifiHttp;
|
||||
|
||||
/** Door manager instance */
|
||||
public doorManager: DoorManager;
|
||||
|
||||
constructor(options: IUnifiAccessOptions) {
|
||||
this.host = options.host.replace(/\/$/, '');
|
||||
this.token = options.token;
|
||||
this.verifySsl = options.verifySsl ?? false;
|
||||
|
||||
// Build base URL with Access API port
|
||||
const baseHost = this.host.startsWith('http') ? this.host : `https://${this.host}`;
|
||||
// Access API is typically on port 12445 at /api/v1/developer
|
||||
const baseUrl = `${baseHost}:${UnifiAccess.API_PORT}/api/v1/developer`;
|
||||
|
||||
this.http = new UnifiHttp(baseUrl, this.verifySsl);
|
||||
this.http.setHeader('Authorization', `Bearer ${this.token}`);
|
||||
|
||||
// Initialize managers
|
||||
this.doorManager = new DoorManager(this);
|
||||
|
||||
logger.log('info', `UnifiAccess initialized for ${this.host}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a request to the Access API
|
||||
*/
|
||||
public async request<T>(
|
||||
method: THttpMethod,
|
||||
endpoint: string,
|
||||
data?: unknown
|
||||
): Promise<T> {
|
||||
return this.http.request<T>(method, endpoint, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlock a door by ID
|
||||
*/
|
||||
public async unlockDoor(doorId: string): Promise<void> {
|
||||
logger.log('info', `Unlocking door: ${doorId}`);
|
||||
|
||||
await this.request('PUT', `/door/${doorId}/unlock`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Lock a door by ID
|
||||
*/
|
||||
public async lockDoor(doorId: string): Promise<void> {
|
||||
logger.log('info', `Locking door: ${doorId}`);
|
||||
|
||||
await this.request('PUT', `/door/${doorId}/lock`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all doors (convenience method)
|
||||
*/
|
||||
public async getDoors() {
|
||||
return this.doorManager.listDoors();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all devices (hubs, readers, etc.)
|
||||
*/
|
||||
public async getDevices(): Promise<IAccessDevice[]> {
|
||||
logger.log('debug', 'Fetching Access devices');
|
||||
|
||||
const response = await this.request<IAccessApiResponse<IAccessDevice[]>>(
|
||||
'GET',
|
||||
'/device'
|
||||
);
|
||||
|
||||
return response.data || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a device by ID
|
||||
*/
|
||||
public async getDeviceById(deviceId: string): Promise<IAccessDevice | null> {
|
||||
try {
|
||||
const response = await this.request<IAccessApiResponse<IAccessDevice>>(
|
||||
'GET',
|
||||
`/device/${deviceId}`
|
||||
);
|
||||
|
||||
return response.data;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all users/credential holders
|
||||
*/
|
||||
public async getUsers(): Promise<IAccessUser[]> {
|
||||
logger.log('debug', 'Fetching Access users');
|
||||
|
||||
const response = await this.request<IAccessApiResponse<IAccessUser[]>>(
|
||||
'GET',
|
||||
'/user'
|
||||
);
|
||||
|
||||
return response.data || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a user by ID
|
||||
*/
|
||||
public async getUserById(userId: string): Promise<IAccessUser | null> {
|
||||
try {
|
||||
const response = await this.request<IAccessApiResponse<IAccessUser>>(
|
||||
'GET',
|
||||
`/user/${userId}`
|
||||
);
|
||||
|
||||
return response.data;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new user
|
||||
*/
|
||||
public async createUser(user: Partial<IAccessUser>): Promise<IAccessUser> {
|
||||
logger.log('info', `Creating user: ${user.first_name} ${user.last_name}`);
|
||||
|
||||
const response = await this.request<IAccessApiResponse<IAccessUser>>(
|
||||
'POST',
|
||||
'/user',
|
||||
user
|
||||
);
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a user
|
||||
*/
|
||||
public async updateUser(userId: string, user: Partial<IAccessUser>): Promise<IAccessUser> {
|
||||
logger.log('info', `Updating user: ${userId}`);
|
||||
|
||||
const response = await this.request<IAccessApiResponse<IAccessUser>>(
|
||||
'PUT',
|
||||
`/user/${userId}`,
|
||||
user
|
||||
);
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a user
|
||||
*/
|
||||
public async deleteUser(userId: string): Promise<void> {
|
||||
logger.log('info', `Deleting user: ${userId}`);
|
||||
|
||||
await this.request('DELETE', `/user/${userId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get access policies/groups
|
||||
*/
|
||||
public async getPolicies(): Promise<IAccessPolicy[]> {
|
||||
logger.log('debug', 'Fetching Access policies');
|
||||
|
||||
const response = await this.request<IAccessApiResponse<IAccessPolicy[]>>(
|
||||
'GET',
|
||||
'/policy'
|
||||
);
|
||||
|
||||
return response.data || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a policy by ID
|
||||
*/
|
||||
public async getPolicyById(policyId: string): Promise<IAccessPolicy | null> {
|
||||
try {
|
||||
const response = await this.request<IAccessApiResponse<IAccessPolicy>>(
|
||||
'GET',
|
||||
`/policy/${policyId}`
|
||||
);
|
||||
|
||||
return response.data;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get locations
|
||||
*/
|
||||
public async getLocations(): Promise<IAccessLocation[]> {
|
||||
logger.log('debug', 'Fetching Access locations');
|
||||
|
||||
const response = await this.request<IAccessApiResponse<IAccessLocation[]>>(
|
||||
'GET',
|
||||
'/location'
|
||||
);
|
||||
|
||||
return response.data || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get access events (entry log)
|
||||
*/
|
||||
public async getEvents(
|
||||
options: { start?: number; end?: number; limit?: number; doorId?: string; userId?: string } = {}
|
||||
): Promise<IAccessEvent[]> {
|
||||
logger.log('debug', 'Fetching Access events');
|
||||
|
||||
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());
|
||||
if (options.doorId) params.append('door_id', options.doorId);
|
||||
if (options.userId) params.append('user_id', options.userId);
|
||||
|
||||
const queryString = params.toString() ? `?${params.toString()}` : '';
|
||||
const response = await this.request<IAccessApiResponse<IAccessEvent[]>>(
|
||||
'GET',
|
||||
`/event${queryString}`
|
||||
);
|
||||
|
||||
return response.data || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recent access events
|
||||
*/
|
||||
public async getRecentEvents(limit: number = 100): Promise<IAccessEvent[]> {
|
||||
return this.getEvents({ limit });
|
||||
}
|
||||
|
||||
/**
|
||||
* Grant user access to a door
|
||||
*/
|
||||
public async grantAccess(userId: string, doorId: string): Promise<void> {
|
||||
logger.log('info', `Granting user ${userId} access to door ${doorId}`);
|
||||
|
||||
await this.request('POST', `/user/${userId}/access`, {
|
||||
door_id: doorId,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Revoke user access from a door
|
||||
*/
|
||||
public async revokeAccess(userId: string, doorId: string): Promise<void> {
|
||||
logger.log('info', `Revoking user ${userId} access from door ${doorId}`);
|
||||
|
||||
await this.request('DELETE', `/user/${userId}/access/${doorId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign NFC card to user
|
||||
*/
|
||||
public async assignNfcCard(
|
||||
userId: string,
|
||||
cardToken: string,
|
||||
alias?: string
|
||||
): Promise<void> {
|
||||
logger.log('info', `Assigning NFC card to user ${userId}`);
|
||||
|
||||
await this.request('POST', `/user/${userId}/nfc_card`, {
|
||||
token: cardToken,
|
||||
alias: alias,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove NFC card from user
|
||||
*/
|
||||
public async removeNfcCard(userId: string, cardId: string): Promise<void> {
|
||||
logger.log('info', `Removing NFC card ${cardId} from user ${userId}`);
|
||||
|
||||
await this.request('DELETE', `/user/${userId}/nfc_card/${cardId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set user PIN code
|
||||
*/
|
||||
public async setUserPin(userId: string, pinCode: string): Promise<void> {
|
||||
logger.log('info', `Setting PIN for user ${userId}`);
|
||||
|
||||
await this.request('PUT', `/user/${userId}`, {
|
||||
pin_code: pinCode,
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user