From 8efeb8b4fa8b9d449fb4a4331d61686ebbd2320e Mon Sep 17 00:00:00 2001 From: JinethBosilu <157296244+JinethBosilu@users.noreply.github.com> Date: Sat, 28 Feb 2026 17:45:18 +0530 Subject: [PATCH 1/4] feat: Update role-based access for field-officers and admin routes to include Investigator role --- .../java/com/crimeLink/analyzer/config/SecurityConfig.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/crimeLink/analyzer/config/SecurityConfig.java b/src/main/java/com/crimeLink/analyzer/config/SecurityConfig.java index ac4771b..1f59934 100644 --- a/src/main/java/com/crimeLink/analyzer/config/SecurityConfig.java +++ b/src/main/java/com/crimeLink/analyzer/config/SecurityConfig.java @@ -73,8 +73,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()) From 6516fb74c6bac9003bca758b54a0196d0602ad90 Mon Sep 17 00:00:00 2001 From: JinethBosilu <157296244+JinethBosilu@users.noreply.github.com> Date: Sun, 1 Mar 2026 03:05:31 +0530 Subject: [PATCH 2/4] feat: Enhance criminal management with CRUD operations and update security roles for ML service endpoints --- .../analyzer/config/SecurityConfig.java | 8 +- .../controller/CriminalController.java | 107 ++++++++++++++++ .../FacialRecognitionController.java | 13 +- .../crimeLink/analyzer/entity/Criminal.java | 67 ++++++++++ .../analyzer/service/CriminalService.java | 117 ++++++++++++++++++ .../service/FacialRecognitionService.java | 14 ++- 6 files changed, 321 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/crimeLink/analyzer/controller/CriminalController.java create mode 100644 src/main/java/com/crimeLink/analyzer/service/CriminalService.java diff --git a/src/main/java/com/crimeLink/analyzer/config/SecurityConfig.java b/src/main/java/com/crimeLink/analyzer/config/SecurityConfig.java index 1f59934..56e1781 100644 --- a/src/main/java/com/crimeLink/analyzer/config/SecurityConfig.java +++ b/src/main/java/com/crimeLink/analyzer/config/SecurityConfig.java @@ -49,9 +49,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") .requestMatchers("/api/database/**").permitAll() .requestMatchers("/api/test").permitAll() diff --git a/src/main/java/com/crimeLink/analyzer/controller/CriminalController.java b/src/main/java/com/crimeLink/analyzer/controller/CriminalController.java new file mode 100644 index 0000000..be61c19 --- /dev/null +++ b/src/main/java/com/crimeLink/analyzer/controller/CriminalController.java @@ -0,0 +1,107 @@ +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 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 — no Python ML service involved. + * + * Endpoints: + * - GET /api/criminals - List all criminals + * - GET /api/criminals/{id} - Get criminal details + * - PUT /api/criminals/{id} - Update criminal profile + */ +@RestController +@RequestMapping("/api/criminals") +@RequiredArgsConstructor +@Slf4j +public class CriminalController { + + private final CriminalService criminalService; + + /** + * Get all criminals (summary list). + */ + @GetMapping + public ResponseEntity getAllCriminals() { + try { + List> 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> 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. + */ + @PutMapping("/{criminalId}") + public ResponseEntity updateCriminal( + @PathVariable String criminalId, + @RequestParam(value = "name", required = false) String name, + @RequestParam(value = "nic", required = false) String nic, + @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> result = criminalService.updateCriminal( + criminalId, name, nic, riskLevel, crimeHistory, + address, contactNumber, secondaryContact, + dateOfBirth, gender, alias, status); + + if (result.isEmpty()) { + return ResponseEntity.status(404) + .body(Map.of("error", "Criminal not found: " + LogSanitizer.sanitize(criminalId))); + } + + Map 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())); + } + } +} diff --git a/src/main/java/com/crimeLink/analyzer/controller/FacialRecognitionController.java b/src/main/java/com/crimeLink/analyzer/controller/FacialRecognitionController.java index d8438fe..473362f 100644 --- a/src/main/java/com/crimeLink/analyzer/controller/FacialRecognitionController.java +++ b/src/main/java/com/crimeLink/analyzer/controller/FacialRecognitionController.java @@ -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)); @@ -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); diff --git a/src/main/java/com/crimeLink/analyzer/entity/Criminal.java b/src/main/java/com/crimeLink/analyzer/entity/Criminal.java index 9620bc1..fa6d55c 100644 --- a/src/main/java/com/crimeLink/analyzer/entity/Criminal.java +++ b/src/main/java/com/crimeLink/analyzer/entity/Criminal.java @@ -1,6 +1,7 @@ package com.crimeLink.analyzer.entity; import jakarta.persistence.*; +import java.time.LocalDate; @Entity @Table(name = "criminals") @@ -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() {} @@ -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; + } } diff --git a/src/main/java/com/crimeLink/analyzer/service/CriminalService.java b/src/main/java/com/crimeLink/analyzer/service/CriminalService.java new file mode 100644 index 0000000..f8ae06c --- /dev/null +++ b/src/main/java/com/crimeLink/analyzer/service/CriminalService.java @@ -0,0 +1,117 @@ +package com.crimeLink.analyzer.service; + +import com.crimeLink.analyzer.entity.Criminal; +import com.crimeLink.analyzer.repository.CriminalRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.time.LocalDate; +import java.util.*; + +/** + * Service for direct criminal record CRUD operations against the database. + * Handles profile data management (no ML operations). + */ +@Service +@RequiredArgsConstructor +@Slf4j +public class CriminalService { + + private final CriminalRepository criminalRepository; + + /** + * Get all criminals as a list of maps (JSON-friendly). + */ + public List> getAllCriminals() { + List criminals = criminalRepository.findAll(); + List> result = new ArrayList<>(); + for (Criminal c : criminals) { + result.add(toSummaryMap(c)); + } + result.sort(Comparator.comparing(m -> (String) m.getOrDefault("name", ""))); + return result; + } + + /** + * Get full details for a specific criminal by ID. + */ + public Optional> getCriminalDetails(String criminalId) { + return criminalRepository.findById(criminalId).map(this::toDetailMap); + } + + /** + * Update an existing criminal's profile fields. + * Only non-null parameters are applied. + * + * @return Updated criminal as a map, or empty if not found. + */ + public Optional> updateCriminal( + String criminalId, + String name, String nic, String riskLevel, String crimeHistory, + String address, String contactNumber, String secondaryContact, + String dateOfBirth, String gender, String alias, String status) { + + Optional opt = criminalRepository.findById(criminalId); + if (opt.isEmpty()) { + return Optional.empty(); + } + + Criminal c = opt.get(); + + if (name != null) c.setName(name); + if (nic != null) c.setNic(nic); + if (riskLevel != null) c.setRiskLevel(riskLevel); + if (crimeHistory != null) c.setCrimeHistory(crimeHistory); + if (address != null) c.setAddress(address); + if (contactNumber != null) c.setContactNumber(contactNumber); + if (secondaryContact != null) c.setSecondaryContact(secondaryContact); + if (dateOfBirth != null) { + try { + c.setDateOfBirth(LocalDate.parse(dateOfBirth)); + } catch (Exception e) { + log.warn("Invalid date_of_birth format: {}", dateOfBirth); + } + } + if (gender != null) c.setGender(gender); + if (alias != null) c.setAlias(alias); + if (status != null) c.setStatus(status); + + Criminal saved = criminalRepository.save(c); + log.info("Criminal updated: {}", criminalId); + + return Optional.of(toDetailMap(saved)); + } + + /* ─── mapping helpers ─── */ + + private Map toSummaryMap(Criminal c) { + Map m = new LinkedHashMap<>(); + m.put("id", c.getId()); + m.put("name", c.getName()); + m.put("nic", c.getNic()); + m.put("risk_level", c.getRiskLevel()); + m.put("primary_photo_url", c.getPrimaryPhotoUrl()); + m.put("status", c.getStatus()); + m.put("has_embedding", false); // embedding lives in Python side; list view doesn't need it + return m; + } + + private Map toDetailMap(Criminal c) { + Map m = new LinkedHashMap<>(); + m.put("id", c.getId()); + m.put("name", c.getName()); + m.put("nic", c.getNic()); + m.put("risk_level", c.getRiskLevel()); + m.put("crime_history", c.getCrimeHistory()); + m.put("primary_photo_url", c.getPrimaryPhotoUrl()); + m.put("status", c.getStatus()); + m.put("address", c.getAddress()); + m.put("contact_number", c.getContactNumber()); + m.put("secondary_contact", c.getSecondaryContact()); + m.put("date_of_birth", c.getDateOfBirth() != null ? c.getDateOfBirth().toString() : null); + m.put("gender", c.getGender()); + m.put("alias", c.getAlias()); + return m; + } +} diff --git a/src/main/java/com/crimeLink/analyzer/service/FacialRecognitionService.java b/src/main/java/com/crimeLink/analyzer/service/FacialRecognitionService.java index f125a1a..7282105 100644 --- a/src/main/java/com/crimeLink/analyzer/service/FacialRecognitionService.java +++ b/src/main/java/com/crimeLink/analyzer/service/FacialRecognitionService.java @@ -122,7 +122,9 @@ public String getFilename() { * @return JSON response from ML service */ public JsonNode registerCriminal(MultipartFile photo, String criminalId, String name, - String nic, String riskLevel) { + String nic, String riskLevel, String crimeHistory, + String address, String contactNumber, String secondaryContact, + String dateOfBirth, String gender, String alias, String status) { log.info("Forwarding criminal registration to ML service: {} ({})", LogSanitizer.sanitize(name), LogSanitizer.sanitize(nic)); String url = facialRecognitionServiceUrl + "/register"; @@ -143,7 +145,7 @@ public JsonNode registerCriminal(MultipartFile photo, String criminalId, String throw new RuntimeException("Failed to read uploaded photo contents", e); } - body.add("photo", new ByteArrayResource(photoBytes) { + body.add("photos", new ByteArrayResource(photoBytes) { @Override public String getFilename() { return photo.getOriginalFilename(); @@ -154,6 +156,14 @@ public String getFilename() { body.add("name", name); body.add("nic", nic); if (riskLevel != null) body.add("risk_level", riskLevel); + if (crimeHistory != null) body.add("crime_history", crimeHistory); + if (address != null) body.add("address", address); + if (contactNumber != null) body.add("contact_number", contactNumber); + if (secondaryContact != null) body.add("secondary_contact", secondaryContact); + if (dateOfBirth != null) body.add("date_of_birth", dateOfBirth); + if (gender != null) body.add("gender", gender); + if (alias != null) body.add("alias", alias); + if (status != null) body.add("status", status); HttpEntity> requestEntity = new HttpEntity<>(body, headers); From 21f7fead717097cedcd8d869e96b46f5bde6853e Mon Sep 17 00:00:00 2001 From: JinethBosilu <157296244+JinethBosilu@users.noreply.github.com> Date: Sun, 1 Mar 2026 04:09:10 +0530 Subject: [PATCH 3/4] feat: Implement CRUD operations for criminal records with photo upload and embedding generation --- .../controller/CriminalController.java | 82 ++++++++- .../repository/CriminalRepository.java | 4 + .../analyzer/service/CriminalService.java | 130 ++++++++++++++- .../service/FacialRecognitionService.java | 52 ++++++ .../service/SupabaseStorageService.java | 156 ++++++++++++++++++ src/main/resources/application.properties | 5 + 6 files changed, 418 insertions(+), 11 deletions(-) create mode 100644 src/main/java/com/crimeLink/analyzer/service/SupabaseStorageService.java diff --git a/src/main/java/com/crimeLink/analyzer/controller/CriminalController.java b/src/main/java/com/crimeLink/analyzer/controller/CriminalController.java index be61c19..20aecce 100644 --- a/src/main/java/com/crimeLink/analyzer/controller/CriminalController.java +++ b/src/main/java/com/crimeLink/analyzer/controller/CriminalController.java @@ -6,6 +6,7 @@ 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; @@ -13,12 +14,14 @@ /** * REST Controller for Criminal record management (CRUD). - * Operates directly against the database via JPA — no Python ML service involved. + * Operates directly against the database via JPA — coordinates with Python ML for embeddings. * * Endpoints: - * - GET /api/criminals - List all criminals - * - GET /api/criminals/{id} - Get criminal details - * - PUT /api/criminals/{id} - Update criminal profile + * - 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") @@ -28,6 +31,49 @@ 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 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). */ @@ -63,13 +109,14 @@ public ResponseEntity getCriminalDetails(@PathVariable String criminalId) { } /** - * Update an existing criminal's profile data. + * 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, @@ -86,7 +133,7 @@ public ResponseEntity updateCriminal( Optional> result = criminalService.updateCriminal( criminalId, name, nic, riskLevel, crimeHistory, address, contactNumber, secondaryContact, - dateOfBirth, gender, alias, status); + dateOfBirth, gender, alias, status, photo); if (result.isEmpty()) { return ResponseEntity.status(404) @@ -104,4 +151,27 @@ public ResponseEntity updateCriminal( .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())); + } + } } diff --git a/src/main/java/com/crimeLink/analyzer/repository/CriminalRepository.java b/src/main/java/com/crimeLink/analyzer/repository/CriminalRepository.java index a4272d2..facf663 100644 --- a/src/main/java/com/crimeLink/analyzer/repository/CriminalRepository.java +++ b/src/main/java/com/crimeLink/analyzer/repository/CriminalRepository.java @@ -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 @@ -17,4 +18,7 @@ public interface CriminalRepository extends JpaRepository { @Query("SELECT c FROM Criminal c WHERE c.contactNumber = :phone OR c.secondaryContact = :phone") Optional findByPhoneNumber(@Param("phone") String phone); + + @Query(value = "SELECT id FROM criminals WHERE face_embedding IS NOT NULL", nativeQuery = true) + List findIdsWithEmbedding(); } diff --git a/src/main/java/com/crimeLink/analyzer/service/CriminalService.java b/src/main/java/com/crimeLink/analyzer/service/CriminalService.java index f8ae06c..50f0c80 100644 --- a/src/main/java/com/crimeLink/analyzer/service/CriminalService.java +++ b/src/main/java/com/crimeLink/analyzer/service/CriminalService.java @@ -5,13 +5,14 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; import java.time.LocalDate; import java.util.*; /** * Service for direct criminal record CRUD operations against the database. - * Handles profile data management (no ML operations). + * Handles profile data management and coordinates with ML service for embeddings. */ @Service @RequiredArgsConstructor @@ -19,15 +20,90 @@ public class CriminalService { private final CriminalRepository criminalRepository; + private final SupabaseStorageService supabaseStorageService; + private final FacialRecognitionService facialRecognitionService; + + /** + * Create a new criminal record with an optional photo upload. + * + * @return A map with criminal_id, name, nic, and message. + */ + public Map createCriminal( + String name, String nic, String riskLevel, String crimeHistory, + String address, String contactNumber, String secondaryContact, + String dateOfBirth, String gender, String alias, String status, + MultipartFile photo) { + + // Generate short UUID (matching Python service format) + String criminalId = UUID.randomUUID().toString().substring(0, 8); + + Criminal c = new Criminal(); + c.setId(criminalId); + c.setName(name); + c.setNic(nic); + c.setRiskLevel(riskLevel != null ? riskLevel : "medium"); + c.setStatus(status != null ? status : "active"); + if (crimeHistory != null) c.setCrimeHistory(crimeHistory); + if (address != null) c.setAddress(address); + if (contactNumber != null) c.setContactNumber(contactNumber); + if (secondaryContact != null) c.setSecondaryContact(secondaryContact); + if (dateOfBirth != null) { + try { + c.setDateOfBirth(LocalDate.parse(dateOfBirth)); + } catch (Exception e) { + log.warn("Invalid date_of_birth format: {}", dateOfBirth); + } + } + if (gender != null) c.setGender(gender); + if (alias != null) c.setAlias(alias); + + // Upload photo to Supabase Storage if provided + String photoUrl = null; + if (photo != null && !photo.isEmpty()) { + try { + photoUrl = supabaseStorageService.uploadPhoto(criminalId, photo); + c.setPrimaryPhotoUrl(photoUrl); + } catch (Exception e) { + log.error("Photo upload failed for criminal {}: {}", criminalId, e.getMessage()); + // Continue without photo — record still gets created + } + } + + Criminal saved = criminalRepository.save(c); + log.info("Criminal created: {} (name: {})", criminalId, name); + + // Generate face embedding via Python ML service (non-blocking) + boolean hasEmbedding = false; + if (photo != null && !photo.isEmpty()) { + try { + facialRecognitionService.generateEmbedding(criminalId, photo); + hasEmbedding = true; + log.info("Face embedding generated for criminal {}", criminalId); + } catch (Exception e) { + log.warn("Embedding generation failed for criminal {} (non-fatal): {}", criminalId, e.getMessage()); + } + } + + Map result = new LinkedHashMap<>(); + result.put("criminal_id", saved.getId()); + result.put("name", saved.getName()); + result.put("nic", saved.getNic()); + result.put("photos_stored", photoUrl != null ? 1 : 0); + result.put("primary_photo_url", photoUrl); + result.put("has_embedding", hasEmbedding); + result.put("message", "Criminal registered successfully"); + return result; + } /** * Get all criminals as a list of maps (JSON-friendly). */ public List> getAllCriminals() { List criminals = criminalRepository.findAll(); + Set idsWithEmbedding = new HashSet<>(criminalRepository.findIdsWithEmbedding()); List> result = new ArrayList<>(); for (Criminal c : criminals) { - result.add(toSummaryMap(c)); + result.add(toSummaryMap(c, idsWithEmbedding.contains(c.getId()))); } result.sort(Comparator.comparing(m -> (String) m.getOrDefault("name", ""))); return result; @@ -50,7 +126,8 @@ public Optional> updateCriminal( String criminalId, String name, String nic, String riskLevel, String crimeHistory, String address, String contactNumber, String secondaryContact, - String dateOfBirth, String gender, String alias, String status) { + String dateOfBirth, String gender, String alias, String status, + MultipartFile photo) { Optional opt = criminalRepository.findById(criminalId); if (opt.isEmpty()) { @@ -77,15 +154,58 @@ public Optional> updateCriminal( if (alias != null) c.setAlias(alias); if (status != null) c.setStatus(status); + // Handle photo change + if (photo != null && !photo.isEmpty()) { + try { + String newPhotoUrl = supabaseStorageService.uploadPhoto(criminalId, photo); + c.setPrimaryPhotoUrl(newPhotoUrl); + log.info("Photo updated for criminal {}", criminalId); + } catch (Exception e) { + log.error("Photo upload failed for criminal {}: {}", criminalId, e.getMessage()); + } + + // Regenerate face embedding with new photo + try { + facialRecognitionService.generateEmbedding(criminalId, photo); + log.info("Embedding regenerated for criminal {}", criminalId); + } catch (Exception e) { + log.warn("Embedding regeneration failed for criminal {} (non-fatal): {}", criminalId, e.getMessage()); + } + } + Criminal saved = criminalRepository.save(c); log.info("Criminal updated: {}", criminalId); return Optional.of(toDetailMap(saved)); } + /** + * Delete a criminal record, associated storage files, and cascade DB relations. + * + * @return true if deleted, false if not found. + */ + public boolean deleteCriminal(String criminalId) { + Optional opt = criminalRepository.findById(criminalId); + if (opt.isEmpty()) { + return false; + } + + // Best-effort: delete photos from Supabase Storage + try { + supabaseStorageService.deleteFolder(criminalId); + } catch (Exception e) { + log.warn("Storage cleanup failed for criminal {} (non-fatal): {}", criminalId, e.getMessage()); + } + + // DB delete — suspect_photos cascade via ON DELETE CASCADE + criminalRepository.deleteById(criminalId); + log.info("Criminal deleted: {}", criminalId); + return true; + } + /* ─── mapping helpers ─── */ - private Map toSummaryMap(Criminal c) { + private Map toSummaryMap(Criminal c, boolean hasEmbedding) { Map m = new LinkedHashMap<>(); m.put("id", c.getId()); m.put("name", c.getName()); @@ -93,7 +213,7 @@ private Map toSummaryMap(Criminal c) { m.put("risk_level", c.getRiskLevel()); m.put("primary_photo_url", c.getPrimaryPhotoUrl()); m.put("status", c.getStatus()); - m.put("has_embedding", false); // embedding lives in Python side; list view doesn't need it + m.put("has_embedding", hasEmbedding); return m; } diff --git a/src/main/java/com/crimeLink/analyzer/service/FacialRecognitionService.java b/src/main/java/com/crimeLink/analyzer/service/FacialRecognitionService.java index 7282105..59883fd 100644 --- a/src/main/java/com/crimeLink/analyzer/service/FacialRecognitionService.java +++ b/src/main/java/com/crimeLink/analyzer/service/FacialRecognitionService.java @@ -234,6 +234,58 @@ public JsonNode getRecognitionHistory(Integer limit) { } } + /** + * Generate a face embedding for an existing criminal by sending the photo + * to the Python ML service's /generate-embedding endpoint. + * + * @param criminalId Existing criminal ID + * @param photo Photo file to extract embedding from + * @return JSON response from ML service + */ + public JsonNode generateEmbedding(String criminalId, MultipartFile photo) { + log.info("Requesting embedding generation for criminal: {}", LogSanitizer.sanitize(criminalId)); + + String url = facialRecognitionServiceUrl + "/generate-embedding"; + + try { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.MULTIPART_FORM_DATA); + + MultiValueMap body = new LinkedMultiValueMap<>(); + + byte[] photoBytes; + try { + photoBytes = photo.getBytes(); + } catch (IOException e) { + log.error("Failed to read photo bytes: {}", e.getMessage()); + throw new RuntimeException("Failed to read uploaded photo", e); + } + + body.add("photo", new ByteArrayResource(photoBytes) { + @Override + public String getFilename() { + return photo.getOriginalFilename(); + } + }); + body.add("criminal_id", criminalId); + + HttpEntity> requestEntity = new HttpEntity<>(body, headers); + + ResponseEntity response = restTemplate.exchange( + url, HttpMethod.POST, requestEntity, String.class); + + log.info("Embedding generated successfully for criminal {}", LogSanitizer.sanitize(criminalId)); + return objectMapper.readTree(response.getBody()); + + } catch (RestClientException e) { + log.warn("ML service unavailable for embedding generation: {}", e.getMessage()); + throw new RuntimeException("Facial recognition service unavailable: " + e.getMessage(), e); + } catch (IOException e) { + log.error("Failed to parse ML response: {}", e.getMessage()); + throw new RuntimeException("Failed to process ML service response: " + e.getMessage(), e); + } + } + /** * Check health status of the facial recognition ML service. * diff --git a/src/main/java/com/crimeLink/analyzer/service/SupabaseStorageService.java b/src/main/java/com/crimeLink/analyzer/service/SupabaseStorageService.java new file mode 100644 index 0000000..b2845be --- /dev/null +++ b/src/main/java/com/crimeLink/analyzer/service/SupabaseStorageService.java @@ -0,0 +1,156 @@ +package com.crimeLink.analyzer.service; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.*; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.multipart.MultipartFile; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.util.ArrayList; +import java.util.List; + +/** + * Service for uploading files to Supabase Storage. + * Uploads criminal photos to the configured bucket and returns public URLs. + */ +@Service +@Slf4j +public class SupabaseStorageService { + + @Value("${supabase.url}") + private String supabaseUrl; + + @Value("${supabase.service-key}") + private String supabaseServiceKey; + + @Value("${supabase.bucket}") + private String bucket; + + private final RestTemplate restTemplate = new RestTemplate(); + + /** + * Upload a photo to Supabase Storage. + * + * @param criminalId The criminal's ID (used as folder name) + * @param file The image file to upload + * @return The public URL of the uploaded photo + */ + public String uploadPhoto(String criminalId, MultipartFile file) { + String originalFilename = file.getOriginalFilename(); + if (originalFilename == null || originalFilename.isBlank()) { + originalFilename = "photo.jpg"; + } + + // Sanitize filename + String safeFilename = originalFilename.replaceAll("[^a-zA-Z0-9._-]", "_"); + String storagePath = criminalId + "/" + safeFilename; + + String uploadUrl = supabaseUrl + "/storage/v1/object/" + bucket + "/" + storagePath; + + try { + byte[] fileBytes = file.getBytes(); + + HttpHeaders headers = new HttpHeaders(); + headers.set("Authorization", "Bearer " + supabaseServiceKey); + headers.set("apikey", supabaseServiceKey); + headers.setContentType(MediaType.parseMediaType( + file.getContentType() != null ? file.getContentType() : "image/jpeg")); + // Upsert mode: overwrite if exists + headers.set("x-upsert", "true"); + + HttpEntity request = new HttpEntity<>(fileBytes, headers); + + ResponseEntity response = restTemplate.exchange( + uploadUrl, HttpMethod.POST, request, String.class); + + if (response.getStatusCode().is2xxSuccessful()) { + String publicUrl = supabaseUrl + "/storage/v1/object/public/" + bucket + "/" + storagePath; + log.info("Photo uploaded successfully: {}", publicUrl); + return publicUrl; + } else { + log.error("Supabase upload failed with status: {}", response.getStatusCode()); + throw new RuntimeException("Failed to upload photo to storage. Status: " + response.getStatusCode()); + } + } catch (Exception e) { + log.error("Failed to upload photo for criminal {}: {}", criminalId, e.getMessage()); + throw new RuntimeException("Photo upload failed: " + e.getMessage(), e); + } + } + + /** + * Delete all files under a criminal's folder in Supabase Storage. + * Lists files first, then issues a bulk delete. + * + * @param criminalId The criminal's ID (used as the folder prefix) + */ + public void deleteFolder(String criminalId) { + try { + // 1) List objects in the folder + String listUrl = supabaseUrl + "/storage/v1/object/list/" + bucket; + + HttpHeaders listHeaders = new HttpHeaders(); + listHeaders.set("Authorization", "Bearer " + supabaseServiceKey); + listHeaders.set("apikey", supabaseServiceKey); + listHeaders.setContentType(MediaType.APPLICATION_JSON); + + String listBody = "{\"prefix\":\"" + criminalId + "/\",\"limit\":100}"; + HttpEntity listRequest = new HttpEntity<>(listBody, listHeaders); + + ResponseEntity listResponse = restTemplate.exchange( + listUrl, HttpMethod.POST, listRequest, String.class); + + if (!listResponse.getStatusCode().is2xxSuccessful() || listResponse.getBody() == null) { + log.warn("Failed to list storage files for criminal {}", criminalId); + return; + } + + ObjectMapper mapper = new ObjectMapper(); + JsonNode files = mapper.readTree(listResponse.getBody()); + + if (!files.isArray() || files.isEmpty()) { + log.info("No storage files found for criminal {}", criminalId); + return; + } + + // 2) Collect file paths + List prefixes = new ArrayList<>(); + for (JsonNode file : files) { + String fileName = file.has("name") ? file.get("name").asText() : null; + if (fileName != null) { + prefixes.add(criminalId + "/" + fileName); + } + } + + if (prefixes.isEmpty()) return; + + // 3) Bulk delete via POST /storage/v1/object/remove + String deleteUrl = supabaseUrl + "/storage/v1/object/" + bucket; + + HttpHeaders deleteHeaders = new HttpHeaders(); + deleteHeaders.set("Authorization", "Bearer " + supabaseServiceKey); + deleteHeaders.set("apikey", supabaseServiceKey); + deleteHeaders.setContentType(MediaType.APPLICATION_JSON); + + String deleteBody = mapper.writeValueAsString(new java.util.LinkedHashMap() {{ + put("prefixes", prefixes); + }}); + HttpEntity deleteRequest = new HttpEntity<>(deleteBody, deleteHeaders); + + ResponseEntity deleteResponse = restTemplate.exchange( + deleteUrl, HttpMethod.DELETE, deleteRequest, String.class); + + if (deleteResponse.getStatusCode().is2xxSuccessful()) { + log.info("Storage files deleted for criminal {}: {} file(s)", criminalId, prefixes.size()); + } else { + log.warn("Storage deletion returned status {} for criminal {}", deleteResponse.getStatusCode(), criminalId); + } + + } catch (Exception e) { + log.warn("Storage cleanup failed for criminal {}: {}", criminalId, e.getMessage()); + } + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 22ee178..dc9abec 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -38,6 +38,11 @@ jwt.refresh-expiration=${REFRESH_TOKEN_EXPIRATION:604800000} python.call-analysis.url=${PYTHON_CALL_ANALYSIS_URL:http://localhost:5001} python.facial-recognition.url=${PYTHON_FACIAL_RECOGNITION_URL:http://localhost:5002} +# Supabase Storage Configuration +supabase.url=${SUPABASE_URL:https://zencayrfzfdrdqvpptjw.supabase.co} +supabase.service-key=${SUPABASE_SERVICE_KEY} +supabase.bucket=${SUPABASE_BUCKET:criminal-photos} + # File Upload Configuration spring.servlet.multipart.enabled=true spring.servlet.multipart.max-file-size=10MB From 562ed0a4a669de3f6a9676ce1d626519fd86a4ff Mon Sep 17 00:00:00 2001 From: JinethBosilu <157296244+JinethBosilu@users.noreply.github.com> Date: Fri, 13 Mar 2026 00:34:53 +0530 Subject: [PATCH 4/4] Sanitize log messages in CriminalService and SupabaseStorageService; add default bucket configuration in application.properties --- .../analyzer/service/CriminalService.java | 27 ++++++++++--------- .../service/SupabaseStorageService.java | 15 ++++++----- src/main/resources/application.properties | 1 + 3 files changed, 23 insertions(+), 20 deletions(-) diff --git a/src/main/java/com/crimeLink/analyzer/service/CriminalService.java b/src/main/java/com/crimeLink/analyzer/service/CriminalService.java index 50f0c80..d7d707d 100644 --- a/src/main/java/com/crimeLink/analyzer/service/CriminalService.java +++ b/src/main/java/com/crimeLink/analyzer/service/CriminalService.java @@ -2,6 +2,7 @@ import com.crimeLink.analyzer.entity.Criminal; import com.crimeLink.analyzer.repository.CriminalRepository; +import com.crimeLink.analyzer.util.LogSanitizer; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -51,7 +52,7 @@ public Map createCriminal( try { c.setDateOfBirth(LocalDate.parse(dateOfBirth)); } catch (Exception e) { - log.warn("Invalid date_of_birth format: {}", dateOfBirth); + log.warn("Invalid date_of_birth format: {}", LogSanitizer.sanitize(dateOfBirth)); } } if (gender != null) c.setGender(gender); @@ -64,13 +65,13 @@ public Map createCriminal( photoUrl = supabaseStorageService.uploadPhoto(criminalId, photo); c.setPrimaryPhotoUrl(photoUrl); } catch (Exception e) { - log.error("Photo upload failed for criminal {}: {}", criminalId, e.getMessage()); + log.error("Photo upload failed for criminal {}: {}", LogSanitizer.sanitize(criminalId), LogSanitizer.sanitize(e.getMessage())); // Continue without photo — record still gets created } } Criminal saved = criminalRepository.save(c); - log.info("Criminal created: {} (name: {})", criminalId, name); + log.info("Criminal created: {} (name: {})", LogSanitizer.sanitize(criminalId), LogSanitizer.sanitize(name)); // Generate face embedding via Python ML service (non-blocking) boolean hasEmbedding = false; @@ -78,9 +79,9 @@ public Map createCriminal( try { facialRecognitionService.generateEmbedding(criminalId, photo); hasEmbedding = true; - log.info("Face embedding generated for criminal {}", criminalId); + log.info("Face embedding generated for criminal {}", LogSanitizer.sanitize(criminalId)); } catch (Exception e) { - log.warn("Embedding generation failed for criminal {} (non-fatal): {}", criminalId, e.getMessage()); + log.warn("Embedding generation failed for criminal {} (non-fatal): {}", LogSanitizer.sanitize(criminalId), LogSanitizer.sanitize(e.getMessage())); } } @@ -147,7 +148,7 @@ public Optional> updateCriminal( try { c.setDateOfBirth(LocalDate.parse(dateOfBirth)); } catch (Exception e) { - log.warn("Invalid date_of_birth format: {}", dateOfBirth); + log.warn("Invalid date_of_birth format: {}", LogSanitizer.sanitize(dateOfBirth)); } } if (gender != null) c.setGender(gender); @@ -159,22 +160,22 @@ public Optional> updateCriminal( try { String newPhotoUrl = supabaseStorageService.uploadPhoto(criminalId, photo); c.setPrimaryPhotoUrl(newPhotoUrl); - log.info("Photo updated for criminal {}", criminalId); + log.info("Photo updated for criminal {}", LogSanitizer.sanitize(criminalId)); } catch (Exception e) { - log.error("Photo upload failed for criminal {}: {}", criminalId, e.getMessage()); + log.error("Photo upload failed for criminal {}: {}", LogSanitizer.sanitize(criminalId), LogSanitizer.sanitize(e.getMessage())); } // Regenerate face embedding with new photo try { facialRecognitionService.generateEmbedding(criminalId, photo); - log.info("Embedding regenerated for criminal {}", criminalId); + log.info("Embedding regenerated for criminal {}", LogSanitizer.sanitize(criminalId)); } catch (Exception e) { - log.warn("Embedding regeneration failed for criminal {} (non-fatal): {}", criminalId, e.getMessage()); + log.warn("Embedding regeneration failed for criminal {} (non-fatal): {}", LogSanitizer.sanitize(criminalId), LogSanitizer.sanitize(e.getMessage())); } } Criminal saved = criminalRepository.save(c); - log.info("Criminal updated: {}", criminalId); + log.info("Criminal updated: {}", LogSanitizer.sanitize(criminalId)); return Optional.of(toDetailMap(saved)); } @@ -194,12 +195,12 @@ public boolean deleteCriminal(String criminalId) { try { supabaseStorageService.deleteFolder(criminalId); } catch (Exception e) { - log.warn("Storage cleanup failed for criminal {} (non-fatal): {}", criminalId, e.getMessage()); + log.warn("Storage cleanup failed for criminal {} (non-fatal): {}", LogSanitizer.sanitize(criminalId), LogSanitizer.sanitize(e.getMessage())); } // DB delete — suspect_photos cascade via ON DELETE CASCADE criminalRepository.deleteById(criminalId); - log.info("Criminal deleted: {}", criminalId); + log.info("Criminal deleted: {}", LogSanitizer.sanitize(criminalId)); return true; } diff --git a/src/main/java/com/crimeLink/analyzer/service/SupabaseStorageService.java b/src/main/java/com/crimeLink/analyzer/service/SupabaseStorageService.java index b2845be..e94ec39 100644 --- a/src/main/java/com/crimeLink/analyzer/service/SupabaseStorageService.java +++ b/src/main/java/com/crimeLink/analyzer/service/SupabaseStorageService.java @@ -7,6 +7,7 @@ import org.springframework.web.client.RestTemplate; import org.springframework.web.multipart.MultipartFile; +import com.crimeLink.analyzer.util.LogSanitizer; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; @@ -69,14 +70,14 @@ public String uploadPhoto(String criminalId, MultipartFile file) { if (response.getStatusCode().is2xxSuccessful()) { String publicUrl = supabaseUrl + "/storage/v1/object/public/" + bucket + "/" + storagePath; - log.info("Photo uploaded successfully: {}", publicUrl); + log.info("Photo uploaded successfully: {}", LogSanitizer.sanitize(publicUrl)); return publicUrl; } else { log.error("Supabase upload failed with status: {}", response.getStatusCode()); throw new RuntimeException("Failed to upload photo to storage. Status: " + response.getStatusCode()); } } catch (Exception e) { - log.error("Failed to upload photo for criminal {}: {}", criminalId, e.getMessage()); + log.error("Failed to upload photo for criminal {}: {}", LogSanitizer.sanitize(criminalId), LogSanitizer.sanitize(e.getMessage())); throw new RuntimeException("Photo upload failed: " + e.getMessage(), e); } } @@ -104,7 +105,7 @@ public void deleteFolder(String criminalId) { listUrl, HttpMethod.POST, listRequest, String.class); if (!listResponse.getStatusCode().is2xxSuccessful() || listResponse.getBody() == null) { - log.warn("Failed to list storage files for criminal {}", criminalId); + log.warn("Failed to list storage files for criminal {}", LogSanitizer.sanitize(criminalId)); return; } @@ -112,7 +113,7 @@ public void deleteFolder(String criminalId) { JsonNode files = mapper.readTree(listResponse.getBody()); if (!files.isArray() || files.isEmpty()) { - log.info("No storage files found for criminal {}", criminalId); + log.info("No storage files found for criminal {}", LogSanitizer.sanitize(criminalId)); return; } @@ -144,13 +145,13 @@ public void deleteFolder(String criminalId) { deleteUrl, HttpMethod.DELETE, deleteRequest, String.class); if (deleteResponse.getStatusCode().is2xxSuccessful()) { - log.info("Storage files deleted for criminal {}: {} file(s)", criminalId, prefixes.size()); + log.info("Storage files deleted for criminal {}: {} file(s)", LogSanitizer.sanitize(criminalId), prefixes.size()); } else { - log.warn("Storage deletion returned status {} for criminal {}", deleteResponse.getStatusCode(), criminalId); + log.warn("Storage deletion returned status {} for criminal {}", deleteResponse.getStatusCode(), LogSanitizer.sanitize(criminalId)); } } catch (Exception e) { - log.warn("Storage cleanup failed for criminal {}: {}", criminalId, e.getMessage()); + log.warn("Storage cleanup failed for criminal {}: {}", LogSanitizer.sanitize(criminalId), LogSanitizer.sanitize(e.getMessage())); } } } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 80d1aa1..abf4b5e 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -41,6 +41,7 @@ python.facial-recognition.url=${PYTHON_FACIAL_RECOGNITION_URL:http://localhost:5 #Supabase Configuration supabase.url=${SUPABASE_URL} supabase.service-key=${SUPABASE_SERVICE_KEY} +supabase.bucket=${SUPABASE_BUCKET:criminal-photos} # File Upload Configuration spring.servlet.multipart.enabled=true