Your IP : 216.73.217.95


Current Path : /home/deltalab/PMS/partner-manager-backend/services/
Upload File :
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,
};