diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..fc679a5 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,10 @@ +# Backend Docker ignores +target +.git +.github +.env +.idea +.vscode +*.iml +database +backups diff --git a/.env.example b/.env.example index a8e355c..a5b37e8 100644 --- a/.env.example +++ b/.env.example @@ -1,8 +1,8 @@ # IMPORTANT: This is a template file. Copy to .env and fill in your values. # DO NOT commit .env file to version control! -# Database Configuration -DB_URL=jdbc:postgresql://your-host:port/database +# Database Configuration (Supabase PostgreSQL) +DB_URL=jdbc:postgresql://your-host:5432/postgres DB_USERNAME=your-username DB_PASSWORD=your-password @@ -10,10 +10,18 @@ DB_PASSWORD=your-password SERVER_PORT=8080 # JWT Configuration -JWT_SECRET=secret_key -JWT_EXPIRATION= -REFRESH_TOKEN_EXPIRATION= +JWT_SECRET=your-jwt-secret-key +JWT_EXPIRATION=900000 +REFRESH_TOKEN_EXPIRATION=604800000 -# Supabase Configurations -SUPABASE_URL= -SUPABASE_SERVICE_KEY= +# Python Microservices URLs (internal networking in production) +PYTHON_CALL_ANALYSIS_URL=http://localhost:5001 +PYTHON_FACIAL_RECOGNITION_URL=http://localhost:5002 + +# Supabase Configuration +SUPABASE_URL=https://your-project.supabase.co +SUPABASE_SERVICE_KEY=your-supabase-service-key +SUPABASE_BUCKET=criminal-photos + +# CORS Configuration (comma-separated origins, use * for all) +CORS_ALLOWED_ORIGINS=* diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..9e3cf6e --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,59 @@ +# ============================================= +# CrimeLink Analyzer - Backend CI/CD +# Repo: github.com/arosha-w/CrimeLinkAnalyzer_backend +# Docker build → ECR → ECS Fargate +# ============================================= +# +# Required GitHub Secrets: +# AWS_ACCESS_KEY_ID +# AWS_SECRET_ACCESS_KEY +# AWS_REGION (e.g., ap-south-1) +# AWS_ACCOUNT_ID (12-digit AWS account ID) +# ECS_CLUSTER_NAME (e.g., crimelink-cluster) +# ECS_SERVICE_NAME (e.g., crimelink-backend-service) + +name: Deploy Backend + +on: + push: + branches: [main] + workflow_dispatch: + +env: + ECR_REPOSITORY: crimelink-backend + AWS_REGION: ${{ secrets.AWS_REGION }} + +jobs: + build-and-deploy: + name: Build & Deploy to ECS + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ${{ env.AWS_REGION }} + + - name: Login to Amazon ECR + id: ecr-login + uses: aws-actions/amazon-ecr-login@v2 + + - name: Build, tag, and push Docker image + env: + ECR_REGISTRY: ${{ steps.ecr-login.outputs.registry }} + IMAGE_TAG: ${{ github.sha }} + run: | + docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG . + docker tag $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG $ECR_REGISTRY/$ECR_REPOSITORY:latest + docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG + docker push $ECR_REGISTRY/$ECR_REPOSITORY:latest + + - name: Force new ECS deployment + run: | + aws ecs update-service \ + --cluster ${{ secrets.ECS_CLUSTER_NAME }} \ + --service ${{ secrets.ECS_SERVICE_NAME }} \ + --force-new-deployment diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..950fcfc --- /dev/null +++ b/Dockerfile @@ -0,0 +1,55 @@ +# ============================================= +# CrimeLink Analyzer - Spring Boot Backend +# Multi-stage build: Maven → JRE 21 +# ============================================= + +# Stage 1: Build with Maven +FROM eclipse-temurin:21-jdk-alpine AS builder + +WORKDIR /app + +# Copy Maven wrapper and POM first (better caching) +COPY mvnw ./ +COPY .mvn .mvn +COPY pom.xml ./ + +# Make mvnw executable and fix Windows CRLF line endings +RUN chmod +x mvnw && sed -i 's/\r$//' mvnw + +# Download dependencies (cached unless pom.xml changes) +RUN ./mvnw dependency:go-offline -B + +# Copy source code +COPY src src + +# Build the application (skip tests for faster builds) +RUN ./mvnw package -DskipTests -B + +# Stage 2: Runtime with minimal JRE +FROM eclipse-temurin:21-jre-alpine + +WORKDIR /app + +# Create non-root user +RUN addgroup -S appgroup && adduser -S appuser -G appgroup + +# Copy the built JAR from builder +COPY --from=builder /app/target/*.jar app.jar + +# Create directories for backups and uploads +RUN mkdir -p /app/backups && chown -R appuser:appgroup /app + +# Switch to non-root user +USER appuser + +# Expose port +EXPOSE 8080 + +# Health check +HEALTHCHECK --interval=30s --timeout=5s --start-period=60s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:8080/api/health || exit 1 + +# JVM tuning for containers +ENV JAVA_OPTS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0 -XX:+UseG1GC" + +ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"] diff --git a/aws/ecs-task-def.json b/aws/ecs-task-def.json new file mode 100644 index 0000000..5d7b9e2 --- /dev/null +++ b/aws/ecs-task-def.json @@ -0,0 +1,52 @@ +{ + "family": "crimelink-backend", + "networkMode": "awsvpc", + "requiresCompatibilities": ["FARGATE"], + "cpu": "512", + "memory": "1024", + "executionRoleArn": "arn:aws:iam::YOUR_ACCOUNT_ID:role/ecsTaskExecutionRole", + "taskRoleArn": "arn:aws:iam::YOUR_ACCOUNT_ID:role/ecsTaskRole", + "containerDefinitions": [ + { + "name": "backend", + "image": "YOUR_ACCOUNT_ID.dkr.ecr.YOUR_REGION.amazonaws.com/crimelink-backend:latest", + "essential": true, + "portMappings": [ + { + "containerPort": 8080, + "protocol": "tcp" + } + ], + "environment": [ + { "name": "SERVER_PORT", "value": "8080" }, + { "name": "SUPABASE_BUCKET", "value": "criminal-photos" }, + { "name": "CORS_ALLOWED_ORIGINS", "value": "https://your-cloudfront-domain.cloudfront.net" } + ], + "secrets": [ + { "name": "DB_URL", "valueFrom": "arn:aws:ssm:YOUR_REGION:YOUR_ACCOUNT_ID:parameter/crimelink/db-url" }, + { "name": "DB_USERNAME", "valueFrom": "arn:aws:ssm:YOUR_REGION:YOUR_ACCOUNT_ID:parameter/crimelink/db-username" }, + { "name": "DB_PASSWORD", "valueFrom": "arn:aws:ssm:YOUR_REGION:YOUR_ACCOUNT_ID:parameter/crimelink/db-password" }, + { "name": "JWT_SECRET", "valueFrom": "arn:aws:ssm:YOUR_REGION:YOUR_ACCOUNT_ID:parameter/crimelink/jwt-secret" }, + { "name": "SUPABASE_URL", "valueFrom": "arn:aws:ssm:YOUR_REGION:YOUR_ACCOUNT_ID:parameter/crimelink/supabase-url" }, + { "name": "SUPABASE_SERVICE_KEY", "valueFrom": "arn:aws:ssm:YOUR_REGION:YOUR_ACCOUNT_ID:parameter/crimelink/supabase-service-key" }, + { "name": "PYTHON_CALL_ANALYSIS_URL", "valueFrom": "arn:aws:ssm:YOUR_REGION:YOUR_ACCOUNT_ID:parameter/crimelink/call-analysis-url" }, + { "name": "PYTHON_FACIAL_RECOGNITION_URL", "valueFrom": "arn:aws:ssm:YOUR_REGION:YOUR_ACCOUNT_ID:parameter/crimelink/facial-recognition-url" } + ], + "healthCheck": { + "command": ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:8080/api/health || exit 1"], + "interval": 30, + "timeout": 5, + "retries": 3, + "startPeriod": 60 + }, + "logConfiguration": { + "logDriver": "awslogs", + "options": { + "awslogs-group": "/ecs/crimelink-backend", + "awslogs-region": "YOUR_REGION", + "awslogs-stream-prefix": "ecs" + } + } + } + ] +} diff --git a/backend_logs.txt b/backend_logs.txt new file mode 100644 index 0000000..589e16b Binary files /dev/null and b/backend_logs.txt differ diff --git a/src/main/java/com/crimeLink/analyzer/config/SecurityConfig.java b/src/main/java/com/crimeLink/analyzer/config/SecurityConfig.java index 7b84314..952d184 100644 --- a/src/main/java/com/crimeLink/analyzer/config/SecurityConfig.java +++ b/src/main/java/com/crimeLink/analyzer/config/SecurityConfig.java @@ -37,6 +37,9 @@ public class SecurityConfig { @Autowired private UserDetailsService userDetailsService; + @org.springframework.beans.factory.annotation.Value("${cors.allowed-origins:*}") + private String corsAllowedOrigins; + @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http @@ -82,7 +85,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti // OIC-only routes .requestMatchers("/api/duty-schedules/**").hasRole("OIC") - .requestMatchers("/api/weapon/**").hasRole("OIC") + .requestMatchers("/api/weapon/**").permitAll() .requestMatchers("/api/weapon-issue/**").hasRole("OIC") // Admin/OIC/Investigator routes (officer data, locations, users) @@ -134,15 +137,15 @@ public PasswordEncoder passwordEncoder() { @Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); - // Use allowedOriginPatterns for wildcard support with credentials - // For production, replace with specific origins - configuration.setAllowedOriginPatterns(List.of("*")); - // Or use specific origins (recommended for production): - // configuration.setAllowedOrigins(Arrays.asList( - // "http://localhost:5173", - // "http://localhost:3000", - // "https://yourdomain.com" - // )); + // Use env var CORS_ALLOWED_ORIGINS to configure origins + // Default: * (all origins) — restrict for production + if ("*".equals(corsAllowedOrigins)) { + configuration.setAllowedOriginPatterns(List.of("*")); + } else { + configuration.setAllowedOrigins( + Arrays.asList(corsAllowedOrigins.split(",")) + ); + } configuration.setAllowedMethods(Arrays.asList( "GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH")); configuration.setAllowedHeaders(List.of("*")); diff --git a/src/main/java/com/crimeLink/analyzer/controller/ReqquiredArgsConstructor.java b/src/main/java/com/crimeLink/analyzer/controller/ReqquiredArgsConstructor.java new file mode 100644 index 0000000..3b8db28 --- /dev/null +++ b/src/main/java/com/crimeLink/analyzer/controller/ReqquiredArgsConstructor.java @@ -0,0 +1,5 @@ +package com.crimeLink.analyzer.controller; + +public @interface ReqquiredArgsConstructor { + +} diff --git a/src/main/java/com/crimeLink/analyzer/controller/UserController.java b/src/main/java/com/crimeLink/analyzer/controller/UserController.java index ab64e04..e618a1a 100644 --- a/src/main/java/com/crimeLink/analyzer/controller/UserController.java +++ b/src/main/java/com/crimeLink/analyzer/controller/UserController.java @@ -16,6 +16,7 @@ public UserController(UserService service) { this.service = service; } + @GetMapping("/field-officers") public List getFieldOfficers() { return service.getFieldOfficers(); } diff --git a/src/main/java/com/crimeLink/analyzer/controller/VehicleController.java b/src/main/java/com/crimeLink/analyzer/controller/VehicleController.java index a5f1139..6d0ad1e 100644 --- a/src/main/java/com/crimeLink/analyzer/controller/VehicleController.java +++ b/src/main/java/com/crimeLink/analyzer/controller/VehicleController.java @@ -17,7 +17,6 @@ @RestController @RequestMapping("/api/vehicles") -@CrossOrigin(origins = {"http://localhost:5173", "http://localhost:3000"}) public class VehicleController { @Autowired diff --git a/src/main/java/com/crimeLink/analyzer/controller/WeaponController.java b/src/main/java/com/crimeLink/analyzer/controller/WeaponController.java index 76f1a44..2618f57 100644 --- a/src/main/java/com/crimeLink/analyzer/controller/WeaponController.java +++ b/src/main/java/com/crimeLink/analyzer/controller/WeaponController.java @@ -69,7 +69,7 @@ public ResponseEntity getAllWeaponsWithDetails() { List weapons = weaponService.getAllWeaponsWithDetails(); return ResponseEntity.ok(weapons); } catch (Exception e) { - e.printStackTrace(); // Log the full stack trace + e.printStackTrace(); // Log the full stack trace return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) .body(createErrorResponse("Failed to fetch weapons with details: " + e.getMessage())); } @@ -89,6 +89,11 @@ public ResponseEntity getWeaponBySerial(@PathVariable String serialNumber) { } } + @GetMapping("/officer/{officerId}") + public List getWeaponsIssuedToOfficer(@PathVariable Integer officerId) { + return weaponService.getWeaponsIssuedToOfficer(officerId); + } + private Map createErrorResponse(String message) { Map response = new HashMap<>(); response.put("error", message); diff --git a/src/main/java/com/crimeLink/analyzer/controller/WeaponIssueController.java b/src/main/java/com/crimeLink/analyzer/controller/WeaponIssueController.java index 73f9f08..902c87e 100644 --- a/src/main/java/com/crimeLink/analyzer/controller/WeaponIssueController.java +++ b/src/main/java/com/crimeLink/analyzer/controller/WeaponIssueController.java @@ -4,6 +4,8 @@ import com.crimeLink.analyzer.dto.OfficerDTO; import com.crimeLink.analyzer.dto.ReturnWeaponRequestDTO; import com.crimeLink.analyzer.dto.WeaponAddDTO; +import com.crimeLink.analyzer.entity.WeaponIssue; +import com.crimeLink.analyzer.repository.WeaponIssueRepository; import com.crimeLink.analyzer.service.WeaponIssueService; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; @@ -21,6 +23,7 @@ public class WeaponIssueController { private final WeaponIssueService weaponIssueService; + private final WeaponIssueRepository weaponIssueRepository; @PostMapping("/issue") public ResponseEntity issueWeapon(@RequestBody IssueWeaponRequestDTO dto) { @@ -83,6 +86,16 @@ public ResponseEntity getAllOfficers() { } } + @GetMapping("/issued/{officerId}") + public List getActiveWeapons(@PathVariable Integer officerId) { + return weaponIssueRepository.findByIssuedTo_UserIdAndReturnedAtIsNullOrderByIssuedAtDesc(officerId); + } + + @GetMapping("/history/{officerId}") + public List getWeaponIssueHistory(@PathVariable Integer officerId) { + return weaponIssueRepository.findByIssuedTo_UserIdOrderByIssuedAtDesc(officerId); + } + private Map createErrorResponse(String message) { Map response = new HashMap<>(); response.put("error", message); diff --git a/src/main/java/com/crimeLink/analyzer/controller/WeaponRequestController.java b/src/main/java/com/crimeLink/analyzer/controller/WeaponRequestController.java new file mode 100644 index 0000000..d43dac9 --- /dev/null +++ b/src/main/java/com/crimeLink/analyzer/controller/WeaponRequestController.java @@ -0,0 +1,57 @@ +package com.crimeLink.analyzer.controller; + +import java.util.List; + +import org.springframework.http.ResponseEntity; +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.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.crimeLink.analyzer.dto.WeaponRequestDto; +import com.crimeLink.analyzer.service.WeaponRequestService; + +import lombok.RequiredArgsConstructor; + +@RestController +@RequestMapping("/api/weapon/requests") +@RequiredArgsConstructor +public class WeaponRequestController { + + private final WeaponRequestService weaponRequestService; + + @PostMapping + public ResponseEntity createRequest(@RequestBody WeaponRequestDto dto) { + WeaponRequestDto createRequest = weaponRequestService.createRequest(dto); + return ResponseEntity.ok(createRequest); + } + + @GetMapping + public ResponseEntity> getAllRequests() { + List requests = weaponRequestService.getAllRequests(); + return ResponseEntity.ok(requests); + } + + @GetMapping("user/{userId}") + public ResponseEntity> getRequestsByUser(@PathVariable Integer userId) { + List requests = weaponRequestService.getRequestsByUser(userId); + return ResponseEntity.ok(requests); + } + + @PutMapping("/{requestId}/approve") + public ResponseEntity approveRequest(@PathVariable Integer requestId) { + WeaponRequestDto approvedRequest = weaponRequestService.approvedRequest(requestId); + + return ResponseEntity.ok(approvedRequest); + } + + @PutMapping("/{requestId}/reject") + public ResponseEntity rejectRequest(@PathVariable Integer requestId) { + WeaponRequestDto rejectedRequest = weaponRequestService.rejectedRequest(requestId); + + return ResponseEntity.ok(rejectedRequest); + } +} diff --git a/src/main/java/com/crimeLink/analyzer/dto/WeaponRequestDto.java b/src/main/java/com/crimeLink/analyzer/dto/WeaponRequestDto.java new file mode 100644 index 0000000..8073c14 --- /dev/null +++ b/src/main/java/com/crimeLink/analyzer/dto/WeaponRequestDto.java @@ -0,0 +1,17 @@ +package com.crimeLink.analyzer.dto; + +import java.time.LocalDateTime; + +import lombok.Data; + +@Data +public class WeaponRequestDto { + private Integer requestId; + private String weaponSerial; + private Integer ammoCount; + private Integer requestedById; + private String requestNote; + private String status; + private LocalDateTime requestedAt; + private LocalDateTime resolvedAt; +} diff --git a/src/main/java/com/crimeLink/analyzer/entity/WeaponRequest.java b/src/main/java/com/crimeLink/analyzer/entity/WeaponRequest.java new file mode 100644 index 0000000..8021a07 --- /dev/null +++ b/src/main/java/com/crimeLink/analyzer/entity/WeaponRequest.java @@ -0,0 +1,47 @@ +package com.crimeLink.analyzer.entity; + +import java.time.LocalDateTime; + +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Entity +@AllArgsConstructor +@NoArgsConstructor +@Data +@Table(name = "weapon_requests") +public class WeaponRequest { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer requestId; + + /* Weapon */ + @ManyToOne + @JoinColumn(name = "weapon_serial", referencedColumnName = "serial_number") + private Weapon weapon; + + /* Issued to */ + @ManyToOne + @JoinColumn(name = "requested_by", referencedColumnName = "user_id") + private User requestedBy; + + private Integer ammoCount; + + private LocalDateTime requestedAt; + private LocalDateTime resolvedAt; + private String requestNote; + + @Enumerated(EnumType.STRING) + private WeaponRequestStatus status; +} diff --git a/src/main/java/com/crimeLink/analyzer/entity/WeaponRequestStatus.java b/src/main/java/com/crimeLink/analyzer/entity/WeaponRequestStatus.java new file mode 100644 index 0000000..fab70c2 --- /dev/null +++ b/src/main/java/com/crimeLink/analyzer/entity/WeaponRequestStatus.java @@ -0,0 +1,7 @@ +package com.crimeLink.analyzer.entity; + +public enum WeaponRequestStatus { + PENDING, + APPROVED, + REJECTED +} diff --git a/src/main/java/com/crimeLink/analyzer/mapper/WeaponRequestMapper.java b/src/main/java/com/crimeLink/analyzer/mapper/WeaponRequestMapper.java new file mode 100644 index 0000000..2981152 --- /dev/null +++ b/src/main/java/com/crimeLink/analyzer/mapper/WeaponRequestMapper.java @@ -0,0 +1,34 @@ +package com.crimeLink.analyzer.mapper; + +import com.crimeLink.analyzer.dto.WeaponRequestDto; +import com.crimeLink.analyzer.entity.User; +import com.crimeLink.analyzer.entity.Weapon; +import com.crimeLink.analyzer.entity.WeaponRequest; + +public class WeaponRequestMapper { + public static WeaponRequestDto mapToWeaponRequestDto(WeaponRequest entity) { + + WeaponRequestDto dto = new WeaponRequestDto(); + + dto.setRequestId(entity.getRequestId()); + dto.setWeaponSerial(entity.getWeapon().getSerialNumber()); + dto.setAmmoCount(entity.getAmmoCount()); + dto.setRequestedById(entity.getRequestedBy().getUserId()); + dto.setRequestNote(entity.getRequestNote()); + dto.setStatus(entity.getStatus().name()); + dto.setRequestedAt(entity.getRequestedAt()); + dto.setResolvedAt(entity.getResolvedAt()); + return dto; + } + + public static WeaponRequest mapToWeaponRequest(WeaponRequestDto dto, Weapon weapon, User user) { + + WeaponRequest entity = new WeaponRequest(); + + entity.setWeapon(weapon); + entity.setRequestedBy(user); + entity.setRequestNote(dto.getRequestNote()); + entity.setAmmoCount(dto.getAmmoCount()); + return entity; + } +} diff --git a/src/main/java/com/crimeLink/analyzer/repository/WeaponIssueRepository.java b/src/main/java/com/crimeLink/analyzer/repository/WeaponIssueRepository.java index 4050461..6db7a56 100644 --- a/src/main/java/com/crimeLink/analyzer/repository/WeaponIssueRepository.java +++ b/src/main/java/com/crimeLink/analyzer/repository/WeaponIssueRepository.java @@ -10,5 +10,11 @@ @Repository public interface WeaponIssueRepository extends JpaRepository { List findByWeapon_SerialNumberAndReturnedAtIsNullOrderByIssuedAtDesc(String serialNumber); + Optional findByWeapon_SerialNumberAndReturnedAtIsNull(String serialNumber); + + List findByIssuedTo_UserIdOrderByIssuedAtDesc(Integer officerId); + + List findByIssuedTo_UserIdAndReturnedAtIsNullOrderByIssuedAtDesc(Integer officerId); + } \ No newline at end of file diff --git a/src/main/java/com/crimeLink/analyzer/repository/WeaponRepository.java b/src/main/java/com/crimeLink/analyzer/repository/WeaponRepository.java index a7e2305..d868332 100644 --- a/src/main/java/com/crimeLink/analyzer/repository/WeaponRepository.java +++ b/src/main/java/com/crimeLink/analyzer/repository/WeaponRepository.java @@ -3,6 +3,8 @@ import com.crimeLink.analyzer.entity.Weapon; import com.crimeLink.analyzer.enums.WeaponStatus; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import java.util.List; @@ -11,6 +13,16 @@ @Repository public interface WeaponRepository extends JpaRepository { boolean existsById(String serialNumber); + List findByStatus(WeaponStatus status); + Optional findBySerialNumber(String serialNumber); + + @Query(""" + SELECT wi.weapon + FROM WeaponIssue wi + WHERE wi.issuedTo.userId = :userId + AND wi.returnedAt IS NULL + """) + List findActiveWeaponsByOfficer(@Param("officerId") Integer officerId); } \ No newline at end of file diff --git a/src/main/java/com/crimeLink/analyzer/repository/WeaponRequestRepository.java b/src/main/java/com/crimeLink/analyzer/repository/WeaponRequestRepository.java new file mode 100644 index 0000000..206e086 --- /dev/null +++ b/src/main/java/com/crimeLink/analyzer/repository/WeaponRequestRepository.java @@ -0,0 +1,16 @@ +package com.crimeLink.analyzer.repository; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import com.crimeLink.analyzer.entity.WeaponRequest; + +@Repository +public interface WeaponRequestRepository extends JpaRepository { + + List findByRequestedBy_UserId(Integer userId); + + List findByStatus(String status); +} diff --git a/src/main/java/com/crimeLink/analyzer/service/CallAnalysisService.java b/src/main/java/com/crimeLink/analyzer/service/CallAnalysisService.java index e0897e7..3ad6656 100644 --- a/src/main/java/com/crimeLink/analyzer/service/CallAnalysisService.java +++ b/src/main/java/com/crimeLink/analyzer/service/CallAnalysisService.java @@ -31,7 +31,7 @@ public class CallAnalysisService { private final RestTemplate restTemplate; private final ObjectMapper objectMapper; - @Value("${python.call-analysis.url:http://localhost:5001}") + @Value("${python.call-analysis.url}") private String callAnalysisServiceUrl; public CallAnalysisService(RestTemplate restTemplate) { diff --git a/src/main/java/com/crimeLink/analyzer/service/FacialRecognitionService.java b/src/main/java/com/crimeLink/analyzer/service/FacialRecognitionService.java index 59883fd..f4408ba 100644 --- a/src/main/java/com/crimeLink/analyzer/service/FacialRecognitionService.java +++ b/src/main/java/com/crimeLink/analyzer/service/FacialRecognitionService.java @@ -31,7 +31,7 @@ public class FacialRecognitionService { private final RestTemplate restTemplate; private final ObjectMapper objectMapper; - @Value("${python.facial-recognition.url:http://localhost:5002}") + @Value("${python.facial-recognition.url}") private String facialRecognitionServiceUrl; public FacialRecognitionService(RestTemplate restTemplate) { diff --git a/src/main/java/com/crimeLink/analyzer/service/WeaponRequestService.java b/src/main/java/com/crimeLink/analyzer/service/WeaponRequestService.java new file mode 100644 index 0000000..89b950f --- /dev/null +++ b/src/main/java/com/crimeLink/analyzer/service/WeaponRequestService.java @@ -0,0 +1,17 @@ +package com.crimeLink.analyzer.service; + +import java.util.List; + +import com.crimeLink.analyzer.dto.WeaponRequestDto; + +public interface WeaponRequestService { + WeaponRequestDto createRequest(WeaponRequestDto requestDto); + + List getAllRequests(); + + List getRequestsByUser(Integer userId); + + WeaponRequestDto approvedRequest(Integer requestId); + + WeaponRequestDto rejectedRequest(Integer requestId); +} diff --git a/src/main/java/com/crimeLink/analyzer/service/WeaponService.java b/src/main/java/com/crimeLink/analyzer/service/WeaponService.java index 972c69b..5c3b995 100644 --- a/src/main/java/com/crimeLink/analyzer/service/WeaponService.java +++ b/src/main/java/com/crimeLink/analyzer/service/WeaponService.java @@ -13,4 +13,5 @@ public interface WeaponService { List getAllWeapons(); Weapon getWeaponBySerial(String serialNumber); List getAllWeaponsWithDetails(); + List getWeaponsIssuedToOfficer(Integer officerId); } \ No newline at end of file diff --git a/src/main/java/com/crimeLink/analyzer/service/impl/WeaponRequestServiceImpl.java b/src/main/java/com/crimeLink/analyzer/service/impl/WeaponRequestServiceImpl.java new file mode 100644 index 0000000..09c5cc4 --- /dev/null +++ b/src/main/java/com/crimeLink/analyzer/service/impl/WeaponRequestServiceImpl.java @@ -0,0 +1,98 @@ +package com.crimeLink.analyzer.service.impl; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; + +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.web.server.ResponseStatusException; + +import com.crimeLink.analyzer.dto.WeaponRequestDto; +import com.crimeLink.analyzer.entity.User; +import com.crimeLink.analyzer.entity.Weapon; +import com.crimeLink.analyzer.entity.WeaponRequest; +import com.crimeLink.analyzer.entity.WeaponRequestStatus; +import com.crimeLink.analyzer.mapper.WeaponRequestMapper; +import com.crimeLink.analyzer.repository.UserRepository; +import com.crimeLink.analyzer.repository.WeaponRepository; +import com.crimeLink.analyzer.repository.WeaponRequestRepository; +import com.crimeLink.analyzer.service.WeaponRequestService; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class WeaponRequestServiceImpl implements WeaponRequestService { + + private final WeaponRequestRepository weaponRequestRepository; + private final WeaponRepository weaponRepository; + private final UserRepository userRepository; + + @Override + public WeaponRequestDto createRequest(WeaponRequestDto requestDto) { + if (requestDto == null) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Request body is required"); + } + + if (requestDto.getWeaponSerial() == null || requestDto.getWeaponSerial().isBlank()) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "weaponSerial is required"); + } + + if (requestDto.getRequestedById() == null) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "requestedById is required"); + } + + if (requestDto.getAmmoCount() != null && requestDto.getAmmoCount() < 0) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "ammoCount cannot be negative"); + } + + Weapon weapon = weaponRepository.findBySerialNumber(requestDto.getWeaponSerial()) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Weapon not found")); + + User user = userRepository.findById(requestDto.getRequestedById()) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "User not found")); + + WeaponRequest request = WeaponRequestMapper.mapToWeaponRequest(requestDto, weapon, user); + + request.setStatus(WeaponRequestStatus.PENDING); + request.setRequestedAt(LocalDateTime.now()); + + WeaponRequest saved = weaponRequestRepository.save(request); + return WeaponRequestMapper.mapToWeaponRequestDto(saved); + } + + @Override + public List getAllRequests() { + return weaponRequestRepository.findAll().stream().map(WeaponRequestMapper::mapToWeaponRequestDto) + .collect(Collectors.toList()); + } + + @Override + public WeaponRequestDto approvedRequest(Integer requestId) { + WeaponRequest request = weaponRequestRepository.findById(requestId) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Request not found")); + + request.setStatus(WeaponRequestStatus.APPROVED); + request.setResolvedAt(LocalDateTime.now()); + + return WeaponRequestMapper.mapToWeaponRequestDto(weaponRequestRepository.save(request)); + } + + @Override + public WeaponRequestDto rejectedRequest(Integer requestId) { + WeaponRequest request = weaponRequestRepository.findById(requestId) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Request not found")); + + request.setStatus(WeaponRequestStatus.REJECTED); + request.setResolvedAt(LocalDateTime.now()); + + return WeaponRequestMapper.mapToWeaponRequestDto(weaponRequestRepository.save(request)); + } + + @Override + public List getRequestsByUser(Integer userId) { + return weaponRequestRepository.findByRequestedBy_UserId(userId).stream() + .map(WeaponRequestMapper::mapToWeaponRequestDto).collect(Collectors.toList()); + } +} diff --git a/src/main/java/com/crimeLink/analyzer/service/impl/WeaponServiceImpl.java b/src/main/java/com/crimeLink/analyzer/service/impl/WeaponServiceImpl.java index 3cdb9a7..7f4a0db 100644 --- a/src/main/java/com/crimeLink/analyzer/service/impl/WeaponServiceImpl.java +++ b/src/main/java/com/crimeLink/analyzer/service/impl/WeaponServiceImpl.java @@ -79,7 +79,8 @@ public List getAllWeaponsWithDetails() { // If weapon is issued, get issue details if (weapon.getStatus() == WeaponStatus.ISSUED) { List issues = weaponIssueRepository - .findByWeapon_SerialNumberAndReturnedAtIsNullOrderByIssuedAtDesc(weapon.getSerialNumber()); + .findByWeapon_SerialNumberAndReturnedAtIsNullOrderByIssuedAtDesc( + weapon.getSerialNumber()); if (!issues.isEmpty()) { WeaponIssue issue = issues.get(0); // Get the most recent issue @@ -129,4 +130,9 @@ public List getAllWeaponsWithDetails() { }) .collect(Collectors.toList()); } + + @Override + public List getWeaponsIssuedToOfficer(Integer officerId) { + return weaponRepository.findActiveWeaponsByOfficer(officerId); + } } \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 5ed118c..0a0605d 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -34,9 +34,9 @@ jwt.secret=${JWT_SECRET} jwt.expiration=${JWT_EXPIRATION:900000} jwt.refresh-expiration=${REFRESH_TOKEN_EXPIRATION:604800000} -# Python Microservices Configuration -python.call-analysis.url=${PYTHON_CALL_ANALYSIS_URL:http://localhost:5001} -python.facial-recognition.url=${PYTHON_FACIAL_RECOGNITION_URL:http://localhost:5002} +# Python Microservices Configuration (REQUIRED — no defaults) +python.call-analysis.url=${PYTHON_CALL_ANALYSIS_URL} +python.facial-recognition.url=${PYTHON_FACIAL_RECOGNITION_URL} #Supabase Configuration supabase.url=${SUPABASE_URL} @@ -50,3 +50,7 @@ spring.servlet.multipart.max-request-size=50MB # Backup Configuration backup.directory=${BACKUP_DIR:./backups} + +# CORS Configuration +cors.allowed-origins=${CORS_ALLOWED_ORIGINS:*} +spring.datasource.hikari.data-source-properties.prepareThreshold=0