feat: cambiar clave desde la app (usuario logueado)

Nuevo PUT /api/auth/change-password: exige la clave actual (a diferencia de
reset-password, que usa un token de correo sin sesion). Opcion "Cambiar clave"
agregada al menu de usuario (popup), disponible para cualquier rol.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Milton Gonzalez
2026-09-06 12:35:25 -05:00
parent a6824db421
commit 8c06d20720
11 changed files with 99 additions and 5 deletions
+6
View File
@@ -39,3 +39,9 @@ export const postResetPassword = asyncHandler(async (req, res) => {
await authService.resetPassword(token, password);
ok(res, { reset: true });
});
export const putChangePassword = asyncHandler(async (req, res) => {
const { currentPassword, newPassword } = req.body || {};
await authService.changePassword(req.currentUser.id, currentPassword, newPassword);
ok(res, { changed: true });
});
+2
View File
@@ -3,6 +3,7 @@ import rateLimit from 'express-rate-limit';
import { requireAuth } from '../../middleware/auth.js';
import {
postLogin, postLogout, getMe, postRegister, getVerifyEmail, postForgotPassword, postResetPassword,
putChangePassword,
} from './controller.js';
const router = Router();
@@ -22,5 +23,6 @@ router.post('/forgot-password', loginLimiter, postForgotPassword);
router.post('/reset-password', loginLimiter, postResetPassword);
router.post('/logout', requireAuth, postLogout);
router.get('/me', requireAuth, getMe);
router.put('/change-password', requireAuth, loginLimiter, putChangePassword);
export default router;
+17
View File
@@ -170,3 +170,20 @@ export async function resetPassword(token, newPassword) {
);
await logAudit({ userId: user.id, action: 'reset_password', entityType: 'user', entityId: user.id });
}
// Cambio de clave desde la app, con sesion activa (distinto del olvide-clave: exige la clave actual).
export async function changePassword(userId, currentPassword, newPassword) {
if (!currentPassword || !newPassword || newPassword.length < 8) {
throw new HttpError(400, 'La clave actual y una clave nueva de al menos 8 caracteres son obligatorias');
}
const [rows] = await pool.query('SELECT password_hash FROM users WHERE id = ?', [userId]);
const user = rows[0];
if (!user) throw new HttpError(404, 'Usuario no encontrado');
const validPassword = await bcrypt.compare(currentPassword, user.password_hash);
if (!validPassword) throw new HttpError(400, 'La clave actual no es correcta');
const passwordHash = await bcrypt.hash(newPassword, 12);
await pool.query('UPDATE users SET password_hash = ? WHERE id = ?', [passwordHash, userId]);
await logAudit({ userId, action: 'change_password', entityType: 'user', entityId: userId });
}