diff --git a/src/services/account.js b/src/services/account.js index 984ae1fe0..180ae1d10 100644 --- a/src/services/account.js +++ b/src/services/account.js @@ -560,10 +560,6 @@ module.exports = class AccountHelper { } const result = { access_token: accessToken, refresh_token: refreshToken, user } - const templateData = await notificationTemplateQueries.findOneEmailTemplate( - process.env.REGISTRATION_EMAIL_TEMPLATE_CODE, - user.organization_id - ) if (plaintextEmailId) { notificationUtils.sendEmailNotification({ diff --git a/src/services/admin.js b/src/services/admin.js index 4b515d6a2..b1f633357 100644 --- a/src/services/admin.js +++ b/src/services/admin.js @@ -9,6 +9,7 @@ // Third-party libraries const _ = require('lodash') const { Op } = require('sequelize') +const bcryptJs = require('bcryptjs') // Constants and generics const common = require('@constants/common') @@ -232,103 +233,144 @@ module.exports = class AdminHelper { } /** - * login admin user - * @method - * @name login - * @param {Object} bodyData - user login data. - * @param {string} bodyData.email - email. - * @param {string} bodyData.password - email. - * @param {string} deviceInformation - device information. - * @returns {JSON} - returns login response + * Handles the login process for admin users. + * + * This method validates user credentials and enforces additional checks + * (such as admin role verification and active session limits) before + * generating access/refresh tokens and creating a user session. + * + * Steps performed: + * 1. Normalize and validate the identifier (email, phone, or username). + * 2. Construct the user lookup query using the DEFAULT_TENANT_CODE. + * 3. Retrieve the user with their associated organizations and roles. + * 4. Verify the user's password using bcrypt. + * 5. Ensure the user has the required admin role (`common.ADMIN_ROLE`). + * 6. Enforce allowed active session limits (if configured). + * 7. Create a new user session record. + * 8. Enrich token payload with user and organization details. + * 9. Generate access and refresh tokens. + * 10. Remove sensitive data and process user image URLs. + * 11. Update session details in Redis for active tracking. + * 12. Return tokens and user details in the success response. + * + * @async + * @param {Object} bodyData - Login request payload. + * @param {string} [bodyData.identifier] - The user's email, phone, or username. + * @param {string} [bodyData.email] - The user's email (alternative to `identifier`). + * @param {string} [bodyData.phone_code] - Phone country code (required if using phone login). + * @param {string} bodyData.password - The user's password. + * @param {Object} deviceInformation - Metadata about the device used for login. + * + * @returns {Promise} A success response containing: + * - `access_token` {string} - JWT for API access. + * - `refresh_token` {string} - JWT for refreshing sessions. + * - `user` {Object} - Sanitized user object with organization details. + * + * @throws {Error} Rethrows unexpected errors for global error handling. + * + * @example + * const loginResponse = await AuthService.login( + * { identifier: 'admin@example.com', password: 'StrongPass123!' }, + * { ip: '192.168.1.1', device: 'Chrome on Windows' } + * ); + * console.log(loginResponse.result.access_token); */ + static async login(bodyData, deviceInformation) { try { - const plaintextEmailId = bodyData.email.toLowerCase() - const encryptedEmailId = emailEncryption.encrypt(plaintextEmailId) - const userCredentials = await UserCredentialQueries.findOne({ email: encryptedEmailId }) - if (!userCredentials) { - return responses.failureResponse({ - message: 'USER_DOESNOT_EXISTS', - statusCode: httpStatusCode.bad_request, + // helper for consistent failure responses + const failure = (message, status = httpStatusCode.bad_request) => + responses.failureResponse({ + message, + statusCode: status, responseCode: 'CLIENT_ERROR', }) + + // 1) Identifier handling: accept `identifier` or fallback to `email` + const rawIdentifier = (bodyData.identifier || bodyData.email || '').toString().trim() + const identifier = rawIdentifier.toLowerCase() + if (!identifier) return failure('IDENTIFIER_REQUIRED', httpStatusCode.bad_request) + + // identifier type helpers + const isEmail = (str) => /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/.test(str) + const isPhone = (str) => /^\+?[1-9]\d{1,14}$/.test(str) + const isUsername = (str) => /^[a-zA-Z0-9_]{3,30}$/.test(str) + + // 2) Build query (skip domain checks; use DEFAULT_TENANT_CODE) + const query = { + [Op.or]: [], + password: { [Op.ne]: null }, + status: common.ACTIVE_STATUS, + tenant_code: process.env.DEFAULT_TENANT_CODE, } - let user = await userQueries.findOne({ - id: userCredentials.user_id, - // organization_id: userCredentials.organization_id, - }) - if (!user.id) { - return responses.failureResponse({ - message: 'USER_DOESNOT_EXISTS', - statusCode: httpStatusCode.bad_request, - responseCode: 'CLIENT_ERROR', + if (isEmail(identifier)) { + query[Op.or].push({ email: emailEncryption.encrypt(identifier) }) + } else if (isPhone(identifier)) { + // expects bodyData.phone_code when phone login is used + query[Op.or].push({ + phone: emailEncryption.encrypt(identifier), + phone_code: bodyData.phone_code, }) + } else { + query[Op.or].push({ username: identifier }) } - const isPasswordCorrect = utils.comparePassword(bodyData.password, user.password) + + // 3) Find user (reuse the helper that returns org associations like user/login) + const userInstance = await userQueries.findUserWithOrganization(query, {}, true) + let user = userInstance ? userInstance.toJSON() : null + + if (!user) return failure('IDENTIFIER_OR_PASSWORD_INVALID', httpStatusCode.bad_request) + + //Password verification (bcrypt async compare) + const isPasswordCorrect = await bcryptJs.compare(bodyData.password, user.password) if (!isPasswordCorrect) { - return responses.failureResponse({ - message: 'PASSWORD_INVALID', - statusCode: httpStatusCode.bad_request, - responseCode: 'CLIENT_ERROR', - }) + return failure('IDENTIFIER_OR_PASSWORD_INVALID', httpStatusCode.bad_request) } - let roles = await roleQueries.findAll( - { id: user.roles }, - { - attributes: { - exclude: ['created_at', 'updated_at', 'deleted_at'], - }, - } + // Check for admin role + const hasAdminRole = user.user_organizations?.some((org) => + org.roles?.some((r) => r.role?.title?.toLowerCase() === common.ADMIN_ROLE) ) - if (!roles) { - return responses.failureResponse({ - message: 'ROLE_NOT_FOUND', - statusCode: httpStatusCode.not_acceptable, - responseCode: 'CLIENT_ERROR', - }) + + if (!hasAdminRole) { + return failure('IDENTIFIER_OR_PASSWORD_INVALID', httpStatusCode.bad_request) + } + // 4) Active session limit enforcement (if configured) + if (process.env.ALLOWED_ACTIVE_SESSIONS != null) { + const activeSessionCount = await userSessionsService.activeUserSessionCounts(user.id) + if (activeSessionCount >= Number(process.env.ALLOWED_ACTIVE_SESSIONS)) { + return failure('ACTIVE_SESSION_LIMIT_EXCEEDED', httpStatusCode.not_acceptable) + } } - // create user session entry and add session_id to token data + // 6) Create user session const userSessionDetails = await userSessionsService.createUserSession( - user.id, // userid - '', // refresh token - '', // Access token - deviceInformation + user.id, + '', // refresh token placeholder + '', // access token placeholder + deviceInformation, + user.tenant_code ) - /** - * Based on user organisation id get user org parent Id value - * If parent org id is present then set it to tenant of user - * if not then set user organisation id to tenant - */ - - // let tenantDetails = await organizationQueries.findOne( - // { id: user.organization_id }, - // { attributes: ['parent_id'] } - // ) + // 7) Token payload enrichment - follow same shape as user/login + // Ensure organizations exist; if not, create a default org object from env - // const tenant_id = - // tenantDetails && tenantDetails.parent_id !== null ? tenantDetails.parent_id : user.organization_id + user = UserTransformDTO.transform(user) const tokenDetail = { data: { id: user.id, name: user.name, session_id: userSessionDetails.result.id, - organizations: [ - { - id: process.env.DEFAULT_ORG_ID, - roles: roles, - }, - ], - tenant_code: process.env.DEFAULT_TENANT_CODE, + organization_ids: user.organizations.map((o) => String(o.id)), + organization_codes: user.organizations.map((o) => String(o.code)), + organizations: user.organizations, + tenant_code: user.tenant_code, }, } - user.user_roles = roles - + // 8) Generate tokens (same helper as user/login) const accessToken = utils.generateToken( tokenDetail, process.env.ACCESS_TOKEN_SECRET, @@ -340,20 +382,23 @@ module.exports = class AdminHelper { common.refreshTokenExpiry ) - /** - * This function call will do below things - * 1: create redis entry for the session - * 2: update user-session with token and refresh_token - */ + // 9) Remove sensitive fields and post-process user image + delete user.password + if (user && user.image) { + user.image = await utils.getDownloadableUrl(user.image) + } + + // 10) Return original identifier and result + user.identifier = identifier + const result = { access_token: accessToken, refresh_token: refreshToken, user } + + // 11) Update session and set Redis data (same flow as user/login) await userSessionsService.updateUserSessionAndsetRedisData( userSessionDetails.result.id, accessToken, refreshToken ) - delete user.password - const result = { access_token: accessToken, refresh_token: refreshToken, user } - return responses.successResponse({ statusCode: httpStatusCode.ok, message: 'LOGGED_IN_SUCCESSFULLY', diff --git a/src/services/organization.js b/src/services/organization.js index 377dc0ecf..01aa26790 100644 --- a/src/services/organization.js +++ b/src/services/organization.js @@ -56,6 +56,8 @@ module.exports = class OrganizationsHelper { domain: domain, organization_id: createdOrganization.id, created_by: loggedInUserId, + updated_by: loggedInUserId, + tenant_code: bodyData.tenant_code, } await orgDomainQueries.create(domainCreationData) }) diff --git a/src/services/user-sessions.js b/src/services/user-sessions.js index 49d865efa..3569e6f23 100644 --- a/src/services/user-sessions.js +++ b/src/services/user-sessions.js @@ -41,7 +41,7 @@ module.exports = class UserSessionsHelper { if (accessToken !== '') { userSessionDetails.token = accessToken } - if (accessToken !== '') { + if (refreshToken !== '') { userSessionDetails.refresh_token = refreshToken }