-
Notifications
You must be signed in to change notification settings - Fork 0
Added locations tracking #22
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
d21d7dd
4c93961
b8234c0
cc01854
1c718f2
56b36cf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -30,8 +30,7 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { | |
| protected void doFilterInternal( | ||
| @NonNull HttpServletRequest request, | ||
| @NonNull HttpServletResponse response, | ||
| @NonNull FilterChain filterChain | ||
| ) throws ServletException, IOException { | ||
| @NonNull FilterChain filterChain) throws ServletException, IOException { | ||
|
|
||
| // ✅ Allow preflight | ||
| if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { | ||
|
|
@@ -40,10 +39,12 @@ protected void doFilterInternal( | |
| } | ||
|
|
||
| String path = request.getServletPath(); | ||
| System.out.println("🔍 JwtAuthFilter - Path: " + path); | ||
|
|
||
| // ✅ Public endpoints (do not try to parse JWT) | ||
| if (path.startsWith("/api/auth") | ||
| || path.startsWith("/api/mobile/auth") | ||
| if (path.startsWith("/api/auth/login") | ||
| || path.startsWith("/api/auth/refresh") | ||
| || path.startsWith("/api/mobile/auth/login") | ||
| || path.startsWith("/api/health") | ||
| || path.startsWith("/api/duties") | ||
| || path.startsWith("/api/leaves")){ | ||
|
|
@@ -52,9 +53,12 @@ protected void doFilterInternal( | |
| } | ||
|
|
||
| final String authHeader = request.getHeader("Authorization"); | ||
| System.out.println("🔍 Auth Header: " | ||
| + (authHeader != null ? authHeader.substring(0, Math.min(20, authHeader.length())) + "..." : "NULL")); | ||
|
|
||
| // ✅ No token -> continue (SecurityConfig will decide permit/deny) | ||
| if (authHeader == null || !authHeader.startsWith("Bearer ")) { | ||
| System.out.println("❌ No Bearer token found"); | ||
| filterChain.doFilter(request, response); | ||
| return; | ||
| } | ||
|
|
@@ -67,20 +71,26 @@ protected void doFilterInternal( | |
| UserDetails userDetails = this.userDetailsService.loadUserByUsername(userEmail); | ||
|
|
||
| if (jwtService.isTokenValid(jwt, userDetails)) { | ||
| UsernamePasswordAuthenticationToken authToken = | ||
| new UsernamePasswordAuthenticationToken( | ||
| userDetails, | ||
| null, | ||
| userDetails.getAuthorities() | ||
| ); | ||
| UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( | ||
| userDetails, | ||
| null, | ||
| userDetails.getAuthorities()); | ||
|
|
||
| authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); | ||
| SecurityContextHolder.getContext().setAuthentication(authToken); | ||
|
|
||
| // 🔍 DEBUG: Log authentication success | ||
| System.out.println("✅ JWT Auth Success: " + userEmail); | ||
| System.out.println(" Authorities: " + userDetails.getAuthorities()); | ||
| System.out.println(" Accessing: " + path); | ||
|
Comment on lines
+81
to
+85
|
||
| } else { | ||
| System.out.println("❌ JWT Invalid for user: " + userEmail); | ||
| } | ||
| } | ||
| } catch (Exception ex) { | ||
| // ✅ DO NOT block request just because token is bad | ||
| // Let SecurityConfig handle authorization | ||
| System.out.println("⚠️ JWT parsing error: " + ex.getMessage()); | ||
| } | ||
|
|
||
| filterChain.doFilter(request, response); | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1,5 +1,8 @@ | ||||||
| package com.crimeLink.analyzer.config; | ||||||
|
|
||||||
| import java.util.Arrays; | ||||||
| import java.util.List; | ||||||
|
|
||||||
| import org.springframework.beans.factory.annotation.Autowired; | ||||||
| import org.springframework.context.annotation.Bean; | ||||||
| import org.springframework.context.annotation.Configuration; | ||||||
|
|
@@ -21,9 +24,6 @@ | |||||
| import org.springframework.web.cors.CorsConfigurationSource; | ||||||
| import org.springframework.web.cors.UrlBasedCorsConfigurationSource; | ||||||
|
|
||||||
| import java.util.Arrays; | ||||||
| import java.util.List; | ||||||
|
|
||||||
| @Configuration | ||||||
| @EnableWebSecurity | ||||||
| @EnableMethodSecurity | ||||||
|
|
@@ -41,32 +41,34 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti | |||||
| .csrf(csrf -> csrf.disable()) | ||||||
| // CRITICAL FIX: Enable CORS using the bean configuration | ||||||
| .cors(cors -> cors.configurationSource(corsConfigurationSource())) | ||||||
| .cors(cors -> cors.configure(http)) | ||||||
| .authorizeHttpRequests(auth -> auth | ||||||
| .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() | ||||||
| .requestMatchers("/api/auth/**").permitAll() | ||||||
| .requestMatchers("/api/health").permitAll() | ||||||
| .requestMatchers("/api/admin/health").permitAll() | ||||||
| .requestMatchers("/api/database/**").permitAll() | ||||||
| .requestMatchers("/api/vehicles/**").permitAll() | ||||||
| .requestMatchers("/api/mobile/auth/**").permitAll() | ||||||
| .requestMatchers("/api/duty-schedules/**").hasRole("OIC") | ||||||
| .requestMatchers("/api/mobile/**").hasRole("FieldOfficer") | ||||||
| .requestMatchers("/api/test").permitAll() | ||||||
| .requestMatchers("/api/leaves/**").permitAll() | ||||||
| .requestMatchers("/api/debug/**").permitAll() // 🔍 Debug endpoints | ||||||
|
arosha-w marked this conversation as resolved.
|
||||||
|
|
||||||
| // Allow duty schedule operations for OIC | ||||||
| .requestMatchers("/api/duty-schedules/**").hasRole("OIC") | ||||||
| // Public endpoints | ||||||
| .requestMatchers("/api/vehicle**").permitAll() | ||||||
|
||||||
| .requestMatchers("/api/vehicle**").permitAll() | |
| .requestMatchers("/api/vehicles/**").permitAll() |
Copilot
AI
Feb 13, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"/api/duties/**" is configured as permitAll. These endpoints expose duty assignments by officerId and date, which is sensitive operational data; permitting unauthenticated access is a security risk. Consider requiring authentication (e.g., hasRole("FieldOfficer")/hasAnyRole(...)) and enforcing that a field officer can only query their own duties.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,85 @@ | ||
| package com.crimeLink.analyzer.controller; | ||
|
|
||
| import java.time.Instant; | ||
| import java.util.HashMap; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
|
|
||
| import org.springframework.format.annotation.DateTimeFormat; | ||
| import org.springframework.security.core.annotation.AuthenticationPrincipal; | ||
| import org.springframework.web.bind.annotation.GetMapping; | ||
| import org.springframework.web.bind.annotation.PathVariable; | ||
| import org.springframework.web.bind.annotation.PostMapping; | ||
| import org.springframework.web.bind.annotation.RequestBody; | ||
| import org.springframework.web.bind.annotation.RequestMapping; | ||
| import org.springframework.web.bind.annotation.RequestParam; | ||
| import org.springframework.web.bind.annotation.RestController; | ||
|
|
||
| import com.crimeLink.analyzer.dto.LocationPointDTO; | ||
| import com.crimeLink.analyzer.entity.User; | ||
| import com.crimeLink.analyzer.service.impl.LocationServiceImpl; | ||
|
|
||
| import lombok.RequiredArgsConstructor; | ||
|
|
||
| @RestController | ||
| @RequestMapping("/api") | ||
| @RequiredArgsConstructor | ||
| public class LocationController { | ||
| private final LocationServiceImpl service; | ||
|
|
||
|
Comment on lines
+20
to
+29
|
||
| @PostMapping("/officers/me/locations/bulk") | ||
| public void uploadMyLocations(@AuthenticationPrincipal User user, @RequestBody List<LocationPointDTO> points) { | ||
| System.out.println("Received locations: " + points.size()); // REMOVE: for testing | ||
| if (user == null) { | ||
| throw new RuntimeException("Unauthorized"); | ||
| } | ||
|
Comment on lines
+31
to
+35
|
||
|
|
||
| if (!"FieldOfficer".equalsIgnoreCase(user.getRole())) { | ||
| throw new RuntimeException("Only field officers can upload locations"); | ||
| } | ||
|
|
||
| String officerBadgeNo = user.getBadgeNo(); | ||
| if (officerBadgeNo == null || officerBadgeNo.isBlank()) { | ||
| throw new RuntimeException("Badge number missing"); | ||
| } | ||
|
Comment on lines
+33
to
+44
|
||
| service.saveBulk(officerBadgeNo, points); | ||
| } | ||
|
|
||
| @GetMapping("/admin/officers/{officerBadgeNo}/locations") | ||
| public Object history( | ||
| @AuthenticationPrincipal User user, | ||
| @PathVariable String officerBadgeNo, | ||
| @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) Instant from, | ||
| @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) Instant to) { | ||
| System.out.println("📍 LocationController.history() called"); | ||
| System.out.println(" Badge: " + officerBadgeNo); | ||
| System.out.println(" From: " + from + ", To: " + to); | ||
| System.out.println(" User: " + (user != null ? user.getEmail() : "NULL")); | ||
| System.out.println(" Role: " + (user != null ? user.getRole() : "NULL")); | ||
| System.out.println(" Authorities: " + (user != null ? user.getAuthorities() : "NULL")); | ||
| return service.getHistory(officerBadgeNo, from, to); | ||
|
Comment on lines
+54
to
+60
|
||
| } | ||
|
|
||
| @GetMapping("/debug/whoami") | ||
| public Map<String, Object> whoAmI(@AuthenticationPrincipal User user) { | ||
| Map<String, Object> info = new HashMap<>(); | ||
| if (user != null) { | ||
| info.put("email", user.getEmail()); | ||
| info.put("name", user.getName()); | ||
| info.put("role", user.getRole()); | ||
| info.put("authorities", user.getAuthorities().stream() | ||
| .map(auth -> auth.getAuthority()) | ||
| .toList()); | ||
| info.put("userId", user.getUserId()); | ||
| info.put("badgeNo", user.getBadgeNo()); | ||
| } else { | ||
| info.put("error", "No authenticated user"); | ||
| } | ||
| return info; | ||
| } | ||
|
|
||
| @GetMapping("/admin/officers/{officerBadgeNo}/locations/last") | ||
| public Object lastLocation(@PathVariable String officerBadgeNo) { | ||
| return service.getLastLocation(officerBadgeNo); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| package com.crimeLink.analyzer.dto; | ||
|
|
||
| import java.time.Instant; | ||
| import java.util.Map; | ||
|
|
||
| public record LocationPointDTO( | ||
| Instant ts, | ||
| double latitude, | ||
| double longitude, | ||
| Float accuracyM, | ||
| Float speedMps, | ||
| Float headingDeg, | ||
| String provider, | ||
| Map<String, Object> meta) { | ||
|
|
||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| package com.crimeLink.analyzer.entity; | ||
|
|
||
| import java.time.Instant; | ||
|
|
||
| import org.hibernate.annotations.JdbcTypeCode; | ||
| import org.hibernate.type.SqlTypes; | ||
|
|
||
| import com.fasterxml.jackson.databind.JsonNode; | ||
|
|
||
| import jakarta.persistence.Column; | ||
| import jakarta.persistence.Entity; | ||
| import jakarta.persistence.GeneratedValue; | ||
| import jakarta.persistence.GenerationType; | ||
| import jakarta.persistence.Id; | ||
| import jakarta.persistence.Index; | ||
| import jakarta.persistence.Table; | ||
| import lombok.AllArgsConstructor; | ||
| import lombok.Getter; | ||
| import lombok.NoArgsConstructor; | ||
| import lombok.Setter; | ||
|
|
||
| @Entity | ||
| @AllArgsConstructor | ||
| @NoArgsConstructor | ||
| @Getter | ||
| @Setter | ||
| @Table(name = "location_points", indexes = { | ||
| @Index(name = "idx_location_points_officer_ts", columnList = "officer_badge_no, ts") }) | ||
| public class LocationPoint { | ||
| @Id | ||
| @GeneratedValue(strategy = GenerationType.IDENTITY) | ||
| private Long id; | ||
|
|
||
| @Column(name = "officer_badge_no", nullable = false, length = 20) | ||
| private String officerBadgeNo; | ||
| private Instant ts; | ||
| private double latitude; | ||
| private double longitude; | ||
|
|
||
| private Float accuracyM; | ||
| private Float speedMps; | ||
| private Float headingDeg; | ||
|
|
||
| private String provider; | ||
|
|
||
| @JdbcTypeCode(SqlTypes.JSON) | ||
| @Column(name = "meta", columnDefinition = "jsonb") | ||
| private JsonNode meta; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| package com.crimeLink.analyzer.repository; | ||
|
|
||
| import java.time.Instant; | ||
| import java.util.List; | ||
|
|
||
| import org.springframework.data.domain.Pageable; | ||
| import org.springframework.data.jpa.repository.JpaRepository; | ||
|
|
||
| import com.crimeLink.analyzer.entity.LocationPoint; | ||
|
|
||
| public interface LocationPointRepository extends JpaRepository<LocationPoint, Long> { | ||
| List<LocationPoint> findByOfficerBadgeNoAndTsBetweenOrderByTsAsc(String officerBadgeNo, Instant from, Instant to); | ||
|
|
||
| List<LocationPoint> findByOfficerBadgeNoOrderByTsDesc(String officerBadgeNo, Pageable pageable); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
JwtAuthenticationFilter uses multiple System.out.println statements, including logging the Authorization header prefix. Avoid printing tokens/user data to stdout; use a logger with configurable levels (debug) and do not log any part of credentials/tokens in production logs.