diff --git a/backend/src/modules/auditLogs/auditLog.interface.ts b/backend/src/modules/auditLogs/auditLog.interface.ts index d9bfef0..2b7a969 100644 --- a/backend/src/modules/auditLogs/auditLog.interface.ts +++ b/backend/src/modules/auditLogs/auditLog.interface.ts @@ -31,6 +31,7 @@ export interface IAuditLog { | "REJECT" | "LOGIN" | "LOGOUT" + | "CHANGE_PASSWORD" | "CLOSE"; recordId?: ObjectId; diff --git a/backend/src/modules/auditLogs/auditLog.model.ts b/backend/src/modules/auditLogs/auditLog.model.ts index dbf4c33..87f7832 100644 --- a/backend/src/modules/auditLogs/auditLog.model.ts +++ b/backend/src/modules/auditLogs/auditLog.model.ts @@ -70,6 +70,8 @@ const auditLogSchema = new mongoose.Schema( "LOGOUT", + "CHANGE_PASSWORD", + "CLOSE" ] diff --git a/backend/src/modules/auth/auth.controller.ts b/backend/src/modules/auth/auth.controller.ts index b671248..b43fa26 100644 --- a/backend/src/modules/auth/auth.controller.ts +++ b/backend/src/modules/auth/auth.controller.ts @@ -34,7 +34,7 @@ export class AuthController { } catch(error:any){ - res.status(401).json({ + res.status(500).json({ message:error.message }); @@ -58,4 +58,32 @@ export class AuthController { }); } } + + async changePassword( + req: Request, + res: Response + ) { + + try { + + const user = (req as any).user; + + const result = await authService.changePassword( + + user.id, + req.body + + ); + + res.status(200).json(result); + + } catch (error: any) { + + res.status(500).json({ + + message: error.message, + + }); + } + } } \ No newline at end of file diff --git a/backend/src/modules/auth/auth.middleware.ts b/backend/src/modules/auth/auth.middleware.ts index 1be21fb..88f3525 100644 --- a/backend/src/modules/auth/auth.middleware.ts +++ b/backend/src/modules/auth/auth.middleware.ts @@ -1,50 +1,74 @@ -import { Request, Response, NextFunction } from "express"; -import jwt from "jsonwebtoken"; +import { Request, Response, NextFunction } from "express"; -export const authMiddleware = ( - req:Request, - res:Response, - next:NextFunction +export const permissionMiddleware = ( + module: string, + action: string ) => { - const authHeader = req.headers.authorization; + return ( + req: Request, + res: Response, + next: NextFunction + ) => { - if(!authHeader){ - return res.status(401).json({ - message:"Token requerido" - }); - } + try { - //Verificar formato Bearer + const user = (req as any).user; - if(!authHeader.startsWith("Bearer ")) { - return res.status(401).json({ - message:"Formato de token inválido" - }); - } - const token = authHeader.split(" ")[1]; - if (!token) - { - return res.status(401).json({ - message:"Token inválido" - }); - } + if (!user) { - try { + return res.status(401).json({ + message: "Usuario no autenticado." + }); - const decoded = jwt.verify ( - token, - process.env.JWT_SECRET as string, - ); + } - (req as any).user = - decoded; - next(); - } catch{ - return res.status(401).json({ - message:"Token inválido" - }); - } + if (!user.permissions) { + + return res.status(403).json({ + message: "El usuario no tiene permisos asignados." + }); + + } + + + const hasPermission = user.permissions.some( + + (permission: any) => + + permission.module === module && + permission.action === action + + ); + + + if (!hasPermission) { + + return res.status(403).json({ + + message: "No tiene permisos para realizar esta acción." + + }); + + } + + + next(); + + } catch (error) { + + return res.status(500).json({ + + message: "Error al validar permisos.", + + error + + }); + + } + + }; + }; \ No newline at end of file diff --git a/backend/src/modules/auth/auth.service.ts b/backend/src/modules/auth/auth.service.ts index 8caf111..6dcdb32 100644 --- a/backend/src/modules/auth/auth.service.ts +++ b/backend/src/modules/auth/auth.service.ts @@ -3,13 +3,20 @@ import jwt from "jsonwebtoken"; import userModel from "../user/user.model"; import auditLogModel from "../auditLogs/auditLog.model"; +import { IChangePassword } from "./changePassword.interface"; + +import { RolePermissionService } from '../role-permissions/rolePermission.service'; + +const service = new RolePermissionService(); + export class AuthService { async login ( userName:string, password:string ){ - const user = await userModel.findOne({ + const user = + await userModel.findOne({ userName }).populate("roleId"); @@ -49,12 +56,27 @@ export class AuthService { const role = user.roleId as any; + const rolePermissions = await service.getByRole( + role._id.toString() + ); + + const permissions = rolePermissions ? + (rolePermissions.permissions as any []).map( + permission => ({ + name:permission.name, + module:permission.module, + action:permission.action + }) + ) + :[] + const token = jwt.sign( { id:user._id, - roleId: user.roleId, - roleName: role.roleName + roleId: role._id, + roleName: role.roleName, + permissions }, process.env.JWT_SECRET as string, @@ -76,6 +98,8 @@ export class AuthService { roleName: role.roleName, + permissions, + active: user.active } }; @@ -84,6 +108,12 @@ export class AuthService { async logout ( userId:string ){ + + if(!userId){ + + throw new Error("Usuario no autenticado."); + + } await auditLogModel.create({ userId, module:"Authentication", @@ -96,4 +126,71 @@ export class AuthService { message:"Sesión cerrada exitosamente" }; } + + async changePassword( + userId: string, + data: IChangePassword + ) { + + // Buscar usuario + const user = await userModel.findById(userId); + + if (!user) { + throw new Error("Usuario no encontrado."); + } + + // Validar contraseña actual + const validPassword = await bcrypt.compare( + data.currentPassword, + user.passwordHash + ); + + if (!validPassword) { + throw new Error("La contraseña actual es incorrecta."); + } + + // Validar que la nueva contraseña sea diferente + if (data.currentPassword === data.newPassword) { + throw new Error( + "La nueva contraseña debe ser diferente a la actual." + ); + } + + // Validar longitud mínima + if (data.newPassword.length < 8) { + throw new Error( + "La nueva contraseña debe tener al menos 8 caracteres." + ); + } + + // Encriptar nueva contraseña + user.passwordHash = await bcrypt.hash( + data.newPassword, + 10 + ); + + await user.save(); + + // Registrar en auditoría + await auditLogModel.create({ + + userId: user._id, + + module: "Authentication", + + action: "CHANGE_PASSWORD", + + description: "El usuario cambió su contraseña.", + + status: "EXITOSO" + + }); + + return { + + message: "Contraseña actualizada correctamente." + + }; + + } } \ No newline at end of file diff --git a/backend/src/modules/auth/changePassword.interface.ts b/backend/src/modules/auth/changePassword.interface.ts new file mode 100644 index 0000000..2027258 --- /dev/null +++ b/backend/src/modules/auth/changePassword.interface.ts @@ -0,0 +1,7 @@ +export interface IChangePassword { + + currentPassword: string; + + newPassword: string; + +} \ No newline at end of file