Compare commits
28
Commits
a2082d50dc
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e29ca86d84 | ||
|
|
db896be056 | ||
|
|
4e6d7f7b02 | ||
|
|
311f3c9818 | ||
|
|
4269f980db | ||
|
|
c23836e5fe | ||
|
|
7ccf9e1bc3 | ||
|
|
8c6e31166a | ||
|
|
0e2120e414 | ||
|
|
d09fb5d9b7 | ||
|
|
99d683796a | ||
|
|
a6fb0e41fe | ||
|
|
ebb59daad4 | ||
|
|
f02f4a295c | ||
|
|
6a50099849 | ||
|
|
5c92129fba | ||
|
|
7807494b34 | ||
|
|
e5edf055ea | ||
|
|
a0960feeb3 | ||
|
|
34156807e4 | ||
|
|
3c655a5dba | ||
|
|
974d5df8b4 | ||
|
|
ab24be8f3c | ||
|
|
8104c959be | ||
|
|
2151f6661f | ||
|
|
c2c56f7c87 | ||
|
|
04be2b4714 | ||
|
|
243edd4355 |
No files matched your search
@@ -0,0 +1,161 @@
|
|||||||
|
name: Build Remsly
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
Compute-Version:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: gitea.tail276105.ts.net/remsly/ci-image:5
|
||||||
|
outputs:
|
||||||
|
version: ${{ steps.v.outputs.version }}
|
||||||
|
steps:
|
||||||
|
- name: Check out repository code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Compute version
|
||||||
|
id: v
|
||||||
|
run: |
|
||||||
|
MAJOR=$(tr -d '[:space:]' < deploy/prod/VERSION)
|
||||||
|
VERSION="$(date -u +%Y.%m)-${MAJOR}-${GITHUB_RUN_NUMBER}"
|
||||||
|
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "Computed version: $VERSION"
|
||||||
|
|
||||||
|
Build-Remsly-Backend:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: Compute-Version
|
||||||
|
container:
|
||||||
|
image: gitea.tail276105.ts.net/remsly/ci-image:5
|
||||||
|
env:
|
||||||
|
VERSION: ${{ needs.Compute-Version.outputs.version }}
|
||||||
|
DB_HOST_PORT: buildPostgres:5432
|
||||||
|
DB_USER: develop
|
||||||
|
DB_PASSWD: develop
|
||||||
|
DB_NAME: develop
|
||||||
|
services:
|
||||||
|
buildPostgres:
|
||||||
|
image: postgres:14-alpine
|
||||||
|
env:
|
||||||
|
POSTGRES_USER: develop
|
||||||
|
POSTGRES_PASSWORD: develop
|
||||||
|
POSTGRES_DB: develop
|
||||||
|
options: >-
|
||||||
|
--health-cmd="pg_isready -U develop -d develop -h 127.0.0.1"
|
||||||
|
--health-interval=10s
|
||||||
|
--health-timeout=5s
|
||||||
|
--health-retries=12
|
||||||
|
steps:
|
||||||
|
- name: Check out repository code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Build BE
|
||||||
|
run: ./gradlew build -PDB_HOST_PORT=buildPostgres:5432 -PDB_USER=develop -PDB_PASSWD=develop -PDB_NAME=develop;
|
||||||
|
- name: Login to Docker Registry
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
registry: gitea.tail276105.ts.net
|
||||||
|
username: ${{ secrets.REGISTRY_USER }}
|
||||||
|
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||||
|
- name: Build Docker
|
||||||
|
run: |
|
||||||
|
REPOSITORY_LC=$(echo "${{ gitea.repository }}" | tr '[:upper:]' '[:lower:]')
|
||||||
|
(
|
||||||
|
cd backend;
|
||||||
|
docker build -t gitea.tail276105.ts.net/${REPOSITORY_LC}-backend:${VERSION} .;
|
||||||
|
docker tag gitea.tail276105.ts.net/${REPOSITORY_LC}-backend:${VERSION} gitea.tail276105.ts.net/${REPOSITORY_LC}-backend:latest
|
||||||
|
)
|
||||||
|
|
||||||
|
- name: Push Docker Images
|
||||||
|
run: |
|
||||||
|
REPOSITORY_LC=$(echo "${{ gitea.repository }}" | tr '[:upper:]' '[:lower:]')
|
||||||
|
docker push gitea.tail276105.ts.net/${REPOSITORY_LC}-backend:${VERSION}
|
||||||
|
docker push gitea.tail276105.ts.net/${REPOSITORY_LC}-backend:latest
|
||||||
|
|
||||||
|
- name: Docker imageTags
|
||||||
|
run: |
|
||||||
|
REPOSITORY_LC=$(echo "${{ gitea.repository }}" | tr '[:upper:]' '[:lower:]')
|
||||||
|
echo "versioned: gitea.tail276105.ts.net/${REPOSITORY_LC}-backend:${VERSION}"
|
||||||
|
echo "latest: gitea.tail276105.ts.net/${REPOSITORY_LC}-backend:latest"
|
||||||
|
|
||||||
|
- name: Log out from Docker registry
|
||||||
|
if: always()
|
||||||
|
run: docker logout gitea.tail276105.ts.net
|
||||||
|
|
||||||
|
Build-Remsly-Frontend:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: Compute-Version
|
||||||
|
container:
|
||||||
|
image: gitea.tail276105.ts.net/remsly/ci-image:5
|
||||||
|
env:
|
||||||
|
VERSION: ${{ needs.Compute-Version.outputs.version }}
|
||||||
|
steps:
|
||||||
|
- name: Check out repository code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Build FE
|
||||||
|
run: |
|
||||||
|
cd frontend
|
||||||
|
npm ci
|
||||||
|
npm run buildProd
|
||||||
|
- name: Login to Docker Registry
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
registry: gitea.tail276105.ts.net
|
||||||
|
username: ${{ secrets.REGISTRY_USER }}
|
||||||
|
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||||
|
- name: Build Docker
|
||||||
|
run: |
|
||||||
|
REPOSITORY_LC=$(echo "${{ gitea.repository }}" | tr '[:upper:]' '[:lower:]')
|
||||||
|
(
|
||||||
|
cd frontend;
|
||||||
|
docker build -t gitea.tail276105.ts.net/${REPOSITORY_LC}-frontend:${VERSION} .;
|
||||||
|
docker tag gitea.tail276105.ts.net/${REPOSITORY_LC}-frontend:${VERSION} gitea.tail276105.ts.net/${REPOSITORY_LC}-frontend:latest
|
||||||
|
)
|
||||||
|
|
||||||
|
- name: Push Docker Images
|
||||||
|
run: |
|
||||||
|
REPOSITORY_LC=$(echo "${{ gitea.repository }}" | tr '[:upper:]' '[:lower:]')
|
||||||
|
docker push gitea.tail276105.ts.net/${REPOSITORY_LC}-frontend:${VERSION}
|
||||||
|
docker push gitea.tail276105.ts.net/${REPOSITORY_LC}-frontend:latest
|
||||||
|
|
||||||
|
- name: Docker imageTags
|
||||||
|
run: |
|
||||||
|
REPOSITORY_LC=$(echo "${{ gitea.repository }}" | tr '[:upper:]' '[:lower:]')
|
||||||
|
echo "versioned: gitea.tail276105.ts.net/${REPOSITORY_LC}-frontend:${VERSION}"
|
||||||
|
echo "latest: gitea.tail276105.ts.net/${REPOSITORY_LC}-frontend:latest"
|
||||||
|
|
||||||
|
- name: Log out from Docker registry
|
||||||
|
if: always()
|
||||||
|
run: docker logout gitea.tail276105.ts.net
|
||||||
|
|
||||||
|
Package-Prod-Config:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [ Compute-Version, Build-Remsly-Backend, Build-Remsly-Frontend ]
|
||||||
|
container:
|
||||||
|
image: gitea.tail276105.ts.net/remsly/ci-image:5
|
||||||
|
env:
|
||||||
|
VERSION: ${{ needs.Compute-Version.outputs.version }}
|
||||||
|
steps:
|
||||||
|
- name: Check out repository code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Bake image versions into docker-compose.yml
|
||||||
|
run: |
|
||||||
|
# The compose file pins backend/frontend to the <<VERSION>> placeholder;
|
||||||
|
# replace it with this build's version. The manually maintained services
|
||||||
|
# (elasticsearch, keycloak, postgres, tailscale) keep their own tags.
|
||||||
|
sed -i "s|<<VERSION>>|${VERSION}|g" deploy/prod/docker-compose.yml
|
||||||
|
echo "Pinned backend/frontend images to ${VERSION}:"
|
||||||
|
grep -nE "remsly-(backend|frontend)" deploy/prod/docker-compose.yml
|
||||||
|
- name: Write full version into VERSION file
|
||||||
|
run: |
|
||||||
|
printf '%s\n' "${VERSION}" > deploy/prod/VERSION
|
||||||
|
echo "config.tar.gz VERSION -> ${VERSION}"
|
||||||
|
- name: Build config.tar.gz
|
||||||
|
run: tar -czf config.tar.gz -C deploy/prod .
|
||||||
|
- name: Upload config to Gitea generic package registry
|
||||||
|
run: |
|
||||||
|
REPOSITORY_LC=$(echo "${{ gitea.repository }}" | tr '[:upper:]' '[:lower:]')
|
||||||
|
OWNER=${REPOSITORY_LC%%/*}
|
||||||
|
curl -fsSL --user "${{ secrets.REGISTRY_USER }}:${{ secrets.REGISTRY_PASSWORD }}" \
|
||||||
|
--upload-file config.tar.gz \
|
||||||
|
"https://gitea.tail276105.ts.net/api/packages/${OWNER}/generic/prod-config/${VERSION}/config.tar.gz"
|
||||||
|
echo "Uploaded prod-config/${VERSION}/config.tar.gz"
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
name: Gitea Actions Demo
|
|
||||||
run-name: ${{ gitea.actor }} is testing out Gitea Actions 🚀
|
|
||||||
on: [push]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
Explore-Gitea-Actions:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
container:
|
|
||||||
image: eclipse-temurin:17
|
|
||||||
env:
|
|
||||||
DB_HOST_PORT: buildPostgres:5432
|
|
||||||
DB_USER: develop
|
|
||||||
DB_PASSWD: develop
|
|
||||||
DB_NAME: develop
|
|
||||||
services:
|
|
||||||
buildPostgres:
|
|
||||||
image: postgres:14-alpine
|
|
||||||
env:
|
|
||||||
POSTGRES_USER: develop
|
|
||||||
POSTGRES_PASSWORD: develop
|
|
||||||
POSTGRES_DB: develop
|
|
||||||
options: >-
|
|
||||||
--health-cmd="pg_isready -U develop -d develop -h 127.0.0.1"
|
|
||||||
--health-interval=10s
|
|
||||||
--health-timeout=5s
|
|
||||||
--health-retries=12
|
|
||||||
steps:
|
|
||||||
- run : uname -n
|
|
||||||
- run: echo $PATH
|
|
||||||
- run: printenv
|
|
||||||
- run: echo "🎉 The job was automatically triggered by a ${{ gitea.event_name }} event."
|
|
||||||
- run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by Gitea!"
|
|
||||||
- run: echo "🔎 The name of your branch is ${{ gitea.ref }} and your repository is ${{ gitea.repository }}."
|
|
||||||
- run: echo "💡 The ${{ gitea.repository }} repository has been cloned to the runner."
|
|
||||||
- run: echo "🖥️ The workflow is now ready to test your code on the runner."
|
|
||||||
- name: Check out repository code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
- name: Build BE
|
|
||||||
run: ./gradlew build -PDB_HOST_PORT=buildPostgres:5432 -PDB_USER=develop -PDB_PASSWD=develop -PDB_NAME=develop;
|
|
||||||
- run: cp backend/build/libs/backend-version.jar /mnt/space/share/backend-version.jar;
|
|
||||||
- name: List files in the repository
|
|
||||||
run: |
|
|
||||||
ls ${{ gitea.workspace }}
|
|
||||||
- run: echo "🍏 This job's status is ${{ job.status }}."
|
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
/frontend/build/
|
/frontend/build/
|
||||||
!gradle/wrapper/gradle-wrapper.jar
|
!gradle/wrapper/gradle-wrapper.jar
|
||||||
http-client.private.env.json
|
http-client.private.env.json
|
||||||
|
deploy/prod/.env
|
||||||
/backend/src/main/resources/*.properties
|
/backend/src/main/resources/*.properties
|
||||||
src/gen
|
src/gen
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
Boxit (also called "Remsly" in CI) is a box/inventory management app:
|
||||||
|
|
||||||
|
- **backend/** — Spring Boot 2.6 (Kotlin, Java 17, Jersey + Spring MVC), PostgreSQL via jOOQ, Elasticsearch for search, Keycloak for auth. Runs on port **5050**.
|
||||||
|
- **frontend/** — Angular 13 app (port **4200**), proxies `/api` to the backend via `proxy-conf.json`.
|
||||||
|
- **JooqHelper/** — small Gradle module providing `BoxitNamingStrategy` used by jOOQ code generation.
|
||||||
|
- **deploy/localDev/boxit/** — docker-compose for local infrastructure (Postgres on 6543, Keycloak on 8081, Apache OIDC proxy, Elasticsearch, Prometheus).
|
||||||
|
- **deploy/prod/** — production docker-compose + Let's Encrypt scripts.
|
||||||
|
- **clients/** — IntelliJ HTTP client files for the REST API, Keycloak, and Elasticsearch (env in `http-client.private.env.json`, template provided).
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
### Backend (Gradle, from repo root)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew build # build + tests (requires running Postgres, see below)
|
||||||
|
./gradlew test # tests only (JUnit 5; integration tests use Testcontainers)
|
||||||
|
./gradlew generateJooq # run Flyway migrations, then regenerate jOOQ classes
|
||||||
|
./gradlew lintKotlin # kotlinter lint
|
||||||
|
./gradlew formatKotlin # kotlinter autoformat
|
||||||
|
./gradlew bootRun # run the backend
|
||||||
|
```
|
||||||
|
|
||||||
|
Run a single test class: `./gradlew test --tests "ch.boxi.boxit.SomeTestClass"`
|
||||||
|
|
||||||
|
DB connection for Flyway/jOOQ codegen is parameterized via Gradle properties
|
||||||
|
`-PDB_HOST_PORT -PDB_USER -PDB_PASSWD -PDB_NAME` (defaults: `localhost:6543`, `gradle`/`gradlepw`/`gradle` — matches the localDev docker-compose Postgres).
|
||||||
|
|
||||||
|
### Frontend (from frontend/)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm start # dev server on :4200 with /api proxy to :5050
|
||||||
|
npm run build # dev build
|
||||||
|
npm run buildProd # production build (also copies index.html -> 404.html)
|
||||||
|
npm test # Karma/Jasmine tests
|
||||||
|
npm run lint # ng lint
|
||||||
|
```
|
||||||
|
|
||||||
|
### Local infrastructure
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd deploy/localDev/boxit && docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
Provides Postgres (port 6543, multiple DBs: `boxit`, `keycloak`, `gradle`, `test`), Keycloak (8081), Apache OIDC proxy, and Elasticsearch. `backend/src/main/resources/application.properties` is not committed — create it from `application.properties-Template`.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Backend data flow: Flyway → jOOQ codegen
|
||||||
|
|
||||||
|
The DB schema is the source of truth. Migrations live in `backend/src/main/resources/db/migration` (Flyway, `V*.sql`). `generateJooq` depends on `flywayMigrate` and generates records/POJOs/DAOs into `backend/src/gen/java` (package `ch.boxi.boxit.gen`, deleted on `clean`, regenerated on compilation). Naming is customized by `JooqHelper`'s `BoxitNamingStrategy`. **After changing a migration, re-run `./gradlew generateJooq` against a running Postgres.**
|
||||||
|
|
||||||
|
### Backend layers (`ch.boxi.boxit`)
|
||||||
|
|
||||||
|
- `restapi/` — REST controllers, DTOs, mappers, pagination (`Paged`, `PaginationDto`, infinite scroll support). Organized by feature (folder, note, task, user, search).
|
||||||
|
- `domain/` — business logic per feature (attachment, folder, location, note, task, user). Uses a Visitor pattern (`Visitable`/`Visitor`) and `Augmentor` abstractions.
|
||||||
|
- `db/` — jOOQ-based repositories built on `SimpleCrudRepository` / `AbstractSimpleCrudRepositoryImpl` plus composable `JooqFilter` classes (`CombinedAndFilter`, `CombinedOrFilter`, ...).
|
||||||
|
- `searchindex/` — Elasticsearch index per feature (folder, note, task) with its own migration support; `db/elasticsearchSync` + `domain/elasticSearchSync` implement DB→ES synchronization.
|
||||||
|
- `clientapi/` — outbound HTTP clients (e.g. OpenStreetMap geocoding).
|
||||||
|
- `security/` — Keycloak/OIDC config and SSL client config (`SSLClientConfig` pins outbound HTTPS to TLS 1.2; trust uses the JVM default cacerts — no custom truststore).
|
||||||
|
|
||||||
|
### Frontend layers (`frontend/src/app`)
|
||||||
|
|
||||||
|
- `api/` — `boxit.api.ts` + DTOs + HTTP error interceptor (mirror of the backend REST API).
|
||||||
|
- `business/` — services/models per feature (item, attachment, location, user), notification and global error handling.
|
||||||
|
- `gui/` — components (header, page, search, filters, item, location, responsive helpers).
|
||||||
|
- `auth/` — `auth.service.ts` (Keycloak-based login).
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
Backend integration tests (`BoxitIntegrationTest`) use Testcontainers. For container reuse, add `testcontainers.reuse.enabled=true` to `~/.testcontainers.properties`.
|
||||||
|
|
||||||
|
### CI
|
||||||
|
|
||||||
|
Gitea Actions (`.gitea/workflows/`) builds on push to `main`: Gradle build with a Postgres service container, then builds/pushes Docker images (`*-backend`, `*-frontend`) to `gitea.tail276105.ts.net` and triggers a prod compose redeploy. `bitbucket-pipelines.yml` and `.space.kts` are legacy.
|
||||||
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
FROM amazoncorretto:17
|
FROM eclipse-temurin:17
|
||||||
ADD build/libs/backend-version.jar boxit-backend.jar
|
ADD build/libs/backend-version.jar boxit-backend.jar
|
||||||
ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom", "-Xmx1024m","-jar","/boxit-backend.jar"]
|
ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom", "-Xmx1024m","-jar","/boxit-backend.jar"]
|
||||||
@@ -33,7 +33,7 @@ dependencies {
|
|||||||
implementation("org.springframework.boot:spring-boot-starter-security")
|
implementation("org.springframework.boot:spring-boot-starter-security")
|
||||||
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
|
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
|
||||||
implementation("org.springframework.boot:spring-boot-starter-actuator")
|
implementation("org.springframework.boot:spring-boot-starter-actuator")
|
||||||
implementation("org.keycloak:keycloak-spring-boot-starter:16.0.0")
|
implementation("org.springframework.boot:spring-boot-starter-oauth2-resource-server")
|
||||||
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
|
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
|
||||||
implementation("io.micrometer:micrometer-core")
|
implementation("io.micrometer:micrometer-core")
|
||||||
implementation("io.micrometer:micrometer-registry-prometheus")
|
implementation("io.micrometer:micrometer-registry-prometheus")
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import java.io.OutputStreamWriter
|
|||||||
import java.net.HttpURLConnection
|
import java.net.HttpURLConnection
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
|
|
||||||
abstract class AbstractRestApi protected constructor(private val metrics: OutGoingRestMetrics, private val externalSystemName: String, private val baseUrl: String) : AbstractRequestResponseLogger(AbstractRequestResponseLogger.Channel.OUTGOING) {
|
abstract class AbstractRestApi protected constructor(private val metrics: OutGoingRestMetrics, private val externalSystemName: String, private val baseUrl: String, private val additionalHeaders: Map<String, String> = emptyMap()) : AbstractRequestResponseLogger(AbstractRequestResponseLogger.Channel.OUTGOING) {
|
||||||
|
|
||||||
protected fun httpGet(uri: String, vararg parameters: Parameter): String {
|
protected fun httpGet(uri: String, vararg parameters: Parameter): String {
|
||||||
return call(
|
return call(
|
||||||
@@ -34,18 +34,17 @@ abstract class AbstractRestApi protected constructor(private val metrics: OutGoi
|
|||||||
val con = url.openConnection() as HttpURLConnection
|
val con = url.openConnection() as HttpURLConnection
|
||||||
con.requestMethod = method.name
|
con.requestMethod = method.name
|
||||||
con.setRequestProperty(REQUEST_PROPERTY_ACCEPT, APPLICATION_JSON)
|
con.setRequestProperty(REQUEST_PROPERTY_ACCEPT, APPLICATION_JSON)
|
||||||
|
additionalHeaders.forEach { (key, value) -> con.setRequestProperty(key, value) }
|
||||||
if (body != null) {
|
if (body != null) {
|
||||||
val writer = OutputStreamWriter(con.outputStream)
|
val writer = OutputStreamWriter(con.outputStream)
|
||||||
writer.write(body)
|
writer.write(body)
|
||||||
writer.flush()
|
writer.flush()
|
||||||
}
|
}
|
||||||
status = HttpStatus.valueOf(con.responseCode)
|
status = HttpStatus.valueOf(con.responseCode)
|
||||||
val reader = BufferedReader(InputStreamReader(con.inputStream))
|
// Read the whole response to EOF. Do NOT loop on reader.ready(): for a network
|
||||||
val sb = StringBuilder()
|
// stream the body may not be buffered yet right after responseCode, so ready()
|
||||||
while (reader.ready()) {
|
// can return false immediately and yield an empty/truncated body.
|
||||||
sb.append(reader.readLine())
|
body = BufferedReader(InputStreamReader(con.inputStream, Charsets.UTF_8)).use { it.readText() }
|
||||||
}
|
|
||||||
body = sb.toString()
|
|
||||||
handleResponse(method, url, uri, stopwatch, con, body)
|
handleResponse(method, url, uri, stopwatch, con, body)
|
||||||
return body
|
return body
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
|
|||||||
+11
-4
@@ -10,13 +10,17 @@ import ch.boxi.boxit.infrastructur.monitoring.http.OutGoingRestMetrics
|
|||||||
import com.google.gson.GsonBuilder
|
import com.google.gson.GsonBuilder
|
||||||
import com.google.gson.reflect.TypeToken
|
import com.google.gson.reflect.TypeToken
|
||||||
import org.springframework.beans.factory.annotation.Autowired
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
|
import org.springframework.beans.factory.annotation.Value
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
import java.io.UnsupportedEncodingException
|
import java.io.UnsupportedEncodingException
|
||||||
import java.net.URLEncoder
|
import java.net.URLEncoder
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
class NominatimApi @Autowired
|
class NominatimApi @Autowired
|
||||||
constructor(metrics: OutGoingRestMetrics) : AbstractRestApi(metrics, OSM_NOMINATIM, BASE_URL) {
|
constructor(
|
||||||
|
metrics: OutGoingRestMetrics,
|
||||||
|
@Value("\${boxit.nominatim.contact-email}") contactEmail: String,
|
||||||
|
) : AbstractRestApi(metrics, OSM_NOMINATIM, BASE_URL, mapOf(HEADER_USER_AGENT to userAgent(contactEmail))) {
|
||||||
|
|
||||||
@Throws(BoxitRuntimeException::class)
|
@Throws(BoxitRuntimeException::class)
|
||||||
fun resolveLocation(location: String?): Coordinates? {
|
fun resolveLocation(location: String?): Coordinates? {
|
||||||
@@ -77,8 +81,11 @@ constructor(metrics: OutGoingRestMetrics) : AbstractRestApi(metrics, OSM_NOMINAT
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object : Logger {
|
companion object : Logger {
|
||||||
private val BASE_URL = "https://nominatim.openstreetmap.org"
|
private const val BASE_URL = "https://nominatim.openstreetmap.org"
|
||||||
val OSM_NOMINATIM = "OSM-Nominatim"
|
private const val OSM_NOMINATIM = "OSM-Nominatim"
|
||||||
val SEARCH_URI = "/search?format=json&q={location}"
|
private const val SEARCH_URI = "/search?format=jsonv2&q={location}"
|
||||||
|
private const val HEADER_USER_AGENT = "User-Agent"
|
||||||
|
|
||||||
|
private fun userAgent(contactEmail: String) = "Remsly/0.1 ($contactEmail)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,8 +2,8 @@ package ch.boxi.boxit.domain.user
|
|||||||
|
|
||||||
import ch.boxi.boxit.db.user.UserRepository
|
import ch.boxi.boxit.db.user.UserRepository
|
||||||
import ch.boxi.boxit.exception.BoxitRuntimeException
|
import ch.boxi.boxit.exception.BoxitRuntimeException
|
||||||
import org.keycloak.KeycloakPrincipal
|
|
||||||
import org.springframework.security.core.context.SecurityContextHolder
|
import org.springframework.security.core.context.SecurityContextHolder
|
||||||
|
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
@@ -14,11 +14,9 @@ class UserService(private val userRepository: UserRepository) {
|
|||||||
|
|
||||||
val currentUserOptional: User?
|
val currentUserOptional: User?
|
||||||
get() {
|
get() {
|
||||||
val securityContext = SecurityContextHolder.getContext()
|
val authentication = SecurityContextHolder.getContext().authentication
|
||||||
val authentication = securityContext.authentication
|
return if (authentication is JwtAuthenticationToken) {
|
||||||
return if (authentication != null) {
|
val extId = AuthenticationExtId(authentication.token.subject)
|
||||||
val keycloakPrincipal = authentication.principal as KeycloakPrincipal<*>
|
|
||||||
val extId = AuthenticationExtId(keycloakPrincipal.name)
|
|
||||||
findBy(extId)
|
findBy(extId)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
|
|||||||
@@ -7,14 +7,11 @@ import ch.boxi.boxit.searchindex.migration.data.MigrationOperation
|
|||||||
import ch.boxi.boxit.searchindex.migration.data.MigrationScript
|
import ch.boxi.boxit.searchindex.migration.data.MigrationScript
|
||||||
import ch.boxi.boxit.searchindex.migration.data.Version
|
import ch.boxi.boxit.searchindex.migration.data.Version
|
||||||
import com.google.common.base.Strings
|
import com.google.common.base.Strings
|
||||||
import com.google.common.io.PatternFilenameFilter
|
import org.springframework.core.io.Resource
|
||||||
import org.springframework.core.io.ClassPathResource
|
import org.springframework.core.io.support.PathMatchingResourcePatternResolver
|
||||||
import org.springframework.http.HttpMethod
|
import org.springframework.http.HttpMethod
|
||||||
import java.io.BufferedReader
|
import java.io.BufferedReader
|
||||||
import java.io.File
|
|
||||||
import java.io.FileInputStream
|
|
||||||
import java.io.InputStreamReader
|
import java.io.InputStreamReader
|
||||||
import java.nio.file.Path
|
|
||||||
import java.util.Arrays
|
import java.util.Arrays
|
||||||
import java.util.SortedSet
|
import java.util.SortedSet
|
||||||
import java.util.regex.Pattern
|
import java.util.regex.Pattern
|
||||||
@@ -29,35 +26,35 @@ class MigrationLoader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun loadMigrations(): SortedSet<MigrationScript> {
|
fun loadMigrations(): SortedSet<MigrationScript> {
|
||||||
val filenameFilter = PatternFilenameFilter(FILENAME_PATTERN)
|
val resources = PathMatchingResourcePatternResolver().getResources("classpath*:elasticsearch/*.edql")
|
||||||
val folderResource = ClassPathResource("elasticsearch")
|
return Arrays.stream(resources)
|
||||||
return Arrays.stream(folderResource.file.listFiles(filenameFilter))
|
.filter { resource -> resource.filename?.let { FILENAME_PATTERN.matcher(it).matches() } ?: false }
|
||||||
.map(File::toPath)
|
|
||||||
.map(this::loadMigration)
|
.map(this::loadMigration)
|
||||||
.toSortedSet()
|
.toSortedSet()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadMigration(path: Path): MigrationScript {
|
private fun loadMigration(resource: Resource): MigrationScript {
|
||||||
LOG.debug("found migration file $path")
|
val filename = resource.filename!!
|
||||||
val matcher = FILENAME_PATTERN.matcher(path.fileName.toString())
|
LOG.debug("found migration file $filename")
|
||||||
|
val matcher = FILENAME_PATTERN.matcher(filename)
|
||||||
|
|
||||||
matcher.matches()
|
matcher.matches()
|
||||||
val script = matcher.group(0)
|
val script = matcher.group(0)
|
||||||
val version = Version.parse(matcher.group(1))
|
val version = Version.parse(matcher.group(1))
|
||||||
val description = matcher.group(2)
|
val description = matcher.group(2)
|
||||||
|
|
||||||
val operations = readOperations(path)
|
val operations = readOperations(resource)
|
||||||
|
|
||||||
return MigrationScript(version, description, script, operations.second.value, operations.first)
|
return MigrationScript(version, description, script, operations.second.value, operations.first)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun readOperations(path: Path): Pair<List<MigrationOperation>, Checksum> {
|
private fun readOperations(resource: Resource): Pair<List<MigrationOperation>, Checksum> {
|
||||||
val operations = mutableListOf<MigrationOperation>()
|
val operations = mutableListOf<MigrationOperation>()
|
||||||
var httpMethod: HttpMethod? = null
|
var httpMethod: HttpMethod? = null
|
||||||
var operationPath: String? = null
|
var operationPath: String? = null
|
||||||
val checkSum: Checksum
|
val checkSum: Checksum
|
||||||
var body = StringBuilder()
|
var body = StringBuilder()
|
||||||
CheckedInputStream(FileInputStream(path.toFile()), CRC32()).use { checkedReader ->
|
CheckedInputStream(resource.inputStream, CRC32()).use { checkedReader ->
|
||||||
BufferedReader(InputStreamReader(checkedReader)).use { reader ->
|
BufferedReader(InputStreamReader(checkedReader)).use { reader ->
|
||||||
reader.lines().forEach { line ->
|
reader.lines().forEach { line ->
|
||||||
val subLine = line.trim()
|
val subLine = line.trim()
|
||||||
|
|||||||
@@ -1,68 +0,0 @@
|
|||||||
package ch.boxi.boxit.security
|
|
||||||
|
|
||||||
import ch.boxi.boxit.domain.user.AuthenticationExtId
|
|
||||||
import ch.boxi.boxit.domain.user.UserService
|
|
||||||
import ch.boxi.boxit.infrastructur.logging.Logger
|
|
||||||
import ch.boxi.boxit.infrastructur.logging.LoggerHelper
|
|
||||||
import org.keycloak.KeycloakPrincipal
|
|
||||||
import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationProvider
|
|
||||||
import org.keycloak.adapters.springsecurity.config.KeycloakWebSecurityConfigurerAdapter
|
|
||||||
import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired
|
|
||||||
import org.springframework.context.annotation.Bean
|
|
||||||
import org.springframework.context.annotation.ComponentScan
|
|
||||||
import org.springframework.context.annotation.Configuration
|
|
||||||
import org.springframework.context.annotation.FilterType
|
|
||||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity
|
|
||||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
|
|
||||||
import org.springframework.security.config.http.SessionCreationPolicy
|
|
||||||
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy
|
|
||||||
import javax.mail.internet.InternetAddress
|
|
||||||
|
|
||||||
// @KeycloakConfiguration
|
|
||||||
@Configuration
|
|
||||||
@ComponentScan(
|
|
||||||
basePackages = ["org.keycloak.adapters.springsecurity"],
|
|
||||||
excludeFilters = [ComponentScan.Filter(type = FilterType.REGEX, pattern = arrayOf("org.keycloak.adapters.springsecurity.management.HttpSessionManager"))]
|
|
||||||
)
|
|
||||||
@EnableWebSecurity
|
|
||||||
class KeycloakConfig
|
|
||||||
@Autowired
|
|
||||||
constructor(private val userService: UserService) : KeycloakWebSecurityConfigurerAdapter() {
|
|
||||||
|
|
||||||
companion object : Logger
|
|
||||||
|
|
||||||
override fun sessionAuthenticationStrategy(): SessionAuthenticationStrategy {
|
|
||||||
return SessionAuthenticationStrategy { authentication, _, _ ->
|
|
||||||
if (authentication is KeycloakAuthenticationToken) {
|
|
||||||
val principal = authentication.principal
|
|
||||||
if (principal is KeycloakPrincipal<*>) {
|
|
||||||
val email = principal.keycloakSecurityContext.token.email
|
|
||||||
val extId = AuthenticationExtId(principal.name)
|
|
||||||
val username = principal.keycloakSecurityContext.token.preferredUsername
|
|
||||||
LoggerHelper.user = username
|
|
||||||
userService.getCreateOrUpdate(extId, username, InternetAddress(email))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(Exception::class)
|
|
||||||
override fun configure(http: HttpSecurity) {
|
|
||||||
super.configure(http)
|
|
||||||
http.sessionManagement()
|
|
||||||
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
|
|
||||||
.and()
|
|
||||||
.headers()
|
|
||||||
.and()
|
|
||||||
.csrf().disable()
|
|
||||||
.anonymous().disable()
|
|
||||||
.authorizeRequests()
|
|
||||||
.anyRequest().authenticated()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
fun authenticationProvider(): KeycloakAuthenticationProvider {
|
|
||||||
return KeycloakAuthenticationProvider()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
package ch.boxi.boxit.security
|
|
||||||
|
|
||||||
import org.keycloak.adapters.springboot.KeycloakSpringBootConfigResolver
|
|
||||||
import org.springframework.context.annotation.Configuration
|
|
||||||
|
|
||||||
@Configuration
|
|
||||||
class KeycloakConfigResolver : KeycloakSpringBootConfigResolver()
|
|
||||||
@@ -1,21 +1,16 @@
|
|||||||
package ch.boxi.boxit.security
|
package ch.boxi.boxit.security
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired
|
|
||||||
import org.springframework.context.annotation.Configuration
|
import org.springframework.context.annotation.Configuration
|
||||||
import org.springframework.core.env.Environment
|
|
||||||
import javax.annotation.PostConstruct
|
import javax.annotation.PostConstruct
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
class SSLClientConfig {
|
class SSLClientConfig {
|
||||||
@Autowired
|
|
||||||
private val env: Environment? = null
|
|
||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
private fun configureSSL() {
|
private fun configureSSL() {
|
||||||
|
// Pin outbound HttpsURLConnection to TLS 1.2. Trust uses the JVM's default cacerts (no custom
|
||||||
|
// trust store): all internal services run over plain HTTP, and the only outbound HTTPS targets
|
||||||
|
// are public hosts (Gravatar, OSM) that the built-in CA bundle already trusts.
|
||||||
System.setProperty("https.protocols", "TLSv1.2")
|
System.setProperty("https.protocols", "TLSv1.2")
|
||||||
|
|
||||||
val trustStore = env!!.getProperty("boxit.server.ssl.trust-store")
|
|
||||||
System.setProperty("javax.net.ssl.trustStore", trustStore)
|
|
||||||
System.setProperty("javax.net.ssl.trustStorePassword", env.getProperty("boxit.server.ssl.trust-store-password")!!)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package ch.boxi.boxit.security
|
||||||
|
|
||||||
|
import ch.boxi.boxit.domain.user.UserService
|
||||||
|
import org.springframework.context.annotation.Bean
|
||||||
|
import org.springframework.context.annotation.Configuration
|
||||||
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity
|
||||||
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
|
||||||
|
import org.springframework.security.config.http.SessionCreationPolicy
|
||||||
|
import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationFilter
|
||||||
|
import org.springframework.security.web.SecurityFilterChain
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stateless bearer-only security: every request must carry a Keycloak-issued JWT (forwarded by the
|
||||||
|
* Apache mod_auth_openidc proxy as `Authorization: Bearer ...`). The token is validated against the
|
||||||
|
* signing keys from `spring.security.oauth2.resourceserver.jwt.jwk-set-uri` and its issuer against
|
||||||
|
* `...jwt.issuer-uri` (see application.properties). Replaces the retired Keycloak Spring Boot adapter.
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
@EnableWebSecurity
|
||||||
|
class SecurityConfig(private val userService: UserService) {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
fun filterChain(http: HttpSecurity): SecurityFilterChain {
|
||||||
|
http
|
||||||
|
.sessionManagement { it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) }
|
||||||
|
.csrf { it.disable() }
|
||||||
|
.anonymous { it.disable() }
|
||||||
|
.authorizeRequests { it.anyRequest().authenticated() }
|
||||||
|
.oauth2ResourceServer { it.jwt { } }
|
||||||
|
// Provision/refresh the local user record from the validated token on every request,
|
||||||
|
// mirroring the behaviour of the previous Keycloak SessionAuthenticationStrategy.
|
||||||
|
.addFilterAfter(UserProvisioningFilter(userService), BearerTokenAuthenticationFilter::class.java)
|
||||||
|
return http.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package ch.boxi.boxit.security
|
||||||
|
|
||||||
|
import ch.boxi.boxit.domain.user.AuthenticationExtId
|
||||||
|
import ch.boxi.boxit.domain.user.UserService
|
||||||
|
import ch.boxi.boxit.infrastructur.logging.LoggerHelper
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder
|
||||||
|
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken
|
||||||
|
import org.springframework.web.filter.OncePerRequestFilter
|
||||||
|
import javax.mail.internet.InternetAddress
|
||||||
|
import javax.servlet.FilterChain
|
||||||
|
import javax.servlet.http.HttpServletRequest
|
||||||
|
import javax.servlet.http.HttpServletResponse
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs after the bearer-token authentication filter: for every authenticated request it upserts the
|
||||||
|
* local user record from the validated JWT (subject -> extId, `preferred_username`, `email`) and
|
||||||
|
* stamps the username onto the log context. Not a Spring-managed bean on purpose, so it is only
|
||||||
|
* active inside the security filter chain (where the SecurityContext is already populated).
|
||||||
|
*/
|
||||||
|
class UserProvisioningFilter(private val userService: UserService) : OncePerRequestFilter() {
|
||||||
|
|
||||||
|
override fun doFilterInternal(request: HttpServletRequest, response: HttpServletResponse, filterChain: FilterChain) {
|
||||||
|
val authentication = SecurityContextHolder.getContext().authentication
|
||||||
|
if (authentication is JwtAuthenticationToken) {
|
||||||
|
val jwt = authentication.token
|
||||||
|
val username = jwt.getClaimAsString("preferred_username") ?: jwt.subject
|
||||||
|
LoggerHelper.user = username
|
||||||
|
val email = jwt.getClaimAsString("email")
|
||||||
|
if (email != null) {
|
||||||
|
userService.getCreateOrUpdate(AuthenticationExtId(jwt.subject), username, InternetAddress(email))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
filterChain.doFilter(request, response)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,10 +11,9 @@ boxit.elasticsearch.port=9200
|
|||||||
boxit.elasticsearch.user=elastic
|
boxit.elasticsearch.user=elastic
|
||||||
boxit.elasticsearch.pass=elasticPassword
|
boxit.elasticsearch.pass=elasticPassword
|
||||||
|
|
||||||
spring.jackson.date-format="yyyy-MM-dd HH:mm:ss"
|
boxit.nominatim.contact-email=your.name@example.com
|
||||||
|
|
||||||
boxit.server.ssl.trust-store=src/main/resources/truststore.jks
|
spring.jackson.date-format="yyyy-MM-dd HH:mm:ss"
|
||||||
boxit.server.ssl.trust-store-password=Tester11
|
|
||||||
|
|
||||||
management.endpoint.metrics.enabled=true
|
management.endpoint.metrics.enabled=true
|
||||||
management.endpoints.web.exposure.include=*
|
management.endpoints.web.exposure.include=*
|
||||||
@@ -23,10 +22,9 @@ management.metrics.export.prometheus.enabled=true
|
|||||||
|
|
||||||
logging.level.root=INFO
|
logging.level.root=INFO
|
||||||
|
|
||||||
keycloak.realm=boxit
|
# Bearer-only JWT validation via Spring Security OAuth2 resource server.
|
||||||
keycloak.bearer-only=true
|
# issuer-uri must match the "iss" claim in the tokens your Keycloak issues (adjust host/realm).
|
||||||
#keycloak.auth-server-url=https://id.boxi.ch/auth
|
# jwk-set-uri is where the signing keys are fetched from - reachable from the backend.
|
||||||
keycloak.auth-server-url=http://kubernetes.docker.internal:8081
|
spring.security.oauth2.resourceserver.jwt.issuer-uri=http://kubernetes.docker.internal:8081/realms/boxit
|
||||||
#keycloak.ssl-required=external
|
spring.security.oauth2.resourceserver.jwt.jwk-set-uri=http://kubernetes.docker.internal:8081/realms/boxit/protocol/openid-connect/certs
|
||||||
keycloak.resource=localdev-server
|
|
||||||
|
|
||||||
Binary file not shown.
@@ -10,11 +10,9 @@ boxit.elasticsearch.port=9200
|
|||||||
boxit.elasticsearch.user=elastic
|
boxit.elasticsearch.user=elastic
|
||||||
boxit.elasticsearch.pass=elasticPassword
|
boxit.elasticsearch.pass=elasticPassword
|
||||||
|
|
||||||
spring.jackson.date-format="yyyy.MM.dd HH:mm:ss"
|
boxit.nominatim.contact-email=test@example.com
|
||||||
|
|
||||||
#boxit.server.ssl.trust-store=truststore
|
spring.jackson.date-format="yyyy.MM.dd HH:mm:ss"
|
||||||
boxit.server.ssl.trust-store=src/main/resources/truststore.jks
|
|
||||||
boxit.server.ssl.trust-store-password=Tester11
|
|
||||||
|
|
||||||
management.endpoint.metrics.enabled=true
|
management.endpoint.metrics.enabled=true
|
||||||
management.endpoints.web.exposure.include=*
|
management.endpoints.web.exposure.include=*
|
||||||
@@ -23,9 +21,8 @@ management.metrics.export.prometheus.enabled=true
|
|||||||
|
|
||||||
logging.level.root=INFO
|
logging.level.root=INFO
|
||||||
|
|
||||||
keycloak.realm=boxit
|
# Bearer-only JWT validation via Spring Security OAuth2 resource server. The decoder is lazy
|
||||||
keycloak.bearer-only=true
|
# (no network call at context startup), so these dummy URLs are enough for the context to load;
|
||||||
keycloak.auth-server-url=https://id.boxi.ch/auth
|
# the tests exercise the service layer directly and never traverse the security filter chain.
|
||||||
keycloak.ssl-required=external
|
spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8081/realms/boxit
|
||||||
keycloak.resource=localdev-server
|
spring.security.oauth2.resourceserver.jwt.jwk-set-uri=http://localhost:8081/realms/boxit/protocol/openid-connect/certs
|
||||||
keycloak.confidential-port=0
|
|
||||||
@@ -147,7 +147,7 @@ IncludeOptional mods-enabled/*.load
|
|||||||
IncludeOptional mods-enabled/*.conf
|
IncludeOptional mods-enabled/*.conf
|
||||||
|
|
||||||
# Include list of ports to listen on
|
# Include list of ports to listen on
|
||||||
Include ports.conf
|
# Include ports.conf
|
||||||
|
|
||||||
|
|
||||||
# Sets the default security model of the Apache2 HTTPD server. It does
|
# Sets the default security model of the Apache2 HTTPD server. It does
|
||||||
@@ -237,7 +237,7 @@ LimitRequestFieldSize 32760
|
|||||||
|
|
||||||
ServerName ${SERVER_NAME}
|
ServerName ${SERVER_NAME}
|
||||||
|
|
||||||
#Listen 80
|
Listen 80
|
||||||
<VirtualHost *:80>
|
<VirtualHost *:80>
|
||||||
DocumentRoot "/var/www/html/app"
|
DocumentRoot "/var/www/html/app"
|
||||||
LogLevel ${APACHE_DEBUG_LEVEL}
|
LogLevel ${APACHE_DEBUG_LEVEL}
|
||||||
@@ -251,7 +251,7 @@ ServerName ${SERVER_NAME}
|
|||||||
form-action 'self'; \
|
form-action 'self'; \
|
||||||
frame-ancestors 'none'; \
|
frame-ancestors 'none'; \
|
||||||
img-src 'self' api.mapbox.com data:; \
|
img-src 'self' api.mapbox.com data:; \
|
||||||
#upgrade-insecure-requests" "expr=%{CONTENT_TYPE} =~ m#text\/(html|javascript)|application\/pdf|xml#i"
|
upgrade-insecure-requests" "expr=%{CONTENT_TYPE} =~ m#text\/(html|javascript)|application\/pdf|xml#i"
|
||||||
Header always append X-Frame-Options SAMEORIGIN
|
Header always append X-Frame-Options SAMEORIGIN
|
||||||
Header always set X-Content-Type-Options nosniff
|
Header always set X-Content-Type-Options nosniff
|
||||||
Header always set Referrer-Policy "same-origin"
|
Header always set Referrer-Policy "same-origin"
|
||||||
@@ -286,7 +286,6 @@ ServerName ${SERVER_NAME}
|
|||||||
|
|
||||||
OIDCRedirectURI ${OIDC_REDIRECT_URI}
|
OIDCRedirectURI ${OIDC_REDIRECT_URI}
|
||||||
OIDCCryptoPassphrase ${OIDC_CRYPTO_PASSPHRASE}
|
OIDCCryptoPassphrase ${OIDC_CRYPTO_PASSPHRASE}
|
||||||
OIDCInfoHook ${OIDC_INFO_HOOK}
|
|
||||||
OIDCDefaultLoggedOutURL ${KC_ADDR}/realms/${KC_REALM}/protocol/openid-connect/logout?redirect_uri=http%3A%2F%2Fkubernetes.docker.internal%3A8800
|
OIDCDefaultLoggedOutURL ${KC_ADDR}/realms/${KC_REALM}/protocol/openid-connect/logout?redirect_uri=http%3A%2F%2Fkubernetes.docker.internal%3A8800
|
||||||
|
|
||||||
# maps the prefered_username claim to the REMOTE_USER environment variable
|
# maps the prefered_username claim to the REMOTE_USER environment variable
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ services:
|
|||||||
# # http://localhost:8081/auth/realms/boxit/account/
|
# # http://localhost:8081/auth/realms/boxit/account/
|
||||||
# # http://localhost:8081/auth/realms/boxit/login-actions/authenticate?execution=ccb6603c-4536-4744-95ef-aa2ab2bc7a32&client_id=account&tab_id=pNMgGgYjRNA
|
# # http://localhost:8081/auth/realms/boxit/login-actions/authenticate?execution=ccb6603c-4536-4744-95ef-aa2ab2bc7a32&client_id=account&tab_id=pNMgGgYjRNA
|
||||||
keycloak:
|
keycloak:
|
||||||
image: boxi.registry.jetbrains.space/p/boxit/containers/boxit-keycloak:0.0.1
|
image: gitea.tail276105.ts.net/remsly/keycloak:4
|
||||||
networks:
|
networks:
|
||||||
boxitNet:
|
boxitNet:
|
||||||
ipv4_address: 10.7.0.20
|
ipv4_address: 10.7.0.20
|
||||||
@@ -57,7 +57,7 @@ services:
|
|||||||
- postgres
|
- postgres
|
||||||
|
|
||||||
apache-oidc:
|
apache-oidc:
|
||||||
image: boxi.registry.jetbrains.space/p/boxit/containers/boxit-apache-oidc:0.0.3
|
image: gitea.tail276105.ts.net/remsly/apacheoidc:3
|
||||||
restart: always
|
restart: always
|
||||||
networks:
|
networks:
|
||||||
boxitNet:
|
boxitNet:
|
||||||
@@ -83,7 +83,7 @@ services:
|
|||||||
- keycloak
|
- keycloak
|
||||||
|
|
||||||
elasticsearch:
|
elasticsearch:
|
||||||
image: boxi.registry.jetbrains.space/p/boxit/containers/boxit-elasticsearch:0.0.7
|
image: gitea.tail276105.ts.net/remsly/elasticsearch:3
|
||||||
restart: always
|
restart: always
|
||||||
volumes:
|
volumes:
|
||||||
- elasticsearch:/usr/share/elasticsearch/data
|
- elasticsearch:/usr/share/elasticsearch/data
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# Template for deploy/prod/.env
|
||||||
|
# Copy to .env and fill in the values: cp .env-template .env
|
||||||
|
# The .env file itself is git-ignored (it holds secrets).
|
||||||
|
|
||||||
|
# --- Database / Keycloak passwords ---
|
||||||
|
REMSLY_PSQL_PASSWORD=
|
||||||
|
REMSLY_PSQL_ADMIN_PASSWORD=
|
||||||
|
KC_PSQL_PASSWORD=
|
||||||
|
KC_ADMIN_PASSWORD=
|
||||||
|
|
||||||
|
# --- Tailscale ---
|
||||||
|
# Reusable, tag:server pre-authorized auth key from the Tailscale admin console.
|
||||||
|
TS_AUTHKEY=
|
||||||
|
TS_DNS_NAME=
|
||||||
|
|
||||||
|
# --- Apache OIDC (mod_auth_openidc) ---
|
||||||
|
OIDC_CRYPTO_PASSPHRASE=
|
||||||
|
OIDC_CLIENT_SECRET=
|
||||||
|
|
||||||
|
# --- Elasticsearch ---
|
||||||
|
ELASTIC_PASSWORD=
|
||||||
|
|
||||||
|
# --- Backend ---
|
||||||
|
# Keycloak client id (resource) the bearer-only backend expects.
|
||||||
|
KEYCLOAK_RESOURCE=
|
||||||
|
|
||||||
|
# --- Gitea registry (used by deploy.sh to download config.tar.gz) ---
|
||||||
|
# User + access token with package read scope for gitea.tail276105.ts.net.
|
||||||
|
GITEA_USER=
|
||||||
|
GITEA_TOKEN=
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
A0
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# Production config for the backend, mounted into the container and activated via
|
||||||
|
# SPRING_CONFIG_ADDITIONAL_LOCATION (see docker-compose.yml). Secrets are injected
|
||||||
|
# from the environment (.env) through ${...} placeholders, so this file is safe to commit.
|
||||||
|
spring.profiles.active=PROD
|
||||||
|
|
||||||
|
server.port=5050
|
||||||
|
|
||||||
|
# Postgres runs as service "postgres" on remslyNet; db/user "remsly" (see POSTGRES_MULTIPLE_DATABASES).
|
||||||
|
spring.datasource.url=jdbc:postgresql://postgres:5432/remsly
|
||||||
|
spring.datasource.username=remsly
|
||||||
|
spring.datasource.password=${REMSLY_PSQL_PASSWORD}
|
||||||
|
|
||||||
|
# Elasticsearch runs as service "elasticsearch" on remslyNet, plain HTTP on 9200.
|
||||||
|
boxit.elasticsearch.host=elasticsearch
|
||||||
|
boxit.elasticsearch.port=9200
|
||||||
|
boxit.elasticsearch.user=elastic
|
||||||
|
boxit.elasticsearch.pass=${ELASTIC_PASSWORD}
|
||||||
|
|
||||||
|
boxit.nominatim.contact-email=remsly@boxi.ch
|
||||||
|
|
||||||
|
spring.jackson.date-format="yyyy-MM-dd HH:mm:ss"
|
||||||
|
|
||||||
|
management.endpoint.metrics.enabled=true
|
||||||
|
management.endpoints.web.exposure.include=*
|
||||||
|
management.endpoint.prometheus.enabled=true
|
||||||
|
management.metrics.export.prometheus.enabled=true
|
||||||
|
|
||||||
|
logging.level.root=INFO
|
||||||
|
|
||||||
|
keycloak.realm=Remsly
|
||||||
|
keycloak.bearer-only=true
|
||||||
|
keycloak.auth-server-url=https://remsly-id.tail276105.ts.net
|
||||||
|
keycloak.resource=${KEYCLOAK_RESOURCE}
|
||||||
|
|
||||||
|
# Bearer-only JWT validation via Spring Security OAuth2 resource server.
|
||||||
|
# issuer-uri : compared against the token's "iss" claim (the public https hostname, set by
|
||||||
|
# KC_HOSTNAME) - a pure string check, no network call.
|
||||||
|
# jwk-set-uri : where the signing keys are fetched. The https tailnet URL is NOT reachable from
|
||||||
|
# this container (tailscale serve binds only the tailnet IP), so pull the JWKS
|
||||||
|
# directly over the bridge on plain http :8080. Keycloak serves the same keys there
|
||||||
|
# regardless of the hostname in the request.
|
||||||
|
spring.security.oauth2.resourceserver.jwt.issuer-uri=https://remsly-id.tail276105.ts.net/realms/Remsly
|
||||||
|
spring.security.oauth2.resourceserver.jwt.jwk-set-uri=http://remsly-id.tail276105.ts.net:8080/realms/Remsly/protocol/openid-connect/certs
|
||||||
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
*.pem
|
|
||||||
-1
@@ -1 +0,0 @@
|
|||||||
{"creation_dt": "2023-03-24T14:43:02Z", "creation_host": "20e2283fb076"}
|
|
||||||
-1
@@ -1 +0,0 @@
|
|||||||
{"n": "u6an4MsyALnC0Ik2b8tcVpikTwzWgtASLGEUg7wEs7_piMKI7wNwroRbCesHujhJu2Ha_hDMrJmCEvT0T2fO4JzoAt1bLE3zWM4sXnWEpTXCrWxayjXbGTblrJcTIQPb7cbiTvyoAQvcThApZcvQEBkKZrlOp3aB62thM2zSI6XFVcbmF3CRCVJdIYN2K3ioQjkkxiAFwQ0EUNWtHzaDvjwFBvwAD2iDDqZh0mHYWWnhwV0okuJ3FBX8fVUr2oskq5cHhc8t8r8l1Iob9lEcgwkRJE93hQ1CMxYT4RX_rbJtyIFbixVkxM99x16nlyJnAWlPzEZrTz_fdcdFeQHPrw", "e": "AQAB", "d": "F7GUUvL1LlhGO_SHVhhUYW7GmGCjmULSeJ4NbBDKezPJNgZyKdPCX5bAtADkKeQ_kxRpBVlzrnt3jBM34erWmU_Wj74JbA-voU_vFEI8bGvcV-9ti0i_kHB5EZDFzo5hL6XEH3sRb4hi2cx6narda5rt0EinoMd64hHd5RNheTSWsVcSbBwzgDqKNB-OTr2vGzK3O3ZmY-I6IE5nVTf1tLkkI3GlPwKOSix1nDFtwHvKV0QTazsBop9MkMDwrBNVYJB_0G6AEtC8RXn9z4FIBZc9BAk4G8bygATwixu3WfUuM5LI3qavwILmQVFzmBfojHHWYf-eQBXf-kwR4KeX2Q", "p": "7MgYLilx05H3tvpy0M5ty2R6i3cE9vzl-eiNPM_rBF-eu-cAjkCtLViKg6VTdr0I8qV4B-NCshPpnF50cuFupF6jTgWZJeAfiOoEWQC6mqNxngX4N98mxDm7lf_3QqRsdhh2cS_5IOJ4cpaJ5WlX89WZvpfAKbCISQ_nejSypQM", "q": "yuG2pUTwPi4QazqX6w3oMAb9C-R6kDoYMNhRm2qCkSMQ9jJlaW80sIypYNzeD2JbF6pAjhF2mN2ehorMwVs97flNrSjGTdMts63JMcI6-w5MOWHmI938knxrBn5ku88QsJpjMdY4f25Oynb5LG3YSiNqQVztc_aC3qElrJ4CvOU", "dp": "PbwBye9WVSImotdzngtkRvZgmQIkEj1nvJusT2GqVc6UdUtKMsycGkmOe7pcI4Yv0gvgWrYfCoz0iKd71wP9s-9Uxp_hCSYBMtJI5eexv2XD1r6Zs_-DfP_Mb9R35DE4ciX4o5QtcSx62O108JJhsxpK5o1sNbJoJhx2br9UoH8", "dq": "TBt6UetXmGvdWuxjGxlfdczZVoHCfKJeesxbaMr-6gVWkpT7OB8wi95ol96jKvBBrdH_X4wrilLw6hXdk9dvlvtD6WZ3ZbCH33SdGh5707xcWdZg6CmZWzZzS79UeKPH5gg0l_h9v0gZJJiXBtS6I62_aX-70Bm0pGGdaXDpJAU", "qi": "L4HAI6WvpBjiwvGGH9IAoxsxqlnCton8uUUf2IQwpZMKe-d9LXPsOdWsyaZneCnc2C_AQsgLkpZIWd47koybwch4vTkbmLrNwtaaazVk53Axdg3ksFT-uWad4YTiqCH-hIt_2xXxvensMqHwRvIk44MHk0kq485nPfGx4E9zvDE", "kty": "RSA"}
|
|
||||||
-1
@@ -1 +0,0 @@
|
|||||||
{"body": {}, "uri": "https://acme-staging-v02.api.letsencrypt.org/acme/acct/94925864"}
|
|
||||||
-1
@@ -1 +0,0 @@
|
|||||||
{"creation_dt": "2023-03-24T14:44:14Z", "creation_host": "ed2dd2431723"}
|
|
||||||
-1
@@ -1 +0,0 @@
|
|||||||
{"n": "5JoKajoOVE7y5Bcchd4KPlVarooN-XLHtEbFRy0zrFG4mAY_Qf-H1eZkmHvPQwv-tDEPFRlabrxXqC3uWoy9uOmqXea7rbnOqQHYRSAhIiW8I0JbuzGHga-ZwujShQDj85eimjBXRKhjD6r3zRElchTWXyeT0_dRrOZ099zAE0qNCnVRxel6IXdFEKz9YrBvIqvEzJC2DBWwF0W6ZBlpiM8RXq1guBU22k0XkoDHE8d_LL6AMrhtXSCPwiL9YOxiyvSTgLHciy4c-WP1-mypy65oK5YLCiVagX4kM8qbxW_0nmFtdYCdhhEBrHktxofHUaps4cPGK_XDkbz5mti9vQ", "e": "AQAB", "d": "D53KSllu8yl1hElj5G03q-xuxm-d5sWFiPYwsd0Po5ztjE0Y8yw-nkkf5B7JGN5v5uVXD2zs9ZWeM1n3522n5ELqJYIGniaMTJVYrdg1Cz0irhHxbEdE54IUcfEvuHBIIjLgo8HbpFAQfFiElxitwy5TOqrceWVyyenwYaJN6MNU6h5_TReay7Smye1NQYnv3E63jDX3Od_rQyrLBV0UE3Y7BOPLGmqHTeeEaDo8zut0HgXIS0ecRrfTt8RkDpv_Xc9AAVzZaPXLNo4Kxo7YWCpoi32oLwUhsBELS6G3i2rQxPTveMnkeU0-1qo6hfoGZFMKpAivl4QX86_D_41tzQ", "p": "_dmRjJn2dVbPNOgvXeencRykjCHk5aRDYDCJ6BKYEsXpOdOZCGH2HlX-43Loh1w5qHcCXlSCX5T1GsOv-EAye2T9Eg-xfpkPHUJ9JlKRk1Oiy6FjFfBr_ObUzK_rC-WyHzcIaU50hyzwoKDhICRdbQvfJnOp02tE1Hlxm15qO28", "q": "5om5xnQAv6RRzkCdsO4Sk2yFXwoTvpL8KS9LG1RqcPoAvy1ey5oEPJLIUyFUmf8-hr5RW2OBgEwGCR3HQgaXXiyxeCSdoM0roEJaEcerk2i8JW_w_S52evvigJb4mOajzjY--WsSa12LpJTTKvSlf-9eWNaNeb7vv9GT1atWs5M", "dp": "lL_gFTbG_909kDAaSXmMWbDCD0dxPMuzm3L17KKwsBd-4h7HBu14bjBQKvHTpjX5p2kNXz9AIkhEFEnlHKBptHrp_KJrjNadz7R9Z25s1Zrlw1Mvn9SBQvH0cnkHT7JqXPY_nI2XpPpFiqqbIN2OTW7udTbDphque58qXjPGJks", "dq": "hP4o1Mpw8B1sL-7oyXvd7XwqSJR4pXUH0P5-doqxSPGAYslaqVv1W7hVpldRBfHQcUzzhCQYPhodVT7_hyBkTCi-TI_a-zvBEheU_DLS0RwXhgVY4cXfBb2vgf0V9qb_EmQK2johixyRBWXxmBNAMli1dcx4GybCCkdt4FKSls8", "qi": "Y_CRKqA-X8fnYAU9WIBVB2mH08m5y_3UhcmzJpcDAeaPygrh4hItYKdBSk9R9IBRiME84nIDW2Xor9c_GPpXcEItsHAHYHiF12hZtZHcEXyp9fFsJzprTWhXr1JRohlsPzCK6pzcB1Hk2ZwUvc0G4wkcaeOfo9LTVQj5eWRlnWQ", "kty": "RSA"}
|
|
||||||
-1
@@ -1 +0,0 @@
|
|||||||
{"body": {}, "uri": "https://acme-v02.api.letsencrypt.org/acme/acct/1025448747"}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
This directory contains your keys and certificates.
|
|
||||||
|
|
||||||
`[cert name]/privkey.pem` : the private key for your certificate.
|
|
||||||
`[cert name]/fullchain.pem`: the certificate file used in most server software.
|
|
||||||
`[cert name]/chain.pem` : used for OCSP stapling in Nginx >=1.3.7.
|
|
||||||
`[cert name]/cert.pem` : will break many server configurations, and should not be used
|
|
||||||
without reading further documentation (see link below).
|
|
||||||
|
|
||||||
WARNING: DO NOT MOVE OR RENAME THESE FILES!
|
|
||||||
Certbot expects these files to remain in this location in order
|
|
||||||
to function properly!
|
|
||||||
|
|
||||||
We recommend not moving these files. For more information, see the Certbot
|
|
||||||
User Guide at https://certbot.eff.org/docs/using.html#where-are-my-certificates.
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
This directory contains your keys and certificates.
|
|
||||||
|
|
||||||
`privkey.pem` : the private key for your certificate.
|
|
||||||
`fullchain.pem`: the certificate file used in most server software.
|
|
||||||
`chain.pem` : used for OCSP stapling in Nginx >=1.3.7.
|
|
||||||
`cert.pem` : will break many server configurations, and should not be used
|
|
||||||
without reading further documentation (see link below).
|
|
||||||
|
|
||||||
WARNING: DO NOT MOVE OR RENAME THESE FILES!
|
|
||||||
Certbot expects these files to remain in this location in order
|
|
||||||
to function properly!
|
|
||||||
|
|
||||||
We recommend not moving these files. For more information, see the Certbot
|
|
||||||
User Guide at https://certbot.eff.org/docs/using.html#where-are-my-certificates.
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
# renew_before_expiry = 30 days
|
|
||||||
version = 0.40.1
|
|
||||||
archive_dir = /etc/letsencrypt/archive/app.boxi.ch
|
|
||||||
cert = /etc/letsencrypt/live/app.boxi.ch/cert.pem
|
|
||||||
privkey = /etc/letsencrypt/live/app.boxi.ch/privkey.pem
|
|
||||||
chain = /etc/letsencrypt/live/app.boxi.ch/chain.pem
|
|
||||||
fullchain = /etc/letsencrypt/live/app.boxi.ch/fullchain.pem
|
|
||||||
|
|
||||||
# Options used in the renewal process
|
|
||||||
[renewalparams]
|
|
||||||
account = ea5cf7614a600d23b821c0af24b17a1d
|
|
||||||
authenticator = standalone
|
|
||||||
server = https://acme-v02.api.letsencrypt.org/directory
|
|
||||||
@@ -1,132 +0,0 @@
|
|||||||
<VirtualHost *:80>
|
|
||||||
DocumentRoot "/var/www/app"
|
|
||||||
ServerName app.boxi.ch
|
|
||||||
# ServerAlias www.${server_name}
|
|
||||||
|
|
||||||
ErrorLog /var/log/apache2/error.log
|
|
||||||
CustomLog /var/log/apache2/access.log combined
|
|
||||||
LogLevel debug
|
|
||||||
|
|
||||||
<Directory />
|
|
||||||
AllowOverride none
|
|
||||||
Options -Indexes -Includes
|
|
||||||
Require all granted
|
|
||||||
<LimitExcept GET POST HEAD>
|
|
||||||
deny from all
|
|
||||||
</LimitExcept>
|
|
||||||
</Directory>
|
|
||||||
|
|
||||||
<Directory "/var/www/app">
|
|
||||||
AllowOverride None
|
|
||||||
Options -Indexes -Includes
|
|
||||||
Require all granted
|
|
||||||
</Directory>
|
|
||||||
|
|
||||||
<Location /status >
|
|
||||||
SetHandler server-status
|
|
||||||
</Location>
|
|
||||||
|
|
||||||
<Location / >
|
|
||||||
Redirect / https://app.boxi.ch/
|
|
||||||
</Location>
|
|
||||||
</VirtualHost>
|
|
||||||
|
|
||||||
<IfModule mod_ssl.c>
|
|
||||||
<VirtualHost *:443>
|
|
||||||
DocumentRoot "/var/www/app"
|
|
||||||
ServerName app.boxi.ch
|
|
||||||
#ServerAlias www.${server_name}
|
|
||||||
|
|
||||||
LogLevel info
|
|
||||||
|
|
||||||
Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains"
|
|
||||||
Header always set Content-Security-Policy "default-src 'self' ; \
|
|
||||||
script-src 'self' 'unsafe-inline' 'unsafe-eval'; \
|
|
||||||
style-src 'self' use.fontawesome.com 'unsafe-inline'; \
|
|
||||||
font-src 'self' data: use.fontawesome.com; \
|
|
||||||
base-uri 'self'; \
|
|
||||||
form-action 'self'; \
|
|
||||||
frame-ancestors 'none'; \
|
|
||||||
img-src 'self' data:; \
|
|
||||||
upgrade-insecure-requests" "expr=%{CONTENT_TYPE} =~ m#text\/(html|javascript)|application\/pdf|xml#i"
|
|
||||||
Header always append X-Frame-Options SAMEORIGIN
|
|
||||||
Header always set X-Content-Type-Options nosniff
|
|
||||||
Header always set Referrer-Policy "same-origin"
|
|
||||||
# this feature is not yet used and must be extended if needed, currently only a sample config
|
|
||||||
Header always set Feature-Policy "payment 'none';"
|
|
||||||
|
|
||||||
<Directory />
|
|
||||||
AllowOverride none
|
|
||||||
Options -Indexes -Includes
|
|
||||||
Require all granted
|
|
||||||
<LimitExcept GET POST HEAD>
|
|
||||||
deny from all
|
|
||||||
</LimitExcept>
|
|
||||||
</Directory>
|
|
||||||
|
|
||||||
<Directory "/var/www/app">
|
|
||||||
AllowOverride None
|
|
||||||
Options -Indexes -Includes
|
|
||||||
Require all granted
|
|
||||||
</Directory>
|
|
||||||
|
|
||||||
<Location /status >
|
|
||||||
SetHandler server-status
|
|
||||||
</Location>
|
|
||||||
|
|
||||||
RequestHeader set X-Forwarded-User expr=%{REMOTE_USER}
|
|
||||||
RequestHeader set Authorization "Bearer %{oidc_access_token}e"
|
|
||||||
Header set User-Details "%{REMOTE_USER}e %{oidc_claim_email}e"
|
|
||||||
|
|
||||||
OIDCProviderMetadataURL ${KC_ADDR}/realms/${KC_REALM}/.well-known/openid-configuration
|
|
||||||
OIDCClientID ${OIDC_CLIENT_ID}
|
|
||||||
OIDCClientSecret ${OIDC_CLIENT_SECRET}
|
|
||||||
OIDCRefreshAccessTokenBeforeExpiry 10
|
|
||||||
OIDCRedirectURI ${OIDC_REDIRECT_URI}
|
|
||||||
OIDCCryptoPassphrase ${OIDC_CRYPTO_PASSPHRASE}
|
|
||||||
OIDCInfoHook ${OIDC_INFO_HOOK}
|
|
||||||
OIDCDefaultLoggedOutURL ${KC_ADDR}/realms/${KC_REALM}/protocol/openid-connect/logout?redirect_uri=https%3A%2F%2Fapp.boxi.ch
|
|
||||||
OIDCRemoteUserClaim preferred_username
|
|
||||||
OIDCStateMaxNumberOfCookies 3 true
|
|
||||||
|
|
||||||
<Location /status >
|
|
||||||
SetHandler server-status
|
|
||||||
</Location>
|
|
||||||
|
|
||||||
<Location />
|
|
||||||
AuthType openid-connect
|
|
||||||
Require valid-user
|
|
||||||
</Location>
|
|
||||||
|
|
||||||
# If the browser is allowed to cache index.html, it starts loading assets/scripts/images before apache had a chance
|
|
||||||
# to check whether or not the user is even authenticated, leading to many failed requests.
|
|
||||||
# Disabling cache for index.html forces the browser to fetch it first, and therefore ensures that the full
|
|
||||||
# authentication mechanic is being run.
|
|
||||||
# See also https://github.com/zmartzone/mod_auth_openidc/issues/113
|
|
||||||
<filesMatch "index.html">
|
|
||||||
FileETag None
|
|
||||||
Header unset ETag
|
|
||||||
Header set Cache-Control "max-age=0, no-cache, no-store, must-revalidate"
|
|
||||||
Header set Pragma "no-cache"
|
|
||||||
Header set Expires "Mon, 21 Mar 1983 14:10:00 UTC"
|
|
||||||
</filesMatch>
|
|
||||||
|
|
||||||
#ProxyPass /ws ${FRONTEND_WS_URL}:${FRONTEND_PORT}/ws retry=0 timeout=86400
|
|
||||||
#ProxyPassReverse /ws ws://${FRONTEND_URL}:${FRONTEND_PORT}/ws
|
|
||||||
|
|
||||||
#ProxyPass /api/ ${BACKEND_URL}:${BACKEND_PORT}/api/ retry=0
|
|
||||||
#ProxyPassReverse /api/ ${BACKEND_URL}:${BACKEND_PORT}/api/
|
|
||||||
|
|
||||||
#ProxyPass / ${FRONTEND_URL}:${FRONTEND_PORT}/ retry=0
|
|
||||||
#ProxyPassReverse / ${FRONTEND_URL}:${FRONTEND_PORT}
|
|
||||||
|
|
||||||
SSLEngine On
|
|
||||||
# SSLUseStapling On
|
|
||||||
SSLProtocol -all +TLSv1.2
|
|
||||||
SSLCipherSuite ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS
|
|
||||||
SSLHonorCipherOrder on
|
|
||||||
SSLCertificateFile /etc/letsencrypt/live/app.boxi.ch/cert.pem
|
|
||||||
SSLCertificateKeyFile /etc/letsencrypt/live/app.boxi.ch/privkey.pem
|
|
||||||
SSLCertificateChainFile /etc/letsencrypt/live/app.boxi.ch/fullchain.pem
|
|
||||||
</VirtualHost>
|
|
||||||
</IfModule>
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
<VirtualHost *:80>
|
|
||||||
DocumentRoot "/var/www/id"
|
|
||||||
ServerName id.boxi.ch
|
|
||||||
# ServerAlias www.${server_name}
|
|
||||||
ErrorLog /var/log/apache2/error.log
|
|
||||||
CustomLog /var/log/apache2/access.log combined
|
|
||||||
LogLevel info
|
|
||||||
<Directory />
|
|
||||||
AllowOverride none
|
|
||||||
Options -Indexes -Includes
|
|
||||||
Require all granted
|
|
||||||
<LimitExcept GET POST HEAD>
|
|
||||||
deny from all
|
|
||||||
</LimitExcept>
|
|
||||||
</Directory>
|
|
||||||
<Directory "/var/www/id">
|
|
||||||
AllowOverride None
|
|
||||||
Options -Indexes -Includes
|
|
||||||
Require all granted
|
|
||||||
</Directory>
|
|
||||||
<Location /status >
|
|
||||||
SetHandler server-status
|
|
||||||
</Location>
|
|
||||||
<Location / >
|
|
||||||
Redirect / https://id.boxi.ch/
|
|
||||||
</Location>
|
|
||||||
</VirtualHost>
|
|
||||||
|
|
||||||
<IfModule mod_ssl.c>
|
|
||||||
<VirtualHost *:443>
|
|
||||||
DocumentRoot "/var/www/id"
|
|
||||||
ServerName id.boxi.ch
|
|
||||||
#ServerAlias www.${server_name}
|
|
||||||
|
|
||||||
LogLevel info
|
|
||||||
|
|
||||||
Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains"
|
|
||||||
Header always set Content-Security-Policy "default-src 'self' ; \
|
|
||||||
style-src 'self' 'unsafe-inline'; \
|
|
||||||
script-src 'self' 'unsafe-inline'; \
|
|
||||||
img-src 'self'; \
|
|
||||||
connect-src 'self'; \
|
|
||||||
form-action 'self'; \
|
|
||||||
frame-src http:; \
|
|
||||||
frame-ancestors 'self'; \
|
|
||||||
object-src 'none'; \
|
|
||||||
upgrade-insecure-requests;"
|
|
||||||
|
|
||||||
#ProxyRequests Off
|
|
||||||
#ProxyPreserveHost On
|
|
||||||
#ProxyAddHeaders On
|
|
||||||
|
|
||||||
#SSLProxyEngine On
|
|
||||||
#SSLProxyVerify none
|
|
||||||
#SSLProxyCheckPeerName off
|
|
||||||
#SSLProxyCheckPeerCN off
|
|
||||||
#SSLProxyCheckPeerName off
|
|
||||||
#SSLProxyCheckPeerExpire off
|
|
||||||
|
|
||||||
#RequestHeader set X-Forwarded-Proto "https"
|
|
||||||
#RequestHeader set X-Forwarded-Port "443"
|
|
||||||
#RequestHeader set X-Forwarded-Host "id.boxi.ch"
|
|
||||||
|
|
||||||
#ProxyPass / http://keycloak:8080/ retry=0
|
|
||||||
#ProxyPassReverse / http://keycloak:8080
|
|
||||||
|
|
||||||
<Location / >
|
|
||||||
Redirect / https://id.boxi.ch:8443/
|
|
||||||
</Location>
|
|
||||||
|
|
||||||
SSLEngine On
|
|
||||||
SSLProtocol -all +TLSv1.2
|
|
||||||
SSLCipherSuite ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS
|
|
||||||
SSLHonorCipherOrder on
|
|
||||||
SSLCertificateFile /etc/letsencrypt/live/app.boxi.ch/cert.pem
|
|
||||||
SSLCertificateKeyFile /etc/letsencrypt/live/app.boxi.ch/privkey.pem
|
|
||||||
SSLCertificateChainFile /etc/letsencrypt/live/app.boxi.ch/fullchain.pem
|
|
||||||
</VirtualHost>
|
|
||||||
</IfModule>
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Boxi the APP</title>
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-content: center;
|
|
||||||
height: 100vh;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>This is the webserver for app.boxi.ch</h1>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Boxi the ID</title>
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-content: center;
|
|
||||||
height: 100vh;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>This is the webserver for id.boxi.ch</h1>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
Executable
+179
@@ -0,0 +1,179 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Boxit/Remsly prod deployment helper.
|
||||||
|
#
|
||||||
|
# One script, two modes:
|
||||||
|
# * Bootstrap (`deploy <version>`): downloads config-<version>.tar.gz from the
|
||||||
|
# Gitea generic package registry into ./config, extracts it to a staging dir
|
||||||
|
# and re-executes the deploy.sh *from the tarball* in install mode. The install
|
||||||
|
# logic therefore always comes from the deployed version (self-updating), which
|
||||||
|
# is what lets a release carry its own migration/special steps.
|
||||||
|
# * Install (`__install`, internal): syncs the staged config into the deploy
|
||||||
|
# directory (never touching .env or ./config) and runs docker compose.
|
||||||
|
#
|
||||||
|
# The deploy directory is the folder this script lives in; it also holds .env
|
||||||
|
# (secrets, host-only) and docker-compose.yml. Ship this script once to the host,
|
||||||
|
# afterwards every deploy refreshes it from the tarball.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./deploy.sh <version> deploy a specific version (e.g. 2026.07-A0-6)
|
||||||
|
# ./deploy.sh deploy <version> same, explicit
|
||||||
|
# ./deploy.sh list-remote [n] list the last n versions in the registry (default 10)
|
||||||
|
# ./deploy.sh list-local list downloaded config archives
|
||||||
|
# ./deploy.sh help
|
||||||
|
#
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# --- Constants ---------------------------------------------------------------
|
||||||
|
GITEA_HOST="gitea.tail276105.ts.net"
|
||||||
|
OWNER="remsly"
|
||||||
|
PKG="prod-config"
|
||||||
|
|
||||||
|
DEPLOY_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
CONFIG_DIR="${DEPLOY_DIR}/config"
|
||||||
|
INSTALLED_FILE="${DEPLOY_DIR}/.installed-version"
|
||||||
|
|
||||||
|
# --- Helpers -----------------------------------------------------------------
|
||||||
|
die() { echo "ERROR: $*" >&2; exit 1; }
|
||||||
|
|
||||||
|
# Read a single key from .env without sourcing it (values are unquoted KEY=value).
|
||||||
|
read_env() {
|
||||||
|
local key="$1" val
|
||||||
|
[ -f "${DEPLOY_DIR}/.env" ] || return 1
|
||||||
|
val="$(grep -E "^${key}=" "${DEPLOY_DIR}/.env" | head -n1 | cut -d= -f2-)" || return 1
|
||||||
|
# strip trailing CR (CRLF files) and surrounding quotes
|
||||||
|
val="${val%$'\r'}"; val="${val%\"}"; val="${val#\"}"; val="${val%\'}"; val="${val#\'}"
|
||||||
|
printf '%s' "$val"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Basic-auth curl against Gitea, using GITEA_USER/GITEA_TOKEN from .env.
|
||||||
|
gitea_curl() {
|
||||||
|
local user token
|
||||||
|
user="$(read_env GITEA_USER || true)"
|
||||||
|
token="$(read_env GITEA_TOKEN || true)"
|
||||||
|
[ -n "$user" ] && [ -n "$token" ] || die "GITEA_USER/GITEA_TOKEN missing in ${DEPLOY_DIR}/.env"
|
||||||
|
curl -fsSL --user "${user}:${token}" "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Resolve the docker compose command (v2 plugin preferred, v1 fallback).
|
||||||
|
compose() {
|
||||||
|
if docker compose version >/dev/null 2>&1; then
|
||||||
|
docker compose "$@"
|
||||||
|
elif command -v docker-compose >/dev/null 2>&1; then
|
||||||
|
docker-compose "$@"
|
||||||
|
else
|
||||||
|
die "neither 'docker compose' nor 'docker-compose' found"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
tarball_url() { printf 'https://%s/api/packages/%s/generic/%s/%s/config.tar.gz' "$GITEA_HOST" "$OWNER" "$PKG" "$1"; }
|
||||||
|
|
||||||
|
# --- Commands ----------------------------------------------------------------
|
||||||
|
cmd_list_remote() {
|
||||||
|
local n="${1:-10}"
|
||||||
|
echo "Last ${n} remote version(s) of ${PKG}:"
|
||||||
|
gitea_curl "https://${GITEA_HOST}/api/v1/packages/${OWNER}?type=generic&q=${PKG}" \
|
||||||
|
| grep -o '"version":"[^"]*"' | cut -d'"' -f4 \
|
||||||
|
| sort -Vr | head -n "$n" \
|
||||||
|
| sed 's/^/ /'
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_list_local() {
|
||||||
|
local current="" f v
|
||||||
|
[ -f "$INSTALLED_FILE" ] && current="$(cat "$INSTALLED_FILE")"
|
||||||
|
echo "Local config archives in ${CONFIG_DIR}:"
|
||||||
|
if ! ls "${CONFIG_DIR}"/config-*.tar.gz >/dev/null 2>&1; then
|
||||||
|
echo " (none)"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
for f in "${CONFIG_DIR}"/config-*.tar.gz; do
|
||||||
|
v="$(basename "$f")"; v="${v#config-}"; v="${v%.tar.gz}"
|
||||||
|
if [ "$v" = "$current" ]; then
|
||||||
|
echo " ${v} <- installed"
|
||||||
|
else
|
||||||
|
echo " ${v}"
|
||||||
|
fi
|
||||||
|
done | sort -Vr
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_deploy() {
|
||||||
|
local version="${1:-}"
|
||||||
|
[ -n "$version" ] || die "version required (e.g. ${0##*/} 2026.07-A0-6)"
|
||||||
|
|
||||||
|
mkdir -p "$CONFIG_DIR"
|
||||||
|
local archive="${CONFIG_DIR}/config-${version}.tar.gz"
|
||||||
|
if [ -f "$archive" ]; then
|
||||||
|
echo "Using cached ${archive}"
|
||||||
|
else
|
||||||
|
echo "Downloading ${version} ..."
|
||||||
|
gitea_curl -o "$archive" "$(tarball_url "$version")" \
|
||||||
|
|| { rm -f "$archive"; die "download of ${version} failed"; }
|
||||||
|
fi
|
||||||
|
|
||||||
|
local stage
|
||||||
|
stage="$(mktemp -d)"
|
||||||
|
tar -xzf "$archive" -C "$stage"
|
||||||
|
[ -f "${stage}/deploy.sh" ] || die "tarball has no deploy.sh"
|
||||||
|
|
||||||
|
# Hand off to the deploy.sh shipped in this version (self-updating install logic).
|
||||||
|
# The running host copy is not overwritten because we exec the staged copy.
|
||||||
|
exec bash "${stage}/deploy.sh" __install "$version" "$DEPLOY_DIR" "$stage"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_install() {
|
||||||
|
local version="$1" deploydir="$2" stage="$3"
|
||||||
|
echo "Installing ${version} into ${deploydir} ..."
|
||||||
|
|
||||||
|
# Sync staged config into the deploy dir; never clobber secrets or downloads.
|
||||||
|
if command -v rsync >/dev/null 2>&1; then
|
||||||
|
rsync -a --exclude='.env' --exclude='config/' "${stage}/" "${deploydir}/"
|
||||||
|
else
|
||||||
|
# cp fallback: copy everything except .env / config (tarball has neither, but be safe)
|
||||||
|
( cd "$stage" && find . -mindepth 1 -maxdepth 1 \
|
||||||
|
! -name '.env' ! -name 'config' \
|
||||||
|
-exec cp -a {} "${deploydir}/" \; )
|
||||||
|
fi
|
||||||
|
|
||||||
|
( cd "$deploydir" && compose pull && compose up -d )
|
||||||
|
|
||||||
|
# Optional per-version migration/special-task hook shipped inside the tarball.
|
||||||
|
if [ -f "${deploydir}/post-deploy.sh" ]; then
|
||||||
|
echo "Running post-deploy.sh hook ..."
|
||||||
|
bash "${deploydir}/post-deploy.sh" "$version"
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s\n' "$version" > "${deploydir}/.installed-version"
|
||||||
|
rm -rf "$stage"
|
||||||
|
echo "Deployed ${version}."
|
||||||
|
}
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
local me="${0##*/}"
|
||||||
|
cat <<EOF
|
||||||
|
${me} - Boxit/Remsly prod deployment helper
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
${me} <version> deploy a specific version (e.g. 2026.07-A0-6)
|
||||||
|
${me} deploy <version> same, explicit
|
||||||
|
${me} list-remote [n] list the last n versions in the registry (default 10)
|
||||||
|
${me} list-local list downloaded config archives under ./config
|
||||||
|
${me} help
|
||||||
|
|
||||||
|
Requires GITEA_USER/GITEA_TOKEN in ./.env for registry access.
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Dispatch ----------------------------------------------------------------
|
||||||
|
main() {
|
||||||
|
local cmd="${1:-help}"
|
||||||
|
case "$cmd" in
|
||||||
|
list-remote) shift; cmd_list_remote "$@" ;;
|
||||||
|
list-local) cmd_list_local ;;
|
||||||
|
deploy) shift; cmd_deploy "$@" ;;
|
||||||
|
__install) shift; cmd_install "$@" ;;
|
||||||
|
help|-h|--help) usage ;;
|
||||||
|
*) cmd_deploy "$cmd" ;; # bare version shorthand
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
+122
-64
@@ -1,98 +1,156 @@
|
|||||||
version: '3'
|
volumes:
|
||||||
|
postgres:
|
||||||
|
driver: local
|
||||||
|
elasticsearch:
|
||||||
|
driver: local
|
||||||
|
tailscale-id:
|
||||||
|
driver: local
|
||||||
|
tailscale-apache:
|
||||||
|
driver: local
|
||||||
|
|
||||||
|
networks:
|
||||||
|
remslyNet:
|
||||||
|
name: remslyNet
|
||||||
|
external: false
|
||||||
|
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:14-alpine
|
image: postgres:14-alpine
|
||||||
restart: always
|
restart: always
|
||||||
networks:
|
networks:
|
||||||
boxitNet:
|
- remslyNet
|
||||||
ipv4_address: 10.5.1.1
|
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_USER: 'boxitadmin'
|
POSTGRES_USER: 'admin'
|
||||||
POSTGRES_PASSWORD: 'kQsQKCQoa.yf.XiGCh.3'
|
POSTGRES_PASSWORD: ${REMSLY_PSQL_ADMIN_PASSWORD}
|
||||||
PGDATA: '/var/lib/postgresql/data/pgdata'
|
PGDATA: '/var/lib/postgresql/data/pgdata'
|
||||||
POSTGRES_MULTIPLE_DATABASES: 'boxit:q_VpgQaeXcjns9PqPmGj:boxit,keycloak:roraJK_ds3KDiHkMRLqf:keycloak'
|
POSTGRES_MULTIPLE_DATABASES: 'remsly:${REMSLY_PSQL_PASSWORD}:remsly,keycloak:${KC_PSQL_PASSWORD}:keycloak'
|
||||||
volumes:
|
volumes:
|
||||||
- ./data/postgres:/var/lib/postgresql/data/pgdata
|
- postgres:/var/lib/postgresql/data/pgdata
|
||||||
- ./config/postgres/create-multiple-postgresql-databases.sh:/docker-entrypoint-initdb.d/create-multiple-postgresql-databases.sh
|
- ./postgres/create-multiple-postgresql-databases.sh:/docker-entrypoint-initdb.d/create-multiple-postgresql-databases.sh
|
||||||
|
|
||||||
|
|
||||||
keycloak:
|
keycloak:
|
||||||
image: boxi.registry.jetbrains.space/p/boxit/containers/boxit-keycloak:0.0.1
|
image: gitea.tail276105.ts.net/remsly/keycloak:4
|
||||||
networks:
|
|
||||||
boxitNet:
|
|
||||||
ipv4_address: 10.5.0.20
|
|
||||||
restart: always
|
restart: always
|
||||||
environment:
|
environment:
|
||||||
KC_BOOTSTRAP_ADMIN_USERNAME: 'admin'
|
KC_BOOTSTRAP_ADMIN_USERNAME: 'admin'
|
||||||
KC_BOOTSTRAP_ADMIN_PASSWORD: 'dzVTry4QhwKBnmM8X2PWZRZA'
|
KC_BOOTSTRAP_ADMIN_PASSWORD: ${KC_ADMIN_PASSWORD}
|
||||||
KC_HTTPS_CERTIFICATE_FILE: '/opt/keycloak/certs/fullchain.pem'
|
KC_HTTP_ENABLED: 'true'
|
||||||
KC_HTTPS_CERTIFICATE_KEY_FILE: '/opt/keycloak/certs/privkey.pem'
|
|
||||||
KC_DB: postgres
|
KC_DB: postgres
|
||||||
KC_DB_URL: 'jdbc:postgresql://postgres:5432/keycloak'
|
KC_DB_URL: 'jdbc:postgresql://postgres:5432/keycloak'
|
||||||
KC_DB_USERNAME: 'keycloak'
|
KC_DB_USERNAME: 'keycloak'
|
||||||
KC_DB_PASSWORD: 'roraJK_ds3KDiHkMRLqf'
|
KC_DB_PASSWORD: ${KC_PSQL_PASSWORD}
|
||||||
KC_HOSTNAME: 'id.boxi.ch'
|
# TLS is terminated by the tailscale-id sidecar; Keycloak runs plain HTTP (8080) behind it
|
||||||
# KC_HOSTNAME_STRICT: 'false'
|
KC_HOSTNAME: 'https://remsly-id.tail276105.ts.net'
|
||||||
# KC_PROXY_HEADERS: xforwarded
|
KC_HOSTNAME_URL: 'https://remsly-id.tail276105.ts.net'
|
||||||
# KC_HTTP_ENABLED: 'true'
|
KC_HOSTNAME_ADMIN_URL: 'https://remsly-id.tail276105.ts.net'
|
||||||
# KC_PROXY: edge
|
# Serve backchannel endpoints (token/jwks/userinfo) under the requesting host so
|
||||||
|
# in-cluster clients can reach Keycloak over the bridge on :8080, while frontend
|
||||||
|
# URLs (issuer, authorization endpoint, logout) stay on the public https hostname.
|
||||||
|
KC_HOSTNAME_BACKCHANNEL_DYNAMIC: 'true'
|
||||||
command:
|
command:
|
||||||
- start
|
- start
|
||||||
- '--optimized'
|
- '--optimized'
|
||||||
# - '--hostname-debug=true' # https://id.boxi.ch:8443/realms/master/hostname-debug
|
# - '--hostname-debug=true' #https://remsly-id.tail276105.ts.net/realms/master/hostname-debug
|
||||||
volumes:
|
volumes:
|
||||||
- ./config/keycloak/theme:/opt/keycloak/themes/boxit
|
- ./keycloak/theme:/opt/keycloak/themes/boxit
|
||||||
- ./config/keycloak/theme.v2:/opt/keycloak/themes/boxit.v2
|
- ./keycloak/theme.v2:/opt/keycloak/themes/boxit.v2
|
||||||
- ./certbot/etc/live/app.boxi.ch/fullchain.pem:/opt/keycloak/certs/fullchain.pem:ro
|
network_mode: service:tailscale-id
|
||||||
- ./certbot/etc/live/app.boxi.ch/privkey.pem:/opt/keycloak/certs/privkey.pem:ro
|
|
||||||
ports:
|
|
||||||
- '8443:8443'
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- postgres
|
- postgres
|
||||||
|
- tailscale-id
|
||||||
|
|
||||||
apache-oidc:
|
apache-oidc:
|
||||||
image: boxi.registry.jetbrains.space/p/boxit/containers/boxit-frontend:2023.1.36
|
# <<VERSION>> is replaced with the build version by the Package-Prod-Config CI job.
|
||||||
|
image: gitea.tail276105.ts.net/remsly/remsly-frontend:<<VERSION>>
|
||||||
restart: always
|
restart: always
|
||||||
networks:
|
|
||||||
boxitNet:
|
|
||||||
ipv4_address: 10.5.0.30
|
|
||||||
environment:
|
environment:
|
||||||
OIDC_CRYPTO_PASSPHRASE: '39938b0a-1bd2-4352-937d-3cf1e2023727'
|
OIDC_CRYPTO_PASSPHRASE: ${OIDC_CRYPTO_PASSPHRASE}
|
||||||
KC_ADDR: 'https://id.boxi.ch:8443'
|
# Browser-facing base URL (issuer, login/logout redirects) - reachable by the browser.
|
||||||
KC_REALM: 'boxit'
|
KC_ADDR: 'https://remsly-id.tail276105.ts.net'
|
||||||
OIDC_CLIENT_ID: 'apacheOIDC'
|
# Server-side base URL for the OIDC metadata + backchannel calls (token exchange, jwks).
|
||||||
OIDC_CLIENT_SECRET: 'Ymye3T55SnH1YfssvZZi6tRIIZ0PJTsK'
|
# tailscale serve only listens on the tailnet IP, so the https:443 URL is unreachable
|
||||||
OIDC_REDIRECT_URI: 'https://app.boxi.ch/openidc'
|
# from this container; reach Keycloak directly over the bridge on plain http :8080.
|
||||||
|
KC_INTERNAL_ADDR: 'http://remsly-id.tail276105.ts.net:8080'
|
||||||
|
KC_REALM: 'Remsly'
|
||||||
|
OIDC_CLIENT_ID: 'ApacheOIDC'
|
||||||
|
OIDC_CLIENT_SECRET: ${OIDC_CLIENT_SECRET}
|
||||||
|
OIDC_REDIRECT_URI: 'https://remsly.tail276105.ts.net/openidc'
|
||||||
APACHE_DEBUG_LEVEL: 'warn'
|
APACHE_DEBUG_LEVEL: 'warn'
|
||||||
BACKEND_URL: 'backend'
|
BACKEND_URL: 'http://backend'
|
||||||
BACKEND_PORT: '5050'
|
BACKEND_PORT: '5050'
|
||||||
volumes:
|
volumes:
|
||||||
- ./config/frontend/apache2.conf:/etc/apache2/apache2.conf
|
- ./frontend/apache2.conf:/etc/apache2/apache2.conf
|
||||||
- ./certbot/etc:/etc/letsencrypt
|
- ./frontend/app.boxi.ch.conf:/etc/apache2/sites-enabled/app.boxi.ch.conf
|
||||||
- ./data/www:/var/www
|
network_mode: service:tailscale-apache
|
||||||
- ./config/frontend/app.boxi.ch.conf:/etc/apache2/sites-enabled/app.boxi.ch.conf
|
|
||||||
- ./config/frontend/id.boxi.ch.conf:/etc/apache2/sites-enabled/id.boxi.ch.conf
|
|
||||||
ports:
|
|
||||||
- '80:80'
|
|
||||||
- '443:443'
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- keycloak
|
- keycloak
|
||||||
|
- backend
|
||||||
|
- tailscale-apache
|
||||||
|
|
||||||
#backend:
|
backend:
|
||||||
#image: boxi.registry.jetbrains.space/p/boxit/containers/boxit-backend:2023.1.36
|
# <<VERSION>> is replaced with the build version by the Package-Prod-Config CI job.
|
||||||
# restart: always
|
image: gitea.tail276105.ts.net/remsly/remsly-backend:<<VERSION>>
|
||||||
# networks:
|
restart: always
|
||||||
# boxitNet:
|
environment:
|
||||||
# ipv4_address: 10.5.0.40
|
# The CI-built image ships no application.properties, so mount one and activate it.
|
||||||
# depends_on:
|
SPRING_CONFIG_ADDITIONAL_LOCATION: 'file:/config/application.properties'
|
||||||
# - postgres
|
# Secrets referenced as ${...} placeholders inside the mounted properties file.
|
||||||
# - keycloak
|
REMSLY_PSQL_PASSWORD: ${REMSLY_PSQL_PASSWORD}
|
||||||
|
ELASTIC_PASSWORD: ${ELASTIC_PASSWORD}
|
||||||
|
KEYCLOAK_RESOURCE: ${KEYCLOAK_RESOURCE}
|
||||||
|
volumes:
|
||||||
|
- ./backend/application.properties:/config/application.properties:ro
|
||||||
|
networks:
|
||||||
|
- remslyNet
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
|
- elasticsearch
|
||||||
|
|
||||||
networks:
|
elasticsearch:
|
||||||
boxitNet:
|
image: gitea.tail276105.ts.net/remsly/elasticsearch:3
|
||||||
driver: bridge
|
restart: always
|
||||||
ipam:
|
volumes:
|
||||||
driver: default
|
- elasticsearch:/usr/share/elasticsearch/data
|
||||||
config:
|
networks:
|
||||||
- subnet: 10.5.0.0/16
|
- remslyNet
|
||||||
|
environment:
|
||||||
|
- ELASTIC_PASSWORD=${ELASTIC_PASSWORD}
|
||||||
|
- discovery.type=single-node
|
||||||
|
- xpack.license.self_generated.type=basic
|
||||||
|
|
||||||
|
tailscale-id:
|
||||||
|
image: tailscale/tailscale:latest
|
||||||
|
hostname: remsly-id
|
||||||
|
environment:
|
||||||
|
TS_AUTHKEY: ${TS_AUTHKEY}
|
||||||
|
TS_USERSPACE: "true"
|
||||||
|
TS_STATE_DIR: /var/lib/tailscale
|
||||||
|
TS_SERVE_CONFIG: /config/serve.json
|
||||||
|
TS_EXTRA_ARGS: --advertise-tags=tag:server
|
||||||
|
volumes:
|
||||||
|
- tailscale-id:/var/lib/tailscale
|
||||||
|
- ./tailscale/serve-id.json:/config/serve.json
|
||||||
|
networks:
|
||||||
|
remslyNet:
|
||||||
|
# Let the backend reach Keycloak via its external URL over the internal network:
|
||||||
|
# the FQDN resolves to this sidecar, hits tailscale serve on :443 (valid LE cert)
|
||||||
|
# and is proxied to Keycloak. Keeps the token issuer == auth-server-url.
|
||||||
|
aliases:
|
||||||
|
- remsly-id.tail276105.ts.net
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
tailscale-apache:
|
||||||
|
image: tailscale/tailscale:latest
|
||||||
|
hostname: remsly
|
||||||
|
environment:
|
||||||
|
TS_AUTHKEY: ${TS_AUTHKEY}
|
||||||
|
TS_USERSPACE: "true"
|
||||||
|
TS_STATE_DIR: /var/lib/tailscale
|
||||||
|
TS_SERVE_CONFIG: /config/serve.json
|
||||||
|
TS_EXTRA_ARGS: --advertise-tags=tag:server
|
||||||
|
volumes:
|
||||||
|
- tailscale-apache:/var/lib/tailscale
|
||||||
|
- ./tailscale/serve-apache.json:/config/serve.json
|
||||||
|
networks:
|
||||||
|
- remslyNet
|
||||||
|
restart: unless-stopped
|
||||||
@@ -1,7 +1,12 @@
|
|||||||
IncludeOptional mods-enabled/*.load
|
IncludeOptional mods-enabled/*.load
|
||||||
IncludeOptional mods-enabled/*.conf
|
IncludeOptional mods-enabled/*.conf
|
||||||
|
|
||||||
# Include ports.conf
|
Listen 80
|
||||||
|
|
||||||
|
User ${APACHE_RUN_USER}
|
||||||
|
Group ${APACHE_RUN_GROUP}
|
||||||
|
DefaultRuntimeDir ${APACHE_RUN_DIR}
|
||||||
|
PidFile ${APACHE_PID_FILE}
|
||||||
|
|
||||||
ServerName app.boxi.ch
|
ServerName app.boxi.ch
|
||||||
ServerAdmin online@boxi.ch
|
ServerAdmin online@boxi.ch
|
||||||
File renamed without changes.
@@ -0,0 +1,93 @@
|
|||||||
|
<VirtualHost *:80>
|
||||||
|
DocumentRoot "/var/www/html/app"
|
||||||
|
ServerName remsly.tail276105.ts.net
|
||||||
|
# ServerAlias www.${server_name}
|
||||||
|
|
||||||
|
ErrorLog /var/log/apache2/error.log
|
||||||
|
CustomLog /var/log/apache2/access.log combined
|
||||||
|
LogLevel info
|
||||||
|
|
||||||
|
# TLS is terminated by the tailscale sidecar; make mod_auth_openidc and the
|
||||||
|
# app aware that the external scheme is https.
|
||||||
|
RequestHeader set X-Forwarded-Proto "https"
|
||||||
|
|
||||||
|
Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains"
|
||||||
|
Header always set Content-Security-Policy "default-src 'self' ; \
|
||||||
|
script-src 'self' 'unsafe-inline' 'unsafe-eval'; \
|
||||||
|
style-src 'self' use.fontawesome.com 'unsafe-inline'; \
|
||||||
|
font-src 'self' data: use.fontawesome.com; \
|
||||||
|
base-uri 'self'; \
|
||||||
|
form-action 'self'; \
|
||||||
|
frame-ancestors 'none'; \
|
||||||
|
img-src 'self' api.mapbox.com data:; \
|
||||||
|
upgrade-insecure-requests" "expr=%{CONTENT_TYPE} =~ m#text\/(html|javascript)|application\/pdf|xml#i"
|
||||||
|
Header always append X-Frame-Options SAMEORIGIN
|
||||||
|
Header always set X-Content-Type-Options nosniff
|
||||||
|
Header always set Referrer-Policy "same-origin"
|
||||||
|
# this feature is not yet used and must be extended if needed, currently only a sample config
|
||||||
|
Header always set Feature-Policy "payment 'none';"
|
||||||
|
|
||||||
|
<Directory />
|
||||||
|
AllowOverride none
|
||||||
|
Options -Indexes -Includes
|
||||||
|
Require all granted
|
||||||
|
<LimitExcept GET POST HEAD>
|
||||||
|
deny from all
|
||||||
|
</LimitExcept>
|
||||||
|
</Directory>
|
||||||
|
|
||||||
|
<Directory "/var/www/html/app">
|
||||||
|
AllowOverride None
|
||||||
|
Options -Indexes -Includes
|
||||||
|
Require all granted
|
||||||
|
# SPA deep-link support: serve index.html (HTTP 200) for any path that does not map to a
|
||||||
|
# real file, so the Angular router can resolve it client-side. Existing assets are still
|
||||||
|
# served directly; /api/ (proxied) and /openidc (mod_auth_openidc) are unaffected.
|
||||||
|
FallbackResource /index.html
|
||||||
|
</Directory>
|
||||||
|
|
||||||
|
RequestHeader set X-Forwarded-User expr=%{REMOTE_USER}
|
||||||
|
RequestHeader set Authorization "Bearer %{oidc_access_token}e"
|
||||||
|
Header set User-Details "%{REMOTE_USER}e %{oidc_claim_email}e"
|
||||||
|
|
||||||
|
# Fetch metadata + run the backchannel (token exchange, jwks) over the internal http:8080
|
||||||
|
# URL; the discovery doc still advertises the https issuer/authorization/logout endpoints
|
||||||
|
# (KC_HOSTNAME_BACKCHANNEL_DYNAMIC=true on Keycloak keeps those frontend URLs public).
|
||||||
|
OIDCProviderMetadataURL ${KC_INTERNAL_ADDR}/realms/${KC_REALM}/.well-known/openid-configuration
|
||||||
|
OIDCClientID ${OIDC_CLIENT_ID}
|
||||||
|
OIDCClientSecret ${OIDC_CLIENT_SECRET}
|
||||||
|
OIDCRefreshAccessTokenBeforeExpiry 10
|
||||||
|
OIDCRedirectURI ${OIDC_REDIRECT_URI}
|
||||||
|
OIDCCryptoPassphrase ${OIDC_CRYPTO_PASSPHRASE}
|
||||||
|
# Where to land after a successful login when no original target URL is known (e.g. SSO entry
|
||||||
|
# via the redirect URI). Without this, mod_auth_openidc aborts with "Invalid Request".
|
||||||
|
OIDCDefaultURL https://remsly.tail276105.ts.net/
|
||||||
|
OIDCDefaultLoggedOutURL ${KC_ADDR}/realms/${KC_REALM}/protocol/openid-connect/logout?redirect_uri=https%3A%2F%2Fremsly.tail276105.ts.net
|
||||||
|
OIDCRemoteUserClaim preferred_username
|
||||||
|
OIDCStateMaxNumberOfCookies 3 true
|
||||||
|
|
||||||
|
<Location /status >
|
||||||
|
SetHandler server-status
|
||||||
|
</Location>
|
||||||
|
|
||||||
|
<Location />
|
||||||
|
AuthType openid-connect
|
||||||
|
Require valid-user
|
||||||
|
</Location>
|
||||||
|
|
||||||
|
# If the browser is allowed to cache index.html, it starts loading assets/scripts/images before apache had a chance
|
||||||
|
# to check whether or not the user is even authenticated, leading to many failed requests.
|
||||||
|
# Disabling cache for index.html forces the browser to fetch it first, and therefore ensures that the full
|
||||||
|
# authentication mechanic is being run.
|
||||||
|
# See also https://github.com/zmartzone/mod_auth_openidc/issues/113
|
||||||
|
<filesMatch "index.html">
|
||||||
|
FileETag None
|
||||||
|
Header unset ETag
|
||||||
|
Header set Cache-Control "max-age=0, no-cache, no-store, must-revalidate"
|
||||||
|
Header set Pragma "no-cache"
|
||||||
|
Header set Expires "Mon, 21 Mar 1983 14:10:00 UTC"
|
||||||
|
</filesMatch>
|
||||||
|
|
||||||
|
ProxyPass /api/ ${BACKEND_URL}:${BACKEND_PORT}/api/ retry=0
|
||||||
|
ProxyPassReverse /api/ ${BACKEND_URL}:${BACKEND_PORT}/api/
|
||||||
|
</VirtualHost>
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Loaded 100 of 207 files, more files were not shown because too many files have changed in this diff.
Show more