From b66ceee85c306e6af98cf5849e77118a3bd7844b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaime=20Jim=C3=A9nez?= Date: Sat, 4 Oct 2025 13:09:35 +0200 Subject: [PATCH] terminado sign up --- .../erp/common/email/EmailService.java | 62 +++++--- .../erp/login/LoginController.java | 16 ++- .../erp/login/SignupService.java | 29 +++- .../com/imprimelibros/erp/users/User.java | 3 +- .../com/imprimelibros/erp/users/UserDao.java | 9 ++ .../com/imprimelibros/erp/users/UserLite.java | 9 ++ src/main/resources/i18n/login_es.properties | 10 +- .../resources/static/assets/css/email.css | 51 +++++-- .../static/assets/images/logo-light-email.png | Bin 0 -> 10692 bytes .../templates/imprimelibros/email/layout.html | 120 ++++++++++------ .../templates/imprimelibros/email/verify.html | 52 ++++--- .../imprimelibros/login/_items/_login.html | 99 +++++++------ .../imprimelibros/login/_items/_signup.html | 89 ++++++------ .../templates/imprimelibros/login/login.html | 10 +- .../erp/verificationEmailTest.java | 132 ++++++++++++++++++ 15 files changed, 491 insertions(+), 200 deletions(-) create mode 100644 src/main/java/com/imprimelibros/erp/users/UserLite.java create mode 100644 src/main/resources/static/assets/images/logo-light-email.png create mode 100644 src/test/java/com/imprimelibros/erp/verificationEmailTest.java 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 7b13e8d..ead795e 100644 --- a/src/main/java/com/imprimelibros/erp/common/email/EmailService.java +++ b/src/main/java/com/imprimelibros/erp/common/email/EmailService.java @@ -4,12 +4,13 @@ import jakarta.mail.MessagingException; import jakarta.mail.internet.MimeMessage; import org.springframework.context.MessageSource; -import org.springframework.core.io.ClassPathResource; import org.springframework.mail.javamail.JavaMailSender; import org.springframework.mail.javamail.MimeMessageHelper; import org.springframework.stereotype.Service; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; import org.thymeleaf.TemplateEngine; import org.thymeleaf.context.Context; +import org.springframework.core.io.ClassPathResource; import java.util.Locale; import java.util.Map; @@ -32,44 +33,67 @@ public class EmailService { Map variables = Map.of( "fullName", fullName, "resetUrl", resetUrl); - sendEmail(to, subject, "imprimelibros/email/reset-password", variables); + sendEmail(to, subject, "imprimelibros/email/reset-password", variables, 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); + "verifyUrl", verifyUrl, + "minutes", 60); + sendEmail(to, subject, "imprimelibros/email/verify", variables, locale); } - // ->>>>>>>> PRIVATE METHODS <<<<<<<<<<<- /****************** * Envía un email usando una plantilla Thymeleaf. + * * @param to * @param subject * @param template * @param variables **********************************************/ - - private void sendEmail(String to, String subject, String template, Map variables) { + private void sendEmail(String to, String subject, String template, Map variables, Locale locale) { try { - Context context = new Context(); - context.setVariables(variables); - String html = templateEngine.process(template, context); + Context ctx = new Context(locale); + ctx.setVariables(variables); + ctx.setVariable("subject", subject); + ctx.setVariable("companyName", "ImprimeLibros"); + ctx.setVariable("year", java.time.Year.now().getValue()); - 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); + // Incrusta el CSS (no uses en emails) + var cssRes = new ClassPathResource("static/assets/css/email.css"); + String emailCss = org.springframework.util.StreamUtils.copyToString(cssRes.getInputStream(), + java.nio.charset.StandardCharsets.UTF_8); + ctx.setVariable("emailCss", emailCss); - mailSender.send(message); - } catch (MessagingException e) { - e.printStackTrace(); + ctx.setVariable("template", template); + String html = templateEngine.process("imprimelibros/email/layout", ctx); + + MimeMessage msg = mailSender.createMimeMessage(); + + MimeMessageHelper h = new MimeMessageHelper( + msg, + MimeMessageHelper.MULTIPART_MODE_MIXED_RELATED, + "UTF-8"); + + h.setFrom("no-reply@imprimelibros.com"); + h.setTo(to); + h.setSubject(subject); + + h.setText(html, true); + + // 3) ahora el inline, con content-type explícito + ClassPathResource logoRes = new ClassPathResource("static/assets/images/logo-light.png"); + h.addInline("logo", logoRes, "image/png"); + + mailSender.send(msg); + + } catch (Exception e) { + throw new RuntimeException("Error enviando email", e); } } + } diff --git a/src/main/java/com/imprimelibros/erp/login/LoginController.java b/src/main/java/com/imprimelibros/erp/login/LoginController.java index 2e574f1..545b39d 100644 --- a/src/main/java/com/imprimelibros/erp/login/LoginController.java +++ b/src/main/java/com/imprimelibros/erp/login/LoginController.java @@ -10,6 +10,7 @@ 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 org.springframework.context.MessageSource; import com.imprimelibros.erp.login.dto.SignupForm; @@ -19,9 +20,11 @@ import jakarta.validation.Valid; public class LoginController { private final SignupService signupService; + private final MessageSource messageSource; - public LoginController(SignupService signupService) { + public LoginController(SignupService signupService, MessageSource messageSource) { this.signupService = signupService; + this.messageSource = messageSource; } @GetMapping("/login") @@ -47,12 +50,13 @@ public class LoginController { if (br.hasErrors()) { ra.addFlashAttribute("org.springframework.validation.BindingResult.signupForm", br); ra.addFlashAttribute("signupForm", form); - ra.addFlashAttribute("signup_error", "Revisa el formulario"); + ra.addFlashAttribute("signup_error", + messageSource.getMessage("login.signup.error.review", null, locale)); return "redirect:/signup"; } try { signupService.register(form, locale); - ra.addFlashAttribute("info", "Te hemos enviado un email para confirmar tu correo."); + ra.addFlashAttribute("info", messageSource.getMessage("login.signup.success", null, locale)); return "redirect:/login"; } catch (IllegalArgumentException ex) { ra.addFlashAttribute("signup_error", ex.getMessage()); @@ -62,12 +66,12 @@ public class LoginController { } @GetMapping("/verify") - public String verify(@RequestParam("token") String token, RedirectAttributes ra) { + public String verify(@RequestParam("token") String token, RedirectAttributes ra, Locale locale) { boolean ok = signupService.verify(token); if (ok) { - ra.addFlashAttribute("info", "¡Cuenta verificada! Ya puedes iniciar sesión."); + ra.addFlashAttribute("info", messageSource.getMessage("login.signup.success.verified", null, locale)); } else { - ra.addFlashAttribute("danger", "Enlace inválido o caducado. Solicita uno nuevo."); + ra.addFlashAttribute("danger", messageSource.getMessage("login.signup.error.token.invalid", null, locale)); } 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 3ba08f0..87aea1b 100644 --- a/src/main/java/com/imprimelibros/erp/login/SignupService.java +++ b/src/main/java/com/imprimelibros/erp/login/SignupService.java @@ -8,6 +8,7 @@ import java.util.Map; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.servlet.support.ServletUriComponentsBuilder; +import org.springframework.context.MessageSource; import com.imprimelibros.erp.common.email.EmailService; import com.imprimelibros.erp.login.dto.SignupForm; @@ -26,6 +27,7 @@ public class SignupService { private final PasswordEncoder passwordEncoder; private final VerificationTokenRepository tokenRepository; private final EmailService emailService; + private final MessageSource messageSource; // minutos de validez del token private static final long TOKEN_MINUTES = 60; @@ -34,22 +36,40 @@ public class SignupService { RoleDao roleRepository, PasswordEncoder passwordEncoder, VerificationTokenRepository tokenRepository, - EmailService emailService) { + EmailService emailService, + MessageSource messageSource) { this.userRepository = userRepository; this.roleRepository = roleRepository; this.passwordEncoder = passwordEncoder; this.tokenRepository = tokenRepository; this.emailService = emailService; + this.messageSource = messageSource; } @Transactional public void register(SignupForm form, Locale locale) { if (!form.getPassword().equals(form.getPasswordConfirm())) { - throw new IllegalArgumentException("Las contraseñas no coinciden"); + throw new IllegalArgumentException( + messageSource.getMessage("login.signup.error.password.mismatch", null, locale)); } - if (userRepository.existsByUserNameIgnoreCase(form.getUsername())) { - throw new IllegalArgumentException("El correo ya está registrado"); + String email = form.getUsername().trim().toLowerCase(); + + var existingLite = userRepository.findLiteByUserNameIgnoreCase(email); + + if (existingLite.isPresent()) { + var lite = existingLite.get(); + + boolean isDeleted = lite.getDeleted(); + if (isDeleted) { + // caso: existe pero borrado + throw new IllegalArgumentException( + messageSource.getMessage("login.signup.error.email.exists-archived", null, locale)); + } else { + // caso: existe y no borrado + throw new IllegalArgumentException( + messageSource.getMessage("login.signup.error.email.exists", null, locale)); + } } // Crear usuario deshabilitado @@ -85,7 +105,6 @@ public class SignupService { locale); } - @Transactional public boolean verify(String tokenValue) { var tokenOpt = tokenRepository.findByToken(tokenValue); diff --git a/src/main/java/com/imprimelibros/erp/users/User.java b/src/main/java/com/imprimelibros/erp/users/User.java index 3576e4f..b60953f 100644 --- a/src/main/java/com/imprimelibros/erp/users/User.java +++ b/src/main/java/com/imprimelibros/erp/users/User.java @@ -57,7 +57,8 @@ public class User { @Column(name = "deleted_by") private Long deletedBy; - @OneToMany(mappedBy = "user", fetch = FetchType.LAZY, cascade = CascadeType.REMOVE, orphanRemoval = true) + @OneToMany(mappedBy = "user", fetch = FetchType.LAZY, cascade = { CascadeType.PERSIST, CascadeType.MERGE, + CascadeType.REMOVE }, orphanRemoval = true) @SQLRestriction("deleted = false") @JsonIgnore private Set rolesLink = new HashSet<>(); diff --git a/src/main/java/com/imprimelibros/erp/users/UserDao.java b/src/main/java/com/imprimelibros/erp/users/UserDao.java index 3afd2e9..cf62722 100644 --- a/src/main/java/com/imprimelibros/erp/users/UserDao.java +++ b/src/main/java/com/imprimelibros/erp/users/UserDao.java @@ -29,6 +29,15 @@ public interface UserDao extends JpaRepository, JpaSpecificationExec boolean existsByUserNameIgnoreCase(String userName); + // Para comprobar si existe al hacer signup + @Query(value = """ + SELECT id, deleted, enabled + FROM users + WHERE LOWER(username) = LOWER(:userName) + LIMIT 1 + """, nativeQuery = true) + Optional findLiteByUserNameIgnoreCase(@Param("userName") String userName); + boolean existsByUserNameIgnoreCaseAndIdNot(String userName, Long id); // Nuevo: para login/negocio "activo" diff --git a/src/main/java/com/imprimelibros/erp/users/UserLite.java b/src/main/java/com/imprimelibros/erp/users/UserLite.java new file mode 100644 index 0000000..0f752af --- /dev/null +++ b/src/main/java/com/imprimelibros/erp/users/UserLite.java @@ -0,0 +1,9 @@ +package com.imprimelibros.erp.users; + +// Proyección para consultas ligeras de usuarios (id, enabled, deleted) +public interface UserLite { + + Long getId(); + Boolean getDeleted(); + Boolean getEnabled(); +} diff --git a/src/main/resources/i18n/login_es.properties b/src/main/resources/i18n/login_es.properties index 42d2669..500bd37 100644 --- a/src/main/resources/i18n/login_es.properties +++ b/src/main/resources/i18n/login_es.properties @@ -18,4 +18,12 @@ 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 +login.error=Credenciales inválidas +login.signup.error.email.exists=El correo electrónico ya está en uso. +login.signup.error.email.exists-archived=El correo pertenece a una cuenta archivada. Por favor, contacta con soporte. +login.signup.error.password.mismatch=Las contraseñas no coinciden. +login.signup.error.review=Por favor, revisa el formulario. +login.signup.error.token.invalid=Enlace inválido o caducado. Solicita uno nuevo. + +login.signup.success=Cuenta creada con éxito. Por favor, revisa tu correo para activar tu cuenta. +login.signup.success.verified=¡Cuenta verificada! Ya puedes iniciar sesión. \ 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 012eb9c..331ce3b 100644 --- a/src/main/resources/static/assets/css/email.css +++ b/src/main/resources/static/assets/css/email.css @@ -1,52 +1,79 @@ -/* Email base */ +/* =========================== + Email base + =========================== */ + +/* Evita modo oscuro forzado */ +:root { + color-scheme: only light; +} + body { - font-family: Arial, Helvetica, sans-serif; margin: 0; padding: 0; - background-color: #f5f7fb; + font-family: Arial, Helvetica, sans-serif; + background-color: #f5f7fb !important; color: #333; } +/* Contenedor principal */ .email-wrapper { width: 100%; - background: #f5f7fb; - padding: 20px; + background-color: #f5f7fb !important; + padding: 20px 10px; } +/* Cuerpo centrado */ .email-body { width: 100%; + text-align: center; } +/* Tarjeta del correo */ .email-container { max-width: 600px; width: 100%; - background: #ffffff; margin: 0 auto; + background-color: #ffffff; border-radius: 8px; overflow: hidden; - box-shadow: 0 2px 6px rgba(0,0,0,0.08); + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08); } +/* Encabezado */ .email-header { - background: #f0f0f0; + background-color: #f0f0f0; padding: 20px; text-align: center; } +.email-header img { + display: block; + margin: 0 auto; +} + +/* Contenido */ .email-content { padding: 24px; font-size: 14px; line-height: 1.6; + color: #333; + text-align: left; } +/* Footer */ .email-footer { - background: #f9f9f9; + background-color: #f9f9f9; padding: 12px; text-align: center; font-size: 12px; color: #777; } +.email-footer a { + color: #2563eb; + text-decoration: none; +} + /* Botones */ .btn { display: inline-block; @@ -56,4 +83,10 @@ body { font-weight: bold; text-decoration: none; text-align: center; + background-color: #2563eb; + color: #fff !important; +} + +.btn:hover { + opacity: 0.9; } diff --git a/src/main/resources/static/assets/images/logo-light-email.png b/src/main/resources/static/assets/images/logo-light-email.png new file mode 100644 index 0000000000000000000000000000000000000000..e7ed384c7c33f8ccbe101f270ba95196127a375d GIT binary patch literal 10692 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 PB - - + - - - Notificación - + + + 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 index 3d51595..0db410d 100644 --- a/src/main/resources/templates/imprimelibros/email/verify.html +++ b/src/main/resources/templates/imprimelibros/email/verify.html @@ -1,25 +1,45 @@ - - - -

