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.
430 lines
9.9 KiB
430 lines
9.9 KiB
/* global Restivus, DDP, DDPCommon */
|
|
import _ from 'underscore';
|
|
const logger = new Logger('API', {});
|
|
|
|
class API extends Restivus {
|
|
constructor(properties) {
|
|
super(properties);
|
|
this.authMethods = [];
|
|
this.fieldSeparator = '.';
|
|
this.defaultFieldsToExclude = {
|
|
joinCode: 0,
|
|
$loki: 0,
|
|
meta: 0,
|
|
members: 0,
|
|
usernames: 0, // Please use the `channel/dm/group.members` endpoint. This is disabled for performance reasons
|
|
importIds: 0
|
|
};
|
|
this.limitedUserFieldsToExclude = {
|
|
avatarOrigin: 0,
|
|
emails: 0,
|
|
phone: 0,
|
|
statusConnection: 0,
|
|
createdAt: 0,
|
|
lastLogin: 0,
|
|
services: 0,
|
|
requirePasswordChange: 0,
|
|
requirePasswordChangeReason: 0,
|
|
roles: 0,
|
|
statusDefault: 0,
|
|
_updatedAt: 0,
|
|
customFields: 0,
|
|
settings: 0
|
|
};
|
|
|
|
this._config.defaultOptionsEndpoint = function _defaultOptionsEndpoint() {
|
|
if (this.request.method === 'OPTIONS' && this.request.headers['access-control-request-method']) {
|
|
if (RocketChat.settings.get('API_Enable_CORS') === true) {
|
|
this.response.writeHead(200, {
|
|
'Access-Control-Allow-Origin': RocketChat.settings.get('API_CORS_Origin'),
|
|
'Access-Control-Allow-Headers': 'Origin, X-Requested-With, Content-Type, Accept, X-User-Id, X-Auth-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();
|
|
};
|
|
}
|
|
|
|
hasHelperMethods() {
|
|
return RocketChat.API.helperMethods.size !== 0;
|
|
}
|
|
|
|
getHelperMethods() {
|
|
return RocketChat.API.helperMethods;
|
|
}
|
|
|
|
getHelperMethod(name) {
|
|
return RocketChat.API.helperMethods.get(name);
|
|
}
|
|
|
|
addAuthMethod(method) {
|
|
this.authMethods.push(method);
|
|
}
|
|
|
|
success(result = {}) {
|
|
if (_.isObject(result)) {
|
|
result.success = true;
|
|
}
|
|
|
|
result = {
|
|
statusCode: 200,
|
|
body: result
|
|
};
|
|
|
|
logger.debug('Success', result);
|
|
|
|
return result;
|
|
}
|
|
|
|
failure(result, errorType) {
|
|
if (_.isObject(result)) {
|
|
result.success = false;
|
|
} else {
|
|
result = {
|
|
success: false,
|
|
error: result
|
|
};
|
|
|
|
if (errorType) {
|
|
result.errorType = errorType;
|
|
}
|
|
}
|
|
|
|
result = {
|
|
statusCode: 400,
|
|
body: result
|
|
};
|
|
|
|
logger.debug('Failure', result);
|
|
|
|
return result;
|
|
}
|
|
|
|
notFound(msg) {
|
|
return {
|
|
statusCode: 404,
|
|
body: {
|
|
success: false,
|
|
error: msg ? msg : 'Resource not found'
|
|
}
|
|
};
|
|
}
|
|
|
|
unauthorized(msg) {
|
|
return {
|
|
statusCode: 403,
|
|
body: {
|
|
success: false,
|
|
error: msg ? msg : 'unauthorized'
|
|
}
|
|
};
|
|
}
|
|
|
|
addRoute(routes, options, endpoints) {
|
|
//Note: required if the developer didn't provide options
|
|
if (typeof endpoints === 'undefined') {
|
|
endpoints = options;
|
|
options = {};
|
|
}
|
|
|
|
//Allow for more than one route using the same option and endpoints
|
|
if (!_.isArray(routes)) {
|
|
routes = [routes];
|
|
}
|
|
|
|
const version = this._config.version;
|
|
|
|
routes.forEach((route) => {
|
|
//Note: This is required due to Restivus calling `addRoute` in the constructor of itself
|
|
if (this.hasHelperMethods()) {
|
|
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;
|
|
endpoints[method].action = function _internalRouteActionHandler() {
|
|
const rocketchatRestApiEnd = RocketChat.metrics.rocketchatRestApi.startTimer({
|
|
method,
|
|
version,
|
|
user_agent: this.request.headers['user-agent'],
|
|
entrypoint: route
|
|
});
|
|
|
|
logger.debug(`${ this.request.method.toUpperCase() }: ${ this.request.url }`);
|
|
let result;
|
|
try {
|
|
result = originalAction.apply(this);
|
|
} catch (e) {
|
|
logger.debug(`${ method } ${ route } threw an error:`, e.stack);
|
|
result = RocketChat.API.v1.failure(e.message, e.error);
|
|
}
|
|
|
|
result = result || RocketChat.API.v1.success();
|
|
|
|
rocketchatRestApiEnd({
|
|
status: result.statusCode
|
|
});
|
|
|
|
return result;
|
|
};
|
|
|
|
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) => {
|
|
// Grab the username or email that the user is logging in with
|
|
const {user, username, email, password, code} = bodyParams;
|
|
|
|
if (password == null) {
|
|
return bodyParams;
|
|
}
|
|
|
|
if (_.without(Object.keys(bodyParams), 'user', 'username', 'email', 'password', 'code').length > 0) {
|
|
return bodyParams;
|
|
}
|
|
|
|
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);
|
|
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,
|
|
message: e.reason || e.message
|
|
}
|
|
};
|
|
}
|
|
|
|
this.user = Meteor.users.findOne({
|
|
_id: auth.id
|
|
});
|
|
|
|
this.userId = this.user._id;
|
|
|
|
// Remove tokenExpires to keep the old behavior
|
|
Meteor.users.update({
|
|
_id: this.user._id,
|
|
'services.resume.loginTokens.hashedToken': Accounts._hashLoginToken(auth.token)
|
|
}, {
|
|
$unset: {
|
|
'services.resume.loginTokens.$.when': 1
|
|
}
|
|
});
|
|
|
|
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() {
|
|
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 < RocketChat.API.v1.authMethods.length; i++) {
|
|
const method = RocketChat.API.v1.authMethods[i];
|
|
|
|
if (typeof method === 'function') {
|
|
const result = method.apply(this, arguments);
|
|
if (!invalidResults.includes(result)) {
|
|
return result;
|
|
}
|
|
}
|
|
}
|
|
|
|
let token;
|
|
if (this.request.headers['x-auth-token']) {
|
|
token = Accounts._hashLoginToken(this.request.headers['x-auth-token']);
|
|
}
|
|
|
|
return {
|
|
userId: this.request.headers['x-user-id'],
|
|
token
|
|
};
|
|
}
|
|
};
|
|
};
|
|
|
|
RocketChat.API = {
|
|
helperMethods: new Map(),
|
|
getUserAuth,
|
|
ApiClass: API
|
|
};
|
|
|
|
const createApi = function _createApi(enableCors) {
|
|
if (!RocketChat.API.v1 || RocketChat.API.v1._config.enableCors !== enableCors) {
|
|
RocketChat.API.v1 = new API({
|
|
version: 'v1',
|
|
useDefaultAuth: true,
|
|
prettyJson: process.env.NODE_ENV === 'development',
|
|
enableCors,
|
|
auth: getUserAuth()
|
|
});
|
|
}
|
|
|
|
if (!RocketChat.API.default || RocketChat.API.default._config.enableCors !== enableCors) {
|
|
RocketChat.API.default = new API({
|
|
useDefaultAuth: true,
|
|
prettyJson: process.env.NODE_ENV === 'development',
|
|
enableCors,
|
|
auth: getUserAuth()
|
|
});
|
|
}
|
|
};
|
|
|
|
// register the API to be re-created once the CORS-setting changes.
|
|
RocketChat.settings.get('API_Enable_CORS', (key, value) => {
|
|
createApi(value);
|
|
});
|
|
|
|
// also create the API immediately
|
|
createApi(!!RocketChat.settings.get('API_Enable_CORS'));
|
|
|