Skip to content

Commit 702da37

Browse files
committed
feat(auth): enforce email verification for all roles
- Always set isVerified=false on registration for USER, ADMIN, PROVIDER - Send verification OTP to all newly registered users - Update AuthService.register() logic and tests accordingly - Fix integration test to manually verify admin and provider after registration - Frontend: prevent double OTP submission in VerifyEmail component - Add submitting state and useCallback to stabilize handleVerify - Add cleanup flag in OtpInput useEffect
1 parent 663486b commit 702da37

14 files changed

Lines changed: 126 additions & 125 deletions

File tree

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
1-
package com.queueless.backend.config;
2-
3-
import com.fasterxml.jackson.core.JsonParser;
4-
import com.fasterxml.jackson.databind.DeserializationContext;
5-
import com.fasterxml.jackson.databind.JsonDeserializer;
6-
import org.springframework.data.mongodb.core.geo.GeoJsonPoint;
7-
8-
import java.io.IOException;
9-
10-
public class GeoJsonPointDeserializer extends JsonDeserializer<GeoJsonPoint> {
11-
@Override
12-
public GeoJsonPoint deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
13-
var node = p.getCodec().readTree(p);
14-
var coords = node.get("coordinates");
15-
double x = coords.get(0).traverse().getValueAsDouble();
16-
double y = coords.get(1).traverse().getValueAsDouble();
17-
return new GeoJsonPoint(x, y);
18-
}
19-
}
1+
//package com.queueless.backend.config;
2+
//
3+
//import com.fasterxml.jackson.core.JsonParser;
4+
//import com.fasterxml.jackson.databind.DeserializationContext;
5+
//import com.fasterxml.jackson.databind.JsonDeserializer;
6+
//import org.springframework.data.mongodb.core.geo.GeoJsonPoint;
7+
//
8+
//import java.io.IOException;
9+
//
10+
//public class GeoJsonPointDeserializer extends JsonDeserializer<GeoJsonPoint> {
11+
// @Override
12+
// public GeoJsonPoint deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
13+
// var node = p.getCodec().readTree(p);
14+
// var coords = node.get("coordinates");
15+
// double x = coords.get(0).traverse().getValueAsDouble();
16+
// double y = coords.get(1).traverse().getValueAsDouble();
17+
// return new GeoJsonPoint(x, y);
18+
// }
19+
//}
Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,25 @@
1-
package com.queueless.backend.config;
2-
3-
import com.fasterxml.jackson.core.JsonGenerator;
4-
import com.fasterxml.jackson.core.JsonParser;
5-
import com.fasterxml.jackson.databind.DeserializationContext;
6-
import com.fasterxml.jackson.databind.JsonDeserializer;
7-
import com.fasterxml.jackson.databind.JsonSerializer;
8-
import com.fasterxml.jackson.databind.SerializerProvider;
9-
import org.springframework.data.mongodb.core.geo.GeoJsonPoint;
10-
11-
import java.io.IOException;
12-
13-
public class GeoJsonPointSerializer extends JsonSerializer<GeoJsonPoint> {
14-
@Override
15-
public void serialize(GeoJsonPoint value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
16-
gen.writeStartObject();
17-
gen.writeStringField("type", "Point");
18-
gen.writeArrayFieldStart("coordinates");
19-
gen.writeNumber(value.getX());
20-
gen.writeNumber(value.getY());
21-
gen.writeEndArray();
22-
gen.writeEndObject();
23-
}
24-
}
25-
1+
//package com.queueless.backend.config;
2+
//
3+
//import com.fasterxml.jackson.core.JsonGenerator;
4+
//import com.fasterxml.jackson.core.JsonParser;
5+
//import com.fasterxml.jackson.databind.DeserializationContext;
6+
//import com.fasterxml.jackson.databind.JsonDeserializer;
7+
//import com.fasterxml.jackson.databind.JsonSerializer;
8+
//import com.fasterxml.jackson.databind.SerializerProvider;
9+
//import org.springframework.data.mongodb.core.geo.GeoJsonPoint;
10+
//
11+
//import java.io.IOException;
12+
//
13+
//public class GeoJsonPointSerializer extends JsonSerializer<GeoJsonPoint> {
14+
// @Override
15+
// public void serialize(GeoJsonPoint value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
16+
// gen.writeStartObject();
17+
// gen.writeStringField("type", "Point");
18+
// gen.writeArrayFieldStart("coordinates");
19+
// gen.writeNumber(value.getX());
20+
// gen.writeNumber(value.getY());
21+
// gen.writeEndArray();
22+
// gen.writeEndObject();
23+
// }
24+
//}
25+
//

