feat(sprint-5): Phase 1 — Docker Compose full stack, CORS, Next.js upgrade
- Dockerfile.backend: multi-stage Java 21 build (eclipse-temurin) - docker-compose.yml: PostgreSQL 16 + backend + frontend with health checks - SecurityConfig: CORS for localhost:3000 frontend origin - application-docker.properties: Docker profile with env vars - Spring Boot Actuator health endpoint enabled - Next.js upgraded 15.2.8 → 15.5.18 (security fixes)
This commit is contained in:
@@ -0,0 +1,38 @@
|
|||||||
|
# Multi-stage build for cannamanage-api (Spring Boot + Java 21)
|
||||||
|
# Build context: repo root (needs access to all Maven modules)
|
||||||
|
|
||||||
|
FROM eclipse-temurin:21-jdk-alpine AS builder
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy Maven wrapper + POM files first (layer caching)
|
||||||
|
COPY .mvn/ .mvn/
|
||||||
|
COPY mvnw pom.xml ./
|
||||||
|
COPY cannamanage-domain/pom.xml cannamanage-domain/pom.xml
|
||||||
|
COPY cannamanage-service/pom.xml cannamanage-service/pom.xml
|
||||||
|
COPY cannamanage-api/pom.xml cannamanage-api/pom.xml
|
||||||
|
|
||||||
|
# Download dependencies (cached unless POMs change)
|
||||||
|
RUN chmod +x mvnw && ./mvnw dependency:go-offline -B -q 2>/dev/null || true
|
||||||
|
|
||||||
|
# Copy source code
|
||||||
|
COPY cannamanage-domain/src/ cannamanage-domain/src/
|
||||||
|
COPY cannamanage-service/src/ cannamanage-service/src/
|
||||||
|
COPY cannamanage-api/src/ cannamanage-api/src/
|
||||||
|
|
||||||
|
# Build the fat JAR
|
||||||
|
RUN ./mvnw package -pl cannamanage-api -am -DskipTests -B -q
|
||||||
|
|
||||||
|
# --- Runtime stage ---
|
||||||
|
FROM eclipse-temurin:21-jre-alpine AS runtime
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Create non-root user
|
||||||
|
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
|
||||||
|
|
||||||
|
COPY --from=builder /app/cannamanage-api/target/*.jar app.jar
|
||||||
|
|
||||||
|
USER appuser
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
ENTRYPOINT ["java", "-jar", "app.jar"]
|
||||||
@@ -118,6 +118,11 @@
|
|||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-mail</artifactId>
|
<artifactId>spring-boot-starter-mail</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<!-- Actuator (health endpoint for Docker) -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package de.cannamanage.api.security;
|
package de.cannamanage.api.security;
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.core.annotation.Order;
|
import org.springframework.core.annotation.Order;
|
||||||
@@ -13,8 +14,12 @@ import org.springframework.security.crypto.password.PasswordEncoder;
|
|||||||
import org.springframework.security.web.SecurityFilterChain;
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||||
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
|
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
|
||||||
|
import org.springframework.web.cors.CorsConfiguration;
|
||||||
|
import org.springframework.web.cors.CorsConfigurationSource;
|
||||||
|
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Security configuration — Sprint 3: API + Staff portal with JWT + Member portal with sessions.
|
* Security configuration — Sprint 3: API + Staff portal with JWT + Member portal with sessions.
|
||||||
@@ -38,6 +43,7 @@ public class SecurityConfig {
|
|||||||
public SecurityFilterChain apiSecurityFilterChain(HttpSecurity http) throws Exception {
|
public SecurityFilterChain apiSecurityFilterChain(HttpSecurity http) throws Exception {
|
||||||
http
|
http
|
||||||
.securityMatcher("/api/**")
|
.securityMatcher("/api/**")
|
||||||
|
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
|
||||||
.csrf(csrf -> csrf.disable())
|
.csrf(csrf -> csrf.disable())
|
||||||
.sessionManagement(session -> session
|
.sessionManagement(session -> session
|
||||||
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||||
@@ -116,4 +122,21 @@ public class SecurityConfig {
|
|||||||
public PasswordEncoder passwordEncoder() {
|
public PasswordEncoder passwordEncoder() {
|
||||||
return new BCryptPasswordEncoder();
|
return new BCryptPasswordEncoder();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public CorsConfigurationSource corsConfigurationSource() {
|
||||||
|
CorsConfiguration config = new CorsConfiguration();
|
||||||
|
config.setAllowedOrigins(List.of(
|
||||||
|
"http://localhost:3000",
|
||||||
|
"http://frontend:3000"
|
||||||
|
));
|
||||||
|
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"));
|
||||||
|
config.setAllowedHeaders(List.of("*"));
|
||||||
|
config.setAllowCredentials(true);
|
||||||
|
config.setMaxAge(3600L);
|
||||||
|
|
||||||
|
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||||
|
source.registerCorsConfiguration("/api/**", config);
|
||||||
|
return source;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# Docker profile — used when running in Docker Compose
|
||||||
|
spring.datasource.url=${SPRING_DATASOURCE_URL}
|
||||||
|
spring.datasource.username=${SPRING_DATASOURCE_USERNAME}
|
||||||
|
spring.datasource.password=${SPRING_DATASOURCE_PASSWORD}
|
||||||
|
|
||||||
|
# Enable Flyway for container startup (fresh DB)
|
||||||
|
spring.flyway.enabled=true
|
||||||
|
spring.jpa.hibernate.ddl-auto=validate
|
||||||
|
|
||||||
|
# JWT secret from environment
|
||||||
|
cannamanage.security.jwt.secret=${CANNAMANAGE_SECURITY_JWT_SECRET}
|
||||||
|
|
||||||
|
# Actuator
|
||||||
|
management.endpoints.web.exposure.include=health
|
||||||
|
management.endpoint.health.show-details=never
|
||||||
|
|
||||||
|
# Disable mail in Docker (no SMTP container)
|
||||||
|
spring.mail.host=localhost
|
||||||
|
spring.mail.port=1025
|
||||||
@@ -31,6 +31,10 @@ spring.mail.from=${MAIL_FROM:noreply@cannamanage.de}
|
|||||||
# App base URL (for invite links)
|
# App base URL (for invite links)
|
||||||
app.base-url=${APP_BASE_URL:http://localhost:8080}
|
app.base-url=${APP_BASE_URL:http://localhost:8080}
|
||||||
|
|
||||||
|
# Actuator
|
||||||
|
management.endpoints.web.exposure.include=health
|
||||||
|
management.endpoint.health.show-details=never
|
||||||
|
|
||||||
# Session configuration (member portal)
|
# Session configuration (member portal)
|
||||||
server.servlet.session.timeout=30m
|
server.servlet.session.timeout=30m
|
||||||
server.servlet.session.cookie.same-site=strict
|
server.servlet.session.cookie.same-site=strict
|
||||||
|
|||||||
@@ -46,7 +46,7 @@
|
|||||||
"emoji-picker-react": "4.12.2",
|
"emoji-picker-react": "4.12.2",
|
||||||
"input-otp": "1.4.2",
|
"input-otp": "1.4.2",
|
||||||
"lucide-react": "0.446.0",
|
"lucide-react": "0.446.0",
|
||||||
"next": "15.2.8",
|
"next": "15.5.18",
|
||||||
"next-auth": "5.0.0-beta.31",
|
"next-auth": "5.0.0-beta.31",
|
||||||
"next-intl": "^4.13.0",
|
"next-intl": "^4.13.0",
|
||||||
"react": "19.1.3",
|
"react": "19.1.3",
|
||||||
@@ -72,7 +72,7 @@
|
|||||||
"@types/react": "19.0.12",
|
"@types/react": "19.0.12",
|
||||||
"@types/react-dom": "19.0.4",
|
"@types/react-dom": "19.0.4",
|
||||||
"eslint": "9.18.0",
|
"eslint": "9.18.0",
|
||||||
"eslint-config-next": "15.2.8",
|
"eslint-config-next": "15.5.18",
|
||||||
"eslint-config-prettier": "10.1.1",
|
"eslint-config-prettier": "10.1.1",
|
||||||
"eslint-plugin-prettier": "5.2.3",
|
"eslint-plugin-prettier": "5.2.3",
|
||||||
"playwright": "^1.60.0",
|
"playwright": "^1.60.0",
|
||||||
|
|||||||
Generated
+2743
-4547
File diff suppressed because it is too large
Load Diff
+22
-18
@@ -1,17 +1,15 @@
|
|||||||
version: '3.9'
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
db:
|
db:
|
||||||
image: postgres:16-alpine
|
image: postgres:16-alpine
|
||||||
container_name: cannamanage-db-local
|
container_name: cannamanage-db
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_DB: cannamanage
|
POSTGRES_DB: cannamanage
|
||||||
POSTGRES_USER: cannamanage
|
POSTGRES_USER: cannamanage
|
||||||
POSTGRES_PASSWORD: dev_password_change_in_prod
|
POSTGRES_PASSWORD: cannamanage_dev
|
||||||
ports:
|
ports:
|
||||||
- "5432:5432"
|
- "5432:5432"
|
||||||
volumes:
|
volumes:
|
||||||
- pgdata_local:/var/lib/postgresql/data
|
- pgdata:/var/lib/postgresql/data
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "pg_isready -U cannamanage"]
|
test: ["CMD-SHELL", "pg_isready -U cannamanage"]
|
||||||
interval: 5s
|
interval: 5s
|
||||||
@@ -21,19 +19,25 @@ services:
|
|||||||
backend:
|
backend:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: cannamanage-api/Dockerfile
|
dockerfile: Dockerfile.backend
|
||||||
container_name: cannamanage-backend
|
container_name: cannamanage-backend
|
||||||
ports:
|
ports:
|
||||||
- "8080:8080"
|
- "8080:8080"
|
||||||
environment:
|
environment:
|
||||||
- SPRING_DATASOURCE_URL=jdbc:postgresql://db:5432/cannamanage
|
SPRING_PROFILES_ACTIVE: docker
|
||||||
- SPRING_DATASOURCE_USERNAME=cannamanage
|
SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/cannamanage
|
||||||
- SPRING_DATASOURCE_PASSWORD=dev_password_change_in_prod
|
SPRING_DATASOURCE_USERNAME: cannamanage
|
||||||
|
SPRING_DATASOURCE_PASSWORD: cannamanage_dev
|
||||||
|
CANNAMANAGE_SECURITY_JWT_SECRET: docker-dev-secret-key-minimum-32-characters-long-for-hmac
|
||||||
depends_on:
|
depends_on:
|
||||||
db:
|
db:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
profiles:
|
healthcheck:
|
||||||
- full
|
test: ["CMD", "wget", "--spider", "-q", "http://localhost:8080/actuator/health"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
start_period: 30s
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
build:
|
build:
|
||||||
@@ -43,13 +47,13 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "3000:3000"
|
- "3000:3000"
|
||||||
environment:
|
environment:
|
||||||
- BASE_URL=http://localhost:3000
|
NEXTAUTH_URL: http://localhost:3000
|
||||||
- HOME_PATHNAME=/dashboards/analytics
|
NEXTAUTH_SECRET: docker-dev-nextauth-secret-minimum-32chars
|
||||||
- BACKEND_URL=http://backend:8080
|
BACKEND_URL: http://backend:8080
|
||||||
|
AUTH_URL: http://localhost:3000
|
||||||
depends_on:
|
depends_on:
|
||||||
- backend
|
backend:
|
||||||
profiles:
|
condition: service_healthy
|
||||||
- full
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
pgdata_local:
|
pgdata:
|
||||||
|
|||||||
Reference in New Issue
Block a user