update
This commit is contained in:
495
controllers/userController.js
Normal file
495
controllers/userController.js
Normal file
@@ -0,0 +1,495 @@
|
||||
const { UsersAuth, UserProfile } = require('../models');
|
||||
const { cacheUtils } = require('../config/redis');
|
||||
const { addDatabaseWriteJob } = require('../config/bullmq');
|
||||
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';
|
||||
|
||||
/**
|
||||
* User Controller - Quản lý người dùng (Auth + Profile)
|
||||
*/
|
||||
class UserController {
|
||||
/**
|
||||
* 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 all users with pagination and caching
|
||||
*/
|
||||
async getAllUsers(req, res, next) {
|
||||
try {
|
||||
const { page = 1, limit = 20, is_active, school_id } = req.query;
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
const cacheKey = `users:list:${page}:${limit}:${is_active || 'all'}:${school_id || 'all'}`;
|
||||
|
||||
const cached = await cacheUtils.get(cacheKey);
|
||||
if (cached) {
|
||||
return res.json({
|
||||
success: true,
|
||||
data: cached,
|
||||
cached: true,
|
||||
});
|
||||
}
|
||||
|
||||
const where = {};
|
||||
if (is_active !== undefined) where.is_active = is_active === 'true';
|
||||
|
||||
const { count, rows } = await UsersAuth.findAndCountAll({
|
||||
where,
|
||||
limit: parseInt(limit),
|
||||
offset: parseInt(offset),
|
||||
include: [{
|
||||
model: UserProfile,
|
||||
as: 'profile',
|
||||
attributes: ['full_name', 'phone', 'school_id'],
|
||||
where: school_id ? { school_id } : undefined,
|
||||
}],
|
||||
order: [['created_at', 'DESC']],
|
||||
});
|
||||
|
||||
const result = {
|
||||
users: rows,
|
||||
pagination: {
|
||||
total: count,
|
||||
page: parseInt(page),
|
||||
limit: parseInt(limit),
|
||||
totalPages: Math.ceil(count / limit),
|
||||
},
|
||||
};
|
||||
|
||||
await cacheUtils.set(cacheKey, result, 1800);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result,
|
||||
cached: false,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user by ID with profile
|
||||
*/
|
||||
async getUserById(req, res, next) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const cacheKey = `user:${id}`;
|
||||
|
||||
const cached = await cacheUtils.get(cacheKey);
|
||||
if (cached) {
|
||||
return res.json({
|
||||
success: true,
|
||||
data: cached,
|
||||
cached: true,
|
||||
});
|
||||
}
|
||||
|
||||
const user = await UsersAuth.findByPk(id, {
|
||||
include: [{
|
||||
model: UserProfile,
|
||||
as: 'profile',
|
||||
}],
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'User not found',
|
||||
});
|
||||
}
|
||||
|
||||
await cacheUtils.set(cacheKey, user, 3600);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: user,
|
||||
cached: false,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new user (async via BullMQ)
|
||||
*/
|
||||
async createUser(req, res, next) {
|
||||
try {
|
||||
const userData = req.body;
|
||||
|
||||
const job = await addDatabaseWriteJob('create', 'UsersAuth', userData);
|
||||
|
||||
await cacheUtils.deletePattern('users:list:*');
|
||||
|
||||
res.status(202).json({
|
||||
success: true,
|
||||
message: 'User creation job queued',
|
||||
jobId: job.id,
|
||||
data: userData,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update user (async via BullMQ)
|
||||
*/
|
||||
async updateUser(req, res, next) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const updates = req.body;
|
||||
|
||||
const job = await addDatabaseWriteJob('update', 'UsersAuth', {
|
||||
id,
|
||||
updates,
|
||||
});
|
||||
|
||||
await cacheUtils.delete(`user:${id}`);
|
||||
await cacheUtils.deletePattern('users:list:*');
|
||||
|
||||
res.status(202).json({
|
||||
success: true,
|
||||
message: 'User update job queued',
|
||||
jobId: job.id,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete user (soft delete via is_active)
|
||||
*/
|
||||
async deleteUser(req, res, next) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const job = await addDatabaseWriteJob('update', 'UsersAuth', {
|
||||
id,
|
||||
updates: { is_active: false },
|
||||
});
|
||||
|
||||
await cacheUtils.delete(`user:${id}`);
|
||||
await cacheUtils.deletePattern('users:list:*');
|
||||
|
||||
res.status(202).json({
|
||||
success: true,
|
||||
message: 'User deletion job queued',
|
||||
jobId: job.id,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user datatypes
|
||||
*/
|
||||
async getUserDatatypes(req, res, next) {
|
||||
try {
|
||||
const datatypes = UsersAuth.rawAttributes;
|
||||
res.json({
|
||||
success: true,
|
||||
data: datatypes,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new UserController();
|
||||
Reference in New Issue
Block a user