terminado sign up

This commit is contained in:
2025-10-04 13:09:35 +02:00
parent d9c4f16cf0
commit b66ceee85c
15 changed files with 491 additions and 200 deletions

View File

@ -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<String, Object> 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<String, Object> 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<String, Object> variables) {
private void sendEmail(String to, String subject, String template, Map<String, Object> 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 <link> 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);
}
}
}

View File

@ -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";
}

View File

@ -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);

View File

@ -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<UserRole> rolesLink = new HashSet<>();

View File

@ -29,6 +29,15 @@ public interface UserDao extends JpaRepository<User, Long>, 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<UserLite> findLiteByUserNameIgnoreCase(@Param("userName") String userName);
boolean existsByUserNameIgnoreCaseAndIdNot(String userName, Long id);
// Nuevo: para login/negocio "activo"

View File

@ -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();
}