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();