Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,13 @@
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

<!-- TOTP (Google Authenticator) support -->
<dependency>
<groupId>dev.samstevens.totp</groupId>
<artifactId>totp</artifactId>
<version>1.7.1</version>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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();
Expand All @@ -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<Product> 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();
Expand All @@ -51,7 +80,7 @@ public List<Product> getRelatedProducts(@PathVariable Long productId) {

System.out.println(stopWatch.prettyPrint());

return relatedProducts;
return ResponseEntity.ok(relatedProducts);
}

@GetMapping("/products/autocomplete")
Expand Down
Original file line number Diff line number Diff line change
@@ -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."
));
}
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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<TotpUser, Long> {
Optional<TotpUser> findByUsername(String username);
}
Original file line number Diff line number Diff line change
@@ -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<TotpUser> 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> 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());
}
}