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 ef2d033..2f03c43 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 @@ -127,15 +130,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/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/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/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