Hola usuario,

-

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

+ Hola + Usuario,

-

- - Verificar cuenta - +

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

-

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

+ + Verificar cuenta +

-

https://...

-

Este enlace caduca en 60 minutos.

- -

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

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

- - \ No newline at end of file +

+ https://... +

+ +

+ + Este enlace caduca en 60 minutos. + +

+ +

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

+ + diff --git a/src/main/resources/templates/imprimelibros/login/_items/_login.html b/src/main/resources/templates/imprimelibros/login/_items/_login.html index ca709fb..13479f0 100644 --- a/src/main/resources/templates/imprimelibros/login/_items/_login.html +++ b/src/main/resources/templates/imprimelibros/login/_items/_login.html @@ -1,63 +1,60 @@
-
-
-
¡Bienvenido!
-

Inicie sesión para continuar:

-
-
-
- - +
+
¡Bienvenido!
+

Inicie sesión para continuar:

+
-
- Credenciales inválidas +
+ + + + +
+ Credenciales inválidas +
+ +
+ + +
+ +
+ - -
- - + +
+ +
+
-
- - -
- - -
-
+
+ + +
-
- - -
- -
- -
+
+ +
- -
+ +
-
-

- ¿No tienes una cuenta? - - Regístrate - -

-
+
+

+ ¿No tienes una cuenta? + + Regístrate + +

