From 20242a5e0e1aab38efe46ea74d015f6e829b8f75 Mon Sep 17 00:00:00 2001 From: Anas Khan Date: Mon, 17 Nov 2025 21:38:17 +0530 Subject: [PATCH 1/4] Error handling fixes 17-11-25 --- .../vms/controller/ApiController.java | 78 +++++++++++++++++++ .../vms/controller/VisitorController.java | 78 +++++++------------ .../vms/exception/GlobalExceptionHandler.java | 42 +++++----- web-backend/src/main/jte/error/400.jte | 8 -- web-backend/src/main/jte/error/404.jte | 8 -- web-backend/src/main/jte/error/500.jte | 8 -- .../src/main/resources/application.yml | 12 ++- .../resources/static/images/error/400.html | 21 +++++ .../resources/static/images/error/404.html | 21 +++++ .../resources/static/images/error/500.html | 21 +++++ 10 files changed, 199 insertions(+), 98 deletions(-) create mode 100644 web-backend/src/main/java/com/statusneo/vms/controller/ApiController.java delete mode 100644 web-backend/src/main/jte/error/400.jte delete mode 100644 web-backend/src/main/jte/error/404.jte delete mode 100644 web-backend/src/main/jte/error/500.jte create mode 100644 web-backend/src/main/resources/static/images/error/400.html create mode 100644 web-backend/src/main/resources/static/images/error/404.html create mode 100644 web-backend/src/main/resources/static/images/error/500.html diff --git a/web-backend/src/main/java/com/statusneo/vms/controller/ApiController.java b/web-backend/src/main/java/com/statusneo/vms/controller/ApiController.java new file mode 100644 index 0000000..6357cc1 --- /dev/null +++ b/web-backend/src/main/java/com/statusneo/vms/controller/ApiController.java @@ -0,0 +1,78 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package com.statusneo.vms.controller; + +import com.statusneo.vms.cache.EmployeeNameCache; +import com.statusneo.vms.model.Visit; +import com.statusneo.vms.repository.VisitRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDateTime; +import java.util.List; + +@RestController +@RequestMapping("/api") +public class ApiController { + + private static final Logger logger = LoggerFactory.getLogger(ApiController.class); + + @Autowired + private VisitRepository visitRepository; + + @Autowired + private EmployeeNameCache employeeNameCache; + + @GetMapping("/report") + public ResponseEntity getReport(@RequestParam String period) { + logger.info("API: Generating report for period: {}", period); + + List visits; + if (period.equals("daily")) { + visits = visitRepository.findAllByVisitDateBetween( + LocalDateTime.now().toLocalDate().atStartOfDay(), + LocalDateTime.now() + ); + } else if (period.equals("monthly")) { + visits = visitRepository.findAllByVisitDateBetween( + LocalDateTime.now().minusMonths(1), + LocalDateTime.now() + ); + } else { + return ResponseEntity.badRequest().body("Invalid period. Use 'daily' or 'monthly'."); + } + + return ResponseEntity.ok(visits); + } + + @GetMapping("/refresh-employee-cache") + public ResponseEntity refreshEmployeeCache() { + logger.info("API: Refreshing employee cache"); + employeeNameCache.initializeCache(); + return ResponseEntity.ok("Employee cache refreshed successfully"); + } + + @GetMapping("/health") + public ResponseEntity healthCheck() { + return ResponseEntity.ok("API is healthy"); + } +} \ No newline at end of file diff --git a/web-backend/src/main/java/com/statusneo/vms/controller/VisitorController.java b/web-backend/src/main/java/com/statusneo/vms/controller/VisitorController.java index 391fba7..82846a2 100644 --- a/web-backend/src/main/java/com/statusneo/vms/controller/VisitorController.java +++ b/web-backend/src/main/java/com/statusneo/vms/controller/VisitorController.java @@ -35,7 +35,6 @@ import org.springframework.ui.Model; import org.springframework.web.bind.annotation.*; -import java.time.LocalDateTime; import java.util.List; @Controller @@ -62,30 +61,11 @@ public class VisitorController { private EmployeeRepository employeeRepository; - @GetMapping("/report") - public ResponseEntity getReport(@RequestParam String period) { - List 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 + return "index"; } - @GetMapping("/search") public String searchEmployees(@RequestParam("employee") String query, Model model) { logger.info("Received search request for employee: {}", query); @@ -94,27 +74,34 @@ public String searchEmployees(@RequestParam("employee") String query, Model mode return "employeeSearchResults"; } - @GetMapping("/refresh-employee-cache") - public ResponseEntity refreshEmployeeCache() { - employeeNameCache.initializeCache(); - return ResponseEntity.ok("Cache refreshed"); + // Optional: Add MVC version of report page if you want a web UI for reports + @GetMapping("/report-page") + public String showReportPage(@RequestParam(defaultValue = "daily") String period, Model model) { + logger.info("Displaying report page for period: {}", period); + model.addAttribute("period", period); + return "report"; // You'll need to create report.jte template + } + + // Optional: Add MVC version of cache refresh status + @GetMapping("/cache-status") + public String showCacheStatus(Model model) { + model.addAttribute("cacheStatus", "Employee cache is active"); + return "cache-status"; // You'll need to create cache-status.jte template } @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) { + @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"; } @@ -122,24 +109,20 @@ public String registerVisitor(@ModelAttribute Visitor visitor, 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, - @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 @@ -149,31 +132,26 @@ public Object confirmVisit(@RequestParam("visitId") Long visitId, // 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")); @@ -184,7 +162,6 @@ 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"; } @@ -200,7 +177,6 @@ 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"; } @@ -226,4 +202,4 @@ private void resolveAndSetHost(Visitor visitor, String hostIdStr, String employe String trimmed = employeeStr.trim(); employeeRepository.findByNameIgnoreCase(trimmed).ifPresent(visitor::setHost); } -} +} \ No newline at end of file diff --git a/web-backend/src/main/java/com/statusneo/vms/exception/GlobalExceptionHandler.java b/web-backend/src/main/java/com/statusneo/vms/exception/GlobalExceptionHandler.java index f1abf22..3713836 100644 --- a/web-backend/src/main/java/com/statusneo/vms/exception/GlobalExceptionHandler.java +++ b/web-backend/src/main/java/com/statusneo/vms/exception/GlobalExceptionHandler.java @@ -22,9 +22,11 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.http.HttpStatus; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; import java.util.NoSuchElementException; @ControllerAdvice @@ -32,37 +34,33 @@ public class GlobalExceptionHandler { private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class); @ExceptionHandler(NoSuchElementException.class) - public String handleNoSuchElement(NoSuchElementException ex, Model model) { - logger.warn("Resource not found", ex); - model.addAttribute("error", "The requested resource was not found."); - return "404"; + @ResponseStatus(HttpStatus.NOT_FOUND) + public String handleNoSuchElement(NoSuchElementException ex) { + logger.warn("Resource not found: {}", ex.getMessage()); + // Spring Boot will automatically serve static/error/404.html + return "error/404"; } - @ExceptionHandler(DataIntegrityViolationException.class) - public String handleDataIntegrityViolation(DataIntegrityViolationException ex, Model model) { - logger.error("Data integrity violation", ex); - model.addAttribute("error", "A data validation error occurred. Please check your input."); - return "error/400"; - } - - @ExceptionHandler(IllegalArgumentException.class) - public String handleIllegalArgument(IllegalArgumentException ex, Model model) { - logger.warn("Invalid argument", ex); - model.addAttribute("error", "Invalid input provided."); + @ExceptionHandler({DataIntegrityViolationException.class, IllegalArgumentException.class}) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public String handleBadRequestExceptions(Exception ex) { + logger.warn("Bad request: {}", ex.getMessage()); return "error/400"; } @ExceptionHandler(TemplateException.class) - public String handleTemplateException(TemplateException ex, Model model) { - logger.error("Template rendering failed", ex); - model.addAttribute("error", "A technical error occurred while rendering the page."); + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public String handleTemplateException(TemplateException ex) { + logger.error("Template rendering failed: {}", ex.getMessage()); + // CRITICAL: Use static page, NOT another template return "error/500"; } @ExceptionHandler(Exception.class) - public String handleGeneralException(Exception ex, Model model) { - logger.error("Unexpected error occurred", ex); - model.addAttribute("error", "Something went wrong. Please try again later."); + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public String handleGeneralException(Exception ex) { + logger.error("Unexpected error occurred: {}", ex.getMessage()); + // CRITICAL: Use static page, NOT JTE template return "error/500"; } -} +} \ No newline at end of file diff --git a/web-backend/src/main/jte/error/400.jte b/web-backend/src/main/jte/error/400.jte deleted file mode 100644 index f922a54..0000000 --- a/web-backend/src/main/jte/error/400.jte +++ /dev/null @@ -1,8 +0,0 @@ - -Bad Request - -

Invalid Request

-

@error

-Back to Home - - \ No newline at end of file diff --git a/web-backend/src/main/jte/error/404.jte b/web-backend/src/main/jte/error/404.jte deleted file mode 100644 index f51a3e6..0000000 --- a/web-backend/src/main/jte/error/404.jte +++ /dev/null @@ -1,8 +0,0 @@ - -404 Not Found - -

Oops! Page Not Found

-

@error

-Back to Home - - \ No newline at end of file diff --git a/web-backend/src/main/jte/error/500.jte b/web-backend/src/main/jte/error/500.jte deleted file mode 100644 index ead84d9..0000000 --- a/web-backend/src/main/jte/error/500.jte +++ /dev/null @@ -1,8 +0,0 @@ - -Server Error - -

Internal Server Error

-

@error

-Back to Home - - \ No newline at end of file diff --git a/web-backend/src/main/resources/application.yml b/web-backend/src/main/resources/application.yml index f283eae..f1efa1a 100644 --- a/web-backend/src/main/resources/application.yml +++ b/web-backend/src/main/resources/application.yml @@ -4,11 +4,15 @@ spring: docker: compose: lifecycle-management: start_only - jpa: hibernate: ddl-auto: none show-sql: true + mvc: + static-path-pattern: /** + web: + resources: + static-locations: classpath:/static/ security: oauth2: client: @@ -26,6 +30,11 @@ spring: baseline-on-migrate: true locations: classpath:db/migration +server: + error: + whitelabel: + enabled: false + visitor: system: notification: @@ -40,6 +49,7 @@ visitor: gg: jte: developmentMode: true + precompile: false vms: system-email: test@email.com diff --git a/web-backend/src/main/resources/static/images/error/400.html b/web-backend/src/main/resources/static/images/error/400.html new file mode 100644 index 0000000..f62a3b2 --- /dev/null +++ b/web-backend/src/main/resources/static/images/error/400.html @@ -0,0 +1,21 @@ + + + + 400 - Bad Request + + + + +
+

400 - Bad Request

+

Invalid request. Please check your input and try again.

+ Return to Home +
+ + \ No newline at end of file diff --git a/web-backend/src/main/resources/static/images/error/404.html b/web-backend/src/main/resources/static/images/error/404.html new file mode 100644 index 0000000..e690dbd --- /dev/null +++ b/web-backend/src/main/resources/static/images/error/404.html @@ -0,0 +1,21 @@ + + + + 404 - Page Not Found + + + + +
+

404 - Page Not Found

+

The page you're looking for doesn't exist or has been moved.

+ Return to Home +
+ + \ No newline at end of file diff --git a/web-backend/src/main/resources/static/images/error/500.html b/web-backend/src/main/resources/static/images/error/500.html new file mode 100644 index 0000000..6b7ead0 --- /dev/null +++ b/web-backend/src/main/resources/static/images/error/500.html @@ -0,0 +1,21 @@ + + + + 500 - Server Error + + + + +
+

500 - Server Error

+

Something went wrong on our end. Please try again later.

+ Return to Home +
+ + \ No newline at end of file From 42f6e1e2ebf65f7922b2dfbbd6e610753fec4c0f Mon Sep 17 00:00:00 2001 From: Anas Khan Date: Mon, 17 Nov 2025 21:40:58 +0530 Subject: [PATCH 2/4] Error handling fixes 17-11-25 (2) --- .../com/statusneo/vms/exception/GlobalExceptionHandler.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/web-backend/src/main/java/com/statusneo/vms/exception/GlobalExceptionHandler.java b/web-backend/src/main/java/com/statusneo/vms/exception/GlobalExceptionHandler.java index 3713836..ba03450 100644 --- a/web-backend/src/main/java/com/statusneo/vms/exception/GlobalExceptionHandler.java +++ b/web-backend/src/main/java/com/statusneo/vms/exception/GlobalExceptionHandler.java @@ -23,7 +23,6 @@ import org.slf4j.LoggerFactory; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.http.HttpStatus; -import org.springframework.ui.Model; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; @@ -37,7 +36,6 @@ public class GlobalExceptionHandler { @ResponseStatus(HttpStatus.NOT_FOUND) public String handleNoSuchElement(NoSuchElementException ex) { logger.warn("Resource not found: {}", ex.getMessage()); - // Spring Boot will automatically serve static/error/404.html return "error/404"; } @@ -52,7 +50,6 @@ public String handleBadRequestExceptions(Exception ex) { @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public String handleTemplateException(TemplateException ex) { logger.error("Template rendering failed: {}", ex.getMessage()); - // CRITICAL: Use static page, NOT another template return "error/500"; } @@ -60,7 +57,6 @@ public String handleTemplateException(TemplateException ex) { @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public String handleGeneralException(Exception ex) { logger.error("Unexpected error occurred: {}", ex.getMessage()); - // CRITICAL: Use static page, NOT JTE template return "error/500"; } } \ No newline at end of file From f9bd859e2a7f3f0600976a4a1b4b4266b3804ff1 Mon Sep 17 00:00:00 2001 From: Anas Khan Date: Wed, 19 Nov 2025 22:31:19 +0530 Subject: [PATCH 3/4] Validation changes 19-11-2025 --- .../vms/config/ValidationConfig.java | 24 +++ .../vms/controller/ApiController.java | 78 -------- .../vms/controller/VisitorController.java | 77 ++++---- .../vms/exception/GlobalExceptionHandler.java | 124 ++++++++++--- .../java/com/statusneo/vms/model/Visitor.java | 10 +- web-backend/src/main/jte/error/400.jte | 94 ++++++++++ web-backend/src/main/jte/index.jte | 166 +++++++++++------- .../src/main/resources/application.yml | 1 + .../static/{images => }/error/400.html | 0 .../static/{images => }/error/404.html | 5 +- .../static/{images => }/error/500.html | 0 11 files changed, 369 insertions(+), 210 deletions(-) create mode 100644 web-backend/src/main/java/com/statusneo/vms/config/ValidationConfig.java delete mode 100644 web-backend/src/main/java/com/statusneo/vms/controller/ApiController.java create mode 100644 web-backend/src/main/jte/error/400.jte rename web-backend/src/main/resources/static/{images => }/error/400.html (100%) rename web-backend/src/main/resources/static/{images => }/error/404.html (74%) rename web-backend/src/main/resources/static/{images => }/error/500.html (100%) diff --git a/web-backend/src/main/java/com/statusneo/vms/config/ValidationConfig.java b/web-backend/src/main/java/com/statusneo/vms/config/ValidationConfig.java new file mode 100644 index 0000000..950607a --- /dev/null +++ b/web-backend/src/main/java/com/statusneo/vms/config/ValidationConfig.java @@ -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; + } +} \ No newline at end of file diff --git a/web-backend/src/main/java/com/statusneo/vms/controller/ApiController.java b/web-backend/src/main/java/com/statusneo/vms/controller/ApiController.java deleted file mode 100644 index 6357cc1..0000000 --- a/web-backend/src/main/java/com/statusneo/vms/controller/ApiController.java +++ /dev/null @@ -1,78 +0,0 @@ -/** - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package com.statusneo.vms.controller; - -import com.statusneo.vms.cache.EmployeeNameCache; -import com.statusneo.vms.model.Visit; -import com.statusneo.vms.repository.VisitRepository; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.time.LocalDateTime; -import java.util.List; - -@RestController -@RequestMapping("/api") -public class ApiController { - - private static final Logger logger = LoggerFactory.getLogger(ApiController.class); - - @Autowired - private VisitRepository visitRepository; - - @Autowired - private EmployeeNameCache employeeNameCache; - - @GetMapping("/report") - public ResponseEntity getReport(@RequestParam String period) { - logger.info("API: Generating report for period: {}", period); - - List visits; - if (period.equals("daily")) { - visits = visitRepository.findAllByVisitDateBetween( - LocalDateTime.now().toLocalDate().atStartOfDay(), - LocalDateTime.now() - ); - } else if (period.equals("monthly")) { - visits = visitRepository.findAllByVisitDateBetween( - LocalDateTime.now().minusMonths(1), - LocalDateTime.now() - ); - } else { - return ResponseEntity.badRequest().body("Invalid period. Use 'daily' or 'monthly'."); - } - - return ResponseEntity.ok(visits); - } - - @GetMapping("/refresh-employee-cache") - public ResponseEntity refreshEmployeeCache() { - logger.info("API: Refreshing employee cache"); - employeeNameCache.initializeCache(); - return ResponseEntity.ok("Employee cache refreshed successfully"); - } - - @GetMapping("/health") - public ResponseEntity healthCheck() { - return ResponseEntity.ok("API is healthy"); - } -} \ No newline at end of file diff --git a/web-backend/src/main/java/com/statusneo/vms/controller/VisitorController.java b/web-backend/src/main/java/com/statusneo/vms/controller/VisitorController.java index 82846a2..b52d8e5 100644 --- a/web-backend/src/main/java/com/statusneo/vms/controller/VisitorController.java +++ b/web-backend/src/main/java/com/statusneo/vms/controller/VisitorController.java @@ -27,14 +27,17 @@ 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; import java.util.List; @Controller @@ -60,55 +63,64 @@ public class VisitorController { @Autowired private EmployeeRepository employeeRepository; - @GetMapping("/") - public String home() { + public String home(Model model) { + model.addAttribute("visitor", new Visitor()); return "index"; } - @GetMapping("/search") - public String searchEmployees(@RequestParam("employee") String query, Model model) { - logger.info("Received search request for employee: {}", query); - List names = employeeNameCache.getEmployeeNamesByPrefix(query == null ? "" : query); - model.addAttribute("employees", names); - return "employeeSearchResults"; - } + @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) { - // Optional: Add MVC version of report page if you want a web UI for reports - @GetMapping("/report-page") - public String showReportPage(@RequestParam(defaultValue = "daily") String period, Model model) { - logger.info("Displaying report page for period: {}", period); - model.addAttribute("period", period); - return "report"; // You'll need to create report.jte template - } + logger.info("Processing visitor registration for: {}", visitor.getEmail()); - // Optional: Add MVC version of cache refresh status - @GetMapping("/cache-status") - public String showCacheStatus(Model model) { - model.addAttribute("cacheStatus", "Employee cache is active"); - return "cache-status"; // You'll need to create cache-status.jte template - } + // Check for validation errors - Spring validation pattern + if (bindingResult.hasErrors()) { + logger.warn("Form validation failed with {} errors", bindingResult.getErrorCount()); + + // Add field errors to model for display in template + model.addAttribute("fieldErrors", bindingResult.getFieldErrors()); + + if (hxRequest != null && hxRequest.equals("true")) { + return "fragments/validation-errors"; // HTMX error fragment + } + return "index"; + } - @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")) { return "fragments/otp-modal"; } - // For regular form submission (fallback) + // Regular form submission return "otp-modal"; } + + @GetMapping("/report") + public String getReport(@RequestParam String period, Model model) { + List visits; + if (period.equals("daily")) { + visits = visitRepository.findAllByVisitDateBetween(LocalDateTime.now().toLocalDate().atStartOfDay(), LocalDateTime.now()); + } else if (period.equals("monthly")) { + visits = visitRepository.findAllByVisitDateBetween(LocalDateTime.now().minusMonths(1), LocalDateTime.now()); + } else { + return "error/400"; + } + model.addAttribute("visits", visits); + return "report"; + } + @PostMapping("/confirm-visit") public Object confirmVisit(@RequestParam("visitId") Long visitId, @RequestParam("otpCode") String otpCode, @@ -125,12 +137,10 @@ public Object confirmVisit(@RequestParam("visitId") Long visitId, model.addAttribute("visit", visit); 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")); @@ -146,7 +156,6 @@ public Object confirmVisit(@RequestParam("visitId") Long visitId, } } - // For regular form submission (fallback): if (result.success()) { return "confirmation-modal"; } else if (!result.reattempt()) { diff --git a/web-backend/src/main/java/com/statusneo/vms/exception/GlobalExceptionHandler.java b/web-backend/src/main/java/com/statusneo/vms/exception/GlobalExceptionHandler.java index ba03450..cbc9e4f 100644 --- a/web-backend/src/main/java/com/statusneo/vms/exception/GlobalExceptionHandler.java +++ b/web-backend/src/main/java/com/statusneo/vms/exception/GlobalExceptionHandler.java @@ -1,21 +1,3 @@ -/** - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ package com.statusneo.vms.exception; import gg.jte.TemplateException; @@ -23,40 +5,126 @@ import org.slf4j.LoggerFactory; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.http.HttpStatus; +import org.springframework.ui.Model; +import org.springframework.validation.BindException; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingServletRequestParameterException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.validation.ConstraintViolationException; import java.util.NoSuchElementException; +import java.util.stream.Collectors; + @ControllerAdvice public class GlobalExceptionHandler { private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class); + @ExceptionHandler(MethodArgumentNotValidException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public String handleMethodArgumentNotValid(MethodArgumentNotValidException ex, Model model) { + String errorMessage = ex.getBindingResult() + .getFieldErrors() + .stream() + .map(error -> error.getField() + ": " + error.getDefaultMessage()) + .collect(Collectors.joining(", ")); + + logger.warn("Method argument validation failed: {}", errorMessage); + model.addAttribute("error", "Validation failed: " + errorMessage); + model.addAttribute("fieldErrors", ex.getBindingResult().getFieldErrors()); + + return "error/400"; + } + + @ExceptionHandler(BindException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public String handleBindException(BindException ex, Model model) { + String errorMessage = ex.getFieldErrors() + .stream() + .map(FieldError::getDefaultMessage) + .collect(Collectors.joining(", ")); + + logger.warn("Binding error: {}", errorMessage); + model.addAttribute("error", "Form binding failed: " + errorMessage); + model.addAttribute("fieldErrors", ex.getFieldErrors()); + + return "error/400"; + } + + @ExceptionHandler(MissingServletRequestParameterException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public String handleMissingParams(MissingServletRequestParameterException ex, Model model) { + logger.warn("Missing required parameter: {}", ex.getParameterName()); + model.addAttribute("error", "Missing required parameter: " + ex.getParameterName()); + return "error/400"; + } + + @ExceptionHandler(MethodArgumentTypeMismatchException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public String handleTypeMismatch(MethodArgumentTypeMismatchException ex, Model model) { + logger.warn("Type mismatch for parameter '{}': expected {}", + ex.getName(), ex.getRequiredType()); + model.addAttribute("error", + String.format("Invalid value for parameter '%s'. Expected type: %s", + ex.getName(), + ex.getRequiredType() != null ? ex.getRequiredType().getSimpleName() : "unknown")); + return "error/400"; + } + + @ExceptionHandler(NoSuchElementException.class) @ResponseStatus(HttpStatus.NOT_FOUND) - public String handleNoSuchElement(NoSuchElementException ex) { + public String handleNoSuchElement(NoSuchElementException ex, HttpServletRequest request) { logger.warn("Resource not found: {}", ex.getMessage()); - return "error/404"; + request.setAttribute(RequestDispatcher.ERROR_STATUS_CODE, HttpStatus.NOT_FOUND.value()); + return "forward:/error/404.html"; } + @ExceptionHandler({DataIntegrityViolationException.class, IllegalArgumentException.class}) @ResponseStatus(HttpStatus.BAD_REQUEST) - public String handleBadRequestExceptions(Exception ex) { - logger.warn("Bad request: {}", ex.getMessage()); + public String handleBadRequestExceptions(Exception ex, Model model) { + logger.warn("Bad request - {}: {}", ex.getClass().getSimpleName(), ex.getMessage()); + model.addAttribute("error", "Invalid request: " + ex.getMessage()); return "error/400"; } + + @ExceptionHandler(ConstraintViolationException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public String handleConstraintViolation(ConstraintViolationException ex, Model model) { + String errorMessage = ex.getConstraintViolations() + .stream() + .map(violation -> violation.getPropertyPath() + ": " + violation.getMessage()) + .collect(Collectors.joining(", ")); + + logger.warn("Constraint violation: {}", errorMessage); + model.addAttribute("error", "Invalid input: " + errorMessage); + return "error/400"; + } + + @ExceptionHandler(TemplateException.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) - public String handleTemplateException(TemplateException ex) { - logger.error("Template rendering failed: {}", ex.getMessage()); - return "error/500"; + public String handleTemplateException(TemplateException ex, HttpServletRequest request) { + logger.error("Template rendering failed: {}", ex.getMessage(), ex); + // NEVER return a JTE template here - use static HTML only + request.setAttribute(RequestDispatcher.ERROR_STATUS_CODE, HttpStatus.INTERNAL_SERVER_ERROR.value()); + return "forward:/error/500.html"; } @ExceptionHandler(Exception.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) - public String handleGeneralException(Exception ex) { - logger.error("Unexpected error occurred: {}", ex.getMessage()); - return "error/500"; + public String handleGeneralException(Exception ex, HttpServletRequest request) { + logger.error("Unexpected error occurred - {}: {}", + ex.getClass().getSimpleName(), ex.getMessage(), ex); + // NEVER return a JTE template here - use static HTML only + request.setAttribute(RequestDispatcher.ERROR_STATUS_CODE, HttpStatus.INTERNAL_SERVER_ERROR.value()); + return "forward:/error/500.html"; } } \ No newline at end of file diff --git a/web-backend/src/main/java/com/statusneo/vms/model/Visitor.java b/web-backend/src/main/java/com/statusneo/vms/model/Visitor.java index 9e617bb..2a1a855 100644 --- a/web-backend/src/main/java/com/statusneo/vms/model/Visitor.java +++ b/web-backend/src/main/java/com/statusneo/vms/model/Visitor.java @@ -69,19 +69,19 @@ public class Visitor { /** * Full name of the visitor. */ - private String name; + public String name; /** * Contact phone number of the visitor. */ @NotNull(message = "Phone number cannot be null") @Pattern(regexp = "^\\d{10}$", message = "Phone number must be exactly 10 digits") - private String phoneNumber; + public String phoneNumber; /** * Email address of the visitor, used for communication and OTP verification. */ - private String email; + public String email; /** * Physical address of the visitor. @@ -91,7 +91,7 @@ public class Visitor { /** * Company of the visitor. */ - private String company; + public String company; /** * Path to the visitor's profile picture stored in the system. @@ -101,7 +101,7 @@ public class Visitor { @Column(name = "laptop_number") - private String laptop; + public String laptop; @ManyToOne @JoinColumn(name = "host_id", referencedColumnName = "id") diff --git a/web-backend/src/main/jte/error/400.jte b/web-backend/src/main/jte/error/400.jte new file mode 100644 index 0000000..4cea066 --- /dev/null +++ b/web-backend/src/main/jte/error/400.jte @@ -0,0 +1,94 @@ +@import org.springframework.validation.FieldError +@import java.util.List + +@param String error = "Bad Request" +@param List fieldErrors = null + + + + + + + 400 - Bad Request + + + +
+

400 - Bad Request

+ +
+ ${error} +
+ + @if(fieldErrors != null && !fieldErrors.isEmpty()) +

Validation Errors:

+
    + @for(FieldError fieldError : fieldErrors) +
  • + ${fieldError.getField()}: + ${fieldError.getDefaultMessage()} +
  • + @endfor +
+ @endif + + Return to Home +
+ + \ No newline at end of file diff --git a/web-backend/src/main/jte/index.jte b/web-backend/src/main/jte/index.jte index f99b330..9e3ea16 100644 --- a/web-backend/src/main/jte/index.jte +++ b/web-backend/src/main/jte/index.jte @@ -1,3 +1,10 @@ +@import org.springframework.validation.FieldError +@import java.util.List +@import com.statusneo.vms.model.Visitor + +@param Visitor visitor = new com.statusneo.vms.model.Visitor() +@param List fieldErrors = null + @@ -24,8 +31,25 @@
- - + + + @if(fieldErrors != null && !fieldErrors.isEmpty()) +
+
+ + + +
+

Please correct the following errors:

+
    + @for(FieldError error : fieldErrors) +
  • ${error.getField()}: ${error.getDefaultMessage()}
  • + @endfor +
+
+
+
+ @endif
@@ -64,8 +89,9 @@ @@ -79,9 +105,18 @@ + pattern="^\d{10}$" + class="w-full px-4 py-3 border ${fieldErrors != null && fieldErrors.stream().anyMatch(e -> e.getField().equals("phoneNumber")) ? "border-red-500" : "border-gray-300"} rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors" + placeholder="Enter your 10-digit phone number"> + @if(fieldErrors != null) + @for(FieldError error : fieldErrors) + @if(error.getField().equals("phoneNumber")) +

${error.getDefaultMessage()}

+ @endif + @endfor + @endif
@@ -94,8 +129,9 @@
@@ -109,6 +145,7 @@ @@ -125,7 +162,7 @@ id="host-search" name="hostSearch" autocomplete="off" - hx-get="/search-employees" + hx-get="/api/hosts/search" hx-trigger="keyup changed delay:300ms" hx-target="#host-results" hx-indicator="#search-spinner" @@ -135,49 +172,52 @@ -
- - - - -
- - - - - - + + + + + + + + - -
+ +
-
-
- - - -
- By checking in, you agree to our visitor policies and terms of service. -
- - + + + - + +
+ By checking in, you agree to our visitor policies and terms of service. +
+ + - + - - - - - - - - - - - - - - \ No newline at end of file + \ No newline at end of file