This commit is contained in:
Ken
2026-01-19 09:33:35 +07:00
parent 374dc12b2d
commit 70838a4bc1
103 changed files with 16929 additions and 2 deletions

View File

@@ -0,0 +1,351 @@
const { UsersAuth, UserProfile } = require('../models');
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const crypto = require('crypto');
// JWT Secret - nên lưu trong environment variable
const JWT_SECRET = process.env.JWT_SECRET || 'sena-secret-key-2026';
const JWT_EXPIRES_IN = '24h';
/**
* Auth Controller - Xác thực và phân quyền
*/
class AuthController {
/**
* Login - Xác thực người dùng
*/
async login(req, res, next) {
try {
const { username, password } = req.body;
// Validate input
if (!username || !password) {
return res.status(400).json({
success: false,
message: 'Username và password là bắt buộc',
});
}
// Tìm user theo username hoặc email
const user = await UsersAuth.findOne({
where: {
[require('sequelize').Op.or]: [
{ username: username },
{ email: username },
],
},
include: [{
model: UserProfile,
as: 'profile',
}],
});
if (!user) {
return res.status(401).json({
success: false,
message: 'Username hoặc password không đúng',
});
}
// Kiểm tra tài khoản có bị khóa không
if (user.is_locked) {
if (user.locked_until && new Date() < new Date(user.locked_until)) {
return res.status(403).json({
success: false,
message: 'Tài khoản bị khóa đến ' + user.locked_until,
});
} else {
// Mở khóa nếu hết thời gian khóa
await user.update({
is_locked: false,
locked_until: null,
failed_login_attempts: 0
});
}
}
// Kiểm tra tài khoản có active không
if (!user.is_active) {
return res.status(403).json({
success: false,
message: 'Tài khoản đã bị vô hiệu hóa',
});
}
// Verify password
const passwordMatch = await bcrypt.compare(password + user.salt, user.password_hash);
if (!passwordMatch) {
// Tăng số lần đăng nhập thất bại
const failedAttempts = user.failed_login_attempts + 1;
const updates = { failed_login_attempts: failedAttempts };
// Khóa tài khoản sau 5 lần thất bại
if (failedAttempts >= 5) {
updates.is_locked = true;
updates.locked_until = new Date(Date.now() + 30 * 60 * 1000); // Khóa 30 phút
}
await user.update(updates);
return res.status(401).json({
success: false,
message: 'Username hoặc password không đúng',
attemptsLeft: Math.max(0, 5 - failedAttempts),
});
}
// Đăng nhập thành công - Reset failed attempts
const sessionId = crypto.randomUUID();
const clientIp = req.ip || req.connection.remoteAddress;
await user.update({
failed_login_attempts: 0,
login_count: user.login_count + 1,
last_login: new Date(),
last_login_ip: clientIp,
current_session_id: sessionId,
});
// Tạo JWT token
const token = jwt.sign(
{
userId: user.id,
username: user.username,
email: user.email,
sessionId: sessionId,
},
JWT_SECRET,
{ expiresIn: JWT_EXPIRES_IN }
);
// Return success response
res.json({
success: true,
message: 'Đăng nhập thành công',
data: {
token,
user: {
id: user.id,
username: user.username,
email: user.email,
profile: user.profile,
last_login: user.last_login,
login_count: user.login_count,
},
},
});
} catch (error) {
next(error);
}
}
/**
* Register - Tạo tài khoản mới
*/
async register(req, res, next) {
try {
const { username, email, password, full_name, phone, school_id } = req.body;
// Validate input
if (!username || !email || !password) {
return res.status(400).json({
success: false,
message: 'Username, email và password là bắt buộc',
});
}
// Kiểm tra username đã tồn tại
const existingUser = await UsersAuth.findOne({
where: {
[require('sequelize').Op.or]: [
{ username },
{ email },
],
},
});
if (existingUser) {
return res.status(409).json({
success: false,
message: 'Username hoặc email đã tồn tại',
});
}
// Hash password
const salt = crypto.randomBytes(16).toString('hex');
const passwordHash = await bcrypt.hash(password + salt, 10);
// Tạo user mới
const newUser = await UsersAuth.create({
username,
email,
password_hash: passwordHash,
salt,
qr_secret: crypto.randomBytes(32).toString('hex'),
});
// Tạo profile nếu có thông tin
if (full_name || phone || school_id) {
await UserProfile.create({
user_id: newUser.id,
full_name: full_name || username,
phone,
school_id,
});
}
res.status(201).json({
success: true,
message: 'Đăng ký tài khoản thành công',
data: {
id: newUser.id,
username: newUser.username,
email: newUser.email,
},
});
} catch (error) {
next(error);
}
}
/**
* Verify Token - Xác thực JWT token
*/
async verifyToken(req, res, next) {
try {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) {
return res.status(401).json({
success: false,
message: 'Token không được cung cấp',
});
}
const decoded = jwt.verify(token, JWT_SECRET);
// Kiểm tra user còn tồn tại và session hợp lệ
const user = await UsersAuth.findByPk(decoded.userId, {
include: [{
model: UserProfile,
as: 'profile',
}],
});
if (!user || !user.is_active) {
return res.status(401).json({
success: false,
message: 'Token không hợp lệ hoặc tài khoản đã bị vô hiệu hóa',
});
}
if (user.current_session_id !== decoded.sessionId) {
return res.status(401).json({
success: false,
message: 'Phiên đăng nhập đã hết hạn',
});
}
res.json({
success: true,
message: 'Token hợp lệ',
data: {
user: {
id: user.id,
username: user.username,
email: user.email,
profile: user.profile,
},
},
});
} catch (error) {
if (error.name === 'JsonWebTokenError' || error.name === 'TokenExpiredError') {
return res.status(401).json({
success: false,
message: 'Token không hợp lệ hoặc đã hết hạn',
});
}
next(error);
}
}
/**
* Logout - Đăng xuất
*/
async logout(req, res, next) {
try {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) {
return res.status(401).json({
success: false,
message: 'Token không được cung cấp',
});
}
const decoded = jwt.verify(token, JWT_SECRET);
// Xóa session hiện tại
await UsersAuth.update(
{ current_session_id: null },
{ where: { id: decoded.userId } }
);
res.json({
success: true,
message: 'Đăng xuất thành công',
});
} catch (error) {
next(error);
}
}
/**
* Get current user info from token
*/
async me(req, res, next) {
try {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) {
return res.status(401).json({
success: false,
message: 'Token không được cung cấp',
});
}
const decoded = jwt.verify(token, JWT_SECRET);
const user = await UsersAuth.findByPk(decoded.userId, {
include: [{
model: UserProfile,
as: 'profile',
}],
attributes: { exclude: ['password_hash', 'salt', 'qr_secret'] },
});
if (!user) {
return res.status(404).json({
success: false,
message: 'User không tồn tại',
});
}
res.json({
success: true,
data: user,
});
} catch (error) {
if (error.name === 'JsonWebTokenError' || error.name === 'TokenExpiredError') {
return res.status(401).json({
success: false,
message: 'Token không hợp lệ hoặc đã hết hạn',
});
}
next(error);
}
}
}
module.exports = new AuthController();