const db = require("../models"); const authService = require("../services/auth/auth.service"); const sessionService = require("../services/auth/session.service"); const { hashPassword, checkPassword } = require("../utils/hashPassword.util"); const { createPasswordReset, consumePasswordResetToken, sendPasswordResetEmail } = require("../utils/passwordReset.utill"); const { createEmailVerification, consumeEmailVerificationToken, sendVerificationEmail } = require("../utils/emailVerification.util"); const { sendPasswordChanged } = require("../services/auth/email.service"); const { logActivity } = require("../services/activity.service"); const { verifyToken } = require("../utils/jwt.util"); const cookieOptions = (maxAge, path = "/") => ({ httpOnly: true, secure: process.env.NODE_ENV === "production", sameSite: process.env.NODE_ENV === "production" ? "none" : "lax", maxAge, path }); const clearCookies = (res) => { res.clearCookie("access_token", cookieOptions(undefined, "/")); res.clearCookie("refresh_token", cookieOptions(undefined, "/api")); }; const projectUser = (user) => ({ id: user.id, firstName: user.firstName, lastName: user.lastName, email: user.email, accountType: user.accountType, accountStatus: user.accountStatus, emailVerified: Boolean(user.emailVerifiedAt) }); const deliverTokens = (req, res, result, clientType = "WEB") => { const accessMs = 15 * 60 * 1000; const refreshMs = Math.max(0, new Date(result.refreshExpiresAt).getTime() - Date.now()); if (clientType === "WEB") { res.cookie("access_token", result.accessToken, cookieOptions(accessMs)); res.cookie("refresh_token", result.refreshToken, cookieOptions(refreshMs, "/api")); return { accessToken: result.accessToken }; } return { accessToken: result.accessToken, refreshToken: result.refreshToken, refreshExpiresAt: result.refreshExpiresAt }; }; exports.login = async (req, res, next) => { try { const result = await authService.beginPasswordLogin(req.validated.body, req); res.status(202).json({ success: true, data: result, message: "If the credentials are valid, a verification code has been sent" }); } catch (error) { next(error); } }; exports.loginReq = exports.login; exports.verifyOtp = async (req, res, next) => { try { const input = req.validated.body; const result = await authService.completeOtpLogin(input, req); const tokens = deliverTokens(req, res, result, input.clientType); await logActivity({ user: result.user, description: "Authentication session created", type: "LOGIN_SUCCEEDED", module: "Authentication" }); res.json({ success: true, data: { user: projectUser(result.user), ...tokens } }); } catch (error) { next(error); } }; exports.refreshToken = async (req, res, next) => { try { const input = req.validated.body; const token = input.refreshToken || req.cookies?.refresh_token; if (!token) throw Object.assign(new Error("Invalid session"), { status: 401, code: "INVALID_SESSION" }); const result = await authService.refresh(token, req); res.json({ success: true, data: deliverTokens(req, res, result, input.clientType) }); } catch (error) { clearCookies(res); if (error.code === "REFRESH_TOKEN_REUSE") console.warn(`[${req.id}] Refresh token replay detected; token family revoked`); next(Object.assign(new Error("Invalid session"), { status: 401, code: "INVALID_SESSION" })); } }; exports.logout = async (req, res, next) => { try { const token = req.body?.refreshToken || req.cookies?.refresh_token; let id = sessionService.tokenId(token); if (!id) { const accessToken = req.cookies?.access_token || (req.headers.authorization?.startsWith("Bearer ") ? req.headers.authorization.slice(7) : null); try { id = accessToken ? verifyToken(accessToken).sid : null; } catch (_error) { id = null; } } if (id) await sessionService.revokeSession(id, "LOGOUT"); clearCookies(res); res.json({ success: true, message: "Logged out successfully" }); } catch (error) { next(error); } }; exports.logoutAll = async (req, res, next) => { try { await db.sequelize.transaction(async (transaction) => { const user = await db.User.findByPk(req.user.id, { transaction, lock: transaction.LOCK.UPDATE }); user.tokenVersion += 1; await user.save({ transaction }); await sessionService.revokeAllUserSessions(user.id, "LOGOUT_ALL", transaction); }); clearCookies(res); await logActivity({ user: req.user, description: "All authentication sessions revoked", type: "LOGOUT_ALL", module: "Authentication" }); res.json({ success: true, message: "Logged out from all devices" }); } catch (error) { next(error); } }; exports.me = async (req, res, next) => { try { const user = await db.User.findByPk(req.user.id, { attributes: { exclude: ["password", "tokenVersion", "passwordChangedAt"] }, include: [{ model: db.Profile, as: "profile" }] }); res.json({ success: true, data: { ...projectUser(user), profile: user.profile, effectivePermissions: req.user.permissions || [] } }); } catch (error) { next(error); } }; exports.forgotPassword = async (req, res, next) => { const message = "If an account exists for this email, a password reset link has been sent"; try { const user = await db.User.findOne({ where: { email: req.validated.body.email } }); if (user) { const token = await createPasswordReset(user.id); await sendPasswordResetEmail(user.email, user.firstName, token); } res.json({ success: true, message }); } catch (error) { console.error(`[${req.id}] Password reset request failed`, { name: error.name, message: error.message }); res.json({ success: true, message }); } }; exports.resetPassword = async (req, res, next) => { try { const input = req.validated.body; const userId = await consumePasswordResetToken(input.token); if (!userId) throw Object.assign(new Error("Reset token is invalid or expired"), { status: 400, code: "INVALID_RESET_TOKEN" }); let changedUser; await db.sequelize.transaction(async (transaction) => { const user = await db.User.findByPk(userId, { transaction, lock: transaction.LOCK.UPDATE }); if (!user || (user.password && await checkPassword(input.newPassword, user.password))) throw Object.assign(new Error("Invalid password change"), { status: 400, code: "INVALID_PASSWORD_CHANGE" }); user.password = await hashPassword(input.newPassword); user.passwordChangedAt = new Date(); user.tokenVersion += 1; await user.save({ transaction }); await sessionService.revokeAllUserSessions(user.id, "PASSWORD_RESET", transaction); changedUser = user; }); sendPasswordChanged(changedUser).catch(() => {}); await logActivity({ user: changedUser, description: "Password reset and sessions revoked", type: "PASSWORD_RESET", module: "Authentication" }); res.json({ success: true, message: "Password reset successfully. Please login again" }); } catch (error) { next(error); } }; exports.changePassword = async (req, res, next) => { try { const input = req.validated.body; let changedUser; await db.sequelize.transaction(async (transaction) => { const user = await db.User.findByPk(req.user.id, { transaction, lock: transaction.LOCK.UPDATE }); if (!user?.password || !await checkPassword(input.currentPassword, user.password)) throw Object.assign(new Error("Current password is incorrect"), { status: 401, code: "INVALID_CREDENTIALS" }); if (await checkPassword(input.newPassword, user.password)) throw Object.assign(new Error("New password must be different"), { status: 400, code: "INVALID_PASSWORD_CHANGE" }); user.password = await hashPassword(input.newPassword); user.passwordChangedAt = new Date(); user.tokenVersion += 1; await user.save({ transaction }); await sessionService.revokeAllUserSessions(user.id, "PASSWORD_CHANGED", transaction); changedUser = user; }); clearCookies(res); sendPasswordChanged(changedUser).catch(() => {}); await logActivity({ user: changedUser, description: "Password changed and sessions revoked", type: "PASSWORD_CHANGED", module: "Authentication" }); res.json({ success: true, message: "Password changed successfully. Please login again" }); } catch (error) { next(error); } }; exports.verifyEmail = async (req, res, next) => { try { const userId = await consumeEmailVerificationToken(req.validated.body.token); if (!userId) throw Object.assign(new Error("Verification token is invalid or expired"), { status: 400, code: "INVALID_VERIFICATION_TOKEN" }); const user = await db.User.findByPk(userId); if (!user) throw Object.assign(new Error("Verification token is invalid or expired"), { status: 400, code: "INVALID_VERIFICATION_TOKEN" }); if (!user.emailVerifiedAt) { user.emailVerifiedAt = new Date(); user.accountStatus = "ACTIVE"; await user.save(); } await logActivity({ user, description: "Email address verified", type: "EMAIL_VERIFIED", module: "Authentication" }); res.json({ success: true, message: "Email verified" }); } catch (error) { next(error); } }; exports.resendVerification = async (req, res, next) => { const message = "If verification is required, a new email has been sent"; try { const user = await db.User.findOne({ where: { email: req.validated.body.email } }); if (user && !user.emailVerifiedAt && user.accountStatus === "PENDING_VERIFICATION") { const token = await createEmailVerification(user.id); await sendVerificationEmail(user.email, user.firstName, token); } res.json({ success: true, message }); } catch (error) { console.error(`[${req.id}] Verification resend failed`, { name: error.name, message: error.message }); res.json({ success: true, message }); } }; exports.oauth = (provider) => async (req, res, next) => { try { const input = req.validated.body; const result = await authService.authenticateOAuth(provider, input, req); const tokens = deliverTokens(req, res, result, input.clientType); await logActivity({ user: result.user, description: `${provider} identity authenticated`, type: `${provider.toUpperCase()}_ACCOUNT_LINKED`, module: "Authentication" }); res.json({ success: true, data: { user: projectUser(result.user), ...tokens } }); } catch (error) { next(Object.assign(error, { status: error.status || 401, code: error.code || "OAUTH_FAILED" })); } }; exports.adminLogin = async (req, res, next) => { try { const { PRIVILEGED_ACCOUNT_TYPES } = require("../constants/accountTypes"); const result = await authService.beginPasswordLogin(req.validated.body, req, PRIVILEGED_ACCOUNT_TYPES); res.status(202).json({ success: true, data: result, message: "If the credentials are valid, a verification code has been sent" }); } catch (error) { next(error); } }; exports.riderLogin = async (req, res, next) => { try { const { ACCOUNT_TYPES } = require("../constants/accountTypes"); const result = await authService.beginPasswordLogin(req.validated.body, req, [ACCOUNT_TYPES.RIDER]); res.status(202).json({ success: true, data: result, message: "If the credentials are valid, a verification code has been sent" }); } catch (error) { next(error); } };