Skip to content
Merged
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
10 changes: 10 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Backend Docker ignores
target
.git
.github
.env
.idea
.vscode
*.iml
database
backups
24 changes: 16 additions & 8 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,19 +1,27 @@
# 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

# Server Configuration
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=*
59 changes: 59 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -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
55 changes: 55 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -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"]
52 changes: 52 additions & 0 deletions aws/ecs-task-def.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
]
}
Binary file added backend_logs.txt
Binary file not shown.
23 changes: 13 additions & 10 deletions src/main/java/com/crimeLink/analyzer/config/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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("*"));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.crimeLink.analyzer.controller;

public @interface ReqquiredArgsConstructor {

}
Comment on lines +3 to +5
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public UserController(UserService service) {
this.service = service;
}

@GetMapping("/field-officers")
public List<User> getFieldOfficers() {
return service.getFieldOfficers();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@

@RestController
@RequestMapping("/api/vehicles")
@CrossOrigin(origins = {"http://localhost:5173", "http://localhost:3000"})
public class VehicleController {

@Autowired
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ public ResponseEntity<?> getAllWeaponsWithDetails() {
List<WeaponResponseDTO> 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()));
}
Expand All @@ -89,6 +89,11 @@ public ResponseEntity<?> getWeaponBySerial(@PathVariable String serialNumber) {
}
}

@GetMapping("/officer/{officerId}")
public List<Weapon> getWeaponsIssuedToOfficer(@PathVariable Integer officerId) {
return weaponService.getWeaponsIssuedToOfficer(officerId);
}
Comment on lines +92 to +95

private Map<String, String> createErrorResponse(String message) {
Map<String, String> response = new HashMap<>();
response.put("error", message);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -21,6 +23,7 @@
public class WeaponIssueController {

private final WeaponIssueService weaponIssueService;
private final WeaponIssueRepository weaponIssueRepository;

@PostMapping("/issue")
public ResponseEntity<?> issueWeapon(@RequestBody IssueWeaponRequestDTO dto) {
Expand Down Expand Up @@ -83,6 +86,16 @@ public ResponseEntity<?> getAllOfficers() {
}
}

@GetMapping("/issued/{officerId}")
public List<WeaponIssue> getActiveWeapons(@PathVariable Integer officerId) {
return weaponIssueRepository.findByIssuedTo_UserIdAndReturnedAtIsNullOrderByIssuedAtDesc(officerId);
}

@GetMapping("/history/{officerId}")
public List<WeaponIssue> getWeaponIssueHistory(@PathVariable Integer officerId) {
return weaponIssueRepository.findByIssuedTo_UserIdOrderByIssuedAtDesc(officerId);
}
Comment on lines +89 to +97
Comment on lines +89 to +97

private Map<String, String> createErrorResponse(String message) {
Map<String, String> response = new HashMap<>();
response.put("error", message);
Expand Down
Original file line number Diff line number Diff line change
@@ -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<WeaponRequestDto> createRequest(@RequestBody WeaponRequestDto dto) {
WeaponRequestDto createRequest = weaponRequestService.createRequest(dto);
return ResponseEntity.ok(createRequest);
}

@GetMapping
public ResponseEntity<List<WeaponRequestDto>> getAllRequests() {
List<WeaponRequestDto> requests = weaponRequestService.getAllRequests();
return ResponseEntity.ok(requests);
}

@GetMapping("user/{userId}")
public ResponseEntity<List<WeaponRequestDto>> getRequestsByUser(@PathVariable Integer userId) {
List<WeaponRequestDto> requests = weaponRequestService.getRequestsByUser(userId);
return ResponseEntity.ok(requests);
}

@PutMapping("/{requestId}/approve")
public ResponseEntity<WeaponRequestDto> approveRequest(@PathVariable Integer requestId) {
WeaponRequestDto approvedRequest = weaponRequestService.approvedRequest(requestId);

return ResponseEntity.ok(approvedRequest);
}

@PutMapping("/{requestId}/reject")
public ResponseEntity<WeaponRequestDto> rejectRequest(@PathVariable Integer requestId) {
WeaponRequestDto rejectedRequest = weaponRequestService.rejectedRequest(requestId);

return ResponseEntity.ok(rejectedRequest);
}
}
Loading
Loading