diff --git a/src/main/java/com/imprimelibros/erp/auth/PasswordResetController.java b/src/main/java/com/imprimelibros/erp/auth/PasswordResetController.java index 1fee1b9..9c585f0 100644 --- a/src/main/java/com/imprimelibros/erp/auth/PasswordResetController.java +++ b/src/main/java/com/imprimelibros/erp/auth/PasswordResetController.java @@ -1,134 +1,92 @@ package com.imprimelibros.erp.auth; -import com.imprimelibros.erp.common.RateLimiterService; -import com.imprimelibros.erp.common.email.EmailService; - -import jakarta.servlet.http.HttpServletRequest; +import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; +import java.util.Locale; + import org.springframework.context.MessageSource; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.mvc.support.RedirectAttributes; -import com.imprimelibros.erp.users.UserDao; - -import java.util.Locale; -import java.util.Map; +import jakarta.servlet.http.HttpServletRequest; @Controller -@RequestMapping("/password") +@RequestMapping("/auth/password") @Validated public class PasswordResetController { - private final PasswordResetService resetService; - private final EmailService emailService; // tu servicio de correo HTML - private final UserDao userRepo; // tu repo de usuarios - private final RateLimiterService rateLimiter; + private final PasswordResetService service; private final MessageSource messageSource; - public PasswordResetController(PasswordResetService resetService, - EmailService emailService, - UserDao userRepo, - RateLimiterService rateLimiter, - MessageSource messageSource) { - this.resetService = resetService; - this.emailService = emailService; - this.userRepo = userRepo; - this.rateLimiter = rateLimiter; + public PasswordResetController(PasswordResetService service, MessageSource messageSource) { + this.service = service; this.messageSource = messageSource; } - /** Endpoint para solicitar el email de reseteo (respuesta neutra y rate limiting) */ - @PostMapping("/request") - @ResponseBody - public ResponseEntity requestReset(@RequestParam("email") String email, HttpServletRequest req, Locale locale) { - String clientIp = extractClientIp(req); - - // RATE LIMIT: 5/15min por IP -> si se supera, 429 con mensaje neutro - if (!rateLimiter.tryConsume("reset:" + clientIp)) { - // No revelamos nada concreto - return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS) - .body(Map.of("message", - messageSource.getMessage("auth.reset.request.success", null, locale))); - } - - userRepo.findByUserNameIgnoreCase(email).ifPresent(user -> { - String token = resetService.createTokenForUser( - user.getId(), - 1, // caduca en 1h - clientIp, - req.getHeader("User-Agent") - ); - String resetLink = buildResetLink(req, token); - try { - emailService.sendPasswordResetMail(user.getUserName(), user.getFullName(), resetLink, locale); - } catch (Exception ignored) { - // Deliberadamente no variamos la respuesta para no filtrar info - } - }); - - // Respuesta neutra SIEMPRE en 200 (si no fue 429) - return ResponseEntity.ok(Map.of("message", - messageSource.getMessage("auth.reset.request.success", null, locale))); + // 4.1 Página "¿Olvidaste tu contraseña?" + @GetMapping("/forgot") + public String forgotForm(Model model, Locale locale) { + model.addAttribute("form", "_forgot-password"); + return "imprimelibros/login/login"; } - /** Muestra formulario si el token es válido */ + // 4.2 Envío del email (si existe) + @PostMapping("/forgot") + public String handleForgot( + @RequestParam @NotBlank @Email String username, + HttpServletRequest request, + Model model, + RedirectAttributes ra, + Locale locale) { + String baseUrl = request.getScheme() + "://" + request.getServerName() + + (request.getServerPort() == 80 || request.getServerPort() == 443 ? "" + : ":" + request.getServerPort()); + String ip = request.getRemoteAddr(); + String ua = request.getHeader("User-Agent"); + service.requestReset(username, baseUrl, ip, ua, 60, locale); + + ra.addFlashAttribute("info", messageSource.getMessage("login.password-recovery.email-info", null, locale)); + return "redirect:/login"; + } + + // 4.3 Formulario de nueva contraseña (a partir del enlace) @GetMapping("/reset") - public String showResetForm(@RequestParam("token") String token, Model model) { - Long userId = resetService.validateTokenAndGetUserId(token); - if (userId == null) { - model.addAttribute("invalidToken", true); - return "auth/reset-invalid"; - } - model.addAttribute("token", token); - return "auth/reset-form"; - } - - /** Procesa y guarda nueva contraseña */ - @PostMapping("/reset") - public String handleReset( + public String resetForm(@RequestParam("uid") Long uid, @RequestParam("token") String token, - @RequestParam("password") @NotBlank String password, - @RequestParam("confirmPassword") @NotBlank String confirmPassword, - Model model, Locale locale - ) { - if (!password.equals(confirmPassword)) { + Model model, Locale locale) { + boolean ok = service.isValid(uid, token); + model.addAttribute("uid", uid); + model.addAttribute("token", token); + model.addAttribute("valid", ok); + + model.addAttribute("form", "_reset-password"); + return "imprimelibros/login/login"; + } + + // 4.4 Confirmación del reseteo + @PostMapping("/reset") + public String doReset(@RequestParam("uid") Long uid, + @RequestParam("token") String token, + @RequestParam("password") String password, + @RequestParam("password2") String password2, + Model model, Locale locale) { + if (!password.equals(password2) || password.length() < 8) { + model.addAttribute("uid", uid); model.addAttribute("token", token); - model.addAttribute("error", messageSource.getMessage("auth.reset.form.passwordsMismatch", null, locale)); - return "auth/reset-form"; + model.addAttribute("danger", messageSource.getMessage("login.password-reset.error", null, locale)); + model.addAttribute("form", "_reset-password"); + return "imprimelibros/login/login"; } - - try { - resetService.consumeTokenAndSetPassword(token, password); - return "redirect:/login?resetOk"; - } catch (IllegalArgumentException ex) { - model.addAttribute("invalidToken", true); - return "auth/reset-invalid"; + if (service.resetPassword(uid, token, password)) { + model.addAttribute("info", messageSource.getMessage("login.password-reset.success", null, locale)); + } else { + model.addAttribute("danger", messageSource.getMessage("login.password-reset.error-link", null, locale)); } - } - - private static String extractClientIp(HttpServletRequest req) { - // Soporte detrás de proxy - String xff = req.getHeader("X-Forwarded-For"); - if (xff != null && !xff.isBlank()) { - // coger el primer IP de la cadena - return xff.split(",")[0].trim(); - } - String realIp = req.getHeader("X-Real-IP"); - if (realIp != null && !realIp.isBlank()) return realIp.trim(); - return req.getRemoteAddr(); - } - - private static String buildResetLink(HttpServletRequest req, String token) { - String scheme = req.getHeader("X-Forwarded-Proto"); - if (scheme == null || scheme.isBlank()) scheme = req.getScheme(); // http/https - String host = req.getHeader("Host"); // respeta el host público bajo proxy - if (host == null || host.isBlank()) host = req.getServerName() + (req.getServerPort() != 80 && req.getServerPort() != 443 ? ":" + req.getServerPort() : ""); - String ctx = req.getContextPath() == null ? "" : req.getContextPath(); - return scheme + "://" + host + ctx + "/password/reset?token=" + token; + model.addAttribute("form", "_login"); + return "imprimelibros/login/login"; } } diff --git a/src/main/java/com/imprimelibros/erp/auth/PasswordResetService.java b/src/main/java/com/imprimelibros/erp/auth/PasswordResetService.java index c5a3dbc..cfba12f 100644 --- a/src/main/java/com/imprimelibros/erp/auth/PasswordResetService.java +++ b/src/main/java/com/imprimelibros/erp/auth/PasswordResetService.java @@ -1,86 +1,124 @@ package com.imprimelibros.erp.auth; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; - -import com.imprimelibros.erp.users.UserDao; - import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.SecureRandom; -import java.time.Instant; -import java.time.temporal.ChronoUnit; -import java.util.HexFormat; +import java.time.LocalDateTime; +import java.util.Base64; +import java.util.Locale; +import java.util.Map; + +import org.springframework.context.MessageSource; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.thymeleaf.context.Context; +import org.thymeleaf.spring6.SpringTemplateEngine; + +import com.imprimelibros.erp.common.email.EmailService; +import com.imprimelibros.erp.users.User; +import com.imprimelibros.erp.users.UserDao; // ajusta al path real de tu UserRepository @Service public class PasswordResetService { private final PasswordResetTokenRepository tokenRepo; - private final UserDao userRepo; // tu repo real + private final UserDao userRepo; private final PasswordEncoder passwordEncoder; + private final JavaMailSender mailSender; + private final SpringTemplateEngine templateEngine; + private final MessageSource messages; + private final EmailService emailService; - private static final SecureRandom RNG = new SecureRandom(); - private static final HexFormat HEX = HexFormat.of(); - - public PasswordResetService(PasswordResetTokenRepository tokenRepo, UserDao userRepo, PasswordEncoder enc) { + public PasswordResetService( + PasswordResetTokenRepository tokenRepo, + UserDao userRepo, + PasswordEncoder passwordEncoder, + JavaMailSender mailSender, + SpringTemplateEngine templateEngine, + MessageSource messages, + EmailService emailService + ) { this.tokenRepo = tokenRepo; this.userRepo = userRepo; - this.passwordEncoder = enc; + this.passwordEncoder = passwordEncoder; + this.mailSender = mailSender; + this.templateEngine = templateEngine; + this.messages = messages; + this.emailService = emailService; } - /** Elimina tokens previos sin usar, genera uno nuevo y lo guarda con auditoría básica. */ - public String createTokenForUser(Long userId, int hoursToExpire, String requestIp, String userAgent) { - // Invalidar anteriores - tokenRepo.deleteAllByUserIdAndUsedAtIsNull(userId); + // 3.1 Solicitar reset (si el email existe, genera token y envía) + @Transactional + public void requestReset(String email, String baseUrl, String ip, String userAgent, int minutes, Locale locale) { + + User user = userRepo.findByUserNameIgnoreCase(email).orElse(null); + // Siempre responder OK aunque no exista para evitar enumeración + if (user == null) return; - // Generar token - byte[] raw = new byte[32]; - RNG.nextBytes(raw); - String token = HEX.formatHex(raw); // token plano (64 hex) - String tokenHash = sha256(token); + tokenRepo.invalidateActiveTokens(user.getId(), LocalDateTime.now()); - PasswordResetToken prt = new PasswordResetToken(); - prt.setUserId(userId); - prt.setTokenHash(tokenHash); - prt.setExpiresAt(Instant.now().plus(hoursToExpire, ChronoUnit.HOURS)); - prt.setRequestIp(truncate(requestIp, 64)); - prt.setUserAgent(truncate(userAgent, 255)); - tokenRepo.save(prt); + String token = generateToken(); // token en claro SOLO para el enlace + String tokenHash = sha256(token); // guardamos hash en DB - return token; // Esto se envía por email + PasswordResetToken row = new PasswordResetToken(); + row.setUserId(user.getId()); + row.setCreatedAt(LocalDateTime.now()); + row.setExpiresAt(LocalDateTime.now().plusMinutes(minutes)); + row.setRequestIp(ip); + row.setUserAgent(userAgent); + row.setTokenHash(tokenHash); + tokenRepo.save(row); + + String resetUrl = baseUrl + "/auth/password/reset?uid=" + user.getId() + "&token=" + token; + + emailService.sendPasswordResetMail(user.getUserName(), user.getFullName(), resetUrl, locale); } - public Long validateTokenAndGetUserId(String tokenPlain) { - return tokenRepo.findByTokenHashAndUsedAtIsNullAndExpiresAtAfter(sha256(tokenPlain), Instant.now()) - .map(PasswordResetToken::getUserId) - .orElse(null); + // 3.2 Validar token (para mostrar el formulario de nueva contraseña) + public boolean isValid(Long userId, String token) { + String hash = sha256(token); + return tokenRepo.findValidByUserAndHash(userId, hash, LocalDateTime.now()).isPresent(); } - public void consumeTokenAndSetPassword(String tokenPlain, String newPassword) { - var tokenOpt = tokenRepo.findByTokenHashAndUsedAtIsNullAndExpiresAtAfter(sha256(tokenPlain), Instant.now()); - var prt = tokenOpt.orElseThrow(() -> new IllegalArgumentException("Token inválido o caducado")); + // 3.3 Confirmar reseteo y marcar token como usado + @Transactional + public boolean resetPassword(Long userId, String token, String newPassword) { + String hash = sha256(token); + var opt = tokenRepo.findValidByUserAndHash(userId, hash, LocalDateTime.now()); + if (opt.isEmpty()) return false; - var user = userRepo.findById(prt.getUserId()) - .orElseThrow(() -> new IllegalStateException("Usuario no encontrado")); + var entry = opt.get(); + var user = userRepo.findById(userId).orElse(null); + if (user == null) return false; user.setPassword(passwordEncoder.encode(newPassword)); userRepo.save(user); - prt.setUsedAt(Instant.now()); - tokenRepo.save(prt); + entry.setUsedAt(LocalDateTime.now()); + tokenRepo.save(entry); + + // (Opcional) invalidar otros tokens activos del usuario + tokenRepo.invalidateActiveTokens(userId, LocalDateTime.now()); + return true; } - private static String sha256(String s) { + // --- helpers --- + private String generateToken() { + byte[] buf = new byte[32]; + new SecureRandom().nextBytes(buf); + return Base64.getUrlEncoder().withoutPadding().encodeToString(buf); + } + private String sha256(String s) { try { - MessageDigest md = MessageDigest.getInstance("SHA-256"); - return HEX.formatHex(md.digest(s.getBytes(StandardCharsets.UTF_8))); + var md = MessageDigest.getInstance("SHA-256"); + return Base64.getEncoder().encodeToString(md.digest(s.getBytes(StandardCharsets.UTF_8))); } catch (Exception e) { throw new RuntimeException(e); } } - private static String truncate(String v, int max) { - if (v == null) return null; - return v.length() <= max ? v : v.substring(0, max); - } + } + diff --git a/src/main/java/com/imprimelibros/erp/auth/PasswordResetToken.java b/src/main/java/com/imprimelibros/erp/auth/PasswordResetToken.java index 40a78fe..2e35f5b 100644 --- a/src/main/java/com/imprimelibros/erp/auth/PasswordResetToken.java +++ b/src/main/java/com/imprimelibros/erp/auth/PasswordResetToken.java @@ -1,38 +1,36 @@ package com.imprimelibros.erp.auth; +import java.time.LocalDateTime; import jakarta.persistence.*; -import java.time.Instant; @Entity -@Table(name = "password_reset_tokens", indexes = { - @Index(name = "idx_prt_token_hash", columnList = "tokenHash"), - @Index(name = "idx_prt_user_id", columnList = "userId") -}) +@Table(name = "password_reset_tokens") public class PasswordResetToken { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Column(nullable = false) private Long userId; + @Column(name = "created_at", nullable = false) + private LocalDateTime createdAt; - @Column(nullable = false, length = 128) - private String tokenHash; // SHA-256 hex + @Column(name = "expires_at", nullable = false) + private LocalDateTime expiresAt; - @Column(nullable = false) - private Instant expiresAt; - - @Column(nullable = false) - private Instant createdAt = Instant.now(); - - private Instant usedAt; - - // Auditoría ligera (GDPR: documéntalo y limita retención) - @Column(length = 64) + @Column(name = "request_ip", length = 64) private String requestIp; - @Column(length = 255) + @Column(name = "token_hash", length = 128, nullable = false) + private String tokenHash; + + @Column(name = "used_at") + private LocalDateTime usedAt; + + @Column(name = "user_agent", length = 255) private String userAgent; + @Column(name = "user_id", nullable = false) + private Long userId; + public Long getId() { return id; } @@ -41,44 +39,20 @@ public class PasswordResetToken { this.id = id; } - public Long getUserId() { - return userId; - } - - public void setUserId(Long userId) { - this.userId = userId; - } - - public String getTokenHash() { - return tokenHash; - } - - public void setTokenHash(String tokenHash) { - this.tokenHash = tokenHash; - } - - public Instant getExpiresAt() { - return expiresAt; - } - - public void setExpiresAt(Instant expiresAt) { - this.expiresAt = expiresAt; - } - - public Instant getCreatedAt() { + public LocalDateTime getCreatedAt() { return createdAt; } - public void setCreatedAt(Instant createdAt) { + public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; } - public Instant getUsedAt() { - return usedAt; + public LocalDateTime getExpiresAt() { + return expiresAt; } - public void setUsedAt(Instant usedAt) { - this.usedAt = usedAt; + public void setExpiresAt(LocalDateTime expiresAt) { + this.expiresAt = expiresAt; } public String getRequestIp() { @@ -89,6 +63,22 @@ public class PasswordResetToken { this.requestIp = requestIp; } + public String getTokenHash() { + return tokenHash; + } + + public void setTokenHash(String tokenHash) { + this.tokenHash = tokenHash; + } + + public LocalDateTime getUsedAt() { + return usedAt; + } + + public void setUsedAt(LocalDateTime usedAt) { + this.usedAt = usedAt; + } + public String getUserAgent() { return userAgent; } @@ -97,6 +87,13 @@ public class PasswordResetToken { this.userAgent = userAgent; } - // getters/setters + public Long getUserId() { + return userId; + } + + public void setUserId(Long userId) { + this.userId = userId; + } + -} +} \ No newline at end of file diff --git a/src/main/java/com/imprimelibros/erp/auth/PasswordResetTokenRepository.java b/src/main/java/com/imprimelibros/erp/auth/PasswordResetTokenRepository.java index 5d398d4..b6a94dc 100644 --- a/src/main/java/com/imprimelibros/erp/auth/PasswordResetTokenRepository.java +++ b/src/main/java/com/imprimelibros/erp/auth/PasswordResetTokenRepository.java @@ -1,13 +1,40 @@ package com.imprimelibros.erp.auth; -import org.springframework.data.jpa.repository.JpaRepository; -import java.time.Instant; +import java.time.LocalDateTime; import java.util.Optional; +import org.springframework.data.jpa.repository.*; +import java.util.List; public interface PasswordResetTokenRepository extends JpaRepository { - Optional findByTokenHashAndUsedAtIsNullAndExpiresAtAfter(String tokenHash, Instant now); + // Para validar un token en el reset + @Query(""" + SELECT t FROM PasswordResetToken t + WHERE t.userId = :userId + AND t.tokenHash = :tokenHash + AND t.usedAt IS NULL + AND t.expiresAt > :now + """) + Optional findValidByUserAndHash(Long userId, String tokenHash, LocalDateTime now); - // Para invalidar anteriores - long deleteAllByUserIdAndUsedAtIsNull(Long userId); + // Invalida cualquier token activo del usuario + @Modifying + @Query(""" + UPDATE PasswordResetToken t + SET t.usedAt = :now + WHERE t.userId = :userId + AND t.usedAt IS NULL + AND t.expiresAt > :now + """) + int invalidateActiveTokens(Long userId, LocalDateTime now); + + // (Opcional) lista de activos, por si quieres inspeccionarlos + @Query(""" + SELECT t FROM PasswordResetToken t + WHERE t.userId = :userId + AND t.usedAt IS NULL + AND t.expiresAt > :now + ORDER BY t.createdAt DESC + """) + List findAllActiveForUser(Long userId, LocalDateTime now); } 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 ead795e..d94f8fa 100644 --- a/src/main/java/com/imprimelibros/erp/common/email/EmailService.java +++ b/src/main/java/com/imprimelibros/erp/common/email/EmailService.java @@ -7,7 +7,6 @@ import org.springframework.context.MessageSource; 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; @@ -29,11 +28,12 @@ public class EmailService { } public void sendPasswordResetMail(String to, String fullName, String resetUrl, Locale locale) { - String subject = messageSource.getMessage("email.resetPassword.title", null, locale); + String subject = messageSource.getMessage("email.reset-password.title", null, locale); Map variables = Map.of( "fullName", fullName, - "resetUrl", resetUrl); - sendEmail(to, subject, "imprimelibros/email/reset-password", variables, locale); + "resetUrl", resetUrl, + "minutes", 60); + sendEmail(to, subject, "imprimelibros/email/password-reset", variables, locale); } public void sendVerificationEmail(String to, String fullName, String verifyUrl, Locale locale) { diff --git a/src/main/java/com/imprimelibros/erp/config/SecurityConfig.java b/src/main/java/com/imprimelibros/erp/config/SecurityConfig.java index b9c5cd5..e348160 100644 --- a/src/main/java/com/imprimelibros/erp/config/SecurityConfig.java +++ b/src/main/java/com/imprimelibros/erp/config/SecurityConfig.java @@ -126,7 +126,7 @@ public class SecurityConfig { "/login", "/signup", "/verify", - "/reset-password", + "/auth/password/**", "/assets/**", "/css/**", "/js/**", diff --git a/src/main/resources/i18n/email_es.properties b/src/main/resources/i18n/email_es.properties index 8d5fa18..c923c9c 100644 --- a/src/main/resources/i18n/email_es.properties +++ b/src/main/resources/i18n/email_es.properties @@ -5,4 +5,19 @@ email.verify.button=Verificar cuenta email.verify.link-instruction=Si no funciona, copia y pega esta URL en tu navegador: email.verify.expiration=Este enlace caduca en {0} minutos. email.verify.ignoreMessage=Si no solicitaste este cambio, puedes ignorar este mensaje. -email.footer=Imprimelibros - Todos los derechos reservados. + +email.reset-password.title=Restablecer contraseña +email.reset-password.body=Haz clic en el siguiente botón para restablecer tu contraseña: +email.reset-password.button=Restablecer contraseña +email.reset-password.link-instruction=Si no funciona, copia y pega esta URL en tu navegador: +email.reset-password.expiration=Este enlace caduca en {0} minutos. +email.reset-password.ignoreMessage=Si no solicitaste este cambio, puedes ignorar este mensaje. + +email.reset.title=Restablecer tu contraseña +email.reset.hello=Hola, +email.reset.instructions=Has solicitado restablecer tu contraseña. +email.reset.button=Restablecer contraseña +email.reset.ignore=Si no solicitaste este cambio, puedes ignorar este correo. +email.verify.expiration=Este enlace caduca en {0} minutos. + +email.footer=Imprimelibros - Todos los derechos reservados. \ No newline at end of file diff --git a/src/main/resources/i18n/login_es.properties b/src/main/resources/i18n/login_es.properties index 500bd37..71b804f 100644 --- a/src/main/resources/i18n/login_es.properties +++ b/src/main/resources/i18n/login_es.properties @@ -17,6 +17,14 @@ login.sign-up=Regístrate login.sign-up-button=Crear cuenta login.sign-up.title=Crear una cuenta login.sign-up.name=Nombre completo +login.password-recovery.title=Recuperar contraseña +login.password-recovery.button=Recuperar contraseña +login.password-recovery.email-info=Se te ha enviado un correo con instrucciones para restablecer tu contraseña. +login.change-password.title=Cambiar contraseña +login.change-password.subtitle=Introduce tu nueva contraseña a continuación. +login.change-password.new-password=Nueva contraseña +login.change-password.confirm-password=Confirmar nueva contraseña +login.change-password.button=Cambiar contraseña login.error=Credenciales inválidas login.signup.error.email.exists=El correo electrónico ya está en uso. @@ -25,5 +33,10 @@ 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.password-reset.error=Las contraseñas no coinciden o son demasiado cortas. +login.password-reset.error-link=El enlace no es válido o ha expirado. + 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 +login.signup.success.verified=¡Cuenta verificada! Ya puedes iniciar sesión. + +login.password-reset.success=Contraseña cambiada correctamente. Ya puedes iniciar sesión. \ No newline at end of file diff --git a/src/main/resources/templates/imprimelibros/email/password-reset.html b/src/main/resources/templates/imprimelibros/email/password-reset.html index 549500a..e5849f1 100644 --- a/src/main/resources/templates/imprimelibros/email/password-reset.html +++ b/src/main/resources/templates/imprimelibros/email/password-reset.html @@ -1,33 +1,45 @@ - + + +

+ Hola + Usuario, +

- - - Recuperación de contraseña - - +

+ + Haz clic en el siguiente botón para restablecer tu contraseña: + +

- -
-
- - Logo Imprimelibros -
+

+ + Restablecer contraseña + +

-

Hola Usuario,

-

Hemos recibido una solicitud para restablecer tu contraseña. Haz clic en el siguiente botón:

+

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

-

- Restablecer contraseña -

+

+ https://... +

-

Si no solicitaste este cambio, puedes ignorar este mensaje.

+

+ + Este enlace caduca en 60 minutos. + +

- - -
- - - \ No newline at end of file +

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

+
+ diff --git a/src/main/resources/templates/imprimelibros/login/_items/_forgot-pass.html b/src/main/resources/templates/imprimelibros/login/_items/_forgot-pass.html deleted file mode 100644 index e69de29..0000000 diff --git a/src/main/resources/templates/imprimelibros/login/_items/_forgot-password.html b/src/main/resources/templates/imprimelibros/login/_items/_forgot-password.html new file mode 100644 index 0000000..bf16a13 --- /dev/null +++ b/src/main/resources/templates/imprimelibros/login/_items/_forgot-password.html @@ -0,0 +1,21 @@ +
+ +
+
Recuperar contraseña
+
+ +
+ + +
+ + +
+ +
+ +
+
+
\ 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 13479f0..b9c8eda 100644 --- a/src/main/resources/templates/imprimelibros/login/_items/_login.html +++ b/src/main/resources/templates/imprimelibros/login/_items/_login.html @@ -22,7 +22,7 @@
diff --git a/src/main/resources/templates/imprimelibros/login/_items/_reset-password.html b/src/main/resources/templates/imprimelibros/login/_items/_reset-password.html new file mode 100644 index 0000000..4c02869 --- /dev/null +++ b/src/main/resources/templates/imprimelibros/login/_items/_reset-password.html @@ -0,0 +1,41 @@ +
+ +
+
Cambiar contraseña
+
+ +
+ + + + + +
+ + +
+ +
+ + +
+ +
+ +
+
+ + + +
\ 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 585d537..514a54a 100644 --- a/src/main/resources/templates/imprimelibros/login/_items/_signup.html +++ b/src/main/resources/templates/imprimelibros/login/_items/_signup.html @@ -1,7 +1,5 @@
- -
¡Bienvenido!

Crear cuenta

@@ -10,8 +8,6 @@
- -