| Current Path : /home/deltalab/PMS/partner-manager-backend/services/ |
| Current File : //home/deltalab/PMS/partner-manager-backend/services/auth.js |
/* eslint-disable new-cap */
/* eslint-disable no-useless-escape */
/* eslint-disable no-param-reassign */
/**
* Authentication service
* Contains data structures and functions used for user authentication
*/
// GraphQL
const { schemaComposer } = require('graphql-compose');
// Authentication
require('dotenv').config();
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const { userModel } = require('../models/mongoose/user');
const { apiKeyModel } = require('../models/mongoose/api-key');
// Constants
const SECRET = process.env.JWT_SECRET;
const SUPER_ADMIN_TYPE = 'ADMIN';
const PARTNER_ADMIN_TYPE = 'PARTNER_ADMIN';
// Login token to be returned
const authTC = schemaComposer.createObjectTC({
name: 'Token',
fields: {
userId: 'String!',
token: 'String!',
partnerId: 'String',
},
});
/**
* Create a suitable tokenTC to be used as response for a valid login
*/
function checkLogin(user, password) {
// Check password
const valid = bcrypt.compareSync(password, user.password);
if (!valid) {
throw new Error('Incorrect password');
}
// Create and return the authentication data
return {
userId: user._id.toString(),
token: jwt.sign({ userId: user._id }, SECRET, { expiresIn: '24h' }),
partnerId: user.partnerId?.toString(),
};
}
/**
* Filter resolvers for authenticated users
* @param {*} resolvers
* @returns
*/
function authenticationRequired(resolvers) {
Object.keys(resolvers).forEach((k) => {
resolvers[k] = resolvers[k].wrapResolve((next) => async (rp) => {
// Check if there is a token in the headers
const token = rp.context.headers['x-access-token'];
// console.log(`checking token ${token}`);
if (!token) {
throw new Error('You must login to view this.');
}
// Verify the token is valid using our little secret
jwt.verify(token, SECRET, (err, decoded) => {
// Get out in case of errors
if (err) {
throw new Error('Invalid token.');
}
// Save decoded data for future references
rp.decoded = decoded;
// add the authentication data to the args, so it can be
// used in custom resolvers
rp.args.decodedAuth = decoded;
});
// This is executed only if verify had no errors
return next(rp);
});
});
return resolvers;
}
/**
* Fetch the user for the specified user Id
*
* @param {string} userId the user Id to search for
* @returns a promise that will result in the user, as defined in the db entity, with the specified id
*/
function getUserByIdAsync(userId) {
return userModel.findById(userId).exec();
}
/**
* Check if the user is of the specified user type
*
* @param {*} user the user entity to check
* @param {string} userType the user type to check
* @return {boolean} true if the user is of the specified user type; otherwise, false.
*/
function checkUserType(user, userType) {
return user?.userType === userType;
}
/**
* Check if the user with the specified id is of the specified type
*
* @param {string} userId the user Id to search for
* @param {string} userType the user type to check against the user type
*
* @returns a promise that will resolve into true in case the user type check is positive; otherwise: false
*/
async function isUserType(userId, userType) {
const user = await getUserByIdAsync(userId);
return checkUserType(user, userType);
}
/**
* Filters authenticated resolvers for only super admin users;
* use it in conjunction with the authentication required function
* cause it depends on its decoded token information and authentication check.
*
* @param {*} resolvers
* @returns
*/
function superAdminRequired(resolvers) {
Object.keys(resolvers).forEach((k) => {
resolvers[k] = resolvers[k].wrapResolve((next) => async (rp) => {
if (!(await isUserType(rp.decoded.userId, SUPER_ADMIN_TYPE))) {
throw new Error('SuperAdmin only api.');
}
return next(rp);
});
});
return resolvers;
}
/**
* Filters authenticated resolvers for only partner admin users;
* use it in conjunction with the authentication required function
* cause it depends on its decoded token information and authentication check.
*
* @param {*} resolvers
* @returns
*/
function partnerAdminRequired(resolvers) {
Object.keys(resolvers).forEach((k) => {
resolvers[k] = resolvers[k].wrapResolve((next) => async (rp) => {
if (!(await isUserType(rp.decoded.userId, PARTNER_ADMIN_TYPE))) {
throw new Error('Partner admin only api.');
}
return next(rp);
});
});
return resolvers;
}
/**
* Filters authenticated resolvers for only partner admin or super admin users;
* use it in conjunction with the authentication required function
* cause it depends on its decoded token information and authentication check.
*
* @param {*} resolvers
* @returns
*/
function partnerOrSuperAdminRequired(resolvers) {
Object.keys(resolvers).forEach((k) => {
resolvers[k] = resolvers[k].wrapResolve((next) => async (rp) => {
const user = await getUserByIdAsync(rp.decoded.userId);
const isSuperAdmin = checkUserType(user, SUPER_ADMIN_TYPE);
const isPartnerAdmin = checkUserType(user, PARTNER_ADMIN_TYPE);
if (!isSuperAdmin && !isPartnerAdmin) {
throw new Error('Partner or super admin only api.');
}
return next(rp);
});
});
return resolvers;
}
/**
* Filters authenticated query resolvers for only super admin users or partner admins that
* query entities that have the same partner as the user;
* with the "mysSelfAllowed" param, the resolver will allow execution of resolvers when
* the _id of the entity is equal to the id of the authenticated user;
*
* use it in conjunction with the authentication required function
* cause it depends on its decoded token information and authentication check.
*
* this wrapper works only for single records fetch, where partnerId can be checked
*
* @param {*} resolvers
* @param {boolean} mysSelfAllowed true to filter the resolver when the _id of the entities is the same as the authenticated user id
* @returns
*/
function querySuperAdminOrMyPartnerAdminRequired(resolvers, mysSelfAllowed) {
Object.keys(resolvers).forEach((k) => {
resolvers[k] = resolvers[k].wrapResolve((next) => async (rp) => {
const user = await getUserByIdAsync(rp.decoded.userId);
const isSuperAdmin = checkUserType(user, SUPER_ADMIN_TYPE);
const isPartnerAdmin = checkUserType(user, PARTNER_ADMIN_TYPE);
const fetchedRecord = await next(rp);
const superAdminUnauthorizedCondition = !isSuperAdmin;
const partnerAdminUnauthorizedCondition = !isPartnerAdmin && fetchedRecord?.partnerId !== user.partnerId;
const myselfUnauthrorizedCondition = !mysSelfAllowed && fetchedRecord?._id !== user._id;
if (superAdminUnauthorizedCondition && partnerAdminUnauthorizedCondition && myselfUnauthrorizedCondition) {
throw new Error('Forbidden.');
}
return fetchedRecord;
});
});
return resolvers;
}
/**
* Filters authenticated mutation resolvers for only super admin users or partner admins that mutate entities that
* have the same partner as the user; with the "mysSelfAllowed" param, the resolver will allow execution of resolvers when
* the _id of the entity that the mutation is writing is equal to the id of the authenticated user;
*
* use it in conjunction with the authentication required function
* cause it depends on its decoded token information and authentication check.
*
* this wrapper works only for single record mutation, where partnerId can be checked on the args or in the record property
*
* @param {*} resolvers
* @param {boolean} mysSelfAllowed true to filter the resolver when the _id of the entities is the same as the authenticated user id
* @returns
*/
function mutationSuperAdminOrMyPartnerAdminRequired(resolvers, mysSelfAllowed) {
Object.keys(resolvers).forEach((k) => {
resolvers[k] = resolvers[k].wrapResolve((next) => async (rp) => {
const user = await getUserByIdAsync(rp.decoded.userId);
const isSuperAdmin = checkUserType(user, SUPER_ADMIN_TYPE);
const isPartnerAdmin = checkUserType(user, PARTNER_ADMIN_TYPE);
const partnerId = rp.args.partnerId || rp.args.record?.partnerId;
// avoid that a regular user creates a super admin
const newUser = rp.args.record;
if (newUser && newUser.userType === SUPER_ADMIN_TYPE && !isSuperAdmin) {
throw new Error('Forbidden.');
}
const superAdminUnauthorizedCondition = !isSuperAdmin;
const partnerAdminUnauthorizedCondition = !isPartnerAdmin && partnerId !== user.partnerId;
const myselfUnauthrorizedCondition = !mysSelfAllowed && rp.args.record?._id !== user._id;
if (superAdminUnauthorizedCondition && partnerAdminUnauthorizedCondition && myselfUnauthrorizedCondition) {
throw new Error('Forbidden.');
}
return next(rp);
});
});
return resolvers;
}
/**
* Filter authenticated resolvers for a specified api key,
* and the specified scope
*
* @param {*} resolvers
* @param {string} scope
* @returns
*/
function apiKeyRequired(resolvers, scope) {
Object.keys(resolvers).forEach((k) => {
resolvers[k] = resolvers[k].wrapResolve((next) => async (rp) => {
// get the API key from the header
const apiKeyHeader = rp.context.headers['x-api-key'];
if (!apiKeyHeader || apiKeyHeader === '') {
throw new Error('Unauthorized');
}
const apiKeyPrefix = apiKeyHeader.split('.')[0];
const apiKey = apiKeyHeader.split('.')[1];
// get the key prefix and search the key with it
const dbKey = await apiKeyModel.findOne({ keyPrefix: apiKeyPrefix, enabled: true }).lean();
// check that the key is correct
const keyValid = bcrypt.compareSync(apiKey, dbKey.key);
if (!keyValid) {
throw new Error('Unauthorized');
}
// if the key is correct, that check that the scope is available
if (!dbKey.scopes.find((s) => s === scope)) {
throw new Error('Unauthorized');
}
return next(rp);
});
});
return resolvers;
}
function isPasswordValid(str) {
if (str.search(/[^a-zA-Z0-9\ \!\"\#\$\%\&\'\(\)\*\+\,\-\.\/\:\;\<\=\>\?\@\[\\\]\^\_\`\{\|\}\~]/) !== -1) {
return false;
}
if (str.length === 0) {
return false;
}
if (str.length < 12) {
return false;
}
if (str.length > 50) {
return false;
}
if (str.search(/\d/) === -1) {
return false;
}
if (str.search(/[a-z]/) === -1) {
return false;
}
if (str.search(/[A-Z]/) === -1) {
return false;
}
if (str.search(/[\ \!\"\#\$\%\&\'\(\)\*\+\,\-\.\/\:\;\<\=\>\?\@\[\\\]\^\_\`\{\|\}\~]/) === -1) {
return false;
}
return true;
}
/**
* Hash the password ("password") string for the "record" in the resolver args;
* to use when need to transform plain password to a storable one
*
* @param {*} resolvers
* @returns
*/
function userPasswordHashWrapper(resolvers) {
Object.keys(resolvers).forEach((k) => {
resolvers[k] = resolvers[k].wrapResolve((next) => async (rp) => {
// check password validity
if (!isPasswordValid(rp.args.record.password)) {
throw new Error('Invalid password');
}
rp.args.record.password = await bcrypt.hash(rp.args.record.password, await bcrypt.genSalt());
return next(rp);
});
});
return resolvers;
}
/**
* Hash the API key ("key") string for the "record" in the resolver args;
* used when transforming API keys to storable ones
*
* @param {*} resolvers
* @returns
*/
function apiKeyHashWrapper(resolvers) {
Object.keys(resolvers).forEach((k) => {
resolvers[k] = resolvers[k].wrapResolve((next) => async (rp) => {
rp.args.record.key = await bcrypt.hash(rp.args.record.key, await bcrypt.genSalt());
return next(rp);
});
});
return resolvers;
}
/**
* Place in the "password" property of the "record" property of the resolvers args the password hash already
* set on the user. This can be used to edit users without changing the password,
* that's mandatory on the schema (and graphql do not allow for standard resolvers partial updates);
* The user id is searched as the parameter id directly in the args and then, if not present,
* in the _id property of the processing record
*
* @param {*} resolvers
* @returns
*/
function userSetActualPasswordHash(resolvers) {
Object.keys(resolvers).forEach((k) => {
resolvers[k] = resolvers[k].wrapResolve((next) => async (rp) => {
const user = await userModel.findById(rp.args._id ?? rp.args.record._id).exec();
rp.args.record.password = user.password;
return next(rp);
});
});
return resolvers;
}
/**
* Place in the "key" property of the "record" property of the resolvers args the API key hash already
* set on the api key entity. This can be used to edit API keys without changing the key,
* that's mandatory on the schema (and graphql do not allow for standard resolvers partial updates);
* The API key id is searched as the parameter id directly in the args and then, if not present,
* in the _id property of the processing record
*
* @param {*} resolvers
* @returns
*/
function apiKeySetActualKeyHash(resolvers) {
Object.keys(resolvers).forEach((k) => {
resolvers[k] = resolvers[k].wrapResolve((next) => async (rp) => {
const apiKey = await apiKeyModel.findById(rp.args._id ?? rp.args.record._id).exec();
rp.args.record.key = apiKey.key;
return next(rp);
});
});
return resolvers;
}
/**
* Create or update the default super admin user (username: deltalab);
* In case of update, the user password and the partner ID is not rewritten.
*/
async function manageDefaultAccessesAsync() {
const adminUsername = process.env.ADMIN_USERNAME;
// generate or update default user
const defaultUser = await userModel.findOne({ username: adminUsername });
const defaultUserData = {
username: adminUsername,
email: process.env.ADMIN_EMAIL,
password: await bcrypt.hash(process.env.ADMIN_PASSWORD, await bcrypt.genSalt()),
active: true,
userType: 'ADMIN',
userModuleAccessibleFeatures: [],
};
if (!defaultUser) {
const userToAdd = new userModel(defaultUserData);
await userToAdd.save();
} else {
// do not update the password and the partnerID, use the already setted one
defaultUserData.password = defaultUser.password;
defaultUserData.partnerId = defaultUser.partnerId;
await userModel.updateOne({ username: adminUsername }, defaultUserData);
}
const apiKeys = (process.env.PMS_REGISTRATION_API_KEY).split('.');
const defaultApiKeyData = {
keyPrefix: apiKeys[0],
key: await bcrypt.hash(apiKeys[1], await bcrypt.genSalt()),
name: 'Module registration',
enabled: true,
scopes: ['MODULE_REGISTRATION'],
};
// generate or update default api key
const defaultApiKey = await apiKeyModel.findOne({ keyPrefix: defaultApiKeyData.keyPrefix });
if (!defaultApiKey) {
const apiKeyToAdd = new apiKeyModel(defaultApiKeyData);
await apiKeyToAdd.save();
} else {
defaultApiKeyData.key = defaultApiKey.key;
await apiKeyModel.updateOne({ keyPrefix: defaultApiKeyData.key }, defaultApiKeyData);
}
}
module.exports = {
authenticationRequired,
superAdminRequired,
partnerAdminRequired,
partnerOrSuperAdminRequired,
querySuperAdminOrMyPartnerAdminRequired,
mutationSuperAdminOrMyPartnerAdminRequired,
authTC,
checkLogin,
userPasswordHashWrapper,
userSetActualPasswordHash,
apiKeyHashWrapper,
apiKeySetActualKeyHash,
apiKeyRequired,
manageDefaultAccessesAsync,
};