| const bcrypt = require("bcryptjs"); |
| const jwt = require("jsonwebtoken"); |
| const { AppError } = require("../utils/appError"); |
|
|
| const ACTIVE_USER_STATUS = "ACTIVE"; |
| const ACCESS_TOKEN_TYPE = "access"; |
| const REFRESH_TOKEN_TYPE = "refresh"; |
|
|
| function normalizeRoleCodes(rawRoles) { |
| if (!Array.isArray(rawRoles)) { |
| return []; |
| } |
|
|
| const unique = new Set(); |
| for (const role of rawRoles) { |
| if (typeof role !== "string") { |
| continue; |
| } |
|
|
| const normalized = role.trim().toLowerCase(); |
| if (normalized) { |
| unique.add(normalized); |
| } |
| } |
|
|
| return Array.from(unique.values()); |
| } |
|
|
| function toPublicUser(userRecord) { |
| return { |
| id: userRecord.id, |
| email: userRecord.email, |
| fullName: userRecord.full_name, |
| status: userRecord.status, |
| isActive: userRecord.is_active, |
| lastLoginAt: userRecord.last_login_at, |
| tenant: { |
| id: userRecord.tenant_id, |
| code: userRecord.tenant_code, |
| name: userRecord.tenant_name, |
| isActive: userRecord.tenant_is_active, |
| }, |
| roles: normalizeRoleCodes(userRecord.roles), |
| }; |
| } |
|
|
| function assertAccountIsActive(userRecord) { |
| if (!userRecord) { |
| throw new AppError("Invalid email or password.", 401); |
| } |
|
|
| if (userRecord.deleted_at) { |
| throw new AppError("User account is disabled.", 403, { |
| code: "USER_DELETED", |
| }); |
| } |
|
|
| if (!userRecord.is_active || String(userRecord.status || "").toUpperCase() !== ACTIVE_USER_STATUS) { |
| throw new AppError("User account is disabled.", 403, { |
| code: "USER_DISABLED", |
| }); |
| } |
|
|
| if (!userRecord.tenant_is_active) { |
| throw new AppError("Tenant is inactive.", 403, { |
| code: "TENANT_INACTIVE", |
| }); |
| } |
|
|
| const roles = normalizeRoleCodes(userRecord.roles); |
| if (roles.length === 0) { |
| throw new AppError("User account has no assigned roles.", 403, { |
| code: "ROLE_MISSING", |
| }); |
| } |
| } |
|
|
| function createAuthService({ config, pool, authRepository }) { |
| const authConfig = config.auth; |
|
|
| function signToken(userRecord, tokenType) { |
| const secret = |
| tokenType === ACCESS_TOKEN_TYPE |
| ? authConfig.jwtAccessSecret |
| : authConfig.jwtRefreshSecret; |
|
|
| const expiresIn = |
| tokenType === ACCESS_TOKEN_TYPE |
| ? authConfig.jwtAccessExpiresIn |
| : authConfig.jwtRefreshExpiresIn; |
|
|
| const roles = normalizeRoleCodes(userRecord.roles); |
|
|
| return jwt.sign( |
| { |
| tokenType, |
| tenantId: userRecord.tenant_id, |
| tenantCode: userRecord.tenant_code, |
| roles, |
| }, |
| secret, |
| { |
| subject: String(userRecord.id), |
| expiresIn, |
| issuer: authConfig.jwtIssuer, |
| audience: authConfig.jwtAudience, |
| } |
| ); |
| } |
|
|
| function issueTokenPair(userRecord) { |
| return { |
| tokenType: "Bearer", |
| accessToken: signToken(userRecord, ACCESS_TOKEN_TYPE), |
| refreshToken: signToken(userRecord, REFRESH_TOKEN_TYPE), |
| accessTokenExpiresIn: authConfig.jwtAccessExpiresIn, |
| refreshTokenExpiresIn: authConfig.jwtRefreshExpiresIn, |
| }; |
| } |
|
|
| function verifyToken(token, tokenType) { |
| const secret = |
| tokenType === ACCESS_TOKEN_TYPE |
| ? authConfig.jwtAccessSecret |
| : authConfig.jwtRefreshSecret; |
|
|
| let decoded; |
| try { |
| decoded = jwt.verify(token, secret, { |
| issuer: authConfig.jwtIssuer, |
| audience: authConfig.jwtAudience, |
| }); |
| } catch (error) { |
| throw new AppError("Invalid or expired token.", 401, { |
| code: "TOKEN_INVALID", |
| }); |
| } |
|
|
| if (typeof decoded !== "object" || decoded === null) { |
| throw new AppError("Invalid token payload.", 401, { |
| code: "TOKEN_PAYLOAD_INVALID", |
| }); |
| } |
|
|
| if (decoded.tokenType !== tokenType) { |
| throw new AppError("Invalid token type.", 401, { |
| code: "TOKEN_TYPE_INVALID", |
| }); |
| } |
|
|
| return decoded; |
| } |
|
|
| async function getAuthenticatedUserById(userId) { |
| const userRecord = await authRepository.getUserAuthById(pool, userId); |
| if (!userRecord) { |
| throw new AppError("User not found.", 401, { |
| code: "USER_NOT_FOUND", |
| }); |
| } |
|
|
| assertAccountIsActive(userRecord); |
| const user = toPublicUser(userRecord); |
|
|
| return { |
| ...user, |
| isSuperAdmin: user.roles.includes("super_admin"), |
| }; |
| } |
|
|
| async function login(input) { |
| const email = String(input?.email || "").trim(); |
| const password = String(input?.password || ""); |
|
|
| if (!email || !password) { |
| throw new AppError("Email and password are required.", 400); |
| } |
|
|
| const userRecord = await authRepository.findUserAuthByEmail(pool, email); |
| if (!userRecord) { |
| throw new AppError("Invalid email or password.", 401); |
| } |
|
|
| const passwordMatches = await bcrypt.compare(password, userRecord.password_hash || ""); |
| if (!passwordMatches) { |
| throw new AppError("Invalid email or password.", 401); |
| } |
|
|
| assertAccountIsActive(userRecord); |
|
|
| await authRepository.touchLastLoginAt(pool, userRecord.id); |
| const refreshedUser = await authRepository.getUserAuthById(pool, userRecord.id); |
| assertAccountIsActive(refreshedUser); |
|
|
| return { |
| user: toPublicUser(refreshedUser), |
| tokens: issueTokenPair(refreshedUser), |
| }; |
| } |
|
|
| async function refresh(refreshToken) { |
| if (!refreshToken || typeof refreshToken !== "string") { |
| throw new AppError("refreshToken is required.", 400); |
| } |
|
|
| const decoded = verifyToken(refreshToken, REFRESH_TOKEN_TYPE); |
| const userId = String(decoded.sub || "").trim(); |
| if (!userId) { |
| throw new AppError("Invalid token subject.", 401); |
| } |
|
|
| const userRecord = await authRepository.getUserAuthById(pool, userId); |
| assertAccountIsActive(userRecord); |
|
|
| return { |
| user: toPublicUser(userRecord), |
| tokens: issueTokenPair(userRecord), |
| }; |
| } |
|
|
| function verifyAccessToken(accessToken) { |
| return verifyToken(accessToken, ACCESS_TOKEN_TYPE); |
| } |
|
|
| return { |
| login, |
| refresh, |
| verifyAccessToken, |
| getAuthenticatedUserById, |
| }; |
| } |
|
|
| module.exports = { |
| createAuthService, |
| }; |
|
|