fix(change-password): return updated user via record outcome w/ flush
CI / Build Native (push) Successful in 7m49s

User.getEntityManager().refresh() requiere una tx activa, y el
caller (AuthResource.changePassword) no es @Transactional -> ese
approach tiraba TransactionRequiredException.

En lugar de refrescar en el caller, cambio AuthService.changePassword
para devolver la User ya actualizada en un record
ChangePasswordOutcome, despues de un flush() explicito para que los
cambios lleguen a DB antes de cerrar la tx. Asi el response del
endpoint refleja el estado real de la fila.
This commit is contained in:
2026-08-15 13:30:26 -04:00
parent 7b76bde15b
commit 0ef2991bcd
2 changed files with 17 additions and 13 deletions
@@ -91,16 +91,14 @@ public class AuthResource {
Optional<User> user = authService.getEntityFromToken(jwt.get());
if (user.isEmpty()) return Response.status(401).build();
User current = user.get();
AuthService.ChangePasswordResult result = authService.changePassword(
AuthService.ChangePasswordOutcome outcome = authService.changePassword(
current.id, body.currentPassword(), body.newPassword());
switch (result) {
switch (outcome.result()) {
case OK:
// Refetch fresh desde DB porque la version cacheada en el
// persistence context tenia mustChangePassword=true (estado
// previo al cambio). Mejor un read fresco.
User refreshed = User.findById(current.id);
if (refreshed == null) return Response.status(401).build();
User.getEntityManager().refresh(refreshed);
// outcome.updatedUser() viene de la propia tx con flush() hecho,
// asi que mustChangePassword=false y todas las columnas reflejan
// el estado actual de la DB.
User refreshed = outcome.updatedUser();
String newToken = authService.buildToken(refreshed.id);
return Response.ok(AuthService.toAuthMe(refreshed))
.cookie(buildAuthCookie(newToken))
@@ -95,19 +95,22 @@ public class AuthService {
}
@Transactional
public ChangePasswordResult changePassword(UUID userId, String currentPassword, String newPassword) {
public ChangePasswordOutcome changePassword(UUID userId, String currentPassword, String newPassword) {
User user = User.findById(userId);
if (user == null) return ChangePasswordResult.NOT_FOUND;
if (user == null) return new ChangePasswordOutcome(ChangePasswordResult.NOT_FOUND, null);
if (currentPassword == null || !BcryptUtil.matches(currentPassword, user.passwordHash)) {
return ChangePasswordResult.WRONG_CURRENT_PASSWORD;
return new ChangePasswordOutcome(ChangePasswordResult.WRONG_CURRENT_PASSWORD, null);
}
if (newPassword == null || newPassword.length() < MIN_PASSWORD_LENGTH) {
return ChangePasswordResult.WEAK_NEW_PASSWORD;
return new ChangePasswordOutcome(ChangePasswordResult.WEAK_NEW_PASSWORD, null);
}
user.passwordHash = BcryptUtil.bcryptHash(newPassword);
user.mustChangePassword = false;
user.persist();
return ChangePasswordResult.OK;
// Forzar flush para que los cambios lleguen a DB antes de salir de la tx.
// Asi el caller (que NO esta en una tx) puede leer el estado actualizado.
User.getEntityManager().flush();
return new ChangePasswordOutcome(ChangePasswordResult.OK, user);
}
@Transactional
@@ -141,4 +144,7 @@ public class AuthService {
public static AuthMeResponse toAuthMe(User user) {
return new AuthMeResponse(user.id, user.username, user.createdAt, user.mustChangePassword, user.isAdmin);
}
public record ChangePasswordOutcome(ChangePasswordResult result, User updatedUser) {
}
}