trabajando en el registro de usuarios

This commit is contained in:
2025-10-03 13:57:22 +02:00
parent 1e24065fb7
commit 5da73a3679
12 changed files with 296 additions and 2 deletions

View File

@ -124,6 +124,7 @@ public class SecurityConfig {
.requestMatchers(
"/",
"/login",
"/signup",
"/assets/**",
"/css/**",
"/js/**",

View File

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

View File

@ -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<String, Object> 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;
}
}

View File

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

View File

@ -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<VerificationToken, Long> {
Optional<VerificationToken> findByToken(String token);
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" viewBox="0 0 6860 4000">
<!-- Generator: Adobe Illustrator 29.8.1, SVG Export Plug-In . SVG Version: 2.1.1 Build 2) -->
<defs>
<style>
.st0 {
fill: url(#Degradado_sin_nombre_5);
}
.st0, .st1, .st2 {
fill-rule: evenodd;
}
.st3 {
opacity: .8;
}
.st1 {
fill: url(#Degradado_sin_nombre_50);
}
.st2 {
fill: url(#Degradado_sin_nombre_46);
}
</style>
<linearGradient id="Degradado_sin_nombre_5" data-name="Degradado sin nombre 5" x1="-671.7" y1="-3131.4" x2="7415.5" y2="2118.3" gradientTransform="translate(171.7 2310.8) scale(.9)" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#fff"/>
<stop offset="1" stop-color="#b5c5c6"/>
</linearGradient>
<linearGradient id="Degradado_sin_nombre_46" data-name="Degradado sin nombre 46" x1="408.2" y1="1897.1" x2="7495.3" y2="4618.6" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#b5c5c6"/>
<stop offset="1" stop-color="#fff" stop-opacity="0"/>
</linearGradient>
<linearGradient id="Degradado_sin_nombre_50" data-name="Degradado sin nombre 50" x1="2641.5" y1="2000" x2="6860" y2="2000" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#b5c5c6"/>
<stop offset="1" stop-color="#fff" stop-opacity="0"/>
</linearGradient>
</defs>
<polygon class="st0" points="0 0 6860 0 6860 4000 0 4000 0 0"/>
<g class="st3">
<path class="st2" d="M6860,2115v1885H0c863.4-2588.5,4628.1-623.4,6860-1885h0Z"/>
</g>
<g class="st3">
<path class="st1" d="M2641.5,0h4218.5v4000C5495.1,1201.7,3984.8,2928.1,2641.5,0Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -53,7 +53,7 @@
<div class="mt-5 text-center">
<p class="mb-0">
<span th:text="#{login.new-account}">¿No tienes una cuenta?</span>
<a th:href="@{/auth-signup-cover}" class="fw-semibold text-primary text-decoration-underline"
<a href="/signup" class="fw-semibold text-primary text-decoration-underline"
th:text="#{login.sign-up}">
Regístrate
</a>

View File

@ -0,0 +1,53 @@
<div th:fragment="_signup">
<div class="p-lg-5 p-4">
<div>
<h5 class="text-primary" th:text="#{login.welcome}">¡Bienvenido!</h5>
<p class="text-muted" th:text="#{login.signup-subtitle}">Regístrate para continuar:</p>
</div>
<div class="mt-4">
<form th:action="@{/signup}" method="post">
<!-- CSRF obligatorio -->
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
<div th:if="${param.error}" class="alert alert-danger"
th:text="#{login.error}">
Credenciales inválidas
</div>
<div class="mb-3">
<label for="username" class="form-label" th:text="#{login.email}">Correo electrónico</label>
<input type="email" class="form-control" id="username" th:placeholder="#{login.email-placeholder}"
name="username">
</div>
<div class="mb-3">
<label class="form-label" for="password-input" th:text="#{login.password}">Contraseña</label>
<div class="position-relative auth-pass-inputgroup mb-3">
<input type="password" class="form-control pe-5 password-input"
th:placeholder="#{login.password-placeholder}" id="password-input" name="password">
<button
class="btn btn-link position-absolute end-0 top-0 text-decoration-none text-muted password-addon"
type="button" id="password-addon"><i class="ri-eye-fill align-middle"></i></button>
</div>
</div>
<div class="mb-3">
<label class="form-label" for="password-confirm-input" th:text="#{login.confirm-password}">Confirmar contraseña</label>
<div class="position-relative auth-pass-inputgroup mb-3">
<input type="password" class="form-control pe-5 password-input"
th:placeholder="#{login.password-placeholder}" id="password-confirm-input" name="password-confirm">
<button
class="btn btn-link position-absolute end-0 top-0 text-decoration-none text-muted password-addon"
type="button" id="password-addon"><i class="ri-eye-fill align-middle"></i></button>
</div>
</div>
<div class="mt-4">
<button class="btn btn-secondary w-100" type="submit" th:text="#{login.sign-up-button}">Crear cuenta</button>
</div>
</form>
</div>
</div>
</div>