From 5da73a367988316d6e848f4d6342466259529c27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaime=20Jim=C3=A9nez?= Date: Fri, 3 Oct 2025 13:57:22 +0200 Subject: [PATCH] trabajando en el registro de usuarios --- .../erp/common/{ => email}/EmailService.java | 0 .../erp/config/SecurityConfig.java | 1 + .../erp/login/LoginController.java | 6 ++ .../erp/login/SignupService.java | 97 +++++++++++++++++++ .../erp/login/VerificationToken.java | 54 +++++++++++ .../login/VerificationTokenRepository.java | 9 ++ .../erp/login/dto/SignupForm.java | 25 +++++ src/main/resources/i18n/login_es.properties | 3 + src/main/resources/static/assets/css/app.css | 2 +- .../images/imprimelibros/cover-login.svg | 46 +++++++++ .../imprimelibros/login/_items/_login.html | 2 +- .../imprimelibros/login/_items/_signup.html | 53 ++++++++++ 12 files changed, 296 insertions(+), 2 deletions(-) rename src/main/java/com/imprimelibros/erp/common/{ => email}/EmailService.java (100%) create mode 100644 src/main/java/com/imprimelibros/erp/login/SignupService.java create mode 100644 src/main/java/com/imprimelibros/erp/login/VerificationToken.java create mode 100644 src/main/java/com/imprimelibros/erp/login/VerificationTokenRepository.java create mode 100644 src/main/java/com/imprimelibros/erp/login/dto/SignupForm.java create mode 100644 src/main/resources/static/assets/images/imprimelibros/cover-login.svg diff --git a/src/main/java/com/imprimelibros/erp/common/EmailService.java b/src/main/java/com/imprimelibros/erp/common/email/EmailService.java similarity index 100% rename from src/main/java/com/imprimelibros/erp/common/EmailService.java rename to src/main/java/com/imprimelibros/erp/common/email/EmailService.java diff --git a/src/main/java/com/imprimelibros/erp/config/SecurityConfig.java b/src/main/java/com/imprimelibros/erp/config/SecurityConfig.java index b8e1cab..836d0bc 100644 --- a/src/main/java/com/imprimelibros/erp/config/SecurityConfig.java +++ b/src/main/java/com/imprimelibros/erp/config/SecurityConfig.java @@ -124,6 +124,7 @@ public class SecurityConfig { .requestMatchers( "/", "/login", + "/signup", "/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 9a7d960..d3f2574 100644 --- a/src/main/java/com/imprimelibros/erp/login/LoginController.java +++ b/src/main/java/com/imprimelibros/erp/login/LoginController.java @@ -15,5 +15,11 @@ public class LoginController { return "imprimelibros/login/login"; } + @GetMapping("/signup") + public String signup(Model model, Locale locale) { + model.addAttribute("form", "_signup"); + return "imprimelibros/login/login"; + } + } diff --git a/src/main/java/com/imprimelibros/erp/login/SignupService.java b/src/main/java/com/imprimelibros/erp/login/SignupService.java new file mode 100644 index 0000000..eab319a --- /dev/null +++ b/src/main/java/com/imprimelibros/erp/login/SignupService.java @@ -0,0 +1,97 @@ +package com.imprimelibros.erp.login; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +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.User; +import com.imprimelibros.erp.users.UserDao; + +import org.springframework.security.crypto.password.PasswordEncoder; + +@Service +public class SignupService { + + private final UserDao userRepository; + private final PasswordEncoder passwordEncoder; + private final VerificationTokenRepository tokenRepository; + private final EmailService emailService; + + // minutos de validez del token + private static final long TOKEN_MINUTES = 60; + + public SignupService(UserDao userRepository, + PasswordEncoder passwordEncoder, + VerificationTokenRepository tokenRepository, + EmailService emailService) { + this.userRepository = userRepository; + this.passwordEncoder = passwordEncoder; + this.tokenRepository = tokenRepository; + this.emailService = emailService; + } + + @Transactional + public void register(SignupForm form) { + if (!form.getPassword().equals(form.getPasswordConfirm())) { + throw new IllegalArgumentException("Las contraseñas no coinciden"); + } + + if (userRepository.existsByUsername(form.getUsername())) { + throw new IllegalArgumentException("El correo ya está registrado"); + } + + // Crear usuario deshabilitado + User user = new User(); + user.setUsername(form.getUsername().trim().toLowerCase()); + user.setPassword(passwordEncoder.encode(form.getPassword())); + user.setEnabled(false); + // TODO: asignar rol por defecto si aplica (e.g., ROLE_USER) + user = userRepository.save(user); + + // Generar token + var token = VerificationToken.create(user.getId(), TOKEN_MINUTES); + tokenRepository.save(token); + + // Construir URL absoluta para /verify + String verifyUrl = ServletUriComponentsBuilder.fromCurrentContextPath() + .path("/verify") + .queryParam("token", token.getToken()) + .build() + .toUriString(); + + // Enviar correo + 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); + } + + @Transactional + public boolean verify(String tokenValue) { + var tokenOpt = tokenRepository.findByToken(tokenValue); + if (tokenOpt.isEmpty()) return false; + + var token = tokenOpt.get(); + if (token.isUsed() || token.isExpired()) return false; + + var user = userRepository.findById(token.getUserId()) + .orElseThrow(() -> new IllegalStateException("Usuario no encontrado para el token")); + + user.setEnabled(true); + userRepository.save(user); + + token.setUsedAt(java.time.LocalDateTime.now()); + tokenRepository.save(token); + + return true; + } +} diff --git a/src/main/java/com/imprimelibros/erp/login/VerificationToken.java b/src/main/java/com/imprimelibros/erp/login/VerificationToken.java new file mode 100644 index 0000000..d68efcd --- /dev/null +++ b/src/main/java/com/imprimelibros/erp/login/VerificationToken.java @@ -0,0 +1,54 @@ +package com.imprimelibros.erp.login; + +import java.time.LocalDateTime; +import java.util.UUID; + +import jakarta.persistence.*; + +@Entity +@Table(name = "verification_tokens", indexes = { + @Index(name = "idx_verification_token_token", columnList = "token", unique = true) +}) +public class VerificationToken { + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable=false, unique=true, length=64) + private String token; + + @Column(nullable=false) + private Long userId; + + @Column(nullable=false) + private LocalDateTime createdAt; + + @Column(nullable=false) + private LocalDateTime expiresAt; + + private LocalDateTime usedAt; + + public static VerificationToken create(Long userId, long minutesValid) { + VerificationToken t = new VerificationToken(); + t.token = UUID.randomUUID().toString().replace("-", ""); + t.userId = userId; + t.createdAt = LocalDateTime.now(); + t.expiresAt = t.createdAt.plusMinutes(minutesValid); + return t; + } + + // getters/setters + public Long getId() { return id; } + public String getToken() { return token; } + public void setToken(String token) { this.token = token; } + public Long getUserId() { return userId; } + public void setUserId(Long userId) { this.userId = userId; } + public LocalDateTime getCreatedAt() { return createdAt; } + public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; } + public LocalDateTime getExpiresAt() { return expiresAt; } + public void setExpiresAt(LocalDateTime expiresAt) { this.expiresAt = expiresAt; } + public LocalDateTime getUsedAt() { return usedAt; } + public void setUsedAt(LocalDateTime usedAt) { this.usedAt = usedAt; } + + public boolean isUsed() { return usedAt != null; } + public boolean isExpired() { return LocalDateTime.now().isAfter(expiresAt); } +} diff --git a/src/main/java/com/imprimelibros/erp/login/VerificationTokenRepository.java b/src/main/java/com/imprimelibros/erp/login/VerificationTokenRepository.java new file mode 100644 index 0000000..b9c8f45 --- /dev/null +++ b/src/main/java/com/imprimelibros/erp/login/VerificationTokenRepository.java @@ -0,0 +1,9 @@ +package com.imprimelibros.erp.login; + +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface VerificationTokenRepository extends JpaRepository { + Optional findByToken(String 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 new file mode 100644 index 0000000..b30c1ef --- /dev/null +++ b/src/main/java/com/imprimelibros/erp/login/dto/SignupForm.java @@ -0,0 +1,25 @@ +package com.imprimelibros.erp.login.dto; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +public class SignupForm { + @NotBlank @Email + private String username; + + @NotBlank @Size(min = 8, message = "La contraseña debe tener al menos 8 caracteres") + private String password; + + @NotBlank + private String passwordConfirm; + + // getters/setters + public String getUsername() { return username; } + public void setUsername(String u) { this.username = u; } + public String getPassword() { return password; } + public void setPassword(String p) { this.password = p; } + public String getPasswordConfirm() { return passwordConfirm; } + public void setPasswordConfirm(String pc) { this.passwordConfirm = pc; } +} + diff --git a/src/main/resources/i18n/login_es.properties b/src/main/resources/i18n/login_es.properties index eb2746e..da4922e 100644 --- a/src/main/resources/i18n/login_es.properties +++ b/src/main/resources/i18n/login_es.properties @@ -1,8 +1,10 @@ login.login=Iniciar sesión login.welcome=Bienvenido login.subtitle=Inicia sesión para continuar: +login.signup-subtitle=Regístrate para continuar: login.email=Correo electrónico login.password=Contraseña +login.confirm-password=Confirmar contraseña login.forgotPassword=¿Olvidaste tu contraseña? login.rememberMe=Recuérdame login.submit=Enviar @@ -12,5 +14,6 @@ login.email-placeholder=Introduce tu correo electrónico 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.error=Credenciales inválidas \ No newline at end of file diff --git a/src/main/resources/static/assets/css/app.css b/src/main/resources/static/assets/css/app.css index 0111cf3..ef5ddfd 100644 --- a/src/main/resources/static/assets/css/app.css +++ b/src/main/resources/static/assets/css/app.css @@ -12728,7 +12728,7 @@ span.flatpickr-weekday { /* Imagen de fondo completa */ .auth-bg-cover { - background: url("../images/cover-bg-login.png") center center / cover no-repeat; + background: url("../images/imprimelibros/cover-login.svg") center center / cover no-repeat; position: relative; z-index: 1; } diff --git a/src/main/resources/static/assets/images/imprimelibros/cover-login.svg b/src/main/resources/static/assets/images/imprimelibros/cover-login.svg new file mode 100644 index 0000000..5dd373e --- /dev/null +++ b/src/main/resources/static/assets/images/imprimelibros/cover-login.svg @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/templates/imprimelibros/login/_items/_login.html b/src/main/resources/templates/imprimelibros/login/_items/_login.html index 3c9afd1..ca709fb 100644 --- a/src/main/resources/templates/imprimelibros/login/_items/_login.html +++ b/src/main/resources/templates/imprimelibros/login/_items/_login.html @@ -53,7 +53,7 @@

¿No tienes una cuenta? - Regístrate diff --git a/src/main/resources/templates/imprimelibros/login/_items/_signup.html b/src/main/resources/templates/imprimelibros/login/_items/_signup.html index e69de29..a88a029 100644 --- a/src/main/resources/templates/imprimelibros/login/_items/_signup.html +++ b/src/main/resources/templates/imprimelibros/login/_items/_signup.html @@ -0,0 +1,53 @@ +

+
+
+
¡Bienvenido!
+

Regístrate para continuar:

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