diff --git a/backend/spring-boot/.run/AllTests.run.xml b/backend/spring-boot/.run/AllTests.run.xml
index 683a9086..2f053439 100644
--- a/backend/spring-boot/.run/AllTests.run.xml
+++ b/backend/spring-boot/.run/AllTests.run.xml
@@ -12,17 +12,8 @@
-
-
-
-
-
-
-
-
-
-
-
+
+
diff --git a/backend/spring-boot/.run/DataTests.run.xml b/backend/spring-boot/.run/DataTests.run.xml
deleted file mode 100644
index 3c3ec463..00000000
--- a/backend/spring-boot/.run/DataTests.run.xml
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/backend/spring-boot/.run/IntegrationTests.run.xml b/backend/spring-boot/.run/IntegrationTests.run.xml
index 2205b5c3..40296b5b 100644
--- a/backend/spring-boot/.run/IntegrationTests.run.xml
+++ b/backend/spring-boot/.run/IntegrationTests.run.xml
@@ -6,12 +6,7 @@
-
-
-
-
-
-
+
diff --git a/backend/spring-boot/.run/UnitTests.run.xml b/backend/spring-boot/.run/UnitTests.run.xml
index a947d087..f80b6324 100644
--- a/backend/spring-boot/.run/UnitTests.run.xml
+++ b/backend/spring-boot/.run/UnitTests.run.xml
@@ -6,8 +6,7 @@
-
-
+
diff --git a/backend/spring-boot/src/main/java/org/bugzkit/api/admin/service/impl/UserServiceImpl.java b/backend/spring-boot/src/main/java/org/bugzkit/api/admin/service/impl/UserServiceImpl.java
index c09265a9..39e8e1f0 100644
--- a/backend/spring-boot/src/main/java/org/bugzkit/api/admin/service/impl/UserServiceImpl.java
+++ b/backend/spring-boot/src/main/java/org/bugzkit/api/admin/service/impl/UserServiceImpl.java
@@ -4,8 +4,8 @@
import org.bugzkit.api.admin.payload.request.PatchUserRequest;
import org.bugzkit.api.admin.payload.request.UserRequest;
import org.bugzkit.api.admin.service.UserService;
-import org.bugzkit.api.auth.jwt.service.AccessTokenService;
-import org.bugzkit.api.auth.jwt.service.RefreshTokenService;
+import org.bugzkit.api.auth.service.AccessTokenService;
+import org.bugzkit.api.auth.service.RefreshTokenService;
import org.bugzkit.api.auth.util.AuthUtil;
import org.bugzkit.api.shared.error.exception.BadRequestException;
import org.bugzkit.api.shared.error.exception.ConflictException;
diff --git a/backend/spring-boot/src/main/java/org/bugzkit/api/auth/controller/AuthController.java b/backend/spring-boot/src/main/java/org/bugzkit/api/auth/controller/AuthController.java
index 97fd1554..2cbbb17a 100644
--- a/backend/spring-boot/src/main/java/org/bugzkit/api/auth/controller/AuthController.java
+++ b/backend/spring-boot/src/main/java/org/bugzkit/api/auth/controller/AuthController.java
@@ -4,7 +4,6 @@
import jakarta.validation.Valid;
import java.util.List;
import org.bugzkit.api.auth.AuthTokens;
-import org.bugzkit.api.auth.jwt.util.JwtUtil;
import org.bugzkit.api.auth.payload.dto.DeviceDTO;
import org.bugzkit.api.auth.payload.request.AuthTokensRequest;
import org.bugzkit.api.auth.payload.request.ForgotPasswordRequest;
@@ -15,6 +14,7 @@
import org.bugzkit.api.auth.service.AuthService;
import org.bugzkit.api.auth.service.DeviceService;
import org.bugzkit.api.auth.util.AuthUtil;
+import org.bugzkit.api.auth.util.JwtUtil;
import org.bugzkit.api.shared.constants.Path;
import org.bugzkit.api.user.payload.dto.UserDTO;
import org.springframework.beans.factory.annotation.Value;
diff --git a/backend/spring-boot/src/main/java/org/bugzkit/api/auth/jwt/event/email/JwtEmail.java b/backend/spring-boot/src/main/java/org/bugzkit/api/auth/email/AuthEmail.java
similarity index 82%
rename from backend/spring-boot/src/main/java/org/bugzkit/api/auth/jwt/event/email/JwtEmail.java
rename to backend/spring-boot/src/main/java/org/bugzkit/api/auth/email/AuthEmail.java
index 5643f72c..4df79e07 100644
--- a/backend/spring-boot/src/main/java/org/bugzkit/api/auth/jwt/event/email/JwtEmail.java
+++ b/backend/spring-boot/src/main/java/org/bugzkit/api/auth/email/AuthEmail.java
@@ -1,4 +1,4 @@
-package org.bugzkit.api.auth.jwt.event.email;
+package org.bugzkit.api.auth.email;
import jakarta.mail.MessagingException;
import java.io.IOException;
@@ -6,7 +6,7 @@
import org.bugzkit.api.user.model.User;
import org.springframework.core.env.Environment;
-public interface JwtEmail {
+public interface AuthEmail {
void sendEmail(EmailService emailService, Environment environment, User user, String token)
throws IOException, MessagingException;
}
diff --git a/backend/spring-boot/src/main/java/org/bugzkit/api/auth/email/AuthEmailPurpose.java b/backend/spring-boot/src/main/java/org/bugzkit/api/auth/email/AuthEmailPurpose.java
new file mode 100644
index 00000000..b1d6c4e2
--- /dev/null
+++ b/backend/spring-boot/src/main/java/org/bugzkit/api/auth/email/AuthEmailPurpose.java
@@ -0,0 +1,6 @@
+package org.bugzkit.api.auth.email;
+
+public enum AuthEmailPurpose {
+ VERIFY_EMAIL,
+ RESET_PASSWORD
+}
diff --git a/backend/spring-boot/src/main/java/org/bugzkit/api/auth/email/AuthEmailSupplier.java b/backend/spring-boot/src/main/java/org/bugzkit/api/auth/email/AuthEmailSupplier.java
new file mode 100644
index 00000000..4777627e
--- /dev/null
+++ b/backend/spring-boot/src/main/java/org/bugzkit/api/auth/email/AuthEmailSupplier.java
@@ -0,0 +1,21 @@
+package org.bugzkit.api.auth.email;
+
+import java.util.EnumMap;
+import java.util.Map;
+import java.util.function.Supplier;
+
+public class AuthEmailSupplier {
+ private static final Map> emailType =
+ new EnumMap<>(AuthEmailPurpose.class);
+
+ static {
+ emailType.put(AuthEmailPurpose.VERIFY_EMAIL, VerificationAuthEmail::new);
+ emailType.put(AuthEmailPurpose.RESET_PASSWORD, ResetPasswordAuthEmail::new);
+ }
+
+ public AuthEmail supplyEmail(AuthEmailPurpose purpose) {
+ final var emailSupplier = emailType.get(purpose);
+ if (emailSupplier == null) throw new IllegalArgumentException("Invalid email type: " + purpose);
+ return emailSupplier.get();
+ }
+}
diff --git a/backend/spring-boot/src/main/java/org/bugzkit/api/auth/jwt/event/OnSendJwtEmail.java b/backend/spring-boot/src/main/java/org/bugzkit/api/auth/email/OnSendAuthEmail.java
similarity index 57%
rename from backend/spring-boot/src/main/java/org/bugzkit/api/auth/jwt/event/OnSendJwtEmail.java
rename to backend/spring-boot/src/main/java/org/bugzkit/api/auth/email/OnSendAuthEmail.java
index 5f2e59cd..bcc383b1 100644
--- a/backend/spring-boot/src/main/java/org/bugzkit/api/auth/jwt/event/OnSendJwtEmail.java
+++ b/backend/spring-boot/src/main/java/org/bugzkit/api/auth/email/OnSendAuthEmail.java
@@ -1,19 +1,18 @@
-package org.bugzkit.api.auth.jwt.event;
+package org.bugzkit.api.auth.email;
import java.io.Serial;
import lombok.Getter;
-import org.bugzkit.api.auth.jwt.util.JwtUtil.JwtPurpose;
import org.bugzkit.api.user.model.User;
import org.springframework.context.ApplicationEvent;
@Getter
-public class OnSendJwtEmail extends ApplicationEvent {
- @Serial private static final long serialVersionUID = 6234594744610595282L;
+public class OnSendAuthEmail extends ApplicationEvent {
+ @Serial private static final long serialVersionUID = 6234594744610595283L;
private final User user;
private final String token;
- private final JwtPurpose purpose;
+ private final AuthEmailPurpose purpose;
- public OnSendJwtEmail(User user, String token, JwtPurpose purpose) {
+ public OnSendAuthEmail(User user, String token, AuthEmailPurpose purpose) {
super(user);
this.user = user;
this.token = token;
diff --git a/backend/spring-boot/src/main/java/org/bugzkit/api/auth/jwt/event/listener/OnSendJwtEmailListener.java b/backend/spring-boot/src/main/java/org/bugzkit/api/auth/email/OnSendAuthEmailListener.java
similarity index 62%
rename from backend/spring-boot/src/main/java/org/bugzkit/api/auth/jwt/event/listener/OnSendJwtEmailListener.java
rename to backend/spring-boot/src/main/java/org/bugzkit/api/auth/email/OnSendAuthEmailListener.java
index 4c3d5335..c7982d67 100644
--- a/backend/spring-boot/src/main/java/org/bugzkit/api/auth/jwt/event/listener/OnSendJwtEmailListener.java
+++ b/backend/spring-boot/src/main/java/org/bugzkit/api/auth/email/OnSendAuthEmailListener.java
@@ -1,28 +1,26 @@
-package org.bugzkit.api.auth.jwt.event.listener;
+package org.bugzkit.api.auth.email;
import jakarta.mail.MessagingException;
import java.io.IOException;
-import org.bugzkit.api.auth.jwt.event.OnSendJwtEmail;
-import org.bugzkit.api.auth.jwt.event.email.JwtEmailSupplier;
import org.bugzkit.api.shared.email.service.EmailService;
import org.springframework.context.ApplicationListener;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;
@Component
-public class OnSendJwtEmailListener implements ApplicationListener {
+public class OnSendAuthEmailListener implements ApplicationListener {
private final EmailService emailService;
private final Environment environment;
- public OnSendJwtEmailListener(EmailService emailService, Environment environment) {
+ public OnSendAuthEmailListener(EmailService emailService, Environment environment) {
this.emailService = emailService;
this.environment = environment;
}
@Override
- public void onApplicationEvent(OnSendJwtEmail event) {
+ public void onApplicationEvent(OnSendAuthEmail event) {
try {
- new JwtEmailSupplier()
+ new AuthEmailSupplier()
.supplyEmail(event.getPurpose())
.sendEmail(emailService, environment, event.getUser(), event.getToken());
} catch (IOException | MessagingException e) {
diff --git a/backend/spring-boot/src/main/java/org/bugzkit/api/auth/jwt/event/email/ResetPasswordEmail.java b/backend/spring-boot/src/main/java/org/bugzkit/api/auth/email/ResetPasswordAuthEmail.java
similarity index 92%
rename from backend/spring-boot/src/main/java/org/bugzkit/api/auth/jwt/event/email/ResetPasswordEmail.java
rename to backend/spring-boot/src/main/java/org/bugzkit/api/auth/email/ResetPasswordAuthEmail.java
index 95ffa85c..8b00267a 100644
--- a/backend/spring-boot/src/main/java/org/bugzkit/api/auth/jwt/event/email/ResetPasswordEmail.java
+++ b/backend/spring-boot/src/main/java/org/bugzkit/api/auth/email/ResetPasswordAuthEmail.java
@@ -1,4 +1,4 @@
-package org.bugzkit.api.auth.jwt.event.email;
+package org.bugzkit.api.auth.email;
import com.google.common.io.CharStreams;
import jakarta.mail.MessagingException;
@@ -11,7 +11,7 @@
import org.springframework.core.env.Environment;
import org.springframework.core.io.ClassPathResource;
-public class ResetPasswordEmail implements JwtEmail {
+public class ResetPasswordAuthEmail implements AuthEmail {
@Override
public void sendEmail(EmailService emailService, Environment environment, User user, String token)
throws IOException, MessagingException {
diff --git a/backend/spring-boot/src/main/java/org/bugzkit/api/auth/jwt/event/email/VerificationEmail.java b/backend/spring-boot/src/main/java/org/bugzkit/api/auth/email/VerificationAuthEmail.java
similarity index 92%
rename from backend/spring-boot/src/main/java/org/bugzkit/api/auth/jwt/event/email/VerificationEmail.java
rename to backend/spring-boot/src/main/java/org/bugzkit/api/auth/email/VerificationAuthEmail.java
index bb40a9f0..9d95d098 100644
--- a/backend/spring-boot/src/main/java/org/bugzkit/api/auth/jwt/event/email/VerificationEmail.java
+++ b/backend/spring-boot/src/main/java/org/bugzkit/api/auth/email/VerificationAuthEmail.java
@@ -1,4 +1,4 @@
-package org.bugzkit.api.auth.jwt.event.email;
+package org.bugzkit.api.auth.email;
import com.google.common.io.CharStreams;
import jakarta.mail.MessagingException;
@@ -11,7 +11,7 @@
import org.springframework.core.env.Environment;
import org.springframework.core.io.ClassPathResource;
-public class VerificationEmail implements JwtEmail {
+public class VerificationAuthEmail implements AuthEmail {
@Override
public void sendEmail(EmailService emailService, Environment environment, User user, String token)
throws IOException, MessagingException {
diff --git a/backend/spring-boot/src/main/java/org/bugzkit/api/auth/jwt/event/email/JwtEmailSupplier.java b/backend/spring-boot/src/main/java/org/bugzkit/api/auth/jwt/event/email/JwtEmailSupplier.java
deleted file mode 100644
index f114d80b..00000000
--- a/backend/spring-boot/src/main/java/org/bugzkit/api/auth/jwt/event/email/JwtEmailSupplier.java
+++ /dev/null
@@ -1,23 +0,0 @@
-package org.bugzkit.api.auth.jwt.event.email;
-
-import java.util.EnumMap;
-import java.util.Map;
-import java.util.function.Supplier;
-import org.bugzkit.api.auth.jwt.util.JwtUtil.JwtPurpose;
-
-public class JwtEmailSupplier {
- private static final Map> emailType =
- new EnumMap<>(JwtPurpose.class);
-
- static {
- emailType.put(JwtPurpose.VERIFY_EMAIL_TOKEN, VerificationEmail::new);
- emailType.put(JwtPurpose.RESET_PASSWORD_TOKEN, ResetPasswordEmail::new);
- }
-
- public JwtEmail supplyEmail(JwtPurpose jwtPurpose) {
- final var emailSupplier = emailType.get(jwtPurpose);
- if (emailSupplier == null)
- throw new IllegalArgumentException("Invalid email type: " + jwtPurpose);
- return emailSupplier.get();
- }
-}
diff --git a/backend/spring-boot/src/main/java/org/bugzkit/api/auth/jwt/service/impl/ResetPasswordTokenServiceImpl.java b/backend/spring-boot/src/main/java/org/bugzkit/api/auth/jwt/service/impl/ResetPasswordTokenServiceImpl.java
deleted file mode 100644
index b37645c2..00000000
--- a/backend/spring-boot/src/main/java/org/bugzkit/api/auth/jwt/service/impl/ResetPasswordTokenServiceImpl.java
+++ /dev/null
@@ -1,71 +0,0 @@
-package org.bugzkit.api.auth.jwt.service.impl;
-
-import com.auth0.jwt.JWT;
-import java.time.Instant;
-import org.bugzkit.api.auth.jwt.event.OnSendJwtEmail;
-import org.bugzkit.api.auth.jwt.redis.repository.UserBlacklistRepository;
-import org.bugzkit.api.auth.jwt.service.ResetPasswordTokenService;
-import org.bugzkit.api.auth.jwt.util.JwtUtil;
-import org.bugzkit.api.auth.jwt.util.JwtUtil.JwtPurpose;
-import org.bugzkit.api.shared.error.exception.BadRequestException;
-import org.bugzkit.api.user.model.User;
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.context.ApplicationEventPublisher;
-import org.springframework.stereotype.Service;
-
-@Service
-public class ResetPasswordTokenServiceImpl implements ResetPasswordTokenService {
- private static final JwtPurpose PURPOSE = JwtPurpose.RESET_PASSWORD_TOKEN;
- private final UserBlacklistRepository userBlacklistRepository;
- private final ApplicationEventPublisher eventPublisher;
-
- @Value("${jwt.secret}")
- private String secret;
-
- @Value("${jwt.reset-password-token.duration}")
- private int tokenDuration;
-
- public ResetPasswordTokenServiceImpl(
- UserBlacklistRepository userBlacklistRepository, ApplicationEventPublisher eventPublisher) {
- this.userBlacklistRepository = userBlacklistRepository;
- this.eventPublisher = eventPublisher;
- }
-
- @Override
- public String create(Long userId) {
- return JWT.create()
- .withIssuer(userId.toString())
- .withClaim("purpose", PURPOSE.name())
- .withIssuedAt(Instant.now())
- .withExpiresAt(Instant.now().plusSeconds(tokenDuration))
- .sign(JwtUtil.getAlgorithm(secret));
- }
-
- @Override
- public void check(String token) {
- verifyToken(token);
- isInUserBlacklist(token);
- }
-
- private void verifyToken(String token) {
- try {
- JwtUtil.verify(token, secret, PURPOSE);
- } catch (RuntimeException e) {
- throw new BadRequestException("auth.tokenInvalid");
- }
- }
-
- private void isInUserBlacklist(String token) {
- final var userId = JwtUtil.getUserId(token);
- final var issuedAt = JwtUtil.getIssuedAt(token);
- final var userInBlacklist = userBlacklistRepository.findById(userId);
- if (userInBlacklist.isEmpty()) return;
- if (issuedAt.isBefore(userInBlacklist.get().getUpdatedAt()))
- throw new BadRequestException("auth.tokenInvalid");
- }
-
- @Override
- public void sendToEmail(User user, String token) {
- eventPublisher.publishEvent(new OnSendJwtEmail(user, token, PURPOSE));
- }
-}
diff --git a/backend/spring-boot/src/main/java/org/bugzkit/api/auth/jwt/service/impl/VerificationTokenServiceImpl.java b/backend/spring-boot/src/main/java/org/bugzkit/api/auth/jwt/service/impl/VerificationTokenServiceImpl.java
deleted file mode 100644
index 0dcaac42..00000000
--- a/backend/spring-boot/src/main/java/org/bugzkit/api/auth/jwt/service/impl/VerificationTokenServiceImpl.java
+++ /dev/null
@@ -1,53 +0,0 @@
-package org.bugzkit.api.auth.jwt.service.impl;
-
-import com.auth0.jwt.JWT;
-import java.time.Instant;
-import org.bugzkit.api.auth.jwt.event.OnSendJwtEmail;
-import org.bugzkit.api.auth.jwt.service.VerificationTokenService;
-import org.bugzkit.api.auth.jwt.util.JwtUtil;
-import org.bugzkit.api.auth.jwt.util.JwtUtil.JwtPurpose;
-import org.bugzkit.api.shared.error.exception.BadRequestException;
-import org.bugzkit.api.user.model.User;
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.context.ApplicationEventPublisher;
-import org.springframework.stereotype.Service;
-
-@Service
-public class VerificationTokenServiceImpl implements VerificationTokenService {
- private static final JwtPurpose PURPOSE = JwtPurpose.VERIFY_EMAIL_TOKEN;
- private final ApplicationEventPublisher eventPublisher;
-
- @Value("${jwt.secret}")
- private String secret;
-
- @Value("${jwt.verify-email-token.duration}")
- private int tokenDuration;
-
- public VerificationTokenServiceImpl(ApplicationEventPublisher eventPublisher) {
- this.eventPublisher = eventPublisher;
- }
-
- @Override
- public String create(Long userId) {
- return JWT.create()
- .withIssuer(userId.toString())
- .withClaim("purpose", PURPOSE.name())
- .withIssuedAt(Instant.now())
- .withExpiresAt(Instant.now().plusSeconds(tokenDuration))
- .sign(JwtUtil.getAlgorithm(secret));
- }
-
- @Override
- public void check(String token) {
- try {
- JwtUtil.verify(token, secret, PURPOSE);
- } catch (RuntimeException e) {
- throw new BadRequestException("auth.tokenInvalid");
- }
- }
-
- @Override
- public void sendToEmail(User user, String token) {
- eventPublisher.publishEvent(new OnSendJwtEmail(user, token, PURPOSE));
- }
-}
diff --git a/backend/spring-boot/src/main/java/org/bugzkit/api/auth/oauth2/OAuth2SuccessHandler.java b/backend/spring-boot/src/main/java/org/bugzkit/api/auth/oauth2/OAuth2SuccessHandler.java
index 8d19f4a1..70006623 100644
--- a/backend/spring-boot/src/main/java/org/bugzkit/api/auth/oauth2/OAuth2SuccessHandler.java
+++ b/backend/spring-boot/src/main/java/org/bugzkit/api/auth/oauth2/OAuth2SuccessHandler.java
@@ -4,9 +4,9 @@
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.stream.Collectors;
-import org.bugzkit.api.auth.jwt.service.AccessTokenService;
-import org.bugzkit.api.auth.jwt.service.RefreshTokenService;
+import org.bugzkit.api.auth.service.AccessTokenService;
import org.bugzkit.api.auth.service.DeviceService;
+import org.bugzkit.api.auth.service.RefreshTokenService;
import org.bugzkit.api.auth.util.AuthUtil;
import org.bugzkit.api.shared.logger.CustomLogger;
import org.bugzkit.api.user.payload.dto.RoleDTO;
diff --git a/backend/spring-boot/src/main/java/org/bugzkit/api/auth/jwt/redis/model/AccessTokenBlacklist.java b/backend/spring-boot/src/main/java/org/bugzkit/api/auth/redis/model/AccessTokenBlacklist.java
similarity index 92%
rename from backend/spring-boot/src/main/java/org/bugzkit/api/auth/jwt/redis/model/AccessTokenBlacklist.java
rename to backend/spring-boot/src/main/java/org/bugzkit/api/auth/redis/model/AccessTokenBlacklist.java
index 0d11a249..7baec339 100644
--- a/backend/spring-boot/src/main/java/org/bugzkit/api/auth/jwt/redis/model/AccessTokenBlacklist.java
+++ b/backend/spring-boot/src/main/java/org/bugzkit/api/auth/redis/model/AccessTokenBlacklist.java
@@ -1,4 +1,4 @@
-package org.bugzkit.api.auth.jwt.redis.model;
+package org.bugzkit.api.auth.redis.model;
import java.io.Serial;
import java.io.Serializable;
diff --git a/backend/spring-boot/src/main/java/org/bugzkit/api/auth/jwt/redis/model/RefreshTokenStore.java b/backend/spring-boot/src/main/java/org/bugzkit/api/auth/redis/model/RefreshTokenStore.java
similarity index 94%
rename from backend/spring-boot/src/main/java/org/bugzkit/api/auth/jwt/redis/model/RefreshTokenStore.java
rename to backend/spring-boot/src/main/java/org/bugzkit/api/auth/redis/model/RefreshTokenStore.java
index 52307152..dcb7ffc7 100644
--- a/backend/spring-boot/src/main/java/org/bugzkit/api/auth/jwt/redis/model/RefreshTokenStore.java
+++ b/backend/spring-boot/src/main/java/org/bugzkit/api/auth/redis/model/RefreshTokenStore.java
@@ -1,4 +1,4 @@
-package org.bugzkit.api.auth.jwt.redis.model;
+package org.bugzkit.api.auth.redis.model;
import java.io.Serial;
import java.io.Serializable;
diff --git a/backend/spring-boot/src/main/java/org/bugzkit/api/auth/redis/model/ResetPasswordTokenStore.java b/backend/spring-boot/src/main/java/org/bugzkit/api/auth/redis/model/ResetPasswordTokenStore.java
new file mode 100644
index 00000000..809f4a52
--- /dev/null
+++ b/backend/spring-boot/src/main/java/org/bugzkit/api/auth/redis/model/ResetPasswordTokenStore.java
@@ -0,0 +1,25 @@
+package org.bugzkit.api.auth.redis.model;
+
+import java.io.Serial;
+import java.io.Serializable;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import org.springframework.data.annotation.Id;
+import org.springframework.data.redis.core.RedisHash;
+import org.springframework.data.redis.core.TimeToLive;
+import org.springframework.data.redis.core.index.Indexed;
+
+@Getter
+@NoArgsConstructor
+@AllArgsConstructor
+@RedisHash(value = "ResetPasswordTokenStore")
+public class ResetPasswordTokenStore implements Serializable {
+ @Serial private static final long serialVersionUID = 4567890123456789012L;
+
+ @Id private String token;
+
+ @Indexed private Long userId;
+
+ @TimeToLive private long timeToLive;
+}
diff --git a/backend/spring-boot/src/main/java/org/bugzkit/api/auth/jwt/redis/model/UserBlacklist.java b/backend/spring-boot/src/main/java/org/bugzkit/api/auth/redis/model/UserBlacklist.java
similarity index 94%
rename from backend/spring-boot/src/main/java/org/bugzkit/api/auth/jwt/redis/model/UserBlacklist.java
rename to backend/spring-boot/src/main/java/org/bugzkit/api/auth/redis/model/UserBlacklist.java
index 8c57a8c9..0b697f8a 100644
--- a/backend/spring-boot/src/main/java/org/bugzkit/api/auth/jwt/redis/model/UserBlacklist.java
+++ b/backend/spring-boot/src/main/java/org/bugzkit/api/auth/redis/model/UserBlacklist.java
@@ -1,4 +1,4 @@
-package org.bugzkit.api.auth.jwt.redis.model;
+package org.bugzkit.api.auth.redis.model;
import java.io.Serial;
import java.io.Serializable;
diff --git a/backend/spring-boot/src/main/java/org/bugzkit/api/auth/redis/model/VerificationTokenStore.java b/backend/spring-boot/src/main/java/org/bugzkit/api/auth/redis/model/VerificationTokenStore.java
new file mode 100644
index 00000000..9a183d3e
--- /dev/null
+++ b/backend/spring-boot/src/main/java/org/bugzkit/api/auth/redis/model/VerificationTokenStore.java
@@ -0,0 +1,25 @@
+package org.bugzkit.api.auth.redis.model;
+
+import java.io.Serial;
+import java.io.Serializable;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import org.springframework.data.annotation.Id;
+import org.springframework.data.redis.core.RedisHash;
+import org.springframework.data.redis.core.TimeToLive;
+import org.springframework.data.redis.core.index.Indexed;
+
+@Getter
+@NoArgsConstructor
+@AllArgsConstructor
+@RedisHash(value = "VerificationTokenStore")
+public class VerificationTokenStore implements Serializable {
+ @Serial private static final long serialVersionUID = 3456789012345678901L;
+
+ @Id private String token;
+
+ @Indexed private Long userId;
+
+ @TimeToLive private long timeToLive;
+}
diff --git a/backend/spring-boot/src/main/java/org/bugzkit/api/auth/jwt/redis/repository/AccessTokenBlacklistRepository.java b/backend/spring-boot/src/main/java/org/bugzkit/api/auth/redis/repository/AccessTokenBlacklistRepository.java
similarity index 59%
rename from backend/spring-boot/src/main/java/org/bugzkit/api/auth/jwt/redis/repository/AccessTokenBlacklistRepository.java
rename to backend/spring-boot/src/main/java/org/bugzkit/api/auth/redis/repository/AccessTokenBlacklistRepository.java
index d261eefb..6894435d 100644
--- a/backend/spring-boot/src/main/java/org/bugzkit/api/auth/jwt/redis/repository/AccessTokenBlacklistRepository.java
+++ b/backend/spring-boot/src/main/java/org/bugzkit/api/auth/redis/repository/AccessTokenBlacklistRepository.java
@@ -1,6 +1,6 @@
-package org.bugzkit.api.auth.jwt.redis.repository;
+package org.bugzkit.api.auth.redis.repository;
-import org.bugzkit.api.auth.jwt.redis.model.AccessTokenBlacklist;
+import org.bugzkit.api.auth.redis.model.AccessTokenBlacklist;
import org.springframework.data.repository.CrudRepository;
public interface AccessTokenBlacklistRepository
diff --git a/backend/spring-boot/src/main/java/org/bugzkit/api/auth/jwt/redis/repository/RefreshTokenStoreRepository.java b/backend/spring-boot/src/main/java/org/bugzkit/api/auth/redis/repository/RefreshTokenStoreRepository.java
similarity index 75%
rename from backend/spring-boot/src/main/java/org/bugzkit/api/auth/jwt/redis/repository/RefreshTokenStoreRepository.java
rename to backend/spring-boot/src/main/java/org/bugzkit/api/auth/redis/repository/RefreshTokenStoreRepository.java
index df5557cb..8bf5585a 100644
--- a/backend/spring-boot/src/main/java/org/bugzkit/api/auth/jwt/redis/repository/RefreshTokenStoreRepository.java
+++ b/backend/spring-boot/src/main/java/org/bugzkit/api/auth/redis/repository/RefreshTokenStoreRepository.java
@@ -1,8 +1,8 @@
-package org.bugzkit.api.auth.jwt.redis.repository;
+package org.bugzkit.api.auth.redis.repository;
import java.util.List;
import java.util.Optional;
-import org.bugzkit.api.auth.jwt.redis.model.RefreshTokenStore;
+import org.bugzkit.api.auth.redis.model.RefreshTokenStore;
import org.springframework.data.repository.CrudRepository;
public interface RefreshTokenStoreRepository extends CrudRepository {
diff --git a/backend/spring-boot/src/main/java/org/bugzkit/api/auth/redis/repository/ResetPasswordTokenStoreRepository.java b/backend/spring-boot/src/main/java/org/bugzkit/api/auth/redis/repository/ResetPasswordTokenStoreRepository.java
new file mode 100644
index 00000000..84fabd01
--- /dev/null
+++ b/backend/spring-boot/src/main/java/org/bugzkit/api/auth/redis/repository/ResetPasswordTokenStoreRepository.java
@@ -0,0 +1,7 @@
+package org.bugzkit.api.auth.redis.repository;
+
+import org.bugzkit.api.auth.redis.model.ResetPasswordTokenStore;
+import org.springframework.data.repository.CrudRepository;
+
+public interface ResetPasswordTokenStoreRepository
+ extends CrudRepository {}
diff --git a/backend/spring-boot/src/main/java/org/bugzkit/api/auth/jwt/redis/repository/UserBlacklistRepository.java b/backend/spring-boot/src/main/java/org/bugzkit/api/auth/redis/repository/UserBlacklistRepository.java
similarity index 57%
rename from backend/spring-boot/src/main/java/org/bugzkit/api/auth/jwt/redis/repository/UserBlacklistRepository.java
rename to backend/spring-boot/src/main/java/org/bugzkit/api/auth/redis/repository/UserBlacklistRepository.java
index 8eeb83b8..9b3ee2fa 100644
--- a/backend/spring-boot/src/main/java/org/bugzkit/api/auth/jwt/redis/repository/UserBlacklistRepository.java
+++ b/backend/spring-boot/src/main/java/org/bugzkit/api/auth/redis/repository/UserBlacklistRepository.java
@@ -1,6 +1,6 @@
-package org.bugzkit.api.auth.jwt.redis.repository;
+package org.bugzkit.api.auth.redis.repository;
-import org.bugzkit.api.auth.jwt.redis.model.UserBlacklist;
+import org.bugzkit.api.auth.redis.model.UserBlacklist;
import org.springframework.data.repository.CrudRepository;
public interface UserBlacklistRepository extends CrudRepository {}
diff --git a/backend/spring-boot/src/main/java/org/bugzkit/api/auth/redis/repository/VerificationTokenStoreRepository.java b/backend/spring-boot/src/main/java/org/bugzkit/api/auth/redis/repository/VerificationTokenStoreRepository.java
new file mode 100644
index 00000000..21ea440e
--- /dev/null
+++ b/backend/spring-boot/src/main/java/org/bugzkit/api/auth/redis/repository/VerificationTokenStoreRepository.java
@@ -0,0 +1,7 @@
+package org.bugzkit.api.auth.redis.repository;
+
+import org.bugzkit.api.auth.redis.model.VerificationTokenStore;
+import org.springframework.data.repository.CrudRepository;
+
+public interface VerificationTokenStoreRepository
+ extends CrudRepository {}
diff --git a/backend/spring-boot/src/main/java/org/bugzkit/api/auth/security/JWTFilter.java b/backend/spring-boot/src/main/java/org/bugzkit/api/auth/security/JWTFilter.java
index 3817aa6e..9c63d27e 100644
--- a/backend/spring-boot/src/main/java/org/bugzkit/api/auth/security/JWTFilter.java
+++ b/backend/spring-boot/src/main/java/org/bugzkit/api/auth/security/JWTFilter.java
@@ -6,9 +6,9 @@
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
-import org.bugzkit.api.auth.jwt.service.AccessTokenService;
-import org.bugzkit.api.auth.jwt.util.JwtUtil;
+import org.bugzkit.api.auth.service.AccessTokenService;
import org.bugzkit.api.auth.util.AuthUtil;
+import org.bugzkit.api.auth.util.JwtUtil;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
diff --git a/backend/spring-boot/src/main/java/org/bugzkit/api/auth/jwt/service/AccessTokenService.java b/backend/spring-boot/src/main/java/org/bugzkit/api/auth/service/AccessTokenService.java
similarity index 87%
rename from backend/spring-boot/src/main/java/org/bugzkit/api/auth/jwt/service/AccessTokenService.java
rename to backend/spring-boot/src/main/java/org/bugzkit/api/auth/service/AccessTokenService.java
index 6eb6d5b7..792353d5 100644
--- a/backend/spring-boot/src/main/java/org/bugzkit/api/auth/jwt/service/AccessTokenService.java
+++ b/backend/spring-boot/src/main/java/org/bugzkit/api/auth/service/AccessTokenService.java
@@ -1,4 +1,4 @@
-package org.bugzkit.api.auth.jwt.service;
+package org.bugzkit.api.auth.service;
import java.util.Set;
import org.bugzkit.api.user.payload.dto.RoleDTO;
diff --git a/backend/spring-boot/src/main/java/org/bugzkit/api/auth/jwt/service/RefreshTokenService.java b/backend/spring-boot/src/main/java/org/bugzkit/api/auth/service/RefreshTokenService.java
similarity index 88%
rename from backend/spring-boot/src/main/java/org/bugzkit/api/auth/jwt/service/RefreshTokenService.java
rename to backend/spring-boot/src/main/java/org/bugzkit/api/auth/service/RefreshTokenService.java
index c68e672b..85db0a42 100644
--- a/backend/spring-boot/src/main/java/org/bugzkit/api/auth/jwt/service/RefreshTokenService.java
+++ b/backend/spring-boot/src/main/java/org/bugzkit/api/auth/service/RefreshTokenService.java
@@ -1,4 +1,4 @@
-package org.bugzkit.api.auth.jwt.service;
+package org.bugzkit.api.auth.service;
import java.util.Set;
import org.bugzkit.api.user.payload.dto.RoleDTO;
diff --git a/backend/spring-boot/src/main/java/org/bugzkit/api/auth/jwt/service/ResetPasswordTokenService.java b/backend/spring-boot/src/main/java/org/bugzkit/api/auth/service/ResetPasswordTokenService.java
similarity index 68%
rename from backend/spring-boot/src/main/java/org/bugzkit/api/auth/jwt/service/ResetPasswordTokenService.java
rename to backend/spring-boot/src/main/java/org/bugzkit/api/auth/service/ResetPasswordTokenService.java
index b308ccea..c5ef59e8 100644
--- a/backend/spring-boot/src/main/java/org/bugzkit/api/auth/jwt/service/ResetPasswordTokenService.java
+++ b/backend/spring-boot/src/main/java/org/bugzkit/api/auth/service/ResetPasswordTokenService.java
@@ -1,11 +1,11 @@
-package org.bugzkit.api.auth.jwt.service;
+package org.bugzkit.api.auth.service;
import org.bugzkit.api.user.model.User;
public interface ResetPasswordTokenService {
String create(Long userId);
- void check(String token);
+ Long checkAndConsume(String token);
void sendToEmail(User user, String token);
}
diff --git a/backend/spring-boot/src/main/java/org/bugzkit/api/auth/jwt/service/VerificationTokenService.java b/backend/spring-boot/src/main/java/org/bugzkit/api/auth/service/VerificationTokenService.java
similarity index 68%
rename from backend/spring-boot/src/main/java/org/bugzkit/api/auth/jwt/service/VerificationTokenService.java
rename to backend/spring-boot/src/main/java/org/bugzkit/api/auth/service/VerificationTokenService.java
index cbc6f6a5..7821380f 100644
--- a/backend/spring-boot/src/main/java/org/bugzkit/api/auth/jwt/service/VerificationTokenService.java
+++ b/backend/spring-boot/src/main/java/org/bugzkit/api/auth/service/VerificationTokenService.java
@@ -1,11 +1,11 @@
-package org.bugzkit.api.auth.jwt.service;
+package org.bugzkit.api.auth.service;
import org.bugzkit.api.user.model.User;
public interface VerificationTokenService {
String create(Long userId);
- void check(String token);
+ Long checkAndConsume(String token);
void sendToEmail(User user, String token);
}
diff --git a/backend/spring-boot/src/main/java/org/bugzkit/api/auth/jwt/service/impl/AccessTokenServiceImpl.java b/backend/spring-boot/src/main/java/org/bugzkit/api/auth/service/impl/AccessTokenServiceImpl.java
similarity index 84%
rename from backend/spring-boot/src/main/java/org/bugzkit/api/auth/jwt/service/impl/AccessTokenServiceImpl.java
rename to backend/spring-boot/src/main/java/org/bugzkit/api/auth/service/impl/AccessTokenServiceImpl.java
index d99f8eca..45d23c6e 100644
--- a/backend/spring-boot/src/main/java/org/bugzkit/api/auth/jwt/service/impl/AccessTokenServiceImpl.java
+++ b/backend/spring-boot/src/main/java/org/bugzkit/api/auth/service/impl/AccessTokenServiceImpl.java
@@ -1,15 +1,15 @@
-package org.bugzkit.api.auth.jwt.service.impl;
+package org.bugzkit.api.auth.service.impl;
import com.auth0.jwt.JWT;
import java.time.Instant;
import java.util.Set;
-import org.bugzkit.api.auth.jwt.redis.model.AccessTokenBlacklist;
-import org.bugzkit.api.auth.jwt.redis.model.UserBlacklist;
-import org.bugzkit.api.auth.jwt.redis.repository.AccessTokenBlacklistRepository;
-import org.bugzkit.api.auth.jwt.redis.repository.UserBlacklistRepository;
-import org.bugzkit.api.auth.jwt.service.AccessTokenService;
-import org.bugzkit.api.auth.jwt.util.JwtUtil;
-import org.bugzkit.api.auth.jwt.util.JwtUtil.JwtPurpose;
+import org.bugzkit.api.auth.redis.model.AccessTokenBlacklist;
+import org.bugzkit.api.auth.redis.model.UserBlacklist;
+import org.bugzkit.api.auth.redis.repository.AccessTokenBlacklistRepository;
+import org.bugzkit.api.auth.redis.repository.UserBlacklistRepository;
+import org.bugzkit.api.auth.service.AccessTokenService;
+import org.bugzkit.api.auth.util.JwtUtil;
+import org.bugzkit.api.auth.util.JwtUtil.JwtPurpose;
import org.bugzkit.api.shared.error.exception.UnauthorizedException;
import org.bugzkit.api.user.payload.dto.RoleDTO;
import org.springframework.beans.factory.annotation.Value;
diff --git a/backend/spring-boot/src/main/java/org/bugzkit/api/auth/service/impl/AuthServiceImpl.java b/backend/spring-boot/src/main/java/org/bugzkit/api/auth/service/impl/AuthServiceImpl.java
index d6d59d84..bc589d93 100644
--- a/backend/spring-boot/src/main/java/org/bugzkit/api/auth/service/impl/AuthServiceImpl.java
+++ b/backend/spring-boot/src/main/java/org/bugzkit/api/auth/service/impl/AuthServiceImpl.java
@@ -3,20 +3,20 @@
import java.util.ArrayList;
import java.util.Collections;
import org.bugzkit.api.auth.AuthTokens;
-import org.bugzkit.api.auth.jwt.service.AccessTokenService;
-import org.bugzkit.api.auth.jwt.service.RefreshTokenService;
-import org.bugzkit.api.auth.jwt.service.ResetPasswordTokenService;
-import org.bugzkit.api.auth.jwt.service.VerificationTokenService;
-import org.bugzkit.api.auth.jwt.util.JwtUtil;
import org.bugzkit.api.auth.payload.request.AuthTokensRequest;
import org.bugzkit.api.auth.payload.request.ForgotPasswordRequest;
import org.bugzkit.api.auth.payload.request.RegisterUserRequest;
import org.bugzkit.api.auth.payload.request.ResetPasswordRequest;
import org.bugzkit.api.auth.payload.request.VerificationEmailRequest;
import org.bugzkit.api.auth.payload.request.VerifyEmailRequest;
+import org.bugzkit.api.auth.service.AccessTokenService;
import org.bugzkit.api.auth.service.AuthService;
import org.bugzkit.api.auth.service.DeviceService;
+import org.bugzkit.api.auth.service.RefreshTokenService;
+import org.bugzkit.api.auth.service.ResetPasswordTokenService;
+import org.bugzkit.api.auth.service.VerificationTokenService;
import org.bugzkit.api.auth.util.AuthUtil;
+import org.bugzkit.api.auth.util.JwtUtil;
import org.bugzkit.api.shared.error.exception.BadRequestException;
import org.bugzkit.api.shared.error.exception.ConflictException;
import org.bugzkit.api.shared.error.exception.UnauthorizedException;
@@ -158,10 +158,10 @@ public void forgotPassword(ForgotPasswordRequest forgotPasswordRequest) {
@Override
@Transactional
public void resetPassword(ResetPasswordRequest resetPasswordRequest) {
- resetPasswordTokenService.check(resetPasswordRequest.token());
+ final var userId = resetPasswordTokenService.checkAndConsume(resetPasswordRequest.token());
final var user =
userRepository
- .findById(JwtUtil.getUserId(resetPasswordRequest.token()))
+ .findById(userId)
.orElseThrow(() -> new BadRequestException("auth.tokenInvalid"));
user.setPassword(bCryptPasswordEncoder.encode(resetPasswordRequest.password()));
accessTokenService.invalidateAllByUserId(user.getId());
@@ -183,10 +183,10 @@ public void sendVerificationMail(VerificationEmailRequest request) {
@Override
public void verifyEmail(VerifyEmailRequest verifyEmailRequest) {
- verificationTokenService.check(verifyEmailRequest.token());
+ final var userId = verificationTokenService.checkAndConsume(verifyEmailRequest.token());
final var user =
userRepository
- .findById(JwtUtil.getUserId(verifyEmailRequest.token()))
+ .findById(userId)
.orElseThrow(() -> new BadRequestException("auth.tokenInvalid"));
if (Boolean.TRUE.equals(user.getActive())) return;
user.setActive(true);
diff --git a/backend/spring-boot/src/main/java/org/bugzkit/api/auth/service/impl/DeviceServiceImpl.java b/backend/spring-boot/src/main/java/org/bugzkit/api/auth/service/impl/DeviceServiceImpl.java
index 6909858b..07cf47c3 100644
--- a/backend/spring-boot/src/main/java/org/bugzkit/api/auth/service/impl/DeviceServiceImpl.java
+++ b/backend/spring-boot/src/main/java/org/bugzkit/api/auth/service/impl/DeviceServiceImpl.java
@@ -2,13 +2,13 @@
import java.time.LocalDateTime;
import java.util.List;
-import org.bugzkit.api.auth.jwt.service.AccessTokenService;
-import org.bugzkit.api.auth.jwt.service.RefreshTokenService;
import org.bugzkit.api.auth.mapper.AuthMapper;
import org.bugzkit.api.auth.model.Device;
import org.bugzkit.api.auth.payload.dto.DeviceDTO;
import org.bugzkit.api.auth.repository.DeviceRepository;
+import org.bugzkit.api.auth.service.AccessTokenService;
import org.bugzkit.api.auth.service.DeviceService;
+import org.bugzkit.api.auth.service.RefreshTokenService;
import org.bugzkit.api.auth.util.AuthUtil;
import org.bugzkit.api.user.repository.UserRepository;
import org.springframework.beans.factory.annotation.Value;
diff --git a/backend/spring-boot/src/main/java/org/bugzkit/api/auth/jwt/service/impl/RefreshTokenServiceImpl.java b/backend/spring-boot/src/main/java/org/bugzkit/api/auth/service/impl/RefreshTokenServiceImpl.java
similarity index 86%
rename from backend/spring-boot/src/main/java/org/bugzkit/api/auth/jwt/service/impl/RefreshTokenServiceImpl.java
rename to backend/spring-boot/src/main/java/org/bugzkit/api/auth/service/impl/RefreshTokenServiceImpl.java
index 72fdcb95..e9d489bd 100644
--- a/backend/spring-boot/src/main/java/org/bugzkit/api/auth/jwt/service/impl/RefreshTokenServiceImpl.java
+++ b/backend/spring-boot/src/main/java/org/bugzkit/api/auth/service/impl/RefreshTokenServiceImpl.java
@@ -1,13 +1,13 @@
-package org.bugzkit.api.auth.jwt.service.impl;
+package org.bugzkit.api.auth.service.impl;
import com.auth0.jwt.JWT;
import java.time.Instant;
import java.util.Set;
-import org.bugzkit.api.auth.jwt.redis.model.RefreshTokenStore;
-import org.bugzkit.api.auth.jwt.redis.repository.RefreshTokenStoreRepository;
-import org.bugzkit.api.auth.jwt.service.RefreshTokenService;
-import org.bugzkit.api.auth.jwt.util.JwtUtil;
-import org.bugzkit.api.auth.jwt.util.JwtUtil.JwtPurpose;
+import org.bugzkit.api.auth.redis.model.RefreshTokenStore;
+import org.bugzkit.api.auth.redis.repository.RefreshTokenStoreRepository;
+import org.bugzkit.api.auth.service.RefreshTokenService;
+import org.bugzkit.api.auth.util.JwtUtil;
+import org.bugzkit.api.auth.util.JwtUtil.JwtPurpose;
import org.bugzkit.api.shared.error.exception.BadRequestException;
import org.bugzkit.api.user.payload.dto.RoleDTO;
import org.springframework.beans.factory.annotation.Value;
diff --git a/backend/spring-boot/src/main/java/org/bugzkit/api/auth/service/impl/ResetPasswordTokenServiceImpl.java b/backend/spring-boot/src/main/java/org/bugzkit/api/auth/service/impl/ResetPasswordTokenServiceImpl.java
new file mode 100644
index 00000000..8a7d6a4e
--- /dev/null
+++ b/backend/spring-boot/src/main/java/org/bugzkit/api/auth/service/impl/ResetPasswordTokenServiceImpl.java
@@ -0,0 +1,52 @@
+package org.bugzkit.api.auth.service.impl;
+
+import java.util.UUID;
+import org.bugzkit.api.auth.email.AuthEmailPurpose;
+import org.bugzkit.api.auth.email.OnSendAuthEmail;
+import org.bugzkit.api.auth.redis.model.ResetPasswordTokenStore;
+import org.bugzkit.api.auth.redis.repository.ResetPasswordTokenStoreRepository;
+import org.bugzkit.api.auth.service.ResetPasswordTokenService;
+import org.bugzkit.api.shared.error.exception.BadRequestException;
+import org.bugzkit.api.user.model.User;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.ApplicationEventPublisher;
+import org.springframework.stereotype.Service;
+
+@Service
+public class ResetPasswordTokenServiceImpl implements ResetPasswordTokenService {
+ private final ResetPasswordTokenStoreRepository resetPasswordTokenStoreRepository;
+ private final ApplicationEventPublisher eventPublisher;
+
+ @Value("${jwt.reset-password-token.duration}")
+ private int tokenDuration;
+
+ public ResetPasswordTokenServiceImpl(
+ ResetPasswordTokenStoreRepository resetPasswordTokenStoreRepository,
+ ApplicationEventPublisher eventPublisher) {
+ this.resetPasswordTokenStoreRepository = resetPasswordTokenStoreRepository;
+ this.eventPublisher = eventPublisher;
+ }
+
+ @Override
+ public String create(Long userId) {
+ final var token = UUID.randomUUID().toString();
+ resetPasswordTokenStoreRepository.save(
+ new ResetPasswordTokenStore(token, userId, tokenDuration));
+ return token;
+ }
+
+ @Override
+ public Long checkAndConsume(String token) {
+ final var stored =
+ resetPasswordTokenStoreRepository
+ .findById(token)
+ .orElseThrow(() -> new BadRequestException("auth.tokenInvalid"));
+ resetPasswordTokenStoreRepository.deleteById(token);
+ return stored.getUserId();
+ }
+
+ @Override
+ public void sendToEmail(User user, String token) {
+ eventPublisher.publishEvent(new OnSendAuthEmail(user, token, AuthEmailPurpose.RESET_PASSWORD));
+ }
+}
diff --git a/backend/spring-boot/src/main/java/org/bugzkit/api/auth/service/impl/VerificationTokenServiceImpl.java b/backend/spring-boot/src/main/java/org/bugzkit/api/auth/service/impl/VerificationTokenServiceImpl.java
new file mode 100644
index 00000000..03e6ea29
--- /dev/null
+++ b/backend/spring-boot/src/main/java/org/bugzkit/api/auth/service/impl/VerificationTokenServiceImpl.java
@@ -0,0 +1,51 @@
+package org.bugzkit.api.auth.service.impl;
+
+import java.util.UUID;
+import org.bugzkit.api.auth.email.AuthEmailPurpose;
+import org.bugzkit.api.auth.email.OnSendAuthEmail;
+import org.bugzkit.api.auth.redis.model.VerificationTokenStore;
+import org.bugzkit.api.auth.redis.repository.VerificationTokenStoreRepository;
+import org.bugzkit.api.auth.service.VerificationTokenService;
+import org.bugzkit.api.shared.error.exception.BadRequestException;
+import org.bugzkit.api.user.model.User;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.ApplicationEventPublisher;
+import org.springframework.stereotype.Service;
+
+@Service
+public class VerificationTokenServiceImpl implements VerificationTokenService {
+ private final VerificationTokenStoreRepository verificationTokenStoreRepository;
+ private final ApplicationEventPublisher eventPublisher;
+
+ @Value("${jwt.verify-email-token.duration}")
+ private int tokenDuration;
+
+ public VerificationTokenServiceImpl(
+ VerificationTokenStoreRepository verificationTokenStoreRepository,
+ ApplicationEventPublisher eventPublisher) {
+ this.verificationTokenStoreRepository = verificationTokenStoreRepository;
+ this.eventPublisher = eventPublisher;
+ }
+
+ @Override
+ public String create(Long userId) {
+ final var token = UUID.randomUUID().toString();
+ verificationTokenStoreRepository.save(new VerificationTokenStore(token, userId, tokenDuration));
+ return token;
+ }
+
+ @Override
+ public Long checkAndConsume(String token) {
+ final var stored =
+ verificationTokenStoreRepository
+ .findById(token)
+ .orElseThrow(() -> new BadRequestException("auth.tokenInvalid"));
+ verificationTokenStoreRepository.deleteById(token);
+ return stored.getUserId();
+ }
+
+ @Override
+ public void sendToEmail(User user, String token) {
+ eventPublisher.publishEvent(new OnSendAuthEmail(user, token, AuthEmailPurpose.VERIFY_EMAIL));
+ }
+}
diff --git a/backend/spring-boot/src/main/java/org/bugzkit/api/auth/jwt/util/JwtUtil.java b/backend/spring-boot/src/main/java/org/bugzkit/api/auth/util/JwtUtil.java
similarity index 91%
rename from backend/spring-boot/src/main/java/org/bugzkit/api/auth/jwt/util/JwtUtil.java
rename to backend/spring-boot/src/main/java/org/bugzkit/api/auth/util/JwtUtil.java
index 6e79a52d..00caecc4 100644
--- a/backend/spring-boot/src/main/java/org/bugzkit/api/auth/jwt/util/JwtUtil.java
+++ b/backend/spring-boot/src/main/java/org/bugzkit/api/auth/util/JwtUtil.java
@@ -1,4 +1,4 @@
-package org.bugzkit.api.auth.jwt.util;
+package org.bugzkit.api.auth.util;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
@@ -40,8 +40,6 @@ public static Instant getIssuedAt(String token) {
public enum JwtPurpose {
ACCESS_TOKEN,
- REFRESH_TOKEN,
- VERIFY_EMAIL_TOKEN,
- RESET_PASSWORD_TOKEN
+ REFRESH_TOKEN
}
}
diff --git a/backend/spring-boot/src/main/java/org/bugzkit/api/shared/config/DataInit.java b/backend/spring-boot/src/main/java/org/bugzkit/api/shared/config/DataInit.java
index 45af2d13..b9403bc9 100644
--- a/backend/spring-boot/src/main/java/org/bugzkit/api/shared/config/DataInit.java
+++ b/backend/spring-boot/src/main/java/org/bugzkit/api/shared/config/DataInit.java
@@ -113,6 +113,14 @@ private void testUsers() {
.lock(false)
.roles(Collections.singleton(userRole))
.build(),
+ User.builder()
+ .username("deactivated3")
+ .email("deactivated3@localhost")
+ .password(bCryptPasswordEncoder.encode(password))
+ .active(false)
+ .lock(false)
+ .roles(Collections.singleton(userRole))
+ .build(),
User.builder()
.username("locked")
.email("locked@localhost")
diff --git a/backend/spring-boot/src/main/java/org/bugzkit/api/user/service/impl/ProfileServiceImpl.java b/backend/spring-boot/src/main/java/org/bugzkit/api/user/service/impl/ProfileServiceImpl.java
index d806d080..86102ed9 100644
--- a/backend/spring-boot/src/main/java/org/bugzkit/api/user/service/impl/ProfileServiceImpl.java
+++ b/backend/spring-boot/src/main/java/org/bugzkit/api/user/service/impl/ProfileServiceImpl.java
@@ -1,9 +1,9 @@
package org.bugzkit.api.user.service.impl;
-import org.bugzkit.api.auth.jwt.service.AccessTokenService;
-import org.bugzkit.api.auth.jwt.service.RefreshTokenService;
-import org.bugzkit.api.auth.jwt.service.VerificationTokenService;
+import org.bugzkit.api.auth.service.AccessTokenService;
import org.bugzkit.api.auth.service.DeviceService;
+import org.bugzkit.api.auth.service.RefreshTokenService;
+import org.bugzkit.api.auth.service.VerificationTokenService;
import org.bugzkit.api.auth.util.AuthUtil;
import org.bugzkit.api.shared.error.exception.BadRequestException;
import org.bugzkit.api.shared.error.exception.ConflictException;
diff --git a/backend/spring-boot/src/main/resources/static/openapi.yml b/backend/spring-boot/src/main/resources/static/openapi.yml
index a782820a..930576e2 100644
--- a/backend/spring-boot/src/main/resources/static/openapi.yml
+++ b/backend/spring-boot/src/main/resources/static/openapi.yml
@@ -828,13 +828,12 @@ components:
properties:
token:
type: string
- format: JWT
password:
type: string
confirmPassword:
type: string
example:
- token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJyb2xlcyI6WyJVU0VSIl0sImlzc3VlZEF0IjoiMjAyMS0xMC0yMFQxNDozMzo0NC43MDcyOTMzMDlaIiwiZXhwIjoxNjM0NzQxMzI0LCJ1c2VySWQiOjJ9.uXOVA1q-o2DtHmwBAzEfqEm8GLpAhXrYo0rlZ_6NFbBGILhkV74x-Iu9W2uSfSlwp1IfKPCHlR6zWVPvAbhWVw
+ token: 550e8400-e29b-41d4-a716-446655440000
password: qwerty321
confirmPassword: qwerty321
VerificationEmailRequest:
@@ -863,9 +862,8 @@ components:
properties:
token:
type: string
- format: JWT
example:
- token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJyb2xlcyI6WyJVU0VSIl0sImlzc3VlZEF0IjoiMjAyMS0xMC0yMFQxNDozMzo0NC43MDcyOTMzMDlaIiwiZXhwIjoxNjM0NzQxMzI0LCJ1c2VySWQiOjJ9.uXOVA1q-o2DtHmwBAzEfqEm8GLpAhXrYo0rlZ_6NFbBGILhkV74x-Iu9W2uSfSlwp1IfKPCHlR6zWVPvAbhWVw
+ token: 550e8400-e29b-41d4-a716-446655440000
PatchProfileRequest:
description: Patch profile request
required: true
diff --git a/backend/spring-boot/src/test/java/org/bugzkit/api/admin/integration/UserControllerIT.java b/backend/spring-boot/src/test/java/org/bugzkit/api/admin/integration/UserControllerIT.java
index 02574736..b50a38c3 100644
--- a/backend/spring-boot/src/test/java/org/bugzkit/api/admin/integration/UserControllerIT.java
+++ b/backend/spring-boot/src/test/java/org/bugzkit/api/admin/integration/UserControllerIT.java
@@ -146,7 +146,7 @@ void findAllUsers() throws Exception {
.cookie(new Cookie("accessToken", accessToken)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.length()").value(10))
- .andExpect(jsonPath("$.total").value(11));
+ .andExpect(jsonPath("$.total").value(12));
}
@Test
diff --git a/backend/spring-boot/src/test/java/org/bugzkit/api/auth/data/RefreshTokenStoreRepositoryIT.java b/backend/spring-boot/src/test/java/org/bugzkit/api/auth/data/RefreshTokenStoreRepositoryIT.java
index fb47b566..ee402485 100644
--- a/backend/spring-boot/src/test/java/org/bugzkit/api/auth/data/RefreshTokenStoreRepositoryIT.java
+++ b/backend/spring-boot/src/test/java/org/bugzkit/api/auth/data/RefreshTokenStoreRepositoryIT.java
@@ -3,8 +3,8 @@
import static org.assertj.core.api.Assertions.assertThat;
import java.util.List;
-import org.bugzkit.api.auth.jwt.redis.model.RefreshTokenStore;
-import org.bugzkit.api.auth.jwt.redis.repository.RefreshTokenStoreRepository;
+import org.bugzkit.api.auth.redis.model.RefreshTokenStore;
+import org.bugzkit.api.auth.redis.repository.RefreshTokenStoreRepository;
import org.bugzkit.api.shared.config.DatabaseContainers;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
diff --git a/backend/spring-boot/src/test/java/org/bugzkit/api/auth/integration/AuthControllerIT.java b/backend/spring-boot/src/test/java/org/bugzkit/api/auth/integration/AuthControllerIT.java
index b4d29737..4e863a9f 100644
--- a/backend/spring-boot/src/test/java/org/bugzkit/api/auth/integration/AuthControllerIT.java
+++ b/backend/spring-boot/src/test/java/org/bugzkit/api/auth/integration/AuthControllerIT.java
@@ -6,6 +6,8 @@
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -18,15 +20,16 @@
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import jakarta.servlet.http.Cookie;
-import org.bugzkit.api.auth.jwt.service.impl.ResetPasswordTokenServiceImpl;
-import org.bugzkit.api.auth.jwt.service.impl.VerificationTokenServiceImpl;
-import org.bugzkit.api.auth.jwt.util.JwtUtil;
import org.bugzkit.api.auth.payload.request.AuthTokensRequest;
import org.bugzkit.api.auth.payload.request.ForgotPasswordRequest;
import org.bugzkit.api.auth.payload.request.RegisterUserRequest;
import org.bugzkit.api.auth.payload.request.ResetPasswordRequest;
import org.bugzkit.api.auth.payload.request.VerificationEmailRequest;
import org.bugzkit.api.auth.payload.request.VerifyEmailRequest;
+import org.bugzkit.api.auth.redis.repository.RefreshTokenStoreRepository;
+import org.bugzkit.api.auth.service.impl.ResetPasswordTokenServiceImpl;
+import org.bugzkit.api.auth.service.impl.VerificationTokenServiceImpl;
+import org.bugzkit.api.auth.util.JwtUtil;
import org.bugzkit.api.shared.config.DatabaseContainers;
import org.bugzkit.api.shared.constants.Path;
import org.bugzkit.api.shared.email.service.EmailService;
@@ -57,6 +60,7 @@ class AuthControllerIT extends DatabaseContainers {
@Autowired private VerificationTokenServiceImpl verificationTokenService;
@Autowired private ResetPasswordTokenServiceImpl resetPasswordService;
@Autowired private UserRepository userRepository;
+ @Autowired private RefreshTokenStoreRepository refreshTokenStoreRepository;
@MockitoBean private EmailService emailService;
@@ -345,6 +349,7 @@ void sendVerificationMail_returnsNoContent_userAlreadyActivated() throws Excepti
@Test
void verifyEmail() throws Exception {
final var user = userRepository.findByUsername("deactivated2").orElseThrow();
+ assertFalse(user.getActive());
final var token = verificationTokenService.create(user.getId());
final var verifyEmailRequest = new VerifyEmailRequest(token);
mockMvc
@@ -353,6 +358,8 @@ void verifyEmail() throws Exception {
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(verifyEmailRequest)))
.andExpect(status().isNoContent());
+ final var updateUser = userRepository.findByUsername("deactivated2").orElseThrow();
+ assertTrue(updateUser.getActive());
}
@Test
@@ -382,10 +389,55 @@ void verifyEmail_returnsNoContent_userAlreadyActivated() throws Exception {
.andExpect(status().isNoContent());
}
+ @Test
+ void verifyEmail_secondUseOfSameToken_throwBadRequest() throws Exception {
+ final var user = userRepository.findByUsername("deactivated3").orElseThrow();
+ final var token = verificationTokenService.create(user.getId());
+ final var verifyEmailRequest = new VerifyEmailRequest(token);
+ // First use should succeed
+ mockMvc
+ .perform(
+ post(Path.AUTH + "/verify-email")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(verifyEmailRequest)))
+ .andExpect(status().isNoContent());
+ // Second use of same token should fail (consumed)
+ mockMvc
+ .perform(
+ post(Path.AUTH + "/verify-email")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(verifyEmailRequest)))
+ .andExpect(status().isBadRequest())
+ .andExpect(content().string(containsString("API_ERROR_AUTH_TOKEN_INVALID")));
+ }
+
+ @Test
+ void resetPassword_secondUseOfSameToken_throwBadRequest() throws Exception {
+ final var user = userRepository.findByUsername("update4").orElseThrow();
+ final var token = resetPasswordService.create(user.getId());
+ final var resetPasswordRequest = new ResetPasswordRequest(token, "qwerty12345", "qwerty12345");
+ // First use should succeed
+ mockMvc
+ .perform(
+ post(Path.AUTH + "/password/reset")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(resetPasswordRequest)))
+ .andExpect(status().isNoContent());
+ // Second use of same token should fail (consumed)
+ mockMvc
+ .perform(
+ post(Path.AUTH + "/password/reset")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(resetPasswordRequest)))
+ .andExpect(status().isBadRequest())
+ .andExpect(content().string(containsString("API_ERROR_AUTH_TOKEN_INVALID")));
+ }
+
@Test
void refreshToken_secondUseOfSameToken_throwBadRequest() throws Exception {
final var authTokens = IntegrationTestUtil.authTokens(mockMvc, objectMapper, "update2");
final var refreshToken = authTokens.refreshToken();
+ Thread.sleep(1000);
// First use should succeed
mockMvc
.perform(
diff --git a/backend/spring-boot/src/test/java/org/bugzkit/api/auth/unit/OAuth2SuccessHandlerTest.java b/backend/spring-boot/src/test/java/org/bugzkit/api/auth/unit/OAuth2SuccessHandlerTest.java
index c48360fa..2cff28b8 100644
--- a/backend/spring-boot/src/test/java/org/bugzkit/api/auth/unit/OAuth2SuccessHandlerTest.java
+++ b/backend/spring-boot/src/test/java/org/bugzkit/api/auth/unit/OAuth2SuccessHandlerTest.java
@@ -9,12 +9,12 @@
import java.util.Collections;
import java.util.Map;
import java.util.Set;
-import org.bugzkit.api.auth.jwt.service.AccessTokenService;
-import org.bugzkit.api.auth.jwt.service.RefreshTokenService;
import org.bugzkit.api.auth.oauth2.OAuth2SuccessHandler;
import org.bugzkit.api.auth.oauth2.OAuth2UserPrincipal;
import org.bugzkit.api.auth.security.UserPrincipal;
+import org.bugzkit.api.auth.service.AccessTokenService;
import org.bugzkit.api.auth.service.DeviceService;
+import org.bugzkit.api.auth.service.RefreshTokenService;
import org.bugzkit.api.shared.logger.CustomLogger;
import org.bugzkit.api.user.model.Role;
import org.bugzkit.api.user.model.Role.RoleName;
diff --git a/backend/spring-boot/src/test/java/org/bugzkit/api/user/integration/UserControllerIT.java b/backend/spring-boot/src/test/java/org/bugzkit/api/user/integration/UserControllerIT.java
index 84b29aab..b356ca1c 100644
--- a/backend/spring-boot/src/test/java/org/bugzkit/api/user/integration/UserControllerIT.java
+++ b/backend/spring-boot/src/test/java/org/bugzkit/api/user/integration/UserControllerIT.java
@@ -41,7 +41,7 @@ void findAllUsers() throws Exception {
.perform(get(Path.USERS).contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.length()").value(10))
- .andExpect(jsonPath("$.total").value(11));
+ .andExpect(jsonPath("$.total").value(12));
}
@Test
diff --git a/docs/src/content/how-it-works/auth.mdx b/docs/src/content/how-it-works/auth.mdx
index 355b4a81..147b5f7b 100644
--- a/docs/src/content/how-it-works/auth.mdx
+++ b/docs/src/content/how-it-works/auth.mdx
@@ -107,22 +107,19 @@ Users can sign in with their Google account as an alternative to email/password
### Account Verification
-When a user signs up, a JWT with the purpose `verify_email_token` is created and sent to the user via email as part of a verification link.
+When a user signs up, a random opaque token (UUID) is generated and stored in Redis alongside the user's ID. The token is sent to the user via email as part of a verification link. When the user clicks the link, the token is looked up in Redis to identify the user, then immediately deleted (one-time use). The token automatically expires via Redis TTL if unused.
### Password Reset
-When a user requests a password reset, a JWT with the purpose `reset_password` is generated and sent to the user via email as part of a reset link.
+When a user requests a password reset, a random opaque token (UUID) is generated and stored in Redis alongside the user's ID. The token is sent to the user via email as part of a reset link. When the user submits a new password with the token, it is looked up in Redis, then immediately deleted (one-time use). The token automatically expires via Redis TTL if unused.
### JWT Structure
-All JWTs share the following structure:
+Access and refresh tokens are JWTs with the following structure:
- **issuer**: ID of a user who created the token.
- **issuedAt**: Timestamp of when the token was created.
- **expireAt**: Timestamp of when the token expires.
-- **purpose**: Describes the token's purpose (`access_token`, `refresh_token`, `reset_password_token`, `verify_email_token`).
-
-Access and refresh tokens include additional claims:
-
+- **purpose**: Describes the token's purpose (`access_token`, `refresh_token`).
- **roles**: Represents the roles assigned to the user.
- **deviceId**: Identifies the device/session the token belongs to.
diff --git a/frontend/svelte-kit/src/lib/models/auth/jwt-payload.ts b/frontend/svelte-kit/src/lib/models/auth/jwt-payload.ts
index 03214fd0..2ed5b261 100644
--- a/frontend/svelte-kit/src/lib/models/auth/jwt-payload.ts
+++ b/frontend/svelte-kit/src/lib/models/auth/jwt-payload.ts
@@ -5,13 +5,11 @@ export interface JwtPayload {
iat: number;
exp: number;
purpose: JwtPurpose;
- roles?: RoleName[];
- deviceId?: string;
+ roles: RoleName[];
+ deviceId: string;
}
export enum JwtPurpose {
ACCESS_TOKEN = 'ACCESS_TOKEN',
REFRESH_TOKEN = 'REFRESH_TOKEN',
- VERIFY_EMAIL_TOKEN = 'VERIFY_EMAIL_TOKEN',
- RESET_PASSWORD_TOKEN = 'RESET_PASSWORD_TOKEN',
}
diff --git a/frontend/svelte-kit/src/routes/admin/user/(components)/activate-dialog.svelte b/frontend/svelte-kit/src/routes/admin/user/(components)/activate-dialog.svelte
index 206cbfb5..e053d226 100644
--- a/frontend/svelte-kit/src/routes/admin/user/(components)/activate-dialog.svelte
+++ b/frontend/svelte-kit/src/routes/admin/user/(components)/activate-dialog.svelte
@@ -41,12 +41,13 @@