mirror of
https://git.imnavajas.es/jjimenez/erp-imprimelibros.git
synced 2026-01-12 16:38:48 +00:00
trabajando en el registro de usuarios
This commit is contained in:
@ -124,6 +124,7 @@ public class SecurityConfig {
|
||||
.requestMatchers(
|
||||
"/",
|
||||
"/login",
|
||||
"/signup",
|
||||
"/assets/**",
|
||||
"/css/**",
|
||||
"/js/**",
|
||||
|
||||
@ -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";
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
97
src/main/java/com/imprimelibros/erp/login/SignupService.java
Normal file
97
src/main/java/com/imprimelibros/erp/login/SignupService.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -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); }
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
@ -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; }
|
||||
}
|
||||
|
||||
@ -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
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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 |
@ -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>
|
||||
|
||||
@ -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>
|
||||
Reference in New Issue
Block a user