diff --git a/pom.xml b/pom.xml
index f814f1f..6eed748 100644
--- a/pom.xml
+++ b/pom.xml
@@ -60,6 +60,13 @@
spring-boot-starter-test
test
+
+
+
+ dev.samstevens.totp
+ totp
+ 1.7.1
+
diff --git a/src/main/java/com/rana/performance_engineering_1/controller/RelatedProductController.java b/src/main/java/com/rana/performance_engineering_1/controller/RelatedProductController.java
index fd57a9f..96928ab 100644
--- a/src/main/java/com/rana/performance_engineering_1/controller/RelatedProductController.java
+++ b/src/main/java/com/rana/performance_engineering_1/controller/RelatedProductController.java
@@ -3,15 +3,14 @@
import com.rana.performance_engineering_1.model.Product;
import com.rana.performance_engineering_1.service.ProductSearchService;
import com.rana.performance_engineering_1.service.RelatedProductService;
+import com.rana.performance_engineering_1.service.TotpService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.util.StopWatch;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.PathVariable;
-import org.springframework.web.bind.annotation.RequestParam;
-import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.bind.annotation.*;
import java.util.List;
+import java.util.Map;
import java.util.Optional;
@RestController
@@ -20,12 +19,28 @@ public class RelatedProductController {
private final RelatedProductService relatedProductService;
private final ProductSearchService productSearchService;
+ private final TotpService totpService;
+ /**
+ * GET /products/{productId}?username=john&otp=123456
+ * Requires valid TOTP OTP to access the product.
+ */
@GetMapping("/products/{productId}")
- public ResponseEntity> getProducts(@PathVariable Long productId) {
-
- // 1. Create and start the stopwatch
+ public ResponseEntity> getProducts(
+ @PathVariable Long productId,
+ @RequestParam String username,
+ @RequestParam String otp) {
+
+ // Verify OTP before returning the product
+ if (!totpService.verifyOtp(username, otp)) {
+ return ResponseEntity.status(401).body(Map.of(
+ "error", "Invalid or expired OTP.",
+ "message", "Please provide a valid OTP from Google Authenticator."
+ ));
+ }
+
+ // OTP is valid — proceed to fetch the product
StopWatch stopWatch = new StopWatch("RelatedProducts");
stopWatch.start();
@@ -38,11 +53,25 @@ public ResponseEntity> getProducts(@PathVariable Long productId) {
}
-
+ /**
+ * GET /products/{productId}/related?username=john&otp=123456
+ * Requires valid TOTP OTP to access related products.
+ */
@GetMapping("/products/{productId}/related")
- public List getRelatedProducts(@PathVariable Long productId) {
-
- // 1. Create and start the stopwatch
+ public ResponseEntity> getRelatedProducts(
+ @PathVariable Long productId,
+ @RequestParam String username,
+ @RequestParam String otp) {
+
+ // Verify OTP before returning related products
+ if (!totpService.verifyOtp(username, otp)) {
+ return ResponseEntity.status(401).body(Map.of(
+ "error", "Invalid or expired OTP.",
+ "message", "Please provide a valid OTP from Google Authenticator."
+ ));
+ }
+
+ // OTP is valid — proceed
StopWatch stopWatch = new StopWatch("RelatedProducts");
stopWatch.start();
@@ -51,7 +80,7 @@ public List getRelatedProducts(@PathVariable Long productId) {
System.out.println(stopWatch.prettyPrint());
- return relatedProducts;
+ return ResponseEntity.ok(relatedProducts);
}
@GetMapping("/products/autocomplete")
diff --git a/src/main/java/com/rana/performance_engineering_1/controller/TotpController.java b/src/main/java/com/rana/performance_engineering_1/controller/TotpController.java
new file mode 100644
index 0000000..04853a8
--- /dev/null
+++ b/src/main/java/com/rana/performance_engineering_1/controller/TotpController.java
@@ -0,0 +1,57 @@
+package com.rana.performance_engineering_1.controller;
+
+import com.rana.performance_engineering_1.dto.TotpSetupResponse;
+import com.rana.performance_engineering_1.dto.TotpVerifyRequest;
+import com.rana.performance_engineering_1.service.TotpService;
+import dev.samstevens.totp.exceptions.QrGenerationException;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Map;
+
+@RestController
+@RequestMapping("/totp")
+@RequiredArgsConstructor
+public class TotpController {
+
+ private final TotpService totpService;
+
+ /**
+ * POST /totp/setup?username=john
+ * Registers a user for TOTP and returns the secret + QR code data URI.
+ * The QR code can be scanned with Google Authenticator.
+ */
+ @PostMapping("/setup")
+ public ResponseEntity> setupTotp(@RequestParam String username) {
+ try {
+ TotpSetupResponse response = totpService.setupTotp(username);
+ return ResponseEntity.ok(response);
+ } catch (QrGenerationException e) {
+ return ResponseEntity.internalServerError()
+ .body(Map.of("error", "Failed to generate QR code: " + e.getMessage()));
+ }
+ }
+
+ /**
+ * POST /totp/verify
+ * Verifies the OTP code provided by the user.
+ * Body: { "username": "john", "otp": "123456" }
+ */
+ @PostMapping("/verify")
+ public ResponseEntity> verifyOtp(@RequestBody TotpVerifyRequest request) {
+ boolean isValid = totpService.verifyOtp(request.getUsername(), request.getOtp());
+
+ if (isValid) {
+ return ResponseEntity.ok(Map.of(
+ "valid", true,
+ "message", "OTP verification successful."
+ ));
+ } else {
+ return ResponseEntity.status(401).body(Map.of(
+ "valid", false,
+ "message", "Invalid OTP. Please try again."
+ ));
+ }
+ }
+}
diff --git a/src/main/java/com/rana/performance_engineering_1/dto/TotpSetupResponse.java b/src/main/java/com/rana/performance_engineering_1/dto/TotpSetupResponse.java
new file mode 100644
index 0000000..b484710
--- /dev/null
+++ b/src/main/java/com/rana/performance_engineering_1/dto/TotpSetupResponse.java
@@ -0,0 +1,13 @@
+package com.rana.performance_engineering_1.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+@Getter
+@AllArgsConstructor
+public class TotpSetupResponse {
+ private String username;
+ private String secret;
+ private String qrCodeUri;
+ private String message;
+}
diff --git a/src/main/java/com/rana/performance_engineering_1/dto/TotpVerifyRequest.java b/src/main/java/com/rana/performance_engineering_1/dto/TotpVerifyRequest.java
new file mode 100644
index 0000000..8f2046b
--- /dev/null
+++ b/src/main/java/com/rana/performance_engineering_1/dto/TotpVerifyRequest.java
@@ -0,0 +1,11 @@
+package com.rana.performance_engineering_1.dto;
+
+import lombok.Getter;
+import lombok.Setter;
+
+@Getter
+@Setter
+public class TotpVerifyRequest {
+ private String username;
+ private String otp;
+}
diff --git a/src/main/java/com/rana/performance_engineering_1/model/TotpUser.java b/src/main/java/com/rana/performance_engineering_1/model/TotpUser.java
new file mode 100644
index 0000000..a190bec
--- /dev/null
+++ b/src/main/java/com/rana/performance_engineering_1/model/TotpUser.java
@@ -0,0 +1,28 @@
+package com.rana.performance_engineering_1.model;
+
+import jakarta.persistence.*;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+@Entity
+@Getter
+@Setter
+@NoArgsConstructor
+public class TotpUser {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @Column(unique = true, nullable = false)
+ private String username;
+
+ @Column(nullable = false)
+ private String secret;
+
+ public TotpUser(String username, String secret) {
+ this.username = username;
+ this.secret = secret;
+ }
+}
diff --git a/src/main/java/com/rana/performance_engineering_1/repository/TotpUserRepository.java b/src/main/java/com/rana/performance_engineering_1/repository/TotpUserRepository.java
new file mode 100644
index 0000000..8d66636
--- /dev/null
+++ b/src/main/java/com/rana/performance_engineering_1/repository/TotpUserRepository.java
@@ -0,0 +1,12 @@
+package com.rana.performance_engineering_1.repository;
+
+import com.rana.performance_engineering_1.model.TotpUser;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.Optional;
+
+@Repository
+public interface TotpUserRepository extends JpaRepository {
+ Optional findByUsername(String username);
+}
diff --git a/src/main/java/com/rana/performance_engineering_1/service/TotpService.java b/src/main/java/com/rana/performance_engineering_1/service/TotpService.java
new file mode 100644
index 0000000..b3bd15a
--- /dev/null
+++ b/src/main/java/com/rana/performance_engineering_1/service/TotpService.java
@@ -0,0 +1,100 @@
+package com.rana.performance_engineering_1.service;
+
+import com.rana.performance_engineering_1.dto.TotpSetupResponse;
+import com.rana.performance_engineering_1.model.TotpUser;
+import com.rana.performance_engineering_1.repository.TotpUserRepository;
+import dev.samstevens.totp.code.*;
+import dev.samstevens.totp.exceptions.QrGenerationException;
+import dev.samstevens.totp.qr.QrData;
+import dev.samstevens.totp.qr.ZxingPngQrGenerator;
+import dev.samstevens.totp.secret.DefaultSecretGenerator;
+import dev.samstevens.totp.secret.SecretGenerator;
+import dev.samstevens.totp.time.SystemTimeProvider;
+import dev.samstevens.totp.time.TimeProvider;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+
+import java.util.Optional;
+
+import static dev.samstevens.totp.util.Utils.getDataUriForImage;
+
+@Service
+@RequiredArgsConstructor
+public class TotpService {
+
+ private final TotpUserRepository totpUserRepository;
+
+ /**
+ * Registers a new user and generates a TOTP secret.
+ * Returns the secret and a QR code URI that can be scanned by Google Authenticator.
+ */
+ public TotpSetupResponse setupTotp(String username) throws QrGenerationException {
+ // Check if user already exists
+ Optional existingUser = totpUserRepository.findByUsername(username);
+ if (existingUser.isPresent()) {
+ // Re-generate setup info for existing user
+ String secret = existingUser.get().getSecret();
+ String qrCodeUri = generateQrCodeUri(username, secret);
+ return new TotpSetupResponse(username, secret, qrCodeUri,
+ "User already registered. Use the existing secret or scan the QR code again.");
+ }
+
+ // Generate a new secret
+ SecretGenerator secretGenerator = new DefaultSecretGenerator();
+ String secret = secretGenerator.generate();
+
+ // Save user with secret
+ TotpUser totpUser = new TotpUser(username, secret);
+ totpUserRepository.save(totpUser);
+
+ // Generate QR code URI
+ String qrCodeUri = generateQrCodeUri(username, secret);
+
+ return new TotpSetupResponse(username, secret, qrCodeUri,
+ "TOTP setup successful. Scan the QR code with Google Authenticator.");
+ }
+
+ /**
+ * Verifies the OTP code provided by the user.
+ */
+ public boolean verifyOtp(String username, String otp) {
+ Optional totpUser = totpUserRepository.findByUsername(username);
+ if (totpUser.isEmpty()) {
+ return false;
+ }
+
+ String secret = totpUser.get().getSecret();
+
+ TimeProvider timeProvider = new SystemTimeProvider();
+ CodeGenerator codeGenerator = new DefaultCodeGenerator();
+ CodeVerifier verifier = new DefaultCodeVerifier(codeGenerator, timeProvider);
+
+ return verifier.isValidCode(secret, otp);
+ }
+
+ /**
+ * Checks if a user is registered for TOTP.
+ */
+ public boolean isUserRegistered(String username) {
+ return totpUserRepository.findByUsername(username).isPresent();
+ }
+
+ /**
+ * Generates a QR code data URI for scanning with Google Authenticator.
+ */
+ private String generateQrCodeUri(String username, String secret) throws QrGenerationException {
+ QrData qrData = new QrData.Builder()
+ .label(username)
+ .secret(secret)
+ .issuer("PerformanceEngineeringApp")
+ .algorithm(HashingAlgorithm.SHA1)
+ .digits(6)
+ .period(30)
+ .build();
+
+ ZxingPngQrGenerator qrGenerator = new ZxingPngQrGenerator();
+ byte[] imageData = qrGenerator.generate(qrData);
+
+ return getDataUriForImage(imageData, qrGenerator.getImageMimeType());
+ }
+}