Compare commits
8
Commits
ebb59daad4
...
4269f980db
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4269f980db | ||
|
|
c23836e5fe | ||
|
|
7ccf9e1bc3 | ||
|
|
8c6e31166a | ||
|
|
0e2120e414 | ||
|
|
d09fb5d9b7 | ||
|
|
99d683796a | ||
|
|
a6fb0e41fe |
No files matched your search
+62
-11
@@ -5,11 +5,30 @@ on:
|
||||
branches: [ main ]
|
||||
|
||||
jobs:
|
||||
Build-Remsly-Backend:
|
||||
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
|
||||
@@ -41,21 +60,21 @@ jobs:
|
||||
run: |
|
||||
REPOSITORY_LC=$(echo "${{ gitea.repository }}" | tr '[:upper:]' '[:lower:]')
|
||||
(
|
||||
cd backend;
|
||||
docker build -t gitea.tail276105.ts.net/${REPOSITORY_LC}-backend:${GITHUB_RUN_NUMBER} .;
|
||||
docker tag gitea.tail276105.ts.net/${REPOSITORY_LC}-backend:${GITHUB_RUN_NUMBER} gitea.tail276105.ts.net/${REPOSITORY_LC}-backend:latest
|
||||
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:${GITHUB_RUN_NUMBER}
|
||||
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:${GITHUB_RUN_NUMBER}"
|
||||
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
|
||||
@@ -64,8 +83,11 @@ jobs:
|
||||
|
||||
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
|
||||
@@ -85,22 +107,51 @@ jobs:
|
||||
REPOSITORY_LC=$(echo "${{ gitea.repository }}" | tr '[:upper:]' '[:lower:]')
|
||||
(
|
||||
cd frontend;
|
||||
docker build -t gitea.tail276105.ts.net/${REPOSITORY_LC}-frontend:${GITHUB_RUN_NUMBER} .;
|
||||
docker tag gitea.tail276105.ts.net/${REPOSITORY_LC}-frontend:${GITHUB_RUN_NUMBER} gitea.tail276105.ts.net/${REPOSITORY_LC}-frontend:latest
|
||||
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:${GITHUB_RUN_NUMBER}
|
||||
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:${GITHUB_RUN_NUMBER}"
|
||||
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
|
||||
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: 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"
|
||||
@@ -62,7 +62,7 @@ The DB schema is the source of truth. Migrations live in `backend/src/main/resou
|
||||
- `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 (truststore at `backend/src/main/resources/truststore.jks`; Elasticsearch CA certs must be imported there — see readme.md).
|
||||
- `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`)
|
||||
|
||||
|
||||
@@ -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.Version
|
||||
import com.google.common.base.Strings
|
||||
import com.google.common.io.PatternFilenameFilter
|
||||
import org.springframework.core.io.ClassPathResource
|
||||
import org.springframework.core.io.Resource
|
||||
import org.springframework.core.io.support.PathMatchingResourcePatternResolver
|
||||
import org.springframework.http.HttpMethod
|
||||
import java.io.BufferedReader
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.InputStreamReader
|
||||
import java.nio.file.Path
|
||||
import java.util.Arrays
|
||||
import java.util.SortedSet
|
||||
import java.util.regex.Pattern
|
||||
@@ -29,35 +26,35 @@ class MigrationLoader {
|
||||
}
|
||||
|
||||
fun loadMigrations(): SortedSet<MigrationScript> {
|
||||
val filenameFilter = PatternFilenameFilter(FILENAME_PATTERN)
|
||||
val folderResource = ClassPathResource("elasticsearch")
|
||||
return Arrays.stream(folderResource.file.listFiles(filenameFilter))
|
||||
.map(File::toPath)
|
||||
val resources = PathMatchingResourcePatternResolver().getResources("classpath*:elasticsearch/*.edql")
|
||||
return Arrays.stream(resources)
|
||||
.filter { resource -> resource.filename?.let { FILENAME_PATTERN.matcher(it).matches() } ?: false }
|
||||
.map(this::loadMigration)
|
||||
.toSortedSet()
|
||||
}
|
||||
|
||||
private fun loadMigration(path: Path): MigrationScript {
|
||||
LOG.debug("found migration file $path")
|
||||
val matcher = FILENAME_PATTERN.matcher(path.fileName.toString())
|
||||
private fun loadMigration(resource: Resource): MigrationScript {
|
||||
val filename = resource.filename!!
|
||||
LOG.debug("found migration file $filename")
|
||||
val matcher = FILENAME_PATTERN.matcher(filename)
|
||||
|
||||
matcher.matches()
|
||||
val script = matcher.group(0)
|
||||
val version = Version.parse(matcher.group(1))
|
||||
val description = matcher.group(2)
|
||||
|
||||
val operations = readOperations(path)
|
||||
val operations = readOperations(resource)
|
||||
|
||||
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>()
|
||||
var httpMethod: HttpMethod? = null
|
||||
var operationPath: String? = null
|
||||
val checkSum: Checksum
|
||||
var body = StringBuilder()
|
||||
CheckedInputStream(FileInputStream(path.toFile()), CRC32()).use { checkedReader ->
|
||||
CheckedInputStream(resource.inputStream, CRC32()).use { checkedReader ->
|
||||
BufferedReader(InputStreamReader(checkedReader)).use { reader ->
|
||||
reader.lines().forEach { line ->
|
||||
val subLine = line.trim()
|
||||
|
||||
@@ -1,21 +1,16 @@
|
||||
package ch.boxi.boxit.security
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.core.env.Environment
|
||||
import javax.annotation.PostConstruct
|
||||
|
||||
@Configuration
|
||||
class SSLClientConfig {
|
||||
@Autowired
|
||||
private val env: Environment? = null
|
||||
|
||||
@PostConstruct
|
||||
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")
|
||||
|
||||
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")!!)
|
||||
}
|
||||
}
|
||||
@@ -13,9 +13,6 @@ boxit.elasticsearch.pass=elasticPassword
|
||||
|
||||
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.endpoints.web.exposure.include=*
|
||||
management.endpoint.prometheus.enabled=true
|
||||
|
||||
Binary file not shown.
@@ -12,10 +12,6 @@ boxit.elasticsearch.pass=elasticPassword
|
||||
|
||||
spring.jackson.date-format="yyyy.MM.dd HH:mm:ss"
|
||||
|
||||
#boxit.server.ssl.trust-store=truststore
|
||||
boxit.server.ssl.trust-store=src/main/resources/truststore.jks
|
||||
boxit.server.ssl.trust-store-password=Tester11
|
||||
|
||||
management.endpoint.metrics.enabled=true
|
||||
management.endpoints.web.exposure.include=*
|
||||
management.endpoint.prometheus.enabled=true
|
||||
|
||||
@@ -251,7 +251,7 @@ Listen 80
|
||||
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"
|
||||
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"
|
||||
|
||||
@@ -11,6 +11,7 @@ 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=
|
||||
@@ -22,3 +23,8 @@ 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
|
||||
@@ -18,12 +18,6 @@ boxit.elasticsearch.pass=${ELASTIC_PASSWORD}
|
||||
|
||||
spring.jackson.date-format="yyyy-MM-dd HH:mm:ss"
|
||||
|
||||
# SSLClientConfig overrides the JVM trust store unconditionally, so point it at the JDK
|
||||
# default CA bundle. That keeps outbound HTTPS (e.g. fetching Keycloak's JWKS over the
|
||||
# Let's Encrypt / tailscale cert) working without shipping a custom truststore.
|
||||
boxit.server.ssl.trust-store=/opt/java/openjdk/lib/security/cacerts
|
||||
boxit.server.ssl.trust-store-password=changeit
|
||||
|
||||
management.endpoint.metrics.enabled=true
|
||||
management.endpoints.web.exposure.include=*
|
||||
management.endpoint.prometheus.enabled=true
|
||||
@@ -31,6 +25,11 @@ 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.
|
||||
@@ -40,3 +39,4 @@ logging.level.root=INFO
|
||||
# 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
|
||||
|
||||
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 "$@"
|
||||
@@ -60,7 +60,8 @@ services:
|
||||
- tailscale-id
|
||||
|
||||
apache-oidc:
|
||||
image: gitea.tail276105.ts.net/remsly/remsly-frontend:4
|
||||
# <<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
|
||||
environment:
|
||||
OIDC_CRYPTO_PASSPHRASE: ${OIDC_CRYPTO_PASSPHRASE}
|
||||
@@ -87,7 +88,8 @@ services:
|
||||
- tailscale-apache
|
||||
|
||||
backend:
|
||||
image: gitea.tail276105.ts.net/remsly/remsly-backend:3
|
||||
# <<VERSION>> is replaced with the build version by the Package-Prod-Config CI job.
|
||||
image: gitea.tail276105.ts.net/remsly/remsly-backend:<<VERSION>>
|
||||
restart: always
|
||||
environment:
|
||||
# The CI-built image ships no application.properties, so mount one and activate it.
|
||||
@@ -151,4 +153,4 @@ services:
|
||||
- ./tailscale/serve-apache.json:/config/serve.json
|
||||
networks:
|
||||
- remslyNet
|
||||
restart: unless-stopped
|
||||
restart: unless-stopped
|
||||
@@ -3,9 +3,6 @@ IncludeOptional mods-enabled/*.conf
|
||||
|
||||
Listen 80
|
||||
|
||||
# Run as the distro's apache user and use the standard runtime dir, otherwise the
|
||||
# child processes run as uid -1 and mod_auth_openidc cannot write its OIDCCacheDir
|
||||
# (state/cache) -> HTTP 500 on the first authenticated request.
|
||||
User ${APACHE_RUN_USER}
|
||||
Group ${APACHE_RUN_GROUP}
|
||||
DefaultRuntimeDir ${APACHE_RUN_DIR}
|
||||
@@ -61,4 +58,4 @@ ErrorLog /dev/stderr
|
||||
CustomLog /dev/stdout combined
|
||||
LogLevel debug
|
||||
|
||||
IncludeOptional sites-enabled/*
|
||||
IncludeOptional sites-enabled/*
|
||||
@@ -40,6 +40,10 @@
|
||||
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}
|
||||
@@ -55,6 +59,9 @@
|
||||
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
|
||||
|
||||
@@ -70,13 +70,13 @@
|
||||
"node_modules/prismjs/components/prism-nginx.min.js",
|
||||
"node_modules/prismjs/components/prism-php.min.js",
|
||||
"node_modules/prismjs/components/prism-perl.min.js",
|
||||
"node_modules/prismjs/components/prism-sql.min.js",
|
||||
"node_modules/prismjs/components/prism-plsql.min.js",
|
||||
"node_modules/prismjs/components/prism-python.min.js",
|
||||
"node_modules/prismjs/components/prism-ruby.min.js",
|
||||
"node_modules/prismjs/components/prism-properties.min.js",
|
||||
"node_modules/prismjs/components/prism-typescript.min.js",
|
||||
"node_modules/prismjs/components/prism-yaml.min.js",
|
||||
"node_modules/prismjs/components/prism-sql.min.js",
|
||||
"node_modules/prismjs/components/prism-css.min.js"
|
||||
],
|
||||
"vendorChunk": true,
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
export class ErrorDto {
|
||||
constructor(private readonly _code: string) {
|
||||
}
|
||||
|
||||
get code() {
|
||||
return this._code;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import {AfterViewInit, Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild} from '@angular/core';
|
||||
import {disableBodyScroll, enableBodyScroll} from 'body-scroll-lock';
|
||||
import {Accent} from "../../../business/item/folder/Accent.model";
|
||||
import {Accent} from '../../../business/item/folder/Accent.model';
|
||||
|
||||
@Component({
|
||||
selector: 'app-modal',
|
||||
@@ -18,7 +18,7 @@ export class ModalComponent implements OnInit, AfterViewInit {
|
||||
@Output()
|
||||
closed = new EventEmitter<void>();
|
||||
|
||||
@ViewChild('modalComponentDialog', { read: false })
|
||||
@ViewChild('modalComponentDialog')
|
||||
dialog: ElementRef;
|
||||
|
||||
isShown = false;
|
||||
|
||||
@@ -10,7 +10,6 @@ import {Background} from '../../../../business/item/folder/Background.model';
|
||||
import {ViewMode} from '../../../../business/item/folder/viewMode.model';
|
||||
import {ImageCroppedEvent} from 'ngx-image-cropper';
|
||||
import {OffsetDateTime} from '@js-joda/core';
|
||||
import {Attachment} from '../../../../business/attachment/Attachment.model';
|
||||
|
||||
@Component({
|
||||
selector: 'app-folder-edit',
|
||||
@@ -19,7 +18,7 @@ import {Attachment} from '../../../../business/attachment/Attachment.model';
|
||||
})
|
||||
export class FolderEditComponent implements OnInit {
|
||||
|
||||
@ViewChild(ModalComponent, {read: false})
|
||||
@ViewChild(ModalComponent)
|
||||
private dialog: ModalComponent;
|
||||
|
||||
public background: Folder;
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ export class StateColumnComponent implements OnInit {
|
||||
@Output() dragStart = new EventEmitter<TaskOverview>();
|
||||
@Output() dropFailed = new EventEmitter<null>();
|
||||
|
||||
@ViewChild('stateElement', { read: false })
|
||||
@ViewChild('stateElement')
|
||||
stateElement: ElementRef;
|
||||
|
||||
dragOver = false;
|
||||
|
||||
@@ -18,10 +18,10 @@ export class NoteHeaderComponent implements OnInit {
|
||||
@Input() public note: Note;
|
||||
@Input() public folder: Folder;
|
||||
|
||||
@ViewChild('infoDialog', {read: false})
|
||||
@ViewChild('infoDialog')
|
||||
private infoDialog: ModalComponent;
|
||||
|
||||
@ViewChild('deleteDialog', {read: false})
|
||||
@ViewChild('deleteDialog')
|
||||
private deleteDialog: ModalComponent;
|
||||
|
||||
hamburgerEntries: DropdownItem[] = [
|
||||
@@ -38,7 +38,6 @@ export class NoteHeaderComponent implements OnInit {
|
||||
];
|
||||
|
||||
constructor(private router: Router,
|
||||
private route: ActivatedRoute,
|
||||
private noteService: NoteService) {
|
||||
}
|
||||
|
||||
@@ -54,7 +53,7 @@ export class NoteHeaderComponent implements OnInit {
|
||||
}
|
||||
|
||||
public onDelete() {
|
||||
this.noteService.delete(this.note).then((ok: any) => {
|
||||
this.noteService.delete(this.note).then(() => {
|
||||
this.router.navigate(['/folder/' + this.note.folderId]);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -18,10 +18,10 @@ export class TaskHeaderComponent implements OnInit {
|
||||
@Input() public task: Task;
|
||||
@Input() public folder: Folder;
|
||||
|
||||
@ViewChild('infoDialog', {read: false})
|
||||
@ViewChild('infoDialog')
|
||||
private infoDialog: ModalComponent;
|
||||
|
||||
@ViewChild('deleteDialog', {read: false})
|
||||
@ViewChild('deleteDialog')
|
||||
private deleteDialog: ModalComponent;
|
||||
|
||||
hamburgerEntries: DropdownItem[] = [
|
||||
@@ -38,7 +38,6 @@ export class TaskHeaderComponent implements OnInit {
|
||||
];
|
||||
|
||||
constructor(private router: Router,
|
||||
private route: ActivatedRoute,
|
||||
private taskService: TaskService) {
|
||||
}
|
||||
|
||||
@@ -54,7 +53,7 @@ export class TaskHeaderComponent implements OnInit {
|
||||
}
|
||||
|
||||
public onDelete() {
|
||||
this.taskService.delete(this.task).then((ok: any) => {
|
||||
this.taskService.delete(this.task).then(() => {
|
||||
this.router.navigate(['/folder/' + this.task.folderId]);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
},
|
||||
"exclude": [
|
||||
"test.ts",
|
||||
"**/*.spec.ts"
|
||||
"**/*.spec.ts",
|
||||
"environments/environment.prod.ts"
|
||||
]
|
||||
}
|
||||
@@ -29,41 +29,6 @@ Until Keycloak is updated you have to:
|
||||
3. Disable "SameSite by default cookies" and "Cookies without SameSite must be secure" flags.
|
||||
4. Relaunch.
|
||||
|
||||
|
||||
OSM Request Trust Certificate
|
||||
=============================
|
||||
**Valid until:** 15. Sep 2025 18:00:00 CEST
|
||||
|
||||
Handle truststore.jks
|
||||
---------------------
|
||||
|
||||
### List truststore Certs:
|
||||
```
|
||||
cd backend/src/main/resources
|
||||
keytool -list -keystore truststore.jks
|
||||
keytool -list -v -keystore truststore.jks
|
||||
```
|
||||
|
||||
### Renew Root Cert in TrustStore
|
||||
1. Download new Cert \\\
|
||||
**From:** [Let's Encrypt](https://letsencrypt.org/certificates/) (Take Active -> Let’s Encrypt R3 Cert in the "der" format)
|
||||
|
||||
2. installing the Cert \\\
|
||||
```
|
||||
cd backend/src/main/resources
|
||||
rm truststore.jks
|
||||
keytool -import -keystore truststore.jks -storepass Tester11 -noprompt -trustcacerts -alias LetsEncryptR3CrossSigned -file ~/Downloads/lets-encrypt-r3-cross-signed.der
|
||||
keytool -import -keystore truststore.jks -storepass Tester11 -noprompt -trustcacerts -alias LetsEncryptR3 -file ~/Downloads/lets-encrypt-r3.der
|
||||
```
|
||||
|
||||
Elasticsrach connection
|
||||
=======================
|
||||
Add the Certs generated by Elasticsearch to the KeyStore:
|
||||
```
|
||||
keytool -import -v -trustcacerts -file ./deploy/localDev/boxit/elasticsearch/certs/ca/ca.crt -alias elasticsearch_ca_cert -keystore ./backend/src/main/resources/truststore.jks
|
||||
keytool -import -v -trustcacerts -file ./deploy/localDev/boxit/elasticsearch/certs/elasticsearch/elasticsearch.crt -alias elasticsearch_cert -keystore ./backend/src/main/resources/truststore.jks
|
||||
```
|
||||
|
||||
Artifacts
|
||||
=========
|
||||
- **LogoFont** \
|
||||
|
||||