diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..47ce93c --- /dev/null +++ b/.env.example @@ -0,0 +1,25 @@ +# ============================================================================= +# Environment Variables for docker-compose-all.yml +# ============================================================================= +# Copy this file to .env and customize values for your environment. +# +# Usage: +# cp .env.example .env +# # Edit .env with your values +# docker compose -f docker-compose-all.yml up -d +# +# SECURITY WARNING: +# - Never commit .env to version control +# - Use secrets management (Vault, AWS Secrets Manager) in production +# ============================================================================= + +# Database password (used by both app and postgres containers) +DB_PASSWORD=your_secure_password_here + +# JWT secrets - MUST be at least 64 bytes for HS512 +# Generate with: openssl rand -base64 64 +JWT_ACCESS_SECRET=your-64-byte-access-secret-generated-with-openssl-rand-base64-64-command +JWT_REFRESH_SECRET=your-64-byte-refresh-secret-generated-with-openssl-rand-base64-64-command + +# CORS allowed origins (comma-separated for multiple) +CORS_ALLOWED_ORIGINS=http://localhost:3000 diff --git a/.gitignore b/.gitignore index 2339246..29e54ab 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,9 @@ build target .DS_Store logs -!auto/build \ No newline at end of file +!auto/build + +# Environment files (contain secrets) +.env +.env.local +.env.*.local \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index b2f967a..2ba3ad1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,31 +1,65 @@ -# Build stage -FROM openjdk:21-jdk-slim AS build +# ============================================================================= +# Multi-stage Dockerfile for Spring Boot Application +# ============================================================================= +# Build: docker build -t user-service . +# Run: docker run -p 3001:3001 -p 9090:9090 user-service +# ============================================================================= + +# ----------------------------------------------------------------------------- +# Build Stage (use Eclipse Temurin with glibc for protoc compatibility) +# ----------------------------------------------------------------------------- +FROM eclipse-temurin:25-jdk AS build WORKDIR /app -# Copy Gradle wrapper and buf-gen files +# Copy Gradle wrapper and build files first (for layer caching) COPY gradle/ gradle/ COPY gradlew build.gradle.kts ./ # Make gradlew executable RUN chmod +x ./gradlew -# Copy source code +# Download dependencies (cached layer if build files unchanged) +RUN ./gradlew dependencies --no-daemon || true + +# Copy source code (including proto files) COPY src ./src -# Build the application +# Build the application (generateProto runs automatically, skip tests) RUN ./gradlew clean bootJar -x test --no-daemon -# Runtime stage -FROM eclipse-temurin:21-jre +# ----------------------------------------------------------------------------- +# Runtime Stage (use Alpine for smaller image size) +# ----------------------------------------------------------------------------- +FROM eclipse-temurin:25-jre-alpine + +# Add non-root user for security +RUN addgroup -g 1001 -S appgroup && \ + adduser -u 1001 -S appuser -G appgroup WORKDIR /app -# Copy JAR from buf-gen stage -COPY --from=build /app/build/libs/user-service.jar user-service.jar +# Copy JAR from build stage (matches bootJar archiveFileName in build.gradle.kts) +COPY --from=build /app/build/libs/user-application.jar app.jar + +# Change ownership to non-root user +RUN chown -R appuser:appgroup /app + +# Switch to non-root user +USER appuser + +# Expose REST and gRPC ports +EXPOSE 3001 9090 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:3001/actuator/health || exit 1 -# Expose port -EXPOSE 3001 +# JVM optimizations for containers +ENV JAVA_OPTS="-XX:+UseContainerSupport \ + -XX:MaxRAMPercentage=75.0 \ + -XX:InitialRAMPercentage=50.0 \ + -Djava.security.egd=file:/dev/./urandom" # Run the application -CMD ["java", "-jar", "user-service.jar"] \ No newline at end of file +ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"] diff --git a/README.md b/README.md index bd43658..ac8714f 100644 --- a/README.md +++ b/README.md @@ -27,8 +27,8 @@ A comprehensive user authentication and management service built with Java 21 an ## Tech Stack -- **Java 21** (Latest LTS) -- **Spring Boot 3.5.3** +- **Java 25** (Latest LTS) +- **Spring Boot 4** - **Spring Security 6** - **Spring Data JPA** - **PostgreSQL 17** diff --git a/auto/docker_logs b/auto/docker_logs new file mode 100755 index 0000000..d9c90eb --- /dev/null +++ b/auto/docker_logs @@ -0,0 +1,3 @@ +#!/usr/bin/env sh + +docker compose -f docker-compose-all.yml up - \ No newline at end of file diff --git a/auto/run_docker b/auto/docker_start similarity index 100% rename from auto/run_docker rename to auto/docker_start diff --git a/auto/docker_stop b/auto/docker_stop new file mode 100755 index 0000000..7cb9395 --- /dev/null +++ b/auto/docker_stop @@ -0,0 +1,3 @@ +#!/usr/bin/env sh + +docker compose -f docker-compose-all.yml down \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 786a6a8..d513e7a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -5,7 +5,6 @@ plugins { id("io.spring.dependency-management") version "1.1.7" id("org.graalvm.buildtools.native") version "0.11.1" id("org.jetbrains.kotlin.jvm") version "2.2.21" - id("io.swagger.core.v3.swagger-gradle-plugin") version "2.2.34" id("org.flywaydb.flyway") version "11.11.1" id("com.diffplug.spotless") version "8.1.0" id("com.google.protobuf") version "0.9.5" @@ -64,12 +63,6 @@ dependencies { testImplementation("org.testcontainers:junit-jupiter") testImplementation("org.testcontainers:postgresql") - // Swagger - implementation("io.swagger.core.v3:swagger-models:2.2.34") - implementation("io.swagger.core.v3:swagger-core:2.2.34") - implementation("jakarta.xml.bind:jakarta.xml.bind-api:4.0.2") - runtimeOnly("org.glassfish.jaxb:jaxb-runtime:4.0.5") - // gRPC and Protobuf implementation("io.grpc:grpc-netty-shaded:1.77.0") implementation("io.grpc:grpc-protobuf:1.77.0") diff --git a/docker-compose-all.yml b/docker-compose-all.yml index 8a311bf..4f5a3d0 100644 --- a/docker-compose-all.yml +++ b/docker-compose-all.yml @@ -1,41 +1,109 @@ +# ============================================================================= +# Docker Compose - Full Application Stack +# ============================================================================= +# Simulates dev/prod environment with all services running in containers. +# +# Usage: +# Start: docker compose -f docker-compose-all.yml up -d +# Stop: docker compose -f docker-compose-all.yml down +# Logs: docker compose -f docker-compose-all.yml logs -f user-service +# Rebuild: docker compose -f docker-compose-all.yml up -d --build +# +# For production, use external secrets management (Vault, AWS Secrets Manager) +# instead of environment variables in this file. +# ============================================================================= + services: + # --------------------------------------------------------------------------- + # Application Service + # --------------------------------------------------------------------------- user-service: build: context: . dockerfile: Dockerfile + container_name: user-application ports: - - "3001:3001" + - "3001:3001" # REST API + - "9090:9090" # gRPC API depends_on: - - postgres + postgres: + condition: service_healthy environment: - - SPRING_PROFILES_ACTIVE=docker - - SPRING_DATASOURCE_URL=jdbc:postgresql://postgres:5432/users - - SPRING_DATASOURCE_USERNAME=test_user_rw - - SPRING_DATASOURCE_PASSWORD=test_user@pass01 - - JWT_ACCESS_SECRET=13483567-651e-410a-b79e-d4c1a66d3bd526b90300-0d1e-4d92-85e0-1747241d052d - - JWT_REFRESH_SECRET=8878f2cc-616e-413a-9b72-2eb32d7bf55c332df003-da39-45b7-82b1-f5f0d3203375 - - CLIENT_URL=http://localhost:3000 - volumes: - - ./logs:/app/logs + # Profile: use 'dev' for development simulation, 'prod' for production + - SPRING_PROFILES_ACTIVE=dev + + # Database connection (matches dev/prod profile expectations) + - DATABASE_URL=jdbc:postgresql://postgres:5432/users + - DATABASE_USERNAME=app_user + - DATABASE_PASSWORD=${DB_PASSWORD:-changeme_in_production} + + # JWT secrets - MUST be overridden in production! + # Generate with: openssl rand -base64 64 + - JWT_ACCESS_SECRET=${JWT_ACCESS_SECRET:-dev-only-access-secret-key-must-be-at-least-64-bytes-long-for-hs512} + - JWT_REFRESH_SECRET=${JWT_REFRESH_SECRET:-dev-only-refresh-secret-key-must-be-at-least-64-bytes-long-for-hs512} + + # CORS - adjust for your frontend URL + - CORS_ALLOWED_ORIGINS=${CORS_ALLOWED_ORIGINS:-http://localhost:3000} + + # JVM options for container environment + - JAVA_OPTS=-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0 + healthcheck: + test: [ "CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3001/actuator/health" ] + interval: 30s + timeout: 10s + retries: 3 + start_period: 60s + restart: unless-stopped + deploy: + resources: + limits: + cpus: '2' + memory: 1G + reservations: + cpus: '0.5' + memory: 512M networks: - - user-network + - app-network + # --------------------------------------------------------------------------- + # PostgreSQL Database + # --------------------------------------------------------------------------- postgres: - image: postgres:17.5-alpine + image: postgres:17-alpine + container_name: user-application-db ports: - - "54321:5432" + - "54321:5432" # External port for debugging (remove in production) environment: - POSTGRES_DB=users - - POSTGRES_USER=test_user_rw - - POSTGRES_PASSWORD=test_user@pass01 + - POSTGRES_USER=app_user + - POSTGRES_PASSWORD=${DB_PASSWORD:-changeme_in_production} + # Performance tuning for container + - POSTGRES_INITDB_ARGS=--encoding=UTF-8 --lc-collate=C --lc-ctype=C volumes: - postgres_data:/var/lib/postgresql/data + healthcheck: + test: [ "CMD-SHELL", "pg_isready -U app_user -d users" ] + interval: 10s + timeout: 5s + retries: 5 + start_period: 30s + restart: unless-stopped + deploy: + resources: + limits: + cpus: '1' + memory: 512M + reservations: + cpus: '0.25' + memory: 256M networks: - - user-network + - app-network volumes: postgres_data: + name: user-service-postgres-data networks: - user-network: - driver: bridge \ No newline at end of file + app-network: + name: user-service-network + driver: bridge diff --git a/docker-compose.yml b/docker-compose.yml index ab0a69e..05e50a4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,26 @@ +# ============================================================================= +# Docker Compose - Local Development (Database Only) +# ============================================================================= +# Used by Spring Boot's Docker Compose integration when: +# spring.docker.compose.enabled=true (in application-local.yml) +# +# This file is auto-detected and managed by Spring Boot. +# The application runs on your host machine, only PostgreSQL runs in Docker. +# +# Usage: +# ./gradlew bootRun --args='--spring.profiles.active=local' +# (Spring Boot automatically starts/stops this compose file) +# +# Manual control: +# Start: docker compose up -d +# Stop: docker compose down +# Reset: docker compose down -v (removes data volume) +# ============================================================================= + services: postgres: - image: postgres:17.5-alpine + image: postgres:17-alpine + container_name: user-service-local-db ports: - "54321:5432" environment: @@ -9,12 +29,12 @@ services: - POSTGRES_PASSWORD=test_user@pass01 volumes: - postgres_data:/var/lib/postgresql/data - networks: - - user-network + healthcheck: + test: ["CMD-SHELL", "pg_isready -U test_user_rw -d users"] + interval: 10s + timeout: 5s + retries: 5 volumes: postgres_data: - -networks: - user-network: - driver: bridge \ No newline at end of file + name: user-service-local-postgres-data diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml new file mode 100644 index 0000000..2ae3c70 --- /dev/null +++ b/src/main/resources/application-dev.yml @@ -0,0 +1,53 @@ +# ============================================================================= +# Development/Staging Profile +# ============================================================================= +# Usage: Set SPRING_PROFILES_ACTIVE=dev +# +# Features: +# - Connects to external PostgreSQL (via environment variables) +# - Swagger UI enabled for API testing +# - Info-level logging +# - JWT secrets from environment variables (required) +# +# Required Environment Variables: +# - DATABASE_URL (e.g., jdbc:postgresql://dev-db.example.com:5432/users) +# - DATABASE_USERNAME +# - DATABASE_PASSWORD +# - JWT_ACCESS_SECRET (min 64 bytes for HS512) +# - JWT_REFRESH_SECRET (min 64 bytes for HS512) +# ============================================================================= + +spring: + # External database connection + datasource: + url: ${DATABASE_URL} + username: ${DATABASE_USERNAME} + password: ${DATABASE_PASSWORD} + driver-class-name: org.postgresql.Driver + hikari: + maximum-pool-size: 5 + minimum-idle: 2 + idle-timeout: 300000 + max-lifetime: 1800000 + connection-timeout: 30000 + + # gRPC reflection enabled for dev debugging + grpc: + server: + reflection: + enabled: true + +# Enable Swagger UI for dev/staging +springdoc: + api-docs: + enabled: true + swagger-ui: + enabled: true + +# Development logging (more verbose than prod) +logging: + level: + org.nkcoder: DEBUG + org.springframework.security: INFO + org.springframework.web: INFO + org.hibernate.SQL: DEBUG diff --git a/src/main/resources/application-docker.yml b/src/main/resources/application-docker.yml deleted file mode 100644 index d268ea8..0000000 --- a/src/main/resources/application-docker.yml +++ /dev/null @@ -1,50 +0,0 @@ -server: - port: 3001 - -spring: - application: - name: user-service - - datasource: - url: jdbc:postgresql://postgres:5432/users - username: test_user_rw - password: test_user@pass01 - driver-class-name: org.postgresql.Driver - - jpa: - hibernate: - ddl-auto: update - show-sql: false - properties: - hibernate: - dialect: org.hibernate.dialect.PostgreSQLDialect - format_sql: true - -# JWT Configuration -jwt: - secret: - access: ${JWT_ACCESS_SECRET} - refresh: ${JWT_REFRESH_SECRET} - expiration: - access: ${JWT_ACCESS_EXPIRES_IN:15m} - refresh: ${JWT_REFRESH_EXPIRES_IN:7d} - issuer: ${JWT_ISSUER:user-service} - -# CORS Configuration -cors: - allowed-origins: ${CLIENT_URL:http://localhost:3000} - allowed-methods: GET,POST,PUT,DELETE,PATCH,OPTIONS - allowed-headers: "*" - allow-credentials: true - max-age: 3600 - -# Logging Configuration -logging: - level: - org.nkcoder: INFO - org.springframework.security: INFO - org.hibernate.SQL: INFO - file: - name: /app/logs/user-service.log - pattern: - console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n" \ No newline at end of file diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index 1432ee1..673c466 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -1,19 +1,43 @@ +# ============================================================================= +# Local Development Profile +# ============================================================================= +# Usage: ./gradlew bootRun --args='--spring.profiles.active=local' +# or set SPRING_PROFILES_ACTIVE=local +# +# Features: +# - Docker Compose auto-starts PostgreSQL container +# - Swagger UI enabled for API exploration +# - Debug logging enabled +# - Default JWT secrets (DO NOT use in production) +# ============================================================================= + spring: - application: - name: user-service + # Auto-start PostgreSQL via Docker Compose docker: compose: enabled: true + lifecycle-management: start-and-stop + skip: + in-tests: true + + # gRPC reflection enabled for local debugging (grpcurl, etc.) + grpc: + server: + reflection: + enabled: true + +# Enable Swagger UI for local development +springdoc: + api-docs: + enabled: true + swagger-ui: + enabled: true -# Logging Configuration +# Debug logging for local development logging: level: - com.timor.user: DEBUG - org.springframework.security: INFO - org.springframework.web: INFO - org.hibernate.SQL: INFO - org.hibernate.type.descriptor.sql.BasicBinder: INFO - com.zaxxer.hikari: INFO - com.zaxxer.hikari.HikariConfig: INFO - pattern: - console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n" + org.nkcoder: DEBUG + org.springframework.security: DEBUG + org.springframework.web: DEBUG + org.hibernate.SQL: DEBUG + org.hibernate.orm.jdbc.bind: TRACE diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index bac483d..d073c9a 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -1,14 +1,69 @@ +# ============================================================================= +# Production Profile +# ============================================================================= +# Usage: Set SPRING_PROFILES_ACTIVE=prod +# +# Features: +# - Connects to external PostgreSQL (via environment variables) +# - Swagger UI DISABLED for security +# - Minimal logging (WARN level) +# - Optimized connection pool settings +# - All secrets from environment variables (required) +# +# Required Environment Variables: +# - DATABASE_URL (e.g., jdbc:postgresql://prod-db.example.com:5432/users) +# - DATABASE_USERNAME +# - DATABASE_PASSWORD +# - JWT_ACCESS_SECRET (min 64 bytes for HS512) +# - JWT_REFRESH_SECRET (min 64 bytes for HS512) +# - CORS_ALLOWED_ORIGINS (e.g., https://myapp.com) +# +# Optional Environment Variables: +# - JWT_ACCESS_EXPIRES_IN (default: 15m) +# - JWT_REFRESH_EXPIRES_IN (default: 7d) +# - JWT_ISSUER (default: user-service) +# ============================================================================= + spring: - profiles: - active: prod + # External database connection with production-optimized pool datasource: url: ${DATABASE_URL} username: ${DATABASE_USERNAME} password: ${DATABASE_PASSWORD} driver-class-name: org.postgresql.Driver hikari: - maximum-pool-size: 10 + maximum-pool-size: 20 minimum-idle: 5 idle-timeout: 300000 max-lifetime: 1800000 - connection-timeout: 30000 \ No newline at end of file + connection-timeout: 30000 + pool-name: UserServiceHikariPool + # Production optimizations + leak-detection-threshold: 60000 + validation-timeout: 5000 + + # Disable gRPC reflection in production for security + grpc: + server: + reflection: + enabled: false + +# Swagger UI DISABLED in production +springdoc: + api-docs: + enabled: false + swagger-ui: + enabled: false + +# Production logging - minimal, structured +logging: + level: + root: WARN + org.nkcoder: INFO + org.springframework.security: WARN + org.springframework.web: WARN + org.hibernate.SQL: WARN + com.zaxxer.hikari: WARN + # Consider using JSON format for log aggregation in production + # pattern: + # console: '{"timestamp":"%d{ISO8601}","level":"%level","logger":"%logger","message":"%msg"}%n' diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 6742fa3..82ee6ad 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,11 +1,31 @@ +# ============================================================================= +# Base Configuration - Shared across all profiles +# ============================================================================= +# Profile-specific configs override these values. +# Activate profiles via: --spring.profiles.active=local|dev|prod +# ============================================================================= + server: port: 3001 servlet: context-path: / + shutdown: graceful spring: application: name: user-service + + # Default: Docker Compose disabled (only enabled in 'local' profile) + docker: + compose: + enabled: false + + # Virtual threads (Project Loom) - enabled for all environments + threads: + virtual: + enabled: true + + # Flyway migrations flyway: enabled: true locations: classpath:db/migration @@ -13,10 +33,12 @@ spring: validate-on-migrate: true validate-migration-naming: true + # JPA/Hibernate defaults jpa: hibernate: ddl-auto: validate show-sql: false + open-in-view: false properties: hibernate: dialect: org.hibernate.dialect.PostgreSQLDialect @@ -26,14 +48,6 @@ spring: batch_size: 20 fetch_size: 100 generate_statistics: false - - docker: - compose: - enabled: false - - threads: - virtual: - enabled: true # gRPC configuration grpc: @@ -44,26 +58,33 @@ spring: reflection: enabled: true +# ----------------------------------------------------------------------------- # JWT Configuration +# ----------------------------------------------------------------------------- +# IMPORTANT: Override secrets via environment variables in dev/prod! +# Default secrets are for local development only. jwt: secret: - access: ${JWT_ACCESS_SECRET:default-hmac512-access-secret-key-for-user-service-2025-extended-64-bytes} - refresh: ${JWT_REFRESH_SECRET:default-hmac512-refresh-secret-key-for-user-service-2025-extended-64b} + access: ${JWT_ACCESS_SECRET:default-hmac512-access-secret-key-for-local-dev-only-not-for-production-64-bytes} + refresh: ${JWT_REFRESH_SECRET:default-hmac512-refresh-secret-key-for-local-dev-only-not-for-prod-64-bytes} expiration: access: ${JWT_ACCESS_EXPIRES_IN:15m} refresh: ${JWT_REFRESH_EXPIRES_IN:7d} issuer: ${JWT_ISSUER:user-service} +# ----------------------------------------------------------------------------- # CORS Configuration +# ----------------------------------------------------------------------------- cors: - allowed-origins: ${CLIENT_URL:http://localhost:3000} + allowed-origins: ${CORS_ALLOWED_ORIGINS:http://localhost:3000} allowed-methods: GET,POST,PUT,DELETE,PATCH,OPTIONS allowed-headers: "*" allow-credentials: true max-age: 3600 - +# ----------------------------------------------------------------------------- # Actuator Configuration +# ----------------------------------------------------------------------------- management: endpoints: web: @@ -72,39 +93,53 @@ management: endpoint: health: show-details: when-authorized + probes: + enabled: true + health: + livenessstate: + enabled: true + readinessstate: + enabled: true info: env: enabled: true -# OpenAPI/Swagger Configuration +# ----------------------------------------------------------------------------- +# OpenAPI/Swagger Configuration (disabled by default, enabled in local/dev) +# ----------------------------------------------------------------------------- springdoc: api-docs: path: /api-docs + enabled: false swagger-ui: path: /swagger-ui.html - enabled: true + enabled: false tags-sorter: alpha operations-sorter: alpha show-actuator: false -# Logging Configuration +# ----------------------------------------------------------------------------- +# Logging Configuration (base level) +# ----------------------------------------------------------------------------- logging: level: - org.nkcoder: DEBUG - org.springframework.security: INFO - org.springframework.web: INFO - org.hibernate.SQL: INFO - org.hibernate.type.descriptor.sql.BasicBinder: INFO - com.zaxxer.hikari: INFO - com.zaxxer.hikari.HikariConfig: INFO + root: INFO + org.nkcoder: INFO + org.springframework.security: WARN + org.springframework.web: WARN + org.hibernate.SQL: WARN + org.hibernate.orm.jdbc.bind: WARN + com.zaxxer.hikari: WARN pattern: - console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n" + console: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n" +# ----------------------------------------------------------------------------- # Application Information +# ----------------------------------------------------------------------------- info: app: name: '@project.name@' version: '@project.version@' description: '@project.description@' java-version: '@java.version@' - spring-boot-version: '@parent.version@' \ No newline at end of file + spring-boot-version: '@parent.version@'