import { Meteor } from 'meteor/meteor'; import { Random } from '@rocket.chat/random'; import { DDPCommon } from 'meteor/ddp-common'; import { DDP } from 'meteor/ddp'; import { Accounts } from 'meteor/accounts-base'; import type { Request, Response } from 'meteor/rocketchat:restivus'; import { Restivus } from 'meteor/rocketchat:restivus'; import _ from 'underscore'; import type { RateLimiterOptionsToCheck } from 'meteor/rate-limit'; import { RateLimiter } from 'meteor/rate-limit'; import type { IMethodConnection, IUser, IRoom } from '@rocket.chat/core-typings'; import type { JoinPathPattern, Method } from '@rocket.chat/rest-typings'; import { Users } from '@rocket.chat/models'; import { getRestPayload } from '../../../server/lib/logger/logPayloads'; import { settings } from '../../settings/server'; import { metrics } from '../../metrics/server'; import { getDefaultUserFields } from '../../utils/server/functions/getDefaultUserFields'; import { checkCodeForUser } from '../../2fa/server/code'; import type { PermissionsPayload } from './api.helpers'; import { checkPermissionsForInvocation, checkPermissions } from './api.helpers'; import { isObject } from '../../../lib/utils/isObject'; import type { FailureResult, InternalError, NotFoundResult, Operations, Options, PartialThis, SuccessResult, UnauthorizedResult, } from './definition'; import { parseJsonQuery } from './helpers/parseJsonQuery'; import { Logger } from '../../logger/server'; import { getUserInfo } from './helpers/getUserInfo'; import { hasPermissionAsync } from '../../authorization/server/functions/hasPermission'; import { apiDeprecationLogger } from '../../lib/server/lib/deprecationWarningLogger'; const logger = new Logger('API'); interface IAPIProperties { useDefaultAuth: boolean; prettyJson: boolean; auth: { token: string; user: () => Promise<{ userId: string; token: string }> }; defaultOptionsEndpoint?: () => Promise; version?: string; enableCors?: boolean; apiPath?: string; } interface IAPIDefaultFieldsToExclude { avatarOrigin: number; emails: number; phone: number; statusConnection: number; createdAt: number; lastLogin: number; services: number; requirePasswordChange: number; requirePasswordChangeReason: number; roles: number; statusDefault: number; _updatedAt: number; settings: number; } type RateLimiterOptions = { numRequestsAllowed?: number; intervalTimeInMS?: number; }; export const defaultRateLimiterOptions: RateLimiterOptions = { numRequestsAllowed: settings.get('API_Enable_Rate_Limiter_Limit_Calls_Default'), intervalTimeInMS: settings.get('API_Enable_Rate_Limiter_Limit_Time_Default'), }; const rateLimiterDictionary: Record< string, { rateLimiter: RateLimiter; options: RateLimiterOptions; } > = {}; const getRequestIP = (req: Request): string | null => { const socket = req.socket || req.connection?.socket; const remoteAddress = req.headers['x-real-ip'] || (typeof socket !== 'string' && (socket?.remoteAddress || req.connection?.remoteAddress || null)); let forwardedFor = req.headers['x-forwarded-for']; if (!socket) { return remoteAddress || forwardedFor || null; } const httpForwardedCount = parseInt(String(process.env.HTTP_FORWARDED_COUNT)) || 0; if (httpForwardedCount <= 0) { return remoteAddress; } if (!forwardedFor || typeof forwardedFor.valueOf() !== 'string') { return remoteAddress; } forwardedFor = forwardedFor.trim().split(/\s*,\s*/); if (httpForwardedCount > forwardedFor.length) { return remoteAddress; } return forwardedFor[forwardedFor.length - httpForwardedCount]; }; let prometheusAPIUserAgent = false; export class APIClass extends Restivus { protected apiPath?: string; public authMethods: ((...args: any[]) => any)[]; protected helperMethods: Map any> = new Map(); public fieldSeparator: string; public defaultFieldsToExclude: { joinCode: number; members: number; importIds: number; e2e: number; }; public defaultLimitedUserFieldsToExclude: IAPIDefaultFieldsToExclude; public limitedUserFieldsToExclude: IAPIDefaultFieldsToExclude; public limitedUserFieldsToExcludeIfIsPrivilegedUser: { services: number; }; constructor(properties: IAPIProperties) { super(properties); this.apiPath = properties.apiPath; this.authMethods = []; this.fieldSeparator = '.'; this.defaultFieldsToExclude = { joinCode: 0, members: 0, importIds: 0, e2e: 0, }; this.defaultLimitedUserFieldsToExclude = { avatarOrigin: 0, emails: 0, phone: 0, statusConnection: 0, createdAt: 0, lastLogin: 0, services: 0, requirePasswordChange: 0, requirePasswordChangeReason: 0, roles: 0, statusDefault: 0, _updatedAt: 0, settings: 0, }; this.limitedUserFieldsToExclude = this.defaultLimitedUserFieldsToExclude; this.limitedUserFieldsToExcludeIfIsPrivilegedUser = { services: 0, }; } public setLimitedCustomFields(customFields: string[]): void { const nonPublicFieds = customFields.reduce((acc, customField) => { acc[`customFields.${customField}`] = 0; return acc; }, {} as Record); this.limitedUserFieldsToExclude = { ...this.defaultLimitedUserFieldsToExclude, ...nonPublicFieds, }; } async parseJsonQuery(this: PartialThis) { return parseJsonQuery(this); } public addAuthMethod(func: (this: PartialThis, ...args: any[]) => any): void { this.authMethods.push(func); } protected shouldAddRateLimitToRoute(options: { rateLimiterOptions?: RateLimiterOptions | boolean }): boolean { const { version } = this._config; const { rateLimiterOptions } = options; return ( (typeof rateLimiterOptions === 'object' || rateLimiterOptions === undefined) && Boolean(version) && !process.env.TEST_MODE && Boolean(defaultRateLimiterOptions.numRequestsAllowed && defaultRateLimiterOptions.intervalTimeInMS) ); } public success(): SuccessResult; public success(result: T): SuccessResult; public success(result: T = {} as T): SuccessResult { if (isObject(result)) { (result as Record).success = true; } const finalResult = { statusCode: 200, body: result, } as SuccessResult; return finalResult as SuccessResult; } public failure(result?: T): FailureResult; public failure( result?: T, errorType?: TErrorType, stack?: TStack, error?: { details: TErrorDetails }, ): FailureResult; public failure( result?: T, errorType?: TErrorType, stack?: TStack, error?: { details: TErrorDetails }, ): FailureResult { const response: { statusCode: 400; body: any & { message?: string; errorType?: string; stack?: string; success?: boolean; details?: Record | string }; } = { statusCode: 400, body: result }; if (isObject(result)) { response.body.success = false; } else { response.body = { success: false, error: result, stack, }; if (errorType) { response.body.errorType = errorType; } if (error && typeof error === 'object' && 'details' in error && error?.details) { try { response.body.details = JSON.parse(error.details as unknown as string); } catch (e) { response.body.details = error.details; } } } return response; } public notFound(msg?: string): NotFoundResult { return { statusCode: 404, body: { success: false, error: msg || 'Resource not found', }, }; } public internalError(msg?: T): InternalError { return { statusCode: 500, body: { success: false, error: msg || 'Internal error occured', }, }; } public unauthorized(msg?: T): UnauthorizedResult { return { statusCode: 403, body: { success: false, error: msg || 'unauthorized', }, }; } public tooManyRequests(msg?: string): { statusCode: number; body: Record & { success?: boolean } } { return { statusCode: 429, body: { success: false, error: msg || 'Too many requests', }, }; } protected getRateLimiter(route: string): { rateLimiter: RateLimiter; options: RateLimiterOptions } { return rateLimiterDictionary[route]; } protected async shouldVerifyRateLimit(route: string, userId: string): Promise { return ( rateLimiterDictionary.hasOwnProperty(route) && settings.get('API_Enable_Rate_Limiter') === true && (process.env.NODE_ENV !== 'development' || settings.get('API_Enable_Rate_Limiter_Dev') === true) && !(userId && (await hasPermissionAsync(userId, 'api-bypass-rate-limit'))) ); } protected async enforceRateLimit( objectForRateLimitMatch: RateLimiterOptionsToCheck, _: any, response: Response, userId: string, ): Promise { if (!(await this.shouldVerifyRateLimit(objectForRateLimitMatch.route, userId))) { return; } rateLimiterDictionary[objectForRateLimitMatch.route].rateLimiter.increment(objectForRateLimitMatch); const attemptResult = rateLimiterDictionary[objectForRateLimitMatch.route].rateLimiter.check(objectForRateLimitMatch); const timeToResetAttempsInSeconds = Math.ceil(attemptResult.timeToReset / 1000); response.setHeader('X-RateLimit-Limit', rateLimiterDictionary[objectForRateLimitMatch.route].options.numRequestsAllowed); response.setHeader('X-RateLimit-Remaining', attemptResult.numInvocationsLeft); response.setHeader('X-RateLimit-Reset', new Date().getTime() + attemptResult.timeToReset); if (!attemptResult.allowed) { throw new Meteor.Error( 'error-too-many-requests', `Error, too many requests. Please slow down. You must wait ${timeToResetAttempsInSeconds} seconds before trying this endpoint again.`, { timeToReset: attemptResult.timeToReset, seconds: timeToResetAttempsInSeconds, }, ); } } public reloadRoutesToRefreshRateLimiter(): void { const { version } = this._config; this._routes.forEach((route) => { if (this.shouldAddRateLimitToRoute(route.options)) { this.addRateLimiterRuleForRoutes({ routes: [route.path], rateLimiterOptions: route.options.rateLimiterOptions || defaultRateLimiterOptions, endpoints: Object.keys(route.endpoints).filter((endpoint) => endpoint !== 'options'), apiVersion: version, }); } }); } protected addRateLimiterRuleForRoutes({ routes, rateLimiterOptions, endpoints, apiVersion, }: { routes: string[]; rateLimiterOptions: RateLimiterOptions | boolean; endpoints: string[]; apiVersion?: string; }): void { if (typeof rateLimiterOptions !== 'object') { throw new Meteor.Error('"rateLimiterOptions" must be an object'); } if (!rateLimiterOptions.numRequestsAllowed) { throw new Meteor.Error('You must set "numRequestsAllowed" property in rateLimiter for REST API endpoint'); } if (!rateLimiterOptions.intervalTimeInMS) { throw new Meteor.Error('You must set "intervalTimeInMS" property in rateLimiter for REST API endpoint'); } const addRateLimitRuleToEveryRoute = (routes: string[]) => { routes.forEach((route) => { rateLimiterDictionary[route] = { rateLimiter: new RateLimiter(), options: rateLimiterOptions, }; const rateLimitRule = { IPAddr: (input: any) => input, route, }; rateLimiterDictionary[route].rateLimiter.addRule( rateLimitRule, rateLimiterOptions.numRequestsAllowed as number, rateLimiterOptions.intervalTimeInMS as number, ); }); }; routes.map((route) => this.namedRoutes(route, endpoints, apiVersion)).map(addRateLimitRuleToEveryRoute); } public async processTwoFactor({ userId, request, invocation, options, connection, }: { userId: string; request: Request; invocation: { twoFactorChecked?: boolean }; options?: Options; connection: IMethodConnection; }): Promise { if (options && (!('twoFactorRequired' in options) || !options.twoFactorRequired)) { return; } const code = request.headers['x-2fa-code']; const method = request.headers['x-2fa-method']; await checkCodeForUser({ user: userId, code, method, options: options && 'twoFactorOptions' in options ? (options as Record).twoFactorOptions || {} : {}, connection, }); invocation.twoFactorChecked = true; } protected getFullRouteName(route: string, method: string, apiVersion?: string): string { let prefix = `/${this.apiPath || ''}`; if (apiVersion) { prefix += `${apiVersion}/`; } return `${prefix}${route}${method}`; } protected namedRoutes(route: string, endpoints: Record | string[], apiVersion?: string): string[] { const routeActions: string[] = Array.isArray(endpoints) ? endpoints : Object.keys(endpoints); return routeActions.map((action) => this.getFullRouteName(route, action, apiVersion)); } addRoute( subpath: TSubPathPattern, operations: Operations>, ): void; addRoute>( subpaths: TSubPathPattern[], operations: Operations, ): void; addRoute( subpath: TSubPathPattern, options: TOptions, operations: Operations, TOptions>, ): void; addRoute, TOptions extends Options>( subpaths: TSubPathPattern[], options: TOptions, operations: Operations, ): void; public addRoute< TSubPathPattern extends string, TPathPattern extends JoinPathPattern, TOptions extends Options, >(subpaths: TSubPathPattern[], options: TOptions, endpoints?: Operations): void { // Note: required if the developer didn't provide options if (endpoints === undefined) { endpoints = options as unknown as Operations; options = {} as TOptions; } const operations = endpoints; const shouldVerifyPermissions = checkPermissions(options); // Allow for more than one route using the same option and endpoints if (!Array.isArray(subpaths)) { subpaths = [subpaths]; } const { version } = this._config; if (this.shouldAddRateLimitToRoute(options)) { this.addRateLimiterRuleForRoutes({ routes: subpaths, rateLimiterOptions: options.rateLimiterOptions || defaultRateLimiterOptions, endpoints: operations as unknown as string[], apiVersion: version, }); } subpaths.forEach((route) => { // Note: This is required due to Restivus calling `addRoute` in the constructor of itself Object.keys(operations).forEach((method) => { const _options = { ...options }; if (typeof operations[method as keyof Operations] === 'function') { (operations as Record)[method as string] = { action: operations[method as keyof Operations], }; } else { const extraOptions: Record = { ...operations[method as keyof Operations] } as Record< string, any >; delete extraOptions.action; Object.assign(_options, extraOptions); } // Add a try/catch for each endpoint const originalAction = (operations[method as keyof Operations] as Record).action; // eslint-disable-next-line @typescript-eslint/no-this-alias const api = this; (operations[method as keyof Operations] as Record).action = async function _internalRouteActionHandler() { const rocketchatRestApiEnd = metrics.rocketchatRestApi.startTimer({ method, version, ...(prometheusAPIUserAgent && { user_agent: this.request.headers['user-agent'] }), entrypoint: route.startsWith('method.call') ? decodeURIComponent(this.request._parsedUrl.pathname.slice(8)) : route, }); this.requestIp = getRequestIP(this.request); const startTime = Date.now(); const log = logger.logger.child({ method: this.request.method, url: this.request.url, userId: this.request.headers['x-user-id'], userAgent: this.request.headers['user-agent'], length: this.request.headers['content-length'], host: this.request.headers.host, referer: this.request.headers.referer, remoteIP: this.requestIp, ...getRestPayload(this.request.body), }); // If the endpoint requires authentication only if anonymous read is disabled, load the user info if it was provided if (!options.authRequired && options.authOrAnonRequired) { const { 'x-user-id': userId, 'x-auth-token': userToken } = this.request.headers; if (userId && userToken) { this.user = await Users.findOne( { 'services.resume.loginTokens.hashedToken': Accounts._hashLoginToken(userToken), '_id': userId, }, { projection: getDefaultUserFields(), }, ); this.userId = this.user?._id; } if (!this.user && !settings.get('Accounts_AllowAnonymousRead')) { return { statusCode: 401, body: { status: 'error', message: 'You must be logged in to do this.', }, }; } } const objectForRateLimitMatch = { IPAddr: this.requestIp, route: `${this.request.route}${this.request.method.toLowerCase()}`, }; let result; const connection = { id: Random.id(), // eslint-disable-next-line @typescript-eslint/no-empty-function close() {}, token: this.token, httpHeaders: this.request.headers, clientAddress: this.requestIp, }; try { if (options.deprecationVersion) { apiDeprecationLogger.endpoint(this.request.route, options.deprecationVersion, this.response); } await api.enforceRateLimit(objectForRateLimitMatch, this.request, this.response, this.userId); if (_options.validateParams) { const requestMethod = this.request.method as Method; const validatorFunc = typeof _options.validateParams === 'function' ? _options.validateParams : _options.validateParams[requestMethod]; if (validatorFunc && !validatorFunc(requestMethod === 'GET' ? this.queryParams : this.bodyParams)) { throw new Meteor.Error('invalid-params', validatorFunc.errors?.map((error: any) => error.message).join('\n ')); } } if ( shouldVerifyPermissions && (!this.userId || !(await checkPermissionsForInvocation( this.userId, _options.permissionsRequired as PermissionsPayload, this.request.method, ))) ) { throw new Meteor.Error('error-unauthorized', 'User does not have the permissions required for this action', { permissions: _options.permissionsRequired, }); } const invocation = new DDPCommon.MethodInvocation({ connection, isSimulation: false, userId: this.userId, }); Accounts._accountData[connection.id] = { connection, }; Accounts._setAccountData(connection.id, 'loginToken', this.token); await api.processTwoFactor({ userId: this.userId, request: this.request, invocation: invocation as unknown as Record, options: _options, connection: connection as unknown as IMethodConnection, }); this.queryOperations = options.queryOperations; this.queryFields = options.queryFields; this.parseJsonQuery = api.parseJsonQuery.bind(this as PartialThis); result = (await DDP._CurrentInvocation.withValue(invocation as any, async () => originalAction.apply(this))) || API.v1.success(); log.http({ status: result.statusCode, responseTime: Date.now() - startTime, }); } catch (e: any) { const apiMethod: string = { 'error-too-many-requests': 'tooManyRequests', 'error-unauthorized': 'unauthorized', }[e.error as string] || 'failure'; result = (API.v1 as Record)[apiMethod]( typeof e === 'string' ? e : e.message, e.error, process.env.TEST_MODE ? e.stack : undefined, e, ); log.http({ err: e, status: result.statusCode, responseTime: Date.now() - startTime, }); } finally { delete Accounts._accountData[connection.id]; } rocketchatRestApiEnd({ status: result.statusCode, }); return result; }; // Allow the endpoints to make usage of the logger which respects the user's settings (operations[method as keyof Operations] as Record).logger = logger; }); super.addRoute(route, options, operations); }); } public updateRateLimiterDictionaryForRoute(route: string, numRequestsAllowed: number, intervalTimeInMS?: number): void { if (rateLimiterDictionary[route]) { rateLimiterDictionary[route].options.numRequestsAllowed = numRequestsAllowed ?? rateLimiterDictionary[route].options.numRequestsAllowed; rateLimiterDictionary[route].options.intervalTimeInMS = intervalTimeInMS ?? rateLimiterDictionary[route].options.intervalTimeInMS; API.v1?.reloadRoutesToRefreshRateLimiter(); } } protected _initAuth(): void { const loginCompatibility = (bodyParams: Record, request: Request): Record => { // Grab the username or email that the user is logging in with const { user, username, email, password, code: bodyCode } = bodyParams; let usernameToLDAPLogin = ''; if (password == null) { return bodyParams; } if (_.without(Object.keys(bodyParams), 'user', 'username', 'email', 'password', 'code').length > 0) { return bodyParams; } const code = bodyCode || request.headers['x-2fa-code']; const auth: Record = { password, }; if (typeof user === 'string') { auth.user = user.includes('@') ? { email: user } : { username: user }; usernameToLDAPLogin = user; } else if (username) { auth.user = { username }; usernameToLDAPLogin = username; } else if (email) { auth.user = { email }; usernameToLDAPLogin = email; } if (auth.user == null) { return bodyParams; } if (auth.password.hashed) { auth.password = { digest: auth.password as { hashed: string }, algorithm: 'sha-256', }; } const objectToLDAPLogin = { ldap: true, username: usernameToLDAPLogin, ldapPass: auth.password, ldapOptions: {}, }; if (settings.get('LDAP_Enable') && !code) { return objectToLDAPLogin; } if (code) { return { totp: { code, login: settings.get('LDAP_Enable') ? objectToLDAPLogin : auth, }, }; } return auth; }; // eslint-disable-next-line @typescript-eslint/no-this-alias const self = this; (this as APIClass<'/v1'>).addRoute<'/v1/login', { authRequired: false }>( 'login' as any, { authRequired: false }, { async post() { const request = this.request as unknown as Request; const args = loginCompatibility(this.bodyParams, request); const invocation = new DDPCommon.MethodInvocation({ connection: { // eslint-disable-next-line @typescript-eslint/no-empty-function close() {}, httpHeaders: this.request.headers, clientAddress: getRequestIP(request) || '', }, }); let auth; try { auth = await DDP._CurrentInvocation.withValue(invocation as any, async () => Meteor.callAsync('login', args)); } catch (error: any) { let e = error; if (error.reason === 'User not found') { e = { error: 'Unauthorized', reason: 'Unauthorized', }; } return { statusCode: 401, body: { status: 'error', error: e.error, details: e.details, message: e.reason || e.message, }, } as unknown as SuccessResult>; } this.user = await Users.findOne( { _id: auth.id, }, { projection: getDefaultUserFields(), }, ); this.userId = (this.user as unknown as IUser)?._id; const response = { status: 'success', data: { userId: this.userId, authToken: auth.token, me: await getUserInfo(this.user || ({} as IUser)), }, }; const extraData = self._config.onLoggedIn?.call(this); if (extraData != null) { _.extend(response.data, { extra: extraData, }); } return response as unknown as SuccessResult>; }, }, ); const logout = async function (this: Restivus): Promise<{ status: string; data: { message: string } }> { // Remove the given auth token from the user's account const authToken = this.request.headers['x-auth-token']; const hashedToken = Accounts._hashLoginToken(authToken); const tokenLocation = self._config?.auth?.token; const index = tokenLocation?.lastIndexOf('.') || 0; const tokenPath = tokenLocation?.substring(0, index) || ''; const tokenFieldName = tokenLocation?.substring(index + 1) || ''; const tokenToRemove: Record = {}; tokenToRemove[tokenFieldName] = hashedToken; const tokenRemovalQuery: Record = {}; tokenRemovalQuery[tokenPath] = tokenToRemove; await Users.updateOne( { _id: this.user._id }, { $pull: tokenRemovalQuery, }, ); const response = { status: 'success', data: { message: "You've been logged out!", }, }; // Call the logout hook with the authenticated user attached const extraData = self._config.onLoggedOut?.call(this); if (extraData != null) { _.extend(response.data, { extra: extraData, }); } return response; }; /* Add a logout endpoint to the API After the user is logged out, the onLoggedOut hook is called (see Restfully.configure() for adding hook). */ return (this as APIClass<'/v1'>).addRoute<'/v1/logout', { authRequired: true }>( 'logout' as any, { authRequired: true, }, { async get() { console.warn('Warning: Default logout via GET will be removed in Restivus v1.0. Use POST instead.'); console.warn(' See https://github.com/kahmali/meteor-restivus/issues/100'); return logout.call(this as unknown as Restivus) as any; }, async post() { return logout.call(this as unknown as Restivus) as any; }, }, ); } } const getUserAuth = function _getUserAuth(...args: any[]): { token: string; user: (this: Restivus) => Promise<{ userId: string; token: string }>; } { const invalidResults = [undefined, null, false]; return { token: 'services.resume.loginTokens.hashedToken', async user() { if (this.bodyParams?.payload) { this.bodyParams = JSON.parse(this.bodyParams.payload); } for await (const method of API.v1?.authMethods || []) { if (typeof method === 'function') { const result = await method.apply(this, args); if (!invalidResults.includes(result)) { return result; } } } let token; if (this.request.headers['x-auth-token']) { token = Accounts._hashLoginToken(this.request.headers['x-auth-token']); } this.token = token || ''; return { userId: this.request.headers['x-user-id'], token, }; }, }; }; const defaultOptionsEndpoint = async function _defaultOptionsEndpoint(this: Restivus): Promise { // check if a pre-flight request if (!this.request.headers['access-control-request-method'] && !this.request.headers.origin) { this.done(); return; } if (!settings.get('API_Enable_CORS')) { this.response.writeHead(405); this.response.write('CORS not enabled. Go to "Admin > General > REST Api" to enable it.'); this.done(); return; } const CORSOriginSetting = String(settings.get('API_CORS_Origin')); const defaultHeaders = { 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, HEAD, PATCH', 'Access-Control-Allow-Headers': 'Origin, X-Requested-With, Content-Type, Accept, X-User-Id, X-Auth-Token, x-visitor-token, Authorization', }; if (CORSOriginSetting === '*') { this.response.writeHead(200, { 'Access-Control-Allow-Origin': '*', ...defaultHeaders, }); this.done(); return; } const origins = CORSOriginSetting.trim() .split(',') .map((origin) => String(origin).trim().toLocaleLowerCase()); // if invalid origin reply without required CORS headers if (!origins.includes(this.request.headers.origin)) { this.done(); return; } this.response.writeHead(200, { 'Access-Control-Allow-Origin': this.request.headers.origin, 'Vary': 'Origin', ...defaultHeaders, }); this.done(); }; const createApi = function _createApi(options: { version?: string } = {}): APIClass { return new APIClass( Object.assign( { apiPath: 'api/', useDefaultAuth: true, prettyJson: process.env.NODE_ENV === 'development', defaultOptionsEndpoint, auth: getUserAuth(), }, options, ) as IAPIProperties, ); }; export const API: { v1: APIClass<'/v1'>; default: APIClass; getUserAuth: () => { token: string; user: (this: Restivus) => Promise<{ userId: string; token: string }> }; ApiClass: typeof APIClass; channels?: { create: { validate: (params: { user: { value: string }; name?: { key: string; value?: string }; members?: { key: string; value?: string[] }; customFields?: { key: string; value?: string }; teams?: { key: string; value?: string[] }; }) => Promise; execute: ( userId: string, params: { name?: string; members?: string[]; customFields?: Record; extraData?: Record; readOnly?: boolean; }, ) => Promise<{ channel: IRoom }>; }; }; } = { getUserAuth, ApiClass: APIClass, v1: createApi({ version: 'v1', }), default: createApi(), }; // register the API to be re-created once the CORS-setting changes. settings.watchMultiple(['API_Enable_CORS', 'API_CORS_Origin'], () => { API.v1 = createApi({ version: 'v1', }); API.default = createApi(); }); settings.watch('Accounts_CustomFields', (value) => { if (!value) { return API.v1?.setLimitedCustomFields([]); } try { const customFields = JSON.parse(value); const nonPublicCustomFields = Object.keys(customFields).filter((customFieldKey) => customFields[customFieldKey].public !== true); API.v1.setLimitedCustomFields(nonPublicCustomFields); } catch (error) { console.warn('Invalid Custom Fields', error); } }); settings.watch('API_Enable_Rate_Limiter_Limit_Time_Default', (value) => { defaultRateLimiterOptions.intervalTimeInMS = value; API.v1.reloadRoutesToRefreshRateLimiter(); }); settings.watch('API_Enable_Rate_Limiter_Limit_Calls_Default', (value) => { defaultRateLimiterOptions.numRequestsAllowed = value; API.v1.reloadRoutesToRefreshRateLimiter(); }); settings.watch('Prometheus_API_User_Agent', (value) => { prometheusAPIUserAgent = value; });