import type { IDataObject } from 'n8n-workflow'; import axios, { AxiosInstance, AxiosError } from 'axios'; export interface AdCollectorConfig { collectorUrl: string; jwtToken: string; } export class AdCollectorApiClient { private axiosInstance: AxiosInstance; private baseUrl: string; constructor(config: AdCollectorConfig) { // Validate URL if (!config.collectorUrl || config.collectorUrl.trim() === '') { throw new Error('AD Collector URL is empty'); } const urlTrimmed = config.collectorUrl.trim(); // Check if URL starts with http:// or https:// if (!urlTrimmed.startsWith('http://') && !urlTrimmed.startsWith('https://')) { throw new Error(`AD Collector URL must start with http:// or https://. Current value: "${urlTrimmed}"`); } // Validate URL format try { new URL(urlTrimmed); } catch (error) { throw new Error(`Invalid AD Collector URL format: "${urlTrimmed}"`); } // Validate JWT token if (!config.jwtToken || config.jwtToken.trim() === '') { throw new Error('AD Collector JWT Token is empty'); } this.baseUrl = urlTrimmed.replace(/\/$/, ''); // Remove trailing slash this.axiosInstance = axios.create({ baseURL: this.baseUrl, headers: { 'Authorization': `Bearer ${config.jwtToken}`, 'Content-Type': 'application/json', }, timeout: 30000, // 30 seconds timeout }); } async listUsers(filter?: string, maxResults?: number): Promise { try { const response = await this.axiosInstance.post('/api/users/list', { filter: filter || '(&(objectClass=user)(objectCategory=person))', maxResults: maxResults || 1000, }); return response.data; } catch (error) { throw this.handleError(error, 'Failed to list users'); } } async getUser(samAccountName: string): Promise { try { const response = await this.axiosInstance.post('/api/users/get', { samAccountName, }); return response.data; } catch (error) { throw this.handleError(error, `Failed to get user: ${samAccountName}`); } } async createUser(userData: IDataObject): Promise { try { const response = await this.axiosInstance.post('/api/users/create', { samAccountName: userData.samAccountName, password: userData.password, firstName: userData.firstName, lastName: userData.lastName, ou: userData.parentOuDn, // Collector API expects 'ou' parameter displayName: userData.cn, userPrincipalName: userData.userPrincipalName, }); return response.data; } catch (error) { throw this.handleError(error, 'Failed to create user'); } } async enableUser(dnOrSam: string): Promise { try { // Try to detect if it's a DN (contains CN=) or samAccountName const payload = dnOrSam.includes('CN=') || dnOrSam.includes('OU=') ? { dn: dnOrSam } : { samAccountName: dnOrSam }; const response = await this.axiosInstance.post('/api/users/enable', payload); return response.data; } catch (error) { throw this.handleError(error, `Failed to enable user: ${dnOrSam}`); } } async disableUser(dnOrSam: string): Promise { try { // Try to detect if it's a DN (contains CN=) or samAccountName const payload = dnOrSam.includes('CN=') || dnOrSam.includes('OU=') ? { dn: dnOrSam } : { samAccountName: dnOrSam }; const response = await this.axiosInstance.post('/api/users/disable', payload); return response.data; } catch (error) { throw this.handleError(error, `Failed to disable user: ${dnOrSam}`); } } async resetPassword(dnOrSam: string, newPassword: string, forceChange: boolean = false): Promise { try { // Try to detect if it's a DN (contains CN=) or samAccountName const payload = dnOrSam.includes('CN=') || dnOrSam.includes('OU=') ? { dn: dnOrSam, newPassword, forceChange } : { samAccountName: dnOrSam, newPassword, forceChange }; const response = await this.axiosInstance.post('/api/users/reset-password', payload); return response.data; } catch (error) { throw this.handleError(error, `Failed to reset password for user: ${dnOrSam}`); } } async listGroups(searchFilter?: string): Promise { try { const response = await this.axiosInstance.post('/api/groups/list', { searchFilter: searchFilter || '', }); return response.data; } catch (error) { throw this.handleError(error, 'Failed to list groups'); } } // ==================== USER OPERATIONS ==================== async setAttributes(dnOrSam: string, attributes: IDataObject): Promise { try { const payload = dnOrSam.includes('CN=') || dnOrSam.includes('OU=') ? { dn: dnOrSam, attributes } : { samAccountName: dnOrSam, attributes }; const response = await this.axiosInstance.post('/api/users/set-attributes', payload); return response.data; } catch (error) { throw this.handleError(error, `Failed to set attributes for user: ${dnOrSam}`); } } async findBySAM(samAccountName: string): Promise { try { const response = await this.axiosInstance.post('/api/users/find-by-sam', { samAccountName, }); return response.data; } catch (error) { throw this.handleError(error, `Failed to find user by SAM: ${samAccountName}`); } } async getUserGroups(samAccountName: string): Promise { try { const response = await this.axiosInstance.post('/api/users/get-groups', { samAccountName, }); return response.data; } catch (error) { throw this.handleError(error, `Failed to get groups for user: ${samAccountName}`); } } async getUserActivity(samAccountName: string): Promise { try { const response = await this.axiosInstance.post('/api/users/get-activity', { samAccountName, }); return response.data; } catch (error) { throw this.handleError(error, `Failed to get activity for user: ${samAccountName}`); } } async unlockAccount(dnOrSam: string): Promise { try { const payload = dnOrSam.includes('CN=') || dnOrSam.includes('OU=') ? { dn: dnOrSam } : { samAccountName: dnOrSam }; const response = await this.axiosInstance.post('/api/users/unlock', payload); return response.data; } catch (error) { throw this.handleError(error, `Failed to unlock account: ${dnOrSam}`); } } async checkPasswordExpiry(samAccountName: string): Promise { try { const response = await this.axiosInstance.post('/api/users/check-password-expiry', { samAccountName, }); return response.data; } catch (error) { throw this.handleError(error, `Failed to check password expiry for user: ${samAccountName}`); } } // ==================== GROUP OPERATIONS ==================== async getGroup(dnOrSam: string): Promise { try { const payload = dnOrSam.includes('CN=') || dnOrSam.includes('OU=') ? { dn: dnOrSam } : { samAccountName: dnOrSam }; const response = await this.axiosInstance.post('/api/groups/get', payload); return response.data; } catch (error) { throw this.handleError(error, `Failed to get group: ${dnOrSam}`); } } async createGroup(groupData: IDataObject): Promise { try { const response = await this.axiosInstance.post('/api/groups/create', { samAccountName: groupData.samAccountName, name: groupData.name, ou: groupData.ou, description: groupData.description, groupType: groupData.groupType, }); return response.data; } catch (error) { throw this.handleError(error, 'Failed to create group'); } } async modifyGroup(dnOrSam: string, attributes: IDataObject): Promise { try { const payload = dnOrSam.includes('CN=') || dnOrSam.includes('OU=') ? { dn: dnOrSam, attributes } : { samAccountName: dnOrSam, attributes }; const response = await this.axiosInstance.post('/api/groups/modify', payload); return response.data; } catch (error) { throw this.handleError(error, `Failed to modify group: ${dnOrSam}`); } } async deleteGroup(dnOrSam: string): Promise { try { const payload = dnOrSam.includes('CN=') || dnOrSam.includes('OU=') ? { dn: dnOrSam } : { samAccountName: dnOrSam }; const response = await this.axiosInstance.post('/api/groups/delete', payload); return response.data; } catch (error) { throw this.handleError(error, `Failed to delete group: ${dnOrSam}`); } } async addGroupMember(userDn: string, groupDn: string): Promise { try { const response = await this.axiosInstance.post('/api/groups/add-member', { userDn, groupDn, }); return response.data; } catch (error) { throw this.handleError(error, `Failed to add member to group`); } } async removeGroupMember(userDn: string, groupDn: string): Promise { try { const response = await this.axiosInstance.post('/api/groups/remove-member', { userDn, groupDn, }); return response.data; } catch (error) { throw this.handleError(error, `Failed to remove member from group`); } } // ==================== OU OPERATIONS ==================== async getOu(dn: string): Promise { try { const response = await this.axiosInstance.post('/api/ous/get', { dn, }); return response.data; } catch (error) { throw this.handleError(error, `Failed to get OU: ${dn}`); } } async listOus(searchFilter?: string, maxResults?: number): Promise { try { const response = await this.axiosInstance.post('/api/ous/list', { searchFilter: searchFilter || '', maxResults: maxResults || 1000, }); return response.data; } catch (error) { throw this.handleError(error, 'Failed to list OUs'); } } async createOu(name: string, parentDn?: string, description?: string): Promise { try { const response = await this.axiosInstance.post('/api/ous/create', { name, parentDn, description, }); return response.data; } catch (error) { throw this.handleError(error, 'Failed to create OU'); } } async modifyOu(dn: string, attributes: IDataObject): Promise { try { const response = await this.axiosInstance.post('/api/ous/modify', { dn, attributes, }); return response.data; } catch (error) { throw this.handleError(error, `Failed to modify OU: ${dn}`); } } async deleteOu(dn: string): Promise { try { const response = await this.axiosInstance.post('/api/ous/delete', { dn, }); return response.data; } catch (error) { throw this.handleError(error, `Failed to delete OU: ${dn}`); } } // ==================== AUDIT OPERATIONS ==================== async runAudit(includeDetails: boolean = false, includeComputers: boolean = false): Promise { try { const response = await this.axiosInstance.post('/api/audit', { includeDetails, includeComputers, }, { timeout: 300000, // 5 minutes timeout for audit operations }); return response.data; } catch (error) { throw this.handleError(error, 'Failed to run AD audit'); } } private handleError(error: unknown, defaultMessage: string): Error { if (axios.isAxiosError(error)) { const axiosError = error as AxiosError; // Extract error message from response if (axiosError.response?.data) { const responseData = axiosError.response.data as any; const errorMessage = responseData.error || responseData.message || defaultMessage; return new Error(`AD Collector API Error: ${errorMessage}`); } // Network or timeout errors if (axiosError.code === 'ECONNREFUSED') { return new Error(`Cannot connect to AD Collector at ${this.baseUrl}. Please check if the Collector is running.`); } if (axiosError.code === 'ETIMEDOUT') { return new Error(`Request to AD Collector timed out. The Collector may be overloaded or unreachable.`); } // Generic axios error return new Error(`${defaultMessage}: ${axiosError.message}`); } // Non-axios error if (error instanceof Error) { return new Error(`${defaultMessage}: ${error.message}`); } return new Error(defaultMessage); } }