You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
702 lines
19 KiB
702 lines
19 KiB
import { Meteor } from 'meteor/meteor';
|
|
import { Random } from 'meteor/random';
|
|
import { DDPCommon } from 'meteor/ddp-common';
|
|
import { DDP } from 'meteor/ddp';
|
|
import { Accounts } from 'meteor/accounts-base';
|
|
import { Restivus } from 'meteor/nimble:restivus';
|
|
import { RateLimiter } from 'meteor/rate-limit';
|
|
import _ from 'underscore';
|
|
|
|
import { Logger } from '../../logger';
|
|
import { settings } from '../../settings';
|
|
import { metrics } from '../../metrics';
|
|
import { hasPermission, hasAllPermission } from '../../authorization';
|
|
import { getDefaultUserFields } from '../../utils/server/functions/getDefaultUserFields';
|
|
import { checkCodeForUser } from '../../2fa/server/code';
|
|
|
|
|
|
const logger = new Logger('API', {});
|
|
const rateLimiterDictionary = {};
|
|
export const defaultRateLimiterOptions = {
|
|
numRequestsAllowed: settings.get('API_Enable_Rate_Limiter_Limit_Calls_Default'),
|
|
intervalTimeInMS: settings.get('API_Enable_Rate_Limiter_Limit_Time_Default'),
|
|
};
|
|
let prometheusAPIUserAgent = false;
|
|
|
|
export let API = {};
|
|
|
|
const getRequestIP = (req) =>
|
|
req.headers['x-forwarded-for']
|
|
|| (req.connection && req.connection.remoteAddress)
|
|
|| (req.socket && req.socket.remoteAddress)
|
|
|| (req.connection && req.connection.socket && req.connection.socket.remoteAddress);
|
|
|
|
export class APIClass extends Restivus {
|
|
constructor(properties) {
|
|
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,
|
|
};
|
|
}
|
|
|
|
setLimitedCustomFields(customFields) {
|
|
const nonPublicFieds = customFields.reduce((acc, customField) => {
|
|
acc[`customFields.${ customField }`] = 0;
|
|
return acc;
|
|
}, {});
|
|
this.limitedUserFieldsToExclude = {
|
|
...this.defaultLimitedUserFieldsToExclude,
|
|
...nonPublicFieds,
|
|
};
|
|
}
|
|
|
|
hasHelperMethods() {
|
|
return API.helperMethods.size !== 0;
|
|
}
|
|
|
|
getHelperMethods() {
|
|
return API.helperMethods;
|
|
}
|
|
|
|
getHelperMethod(name) {
|
|
return API.helperMethods.get(name);
|
|
}
|
|
|
|
addAuthMethod(method) {
|
|
this.authMethods.push(method);
|
|
}
|
|
|
|
shouldAddRateLimitToRoute(options) {
|
|
const { version } = this._config;
|
|
const { rateLimiterOptions } = options;
|
|
return (typeof rateLimiterOptions === 'object' || rateLimiterOptions === undefined) && Boolean(version) && !process.env.TEST_MODE && Boolean(defaultRateLimiterOptions.numRequestsAllowed && defaultRateLimiterOptions.intervalTimeInMS);
|
|
}
|
|
|
|
success(result = {}) {
|
|
if (_.isObject(result)) {
|
|
result.success = true;
|
|
}
|
|
|
|
result = {
|
|
statusCode: 200,
|
|
body: result,
|
|
};
|
|
|
|
logger.debug('Success', result);
|
|
|
|
return result;
|
|
}
|
|
|
|
failure(result, errorType, stack, error) {
|
|
if (_.isObject(result)) {
|
|
result.success = false;
|
|
} else {
|
|
result = {
|
|
success: false,
|
|
error: result,
|
|
stack,
|
|
};
|
|
|
|
if (errorType) {
|
|
result.errorType = errorType;
|
|
}
|
|
|
|
if (error && error.details) {
|
|
try {
|
|
result.details = JSON.parse(error.details);
|
|
} catch (e) {
|
|
result.details = error.details;
|
|
}
|
|
}
|
|
}
|
|
|
|
result = {
|
|
statusCode: 400,
|
|
body: result,
|
|
};
|
|
|
|
logger.debug('Failure', result);
|
|
|
|
return result;
|
|
}
|
|
|
|
notFound(msg) {
|
|
return {
|
|
statusCode: 404,
|
|
body: {
|
|
success: false,
|
|
error: msg || 'Resource not found',
|
|
},
|
|
};
|
|
}
|
|
|
|
internalError(msg) {
|
|
return {
|
|
statusCode: 500,
|
|
body: {
|
|
success: false,
|
|
error: msg || 'Internal error occured',
|
|
},
|
|
};
|
|
}
|
|
|
|
unauthorized(msg) {
|
|
return {
|
|
statusCode: 403,
|
|
body: {
|
|
success: false,
|
|
error: msg || 'unauthorized',
|
|
},
|
|
};
|
|
}
|
|
|
|
tooManyRequests(msg) {
|
|
return {
|
|
statusCode: 429,
|
|
body: {
|
|
success: false,
|
|
error: msg || 'Too many requests',
|
|
},
|
|
};
|
|
}
|
|
|
|
getRateLimiter(route) {
|
|
return rateLimiterDictionary[route];
|
|
}
|
|
|
|
shouldVerifyRateLimit(route, userId) {
|
|
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 && hasPermission(userId, 'api-bypass-rate-limit'));
|
|
}
|
|
|
|
enforceRateLimit(objectForRateLimitMatch, request, response, userId) {
|
|
if (!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,
|
|
});
|
|
}
|
|
}
|
|
|
|
reloadRoutesToRefreshRateLimiter() {
|
|
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,
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
addRateLimiterRuleForRoutes({ routes, rateLimiterOptions, endpoints, apiVersion }) {
|
|
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) => {
|
|
routes.forEach((route) => {
|
|
rateLimiterDictionary[route] = {
|
|
rateLimiter: new RateLimiter(),
|
|
options: rateLimiterOptions,
|
|
};
|
|
const rateLimitRule = {
|
|
IPAddr: (input) => input,
|
|
route,
|
|
};
|
|
rateLimiterDictionary[route].rateLimiter.addRule(rateLimitRule, rateLimiterOptions.numRequestsAllowed, rateLimiterOptions.intervalTimeInMS);
|
|
});
|
|
};
|
|
routes
|
|
.map((route) => this.namedRoutes(route, endpoints, apiVersion))
|
|
.map(addRateLimitRuleToEveryRoute);
|
|
}
|
|
|
|
processTwoFactor({ userId, request, invocation, options, connection }) {
|
|
const code = request.headers['x-2fa-code'];
|
|
const method = request.headers['x-2fa-method'];
|
|
|
|
checkCodeForUser({ user: userId, code, method, options, connection });
|
|
|
|
invocation.twoFactorChecked = true;
|
|
}
|
|
|
|
getFullRouteName(route, method, apiVersion = null) {
|
|
let prefix = `/${ this.apiPath || '' }`;
|
|
if (apiVersion) {
|
|
prefix += `${ apiVersion }/`;
|
|
}
|
|
return `${ prefix }${ route }${ method }`;
|
|
}
|
|
|
|
namedRoutes(route, endpoints, apiVersion) {
|
|
const routeActions = Array.isArray(endpoints) ? endpoints : Object.keys(endpoints);
|
|
|
|
return routeActions.map((action) => this.getFullRouteName(route, action, apiVersion));
|
|
}
|
|
|
|
addRoute(routes, options, endpoints) {
|
|
// Note: required if the developer didn't provide options
|
|
if (typeof endpoints === 'undefined') {
|
|
endpoints = options;
|
|
options = {};
|
|
}
|
|
|
|
let shouldVerifyPermissions;
|
|
|
|
if (!_.isArray(options.permissionsRequired)) {
|
|
options.permissionsRequired = undefined;
|
|
shouldVerifyPermissions = false;
|
|
} else {
|
|
shouldVerifyPermissions = !!options.permissionsRequired.length;
|
|
}
|
|
|
|
|
|
// Allow for more than one route using the same option and endpoints
|
|
if (!_.isArray(routes)) {
|
|
routes = [routes];
|
|
}
|
|
const { version } = this._config;
|
|
if (this.shouldAddRateLimitToRoute(options)) {
|
|
this.addRateLimiterRuleForRoutes({
|
|
routes,
|
|
rateLimiterOptions: options.rateLimiterOptions || defaultRateLimiterOptions,
|
|
endpoints,
|
|
apiVersion: version,
|
|
});
|
|
}
|
|
routes.forEach((route) => {
|
|
// Note: This is required due to Restivus calling `addRoute` in the constructor of itself
|
|
Object.keys(endpoints).forEach((method) => {
|
|
if (typeof endpoints[method] === 'function') {
|
|
endpoints[method] = { action: endpoints[method] };
|
|
}
|
|
// Add a try/catch for each endpoint
|
|
const originalAction = endpoints[method].action;
|
|
const api = this;
|
|
endpoints[method].action = 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,
|
|
});
|
|
|
|
logger.debug(`${ this.request.method.toUpperCase() }: ${ this.request.url }`);
|
|
this.requestIp = getRequestIP(this.request);
|
|
const objectForRateLimitMatch = {
|
|
IPAddr: this.requestIp,
|
|
route: `${ this.request.route }${ this.request.method.toLowerCase() }`,
|
|
};
|
|
let result;
|
|
|
|
const connection = {
|
|
id: Random.id(),
|
|
close() {},
|
|
token: this.token,
|
|
httpHeaders: this.request.headers,
|
|
clientAddress: this.requestIp,
|
|
};
|
|
|
|
try {
|
|
api.enforceRateLimit(objectForRateLimitMatch, this.request, this.response, this.userId);
|
|
|
|
if (shouldVerifyPermissions && (!this.userId || !hasAllPermission(this.userId, options.permissionsRequired))) {
|
|
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);
|
|
|
|
if (options.twoFactorRequired) {
|
|
api.processTwoFactor({ userId: this.userId, request: this.request, invocation, options: options.twoFactorOptions, connection });
|
|
}
|
|
|
|
result = DDP._CurrentInvocation.withValue(invocation, () => originalAction.apply(this));
|
|
} catch (e) {
|
|
logger.debug(`${ method } ${ route } threw an error:`, e.stack);
|
|
|
|
const apiMethod = {
|
|
'error-too-many-requests': 'tooManyRequests',
|
|
'error-unauthorized': 'unauthorized',
|
|
}[e.error] || 'failure';
|
|
|
|
result = API.v1[apiMethod](typeof e === 'string' ? e : e.message, e.error, undefined, e);
|
|
} finally {
|
|
delete Accounts._accountData[connection.id];
|
|
}
|
|
|
|
result = result || API.v1.success();
|
|
|
|
rocketchatRestApiEnd({
|
|
status: result.statusCode,
|
|
});
|
|
|
|
return result;
|
|
};
|
|
|
|
if (this.hasHelperMethods()) {
|
|
for (const [name, helperMethod] of this.getHelperMethods()) {
|
|
endpoints[method][name] = helperMethod;
|
|
}
|
|
}
|
|
|
|
// Allow the endpoints to make usage of the logger which respects the user's settings
|
|
endpoints[method].logger = logger;
|
|
});
|
|
|
|
super.addRoute(route, options, endpoints);
|
|
});
|
|
}
|
|
|
|
_initAuth() {
|
|
const loginCompatibility = (bodyParams, request) => {
|
|
// Grab the username or email that the user is logging in with
|
|
const { user, username, email, password, code: bodyCode } = bodyParams;
|
|
|
|
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 = {
|
|
password,
|
|
};
|
|
|
|
if (typeof user === 'string') {
|
|
auth.user = user.includes('@') ? { email: user } : { username: user };
|
|
} else if (username) {
|
|
auth.user = { username };
|
|
} else if (email) {
|
|
auth.user = { email };
|
|
}
|
|
|
|
if (auth.user == null) {
|
|
return bodyParams;
|
|
}
|
|
|
|
if (auth.password.hashed) {
|
|
auth.password = {
|
|
digest: auth.password,
|
|
algorithm: 'sha-256',
|
|
};
|
|
}
|
|
|
|
if (code) {
|
|
return {
|
|
totp: {
|
|
code,
|
|
login: auth,
|
|
},
|
|
};
|
|
}
|
|
|
|
return auth;
|
|
};
|
|
|
|
const self = this;
|
|
|
|
this.addRoute('login', { authRequired: false }, {
|
|
post() {
|
|
const args = loginCompatibility(this.bodyParams, this.request);
|
|
const getUserInfo = self.getHelperMethod('getUserInfo');
|
|
|
|
const invocation = new DDPCommon.MethodInvocation({
|
|
connection: {
|
|
close() {},
|
|
},
|
|
});
|
|
|
|
let auth;
|
|
try {
|
|
auth = DDP._CurrentInvocation.withValue(invocation, () => Meteor.call('login', args));
|
|
} catch (error) {
|
|
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,
|
|
},
|
|
};
|
|
}
|
|
|
|
this.user = Meteor.users.findOne({
|
|
_id: auth.id,
|
|
}, {
|
|
fields: getDefaultUserFields(),
|
|
});
|
|
|
|
this.userId = this.user._id;
|
|
|
|
const response = {
|
|
status: 'success',
|
|
data: {
|
|
userId: this.userId,
|
|
authToken: auth.token,
|
|
me: getUserInfo(this.user),
|
|
},
|
|
};
|
|
|
|
const extraData = self._config.onLoggedIn && self._config.onLoggedIn.call(this);
|
|
|
|
if (extraData != null) {
|
|
_.extend(response.data, {
|
|
extra: extraData,
|
|
});
|
|
}
|
|
|
|
return response;
|
|
},
|
|
});
|
|
|
|
const logout = function() {
|
|
// 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('.');
|
|
const tokenPath = tokenLocation.substring(0, index);
|
|
const tokenFieldName = tokenLocation.substring(index + 1);
|
|
const tokenToRemove = {};
|
|
tokenToRemove[tokenFieldName] = hashedToken;
|
|
const tokenRemovalQuery = {};
|
|
tokenRemovalQuery[tokenPath] = tokenToRemove;
|
|
|
|
Meteor.users.update(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 && 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.addRoute('logout', {
|
|
authRequired: true,
|
|
}, {
|
|
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);
|
|
},
|
|
post: logout,
|
|
});
|
|
}
|
|
}
|
|
|
|
const getUserAuth = function _getUserAuth(...args) {
|
|
const invalidResults = [undefined, null, false];
|
|
return {
|
|
token: 'services.resume.loginTokens.hashedToken',
|
|
user() {
|
|
if (this.bodyParams && this.bodyParams.payload) {
|
|
this.bodyParams = JSON.parse(this.bodyParams.payload);
|
|
}
|
|
|
|
for (let i = 0; i < API.v1.authMethods.length; i++) {
|
|
const method = API.v1.authMethods[i];
|
|
|
|
if (typeof method === 'function') {
|
|
const result = 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,
|
|
};
|
|
},
|
|
};
|
|
};
|
|
|
|
API = {
|
|
helperMethods: new Map(),
|
|
getUserAuth,
|
|
ApiClass: APIClass,
|
|
};
|
|
|
|
const defaultOptionsEndpoint = function _defaultOptionsEndpoint() {
|
|
if (this.request.method === 'OPTIONS' && this.request.headers['access-control-request-method']) {
|
|
if (settings.get('API_Enable_CORS') === true) {
|
|
this.response.writeHead(200, {
|
|
'Access-Control-Allow-Origin': settings.get('API_CORS_Origin'),
|
|
'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',
|
|
});
|
|
} else {
|
|
this.response.writeHead(405);
|
|
this.response.write('CORS not enabled. Go to "Admin > General > REST Api" to enable it.');
|
|
}
|
|
} else {
|
|
this.response.writeHead(404);
|
|
}
|
|
this.done();
|
|
};
|
|
|
|
const createApi = function _createApi(_api, options = {}) {
|
|
_api = _api || new APIClass(Object.assign({
|
|
apiPath: 'api/',
|
|
useDefaultAuth: true,
|
|
prettyJson: process.env.NODE_ENV === 'development',
|
|
defaultOptionsEndpoint,
|
|
auth: getUserAuth(),
|
|
}, options));
|
|
|
|
delete _api._config.defaultHeaders['Access-Control-Allow-Origin'];
|
|
delete _api._config.defaultHeaders['Access-Control-Allow-Headers'];
|
|
delete _api._config.defaultHeaders.Vary;
|
|
|
|
if (settings.get('API_Enable_CORS')) {
|
|
const origin = settings.get('API_CORS_Origin');
|
|
|
|
if (origin) {
|
|
_api._config.defaultHeaders['Access-Control-Allow-Origin'] = origin;
|
|
|
|
if (origin !== '*') {
|
|
_api._config.defaultHeaders.Vary = 'Origin';
|
|
}
|
|
}
|
|
|
|
_api._config.defaultHeaders['Access-Control-Allow-Headers'] = 'Origin, X-Requested-With, Content-Type, Accept, X-User-Id, X-Auth-Token';
|
|
}
|
|
|
|
return _api;
|
|
};
|
|
|
|
const createApis = function _createApis() {
|
|
API.v1 = createApi(API.v1, {
|
|
version: 'v1',
|
|
});
|
|
|
|
API.default = createApi(API.default);
|
|
};
|
|
|
|
// also create the API immediately
|
|
createApis();
|
|
|
|
// register the API to be re-created once the CORS-setting changes.
|
|
settings.get(/^(API_Enable_CORS|API_CORS_Origin)$/, () => {
|
|
createApis();
|
|
});
|
|
|
|
settings.get('Accounts_CustomFields', (key, 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.get('API_Enable_Rate_Limiter_Limit_Time_Default', (key, value) => {
|
|
defaultRateLimiterOptions.intervalTimeInMS = value;
|
|
API.v1.reloadRoutesToRefreshRateLimiter();
|
|
});
|
|
|
|
settings.get('API_Enable_Rate_Limiter_Limit_Calls_Default', (key, value) => {
|
|
defaultRateLimiterOptions.numRequestsAllowed = value;
|
|
API.v1.reloadRoutesToRefreshRateLimiter();
|
|
});
|
|
|
|
settings.get('Prometheus_API_User_Agent', (key, value) => {
|
|
prometheusAPIUserAgent = value;
|
|
});
|
|
|