Files
gameno-api/components/auth/authService.js
T
kavehhn 192c595c68 feat: add password change, class unenroll, and optional notify flags
Let admins skip SMS on user, class, and invoice actions, and let users change their own password with the current one.
2026-08-15 23:48:18 +03:30

142 lines
3.9 KiB
JavaScript

// /components/auth/authService.js
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
const User = require('../users/userModel');
const config = require('../../config/config');
const AppError = require('../../utils/AppError');
const { isBootstrapSuperAdminDisabled } = require('../../utils/superAdmin');
const { assertPasswordStrength } = require('../../utils/passwordRules');
const generateTokens = (user) => {
const payload = {
id: user._id,
username: user.username,
role: user.role?._id || user.role
};
const accessToken = jwt.sign(payload, config.JWT_ACCESS_SECRET, {
expiresIn: config.JWT_ACCESS_EXPIRES_IN
});
const refreshToken = jwt.sign({ id: user._id }, config.JWT_REFRESH_SECRET, {
expiresIn: config.JWT_REFRESH_EXPIRES_IN
});
return { accessToken, refreshToken };
};
const login = async (username, password) => {
const user = await User.findOne({ username }).populate('role');
if (!user || !user.isActive || isBootstrapSuperAdminDisabled(user)) {
throw new AppError('INVALID_CREDENTIALS');
}
const isMatch = await bcrypt.compare(password, user.passwordHash);
if (!isMatch) {
throw new AppError('INVALID_CREDENTIALS');
}
const { accessToken, refreshToken: refreshTokenString } = generateTokens(user);
const expiresAt = new Date();
expiresAt.setDate(expiresAt.getDate() + 7);
user.refreshTokens.push({ token: refreshTokenString, expiresAt });
await user.save();
const userObject = user.toObject();
delete userObject.passwordHash;
delete userObject.refreshTokens;
return {
user: userObject,
accessToken,
refreshToken: refreshTokenString
};
};
const refreshToken = async (refreshTokenString) => {
let decoded;
try {
decoded = jwt.verify(refreshTokenString, config.JWT_REFRESH_SECRET);
} catch (err) {
throw new AppError('INVALID_REFRESH_TOKEN');
}
const user = await User.findById(decoded.id).populate('role');
if (!user || !user.isActive || isBootstrapSuperAdminDisabled(user)) {
throw new AppError('INVALID_REFRESH_TOKEN');
}
const tokenIndex = user.refreshTokens.findIndex(rt => rt.token === refreshTokenString);
if (tokenIndex === -1) {
throw new AppError('INVALID_REFRESH_TOKEN');
}
if (new Date() > new Date(user.refreshTokens[tokenIndex].expiresAt)) {
user.refreshTokens.splice(tokenIndex, 1);
await user.save();
throw new AppError('TOKEN_EXPIRED');
}
user.refreshTokens.splice(tokenIndex, 1);
const { accessToken, refreshToken: newRefreshToken } = generateTokens(user);
const expiresAt = new Date();
expiresAt.setDate(expiresAt.getDate() + 7);
user.refreshTokens.push({ token: newRefreshToken, expiresAt });
await user.save();
return {
accessToken,
refreshToken: newRefreshToken
};
};
const logout = async (userId, refreshTokenString) => {
const user = await User.findById(userId);
if (user) {
user.refreshTokens = user.refreshTokens.filter(rt => rt.token !== refreshTokenString);
await user.save();
}
return true;
};
const changePassword = async (userId, { currentPassword, newPassword, refreshToken: currentRefreshToken }) => {
if (!currentPassword) {
throw new AppError('INVALID_CURRENT_PASSWORD');
}
assertPasswordStrength(newPassword);
const user = await User.findById(userId);
if (!user || !user.isActive || isBootstrapSuperAdminDisabled(user)) {
throw new AppError('USER_NOT_FOUND');
}
const isMatch = await bcrypt.compare(currentPassword, user.passwordHash);
if (!isMatch) {
throw new AppError('INVALID_CURRENT_PASSWORD');
}
user.passwordHash = await bcrypt.hash(newPassword, 10);
if (currentRefreshToken) {
user.refreshTokens = user.refreshTokens.filter((rt) => rt.token === currentRefreshToken);
} else {
user.refreshTokens = [];
}
await user.save();
return true;
};
module.exports = {
login,
refreshToken,
logout,
changePassword
};