Skip to content

Commit f782bb7

Browse files
authored
Merge pull request #22 from arosha-w/isira
Added locations tracking
2 parents 9f62b90 + 56b36cf commit f782bb7

14 files changed

Lines changed: 350 additions & 43 deletions

src/main/java/com/crimeLink/analyzer/config/JwtAuthenticationFilter.java

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,7 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
3030
protected void doFilterInternal(
3131
@NonNull HttpServletRequest request,
3232
@NonNull HttpServletResponse response,
33-
@NonNull FilterChain filterChain
34-
) throws ServletException, IOException {
33+
@NonNull FilterChain filterChain) throws ServletException, IOException {
3534

3635
// ✅ Allow preflight
3736
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
@@ -40,10 +39,12 @@ protected void doFilterInternal(
4039
}
4140

4241
String path = request.getServletPath();
42+
System.out.println("🔍 JwtAuthFilter - Path: " + path);
4343

4444
// ✅ Public endpoints (do not try to parse JWT)
45-
if (path.startsWith("/api/auth")
46-
|| path.startsWith("/api/mobile/auth")
45+
if (path.startsWith("/api/auth/login")
46+
|| path.startsWith("/api/auth/refresh")
47+
|| path.startsWith("/api/mobile/auth/login")
4748
|| path.startsWith("/api/health")
4849
|| path.startsWith("/api/duties")
4950
|| path.startsWith("/api/leaves")){
@@ -52,9 +53,12 @@ protected void doFilterInternal(
5253
}
5354

5455
final String authHeader = request.getHeader("Authorization");
56+
System.out.println("🔍 Auth Header: "
57+
+ (authHeader != null ? authHeader.substring(0, Math.min(20, authHeader.length())) + "..." : "NULL"));
5558

5659
// ✅ No token -> continue (SecurityConfig will decide permit/deny)
5760
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
61+
System.out.println("❌ No Bearer token found");
5862
filterChain.doFilter(request, response);
5963
return;
6064
}
@@ -67,20 +71,26 @@ protected void doFilterInternal(
6771
UserDetails userDetails = this.userDetailsService.loadUserByUsername(userEmail);
6872

6973
if (jwtService.isTokenValid(jwt, userDetails)) {
70-
UsernamePasswordAuthenticationToken authToken =
71-
new UsernamePasswordAuthenticationToken(
72-
userDetails,
73-
null,
74-
userDetails.getAuthorities()
75-
);
74+
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
75+
userDetails,
76+
null,
77+
userDetails.getAuthorities());
7678

7779
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
7880
SecurityContextHolder.getContext().setAuthentication(authToken);
81+
82+
// 🔍 DEBUG: Log authentication success
83+
System.out.println("✅ JWT Auth Success: " + userEmail);
84+
System.out.println(" Authorities: " + userDetails.getAuthorities());
85+
System.out.println(" Accessing: " + path);
86+
} else {
87+
System.out.println("❌ JWT Invalid for user: " + userEmail);
7988
}
8089
}
8190
} catch (Exception ex) {
8291
// ✅ DO NOT block request just because token is bad
8392
// Let SecurityConfig handle authorization
93+
System.out.println("⚠️ JWT parsing error: " + ex.getMessage());
8494
}
8595

8696
filterChain.doFilter(request, response);

src/main/java/com/crimeLink/analyzer/config/SecurityConfig.java

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
package com.crimeLink.analyzer.config;
22

3+
import java.util.Arrays;
4+
import java.util.List;
5+
36
import org.springframework.beans.factory.annotation.Autowired;
47
import org.springframework.context.annotation.Bean;
58
import org.springframework.context.annotation.Configuration;
@@ -21,9 +24,6 @@
2124
import org.springframework.web.cors.CorsConfigurationSource;
2225
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
2326

