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
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 @@ -18,6 +18,7 @@
*/
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 +28,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,6 +64,56 @@ public class VisitorController {
@Autowired
private EmployeeRepository employeeRepository;

@GetMapping("/")
public String home(Model model) {
model.addAttribute("visitor", new Visitor());
return "index";
}

@GetMapping("/register")
public String showRegistrationForm(Model model) {
model.addAttribute("visitor", new Visitor());
model.addAttribute("employees", employeeRepository.findAll());
return "visitorRegistration";
}

@PostMapping("/register")
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());

if (bindingResult.hasErrors()) {
logger.warn("Form validation failed with {} errors", bindingResult.getErrorCount());

List<FieldError> fieldErrors = bindingResult.getFieldErrors();
model.addAttribute("fieldErrors", fieldErrors);
model.addAttribute("visitor", visitor);
model.addAttribute("employees", employeeRepository.findAll());

if (hxRequest != null && hxRequest.equals("true")) {
return "fragments/validation-errors";
}
return "visitorRegistration";
}

resolveAndSetHost(visitor, host, employee);

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

if (hxRequest != null && hxRequest.equals("true")) {
return "fragments/otp-modal";
}

return "visitorRegistrationResult";
}

@GetMapping("/report")
public ResponseEntity<?> getReport(@RequestParam String period) {
Expand All @@ -75,46 +128,17 @@ public ResponseEntity<?> getReport(@RequestParam String 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
}

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

@GetMapping("/refresh-employee-cache")
public ResponseEntity<String> refreshEmployeeCache() {
employeeNameCache.initializeCache();
return ResponseEntity.ok("Cache refreshed");
}

@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
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
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";
}

// 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,
Expand All @@ -124,48 +148,37 @@ public Object confirmVisit(@RequestParam("visitId") Long visitId,
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"));
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"));

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 @@ -176,14 +189,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 @@ -192,8 +206,11 @@ 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";

if (hxRequest != null && hxRequest.equals("true")) {
return "fragments/otp-modal";
}
return "otp-modal";
}

/**
Expand All @@ -202,20 +219,17 @@ public String resendOtp(@RequestParam("visitId") Long visitId, Model model) {
private void resolveAndSetHost(Visitor visitor, String hostIdStr, String employeeStr) {
if (visitor == null) return;

// If explicit host id provided, prefer it
if (hostIdStr != null && !hostIdStr.isBlank()) {
try {
Long id = Long.valueOf(hostIdStr.trim());
employeeRepository.findById(id).ifPresent(visitor::setHost);
return;
} catch (NumberFormatException ignored) {
// fall through to name resolution
}
}

// Fall back to name-based resolution if provided
if (employeeStr == null || employeeStr.isBlank()) return;
String trimmed = employeeStr.trim();
employeeRepository.findByNameIgnoreCase(trimmed).ifPresent(visitor::setHost);
}
}
}
Loading
Loading