diff --git a/src/main/java/com/imprimelibros/erp/common/email/EmailService.java b/src/main/java/com/imprimelibros/erp/common/email/EmailService.java index f9d02d4..7b13e8d 100644 --- a/src/main/java/com/imprimelibros/erp/common/email/EmailService.java +++ b/src/main/java/com/imprimelibros/erp/common/email/EmailService.java @@ -27,28 +27,49 @@ public class EmailService { this.messageSource = messageSource; } - public void sendPasswordResetMail(String to, String username, String resetLink, Locale locale) throws MessagingException { - MimeMessage message = mailSender.createMimeMessage(); - MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8"); + public void sendPasswordResetMail(String to, String fullName, String resetUrl, Locale locale) { + String subject = messageSource.getMessage("email.resetPassword.title", null, locale); + Map variables = Map.of( + "fullName", fullName, + "resetUrl", resetUrl); + sendEmail(to, subject, "imprimelibros/email/reset-password", variables); + } - helper.setFrom("no-reply@imprimelibros.com"); - helper.setTo(to); - helper.setSubject(messageSource.getMessage("email.resetPassword.title", null, locale)); + public void sendVerificationEmail(String to, String fullName, String verifyUrl, Locale locale) { + String subject = messageSource.getMessage("email.verify.title", null, locale); + Map variables = Map.of( + "fullName", fullName, + "verifyUrl", verifyUrl); + sendEmail(to, subject, "imprimelibros/email/verify", variables); + } - // Variables para la plantilla - Context context = new Context(); - context.setVariables(Map.of( - "username", username, - "resetLink", resetLink, - "year", String.valueOf(java.time.Year.now().getValue()) - )); - // Procesar plantilla HTML - String html = templateEngine.process("email/password-reset", context); - helper.setText(html, true); + // ->>>>>>>> PRIVATE METHODS <<<<<<<<<<<- - helper.addInline("companyLogo", new ClassPathResource("static/images/logo-light.png")); + /****************** + * Envía un email usando una plantilla Thymeleaf. + * @param to + * @param subject + * @param template + * @param variables + **********************************************/ - mailSender.send(message); + private void sendEmail(String to, String subject, String template, Map variables) { + try { + Context context = new Context(); + context.setVariables(variables); + String html = templateEngine.process(template, context); + + MimeMessage message = mailSender.createMimeMessage(); + MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8"); + helper.setFrom("no-reply@imprimelibros.com"); + helper.setTo(to); + helper.setSubject(subject); + helper.setText(html, true); + + mailSender.send(message); + } catch (MessagingException e) { + e.printStackTrace(); + } } } diff --git a/src/main/java/com/imprimelibros/erp/config/SecurityConfig.java b/src/main/java/com/imprimelibros/erp/config/SecurityConfig.java index 836d0bc..b9c5cd5 100644 --- a/src/main/java/com/imprimelibros/erp/config/SecurityConfig.java +++ b/src/main/java/com/imprimelibros/erp/config/SecurityConfig.java @@ -125,6 +125,8 @@ public class SecurityConfig { "/", "/login", "/signup", + "/verify", + "/reset-password", "/assets/**", "/css/**", "/js/**", diff --git a/src/main/java/com/imprimelibros/erp/login/LoginController.java b/src/main/java/com/imprimelibros/erp/login/LoginController.java index d3f2574..2e574f1 100644 --- a/src/main/java/com/imprimelibros/erp/login/LoginController.java +++ b/src/main/java/com/imprimelibros/erp/login/LoginController.java @@ -4,11 +4,26 @@ import java.util.Locale; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; +import org.springframework.validation.BindingResult; 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.RequestParam; +import org.springframework.web.servlet.mvc.support.RedirectAttributes; + +import com.imprimelibros.erp.login.dto.SignupForm; + +import jakarta.validation.Valid; @Controller public class LoginController { + private final SignupService signupService; + + public LoginController(SignupService signupService) { + this.signupService = signupService; + } + @GetMapping("/login") public String index(Model model, Locale locale) { model.addAttribute("form", "_login"); @@ -17,9 +32,44 @@ public class LoginController { @GetMapping("/signup") public String signup(Model model, Locale locale) { + if (!model.containsAttribute("signupForm")) { + model.addAttribute("signupForm", new SignupForm()); + } model.addAttribute("form", "_signup"); return "imprimelibros/login/login"; } + @PostMapping("/signup") + public String doSignup(@Valid @ModelAttribute("signupForm") SignupForm form, + BindingResult br, + RedirectAttributes ra, + Locale locale) { + if (br.hasErrors()) { + ra.addFlashAttribute("org.springframework.validation.BindingResult.signupForm", br); + ra.addFlashAttribute("signupForm", form); + ra.addFlashAttribute("signup_error", "Revisa el formulario"); + return "redirect:/signup"; + } + try { + signupService.register(form, locale); + ra.addFlashAttribute("info", "Te hemos enviado un email para confirmar tu correo."); + return "redirect:/login"; + } catch (IllegalArgumentException ex) { + ra.addFlashAttribute("signup_error", ex.getMessage()); + ra.addFlashAttribute("signupForm", form); + return "redirect:/signup"; + } + } + + @GetMapping("/verify") + public String verify(@RequestParam("token") String token, RedirectAttributes ra) { + boolean ok = signupService.verify(token); + if (ok) { + ra.addFlashAttribute("info", "¡Cuenta verificada! Ya puedes iniciar sesión."); + } else { + ra.addFlashAttribute("danger", "Enlace inválido o caducado. Solicita uno nuevo."); + } + return "redirect:/login"; + } } diff --git a/src/main/java/com/imprimelibros/erp/login/SignupService.java b/src/main/java/com/imprimelibros/erp/login/SignupService.java index eab319a..3ba08f0 100644 --- a/src/main/java/com/imprimelibros/erp/login/SignupService.java +++ b/src/main/java/com/imprimelibros/erp/login/SignupService.java @@ -1,6 +1,8 @@ package com.imprimelibros.erp.login; import java.util.HashMap; +import java.util.HashSet; +import java.util.Locale; import java.util.Map; import org.springframework.stereotype.Service; @@ -9,6 +11,8 @@ import org.springframework.web.servlet.support.ServletUriComponentsBuilder; import com.imprimelibros.erp.common.email.EmailService; import com.imprimelibros.erp.login.dto.SignupForm; +import com.imprimelibros.erp.users.Role; +import com.imprimelibros.erp.users.RoleDao; import com.imprimelibros.erp.users.User; import com.imprimelibros.erp.users.UserDao; @@ -18,6 +22,7 @@ import org.springframework.security.crypto.password.PasswordEncoder; public class SignupService { private final UserDao userRepository; + private final RoleDao roleRepository; private final PasswordEncoder passwordEncoder; private final VerificationTokenRepository tokenRepository; private final EmailService emailService; @@ -26,31 +31,36 @@ public class SignupService { private static final long TOKEN_MINUTES = 60; public SignupService(UserDao userRepository, - PasswordEncoder passwordEncoder, - VerificationTokenRepository tokenRepository, - EmailService emailService) { + RoleDao roleRepository, + PasswordEncoder passwordEncoder, + VerificationTokenRepository tokenRepository, + EmailService emailService) { this.userRepository = userRepository; + this.roleRepository = roleRepository; this.passwordEncoder = passwordEncoder; this.tokenRepository = tokenRepository; this.emailService = emailService; } @Transactional - public void register(SignupForm form) { + public void register(SignupForm form, Locale locale) { if (!form.getPassword().equals(form.getPasswordConfirm())) { throw new IllegalArgumentException("Las contraseñas no coinciden"); } - if (userRepository.existsByUsername(form.getUsername())) { + if (userRepository.existsByUserNameIgnoreCase(form.getUsername())) { throw new IllegalArgumentException("El correo ya está registrado"); } // Crear usuario deshabilitado User user = new User(); - user.setUsername(form.getUsername().trim().toLowerCase()); + user.setUserName(form.getUsername().trim().toLowerCase()); + user.setFullName(form.getName().trim()); user.setPassword(passwordEncoder.encode(form.getPassword())); user.setEnabled(false); - // TODO: asignar rol por defecto si aplica (e.g., ROLE_USER) + var roles = new HashSet(); + roles.add(roleRepository.findRoleByName("USER").orElseThrow()); + user.setRoles(roles); user = userRepository.save(user); // Generar token @@ -68,20 +78,23 @@ public class SignupService { Map model = new HashMap<>(); model.put("verifyUrl", verifyUrl); model.put("minutes", TOKEN_MINUTES); - emailService.sendTemplate( - user.getUsername(), - "Confirma tu correo | ImprimeLibros ERP", - "mail/verify-email", - model); + emailService.sendVerificationEmail( + user.getUserName(), + user.getFullName(), + verifyUrl, + locale); } + @Transactional public boolean verify(String tokenValue) { var tokenOpt = tokenRepository.findByToken(tokenValue); - if (tokenOpt.isEmpty()) return false; + if (tokenOpt.isEmpty()) + return false; var token = tokenOpt.get(); - if (token.isUsed() || token.isExpired()) return false; + if (token.isUsed() || token.isExpired()) + return false; var user = userRepository.findById(token.getUserId()) .orElseThrow(() -> new IllegalStateException("Usuario no encontrado para el token")); diff --git a/src/main/java/com/imprimelibros/erp/login/dto/SignupForm.java b/src/main/java/com/imprimelibros/erp/login/dto/SignupForm.java index b30c1ef..fc3bc5d 100644 --- a/src/main/java/com/imprimelibros/erp/login/dto/SignupForm.java +++ b/src/main/java/com/imprimelibros/erp/login/dto/SignupForm.java @@ -5,18 +5,26 @@ import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; public class SignupForm { - @NotBlank @Email + + @NotBlank(message = "{usuarios.error.correo}") + @Email(message = "{usuarios.error.correo.invalido}") private String username; - @NotBlank @Size(min = 8, message = "La contraseña debe tener al menos 8 caracteres") + @NotBlank(message = "{usuarios.error.nombre}") + private String name; + + @NotBlank(message = "{usuarios.error.contraseña}") + @Size(min = 6, message = "{usuarios.error.contraseña.tamaño}") private String password; - @NotBlank + @NotBlank(message = "{usuarios.error.confirmPassword.requerida}") private String passwordConfirm; // getters/setters public String getUsername() { return username; } public void setUsername(String u) { this.username = u; } + public String getName() { return name; } + public void setName(String n) { this.name = n; } public String getPassword() { return password; } public void setPassword(String p) { this.password = p; } public String getPasswordConfirm() { return passwordConfirm; } diff --git a/src/main/resources/i18n/email_es.properties b/src/main/resources/i18n/email_es.properties index ee041d7..8d5fa18 100644 --- a/src/main/resources/i18n/email_es.properties +++ b/src/main/resources/i18n/email_es.properties @@ -1,6 +1,8 @@ -email.resetPassword.title=Restablecer contraseña email.greeting=Hola -email.resetPassword.body=Hemos recibido una solicitud para restablecer tu contraseña. Haz clic en el siguiente botón: -email.resetPassword.button=Restablecer contraseña -email.resetPassword.ignoreMessage=Si no solicitaste este cambio, puedes ignorar este mensaje. +email.verify.title=Verifica tu correo +email.verify.body=Haz clic en el siguiente botón para verificar tu correo electrónico: +email.verify.button=Verificar cuenta +email.verify.link-instruction=Si no funciona, copia y pega esta URL en tu navegador: +email.verify.expiration=Este enlace caduca en {0} minutos. +email.verify.ignoreMessage=Si no solicitaste este cambio, puedes ignorar este mensaje. email.footer=Imprimelibros - Todos los derechos reservados. diff --git a/src/main/resources/i18n/login_es.properties b/src/main/resources/i18n/login_es.properties index da4922e..42d2669 100644 --- a/src/main/resources/i18n/login_es.properties +++ b/src/main/resources/i18n/login_es.properties @@ -15,5 +15,7 @@ login.password-placeholder=Introduce tu contraseña login.new-account=¿No tienes una cuenta? login.sign-up=Regístrate login.sign-up-button=Crear cuenta +login.sign-up.title=Crear una cuenta +login.sign-up.name=Nombre completo login.error=Credenciales inválidas \ No newline at end of file diff --git a/src/main/resources/static/assets/css/email.css b/src/main/resources/static/assets/css/email.css index 20ca139..012eb9c 100644 --- a/src/main/resources/static/assets/css/email.css +++ b/src/main/resources/static/assets/css/email.css @@ -1,35 +1,59 @@ +/* Email base */ body { - font-family: Arial, sans-serif; - background: #f8f9fa; - padding: 20px; + font-family: Arial, Helvetica, sans-serif; + margin: 0; + padding: 0; + background-color: #f5f7fb; + color: #333; } -.container { - background: #fff; - border-radius: 8px; - padding: 20px; - max-width: 600px; - margin: auto; - box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1); +.email-wrapper { + width: 100%; + background: #f5f7fb; + padding: 20px; } -.header { - text-align: center; - margin-bottom: 20px; +.email-body { + width: 100%; } +.email-container { + max-width: 600px; + width: 100%; + background: #ffffff; + margin: 0 auto; + border-radius: 8px; + overflow: hidden; + box-shadow: 0 2px 6px rgba(0,0,0,0.08); +} + +.email-header { + background: #f0f0f0; + padding: 20px; + text-align: center; +} + +.email-content { + padding: 24px; + font-size: 14px; + line-height: 1.6; +} + +.email-footer { + background: #f9f9f9; + padding: 12px; + text-align: center; + font-size: 12px; + color: #777; +} + +/* Botones */ .btn { - display: inline-block; - padding: 10px 20px; - background: #0d6efd; - color: #fff; - text-decoration: none; - border-radius: 5px; + display: inline-block; + padding: 12px 20px; + border-radius: 6px; + font-size: 14px; + font-weight: bold; + text-decoration: none; + text-align: center; } - -.footer { - margin-top: 20px; - font-size: 12px; - color: #6c757d; - text-align: center; -} \ No newline at end of file diff --git a/src/main/resources/templates/imprimelibros/email/layout.html b/src/main/resources/templates/imprimelibros/email/layout.html new file mode 100644 index 0000000..877b5d9 --- /dev/null +++ b/src/main/resources/templates/imprimelibros/email/layout.html @@ -0,0 +1,52 @@ + + + + + + + Notificación + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/templates/imprimelibros/email/verify.html b/src/main/resources/templates/imprimelibros/email/verify.html new file mode 100644 index 0000000..3d51595 --- /dev/null +++ b/src/main/resources/templates/imprimelibros/email/verify.html @@ -0,0 +1,25 @@ + + + + +

Hola usuario,

+

Haz clic en el siguiente botón para verificar tu correo electrónico: +

+ +

+ + Verificar cuenta + +

+ +

Si no funciona, copia y pega esta URL en tu navegador: +

+

https://...

+ +

Este enlace caduca en 60 minutos.

+ +

Si no solicitaste este cambio, puedes ignorar este mensaje. +

+ + + \ No newline at end of file diff --git a/src/main/resources/templates/imprimelibros/login/_items/_signup.html b/src/main/resources/templates/imprimelibros/login/_items/_signup.html index a88a029..c8a4ac0 100644 --- a/src/main/resources/templates/imprimelibros/login/_items/_signup.html +++ b/src/main/resources/templates/imprimelibros/login/_items/_signup.html @@ -1,53 +1,56 @@
+
¡Bienvenido!
-

Regístrate para continuar:

+

Crear cuenta

-
-
- - +
+
+
-
- Credenciales inválidas + + + + + + + +
+ + +
+
+ +
+ + +
+
+ +
+ + +
+
+ +
+ + +
+
-
- - -
- -
- -
- - -
-
- -
- -
- - -
-
- -
- -
- - -
+
+ +
+
\ No newline at end of file