481 lines
13 KiB
JavaScript
481 lines
13 KiB
JavaScript
const { UsersAuth, UserProfile, Role, Permission, UserAssignment, School, Class, Grade } = require('../models');
|
|
const roleHelperService = require('../services/roleHelperService');
|
|
const bcrypt = require('bcrypt');
|
|
const jwt = require('jsonwebtoken');
|
|
const crypto = require('crypto');
|
|
const { Op } = require('sequelize');
|
|
|
|
// 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 {
|
|
/**
|
|
* @swagger
|
|
* /api/auth/login:
|
|
* post:
|
|
* tags: [Authentication]
|
|
* summary: Đăng nhập vào hệ thống
|
|
* description: Xác thực người dùng bằng username/email và password
|
|
* requestBody:
|
|
* required: true
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* required:
|
|
* - username
|
|
* - password
|
|
* properties:
|
|
* username:
|
|
* type: string
|
|
* example: student001
|
|
* password:
|
|
* type: string
|
|
* format: password
|
|
* example: password123
|
|
* responses:
|
|
* 200:
|
|
* description: Đăng nhập thành công
|
|
* 401:
|
|
* description: Username hoặc password không đúng
|
|
* 403:
|
|
* description: Tài khoản bị khóa
|
|
*/
|
|
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,
|
|
});
|
|
|
|
// Load role và permissions bằng helper service
|
|
const { roleInfo, permissions } = await roleHelperService.getUserRoleAndPermissions(user.id);
|
|
|
|
// Tạo JWT token
|
|
const token = jwt.sign(
|
|
{
|
|
userId: user.id,
|
|
username: user.username,
|
|
email: user.email,
|
|
sessionId: sessionId,
|
|
roleCode: roleInfo?.role_code,
|
|
},
|
|
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,
|
|
role: roleInfo,
|
|
permissions: permissions,
|
|
last_login: user.last_login,
|
|
login_count: user.login_count,
|
|
},
|
|
},
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @swagger
|
|
* /api/auth/register:
|
|
* post:
|
|
* tags: [Authentication]
|
|
* summary: Đăng ký tài khoản mới
|
|
* description: Tạo tài khoản người dùng mới với username, email và password. Tự động tạo profile nếu có thông tin.
|
|
* requestBody:
|
|
* required: true
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* required:
|
|
* - username
|
|
* - email
|
|
* - password
|
|
* properties:
|
|
* username:
|
|
* type: string
|
|
* minLength: 3
|
|
* maxLength: 50
|
|
* example: newuser123
|
|
* email:
|
|
* type: string
|
|
* format: email
|
|
* example: user@example.com
|
|
* password:
|
|
* type: string
|
|
* format: password
|
|
* minLength: 6
|
|
* example: password123
|
|
* full_name:
|
|
* type: string
|
|
* example: Nguyễn Văn A
|
|
* phone:
|
|
* type: string
|
|
* example: "0901234567"
|
|
* school_id:
|
|
* type: string
|
|
* format: uuid
|
|
* responses:
|
|
* 201:
|
|
* description: Đăng ký tài khoản thành công
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* properties:
|
|
* success:
|
|
* type: boolean
|
|
* example: true
|
|
* message:
|
|
* type: string
|
|
* example: Đăng ký tài khoản thành công
|
|
* data:
|
|
* type: object
|
|
* properties:
|
|
* id:
|
|
* type: string
|
|
* format: uuid
|
|
* username:
|
|
* type: string
|
|
* email:
|
|
* type: string
|
|
* 400:
|
|
* description: Thiếu thông tin bắt buộc
|
|
* 409:
|
|
* description: Username hoặc email đã tồn tạ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',
|
|
});
|
|
}
|
|
|
|
// Check if user exists
|
|
const existingUser = await UsersAuth.findOne({
|
|
where: {
|
|
[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 hashedPassword = await bcrypt.hash(password, 10);
|
|
|
|
// Create user auth
|
|
const user = await UsersAuth.create({
|
|
username,
|
|
email,
|
|
password_hash: hashedPassword,
|
|
status: 'active',
|
|
});
|
|
|
|
// Create user profile if we have additional info
|
|
if (full_name || phone) {
|
|
await UserProfile.create({
|
|
user_id: user.id,
|
|
full_name: full_name || null,
|
|
phone: phone || null,
|
|
school_id: school_id || null,
|
|
});
|
|
}
|
|
|
|
res.status(201).json({
|
|
success: true,
|
|
message: 'Đăng ký tài khoản thành công',
|
|
data: {
|
|
id: user.id,
|
|
username: user.username,
|
|
email: user.email,
|
|
},
|
|
});
|
|
} catch (error) {
|
|
console.error('Register error:', error);
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @swagger
|
|
* /api/auth/verify-token:
|
|
* post:
|
|
* tags: [Authentication]
|
|
* summary: Xác thực JWT token
|
|
* description: Kiểm tra tính hợp lệ của JWT token
|
|
* security:
|
|
* - bearerAuth: []
|
|
* responses:
|
|
* 200:
|
|
* description: Token hợp lệ
|
|
* 401:
|
|
* description: Token không hợp lệ
|
|
*/
|
|
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);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @swagger
|
|
* /api/auth/logout:
|
|
* post:
|
|
* tags: [Authentication]
|
|
* summary: Đăng xuất
|
|
* description: Xóa session hiện tại
|
|
* security:
|
|
* - bearerAuth: []
|
|
* responses:
|
|
* 200:
|
|
* description: Đăng xuất thành công
|
|
* 401:
|
|
* description: Token không hợp lệ
|
|
*/
|
|
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',
|
|
});
|
|
}
|
|
|
|
// Load role và permissions bằng helper service
|
|
const { roleInfo, permissions } = await roleHelperService.getUserRoleAndPermissions(user.id);
|
|
|
|
res.json({
|
|
success: true,
|
|
data: {
|
|
...user.toJSON(),
|
|
role: roleInfo,
|
|
permissions: permissions,
|
|
},
|
|
});
|
|
} 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(); |