import { createWriteStream } from 'fs' import Axios from 'axios' import { AxiosRequestConfig } from 'axios' const { DEVICE_API_KEY, DEVICE_API_URL } = process.env const Api = Axios.create({ baseURL: DEVICE_API_URL, headers: DEVICE_API_KEY ? { Key: DEVICE_API_KEY } : {}, }) export function setDeviceApiKey(key: string): void { Api.defaults.headers['Key'] = key } export function setDeviceApiUrl(url: string): void { Api.defaults.baseURL = url } export interface ApiError { msg: string message: string stack: string isAxiosError: boolean isApiError: boolean status: number host: string data: { [key: string]: any } } function handleApiError(err: any, newStack: string): ApiError { const msg = err?.response?.data?.msg || err.message return { msg, message: msg, stack: err.isAxiosError ? `Error: ${msg}\n` + newStack .split('\n') .filter((s, i) => i > 6) .join('\n') : err.stack, isAxiosError: err.isAxiosError || false, isApiError: true, status: err?.response?.status || null, host: err?.request?.host || 'localhost', data: err?.response?.data || null, } } export interface UserInfo { _id: string email: string phone: string firstName: string lastName: string country: string state: string permission: string subscription: string verified: string[] customerId: string twoFactorAuth: string loginKey: string last2FALoginAttempt: Date | string lastPasswordReset: Date | string lastLoggedIn: Date | string enabled: boolean createdAt: any updatedAt: any } export interface TrimmedDevice { _id: string name: string address: string mac: string scripts: number devices: number lastPinged: string createdAt: string updatedAt: string } export interface ScriptTrigger { id: string name: string mac: string inputs: { [key: string]: any } } export interface ScriptCondition { id: string inverted: boolean name: string mac: string inputs: { [key: string]: any } } export interface ScriptAction { id: string deviceId: string name: string mac: string inputs: { [key: string]: any } } export interface Script { id: string name: string conditional: 'and' | 'or' trigger: ScriptTrigger conditions: ScriptCondition[] actions: ScriptAction[] } export interface ScriptError { scriptName: string scriptId: string fault: 'condition' | 'action' | 'trigger' | 'none' faultId: string msg: string fatal: boolean } export interface DeviceInfo { user: UserInfo code: string name: string mac: string address: string hiddenCredentials: { [key: string]: any } scripts: Script[] data: { [key: string]: any } lastPinged: string createdAt: string updatedAt: string } export interface DeviceJSON { label?: string name: string mac: string ip: string oui: string companyName: string companyAddress: string integration?: string timesSeen?: number tags?: string[] online?: boolean timesDisconnected?: number seenAt?: string discovertedAt?: string disconnectedAt?: string state?: { [key: string]: string | number | boolean | bigint | string[] | number[] | boolean[] | bigint[] } dashboard?: { [key: string]: any }[] } export interface IntegrationJSON { name: string display: string description: string triggers: { [key: string]: any } conditions: { [key: string]: any } actions: { [key: string]: any } } const apiVersion = 4 export interface GetDeviceStatusResponse { msg: string version: number } /** * Gets the status of the server */ export async function getDeviceStatus(): Promise { const newStack = new Error().stack try { const { data } = await Api.get('/') return data } catch (err) { throw handleApiError(err, newStack) } } /** * Checks whether the server's version matches the api's version */ export async function checkDeviceVersionMatch(): Promise { const newStack = new Error().stack try { const { data } = await Api.get('/') if (apiVersion !== data.version) { throw new Error(`Api version ${apiVersion} does not match server version ${data.version}`) } } catch (err) { throw handleApiError(err, newStack) } } interface CustomRequestParams extends AxiosRequestConfig { forwardIps?: string } export interface CustomDeviceRequestResponse { [key: string]: any } /** * Custom server request for testing */ export async function customDeviceRequest( config: CustomRequestParams ): Promise { const newStack = new Error().stack try { const { data } = await Api.request({ headers: { 'X-Forwarded-For': config?.forwardIps || '::1' }, ...config, }) return data } catch (err) { throw handleApiError(err, newStack) } } export interface UserGetDevicesResponse { msg: string devices: TrimmedDevice[] } /** * Returns the user's devices using the user's auth token */ export async function userGetDevices({ token, forwardIps, }: { token: string forwardIps?: string }): Promise { const newStack = new Error().stack try { const { data } = await Api.get('/api/users', { headers: { Authorization: token, 'X-Forwarded-For': forwardIps || '::1', }, }) return data } catch (err) { throw handleApiError(err, newStack) } } export interface MsgOnlyResponse { msg: string } /** * Updates all the user's devices to contain the latest user information */ export async function userUpdateDevices({ token, forwardIps, }: { token: string forwardIps?: string }): Promise { const newStack = new Error().stack try { const { data } = await Api.post( '/api/users', {}, { headers: { Authorization: token, 'X-Forwarded-For': forwardIps || '::1', }, } ) return data } catch (err) { throw handleApiError(err, newStack) } } export interface UserGetDeviceResponse { msg: string device: DeviceInfo } /** * Returns the user's device using a user's auth token */ export async function userGetDevice({ token, deviceId, forwardIps, }: { token: string deviceId: string forwardIps?: string }): Promise { const newStack = new Error().stack try { const { data } = await Api.get(`/api/users/${deviceId}`, { headers: { Authorization: token, 'X-Forwarded-For': forwardIps || '::1', }, }) return data } catch (err) { throw handleApiError(err, newStack) } } export interface UserGetDeviceSettingsResponse { msg: string name: string code: string mac: string lastPinged: string settings: { [key: string]: any } credentials: { [key: string]: string } // Hidden credentials settingsSchema: { [key: string]: any } credentialsSchema: { [key: string]: any } } /** * Gets the device's settings, credentials and name */ export async function userGetDeviceSettings({ token, deviceId, forwardIps, }: { token: string deviceId: string forwardIps?: string }): Promise { const newStack = new Error().stack try { const { data } = await Api.get(`/api/users/${deviceId}/settings`, { headers: { Authorization: token, 'X-Forwarded-For': forwardIps || '::1', }, }) return data } catch (err) { throw handleApiError(err, newStack) } } /** * Updates the device's settings and name using the user's auth token */ export async function userUpdateDeviceSettings({ token, deviceId, name, settings, credentials, forwardIps, }: { name: string token: string deviceId: string settings: { [key: string]: any } credentials: { [key: string]: string } forwardIps?: string }): Promise { const newStack = new Error().stack try { const { data } = await Api.post( `/api/users/${deviceId}/settings`, { credentials, settings, name, }, { headers: { Authorization: token, 'X-Forwarded-For': forwardIps || '::1', }, } ) return data } catch (err) { throw handleApiError(err, newStack) } } export interface UserGetDeviceScriptsResponse { msg: string deviceId: string name: string scripts: Script[] devices: DeviceJSON[] integrations: IntegrationJSON[] scriptErrors: ScriptError[] ownedDevices: { _id: string; mac: string; address: string; name: string }[] } /** * Gets the device's scripts using the user's auth token */ export async function userGetDeviceScripts({ token, deviceId, forwardIps, }: { token: string deviceId: string forwardIps?: string }): Promise { const newStack = new Error().stack try { const { data } = await Api.get(`/api/users/${deviceId}/scripts`, { headers: { Authorization: token, 'X-Forwarded-For': forwardIps || '::1', }, }) return data } catch (err) { throw handleApiError(err, newStack) } } /** * Updates the device's scripts using the user's auth token */ export async function userUpdateDeviceScripts({ token, deviceId, scripts, forwardIps, }: { token: string deviceId: string scripts: Script[] forwardIps?: string }): Promise { const newStack = new Error().stack try { const { data } = await Api.post( `/api/users/${deviceId}/scripts`, { scripts, }, { headers: { Authorization: token, 'X-Forwarded-For': forwardIps || '::1', }, } ) return data } catch (err) { throw handleApiError(err, newStack) } } export interface UserSendDeviceActionsResponse { msg: string results: string[] } /** * Sends actions to a device or multiple devices */ export async function userSendDeviceActions({ token, actions, forwardIps, }: { token: string actions: ScriptAction[] forwardIps?: string }): Promise { const newStack = new Error().stack try { const { data } = await Api.post( '/api/users/actions', { actions, }, { headers: { Authorization: token, 'X-Forwarded-For': forwardIps || '::1', }, } ) return data } catch (err) { throw handleApiError(err, newStack) } } export interface UserGetDeviceAssignStatusResponse { msg: string device: { name: string; address: string; mac: string; user: null; lastPinged: string } } /** * Returns the device info if it is currently unassigned */ export async function userGetDeviceAssignStatus({ token, code, forwardIps, }: { token: string code: string forwardIps?: string }): Promise { const newStack = new Error().stack try { const { data } = await Api.get(`/api/users/assign/${code}`, { headers: { Authorization: token, 'X-Forwarded-For': forwardIps || '::1', }, }) return data } catch (err) { throw handleApiError(err, newStack) } } export interface UserAssignDeviceResponse { msg: string device: { _id: string name: string address: string mac: string lastPinged: string user: UserInfo } } /** * Attempts to assign the device to the user */ export async function userAssignDevice({ token, code, name, forwardIps, }: { token: string code: string name: string forwardIps?: string }): Promise { const newStack = new Error().stack try { const { data } = await Api.put( `/api/users/assign/${code}`, { name, }, { headers: { Authorization: token, 'X-Forwarded-For': forwardIps || '::1', }, } ) return data } catch (err) { throw handleApiError(err, newStack) } } /** * Attempts to un-assign the device from the user */ export async function userUnassignDevice({ token, code, forwardIps, }: { token: string code: string forwardIps?: string }): Promise { const newStack = new Error().stack try { const { data } = await Api.delete(`/api/users/assign/${code}`, { headers: { Authorization: token, 'X-Forwarded-For': forwardIps || '::1', }, }) return data } catch (err) { throw handleApiError(err, newStack) } } /** * Deletes the device from the database */ export async function userDeleteDevice({ token, deviceId, forwardIps, }: { token: string deviceId: string forwardIps?: string }): Promise { const newStack = new Error().stack try { const { data } = await Api.delete(`/api/users/delete/${deviceId}`, { headers: { Authorization: token, 'X-Forwarded-For': forwardIps || '::1', }, }) return data } catch (err) { throw handleApiError(err, newStack) } } export interface ClientLog { msg: string timestamp: number times: number err: boolean } export interface ClientCrash { msg: string timestamp: number version: number } export interface UserGetDeviceLogsResponse { msg: string history: ClientLog[] crashes: ClientCrash[] } /** * Gets the logs from the device. Only works on support or admin users. */ export async function userGetDeviceLogs({ token, deviceId, forwardIps, }: { token: string deviceId: string forwardIps?: string }): Promise { const newStack = new Error().stack try { const { data } = await Api.get(`/api/users/logs/${deviceId}`, { headers: { Authorization: token, 'X-Forwarded-For': forwardIps || '::1', }, }) return data } catch (err) { throw handleApiError(err, newStack) } } export interface DeviceRegisterResponse { msg: string code: string deviceId: string mac: string privateKey: string window: string errors: { [key: string]: any }[] } /** * Registers a new device in the database */ export async function deviceRegister({ code, mac, forwardIps, }: { code: string mac: string forwardIps?: string }): Promise { const newStack = new Error().stack try { const { data } = await Api.put( '/api/devices', { code, mac, }, { headers: { 'X-Forwarded-For': forwardIps || '::1', }, } ) return data } catch (err) { throw handleApiError(err, newStack) } } /** * Checks to validate the device Id */ export async function deviceValidateId({ deviceId, forwardIps, }: { deviceId: string forwardIps?: string }): Promise { const newStack = new Error().stack try { const { data } = await Api.get(`/api/devices/validate/${deviceId}`, { headers: { 'X-Forwarded-For': forwardIps || '::1', }, }) return data } catch (err) { throw handleApiError(err, newStack) } } /** * Updates the device's logs */ export async function deviceUpdateLogs({ token, logs, forwardIps, }: { token: string logs: any[] forwardIps?: string }): Promise { const newStack = new Error().stack try { const { data } = await Api.post( '/api/devices/logs', { logs, }, { headers: { Authorization: token, 'X-Forwarded-For': forwardIps || '::1', }, } ) return data } catch (err) { throw handleApiError(err, newStack) } } /** * Updates the device's logs */ export async function deviceUpdateCrashes({ token, crash, forwardIps, }: { token: string crash: { [key: string]: any } forwardIps?: string }): Promise { const newStack = new Error().stack try { const { data } = await Api.put( '/api/devices/crashes', { crash, }, { headers: { Authorization: token, 'X-Forwarded-For': forwardIps || '::1', }, } ) return data } catch (err) { throw handleApiError(err, newStack) } } export interface DeviceGetDataResponse { scripts: Script[] user: UserInfo | null name: string settings: { [key: string]: any } actions: ScriptAction[] credentials: string | null } /** * Gets device data */ export async function deviceGetData({ token, forwardIps, }: { token: string forwardIps?: string }): Promise { const newStack = new Error().stack try { const { data: resData } = await Api.get('/api/devices/data', { headers: { Authorization: token, 'X-Forwarded-For': forwardIps || '::1', }, }) return resData } catch (err) { throw handleApiError(err, newStack) } } export interface DeviceUpdateDataResponse { scripts: Script[] user: UserInfo | null name: string settings: { [key: string]: any } actions: ScriptAction[] credentials: string | null } /** * Updates the device data */ export async function deviceUpdateData({ token, data, forwardIps, }: { token: string data: { [key: string]: any } forwardIps?: string }): Promise { const newStack = new Error().stack try { const { data: resData } = await Api.put( '/api/devices/data', { data, }, { headers: { Authorization: token, 'X-Forwarded-For': forwardIps || '::1', }, } ) return resData } catch (err) { throw handleApiError(err, newStack) } } export interface DeviceSetStatusResponse { status: string | null } /** * Sets the device's status */ export async function deviceSetStatus({ token, status, forwardIps, }: { token: string status: string forwardIps?: string }): Promise { const newStack = new Error().stack try { const { data } = await Api.put( `/api/devices/status/${status}`, {}, { headers: { Authorization: token, 'X-Forwarded-For': forwardIps || '::1', }, } ) return data } catch (err) { throw handleApiError(err, newStack) } } export interface DeviceClearStatusResponse { status: string | null } /** * Clears the device's status */ export async function deviceClearStatus({ token, forwardIps, }: { token: string forwardIps?: string }): Promise { const newStack = new Error().stack try { const { data } = await Api.delete('/api/devices/status', { headers: { Authorization: token, 'X-Forwarded-For': forwardIps || '::1', }, }) return data } catch (err) { throw handleApiError(err, newStack) } } export interface DeviceSendActionsResponse { actions: ScriptAction[] } /** * Sends actions from the device to other devices */ export async function deviceSendActions({ token, actions, forwardIps, }: { token: string actions: ScriptAction[] forwardIps?: string }): Promise { const newStack = new Error().stack try { const { data } = await Api.put( '/api/devices/actions', { actions, }, { headers: { Authorization: token, 'X-Forwarded-For': forwardIps || '::1', }, } ) return data } catch (err) { throw handleApiError(err, newStack) } } export interface DeviceSendEmailResponse { actions: ScriptAction[] } /** * Sends an email as the device */ export async function deviceSendEmail({ token, msg, forwardIps, }: { token: string msg: string forwardIps?: string }): Promise { const newStack = new Error().stack try { const { data } = await Api.post( '/api/devices/email', { msg, }, { headers: { Authorization: token, 'X-Forwarded-For': forwardIps || '::1', }, } ) return data } catch (err) { throw handleApiError(err, newStack) } } export interface DeviceSendSMSResponse { actions: ScriptAction[] } /** * Sends a phone SMS message as the device */ export async function deviceSendSMS({ token, msg, forwardIps, }: { token: string msg: string forwardIps?: string }): Promise { const newStack = new Error().stack try { const { data } = await Api.post( '/api/devices/phone', { msg, }, { headers: { Authorization: token, 'X-Forwarded-For': forwardIps || '::1', }, } ) return data } catch (err) { throw handleApiError(err, newStack) } } export interface Location { range: number[] country: string region: string eu: string timezone: string city: string ll: number[] metro: number area: number } export interface StartupDeviceInfo { user: { [key: string]: any } name: string mac: string address: string lastPinged: string status: string keepLogs: string limits: { [key: string]: any } createdAt: string | Date updatedAt: string | Date } export interface DeviceGetStartupInfoResponse { msg: string // Server msg location: Location // Device location information device: StartupDeviceInfo // Some device info latest: number // Latest version } /** * Gets the device's location and device info */ export async function deviceGetStartupInfo({ token, forwardIps, }: { token: string forwardIps?: string }): Promise { const newStack = new Error().stack try { const { data } = await Api.get('/api/devices/startup', { headers: { Authorization: token, 'X-Forwarded-For': forwardIps || '::1', }, }) return data } catch (err) { throw handleApiError(err, newStack) } } export interface DeviceGetLocationResponse { msg: string location: Location } /** * Gets the device's location by it's ip * @deprecated */ export async function deviceGetLocation({ token, forwardIps, }: { token: string forwardIps?: string }): Promise { const newStack = new Error().stack try { const { data } = await Api.get('/api/devices/location', { headers: { Authorization: token, 'X-Forwarded-For': forwardIps || '::1', }, }) return data } catch (err) { throw handleApiError(err, newStack) } } export interface GetLatestVersionResponse { latest: number } /** * Query latest version * @deprecated */ export async function getLatestVersion({ token, forwardIps, }: { token: string forwardIps?: string | string[] }): Promise { const newStack = new Error().stack try { const { data } = await Api.get('/api/devices/downloads/', { headers: { Authorization: token, 'X-Forwarded-For': forwardIps || '::1' }, }) return data } catch (err) { throw handleApiError(err, newStack) } } /** * Download the specified version */ export async function downloadVersion({ token, version, output, forwardIps, }: { token: string version: string output: string forwardIps?: string | string[] }): Promise { const writer = createWriteStream(output) return Api.request({ method: 'get', url: `/api/devices/downloads/${version}`, responseType: 'stream', headers: { Authorization: token, 'X-Forwarded-For': forwardIps || '::1' }, // eslint-disable-next-line @typescript-eslint/ban-types }).then((res: { data: { pipe: Function } }) => { return new Promise((resolve, reject) => { res.data.pipe(writer) let error: Error = null writer.on('error', (err) => { error = err writer.close() reject(err) }) writer.on('close', () => { if (!error) { resolve(true) } }) }) }) }