fix login

This commit is contained in:
yotakii
2026-03-05 12:16:29 +03:00
parent 625bc26a05
commit 6d82c5007c
2 changed files with 133 additions and 177 deletions

View File

@@ -1,6 +1,6 @@
import { Request, Response } from 'express' import { Request, Response } from 'express'
import { authService } from './auth.service' import { authService } from './auth.service'
import { AuthRequest } from '@/shared/middleware/auth' import { AuthRequest } from '../../shared/middleware/auth'
export const authController = { export const authController = {
register: async (req: Request, res: Response) => { register: async (req: Request, res: Response) => {
@@ -21,7 +21,7 @@ export const authController = {
login: async (req: Request, res: Response) => { login: async (req: Request, res: Response) => {
try { try {
const { email, password } = req.body const { email, password } = req.body
if (!email || !password) { 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({ res.status(200).json({
success: true, success: true,
@@ -39,9 +39,9 @@ export const authController = {
data: result data: result
}) })
} catch (error: any) { } catch (error: any) {
res.status(401).json({ res.status(error?.statusCode || 401).json({
success: false, success: false,
message: error.message message: error.message || 'بيانات الدخول غير صحيحة'
}) })
} }
}, },

View File

@@ -1,26 +1,27 @@
import bcrypt from 'bcryptjs'; import bcrypt from 'bcryptjs'
import jwt, { Secret, SignOptions } from 'jsonwebtoken'; import jwt, { Secret, SignOptions } from 'jsonwebtoken'
import prisma from '../../config/database'; import prisma from '../../config/database'
import { config } from '../../config'; import { config } from '../../config'
import { AppError } from '../../shared/middleware/errorHandler'; import { AppError } from '../../shared/middleware/errorHandler'
class AuthService { class AuthService {
async register(data: { async register(data: {
email: string; email: string
username: string; username: string
password: string; password: string
employeeId?: string; employeeId?: string
}) { }) {
// Hash password const hashedPassword = await bcrypt.hash(
const hashedPassword = await bcrypt.hash(data.password, config.security.bcryptRounds); data.password,
config.security.bcryptRounds
)
// Create user
const user = await prisma.user.create({ const user = await prisma.user.create({
data: { data: {
email: data.email, email: data.email,
username: data.username, username: data.username,
password: hashedPassword, password: hashedPassword,
employeeId: data.employeeId, employeeId: data.employeeId
}, },
select: { select: {
id: true, id: true,
@@ -28,154 +29,140 @@ class AuthService {
username: true, username: true,
employeeId: true, employeeId: true,
isActive: 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({ await prisma.user.update({
where: { id: user.id }, where: { id: user.id },
data: { refreshToken: tokens.refreshToken }, data: { refreshToken: tokens.refreshToken }
}); })
return { return {
user, user,
...tokens, ...tokens
}; }
} }
async login(email: string, password: string) { async login(emailOrUsername: string, password: string) {
const identifier = (email || '').toString().trim(); const identifier = (emailOrUsername || '').trim()
const isEmail = identifier.includes('@'); const isEmail = identifier.includes('@')
let user: any = null; let user: any = null
if (isEmail) { if (isEmail) {
// email may be duplicated => use findMany and validate const matches = await prisma.user.findMany({
const users = await prisma.user.findMany({
where: { email: identifier }, where: { email: identifier },
include: { include: {
employee: { employee: {
include: { include: {
position: { position: { include: { permissions: true } },
include: { permissions: true }, department: true
}, }
department: true, }
}, }
}, })
},
});
if (users.length === 0) { if (matches.length === 0) {
throw new AppError(401, 'بيانات الدخول غير صحيحة - Invalid credentials'); throw new AppError(401, 'بيانات الدخول غير صحيحة - Invalid credentials')
} }
if (users.length > 1) { if (matches.length > 1) {
throw new AppError( throw new AppError(
400, 400,
'هذا البريد مستخدم لأكثر من حساب. الرجاء تسجيل الدخول باسم المستخدم - Email shared, use username' 'هذا البريد مستخدم لأكثر من حساب. الرجاء تسجيل الدخول باسم المستخدم - Use username'
); )
} }
user = users[0]; user = matches[0]
} else { } else {
// username is unique => findUnique OK
user = await prisma.user.findUnique({ user = await prisma.user.findUnique({
where: { username: identifier }, where: { username: identifier },
include: { include: {
employee: { employee: {
include: { include: {
position: { position: { include: { permissions: true } },
include: { permissions: true }, department: true
}, }
department: true, }
}, }
}, })
},
});
if (!user) { if (!user) {
throw new AppError(401, 'بيانات الدخول غير صحيحة - Invalid credentials'); throw new AppError(401, 'بيانات الدخول غير صحيحة - Invalid credentials')
} }
} }
// Check if user is active
if (!user.isActive) { 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()) { 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) { if (!isPasswordValid) {
// Increment failed login attempts const failedAttempts = (user.failedLoginAttempts || 0) + 1
const failedAttempts = (user.failedLoginAttempts || 0) + 1; const updateData: any = { failedLoginAttempts: failedAttempts }
const updateData: any = { failedLoginAttempts: failedAttempts };
// Lock account after 5 failed attempts
if (failedAttempts >= 5) { 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({ await prisma.user.update({
where: { id: user.id }, 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') { 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({ await prisma.user.update({
where: { id: user.id }, where: { id: user.id },
data: { data: {
failedLoginAttempts: 0, failedLoginAttempts: 0,
lockedUntil: null, 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({ await prisma.user.update({
where: { id: user.id }, where: { id: user.id },
data: { refreshToken: tokens.refreshToken }, data: { refreshToken: tokens.refreshToken }
}); })
// Return user data without password, with role info const { password: _pw, ...userWithoutPassword } = user
const { password: _, ...userWithoutPassword } = user;
const role = user.employee?.position
// Format role and permissions ? {
const role = user.employee?.position ? { id: user.employee.position.id,
id: user.employee.position.id, name: user.employee.position.titleAr || user.employee.position.title,
name: user.employee.position.titleAr || user.employee.position.title, nameEn: user.employee.position.title,
nameEn: user.employee.position.title, permissions: user.employee.position.permissions || []
permissions: user.employee.position.permissions || [] }
} : null; : null
return { return {
user: { user: {
...userWithoutPassword, ...userWithoutPassword,
role role
}, },
...tokens, ...tokens
}; }
} }
async getUserById(userId: string) { async getUserById(userId: string) {
@@ -184,77 +171,57 @@ class AuthService {
include: { include: {
employee: { employee: {
include: { include: {
position: { position: { include: { permissions: true } },
include: { department: true
permissions: true, }
}, }
}, }
department: true, })
},
},
},
});
if (!user) { if (!user) throw new AppError(404, 'المستخدم غير موجود - User not found')
throw new AppError(404, 'المستخدم غير موجود - User not found'); if (!user.isActive) throw new AppError(403, 'الحساب غير مفعل - Account is inactive')
}
if (!user.isActive) { const { password: _pw, ...userWithoutPassword } = user
throw new AppError(403, 'الحساب غير مفعل - Account is inactive');
}
// Format user data const role = user.employee?.position
const { password: _, ...userWithoutPassword } = user; ? {
id: user.employee.position.id,
const role = user.employee?.position ? { name: user.employee.position.titleAr || user.employee.position.title,
id: user.employee.position.id, nameEn: user.employee.position.title,
name: user.employee.position.titleAr || user.employee.position.title, permissions: user.employee.position.permissions || []
nameEn: user.employee.position.title, }
permissions: user.employee.position.permissions || [] : null
} : null;
return { return { ...userWithoutPassword, role }
...userWithoutPassword,
role
};
} }
async refreshToken(refreshToken: string) { async refreshToken(refreshToken: string) {
try { try {
const decoded = jwt.verify(refreshToken, config.jwt.secret) as { const decoded = jwt.verify(refreshToken, config.jwt.secret) as { id: string; email: string }
id: string;
email: string;
};
// Verify refresh token matches stored token
const user = await prisma.user.findUnique({
where: { id: decoded.id },
});
const user = await prisma.user.findUnique({ where: { id: decoded.id } })
if (!user || user.refreshToken !== refreshToken || !user.isActive) { 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({ await prisma.user.update({
where: { id: user.id }, where: { id: user.id },
data: { refreshToken: tokens.refreshToken }, data: { refreshToken: tokens.refreshToken }
}); })
return tokens; return tokens
} catch (error) { } catch {
throw new AppError(401, 'رمز غير صالح - Invalid token'); throw new AppError(401, 'رمز غير صالح - Invalid token')
} }
} }
async logout(userId: string) { async logout(userId: string) {
await prisma.user.update({ await prisma.user.update({
where: { id: userId }, where: { id: userId },
data: { refreshToken: null }, data: { refreshToken: null }
}); })
} }
async getUserProfile(userId: string) { async getUserProfile(userId: string) {
@@ -268,46 +235,35 @@ class AuthService {
lastLogin: true, lastLogin: true,
employee: { employee: {
include: { include: {
position: { position: { include: { permissions: true } },
include: { department: true
permissions: true, }
}, }
}, }
department: true, })
},
},
},
});
if (!user) { if (!user) throw new AppError(404, 'المستخدم غير موجود - User not found')
throw new AppError(404, 'المستخدم غير موجود - User not found'); return user
}
return user;
} }
private generateTokens(userId: string, email: string) { private generateTokens(userId: string, email: string) {
const payload = { id: userId, email }; const payload = { id: userId, email }
const secret = config.jwt.secret as Secret; const secret = config.jwt.secret as Secret
const accessToken = jwt.sign(
payload,
secret,
{ expiresIn: config.jwt.expiresIn } as SignOptions
);
const refreshToken = jwt.sign( const accessToken = jwt.sign(payload, secret, {
payload, expiresIn: config.jwt.expiresIn
secret, } as SignOptions)
{ expiresIn: config.jwt.refreshExpiresIn } as SignOptions
); const refreshToken = jwt.sign(payload, secret, {
expiresIn: config.jwt.refreshExpiresIn
} as SignOptions)
return { return {
accessToken, accessToken,
refreshToken, refreshToken,
expiresIn: config.jwt.expiresIn, expiresIn: config.jwt.expiresIn
}; }
} }
} }
export const authService = new AuthService(); export const authService = new AuthService()