340 lines
8.2 KiB
TypeScript
340 lines
8.2 KiB
TypeScript
|
|
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,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|