Skip to content
Closed
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.statusneo.vms.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import org.springframework.validation.beanvalidation.MethodValidationPostProcessor;


@Configuration
public class ValidationConfig {


@Bean
public LocalValidatorFactoryBean validator() {
return new LocalValidatorFactoryBean();
}

@Bean
public MethodValidationPostProcessor methodValidationPostProcessor() {
MethodValidationPostProcessor processor = new MethodValidationPostProcessor();
processor.setValidator(validator());
return processor;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
* under the License.
*/
package com.statusneo.vms.controller;

import org.springframework.validation.FieldError;
import com.statusneo.vms.cache.EmployeeNameCache;
import com.statusneo.vms.dto.VerificationResult;
import com.statusneo.vms.model.Visit;
Expand All @@ -27,12 +27,14 @@
import com.statusneo.vms.service.GraphDirectoryService;
import com.statusneo.vms.service.OtpService;
import com.statusneo.vms.service.VisitService;
import jakarta.validation.Valid;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;

import java.time.LocalDateTime;
Expand Down Expand Up @@ -61,119 +63,118 @@ public class VisitorController {
@Autowired
private EmployeeRepository employeeRepository;


@GetMapping("/report")
public ResponseEntity<?> getReport(@RequestParam String period) {
List<Visit> visit;
if (period.equals("daily")) {
visit = visitRepository.findAllByVisitDateBetween(LocalDateTime.now().toLocalDate().atStartOfDay(), LocalDateTime.now());
} else if (period.equals("monthly")) {
visit = visitRepository.findAllByVisitDateBetween(LocalDateTime.now().minusMonths(1), LocalDateTime.now());
} else {
return ResponseEntity.badRequest().body("Invalid period");
}
return ResponseEntity.ok(visit);
}

// @RequestMapping("/error")
public String handleError() {
return "Custom error page!";
}

@GetMapping("/")
public String home() {
return "index"; // Looks for src/main/resources/templates/simple.html
}


@GetMapping("/search")
public String searchEmployees(@RequestParam("employee") String query, Model model) {
logger.info("Received search request for employee: {}", query);
List<String> names = employeeNameCache.getEmployeeNamesByPrefix(query == null ? "" : query);
model.addAttribute("employees", names);
return "employeeSearchResults";
public String home(Model model) {
model.addAttribute("visitor", new Visitor());
return "index";
}

@GetMapping("/refresh-employee-cache")
public ResponseEntity<String> refreshEmployeeCache() {
employeeNameCache.initializeCache();
return ResponseEntity.ok("Cache refreshed");
// Add this method to serve the registration form
@GetMapping("/register")
public String showRegistrationForm(Model model) {
model.addAttribute("visitor", new Visitor());
model.addAttribute("employees", employeeRepository.findAll()); // or employeeService.getAllEmployees()
return "visitorRegistration";
}

@PostMapping("/register")
public String registerVisitor(@ModelAttribute Visitor visitor,
@RequestParam(value = "host", required = false) String host,
@RequestParam(value = "employee", required = false) String employee,
@RequestHeader(value = "HX-Request", required = false) String hxRequest,
Model model) {
// prefer explicit host id, fall back to name
public String registerVisitor(
@Valid @ModelAttribute("visitor") Visitor visitor,
BindingResult bindingResult,
@RequestParam(value = "host", required = false) String host,
@RequestParam(value = "employee", required = false) String employee,
@RequestHeader(value = "HX-Request", required = false) String hxRequest,
Model model) {

logger.info("Processing visitor registration for: {}", visitor.getEmail());

// Check for validation errors - Spring validation pattern
if (bindingResult.hasErrors()) {
logger.warn("Form validation failed with {} errors", bindingResult.getErrorCount());

// Add field errors and visitor to model for display in template
List<FieldError> fieldErrors = bindingResult.getFieldErrors();
model.addAttribute("fieldErrors", fieldErrors);
model.addAttribute("visitor", visitor);
model.addAttribute("employees", employeeRepository.findAll()); // Add employees for dropdown

if (hxRequest != null && hxRequest.equals("true")) {
return "fragments/validation-errors"; // HTMX error fragment
}
// Return the registration form template to show validation errors
return "visitorRegistration";
}

resolveAndSetHost(visitor, host, employee);

Visit savedVisit = visitService.registerVisit(visitor);
model.addAttribute("visitId", savedVisit.getId());

// If it's an HTMX request, just return the modal fragment
model.addAttribute("visit", savedVisit);

if (hxRequest != null && hxRequest.equals("true")) {
// JTE doesn't use Thymeleaf fragment syntax ("::"). Return the template name
// that corresponds to src/main/jte/fragments/otp-modal.jte
return "fragments/otp-modal";
}

// For regular form submission (fallback)
return "otp-modal";
// Regular form submission - return result view
return "visitorRegistrationResult";
}

@GetMapping("/report")
public String getReport(@RequestParam String period, Model model) {
List<Visit> visits;
LocalDateTime now = LocalDateTime.now();

if (period.equals("daily")) {
visits = visitRepository.findAllByVisitDateBetween(now.toLocalDate().atStartOfDay(), now);
} else if (period.equals("monthly")) {
visits = visitRepository.findAllByVisitDateBetween(now.minusMonths(1), now);
} else {
return "error/400";
}
model.addAttribute("visits", visits);
return "report";
}

// Updated to return Object so we can return ResponseEntity for HTMX redirects
@PostMapping("/confirm-visit")
public Object confirmVisit(@RequestParam("visitId") Long visitId,
@RequestParam("otpCode") String otpCode,
@RequestHeader(value = "HX-Request", required = false) String hxRequest,
Model model) {
@RequestParam("otpCode") String otpCode,
@RequestHeader(value = "HX-Request", required = false) String hxRequest,
Model model) {
VerificationResult result = visitService.confirmVisit(visitId, otpCode);
model.addAttribute("result", result);
model.addAttribute("visitId", visitId);

// If it's an HTMX request, return a fragment or an HX-Redirect when attempts exhausted
if (hxRequest != null && hxRequest.equals("true")) {
if (result.success()) {
// Pass the visit to get visitor details for success message
Visit visit = visitRepository.findById(visitId)
.orElseThrow(() -> new IllegalArgumentException("Visit not found"));
.orElseThrow(() -> new IllegalArgumentException("Visit not found"));
model.addAttribute("visit", visit);
// Return the JTE template for success message
return "fragments/success-message";
} else {
// If no more reattempts allowed, tell HTMX to redirect to the entry page
if (!result.reattempt()) {
return ResponseEntity.ok().header("HX-Redirect", "/").build();
}

// Auto-resend OTP when a failed attempt occurred and reattempts remain
Visit visit = visitRepository.findById(visitId)
.orElseThrow(() -> new IllegalArgumentException("Visit not found"));
.orElseThrow(() -> new IllegalArgumentException("Visit not found"));

VerificationResult resendResult = otpService.generateOtp(visit, false); // don't reset attempt counter
VerificationResult resendResult = otpService.generateOtp(visit, false);

// Decide the message to show in the modal: prefer an explicit resend message when OTP re-sent successfully
if (resendResult.success()) {
model.addAttribute("serverMessage", "Invalid OTP. A new OTP has been sent to your email.");
} else {
// If resend failed (cooldown or limit), show that message instead
model.addAttribute("serverMessage", resendResult.message());
}

// Re-show the otp modal with an error message so HTMX swaps it in place
return "fragments/otp-modal";
}
}

// For regular form submission (fallback):

if (result.success()) {
return "confirmation-modal";
} else if (!result.reattempt()) {
// Attempts exhausted: redirect to blank visitor entry form
return "redirect:/";
} else {
// Auto-resend for non-HTMX fallback as well
Visit visit = visitRepository.findById(visitId)
.orElseThrow(() -> new IllegalArgumentException("Visit not found"));

Expand All @@ -184,14 +185,15 @@ public Object confirmVisit(@RequestParam("visitId") Long visitId,
model.addAttribute("serverMessage", resendResult.message());
}

// Re-show otp page with message for non-HTMX fallback
model.addAttribute("visitId", visitId);
return "otp-modal";
}
}

@PostMapping("/resend-otp")
public String resendOtp(@RequestParam("visitId") Long visitId, Model model) {
public String resendOtp(@RequestParam("visitId") Long visitId,
@RequestHeader(value = "HX-Request", required = false) String hxRequest,
Model model) {
Visit visit = visitRepository.findById(visitId)
.orElseThrow(() -> new IllegalArgumentException("Visit not found"));

Expand All @@ -200,8 +202,12 @@ public String resendOtp(@RequestParam("visitId") Long visitId, Model model) {
model.addAttribute("result", result);
model.addAttribute("visitId", visitId);
model.addAttribute("serverMessage", result.message());
// For HTMX flows this should probably return the otp modal again so the UI is updated.
return "fragments/otp-modal";

// Handle both HTMX and regular requests
if (hxRequest != null && hxRequest.equals("true")) {
return "fragments/otp-modal";
}
return "otp-modal";
}

/**
Expand All @@ -226,4 +232,4 @@ private void resolveAndSetHost(Visitor visitor, String hostIdStr, String employe
String trimmed = employeeStr.trim();
employeeRepository.findByNameIgnoreCase(trimmed).ifPresent(visitor::setHost);
}
}
}
Loading