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
3 changes: 2 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ on:
- 'platform/**'
- '.github/actions/**'
- '.github/workflows/**'
- 'docker-compose.dev.yml'
- 'docker-compose.yml'
- 'services/*/docker-compose.yml'
workflow_dispatch:

concurrency:
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ Domain-Driven Design with a clean layered architecture:
### Start the dev environment

```bash
docker compose -f docker-compose.dev.yml up -d
docker compose up -d
```

This starts:
Expand All @@ -55,7 +55,7 @@ This starts:

### Environment files

The dev compose files include sensible defaults. For production-like secrets,
The compose files include sensible defaults. For production-like secrets,
copy the examples:

```bash
Expand Down
6 changes: 3 additions & 3 deletions dev-setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
# (required by docker-mailserver with SSL_TYPE=self-signed)
#
# After running this script:
# docker compose -f docker-compose.dev.yml up
# docker compose up
# =============================================================================
set -euo pipefail

Expand Down Expand Up @@ -79,7 +79,7 @@ fi
# ── services/listmonk/.listmonk.env ───────────────────────────────────────────
LISTMONK_ENV="${SCRIPT_DIR}/services/listmonk/.listmonk.env"
if [[ ! -f "${LISTMONK_ENV}" ]]; then
# Credentials match the hardcoded values in services/listmonk/docker-compose.dev.yml
# Credentials match the hardcoded values in services/listmonk/docker-compose.yml
# so Listmonk and the API agree without extra config.
cat > "${LISTMONK_ENV}" <<'EOF'
LISTMONK_DB_PASSWORD=listmonk
Expand Down Expand Up @@ -175,5 +175,5 @@ fi
echo ""
echo "==> Done. Start the dev environment with:"
echo ""
echo " docker compose -f docker-compose.dev.yml up"
echo " docker compose up"
echo ""
5 changes: 0 additions & 5 deletions docker-compose.dev.yml

This file was deleted.

5 changes: 5 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
include:
- services/api/docker-compose.yml
- services/frontend/docker-compose.yml
- services/stalwart/docker-compose.yml
- services/listmonk/docker-compose.yml
4 changes: 2 additions & 2 deletions platform/cluster/flux/apps/data/mariadb/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ spec:
image:
repository: bitnamilegacy/mariadb
# Pin MariaDB 10.11 LTS to match the old VPS and the rest of the
# codebase (docker-compose.yml, CI workflows, services/api/docker-
# compose.dev.yml — all pinned to mariadb:10.11.10). Bitnami chart
# codebase (services/api/docker-compose.yml + CI workflows — all
# pinned to mariadb:10.11.10). Bitnami chart
# 18.x defaults to MariaDB 11.x, which tightened a number of DDL
# rules (e.g. refusing `MODIFY COLUMN` on a column referenced by a
# foreign key — ERROR 1832) that our Flyway V28 migration
Expand Down
10 changes: 5 additions & 5 deletions scripts/generate_openapi.sh
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,15 @@ if ! docker compose version >/dev/null 2>&1; then
exit 1
fi

if [ ! -f "docker-compose.dev.yml" ]; then
echo "docker-compose.dev.yml not found in current directory" >&2
if [ ! -f "docker-compose.yml" ]; then
echo "docker-compose.yml not found in current directory" >&2
exit 1
fi

# ---- Fetch Blueshell spec from running API container ----

echo "Fetching Blueshell OpenAPI spec from API container..."
docker compose -f docker-compose.dev.yml exec -T api sh -c \
docker compose exec -T api sh -c \
"curl -fsSS http://localhost:8080/v3/api-docs" > "${API_OPENAPI_SPEC%.yaml}.raw.json"

# ---- Shared steps ----
Expand All @@ -46,8 +46,8 @@ echo "Generating frontend TypeScript clients..."
# but forces a recreate when mounts or env have drifted (e.g. after
# the openapi.yaml → openapi.json rename). Without this, `exec` runs
# against a stale container that still has the old bind-mounts.
docker compose -f docker-compose.dev.yml up -d frontend
docker compose -f docker-compose.dev.yml exec frontend sh -c \
docker compose up -d frontend
docker compose exec frontend sh -c \
"cd /usr/app && yarn gen:all && (yarn lint:gen || true)"

echo "OpenAPI spec and frontend clients generated successfully."
75 changes: 0 additions & 75 deletions services/api/docker-compose.dev.yml

This file was deleted.

60 changes: 37 additions & 23 deletions services/api/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
services:
db:
image: mariadb:10.11.10
networks:
- database
container_name: db
ports:
- "3307:3306"
volumes:
- db_data:/var/lib/mysql
- data:/var/lib/mysql
- "./0_init.sql:/docker-entrypoint-initdb.d/0_init.sql"
- "./1_dump.sql:/docker-entrypoint-initdb.d/1_dump.sql"
networks:
- global_network
restart: unless-stopped
env_file:
- .db.env
environment:
- TZ=Europe/Amsterdam
healthcheck:
test: [ "CMD", "mysqladmin", "ping", "-h", "localhost" ]
interval: 30s
Expand All @@ -17,45 +23,53 @@ services:
start_period: 30s

api:
image: ghcr.io/esa-blueshell/api:${IMAGE_TAG:-latest}
pull_policy: always
image: api:dev
pull_policy: never
build:
context: ../..
dockerfile: services/api/Dockerfile
# API image is a multi-stage build that compiles every module in the
# monorepo that :services:api depends on, so the whole repo is the
# context. .dockerignore keeps frontend/node_modules etc. out.
restart: unless-stopped
dockerfile: services/api/Dockerfile-dev
volumes:
# Mount the whole repo into /src so hot-reload sees api + build-logic + libs edits.
- ../..:/src
- ../../storage:/home/storage
- gradle-cache:/root/.gradle/caches
- playwright-browsers:/ms-playwright
- playwright-browsers:/root/.cache/ms-playwright
- listmonk-secrets:/run/secrets/listmonk:ro
ports:
- "8080:8080"
- "5005:5005"
networks:
- global_network
env_file:
- .api.env
environment:
- TZ=Europe/Amsterdam
- SPRING_PROFILES_ACTIVE=dev
- APP_URL=http://localhost:8080
- FRONTEND_URL=http://localhost:3000
# Listmonk credentials — must be set; no defaults to avoid silent misconfiguration
- LISTMONK_API_USERNAME=${LISTMONK_ADMIN_USERNAME:?LISTMONK_ADMIN_USERNAME must be set in .listmonk.env}
- LISTMONK_API_PASSWORD=${LISTMONK_ADMIN_PASSWORD:?LISTMONK_ADMIN_PASSWORD must be set in .listmonk.env}
volumes:
- "../../storage:/home/storage"
- "listmonk-secrets:/run/secrets/listmonk:ro"
- PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
- LISTMONK_FROM_ADDRESS=no-reply@localhost
- LISTMONK_REPLY_TO_ADDRESS=dev@localhost
- LISTMONK_API_USERNAME=${LISTMONK_ADMIN_USERNAME:-listmonk}
- LISTMONK_API_PASSWORD=${LISTMONK_ADMIN_PASSWORD:-listmonk}
depends_on:
db:
condition: service_healthy
listmonk-setup:
condition: service_completed_successfully
networks:
- frontback
- database
healthcheck:
test: [ "CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/health" ]
test: [ "CMD", "curl", "http://localhost:8080/health" ]
interval: 30s
timeout: 10s
retries: 20
start_period: 30s

networks:
frontback:
database:
global_network:
driver: bridge

volumes:
db_data:
data:
gradle-cache:
playwright-browsers:
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import org.springframework.test.context.ActiveProfiles
* Brevo credentials are resolved by Spring from the environment (BREVO_API_KEY etc.)
* exactly as in production. Run via Docker where the env file is loaded:
*
* docker compose -f docker-compose.dev.yml run api \
* docker compose run api \
* ./gradlew :api:test --tests "*.BrevoContactAdapterLiveIT"
*
* Tests are ordered: each builds on the state left by the previous one.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import org.springframework.test.context.ActiveProfiles
* Listmonk credentials are resolved by Spring from the environment (LISTMONK_* vars)
* exactly as in production. Run via Docker where the env file is loaded:
*
* docker compose -f docker-compose.dev.yml run api \
* docker compose run api \
* ./gradlew :api:test --tests "*.ListmonkContactAdapterLiveIT"
*
* Tests are ordered: each builds on the state left by the previous one.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import net.blueshell.api.domain.user.application.exception.UserNotFoundException
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource
import org.springframework.security.web.context.SecurityContextRepository
import org.springframework.stereotype.Component
import org.springframework.web.filter.OncePerRequestFilter
import java.io.IOException
Expand All @@ -18,7 +19,8 @@ class JwtAuthFilter(
private val jwtTokenUtil: JwtTokenUtil,
private val jwtRevocationService: JwtRevocationService,
private val userService: UserService,
private val authTokenCookieService: AuthTokenCookieService
private val authTokenCookieService: AuthTokenCookieService,
private val securityContextRepository: SecurityContextRepository,
) :
OncePerRequestFilter() {
@Throws(ServletException::class, IOException::class)
Expand Down Expand Up @@ -58,7 +60,12 @@ class JwtAuthFilter(
principal, null, principal.authorities
)
auth.details = WebAuthenticationDetailsSource().buildDetails(request)
SecurityContextHolder.getContext().authentication = auth
// saveContext is required under requireExplicitSave=true so
// deferred resolvers (e.g. SAS authorize) see the principal.
val context = SecurityContextHolder.createEmptyContext()
context.authentication = auth
SecurityContextHolder.setContext(context)
securityContextRepository.saveContext(context, request, response)
}

filterChain.doFilter(request, response)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package net.blueshell.api.infrastructure.security

import org.springframework.boot.web.servlet.FilterRegistrationBean
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.web.context.DelegatingSecurityContextRepository
import org.springframework.security.web.context.HttpSessionSecurityContextRepository
import org.springframework.security.web.context.RequestAttributeSecurityContextRepository
import org.springframework.security.web.context.SecurityContextRepository

@Configuration
class SecurityContextRepositoryConfig {

@Bean
fun securityContextRepository(): SecurityContextRepository =
DelegatingSecurityContextRepository(
RequestAttributeSecurityContextRepository(),
HttpSessionSecurityContextRepository(),
)

// JwtAuthFilter is @Component, which Spring Boot would otherwise
// auto-register as a servlet-container filter outside the security
// chain. It must only run inside the SecurityFilterChain, where its
// SecurityContext mutation is saved via the repository.
@Bean
fun jwtAuthFilterRegistration(filter: JwtAuthFilter): FilterRegistrationBean<JwtAuthFilter> =
FilterRegistrationBean(filter).apply { isEnabled = false }
}
38 changes: 0 additions & 38 deletions services/frontend/docker-compose.dev.yml

This file was deleted.

Loading
Loading