diff --git a/src/main/java/com/imprimelibros/erp/users/ProfileController.java b/src/main/java/com/imprimelibros/erp/users/ProfileController.java new file mode 100644 index 0000000..a43fbd4 --- /dev/null +++ b/src/main/java/com/imprimelibros/erp/users/ProfileController.java @@ -0,0 +1,155 @@ +package com.imprimelibros.erp.users; + +import java.util.Locale; + +import org.springframework.context.MessageSource; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.Authentication; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.validation.BindingResult; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.servlet.mvc.support.RedirectAttributes; + +import com.imprimelibros.erp.config.Sanitizer; +import com.imprimelibros.erp.users.validation.ProfileForm; + +@Controller +@RequestMapping("/pages-profile") +@PreAuthorize("isAuthenticated()") +public class ProfileController { + + private final UserDao userDao; + private final PasswordEncoder passwordEncoder; + private final MessageSource messageSource; + private final Sanitizer sanitizer; + + public ProfileController(UserDao userDao, PasswordEncoder passwordEncoder, + MessageSource messageSource, Sanitizer sanitizer) { + this.userDao = userDao; + this.passwordEncoder = passwordEncoder; + this.messageSource = messageSource; + this.sanitizer = sanitizer; + } + + @GetMapping + public String view( + Authentication authentication, + @RequestParam(name = "success", required = false) String success, + Model model, + Locale locale) { + + if (authentication == null) { + return "redirect:/login"; + } + + User user = userDao.findByUserNameIgnoreCase(authentication.getName()).orElse(null); + if (user == null) { + return "redirect:/login"; + } + + ProfileForm form = new ProfileForm(); + form.setId(user.getId()); + form.setFullName(user.getFullName()); + form.setUserName(user.getUserName()); + + model.addAttribute("user", form); + model.addAttribute("success", success != null); + return "imprimelibros/users/profile"; + } + + @PostMapping + public String update( + Authentication authentication, + @Validated @ModelAttribute("user") ProfileForm form, + BindingResult binding, + Model model, + RedirectAttributes redirectAttributes, + Locale locale) { + + if (authentication == null) { + return "redirect:/login"; + } + + User user = userDao.findByUserNameIgnoreCase(authentication.getName()).orElse(null); + if (user == null) { + return "redirect:/login"; + } + + String normalized = sanitizer.plain(form.getUserName()); + if (normalized != null) { + normalized = normalized.trim().toLowerCase(); + } + + if (normalized == null || normalized.isBlank()) { + binding.rejectValue("userName", "usuarios.error.email", + messageSource.getMessage("usuarios.error.email", null, locale)); + } else if (userDao.existsByUserNameIgnoreCaseAndIdNot(normalized, user.getId())) { + binding.rejectValue("userName", "usuarios.error.duplicado", + messageSource.getMessage("usuarios.error.duplicado", null, locale)); + } + + String cleanName = sanitizer.plain(form.getFullName()); + if (cleanName == null || cleanName.isBlank()) { + binding.rejectValue("fullName", "usuarios.error.nombre", + messageSource.getMessage("usuarios.error.nombre", null, locale)); + } + + boolean wantsPasswordChange = hasText(form.getCurrentPassword()) + || hasText(form.getNewPassword()) + || hasText(form.getConfirmPassword()); + + if (wantsPasswordChange) { + if (!hasText(form.getCurrentPassword())) { + binding.rejectValue("currentPassword", "usuarios.error.password.actual", + messageSource.getMessage("usuarios.error.password.actual", null, locale)); + } else if (!passwordEncoder.matches(form.getCurrentPassword(), user.getPassword())) { + binding.rejectValue("currentPassword", "usuarios.error.password.actual.incorrecta", + messageSource.getMessage("usuarios.error.password.actual.incorrecta", null, locale)); + } + + if (!hasText(form.getNewPassword())) { + binding.rejectValue("newPassword", "usuarios.error.password.nueva.requerida", + messageSource.getMessage("usuarios.error.password.nueva.requerida", null, locale)); + } else if (form.getNewPassword().length() < 6) { + binding.rejectValue("newPassword", "usuarios.error.password.min", + messageSource.getMessage("usuarios.error.password.min", null, locale)); + } + + if (!hasText(form.getConfirmPassword())) { + binding.rejectValue("confirmPassword", "usuarios.error.confirmPassword.requerida", + messageSource.getMessage("usuarios.error.confirmPassword.requerida", null, locale)); + } else if (hasText(form.getNewPassword()) && !form.getNewPassword().equals(form.getConfirmPassword())) { + binding.rejectValue("confirmPassword", "usuarios.error.password-coinciden", + messageSource.getMessage("usuarios.error.password-coinciden", null, locale)); + } + } + + if (binding.hasErrors()) { + model.addAttribute("success", false); + return "imprimelibros/users/profile"; + } + + user.setFullName(cleanName.trim()); + user.setUserName(normalized); + + if (wantsPasswordChange) { + user.setPassword(passwordEncoder.encode(form.getNewPassword())); + } + + userDao.save(user); + + redirectAttributes.addAttribute("success", "1"); + return "redirect:/pages-profile"; + } + + private static boolean hasText(String value) { + return value != null && !value.isBlank(); + } +} diff --git a/src/main/java/com/imprimelibros/erp/users/validation/ProfileForm.java b/src/main/java/com/imprimelibros/erp/users/validation/ProfileForm.java new file mode 100644 index 0000000..0055444 --- /dev/null +++ b/src/main/java/com/imprimelibros/erp/users/validation/ProfileForm.java @@ -0,0 +1,68 @@ +package com.imprimelibros.erp.users.validation; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; + +public class ProfileForm { + + private Long id; + + @NotBlank(message = "{usuarios.error.nombre}") + private String fullName; + + @NotBlank(message = "{usuarios.error.email}") + @Email(message = "{usuarios.error.email.formato}") + private String userName; + + private String currentPassword; + private String newPassword; + private String confirmPassword; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getFullName() { + return fullName; + } + + public void setFullName(String fullName) { + this.fullName = fullName; + } + + public String getUserName() { + return userName; + } + + public void setUserName(String userName) { + this.userName = userName; + } + + public String getCurrentPassword() { + return currentPassword; + } + + public void setCurrentPassword(String currentPassword) { + this.currentPassword = currentPassword; + } + + public String getNewPassword() { + return newPassword; + } + + public void setNewPassword(String newPassword) { + this.newPassword = newPassword; + } + + public String getConfirmPassword() { + return confirmPassword; + } + + public void setConfirmPassword(String confirmPassword) { + this.confirmPassword = confirmPassword; + } +} diff --git a/src/main/resources/i18n/users_en.properties b/src/main/resources/i18n/users_en.properties index 73daca1..9f1b17e 100644 --- a/src/main/resources/i18n/users_en.properties +++ b/src/main/resources/i18n/users_en.properties @@ -1,3 +1,23 @@ +usuarios.form.nombre=Full name +usuarios.form.email=Email +usuarios.form.confirmarPassword=Confirm password +usuarios.form.password.actual=Current password +usuarios.form.password.nueva=New password +usuarios.form.password.nota=You can only change the password if you provide the current one. + +usuarios.error.nombre=Name is required. +usuarios.error.email=Email is required. +usuarios.error.email.formato=Email is not valid. +usuarios.error.password.min=Password must be at least 6 characters. +usuarios.error.password.actual=Current password is required. +usuarios.error.password.actual.incorrecta=Current password is not correct. +usuarios.error.password.nueva.requerida=New password is required. +usuarios.error.confirmPassword.requerida=Password confirmation is required. +usuarios.error.password-coinciden=Passwords do not match. +usuarios.error.duplicado=There is already a user with that email. + usuarios.impersonate.title=Sign in as user usuarios.impersonate.text=You are about to sign in as {0}. You can return to your user from the menu. usuarios.impersonate.button=Continue +usuarios.profile.title=Edit profile +usuarios.profile.success=Profile updated successfully. diff --git a/src/main/resources/i18n/users_es.properties b/src/main/resources/i18n/users_es.properties index c528825..fcd1b40 100644 --- a/src/main/resources/i18n/users_es.properties +++ b/src/main/resources/i18n/users_es.properties @@ -20,6 +20,9 @@ usuarios.form.nombre=Nombre completo usuarios.form.email=Correo electrónico usuarios.form.password=Contraseña usuarios.form.confirmarPassword=Confirmar contraseña +usuarios.form.password.actual=Contraseña actual +usuarios.form.password.nueva=Nueva contraseña +usuarios.form.password.nota=Solo podrás cambiar la contraseña si indicas la actual. usuarios.form.rol=Rol usuarios.form.estado=Estado @@ -37,6 +40,9 @@ usuarios.error.email.formato=El correo electrónico no es válido. usuarios.error.rol=El rol seleccionado no es válido. usuarios.error.password.requerida=La contraseña es obligatoria. usuarios.error.password.min=La contraseña debe tener al menos 6 caracteres. +usuarios.error.password.actual=La contraseña actual es obligatoria. +usuarios.error.password.actual.incorrecta=La contraseña actual no es correcta. +usuarios.error.password.nueva.requerida=La nueva contraseña es obligatoria. usuarios.error.confirmPassword.requerida=La confirmación de la contraseña es obligatoria. usuarios.error.password-coinciden=Las contraseñas no coinciden. usuarios.error.delete-relational-data=No se puede eliminar el usuario porque tiene datos relacionados. @@ -55,5 +61,7 @@ usuarios.delete.text=¿Está seguro de que desea eliminar al usuario?
Esta ac usuarios.delete.ok.title=Usuario eliminado usuarios.delete.ok.text=El usuario ha sido eliminado con éxito. usuarios.impersonate.title=Entrar como usuario -usuarios.impersonate.text=Vas a iniciar sesión como {0}. Podrás volver a tu usuario desde el menú. +usuarios.impersonate.text=Vas a iniciar sesión como {0}. Podrás volver a tu usuario desde el menú. usuarios.impersonate.button=Entrar +usuarios.profile.title=Editar perfil +usuarios.profile.success=Perfil actualizado correctamente. diff --git a/src/main/resources/templates/imprimelibros/partials/topbar.html b/src/main/resources/templates/imprimelibros/partials/topbar.html index a8f5dc1..f58ea4a 100644 --- a/src/main/resources/templates/imprimelibros/partials/topbar.html +++ b/src/main/resources/templates/imprimelibros/partials/topbar.html @@ -100,9 +100,6 @@ Perfil - - Mensajes
+ + + + + + + + +
+
+ + +
+ + + +
+
+
+
+
+
Editar perfil
+
+
+ +
Perfil actualizado.
+ +
+ +
+
+
+ +
+ + +
Error
+
+ +
+ + +
Error
+
+ +
+ +
+ + +
Error
+
+ +
+ + +
+ Solo podrás cambiar la contraseña si indicas la actual. +
+
Error
+
+ +
+ + +
Error
+
+ +
+ +
+
+
+
+
+
+
+
+ + + + + + +