24-
import java.util.Arrays;
25-
import java.util.List;
26-
2727
@Configuration
2828
@EnableWebSecurity
2929
@EnableMethodSecurity
@@ -41,32 +41,34 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
4141
.csrf(csrf -> csrf.disable())
4242
// CRITICAL FIX: Enable CORS using the bean configuration
4343
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
44-
.cors(cors -> cors.configure(http))
4544
.authorizeHttpRequests(auth -> auth
4645
.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
4746
.requestMatchers("/api/auth/**").permitAll()
4847
.requestMatchers("/api/health").permitAll()
4948
.requestMatchers("/api/admin/health").permitAll()
5049
.requestMatchers("/api/database/**").permitAll()
51-
.requestMatchers("/api/vehicles/**").permitAll()
52-
.requestMatchers("/api/mobile/auth/**").permitAll()
53-
.requestMatchers("/api/duty-schedules/**").hasRole("OIC")
54-
.requestMatchers("/api/mobile/**").hasRole("FieldOfficer")
5550
.requestMatchers("/api/test").permitAll()
56-
.requestMatchers("/api/leaves/**").permitAll()
51+
.requestMatchers("/api/debug/**").permitAll() // 🔍 Debug endpoints
5752

58-
// Allow duty schedule operations for OIC
59-
.requestMatchers("/api/duty-schedules/**").hasRole("OIC")
53+
// Public endpoints
54+
.requestMatchers("/api/vehicle**").permitAll()
55+
.requestMatchers("/api/mobile/auth/**").permitAll()
56+
.requestMatchers("/api/duties/**").permitAll()
57+
.requestMatchers("/api/crime-reports/map").permitAll()
6058

59+
// Field Officer routes
60+
.requestMatchers("/api/officers/me/**").hasRole("FieldOfficer")
61+
.requestMatchers("/api/mobile/**").hasRole("FieldOfficer")
62+
.requestMatchers("/api/leaves/**").permitAll()
6163

62-
// Allow duty schedule operations for OIC
64+
// OIC-only routes
6365
.requestMatchers("/api/duty-schedules/**").hasRole("OIC")
64-
65-
// Allow weapon operations for OIC
6666
.requestMatchers("/api/weapon/**").hasRole("OIC")
6767
.requestMatchers("/api/weapon-issue/**").hasRole("OIC")
68-
.requestMatchers("/api/duties/**").permitAll()
69-
.requestMatchers("/duties/**").permitAll()
68+
69+
// Admin/OIC routes (officer data, locations, users)
70+
.requestMatchers("/api/users/field-officers").hasAnyRole("Admin", "OIC")
71+
.requestMatchers("/api/admin/**").hasAnyRole("OIC", "Admin")
7072

7173
.anyRequest().authenticated())
7274
.sessionManagement(session -> session

src/main/java/com/crimeLink/analyzer/controller/AuthController.java

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -44,25 +44,34 @@ public ResponseEntity<LoginResponse> login(
4444
public ResponseEntity<TokenRefreshResponse> refreshToken(@RequestBody TokenRefreshRequest request) {
4545
String refreshTokenStr = request.getRefreshToken();
4646

47-
return refreshTokenService.findByToken(refreshTokenStr)
48-
.map(refreshTokenService::verifyExpiration)
49-
.map(RefreshToken::getUser)
50-
.map(user -> {
47+
if (refreshTokenStr == null || refreshTokenStr.isBlank()) {
48+
return ResponseEntity.badRequest().body(new TokenRefreshResponse(
49+
false,
50+
"Refresh token is required",
51+
null,
52+
null
53+
));
54+
}
55+
56+
return refreshTokenService.findValidToken(refreshTokenStr)
57+
.map(validToken -> {
58+
RefreshToken rotated = refreshTokenService.rotateRefreshToken(validToken);
59+
User user = rotated.getUser();
5160
String accessToken = jwtService.generateToken(user);
61+
5262
return ResponseEntity.ok(new TokenRefreshResponse(
5363
true,
5464
"Token refreshed successfully",
5565
accessToken,
56-
refreshTokenStr
66+
rotated.getToken()
5767
));
5868
})
59-
.orElseGet(() -> ResponseEntity.status(401)
60-
.body(new TokenRefreshResponse(
61-
false,
62-
"Invalid refresh token",
63-
null,
64-
null
65-
)));
69+
.orElseGet(() -> ResponseEntity.status(401).body(new TokenRefreshResponse(
70+
false,
71+
"Invalid or expired refresh token",
72+
null,
73+
null
74+
)));
6675
}
6776

6877
@PostMapping("/logout")

src/main/java/com/crimeLink/analyzer/controller/DutyScheduleController.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ public ResponseEntity<List<OfficerDutyRowDTO>> getOfficersForDate(
3434
List<OfficerDutyRowDTO> rows = dutyService.getOfficerRowsForDate(date);
3535
return ResponseEntity.ok(rows);
3636
}
37+
@GetMapping("/locations")
38+
public ResponseEntity<List<String>> getDutyLocations() {
39+
return ResponseEntity.ok(dutyService.getDutyLocations());
40+
}
3741
// 2) Create / Save a duty (upsert via service.saveDuty)
3842
@PostMapping
3943
public ResponseEntity<?> createDuty(@RequestBody DutyScheduleRequest request) {
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package com.crimeLink.analyzer.controller;
2+
3+
import java.time.Instant;
4+
import java.util.HashMap;
5+
import java.util.List;
6+
import java.util.Map;
7+
8+
import org.springframework.format.annotation.DateTimeFormat;
9+
import org.springframework.security.core.annotation.AuthenticationPrincipal;
10+
import org.springframework.web.bind.annotation.GetMapping;
11+
import org.springframework.web.bind.annotation.PathVariable;
12+
import org.springframework.web.bind.annotation.PostMapping;
13+
import org.springframework.web.bind.annotation.RequestBody;
14+
import org.springframework.web.bind.annotation.RequestMapping;
15+
import org.springframework.web.bind.annotation.RequestParam;
16+
import org.springframework.web.bind.annotation.RestController;
17+
18+
import com.crimeLink.analyzer.dto.LocationPointDTO;
19+
import com.crimeLink.analyzer.entity.User;
20+
import com.crimeLink.analyzer.service.impl.LocationServiceImpl;
21+
22+
import lombok.RequiredArgsConstructor;
23+
24+
@RestController
25+
@RequestMapping("/api")
26+
@RequiredArgsConstructor
27+
public class LocationController {
28+
private final LocationServiceImpl service;
29+
30+
@PostMapping("/officers/me/locations/bulk")
31+
public void uploadMyLocations(@AuthenticationPrincipal User user, @RequestBody List<LocationPointDTO> points) {
32+
System.out.println("Received locations: " + points.size()); // REMOVE: for testing
33+
if (user == null) {
34+
throw new RuntimeException("Unauthorized");
35+
}
36+
37+
if (!"FieldOfficer".equalsIgnoreCase(user.getRole())) {
38+
throw new RuntimeException("Only field officers can upload locations");
39+
}
40+
41+
String officerBadgeNo = user.getBadgeNo();
42+
if (officerBadgeNo == null || officerBadgeNo.isBlank()) {
43+
throw new RuntimeException("Badge number missing");
44+
}
45+
service.saveBulk(officerBadgeNo, points);
46+
}
47+
48+
@GetMapping("/admin/officers/{officerBadgeNo}/locations")
49+
public Object history(
50+
@AuthenticationPrincipal User user,
51+
@PathVariable String officerBadgeNo,
52+
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) Instant from,
53+
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) Instant to) {
54+
System.out.println("📍 LocationController.history() called");
55+
System.out.println(" Badge: " + officerBadgeNo);
56+
System.out.println(" From: " + from + ", To: " + to);
57+
System.out.println(" User: " + (user != null ? user.getEmail() : "NULL"));
58+
System.out.println(" Role: " + (user != null ? user.getRole() : "NULL"));
59+
System.out.println(" Authorities: " + (user != null ? user.getAuthorities() : "NULL"));
60+
return service.getHistory(officerBadgeNo, from, to);
61+
}
62+
63+
@GetMapping("/debug/whoami")
64+
public Map<String, Object> whoAmI(@AuthenticationPrincipal User user) {
65+
Map<String, Object> info = new HashMap<>();
66+
if (user != null) {
67+
info.put("email", user.getEmail());
68+
info.put("name", user.getName());
69+
info.put("role", user.getRole());
70+
info.put("authorities", user.getAuthorities().stream()
71+
.map(auth -> auth.getAuthority())
72+
.toList());
73+
info.put("userId", user.getUserId());
74+
info.put("badgeNo", user.getBadgeNo());
75+
} else {
76+
info.put("error", "No authenticated user");
77+
}
78+
return info;
79+
}
80+
81+
@GetMapping("/admin/officers/{officerBadgeNo}/locations/last")
82+
public Object lastLocation(@PathVariable String officerBadgeNo) {
83+
return service.getLastLocation(officerBadgeNo);
84+
}
85+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package com.crimeLink.analyzer.dto;
2+
3+
import java.time.Instant;
4+
import java.util.Map;
5+
6+
public record LocationPointDTO(
7+
Instant ts,
8+
double latitude,
9+
double longitude,
10+
Float accuracyM,
11+
Float speedMps,
12+
Float headingDeg,
13+
String provider,
14+
Map<String, Object> meta) {
15+
16+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package com.crimeLink.analyzer.entity;
2+
3+
import java.time.Instant;
4+
5+
import org.hibernate.annotations.JdbcTypeCode;
6+
import org.hibernate.type.SqlTypes;
7+
8+
import com.fasterxml.jackson.databind.JsonNode;
9+
10+
import jakarta.persistence.Column;
11+
import jakarta.persistence.Entity;
12+
import jakarta.persistence.GeneratedValue;
13+
import jakarta.persistence.GenerationType;
14+
import jakarta.persistence.Id;
15+
import jakarta.persistence.Index;
16+
import jakarta.persistence.Table;
17+
import lombok.AllArgsConstructor;
18+
import lombok.Getter;
19+
import lombok.NoArgsConstructor;
20+
import lombok.Setter;
21+
22+
@Entity
23+
@AllArgsConstructor
24+
@NoArgsConstructor
25+
@Getter
26+
@Setter
27+
@Table(name = "location_points", indexes = {
28+
@Index(name = "idx_location_points_officer_ts", columnList = "officer_badge_no, ts") })
29+
public class LocationPoint {
30+
@Id
31+
@GeneratedValue(strategy = GenerationType.IDENTITY)
32+
private Long id;
33+
34+
@Column(name = "officer_badge_no", nullable = false, length = 20)
35+
private String officerBadgeNo;
36+
private Instant ts;
37+
private double latitude;
38+
private double longitude;
39+
40+
private Float accuracyM;
41+
private Float speedMps;
42+
private Float headingDeg;
43+
44+
private String provider;
45+
46+
@JdbcTypeCode(SqlTypes.JSON)
47+
@Column(name = "meta", columnDefinition = "jsonb")
48+
private JsonNode meta;
49+
}

src/main/java/com/crimeLink/analyzer/repository/DutyScheduleRepository.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import com.crimeLink.analyzer.entity.DutySchedule;
44
import org.springframework.data.jpa.repository.JpaRepository;
5+
import org.springframework.data.jpa.repository.Query;
56
import org.springframework.stereotype.Repository;
67

78
import java.time.LocalDate;
@@ -38,4 +39,13 @@ long countByAssignedOfficer_UserIdAndDateBetween(
3839
LocalDate start,
3940
LocalDate end
4041
);
42+
43+
@Query("""
44+
SELECT DISTINCT d.location
45+
FROM DutySchedule d
46+
WHERE d.location IS NOT NULL
47+
AND LENGTH(TRIM(d.location)) > 0
48+
ORDER BY d.location
49+
""")
50+
List<String> findDistinctLocations();
4151
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.crimeLink.analyzer.repository;
2+
3+
import java.time.Instant;
4+
import java.util.List;
5+
6+
import org.springframework.data.domain.Pageable;
7+
import org.springframework.data.jpa.repository.JpaRepository;
8+
9+
import com.crimeLink.analyzer.entity.LocationPoint;
10+
11+
public interface LocationPointRepository extends JpaRepository<LocationPoint, Long> {
12+
List<LocationPoint> findByOfficerBadgeNoAndTsBetweenOrderByTsAsc(String officerBadgeNo, Instant from, Instant to);
13+
14+
List<LocationPoint> findByOfficerBadgeNoOrderByTsDesc(String officerBadgeNo, Pageable pageable);
15+
}

src/main/java/com/crimeLink/analyzer/repository/RefreshTokenRepository.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313
@Repository
1414
public interface RefreshTokenRepository extends JpaRepository<RefreshToken, Long> {
1515
Optional<RefreshToken> findByToken(String token);
16+
17+
@Query("SELECT rt FROM RefreshToken rt JOIN FETCH rt.user WHERE rt.token = ?1")
18+
Optional<RefreshToken> findByTokenWithUser(String token);
1619

1720
@Modifying
1821
@Query("DELETE FROM RefreshToken rt WHERE rt.expiryDate < ?1")

0 commit comments

Comments
 (0)