backend/backend/src/main/java/com/queueless/backend/controller/AdminController.java

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import org.springframework.http.ResponseEntity;
2727
import org.springframework.security.core.Authentication;
2828
import org.springframework.security.core.context.SecurityContextHolder;
29+
import org.springframework.validation.annotation.Validated;
2930
import org.springframework.web.bind.annotation.*;
3031

3132
import java.io.IOException;
@@ -39,6 +40,7 @@
3940
@RequestMapping("/api/admin")
4041
@RequiredArgsConstructor
4142
@Tag(name = "Admin", description = "Endpoints for admin dashboard and management")
43+
@Validated
4244
public class AdminController {
4345

4446
private final PaymentRepository paymentRepository;
@@ -95,23 +97,6 @@ public Map<String, Object> getAdminStats() {
9597
}
9698
}
9799

98-
// @GetMapping("/providers")
99-
// @AdminOnly
100-
// @Operation(summary = "Get providers with their queues", description = "Returns all providers under this admin, along with their queues and statistics.")
101-
// @ApiResponse(responseCode = "200", description = "List of provider data")
102-
// @ApiResponse(responseCode = "500", description = "Internal server error")
103-
// public List<Map<String, Object>> getProvidersWithQueues() {
104-
// Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
105-
// String adminId = authentication.getName();
106-
//
107-
// try {
108-
// return adminService.getProvidersWithQueues(adminId);
109-
// } catch (Exception e) {
110-
// log.error("Failed to fetch providers with queues for adminId: {}", adminId, e);
111-
// throw new RuntimeException("Failed to fetch providers data");
112-
// }
113-
// }
114-
115100
@GetMapping("/queues")
116101
@AdminOnly
117102
@Operation(summary = "Get all queues under admin's places", description = "Returns a list of all queues belonging to places owned by the admin.")

backend/backend/src/main/java/com/queueless/backend/controller/PlaceController.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
import io.swagger.v3.oas.annotations.responses.ApiResponse;
1212
import io.swagger.v3.oas.annotations.tags.Tag;
1313
import jakarta.validation.Valid;
14+
import jakarta.validation.constraints.Max;
15+
import jakarta.validation.constraints.Positive;
1416
import lombok.RequiredArgsConstructor;
1517
import lombok.extern.slf4j.Slf4j;
1618
import org.springframework.data.domain.Page;
@@ -20,6 +22,7 @@
2022
import org.springframework.http.ResponseEntity;
2123
import org.springframework.security.core.Authentication;
2224
import org.springframework.security.core.context.SecurityContextHolder;
25+
import org.springframework.validation.annotation.Validated;
2326
import org.springframework.web.bind.annotation.*;
2427

