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
13 changes: 10 additions & 3 deletions src/main/java/com/crimeLink/analyzer/config/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,15 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
.requestMatchers("/api/facial/health").permitAll() // ML service health check
.requestMatchers("/api/call-analysis/health").permitAll() // ML service health check

// ML Service endpoints - Investigator role only
// ML Service endpoints
.requestMatchers("/api/call-analysis/**").hasRole("Investigator")
.requestMatchers("/api/facial/register").hasAnyRole("Investigator", "OIC")
.requestMatchers("/api/facial/criminals").hasAnyRole("Investigator", "OIC")
.requestMatchers("/api/facial/**").hasRole("Investigator")

// Criminal CRUD (direct DB, no Python)
.requestMatchers("/api/criminals/**").hasAnyRole("Investigator", "OIC")
.requestMatchers("/api/criminals").hasAnyRole("Investigator", "OIC")

Copilot AI Mar 12, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/api/criminals/** already matches /api/criminals, so the additional matcher for /api/criminals is redundant and never reached. Removing the duplicate reduces confusion about which rule applies.

Suggested change
.requestMatchers("/api/criminals").hasAnyRole("Investigator", "OIC")

Copilot uses AI. Check for mistakes.

.requestMatchers("/api/database/**").permitAll()
.requestMatchers("/api/test").permitAll()
Expand All @@ -79,8 +85,9 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
.requestMatchers("/api/weapon/**").hasRole("OIC")
.requestMatchers("/api/weapon-issue/**").hasRole("OIC")

// Admin/OIC routes (officer data, locations, users)
.requestMatchers("/api/users/field-officers").hasAnyRole("Admin", "OIC")
// Admin/OIC/Investigator routes (officer data, locations, users)
.requestMatchers("/api/users/field-officers").hasAnyRole("Admin", "OIC", "Investigator")
.requestMatchers("/api/admin/officers/*/locations/**").hasAnyRole("Admin", "OIC", "Investigator")
.requestMatchers("/api/admin/**").hasAnyRole("OIC", "Admin")

.anyRequest().authenticated())
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
package com.crimeLink.analyzer.controller;

import com.crimeLink.analyzer.service.CriminalService;
import com.crimeLink.analyzer.util.LogSanitizer;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.util.List;
import java.util.Map;
import java.util.Optional;

/**
* REST Controller for Criminal record management (CRUD).
* Operates directly against the database via JPA — coordinates with Python ML for embeddings.
*
* Endpoints:
* - POST /api/criminals - Create new criminal (with photo upload + embedding)
* - GET /api/criminals - List all criminals
* - GET /api/criminals/{id} - Get criminal details
* - PUT /api/criminals/{id} - Update criminal profile (with optional photo)
* - DELETE /api/criminals/{id} - Delete criminal record
*/
@RestController
@RequestMapping("/api/criminals")
@RequiredArgsConstructor
@Slf4j
public class CriminalController {

private final CriminalService criminalService;

/**
* Create a new criminal record with optional photo.
*/
@PostMapping
public ResponseEntity<?> createCriminal(
@RequestParam("name") String name,
@RequestParam("nic") String nic,
@RequestParam(value = "photo", required = false) MultipartFile photo,
@RequestParam(value = "risk_level", required = false) String riskLevel,
@RequestParam(value = "crime_history", required = false) String crimeHistory,
@RequestParam(value = "address", required = false) String address,
@RequestParam(value = "contact_number", required = false) String contactNumber,
@RequestParam(value = "secondary_contact", required = false) String secondaryContact,
@RequestParam(value = "date_of_birth", required = false) String dateOfBirth,
@RequestParam(value = "gender", required = false) String gender,
@RequestParam(value = "alias", required = false) String alias,
@RequestParam(value = "status", required = false) String status) {

try {
log.info("Criminal registration requested: name={}, nic={}",
LogSanitizer.sanitize(name), LogSanitizer.sanitize(nic));

if (name == null || name.trim().isEmpty()) {
return ResponseEntity.badRequest().body(Map.of("error", "Name is required"));
}
if (nic == null || nic.trim().isEmpty()) {
return ResponseEntity.badRequest().body(Map.of("error", "NIC is required"));
}

Map<String, Object> result = criminalService.createCriminal(
name.trim(), nic.trim(), riskLevel, crimeHistory,
address, contactNumber, secondaryContact,
dateOfBirth, gender, alias, status, photo);

return ResponseEntity.ok(result);

} catch (Exception e) {
log.error("Criminal registration failed: {}", e.getMessage());
return ResponseEntity.internalServerError()
.body(Map.of("error", "Registration failed: " + e.getMessage()));
}
}

/**
* Get all criminals (summary list).
*/
@GetMapping
public ResponseEntity<?> getAllCriminals() {
try {
List<Map<String, Object>> criminals = criminalService.getAllCriminals();
return ResponseEntity.ok(criminals);
} catch (Exception e) {
log.error("Failed to fetch criminals: {}", e.getMessage());
return ResponseEntity.internalServerError()
.body(Map.of("error", "Failed to fetch criminals: " + e.getMessage()));
}
}

/**
* Get full details for a specific criminal.
*/
@GetMapping("/{criminalId}")
public ResponseEntity<?> getCriminalDetails(@PathVariable String criminalId) {
try {
Optional<Map<String, Object>> result = criminalService.getCriminalDetails(criminalId);
if (result.isEmpty()) {
return ResponseEntity.status(404)
.body(Map.of("error", "Criminal not found: " + LogSanitizer.sanitize(criminalId)));
}
return ResponseEntity.ok(result.get());
} catch (Exception e) {
log.error("Failed to fetch criminal {}: {}", LogSanitizer.sanitize(criminalId), e.getMessage());
return ResponseEntity.internalServerError()
.body(Map.of("error", "Failed to fetch criminal details: " + e.getMessage()));
}
}

/**
* Update an existing criminal's profile data (with optional photo change).
*/
@PutMapping("/{criminalId}")
public ResponseEntity<?> updateCriminal(
@PathVariable String criminalId,
@RequestParam(value = "name", required = false) String name,
@RequestParam(value = "nic", required = false) String nic,
@RequestParam(value = "photo", required = false) MultipartFile photo,
@RequestParam(value = "risk_level", required = false) String riskLevel,
@RequestParam(value = "crime_history", required = false) String crimeHistory,
@RequestParam(value = "address", required = false) String address,
@RequestParam(value = "contact_number", required = false) String contactNumber,
@RequestParam(value = "secondary_contact", required = false) String secondaryContact,
@RequestParam(value = "date_of_birth", required = false) String dateOfBirth,
@RequestParam(value = "gender", required = false) String gender,
@RequestParam(value = "alias", required = false) String alias,
@RequestParam(value = "status", required = false) String status) {

try {
log.info("Criminal update requested for ID: {}", LogSanitizer.sanitize(criminalId));

Optional<Map<String, Object>> result = criminalService.updateCriminal(
criminalId, name, nic, riskLevel, crimeHistory,
address, contactNumber, secondaryContact,
dateOfBirth, gender, alias, status, photo);

if (result.isEmpty()) {
return ResponseEntity.status(404)
.body(Map.of("error", "Criminal not found: " + LogSanitizer.sanitize(criminalId)));
}

Map<String, Object> response = new java.util.LinkedHashMap<>();
response.put("message", "Criminal updated successfully");
response.put("criminal", result.get());
return ResponseEntity.ok(response);

} catch (Exception e) {
log.error("Criminal update failed for {}: {}", LogSanitizer.sanitize(criminalId), e.getMessage());
return ResponseEntity.internalServerError()
.body(Map.of("error", "Update failed: " + e.getMessage()));
}
}

/**
* Delete a criminal record (cascades to suspect_photos, cleans up storage).
*/
@DeleteMapping("/{criminalId}")
public ResponseEntity<?> deleteCriminal(@PathVariable String criminalId) {
try {
log.info("Criminal deletion requested for ID: {}", LogSanitizer.sanitize(criminalId));

boolean deleted = criminalService.deleteCriminal(criminalId);
if (!deleted) {
return ResponseEntity.status(404)
.body(Map.of("error", "Criminal not found: " + LogSanitizer.sanitize(criminalId)));
}

return ResponseEntity.ok(Map.of("message", "Criminal deleted successfully", "criminal_id", criminalId));

} catch (Exception e) {
log.error("Criminal deletion failed for {}: {}", LogSanitizer.sanitize(criminalId), e.getMessage());
return ResponseEntity.internalServerError()
.body(Map.of("error", "Deletion failed: " + e.getMessage()));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,15 @@ public ResponseEntity<?> registerCriminal(
@RequestParam(value = "criminal_id", required = false) String criminalId,
@RequestParam("name") String name,
@RequestParam("nic") String nic,
@RequestParam(value = "risk_level", required = false, defaultValue = "medium") String riskLevel) {
@RequestParam(value = "risk_level", required = false, defaultValue = "medium") String riskLevel,
@RequestParam(value = "crime_history", required = false) String crimeHistory,
@RequestParam(value = "address", required = false) String address,
@RequestParam(value = "contact_number", required = false) String contactNumber,
@RequestParam(value = "secondary_contact", required = false) String secondaryContact,
@RequestParam(value = "date_of_birth", required = false) String dateOfBirth,
@RequestParam(value = "gender", required = false) String gender,
@RequestParam(value = "alias", required = false) String alias,
@RequestParam(value = "status", required = false, defaultValue = "active") String status) {

try {
log.info("Criminal registration requested: {} ({})", LogSanitizer.sanitize(name), LogSanitizer.sanitize(nic));
Expand All @@ -122,7 +130,8 @@ public ResponseEntity<?> registerCriminal(

// Forward to ML service
JsonNode result = facialRecognitionService.registerCriminal(
photo, criminalId, name, nic, riskLevel);
photo, criminalId, name, nic, riskLevel, crimeHistory,
address, contactNumber, secondaryContact, dateOfBirth, gender, alias, status);

return ResponseEntity.ok(result);

Expand Down
67 changes: 67 additions & 0 deletions src/main/java/com/crimeLink/analyzer/entity/Criminal.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.crimeLink.analyzer.entity;

import jakarta.persistence.*;
import java.time.LocalDate;

@Entity
@Table(name = "criminals")
Expand Down Expand Up @@ -28,6 +29,24 @@ public class Criminal {
@Column(name = "status", length = 50)
private String status;

@Column(name = "risk_level", length = 50)
private String riskLevel;

@Column(name = "crime_history", columnDefinition = "TEXT")
private String crimeHistory;

@Column(name = "primary_photo_url", length = 500)
private String primaryPhotoUrl;

@Column(name = "date_of_birth")
private LocalDate dateOfBirth;

@Column(name = "gender", length = 10)
private String gender;

@Column(name = "alias", length = 255)
private String alias;

// Constructors
public Criminal() {}

Expand Down Expand Up @@ -87,4 +106,52 @@ public String getStatus() {
public void setStatus(String status) {
this.status = status;
}

public String getRiskLevel() {
return riskLevel;
}

public void setRiskLevel(String riskLevel) {
this.riskLevel = riskLevel;
}

public String getCrimeHistory() {
return crimeHistory;
}

public void setCrimeHistory(String crimeHistory) {
this.crimeHistory = crimeHistory;
}

public String getPrimaryPhotoUrl() {
return primaryPhotoUrl;
}

public void setPrimaryPhotoUrl(String primaryPhotoUrl) {
this.primaryPhotoUrl = primaryPhotoUrl;
}

public LocalDate getDateOfBirth() {
return dateOfBirth;
}

public void setDateOfBirth(LocalDate dateOfBirth) {
this.dateOfBirth = dateOfBirth;
}

public String getGender() {
return gender;
}

public void setGender(String gender) {
this.gender = gender;
}

public String getAlias() {
return alias;
}

public void setAlias(String alias) {
this.alias = alias;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

import java.util.List;
import java.util.Optional;

@Repository
Expand All @@ -17,4 +18,7 @@ public interface CriminalRepository extends JpaRepository<Criminal, String> {

@Query("SELECT c FROM Criminal c WHERE c.contactNumber = :phone OR c.secondaryContact = :phone")
Optional<Criminal> findByPhoneNumber(@Param("phone") String phone);

@Query(value = "SELECT id FROM criminals WHERE face_embedding IS NOT NULL", nativeQuery = true)
List<String> findIdsWithEmbedding();
}
Loading