From 6d82c5007c9db746387ade636d23e3ee224da3fd Mon Sep 17 00:00:00 2001 From: yotakii Date: Thu, 5 Mar 2026 12:16:29 +0300 Subject: [PATCH] fix login --- backend/src/modules/auth/auth.controller.ts | 10 +- backend/src/modules/auth/auth.service.ts | 300 +++++++++----------- 2 files changed, 133 insertions(+), 177 deletions(-) diff --git a/backend/src/modules/auth/auth.controller.ts b/backend/src/modules/auth/auth.controller.ts index da8af0f..f762e29 100644 --- a/backend/src/modules/auth/auth.controller.ts +++ b/backend/src/modules/auth/auth.controller.ts @@ -1,6 +1,6 @@ import { Request, Response } from 'express' import { authService } from './auth.service' -import { AuthRequest } from '@/shared/middleware/auth' +import { AuthRequest } from '../../shared/middleware/auth' export const authController = { register: async (req: Request, res: Response) => { @@ -21,7 +21,7 @@ export const authController = { login: async (req: Request, res: Response) => { try { - + const { email, password } = req.body if (!email || !password) { @@ -31,7 +31,7 @@ export const authController = { }) } - const result = await authService.login(email, password) + const result = await authService.login(String(email).trim(), String(password)) res.status(200).json({ success: true, @@ -39,9 +39,9 @@ export const authController = { data: result }) } catch (error: any) { - res.status(401).json({ + res.status(error?.statusCode || 401).json({ success: false, - message: error.message + message: error.message || 'بيانات الدخول غير صحيحة' }) } }, diff --git a/backend/src/modules/auth/auth.service.ts b/backend/src/modules/auth/auth.service.ts index 28a7108..e525f0e 100644 --- a/backend/src/modules/auth/auth.service.ts +++ b/backend/src/modules/auth/auth.service.ts @@ -1,26 +1,27 @@ -import bcrypt from 'bcryptjs'; -import jwt, { Secret, SignOptions } from 'jsonwebtoken'; -import prisma from '../../config/database'; -import { config } from '../../config'; -import { AppError } from '../../shared/middleware/errorHandler'; +import bcrypt from 'bcryptjs' +import jwt, { Secret, SignOptions } from 'jsonwebtoken' +import prisma from '../../config/database' +import { config } from '../../config' +import { AppError } from '../../shared/middleware/errorHandler' class AuthService { - async register(data: { - email: string; - username: string; - password: string; - employeeId?: string; + async register(data: { + email: string + username: string + password: string + employeeId?: string }) { - // Hash password - const hashedPassword = await bcrypt.hash(data.password, config.security.bcryptRounds); + const hashedPassword = await bcrypt.hash( + data.password, + config.security.bcryptRounds + ) - // Create user const user = await prisma.user.create({ data: { email: data.email, username: data.username, password: hashedPassword, - employeeId: data.employeeId, + employeeId: data.employeeId }, select: { id: true, @@ -28,154 +29,140 @@ class AuthService { username: true, employeeId: true, isActive: true, - createdAt: true, - }, - }); + createdAt: true + } + }) - // Generate tokens - const tokens = this.generateTokens(user.id, user.email); + const tokens = this.generateTokens(user.id, user.email) - // Save refresh token await prisma.user.update({ where: { id: user.id }, - data: { refreshToken: tokens.refreshToken }, - }); + data: { refreshToken: tokens.refreshToken } + }) return { user, - ...tokens, - }; + ...tokens + } } - - async login(email: string, password: string) { - const identifier = (email || '').toString().trim(); - const isEmail = identifier.includes('@'); + + async login(emailOrUsername: string, password: string) { + const identifier = (emailOrUsername || '').trim() + const isEmail = identifier.includes('@') - let user: any = null; + let user: any = null if (isEmail) { - // email may be duplicated => use findMany and validate - const users = await prisma.user.findMany({ + const matches = await prisma.user.findMany({ where: { email: identifier }, include: { employee: { include: { - position: { - include: { permissions: true }, - }, - department: true, - }, - }, - }, - }); + position: { include: { permissions: true } }, + department: true + } + } + } + }) - if (users.length === 0) { - throw new AppError(401, 'بيانات الدخول غير صحيحة - Invalid credentials'); + if (matches.length === 0) { + throw new AppError(401, 'بيانات الدخول غير صحيحة - Invalid credentials') } - if (users.length > 1) { + if (matches.length > 1) { throw new AppError( 400, - 'هذا البريد مستخدم لأكثر من حساب. الرجاء تسجيل الدخول باسم المستخدم - Email shared, use username' - ); + 'هذا البريد مستخدم لأكثر من حساب. الرجاء تسجيل الدخول باسم المستخدم - Use username' + ) } - user = users[0]; + user = matches[0] } else { - // username is unique => findUnique OK user = await prisma.user.findUnique({ where: { username: identifier }, include: { employee: { include: { - position: { - include: { permissions: true }, - }, - department: true, - }, - }, - }, - }); + position: { include: { permissions: true } }, + department: true + } + } + } + }) if (!user) { - throw new AppError(401, 'بيانات الدخول غير صحيحة - Invalid credentials'); + throw new AppError(401, 'بيانات الدخول غير صحيحة - Invalid credentials') } } - // Check if user is active if (!user.isActive) { - throw new AppError(403, 'الحساب غير مفعل - Account is inactive'); + throw new AppError(403, 'الحساب غير مفعل - Account is inactive') } - // Check if account is locked if (user.lockedUntil && user.lockedUntil > new Date()) { - throw new AppError(403, 'الحساب مقفل مؤقتاً - Account is temporarily locked'); + throw new AppError(403, 'الحساب مقفل مؤقتاً - Account is temporarily locked') } - // Verify password - const isPasswordValid = await bcrypt.compare(password, user.password); + const isPasswordValid = await bcrypt.compare(password, user.password) if (!isPasswordValid) { - // Increment failed login attempts - const failedAttempts = (user.failedLoginAttempts || 0) + 1; - const updateData: any = { failedLoginAttempts: failedAttempts }; + const failedAttempts = (user.failedLoginAttempts || 0) + 1 + const updateData: any = { failedLoginAttempts: failedAttempts } - // Lock account after 5 failed attempts if (failedAttempts >= 5) { - updateData.lockedUntil = new Date(Date.now() + 30 * 60 * 1000); // Lock for 30 minutes + updateData.lockedUntil = new Date(Date.now() + 30 * 60 * 1000) } await prisma.user.update({ where: { id: user.id }, - data: updateData, - }); + data: updateData + }) - throw new AppError(401, 'بيانات الدخول غير صحيحة - Invalid credentials'); + throw new AppError(401, 'بيانات الدخول غير صحيحة - Invalid credentials') } - // Check HR requirement: Must have active employee record if (!user.employee || user.employee.status !== 'ACTIVE') { - throw new AppError(403, 'الوصول مرفوض - Access denied. Active employee record required.'); + throw new AppError( + 403, + 'الوصول مرفوض - Access denied. Active employee record required.' + ) } - // Reset failed attempts await prisma.user.update({ where: { id: user.id }, data: { failedLoginAttempts: 0, lockedUntil: null, - lastLogin: new Date(), - }, - }); + lastLogin: new Date() + } + }) - // Generate tokens - const tokens = this.generateTokens(user.id, user.email); + const tokens = this.generateTokens(user.id, user.email) - // Save refresh token await prisma.user.update({ where: { id: user.id }, - data: { refreshToken: tokens.refreshToken }, - }); + data: { refreshToken: tokens.refreshToken } + }) - // Return user data without password, with role info - const { password: _, ...userWithoutPassword } = user; - - // Format role and permissions - const role = user.employee?.position ? { - id: user.employee.position.id, - name: user.employee.position.titleAr || user.employee.position.title, - nameEn: user.employee.position.title, - permissions: user.employee.position.permissions || [] - } : null; + const { password: _pw, ...userWithoutPassword } = user + + const role = user.employee?.position + ? { + id: user.employee.position.id, + name: user.employee.position.titleAr || user.employee.position.title, + nameEn: user.employee.position.title, + permissions: user.employee.position.permissions || [] + } + : null return { user: { ...userWithoutPassword, role }, - ...tokens, - }; + ...tokens + } } async getUserById(userId: string) { @@ -184,77 +171,57 @@ class AuthService { include: { employee: { include: { - position: { - include: { - permissions: true, - }, - }, - department: true, - }, - }, - }, - }); + position: { include: { permissions: true } }, + department: true + } + } + } + }) - if (!user) { - throw new AppError(404, 'المستخدم غير موجود - User not found'); - } + if (!user) throw new AppError(404, 'المستخدم غير موجود - User not found') + if (!user.isActive) throw new AppError(403, 'الحساب غير مفعل - Account is inactive') - if (!user.isActive) { - throw new AppError(403, 'الحساب غير مفعل - Account is inactive'); - } + const { password: _pw, ...userWithoutPassword } = user - // Format user data - const { password: _, ...userWithoutPassword } = user; - - const role = user.employee?.position ? { - id: user.employee.position.id, - name: user.employee.position.titleAr || user.employee.position.title, - nameEn: user.employee.position.title, - permissions: user.employee.position.permissions || [] - } : null; + const role = user.employee?.position + ? { + id: user.employee.position.id, + name: user.employee.position.titleAr || user.employee.position.title, + nameEn: user.employee.position.title, + permissions: user.employee.position.permissions || [] + } + : null - return { - ...userWithoutPassword, - role - }; + return { ...userWithoutPassword, role } } async refreshToken(refreshToken: string) { try { - const decoded = jwt.verify(refreshToken, config.jwt.secret) as { - id: string; - email: string; - }; - - // Verify refresh token matches stored token - const user = await prisma.user.findUnique({ - where: { id: decoded.id }, - }); + const decoded = jwt.verify(refreshToken, config.jwt.secret) as { id: string; email: string } + const user = await prisma.user.findUnique({ where: { id: decoded.id } }) if (!user || user.refreshToken !== refreshToken || !user.isActive) { - throw new AppError(401, 'رمز غير صالح - Invalid token'); + throw new AppError(401, 'رمز غير صالح - Invalid token') } - // Generate new tokens - const tokens = this.generateTokens(user.id, user.email); + const tokens = this.generateTokens(user.id, user.email) - // Update refresh token await prisma.user.update({ where: { id: user.id }, - data: { refreshToken: tokens.refreshToken }, - }); + data: { refreshToken: tokens.refreshToken } + }) - return tokens; - } catch (error) { - throw new AppError(401, 'رمز غير صالح - Invalid token'); + return tokens + } catch { + throw new AppError(401, 'رمز غير صالح - Invalid token') } } async logout(userId: string) { await prisma.user.update({ where: { id: userId }, - data: { refreshToken: null }, - }); + data: { refreshToken: null } + }) } async getUserProfile(userId: string) { @@ -268,46 +235,35 @@ class AuthService { lastLogin: true, employee: { include: { - position: { - include: { - permissions: true, - }, - }, - department: true, - }, - }, - }, - }); + position: { include: { permissions: true } }, + department: true + } + } + } + }) - if (!user) { - throw new AppError(404, 'المستخدم غير موجود - User not found'); - } - - return user; + if (!user) throw new AppError(404, 'المستخدم غير موجود - User not found') + return user } private generateTokens(userId: string, email: string) { - const payload = { id: userId, email }; - const secret = config.jwt.secret as Secret; - - const accessToken = jwt.sign( - payload, - secret, - { expiresIn: config.jwt.expiresIn } as SignOptions - ); + const payload = { id: userId, email } + const secret = config.jwt.secret as Secret - const refreshToken = jwt.sign( - payload, - secret, - { expiresIn: config.jwt.refreshExpiresIn } as SignOptions - ); + const accessToken = jwt.sign(payload, secret, { + expiresIn: config.jwt.expiresIn + } as SignOptions) + + const refreshToken = jwt.sign(payload, secret, { + expiresIn: config.jwt.refreshExpiresIn + } as SignOptions) return { accessToken, refreshToken, - expiresIn: config.jwt.expiresIn, - }; + expiresIn: config.jwt.expiresIn + } } } -export const authService = new AuthService(); \ No newline at end of file +export const authService = new AuthService() \ No newline at end of file