2528
import java.util.List;
@@ -30,6 +33,7 @@
3033
@RequestMapping("/api/places")
3134
@RequiredArgsConstructor
3235
@Tag(name = "Places", description = "Endpoints for managing places")
36+
@Validated
3337
public class PlaceController {
3438

3539
private final PlaceService placeService;
@@ -161,7 +165,7 @@ public ResponseEntity<List<PlaceDTO>> getPlacesByType(@PathVariable String type)
161165
public ResponseEntity<List<PlaceDTO>> getNearbyPlaces(
162166
@Parameter(description = "Longitude") @RequestParam double longitude,
163167
@Parameter(description = "Latitude") @RequestParam double latitude,
164-
@Parameter(description = "Radius in kilometers") @RequestParam(defaultValue = "5") double radius) {
168+
@Parameter(description = "Radius in kilometers") @RequestParam(defaultValue = "5") @Positive @Max(50) double radius) {
165169
log.debug("Searching nearby places [lon={}, lat={}, radius={}km]", longitude, latitude, radius);
166170
List<Place> places = placeService.getNearbyPlaces(longitude, latitude, radius);
167171
log.info("Found {} nearby places", places.size());

backend/backend/src/main/java/com/queueless/backend/controller/UserController.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,6 @@ public ResponseEntity<Map<String, Object>> getUserTokenHistory(
205205
return ResponseEntity.ok(data);
206206
}
207207

208-
// In UserController.java
209208

210209
@PostMapping("/fcm-token")
211210
@Authenticated

backend/backend/src/main/java/com/queueless/backend/dto/SearchRequestDTO.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.queueless.backend.dto;
22

3+
import jakarta.validation.constraints.Positive;
34
import lombok.Data;
45
import lombok.AllArgsConstructor;
56
import lombok.NoArgsConstructor;
@@ -20,13 +21,16 @@ public class SearchRequestDTO {
2021
@Min(0) @Max(5)
2122
private Double minRating = 0.0;
2223

24+
@Positive
2325
private Integer maxWaitTime;
2426
private Boolean supportsGroupToken;
2527
private Boolean emergencySupport;
2628
private Boolean isActive = true;
2729

2830
private Double longitude;
2931
private Double latitude;
32+
33+
@Positive
3034
private Double radius = 5.0; // in kilometers
3135

3236
private Boolean searchPlaces = true;

backend/backend/src/main/java/com/queueless/backend/exception/GlobalExceptionHandler.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.queueless.backend.exception;
22

33
import jakarta.servlet.http.HttpServletRequest;
4+
import jakarta.validation.ConstraintViolationException;
45
import lombok.extern.slf4j.Slf4j;
56
import org.springframework.http.HttpStatus;
67
import org.springframework.http.ResponseEntity;
@@ -139,6 +140,18 @@ public ResponseEntity<ApiError> handleInvalidToken(InvalidTokenException ex, Htt
139140
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(error);
140141
}
141142

143+
@ExceptionHandler(ConstraintViolationException.class)
144+
public ResponseEntity<ApiError> handleConstraintViolation(ConstraintViolationException ex, HttpServletRequest request) {
145+
log.warn("Constraint violation: {} - {}", request.getRequestURI(), ex.getMessage());
146+
ApiError error = new ApiError(
147+
HttpStatus.BAD_REQUEST.value(),
148+
HttpStatus.BAD_REQUEST.getReasonPhrase(),
149+
ex.getMessage(),
150+
request.getRequestURI()
151+
);
152+
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(error);
153+
}
154+
142155
@lombok.Data
143156
@lombok.NoArgsConstructor
144157
public static class ValidationErrorResponse extends ApiError {

backend/backend/src/main/java/com/queueless/backend/security/annotations/AdminOnly.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
// Create security annotations package
21
package com.queueless.backend.security.annotations;
32

43
import org.springframework.security.access.prepost.PreAuthorize;

backend/backend/src/main/java/com/queueless/backend/service/AuthService.java

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -81,10 +81,8 @@ public String register(RegisterRequest request) {
8181
throw new RuntimeException("Email already registered");
8282
}
8383

84-
boolean isVerifiedAutomatically = false;
84+
// Token validation for ADMIN and PROVIDER
8585
Token token = null;
86-
87-
// For ADMIN or PROVIDER, validate the token
8886
if (request.getRole() == Role.ADMIN || request.getRole() == Role.PROVIDER) {
8987
if (request.getToken() == null || request.getToken().isEmpty()) {
9088
log.error("Registration failed - Token required for role: {}", request.getRole());
@@ -122,7 +120,6 @@ public String register(RegisterRequest request) {
122120
token.setUsed(true);
123121
tokenRepository.save(token);
124122
log.debug("Token {} marked as used for {}", token.getTokenValue(), request.getEmail());
125-
isVerifiedAutomatically = true;
126123
}
127124

128125
// Build user preferences
@@ -161,6 +158,7 @@ public String register(RegisterRequest request) {
161158
}
162159
}
163160

161+
// Build user – isVerified is always false initially
164162
User user = User.builder()
165163
.name(request.getName())
166164
.email(request.getEmail())
@@ -169,7 +167,7 @@ public String register(RegisterRequest request) {
169167
.role(request.getRole())
170168
.placeId(request.getPlaceId())
171169
.profileImageUrl(null)
172-
.isVerified(isVerifiedAutomatically)
170+
.isVerified(false) // <-- changed: always false
173171
.preferences(userPreferences)
174172
.ownedPlaceIds(new ArrayList<>())
175173
.adminId(adminId)
@@ -178,14 +176,13 @@ public String register(RegisterRequest request) {
178176
.build();
179177

180178
userRepository.save(user);
181-
if (request.getRole() == Role.USER) {
182-
sendVerificationOtp(request.getEmail());
183-
}
184-
log.info("Registration successful for email: {} | Role: {}", user.getEmail(), user.getRole());
185179

186-
return "User registered successfully!";
187-
}
180+
// Send verification OTP for ALL roles
181+
sendVerificationOtp(request.getEmail());
188182

183+
log.info("Registration successful for email: {} | Role: {}", user.getEmail(), user.getRole());
184+
return "User registered successfully! Please verify your email.";
185+
}
189186
private void sendVerificationOtp(String email) {
190187
// Clean previous OTPs
191188
otpRepository.deleteByEmail(email);

backend/backend/src/test/java/com/queueless/backend/integration/QueueFlowIntegrationTest.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,12 +99,18 @@ void fullQueueFlow() throws Exception {
9999
ResponseEntity<String> adminRegResponse = restTemplate.postForEntity("/api/auth/register", adminReq, String.class);
100100
assertThat(adminRegResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
101101

102+
103+
// Manually verify admin
104+
var adminUser = userRepository.findByEmail("admin@example.com").orElseThrow();
105+
adminUser.setIsVerified(true);
106+
userRepository.save(adminUser);
107+
adminId = adminUser.getId();
102108
// 4. Login as ADMIN
103109
LoginRequest adminLogin = new LoginRequest("admin@example.com", "AdminPass123");
104110
ResponseEntity<JwtResponse> adminLoginResponse = restTemplate.postForEntity("/api/auth/login", adminLogin, JwtResponse.class);
105111
assertThat(adminLoginResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
106112
adminToken = adminLoginResponse.getBody().getToken();
107-
adminId = adminLoginResponse.getBody().getUserId();
113+
108114

109115
// 5. Create a place
110116
PlaceDTO placeDto = new PlaceDTO();
@@ -163,6 +169,10 @@ void fullQueueFlow() throws Exception {
163169
assertThat(providerRegResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
164170
providerEmail = "provider@example.com";
165171

172+
// Manually verify provider
173+
var providerUser = userRepository.findByEmail("provider@example.com").orElseThrow();
174+
providerUser.setIsVerified(true);
175+
userRepository.save(providerUser);
166176
// 9. Login as PROVIDER
167177
LoginRequest providerLogin = new LoginRequest(providerEmail, "ProvPass123");
168178
ResponseEntity<JwtResponse> providerLoginResponse = restTemplate.postForEntity("/api/auth/login", providerLogin, JwtResponse.class);

0 commit comments

Comments
 (0)