feat: implement identity and security features
- Added account types and privileged account types constants. - Created admin user controller for updating user security fields. - Developed role assignment controller for managing user roles. - Implemented validation middleware for request schemas. - Defined user role and auth session models for database interactions. - Created services for authentication, email notifications, and OTP handling. - Developed OAuth service for Google and Apple authentication. - Added JWT utility functions for token generation and verification. - Implemented comprehensive tests for authentication, session management, and password policies. - Created migration for updating user schema and adding new tables for auth sessions and user identities.
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
const db = require("../models");
|
||||
const { ACCOUNT_TYPE_VALUES } = require("../constants/accountTypes");
|
||||
const { revokeAllUserSessions } = require("../services/auth/session.service");
|
||||
const { logActivity } = require("../services/activity.service");
|
||||
|
||||
const updateSecurityField = (field) => async (req, res, next) => {
|
||||
try {
|
||||
const value = req.body[field];
|
||||
if (field === "accountType" && !ACCOUNT_TYPE_VALUES.includes(value)) return res.status(400).json({ success: false, error: { code: "INVALID_ACCOUNT_TYPE", message: "Invalid account type" } });
|
||||
if (field === "accountStatus" && !["PENDING_VERIFICATION", "ACTIVE", "SUSPENDED", "DEACTIVATED"].includes(value)) return res.status(400).json({ success: false, error: { code: "INVALID_ACCOUNT_STATUS", message: "Invalid account status" } });
|
||||
if (req.params.id === req.user.id) return res.status(400).json({ success: false, error: { code: "SELF_SECURITY_CHANGE_DENIED", message: "Security-sensitive self changes are not allowed" } });
|
||||
let target; let previous;
|
||||
await db.sequelize.transaction(async (transaction) => {
|
||||
target = await db.User.findByPk(req.params.id, { transaction, lock: transaction.LOCK.UPDATE });
|
||||
if (!target) throw Object.assign(new Error("User not found"), { status: 404, code: "USER_NOT_FOUND" });
|
||||
previous = target[field]; target[field] = value; target.tokenVersion += 1; await target.save({ transaction });
|
||||
await revokeAllUserSessions(target.id, `ADMIN_${field.toUpperCase()}_CHANGE`, transaction);
|
||||
});
|
||||
await logActivity({ user: req.user, description: `${field} changed for ${target.id} from ${previous} to ${value}; reason: ${req.body.reason || "not supplied"}; request: ${req.id}`, type: field === "accountStatus" ? "ACCOUNT_STATUS_CHANGED" : "ACCOUNT_TYPE_CHANGED", module: "Identity Administration" });
|
||||
res.json({ success: true, data: { id: target.id, [field]: target[field] } });
|
||||
} catch (error) { next(error); }
|
||||
};
|
||||
exports.updateStatus = updateSecurityField("accountStatus");
|
||||
exports.updateAccountType = updateSecurityField("accountType");
|
||||
+159
-503
@@ -1,536 +1,192 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Niolla
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is proprietary and confidential.
|
||||
* Unauthorized copying, modification, distribution, or use
|
||||
* of this file, via any medium, is strictly prohibited.
|
||||
*/
|
||||
|
||||
// app/controllers/auth.controller.js
|
||||
|
||||
const { checkPassword, hashPassword } = require("../utils/hashPassword.util");
|
||||
const { sendMail } = require("../utils/mail.util");
|
||||
const {
|
||||
validatePassword,
|
||||
} = require("../utils/validation/validatePassword.util");
|
||||
const { validateEmail } = require("../utils/validation/validateEmail.util");
|
||||
const { generateOTP, validateOTP } = require("../utils/otp.util");
|
||||
const { getCachedUser, clearUserCache } = require("../utils/cache.util");
|
||||
const { generateToken } = require("../utils/jwt.util");
|
||||
const {
|
||||
createRefreshSession,
|
||||
validateRefreshSession,
|
||||
deleteRefreshSession,
|
||||
deleteAllUserSessions,
|
||||
} = require("../utils/refreshSession.util");
|
||||
const {
|
||||
createPasswordReset,
|
||||
verifyPasswordResetToken,
|
||||
deletePasswordReset,
|
||||
sendPasswordResetEmail,
|
||||
sendPasswordChangedEmail,
|
||||
} = require("../utils/passwordReset.utill");
|
||||
const db = require("../models");
|
||||
const { log } = require("../utils/consoleLog.utill");
|
||||
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 appName = process.env.APP_NAME || "Niolla";
|
||||
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 };
|
||||
};
|
||||
|
||||
const User = db.User;
|
||||
|
||||
// Login Step 1: Request OTP
|
||||
exports.loginReq = async (req, res) => {
|
||||
exports.login = async (req, res, next) => {
|
||||
try {
|
||||
const { email, password } = req.body;
|
||||
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;
|
||||
|
||||
const user = await getCachedUser(email);
|
||||
if (!user) {
|
||||
return res
|
||||
.status(404)
|
||||
.send({ success: false, message: "User Not Found" });
|
||||
}
|
||||
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); }
|
||||
};
|
||||
|
||||
const passwordIsValid = await checkPassword(password, user.password);
|
||||
if (!passwordIsValid) {
|
||||
return res
|
||||
.status(401)
|
||||
.send({ success: false, message: "Invalid Password" });
|
||||
}
|
||||
|
||||
const otp = generateOTP(email);
|
||||
|
||||
await sendMail({
|
||||
to: email,
|
||||
subject: `OTP for Your ${appName} Account`,
|
||||
templateName: "otp",
|
||||
templateVars: {
|
||||
firstName: user.firstName,
|
||||
otp: otp,
|
||||
},
|
||||
text: `Hello ${user.firstName}, your otp is ${otp}`,
|
||||
});
|
||||
|
||||
log(`OTP for ${email}: ${otp}`);
|
||||
log(`OTP sent to ${email} successfully.`);
|
||||
|
||||
res.status(201).send({ success: true, message: "OTP Sent Successfully" });
|
||||
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) {
|
||||
log("Error occurred while sending OTP:", error);
|
||||
res.status(500).send({ success: false, message: error.message });
|
||||
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" }));
|
||||
}
|
||||
};
|
||||
|
||||
// Login Step 2: Verify OTP and issue JWT
|
||||
exports.login = async (req, res) => {
|
||||
exports.logout = async (req, res, next) => {
|
||||
try {
|
||||
const { email, otp } = req.body;
|
||||
|
||||
if (!email || !otp) {
|
||||
return res
|
||||
.status(400)
|
||||
.send({ success: false, message: "Email and OTP are required" });
|
||||
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); }
|
||||
};
|
||||
|
||||
const user = await getCachedUser(email);
|
||||
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); }
|
||||
};
|
||||
|
||||
if (!user) {
|
||||
return res
|
||||
.status(404)
|
||||
.send({ success: false, message: "User Not Found" });
|
||||
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);
|
||||
}
|
||||
|
||||
if (user.accountStatus !== "ACTIVE") {
|
||||
return res.status(403).send({
|
||||
success: false,
|
||||
message: "Account is not active",
|
||||
});
|
||||
}
|
||||
|
||||
const isValidOTP = validateOTP(email, String(otp));
|
||||
|
||||
if (!isValidOTP) {
|
||||
return res
|
||||
.status(401)
|
||||
.send({ success: false, message: "Invalid or Expired OTP" });
|
||||
}
|
||||
|
||||
// Generate JWT token
|
||||
const token = generateToken({
|
||||
id: user.id,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
email: user.email,
|
||||
accountType: user.accountType,
|
||||
});
|
||||
|
||||
const { refreshToken } = createRefreshSession(user.id);
|
||||
|
||||
// 3. Set JWT as HttpOnly cookie
|
||||
res.cookie("access_token", token, {
|
||||
httpOnly: true, // JS cannot access
|
||||
secure: process.env.NODE_ENV === "production", // HTTPS only in prod
|
||||
sameSite: process.env.NODE_ENV === "production" ? "None" : "Lax",
|
||||
maxAge: 15 * 60 * 1000, // 1 day
|
||||
});
|
||||
|
||||
res.cookie("refresh_token", refreshToken, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
sameSite: process.env.NODE_ENV === "production" ? "None" : "Lax",
|
||||
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
|
||||
});
|
||||
|
||||
log(`JWT issued for ${email}`);
|
||||
clearUserCache(email);
|
||||
|
||||
res.status(200).send({
|
||||
success: true,
|
||||
message: "Login Successful",
|
||||
data: {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
role: user.role,
|
||||
accountType: user.accountType,
|
||||
accessToken: token,
|
||||
},
|
||||
});
|
||||
res.json({ success: true, message });
|
||||
} catch (error) {
|
||||
log("Error occurred during login:", error);
|
||||
res.status(500).send({ success: false, message: error.message });
|
||||
console.error(`[${req.id}] Password reset request failed`, { name: error.name, message: error.message });
|
||||
res.json({ success: true, message });
|
||||
}
|
||||
};
|
||||
|
||||
exports.refreshToken = async (req, res) => {
|
||||
exports.resetPassword = async (req, res, next) => {
|
||||
try {
|
||||
const refreshToken = req.cookies?.refresh_token;
|
||||
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); }
|
||||
};
|
||||
|
||||
if (!refreshToken) {
|
||||
return res.status(401).send({
|
||||
success: false,
|
||||
message: "Refresh token is required",
|
||||
});
|
||||
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);
|
||||
}
|
||||
|
||||
const session = validateRefreshSession(refreshToken);
|
||||
|
||||
if (!session) {
|
||||
res.clearCookie("refresh_token");
|
||||
|
||||
return res.status(401).send({
|
||||
success: false,
|
||||
message: "Invalid or expired session. Please login again.",
|
||||
});
|
||||
}
|
||||
|
||||
const user = await User.findByPk(session.userId);
|
||||
|
||||
if (!user || user.accountStatus !== "ACTIVE") {
|
||||
deleteRefreshSession(session.sessionId);
|
||||
|
||||
return res.status(401).send({
|
||||
success: false,
|
||||
message: "Session is no longer valid",
|
||||
});
|
||||
}
|
||||
|
||||
// Generate new Access Token
|
||||
const token = generateToken({
|
||||
id: user.id,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
email: user.email,
|
||||
accountType: user.accountType,
|
||||
});
|
||||
|
||||
// Generate new Refresh Token
|
||||
const { refreshToken: newRefreshToken } = createRefreshSession(user.id);
|
||||
|
||||
deleteRefreshSession(session.sessionId);
|
||||
|
||||
// Replace access cookie
|
||||
res.cookie("access_token", token, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
sameSite: process.env.NODE_ENV === "production" ? "None" : "Lax",
|
||||
maxAge: 15 * 60 * 1000,
|
||||
});
|
||||
|
||||
// Replace refresh cookie
|
||||
res.cookie("refresh_token", newRefreshToken, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
sameSite: process.env.NODE_ENV === "production" ? "None" : "Lax",
|
||||
maxAge: 7 * 24 * 60 * 60 * 1000,
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
success: true,
|
||||
message: "Session refreshed successfully",
|
||||
});
|
||||
res.json({ success: true, message });
|
||||
} catch (error) {
|
||||
console.error("REFRESH ERROR:", error);
|
||||
|
||||
return res.status(401).send({
|
||||
success: false,
|
||||
message: "Invalid or expired session. Please login again.",
|
||||
});
|
||||
console.error(`[${req.id}] Verification resend failed`, { name: error.name, message: error.message });
|
||||
res.json({ success: true, message });
|
||||
}
|
||||
};
|
||||
|
||||
exports.forgotPassword = async (req, res) => {
|
||||
exports.oauth = (provider) => async (req, res, next) => {
|
||||
try {
|
||||
let { email } = req.body;
|
||||
|
||||
if (!email) {
|
||||
return res.status(400).send({
|
||||
success: false,
|
||||
message: "Email is required",
|
||||
});
|
||||
}
|
||||
|
||||
email = email.trim().toLowerCase();
|
||||
|
||||
if (!validateEmail(email)) {
|
||||
return res.status(400).send({
|
||||
success: false,
|
||||
message: "Invalid email address",
|
||||
});
|
||||
}
|
||||
|
||||
const user = await User.findOne({
|
||||
where: { email },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return res.status(200).send({
|
||||
success: true,
|
||||
message:
|
||||
"If an account exists for this email, a password reset link has been sent.",
|
||||
});
|
||||
}
|
||||
|
||||
const resetToken = await createPasswordReset(user.id);
|
||||
|
||||
await sendPasswordResetEmail(user.email, user.firstName, resetToken);
|
||||
|
||||
return res.status(200).send({
|
||||
success: true,
|
||||
message:
|
||||
"If an account exists for this email, a password reset link has been sent.",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("FORGOT PASSWORD ERROR:", error);
|
||||
|
||||
return res.status(500).send({
|
||||
success: false,
|
||||
message: "Unable to process password reset request",
|
||||
});
|
||||
}
|
||||
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.resetPassword = async (req, res) => {
|
||||
exports.adminLogin = async (req, res, next) => {
|
||||
try {
|
||||
const { token, newPassword, confirmPassword } = req.body;
|
||||
|
||||
if (!token || !newPassword || !confirmPassword) {
|
||||
return res.status(400).send({
|
||||
success: false,
|
||||
message: "Token, new password and confirm password are required",
|
||||
});
|
||||
}
|
||||
|
||||
if (newPassword !== confirmPassword) {
|
||||
return res.status(400).send({
|
||||
success: false,
|
||||
message: "Passwords do not match",
|
||||
});
|
||||
}
|
||||
|
||||
if (!validatePassword(newPassword)) {
|
||||
return res.status(400).send({
|
||||
success: false,
|
||||
message: "Password does not meet the required criteria",
|
||||
});
|
||||
}
|
||||
|
||||
const verification = await verifyPasswordResetToken(token);
|
||||
|
||||
if (!verification) {
|
||||
return res.status(400).send({
|
||||
success: false,
|
||||
message: "Reset token is invalid or expired",
|
||||
});
|
||||
}
|
||||
|
||||
const { userId, redisKey } = verification;
|
||||
|
||||
const user = await User.findByPk(userId);
|
||||
|
||||
if (!user) {
|
||||
await deletePasswordReset(redisKey);
|
||||
|
||||
return res.status(400).send({
|
||||
success: false,
|
||||
message: "Reset token is invalid or expired",
|
||||
});
|
||||
}
|
||||
|
||||
const samePassword = await checkPassword(newPassword, user.password);
|
||||
|
||||
if (samePassword) {
|
||||
return res.status(400).send({
|
||||
success: false,
|
||||
message: "New password must be different from the current password",
|
||||
});
|
||||
}
|
||||
|
||||
const hashedPassword = await hashPassword(newPassword);
|
||||
|
||||
user.password = hashedPassword;
|
||||
|
||||
user.passwordChangedAt = new Date();
|
||||
|
||||
await user.save();
|
||||
|
||||
await sendPasswordChangedEmail(user.email, user.firstName);
|
||||
|
||||
await deletePasswordReset(redisKey);
|
||||
|
||||
deleteAllUserSessions(user.id);
|
||||
|
||||
return res.status(200).send({
|
||||
success: true,
|
||||
message: "Password reset successfully. Please login again.",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("RESET PASSWORD ERROR:", error);
|
||||
|
||||
return res.status(500).send({
|
||||
success: false,
|
||||
message: "Failed to reset password",
|
||||
});
|
||||
}
|
||||
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.changePassword = async (req, res) => {
|
||||
exports.riderLogin = async (req, res, next) => {
|
||||
try {
|
||||
// User ID comes from authenticate middleware
|
||||
const userId = req.user.id;
|
||||
|
||||
const { currentPassword, newPassword, confirmPassword } = req.body;
|
||||
|
||||
// 1. Check required fields
|
||||
if (!currentPassword || !newPassword || !confirmPassword) {
|
||||
return res.status(400).send({
|
||||
success: false,
|
||||
message:
|
||||
"Current password, new password and confirm password are required",
|
||||
});
|
||||
}
|
||||
|
||||
// 2. Check new password and confirmation
|
||||
if (newPassword !== confirmPassword) {
|
||||
return res.status(400).send({
|
||||
success: false,
|
||||
message: "New password and confirm password do not match",
|
||||
});
|
||||
}
|
||||
|
||||
// 3. Validate password policy
|
||||
const passwordValid = validatePassword(newPassword);
|
||||
|
||||
if (!passwordValid) {
|
||||
return res.status(400).send({
|
||||
success: false,
|
||||
message: "New password does not meet the required criteria",
|
||||
});
|
||||
}
|
||||
|
||||
// 4. Get logged-in user from database
|
||||
const user = await User.findByPk(userId);
|
||||
|
||||
if (!user) {
|
||||
return res.status(404).send({
|
||||
success: false,
|
||||
message: "User not found",
|
||||
});
|
||||
}
|
||||
|
||||
// 5. Check current password
|
||||
const currentPasswordValid = await checkPassword(
|
||||
currentPassword,
|
||||
user.password,
|
||||
);
|
||||
|
||||
if (!currentPasswordValid) {
|
||||
return res.status(401).send({
|
||||
success: false,
|
||||
message: "Current password is incorrect",
|
||||
});
|
||||
}
|
||||
|
||||
// 6. Make sure new password is different
|
||||
const sameAsOldPassword = await checkPassword(newPassword, user.password);
|
||||
|
||||
if (sameAsOldPassword) {
|
||||
return res.status(400).send({
|
||||
success: false,
|
||||
message: "New password must be different from current password",
|
||||
});
|
||||
}
|
||||
|
||||
// 7. Hash new password
|
||||
const hashedPassword = await hashPassword(newPassword);
|
||||
|
||||
// 8. Update user
|
||||
user.password = hashedPassword;
|
||||
user.passwordChangedAt = new Date();
|
||||
|
||||
await user.save();
|
||||
|
||||
// 9. Revoke all refresh sessions
|
||||
deleteAllUserSessions(user.id);
|
||||
|
||||
// 10. Clear auth cookies
|
||||
res.clearCookie("access_token", {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
sameSite: process.env.NODE_ENV === "production" ? "None" : "Lax",
|
||||
});
|
||||
|
||||
res.clearCookie("refresh_token", {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
sameSite: process.env.NODE_ENV === "production" ? "None" : "Lax",
|
||||
});
|
||||
|
||||
// 11. Send confirmation email
|
||||
try {
|
||||
await sendPasswordChangedEmail(user.email, user.firstName);
|
||||
} catch (mailError) {
|
||||
console.error("PASSWORD CHANGED EMAIL ERROR:", mailError);
|
||||
}
|
||||
|
||||
// 12. Response
|
||||
return res.status(200).send({
|
||||
success: true,
|
||||
message: "Password changed successfully. Please login again.",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("CHANGE PASSWORD ERROR:", error);
|
||||
|
||||
return res.status(500).send({
|
||||
success: false,
|
||||
message: "Failed to change password",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Logout: Clear the JWT cookie
|
||||
exports.logout = async (req, res) => {
|
||||
try {
|
||||
// 1. Get refresh token from cookie
|
||||
const refreshToken = req.cookies?.refresh_token;
|
||||
|
||||
// 2. If refresh token exists, find its session
|
||||
if (refreshToken) {
|
||||
const session = validateRefreshSession(refreshToken);
|
||||
|
||||
// 3. Delete refresh session from server RAM
|
||||
if (session) {
|
||||
deleteRefreshSession(session.sessionId);
|
||||
|
||||
console.log(`Refresh session deleted: ${session.sessionId}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Clear access token cookie
|
||||
res.clearCookie("access_token", {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
sameSite: process.env.NODE_ENV === "production" ? "None" : "Lax",
|
||||
});
|
||||
|
||||
// 5. Clear refresh token cookie
|
||||
res.clearCookie("refresh_token", {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
sameSite: process.env.NODE_ENV === "production" ? "None" : "Lax",
|
||||
});
|
||||
|
||||
// 6. Send response
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
message: "Logged out successfully",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("LOGOUT ERROR:", error);
|
||||
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "Failed to logout",
|
||||
});
|
||||
}
|
||||
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); }
|
||||
};
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
const db = require("../models");
|
||||
const { clearPermissionCache } = require("../utils/cache.util");
|
||||
const { logActivity } = require("../services/activity.service");
|
||||
|
||||
exports.assignRole = async (req, res, next) => {
|
||||
try {
|
||||
const [assignment] = await db.UserRole.findOrCreate({ where: { user_id: req.params.userId, role_id: req.params.roleId } });
|
||||
await clearPermissionCache(req.params.userId);
|
||||
await logActivity({ user: req.user, description: `Role ${req.params.roleId} assigned to user ${req.params.userId}`, type: "ROLE_ASSIGNED", module: "Authorization" });
|
||||
res.status(201).json({ success: true, data: { id: assignment.id, userId: assignment.user_id, roleId: assignment.role_id } });
|
||||
} catch (error) { next(error); }
|
||||
};
|
||||
|
||||
exports.removeRole = async (req, res, next) => {
|
||||
try {
|
||||
await db.UserRole.destroy({ where: { user_id: req.params.userId, role_id: req.params.roleId } });
|
||||
await clearPermissionCache(req.params.userId);
|
||||
await logActivity({ user: req.user, description: `Role ${req.params.roleId} removed from user ${req.params.userId}`, type: "ROLE_REMOVED", module: "Authorization" });
|
||||
res.json({ success: true, message: "Role removed" });
|
||||
} catch (error) { next(error); }
|
||||
};
|
||||
@@ -44,7 +44,6 @@ exports.createNewUser = async (req, res) => {
|
||||
lastName,
|
||||
email,
|
||||
password,
|
||||
accountType,
|
||||
address,
|
||||
phoneNumber,
|
||||
businessName,
|
||||
@@ -56,23 +55,23 @@ exports.createNewUser = async (req, res) => {
|
||||
note,
|
||||
} = req.body;
|
||||
|
||||
if (!firstName || !lastName || !email || !password || !accountType) {
|
||||
if (!firstName || !lastName || !email || !password) {
|
||||
await transaction.rollback();
|
||||
|
||||
return res.status(400).send({
|
||||
success: false,
|
||||
message:
|
||||
"First name, last name, email, password and account type are required",
|
||||
"First name, last name, email and password are required",
|
||||
});
|
||||
}
|
||||
|
||||
if (accountType !== "customer" && accountType !== "business_customer") {
|
||||
if (req.body.accountType && req.body.accountType !== "customer") {
|
||||
await transaction.rollback();
|
||||
|
||||
return res.status(400).send({
|
||||
success: false,
|
||||
message:
|
||||
"Invalid account type. Must be either 'customer' or 'business_customer'",
|
||||
"Public registration creates customer accounts only",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -118,7 +117,7 @@ exports.createNewUser = async (req, res) => {
|
||||
lastName,
|
||||
email,
|
||||
password: hashedPassword,
|
||||
accountType,
|
||||
accountType: "customer",
|
||||
accountStatus: "PENDING_VERIFICATION",
|
||||
emailVerifiedAt: null,
|
||||
},
|
||||
@@ -139,8 +138,8 @@ exports.createNewUser = async (req, res) => {
|
||||
{ transaction },
|
||||
);
|
||||
|
||||
//create customer and business customer
|
||||
if (accountType === "customer") {
|
||||
// Create the customer identity extension for public registration.
|
||||
{
|
||||
const customerData = {
|
||||
address,phoneNumber,
|
||||
};
|
||||
@@ -150,23 +149,6 @@ exports.createNewUser = async (req, res) => {
|
||||
customerData,
|
||||
transaction,
|
||||
);
|
||||
} else if (accountType === "business_customer") {
|
||||
const businessData = {
|
||||
businessName,
|
||||
businessRegistrationNumber,
|
||||
businessType,
|
||||
contactName,
|
||||
phoneNumber,
|
||||
businessEmail,
|
||||
expectedMonthlyVolume,
|
||||
note,
|
||||
};
|
||||
|
||||
await createBusinessCustomerDetails(
|
||||
newUser.id,
|
||||
businessData,
|
||||
transaction,
|
||||
);
|
||||
}
|
||||
|
||||
await transaction.commit();
|
||||
@@ -449,6 +431,7 @@ exports.updateUser = async (req, res) => {
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
await transaction.rollback();
|
||||
return res.status(404).send({
|
||||
success: false,
|
||||
message: "User not found",
|
||||
@@ -456,6 +439,7 @@ exports.updateUser = async (req, res) => {
|
||||
}
|
||||
|
||||
if (!UserProfile) {
|
||||
await transaction.rollback();
|
||||
return res.status(404).send({
|
||||
success: false,
|
||||
message: "User profile not found",
|
||||
@@ -533,6 +517,7 @@ exports.deleteUser = async (req, res) => {
|
||||
where: { id },
|
||||
});
|
||||
if (!user) {
|
||||
await transaction.rollback();
|
||||
return res.status(404).send({
|
||||
success: false,
|
||||
message: "User not found",
|
||||
@@ -560,3 +545,22 @@ exports.deleteUser = async (req, res) => {
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
exports.getCurrentUser = async (req, res, next) => {
|
||||
try {
|
||||
const user = await User.findByPk(req.user.id, { attributes: { exclude: ["password", "tokenVersion", "passwordChangedAt"] }, include: [{ model: Profile, as: "profile" }] });
|
||||
res.json({ success: true, data: user });
|
||||
} catch (error) { next(error); }
|
||||
};
|
||||
|
||||
exports.updateCurrentUser = async (req, res, next) => {
|
||||
try {
|
||||
const allowed = ["firstName", "lastName"];
|
||||
const supplied = Object.keys(req.body);
|
||||
if (supplied.some((key) => !allowed.includes(key))) return res.status(400).json({ success: false, error: { code: "UNSAFE_FIELD", message: "Only firstName and lastName may be updated" } });
|
||||
const updates = Object.fromEntries(supplied.map((key) => [key, req.body[key]]).filter(([, value]) => typeof value === "string" && value.trim()));
|
||||
await User.update(updates, { where: { id: req.user.id } });
|
||||
const user = await User.findByPk(req.user.id, { attributes: ["id", "firstName", "lastName", "email", "accountType", "accountStatus"] });
|
||||
res.json({ success: true, data: user });
|
||||
} catch (error) { next(error); }
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user