\ 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 c8a4ac0..585d537 100644 --- a/src/main/resources/templates/imprimelibros/login/_items/_signup.html +++ b/src/main/resources/templates/imprimelibros/login/_items/_signup.html @@ -1,56 +1,51 @@
-
-
-
¡Bienvenido!
-

Crear cuenta

+ + +
+
¡Bienvenido!
+

Crear cuenta

+
+ + +
+ + + + + +
+ + +
-
-
-
+
+ + +
+
- - +
+ + +
+
- - - - -
- - -
+
+ + +
+
-
- - -
-
- -
- - -
-
- -
- - -
-
-
- -
- -
- -
+
+ +
+
\ No newline at end of file diff --git a/src/main/resources/templates/imprimelibros/login/login.html b/src/main/resources/templates/imprimelibros/login/login.html index ff5be3e..e3f3cda 100644 --- a/src/main/resources/templates/imprimelibros/login/login.html +++ b/src/main/resources/templates/imprimelibros/login/login.html @@ -37,7 +37,15 @@
-
+ +
+
+
+
+ +
+
diff --git a/src/test/java/com/imprimelibros/erp/verificationEmailTest.java b/src/test/java/com/imprimelibros/erp/verificationEmailTest.java new file mode 100644 index 0000000..ccaf2e8 --- /dev/null +++ b/src/test/java/com/imprimelibros/erp/verificationEmailTest.java @@ -0,0 +1,132 @@ +package com.imprimelibros.erp; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.File; +import java.io.FileOutputStream; +import java.nio.charset.StandardCharsets; +import java.time.Year; +import java.util.Locale; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.support.ResourceBundleMessageSource; +import org.springframework.core.io.ClassPathResource; +import org.springframework.util.StreamUtils; +import org.thymeleaf.context.Context; +import org.thymeleaf.spring6.SpringTemplateEngine; +import org.thymeleaf.templatemode.TemplateMode; +import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver; +import org.springframework.boot.test.context.SpringBootTest; + +import com.imprimelibros.erp.common.email.EmailService; + +@SpringBootTest +public class verificationEmailTest { + + @Autowired + private EmailService emailService; + + + private SpringTemplateEngine buildEngineForTests() { + ClassLoaderTemplateResolver resolver = new ClassLoaderTemplateResolver(); + resolver.setPrefix("templates/"); + resolver.setSuffix(".html"); + resolver.setTemplateMode(TemplateMode.HTML); + resolver.setCharacterEncoding("UTF-8"); + resolver.setCacheable(false); + + // MessageSource para tests (apunta a tus ficheros de la carpeta i18n) + ResourceBundleMessageSource ms = new ResourceBundleMessageSource(); + ms.setBasenames( + "i18n/app", + "i18n/auth", + "i18n/email", // <-- imprescindible para las claves email.* + "i18n/login", + "i18n/margenesPresupuesto", + "i18n/presupuesto", + "i18n/users", + "i18n/validation"); + ms.setDefaultEncoding("UTF-8"); + ms.setFallbackToSystemLocale(false); + + SpringTemplateEngine engine = new SpringTemplateEngine(); + engine.setTemplateResolver(resolver); + engine.setTemplateEngineMessageSource(ms); // <-- clave + + return engine; + } + + private String readClasspath(String path) throws Exception { + ClassPathResource res = new ClassPathResource(path); + return StreamUtils.copyToString(res.getInputStream(), StandardCharsets.UTF_8); + } + + @Test + void render_verify_email_should_include_content_and_white_card() throws Exception { + SpringTemplateEngine engine = buildEngineForTests(); + + Locale locale = Locale.forLanguageTag("es-ES"); + Context ctx = new Context(locale); + String verifyUrl = "https://imprimelibros.com/verify?token=TESTTOKEN"; + ctx.setVariable("subject", "Verifica tu correo"); + ctx.setVariable("companyName", "ImprimeLibros"); + ctx.setVariable("year", Year.now().getValue()); + ctx.setVariable("fullName", "Juan Pérez"); + ctx.setVariable("verifyUrl", verifyUrl); + ctx.setVariable("minutes", 60); + + // CSS embebido para el test (si lo usas) + String emailCss = readClasspath("static/assets/css/email.css"); + ctx.setVariable("emailCss", emailCss); + + // === Render === + String html = engine.process("imprimelibros/email/verify", ctx); + + // Vuelca siempre el HTML para inspección + File outDir = new File("target/email-test"); + outDir.mkdirs(); + File out = new File(outDir, "verify.html"); + try (FileOutputStream fos = new FileOutputStream(out)) { + fos.write(html.getBytes(StandardCharsets.UTF_8)); + } + System.out.println("📧 HTML generado en: " + out.getAbsolutePath()); + + // === Asserts robustos (sin depender de traducciones) === + + // 1) El slot se ha reemplazado (aparece la celda y hay contenido dentro) + assertThat(html) + .as("Debe existir la celda del contenido") + .contains("]*>.*?

.*?

.*?"); + + // 2) Debe aparecer la URL del botón de verificación + assertThat(html).as("El verifyUrl debe estar inyectado").contains(verifyUrl); + + // 3) Debe existir un enlace con clase 'btn' (el botón) + assertThat(html).as("Debe existir el botón con clase .btn").contains("