mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2026-09-02 21:03:34 +03:00
Merge remote-tracking branch 'origin/main' into kill-policies-page
# Conflicts: # app/proprietary/src/main/java/stirling/software/proprietary/policy/model/Policy.java # app/proprietary/src/main/java/stirling/software/proprietary/policy/store/JpaPolicyStore.java # frontend/editor/src/portal/components/policies/PolicySetupWizard.tsx # frontend/editor/src/portal/views/PipelineBuilder.tsx # frontend/editor/src/portal/views/Pipelines.test.tsx # frontend/editor/src/portal/views/Pipelines.tsx # frontend/editor/src/portal/views/Policies.tsx
This commit is contained in:
@@ -182,7 +182,7 @@ jobs:
|
||||
fetch-depth: 0 # Fetch full history for commit hash detection
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
||||
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
|
||||
|
||||
- name: Get version number
|
||||
id: versionNumber
|
||||
@@ -220,6 +220,42 @@ jobs:
|
||||
echo "app_short=${APP_HASH:0:8}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
# The Stirling account previews connect to. Derived from the ref rather than stored as a URL
|
||||
# so it cannot drift from the key: a mismatched pair is accepted by the browser and rejected
|
||||
# by Supabase, surfacing much later as "session expired" on Usage rather than at sign-in.
|
||||
# Secret only to match Saas-Dev-Deploy.yml, which owns the same value; a project ref is not
|
||||
# itself sensitive, which is why SAAS_API_BASE_URL next to it is a plain variable.
|
||||
- name: Resolve Stirling account config
|
||||
id: saas
|
||||
env:
|
||||
PROJECT_REF: ${{ secrets.SAAS_DB_PROJECT_REF }}
|
||||
API_BASE_OVERRIDE: ${{ vars.SAAS_API_BASE_URL }}
|
||||
run: |
|
||||
# Set, this is the one value both halves use: the browser's portal reads and the backend's
|
||||
# register/entitlement calls have to land on the same SaaS, and nothing checks that they
|
||||
# do. Unset, only the backend gets a base, from its own compiled-in default.
|
||||
API_BASE="${API_BASE_OVERRIDE:-https://stirling.com/app}"
|
||||
echo "backend_base=${API_BASE}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
if [ -z "${PROJECT_REF}" ]; then
|
||||
echo "Not configured for this environment: the preview will build without a Stirling"
|
||||
echo "account, and the connect dialog will say so. To wire one up, set on the"
|
||||
echo "pr-preview environment the secrets SAAS_DB_PROJECT_REF and"
|
||||
echo "SAAS_SUPABASE_PUBLISHABLE_KEY, both from the same Supabase project."
|
||||
echo "supabase_url=" >> "$GITHUB_OUTPUT"
|
||||
echo "frontend_base=" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
# Only whether, not which: the ref is a secret here, so Actions masks it out of any
|
||||
# line it appears in, derived URL included.
|
||||
echo "Stirling account configured, at ${API_BASE}."
|
||||
echo "supabase_url=https://${PROJECT_REF}.supabase.co" >> "$GITHUB_OUTPUT"
|
||||
# Deliberately the override and not API_BASE: the backend's default is a subpath URL
|
||||
# nobody has confirmed answers /api/v1, and prod CORS does not list preview hostnames,
|
||||
# so portal reads stay off until someone sets a base they have checked. Empty leaves the
|
||||
# committed .env default alone, which is the clean "not configured" state.
|
||||
echo "frontend_base=${API_BASE_OVERRIDE}" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Check if image exists
|
||||
id: check-image
|
||||
run: |
|
||||
@@ -246,6 +282,9 @@ jobs:
|
||||
build-args: |
|
||||
VERSION_TAG=v2-alpha
|
||||
BUILD_PORTAL=${{ env.BUILD_PORTAL }}
|
||||
VITE_SUPABASE_URL=${{ steps.saas.outputs.supabase_url }}
|
||||
VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY=${{ secrets.SAAS_SUPABASE_PUBLISHABLE_KEY }}
|
||||
VITE_SAAS_API_URL=${{ steps.saas.outputs.frontend_base }}
|
||||
platforms: linux/amd64
|
||||
|
||||
- name: Set up SSH
|
||||
@@ -279,6 +318,13 @@ jobs:
|
||||
environment:
|
||||
DISABLE_ADDITIONAL_FEATURES: "false"
|
||||
STIRLING_BILLING_ACCOUNT_LINK_ENABLED: "true"
|
||||
STIRLING_BILLING_ACCOUNT_LINK_SAAS_BASE_URL: "${{ steps.saas.outputs.backend_base }}"
|
||||
# Off so preview traffic never accrues against a real wallet or trips its cap. The
|
||||
# 402 gate is separate and stays on, so gating is still testable here.
|
||||
STIRLING_BILLING_ACCOUNT_LINK_METERING_ENABLED: "false"
|
||||
# Stated rather than inferred from the request: the callback has to come back to the
|
||||
# preview hostname, not to the container's own :8080 behind this proxy.
|
||||
SYSTEM_FRONTENDURL: "https://${V2_PORT}.ssl.stirlingpdf.cloud"
|
||||
SECURITY_ENABLELOGIN: "true"
|
||||
SECURITY_INITIALLOGIN_USERNAME: "${TEST_LOGIN_USERNAME}"
|
||||
SECURITY_INITIALLOGIN_PASSWORD: "${TEST_LOGIN_PASSWORD}"
|
||||
@@ -353,7 +399,7 @@ jobs:
|
||||
|
||||
- name: Install Task for Storybook
|
||||
if: steps.sb-changes.outputs.storybook == 'true'
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
|
||||
|
||||
- name: Build and deploy Storybook
|
||||
id: storybook
|
||||
|
||||
@@ -206,7 +206,7 @@ jobs:
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
|
||||
- name: Run Gradle Command
|
||||
run: |
|
||||
if [ "${{ needs.check-comment.outputs.disable_security }}" == "true" ]; then
|
||||
@@ -222,7 +222,7 @@ jobs:
|
||||
STIRLING_PDF_DESKTOP_UI: false
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
||||
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
|
||||
uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -45,7 +45,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
||||
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
|
||||
@@ -34,10 +34,9 @@ jobs:
|
||||
cache-dependency-glob: |
|
||||
engine/pyproject.toml
|
||||
engine/uv.lock
|
||||
cache-suffix: ai-engine
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
|
||||
|
||||
- name: Quality-check engine
|
||||
id: engine-check
|
||||
|
||||
@@ -52,7 +52,7 @@ jobs:
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
|
||||
- name: Check Java formatting (Spotless)
|
||||
# Runs once per matrix combination - pick the cheapest leg
|
||||
# (core - no proprietary, no saas) so we don't wait for the
|
||||
|
||||
@@ -95,7 +95,7 @@ jobs:
|
||||
cache: "npm"
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
|
||||
- name: Install Playwright (chromium only)
|
||||
run: task e2e:install -- chromium
|
||||
- name: Build frontend (needed for playwright's vite preview webServer)
|
||||
|
||||
@@ -42,7 +42,6 @@ jobs:
|
||||
cache-dependency-glob: |
|
||||
engine/pyproject.toml
|
||||
engine/uv.lock
|
||||
cache-suffix: generated-models
|
||||
|
||||
- name: Restore cache Gradle User Home
|
||||
if: inputs.use_shared_cache
|
||||
@@ -76,7 +75,7 @@ jobs:
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
|
||||
|
||||
- name: Verify generated models are up to date
|
||||
id: models-check
|
||||
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
|
||||
- name: Check licenses for compatibility
|
||||
run: task backend:licenses:check
|
||||
env:
|
||||
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
|
||||
- name: Generate OpenAPI documentation
|
||||
run: task backend:swagger
|
||||
env:
|
||||
|
||||
@@ -57,7 +57,7 @@ jobs:
|
||||
# runtime token isn't exposed) since the docker driver can't use it.
|
||||
- name: Set up Docker Buildx
|
||||
if: inputs.docker-base-changed != 'true'
|
||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
||||
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
|
||||
|
||||
# Expose ACTIONS_RUNTIME_TOKEN / ACTIONS_RESULTS_URL for docker buildx type=gha cache backend.
|
||||
- name: Expose GitHub runtime for Buildx cache
|
||||
|
||||
@@ -45,7 +45,7 @@ jobs:
|
||||
cache: "npm"
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
|
||||
- name: Install Playwright (chromium only)
|
||||
run: task e2e:install -- chromium
|
||||
- name: Build frontend (production bundle for vite preview)
|
||||
|
||||
@@ -44,7 +44,7 @@ jobs:
|
||||
cache: "npm"
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
|
||||
- name: Build frontend (production bundle for vite preview)
|
||||
env:
|
||||
VITE_BUILD_FOR_PREVIEW: "1"
|
||||
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
cache: "npm"
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
|
||||
- name: a11y gate (changed stories)
|
||||
run: task frontend:storybook:a11y:changed -- origin/${{ github.base_ref || 'main' }}
|
||||
- name: Upload scan reports
|
||||
|
||||
@@ -97,7 +97,7 @@ jobs:
|
||||
run: npm ci --ignore-scripts --audit=false --fund=false
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
|
||||
|
||||
- name: Generate frontend license report (Push only)
|
||||
if: github.event_name == 'push'
|
||||
@@ -367,7 +367,7 @@ jobs:
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
|
||||
|
||||
- name: Check licenses and generate report
|
||||
id: license-check
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
cache: "npm"
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
|
||||
- name: Quality-check frontend
|
||||
id: frontend-check
|
||||
run: task frontend:check:all
|
||||
|
||||
@@ -69,7 +69,7 @@ jobs:
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
|
||||
- name: Get version number
|
||||
id: versionNumber
|
||||
run: |
|
||||
@@ -169,7 +169,7 @@ jobs:
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
|
||||
|
||||
- name: Build JAR
|
||||
run: ./gradlew build ${{ matrix.variant.build_frontend && '-PbuildWithFrontend=true' || '' }} -x spotlessApply -x spotlessCheck -x test -x sonarqube
|
||||
@@ -268,7 +268,7 @@ jobs:
|
||||
distribution: ${{ matrix.platform == 'windows-11-arm' && 'microsoft' || 'temurin' }}
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
|
||||
|
||||
# Build the universal JRE before desktop:prepare so the jlink:runtime
|
||||
# task short-circuits on its `test -d runtime/jre` status check.
|
||||
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
|
||||
- name: Install all Playwright browsers
|
||||
run: task e2e:install
|
||||
|
||||
@@ -89,7 +89,7 @@ jobs:
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
|
||||
|
||||
- name: a11y gate (every story, ${{ matrix.theme }})
|
||||
run: task frontend:storybook:a11y:${{ matrix.theme }}
|
||||
@@ -162,7 +162,7 @@ jobs:
|
||||
engine/uv.lock
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
|
||||
|
||||
- name: Start the fat image with login and storage enabled
|
||||
run: docker compose -f docker/embedded/compose/test_cicd.yml up -d --build
|
||||
|
||||
@@ -31,10 +31,9 @@ jobs:
|
||||
cache-dependency-glob: |
|
||||
engine/pyproject.toml
|
||||
engine/uv.lock
|
||||
cache-suffix: pre-commit
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
|
||||
|
||||
- name: Run pre-commit checks
|
||||
run: task pre-commit
|
||||
|
||||
@@ -69,7 +69,7 @@ jobs:
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
||||
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
|
||||
|
||||
@@ -85,10 +85,10 @@ jobs:
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
||||
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
|
||||
- name: Get version number
|
||||
id: versionNumber
|
||||
run: echo "versionNumber=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT
|
||||
|
||||
@@ -75,6 +75,6 @@ jobs:
|
||||
|
||||
# Upload the results to GitHub's code scanning dashboard.
|
||||
- name: "Upload to code-scanning"
|
||||
uses: github/codeql-action/upload-sarif@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7
|
||||
uses: github/codeql-action/upload-sarif@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
|
||||
@@ -63,7 +63,7 @@ jobs:
|
||||
SWAGGERHUB_USER: "Frooodle"
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
|
||||
- name: Get version number
|
||||
id: versionNumber
|
||||
run: echo "versionNumber=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT
|
||||
|
||||
@@ -59,14 +59,13 @@ jobs:
|
||||
cache-dependency-glob: |
|
||||
engine/pyproject.toml
|
||||
engine/uv.lock
|
||||
cache-suffix: sync-files
|
||||
|
||||
- name: Install Python dependencies
|
||||
run: |
|
||||
uv sync --project engine --locked --group tools
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
|
||||
|
||||
- name: Sync translation TOML files
|
||||
run: |
|
||||
|
||||
@@ -212,7 +212,7 @@ jobs:
|
||||
distribution: ${{ matrix.platform == 'windows-11-arm' && 'microsoft' || 'temurin' }}
|
||||
|
||||
- name: Setup Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
|
||||
|
||||
- name: Build universal macOS JRE
|
||||
if: matrix.platform == 'macos-15'
|
||||
|
||||
@@ -127,7 +127,7 @@ jobs:
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
|
||||
- name: Build application
|
||||
run: task backend:build
|
||||
env:
|
||||
@@ -142,7 +142,7 @@ jobs:
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
||||
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
|
||||
|
||||
- name: Set base image and platform for this build
|
||||
id: build-params
|
||||
@@ -229,7 +229,7 @@ jobs:
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
||||
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
|
||||
|
||||
- name: Build docker/unoserver/Dockerfile
|
||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
||||
|
||||
@@ -48,7 +48,7 @@ public class EndpointConfiguration {
|
||||
private final ApplicationProperties applicationProperties;
|
||||
@Getter private Map<String, Boolean> endpointStatuses = new ConcurrentHashMap<>();
|
||||
private Map<String, Set<String>> endpointGroups = new ConcurrentHashMap<>();
|
||||
private Set<String> disabledGroups = new HashSet<>();
|
||||
private Set<String> disabledGroups = ConcurrentHashMap.newKeySet();
|
||||
private Map<String, DisableReason> endpointDisableReasons = new ConcurrentHashMap<>();
|
||||
private Map<String, DisableReason> groupDisableReasons = new ConcurrentHashMap<>();
|
||||
private Map<String, Set<String>> endpointAlternatives = new ConcurrentHashMap<>();
|
||||
|
||||
@@ -237,7 +237,7 @@ public class TabulaTableParser implements TableParser {
|
||||
score -= 0.3f;
|
||||
}
|
||||
|
||||
return Math.max(0f, Math.min(1f, score));
|
||||
return Math.clamp(score, 0f, 1f);
|
||||
}
|
||||
|
||||
private Bounds tableBounds(Table table) {
|
||||
|
||||
+2
-1
@@ -15,7 +15,8 @@ public class StringToMapPropertyEditor extends PropertyEditorSupport {
|
||||
@Override
|
||||
public void setAsText(String text) throws IllegalArgumentException {
|
||||
try {
|
||||
TypeReference<HashMap<String, String>> typeRef = new TypeReference<>() {};
|
||||
TypeReference<HashMap<String, String>> typeRef =
|
||||
new TypeReference<HashMap<String, String>>() {};
|
||||
Map<String, String> map = objectMapper.readValue(text, typeRef);
|
||||
setValue(map);
|
||||
} catch (Exception e) {
|
||||
|
||||
+1
-1
@@ -237,7 +237,7 @@ public class EditTextController {
|
||||
|
||||
Matcher matcher = edit.pattern().matcher(joined);
|
||||
List<MatchSpan> spans = new ArrayList<>();
|
||||
StringBuffer interpolation = new StringBuffer();
|
||||
StringBuilder interpolation = new StringBuilder();
|
||||
int previousAppendPosition = 0;
|
||||
while (matcher.find()) {
|
||||
if (matcher.start() == matcher.end()) {
|
||||
|
||||
@@ -95,7 +95,8 @@ public class UIDataController {
|
||||
|
||||
try (InputStream is = resource.getInputStream()) {
|
||||
Map<String, List<Dependency>> licenseData =
|
||||
objectMapper.readValue(is, new TypeReference<>() {});
|
||||
objectMapper.readValue(
|
||||
is, new TypeReference<Map<String, List<Dependency>>>() {});
|
||||
data.setDependencies(licenseData.get("dependencies"));
|
||||
} catch (IOException e) {
|
||||
log.error("Failed to load licenses data", e);
|
||||
|
||||
+6
-3
@@ -25,12 +25,15 @@ final class FormPayloadParser {
|
||||
private static final String KEY_VALUE = "value";
|
||||
private static final String KEY_DEFAULT_VALUE = "defaultValue";
|
||||
|
||||
private static final TypeReference<Map<String, Object>> MAP_TYPE = new TypeReference<>() {};
|
||||
private static final TypeReference<Map<String, Object>> MAP_TYPE =
|
||||
new TypeReference<Map<String, Object>>() {};
|
||||
private static final TypeReference<List<FormUtils.ModifyFormFieldDefinition>>
|
||||
MODIFY_FIELD_LIST_TYPE = new TypeReference<>() {};
|
||||
MODIFY_FIELD_LIST_TYPE =
|
||||
new TypeReference<List<FormUtils.ModifyFormFieldDefinition>>() {};
|
||||
private static final TypeReference<List<FormUtils.NewFormFieldDefinition>> NEW_FIELD_LIST_TYPE =
|
||||
new TypeReference<>() {};
|
||||
private static final TypeReference<List<String>> STRING_LIST_TYPE = new TypeReference<>() {};
|
||||
private static final TypeReference<List<String>> STRING_LIST_TYPE =
|
||||
new TypeReference<List<String>>() {};
|
||||
|
||||
private FormPayloadParser() {}
|
||||
|
||||
|
||||
+3
-1
@@ -96,7 +96,9 @@ public class AddCommentsController {
|
||||
|
||||
List<CommentSpecDto> dtos;
|
||||
try {
|
||||
dtos = objectMapper.readValue(commentsJson, new TypeReference<>() {});
|
||||
dtos =
|
||||
objectMapper.readValue(
|
||||
commentsJson, new TypeReference<List<CommentSpecDto>>() {});
|
||||
} catch (JacksonException e) {
|
||||
throw new ResponseStatusException(
|
||||
HttpStatus.BAD_REQUEST, "comments must be a JSON array of CommentSpec objects");
|
||||
|
||||
+13
@@ -338,6 +338,19 @@ public class ConfigController {
|
||||
// Premium/Enterprise settings
|
||||
configData.put("premiumEnabled", applicationProperties.getPremium().isEnabled());
|
||||
|
||||
// Whether this instance can link a Stirling (SaaS) account at all. The account-link
|
||||
// beans live in :proprietary and are @ConditionalOnProperty on this same key, so when
|
||||
// it is off they are absent and /api/v1/account-link/* returns 404. The frontend cannot
|
||||
// tell that 404 apart from "not linked yet", so it needs this told to it explicitly
|
||||
// before it can prompt anyone to link. Read from the environment rather than
|
||||
// AccountLinkProperties because :core must not depend on :proprietary.
|
||||
configData.put(
|
||||
"accountLinkAvailable",
|
||||
applicationContext
|
||||
.getEnvironment()
|
||||
.getProperty(
|
||||
"stirling.billing.account-link.enabled", Boolean.class, false));
|
||||
|
||||
// AI Engine settings
|
||||
ApplicationProperties.AiEngine aiEngineConfig = applicationProperties.getAiEngine();
|
||||
configData.put("aiEngineEnabled", aiEngineConfig.isEnabled());
|
||||
|
||||
+11
-8
@@ -4,7 +4,9 @@ import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -21,7 +23,7 @@ public class WeeklyActiveUsersService {
|
||||
private final Map<String, Instant> activeBrowsers = new ConcurrentHashMap<>();
|
||||
|
||||
// Track total unique browsers seen (overall)
|
||||
private long totalUniqueBrowsers = 0;
|
||||
private final AtomicLong totalUniqueBrowsers = new AtomicLong(0);
|
||||
|
||||
// Application start time
|
||||
private final Instant startTime = Instant.now();
|
||||
@@ -36,12 +38,12 @@ public class WeeklyActiveUsersService {
|
||||
return;
|
||||
}
|
||||
|
||||
boolean isNewBrowser = !activeBrowsers.containsKey(browserId);
|
||||
activeBrowsers.put(browserId, Instant.now());
|
||||
Instant now = Instant.now();
|
||||
Instant previous = activeBrowsers.put(browserId, now);
|
||||
|
||||
if (isNewBrowser) {
|
||||
totalUniqueBrowsers++;
|
||||
log.debug("New browser recorded: {} (Total: {})", browserId, totalUniqueBrowsers);
|
||||
if (previous == null) {
|
||||
long total = totalUniqueBrowsers.incrementAndGet();
|
||||
log.debug("New browser recorded: {} (Total: {})", browserId, total);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +63,7 @@ public class WeeklyActiveUsersService {
|
||||
* @return Total unique browsers count
|
||||
*/
|
||||
public long getTotalUniqueBrowsers() {
|
||||
return totalUniqueBrowsers;
|
||||
return totalUniqueBrowsers.get();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -88,7 +90,8 @@ public class WeeklyActiveUsersService {
|
||||
activeBrowsers.entrySet().removeIf(entry -> entry.getValue().isBefore(sevenDaysAgo));
|
||||
}
|
||||
|
||||
/** Manual cleanup trigger (can be called by scheduled task if needed) */
|
||||
/** Scheduled cleanup trigger running every hour */
|
||||
@Scheduled(fixedRate = 3600000)
|
||||
public void performCleanup() {
|
||||
int sizeBefore = activeBrowsers.size();
|
||||
cleanupOldEntries();
|
||||
|
||||
@@ -94,7 +94,7 @@
|
||||
{
|
||||
"moduleName": "com.fasterxml.jackson.core:jackson-core",
|
||||
"moduleUrl": "https://github.com/FasterXML/jackson-core",
|
||||
"moduleVersion": "2.22.1",
|
||||
"moduleVersion": "2.22.2",
|
||||
"moduleLicense": "The Apache Software License, Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt"
|
||||
},
|
||||
@@ -108,7 +108,7 @@
|
||||
{
|
||||
"moduleName": "com.fasterxml.jackson.core:jackson-databind",
|
||||
"moduleUrl": "https://github.com/FasterXML/jackson",
|
||||
"moduleVersion": "2.22.1",
|
||||
"moduleVersion": "2.22.2",
|
||||
"moduleLicense": "The Apache Software License, Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt"
|
||||
},
|
||||
@@ -143,7 +143,7 @@
|
||||
{
|
||||
"moduleName": "com.fasterxml.jackson:jackson-bom",
|
||||
"moduleUrl": "https://github.com/FasterXML/jackson-bom",
|
||||
"moduleVersion": "2.22.1",
|
||||
"moduleVersion": "2.22.2",
|
||||
"moduleLicense": "The Apache Software License, Version 2.0",
|
||||
"moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt"
|
||||
},
|
||||
@@ -440,42 +440,14 @@
|
||||
{
|
||||
"moduleName": "com.stirling:jpdfium",
|
||||
"moduleUrl": "https://github.com/Stirling-Tools/JPDFium",
|
||||
"moduleVersion": "1.0.4",
|
||||
"moduleLicense": "MIT License",
|
||||
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
|
||||
},
|
||||
{
|
||||
"moduleName": "com.stirling:jpdfium-natives-darwin-arm64",
|
||||
"moduleUrl": "https://github.com/Stirling-Tools/JPDFium",
|
||||
"moduleVersion": "1.0.4",
|
||||
"moduleLicense": "MIT License",
|
||||
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
|
||||
},
|
||||
{
|
||||
"moduleName": "com.stirling:jpdfium-natives-darwin-x64",
|
||||
"moduleUrl": "https://github.com/Stirling-Tools/JPDFium",
|
||||
"moduleVersion": "1.0.4",
|
||||
"moduleLicense": "MIT License",
|
||||
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
|
||||
},
|
||||
{
|
||||
"moduleName": "com.stirling:jpdfium-natives-linux-arm64",
|
||||
"moduleUrl": "https://github.com/Stirling-Tools/JPDFium",
|
||||
"moduleVersion": "1.0.4",
|
||||
"moduleVersion": "1.1.3",
|
||||
"moduleLicense": "MIT License",
|
||||
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
|
||||
},
|
||||
{
|
||||
"moduleName": "com.stirling:jpdfium-natives-linux-x64",
|
||||
"moduleUrl": "https://github.com/Stirling-Tools/JPDFium",
|
||||
"moduleVersion": "1.0.4",
|
||||
"moduleLicense": "MIT License",
|
||||
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
|
||||
},
|
||||
{
|
||||
"moduleName": "com.stirling:jpdfium-natives-windows-x64",
|
||||
"moduleUrl": "https://github.com/Stirling-Tools/JPDFium",
|
||||
"moduleVersion": "1.0.4",
|
||||
"moduleVersion": "1.1.3",
|
||||
"moduleLicense": "MIT License",
|
||||
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
|
||||
},
|
||||
@@ -521,36 +493,18 @@
|
||||
"moduleLicense": "GNU General Public License, version 2 with the GNU Classpath Exception",
|
||||
"moduleLicenseUrl": "https://www.gnu.org/software/classpath/license.html"
|
||||
},
|
||||
{
|
||||
"moduleName": "com.twelvemonkeys.common:common-image",
|
||||
"moduleVersion": "3.13.1",
|
||||
"moduleLicense": "The BSD License",
|
||||
"moduleLicenseUrl": "https://github.com/haraldk/TwelveMonkeys#license"
|
||||
},
|
||||
{
|
||||
"moduleName": "com.twelvemonkeys.common:common-image",
|
||||
"moduleVersion": "3.14.0",
|
||||
"moduleLicense": "The BSD License",
|
||||
"moduleLicenseUrl": "https://github.com/haraldk/TwelveMonkeys#license"
|
||||
},
|
||||
{
|
||||
"moduleName": "com.twelvemonkeys.common:common-io",
|
||||
"moduleVersion": "3.13.1",
|
||||
"moduleLicense": "The BSD License",
|
||||
"moduleLicenseUrl": "https://github.com/haraldk/TwelveMonkeys#license"
|
||||
},
|
||||
{
|
||||
"moduleName": "com.twelvemonkeys.common:common-io",
|
||||
"moduleVersion": "3.14.0",
|
||||
"moduleLicense": "The BSD License",
|
||||
"moduleLicenseUrl": "https://github.com/haraldk/TwelveMonkeys#license"
|
||||
},
|
||||
{
|
||||
"moduleName": "com.twelvemonkeys.common:common-lang",
|
||||
"moduleVersion": "3.13.1",
|
||||
"moduleLicense": "The BSD License",
|
||||
"moduleLicenseUrl": "https://github.com/haraldk/TwelveMonkeys#license"
|
||||
},
|
||||
{
|
||||
"moduleName": "com.twelvemonkeys.common:common-lang",
|
||||
"moduleVersion": "3.14.0",
|
||||
@@ -569,12 +523,6 @@
|
||||
"moduleLicense": "The BSD License",
|
||||
"moduleLicenseUrl": "https://github.com/haraldk/TwelveMonkeys#license"
|
||||
},
|
||||
{
|
||||
"moduleName": "com.twelvemonkeys.imageio:imageio-core",
|
||||
"moduleVersion": "3.13.1",
|
||||
"moduleLicense": "The BSD License",
|
||||
"moduleLicenseUrl": "https://github.com/haraldk/TwelveMonkeys#license"
|
||||
},
|
||||
{
|
||||
"moduleName": "com.twelvemonkeys.imageio:imageio-core",
|
||||
"moduleVersion": "3.14.0",
|
||||
@@ -587,12 +535,6 @@
|
||||
"moduleLicense": "The BSD License",
|
||||
"moduleLicenseUrl": "https://github.com/haraldk/TwelveMonkeys#license"
|
||||
},
|
||||
{
|
||||
"moduleName": "com.twelvemonkeys.imageio:imageio-metadata",
|
||||
"moduleVersion": "3.13.1",
|
||||
"moduleLicense": "The BSD License",
|
||||
"moduleLicenseUrl": "https://github.com/haraldk/TwelveMonkeys#license"
|
||||
},
|
||||
{
|
||||
"moduleName": "com.twelvemonkeys.imageio:imageio-metadata",
|
||||
"moduleVersion": "3.14.0",
|
||||
@@ -605,24 +547,12 @@
|
||||
"moduleLicense": "The BSD License",
|
||||
"moduleLicenseUrl": "https://github.com/haraldk/TwelveMonkeys#license"
|
||||
},
|
||||
{
|
||||
"moduleName": "com.twelvemonkeys.imageio:imageio-tiff",
|
||||
"moduleVersion": "3.13.1",
|
||||
"moduleLicense": "The BSD License",
|
||||
"moduleLicenseUrl": "https://github.com/haraldk/TwelveMonkeys#license"
|
||||
},
|
||||
{
|
||||
"moduleName": "com.twelvemonkeys.imageio:imageio-tiff",
|
||||
"moduleVersion": "3.14.0",
|
||||
"moduleLicense": "The BSD License",
|
||||
"moduleLicenseUrl": "https://github.com/haraldk/TwelveMonkeys#license"
|
||||
},
|
||||
{
|
||||
"moduleName": "com.twelvemonkeys.imageio:imageio-webp",
|
||||
"moduleVersion": "3.13.1",
|
||||
"moduleLicense": "The BSD License",
|
||||
"moduleLicenseUrl": "https://github.com/haraldk/TwelveMonkeys#license"
|
||||
},
|
||||
{
|
||||
"moduleName": "com.twelvemonkeys.imageio:imageio-webp",
|
||||
"moduleVersion": "3.14.0",
|
||||
@@ -769,13 +699,6 @@
|
||||
"moduleLicense": "The Apache Software License, Version 2.0",
|
||||
"moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt"
|
||||
},
|
||||
{
|
||||
"moduleName": "commons-beanutils:commons-beanutils",
|
||||
"moduleUrl": "https://commons.apache.org/proper/commons-beanutils",
|
||||
"moduleVersion": "1.11.0",
|
||||
"moduleLicense": "Apache-2.0",
|
||||
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt"
|
||||
},
|
||||
{
|
||||
"moduleName": "commons-cli:commons-cli",
|
||||
"moduleUrl": "http://commons.apache.org/proper/commons-cli/",
|
||||
@@ -790,13 +713,6 @@
|
||||
"moduleLicense": "Apache-2.0",
|
||||
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt"
|
||||
},
|
||||
{
|
||||
"moduleName": "commons-collections:commons-collections",
|
||||
"moduleUrl": "http://commons.apache.org/collections/",
|
||||
"moduleVersion": "3.2.2",
|
||||
"moduleLicense": "Apache License, Version 2.0",
|
||||
"moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt"
|
||||
},
|
||||
{
|
||||
"moduleName": "commons-io:commons-io",
|
||||
"moduleUrl": "https://commons.apache.org/proper/commons-io/",
|
||||
@@ -1360,13 +1276,6 @@
|
||||
"moduleLicense": "Apache-2.0",
|
||||
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.apache.commons:commons-math3",
|
||||
"moduleUrl": "http://commons.apache.org/proper/commons-math/",
|
||||
"moduleVersion": "3.6.1",
|
||||
"moduleLicense": "Apache License, Version 2.0",
|
||||
"moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.apache.commons:commons-text",
|
||||
"moduleUrl": "https://commons.apache.org/proper/commons-text",
|
||||
|
||||
@@ -59,7 +59,7 @@ public enum AuditLevel {
|
||||
*/
|
||||
public static AuditLevel fromInt(int level) {
|
||||
// Ensure level is within valid bounds
|
||||
int boundedLevel = Math.min(Math.max(level, 0), 3);
|
||||
int boundedLevel = Math.clamp(level, 0, 3);
|
||||
|
||||
for (AuditLevel auditLevel : values()) {
|
||||
if (auditLevel.level == boundedLevel) {
|
||||
|
||||
+11
-9
@@ -17,16 +17,16 @@ import org.springframework.data.redis.core.ScanOptions;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.cluster.JobStore;
|
||||
import stirling.software.common.cluster.JobStoreEntry;
|
||||
|
||||
import tools.jackson.core.JacksonException;
|
||||
import tools.jackson.core.type.TypeReference;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
/**
|
||||
* Valkey-backed {@link JobStore}. Each job is one hash; a reverse index maps fileId to jobId.
|
||||
*
|
||||
@@ -44,8 +44,10 @@ public class ValkeyJobStore implements JobStore {
|
||||
private static final String FILE_INDEX_PREFIX = "stirling:file2job:";
|
||||
|
||||
private static final ObjectMapper MAPPER = new ObjectMapper();
|
||||
private static final TypeReference<List<String>> LIST_STRING = new TypeReference<>() {};
|
||||
private static final TypeReference<Map<String, String>> MAP_STRING = new TypeReference<>() {};
|
||||
private static final TypeReference<List<String>> LIST_STRING =
|
||||
new TypeReference<List<String>>() {};
|
||||
private static final TypeReference<Map<String, String>> MAP_STRING =
|
||||
new TypeReference<Map<String, String>>() {};
|
||||
|
||||
private final StringRedisTemplate template;
|
||||
|
||||
@@ -265,7 +267,7 @@ public class ValkeyJobStore implements JobStore {
|
||||
}
|
||||
try {
|
||||
return MAPPER.readValue(v.toString(), MAP_STRING);
|
||||
} catch (JsonProcessingException e) {
|
||||
} catch (JacksonException e) {
|
||||
log.warn(
|
||||
"JobStore {} field 'resultMeta' is not valid JSON '{}' - treating as empty",
|
||||
key,
|
||||
@@ -277,7 +279,7 @@ public class ValkeyJobStore implements JobStore {
|
||||
private static String writeJson(Object value) {
|
||||
try {
|
||||
return MAPPER.writeValueAsString(value);
|
||||
} catch (JsonProcessingException e) {
|
||||
} catch (JacksonException e) {
|
||||
throw new IllegalStateException("Failed to JSON-serialize JobStore field", e);
|
||||
}
|
||||
}
|
||||
@@ -286,7 +288,7 @@ public class ValkeyJobStore implements JobStore {
|
||||
try {
|
||||
List<String> parsed = MAPPER.readValue(json, LIST_STRING);
|
||||
return parsed == null ? new ArrayList<>() : parsed;
|
||||
} catch (JsonProcessingException e) {
|
||||
} catch (JacksonException e) {
|
||||
log.warn(
|
||||
"JobStore {} field 'fileIds' is not valid JSON '{}' - treating as empty",
|
||||
key,
|
||||
|
||||
+1
-1
@@ -35,7 +35,7 @@ public class AuditConfigurationProperties {
|
||||
|
||||
// Ensure level is within valid bounds (0-3)
|
||||
int configLevel = auditConfig.getLevel();
|
||||
this.level = Math.min(Math.max(configLevel, 0), 3);
|
||||
this.level = Math.clamp(configLevel, 0, 3);
|
||||
|
||||
// Retention days (0 means infinite)
|
||||
this.retentionDays = auditConfig.getRetentionDays();
|
||||
|
||||
+1
-1
@@ -48,7 +48,7 @@ public class UsageRestController {
|
||||
@RequestParam(value = "dataType", defaultValue = "all") String dataType,
|
||||
@RequestParam(value = "days", defaultValue = "30") Integer days) {
|
||||
|
||||
int lookbackDays = Math.max(1, Math.min(days, 365));
|
||||
int lookbackDays = Math.clamp(days, 1, 365);
|
||||
|
||||
// Get audit events filtered by type
|
||||
List<PersistentAuditEvent> events = getEventsByDataType(dataType, lookbackDays);
|
||||
|
||||
+2
-1
@@ -1,5 +1,6 @@
|
||||
package stirling.software.proprietary.model;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
@@ -19,7 +20,7 @@ import lombok.*;
|
||||
@ToString
|
||||
public class UserLicenseSettings implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
@Serial private static final long serialVersionUID = 1L;
|
||||
|
||||
public static final Long SINGLETON_ID = 1L;
|
||||
|
||||
|
||||
+11
-1
@@ -336,6 +336,15 @@ public class PolicyController {
|
||||
* nothing to check.
|
||||
*/
|
||||
private void requireAccessibleOutput(Policy policy) {
|
||||
// An editor policy hands its results back to the workspace the file came from. A stored
|
||||
// destination would send the run to a folder or bucket instead, leaving the editor's copy
|
||||
// untouched - and the editor's import would then have nothing to collect.
|
||||
if (policy.editor().allowed() && !policy.outputIds().isEmpty()) {
|
||||
throw new ResponseStatusException(
|
||||
HttpStatus.BAD_REQUEST,
|
||||
"An editor policy delivers back to the editor and can't also have a"
|
||||
+ " destination");
|
||||
}
|
||||
for (String outputId : policy.outputIds()) {
|
||||
Source destination =
|
||||
sourceStore
|
||||
@@ -395,7 +404,8 @@ public class PolicyController {
|
||||
policy.steps(),
|
||||
policy.output(),
|
||||
policy.outputIds(),
|
||||
teamId);
|
||||
teamId,
|
||||
policy.editor());
|
||||
}
|
||||
|
||||
/** Output secrets never leave the server: reads return the redaction sentinel instead. */
|
||||
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package stirling.software.proprietary.policy.model;
|
||||
|
||||
/**
|
||||
* How a policy participates in the editor: it fires in the browser as each file passes through,
|
||||
* rather than being swept from a stored {@code Source} on a trigger.
|
||||
*
|
||||
* <p>An object rather than a bare flag so the moment it fires ({@code runOn}) travels with the
|
||||
* decision, and so later editor-only settings have somewhere to live.
|
||||
*
|
||||
* @param allowed whether the editor may run this policy at all
|
||||
* @param runOn which moment it fires on: {@code "upload"} or {@code "export"}
|
||||
*/
|
||||
public record EditorConfig(boolean allowed, String runOn) {
|
||||
|
||||
public static final String UPLOAD = "upload";
|
||||
public static final String EXPORT = "export";
|
||||
|
||||
public EditorConfig {
|
||||
runOn = EXPORT.equals(runOn) ? EXPORT : UPLOAD;
|
||||
}
|
||||
|
||||
/** Not an editor policy: swept server-side, or run only on demand. */
|
||||
public static EditorConfig disabled() {
|
||||
return new EditorConfig(false, UPLOAD);
|
||||
}
|
||||
|
||||
public static EditorConfig onUpload() {
|
||||
return new EditorConfig(true, UPLOAD);
|
||||
}
|
||||
|
||||
public static EditorConfig onExport() {
|
||||
return new EditorConfig(true, EXPORT);
|
||||
}
|
||||
}
|
||||
+39
-8
@@ -1,6 +1,7 @@
|
||||
package stirling.software.proprietary.policy.model;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* A stored automation: ordered tool steps, input bindings, and output destinations.
|
||||
@@ -26,7 +27,8 @@ public record Policy(
|
||||
List<PipelineStep> steps,
|
||||
OutputSpec output,
|
||||
List<String> outputIds,
|
||||
Long teamId) {
|
||||
Long teamId,
|
||||
EditorConfig editor) {
|
||||
|
||||
public Policy {
|
||||
icon = icon == null ? "" : icon;
|
||||
@@ -34,12 +36,13 @@ public record Policy(
|
||||
steps = steps == null ? List.of() : steps;
|
||||
output = output == null ? OutputSpec.inline() : output;
|
||||
outputIds = outputIds == null ? List.of() : List.copyOf(outputIds);
|
||||
editor = editor == null ? EditorConfig.disabled() : editor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Without the {@code required} flag or an {@code icon}: defaults to not org-required and no
|
||||
* icon. Kept for the many callers and tests written before those existed; the frontend and
|
||||
* stores that care use the full constructor.
|
||||
* Without the {@code required} flag, {@code icon}, or editor participation: defaults to not
|
||||
* org-required, no icon, and a swept/on-demand policy. Kept for the many callers and tests
|
||||
* written before those fields; the frontend and stores that care use the full constructor.
|
||||
*/
|
||||
public Policy(
|
||||
String id,
|
||||
@@ -51,7 +54,26 @@ public record Policy(
|
||||
OutputSpec output,
|
||||
List<String> outputIds,
|
||||
Long teamId) {
|
||||
this(id, name, owner, enabled, false, "", inputs, steps, output, outputIds, teamId);
|
||||
this(id, name, owner, enabled, false, "", inputs, steps, output, outputIds, teamId, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Without the {@code required} flag or {@code icon} but with explicit editor participation: the
|
||||
* seeded Classification policy runs on the editor, so it must set {@link EditorConfig} even
|
||||
* though it predates the org-required and icon fields.
|
||||
*/
|
||||
public Policy(
|
||||
String id,
|
||||
String name,
|
||||
String owner,
|
||||
boolean enabled,
|
||||
List<PipelineInput> inputs,
|
||||
List<PipelineStep> steps,
|
||||
OutputSpec output,
|
||||
List<String> outputIds,
|
||||
Long teamId,
|
||||
EditorConfig editor) {
|
||||
this(id, name, owner, enabled, false, "", inputs, steps, output, outputIds, teamId, editor);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -91,6 +113,14 @@ public record Policy(
|
||||
return inputs.stream().map(PipelineInput::sourceId).toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* The moment this policy fires in the editor ("upload" / "export"), or empty when the editor
|
||||
* does not run it. Legacy blobs are lifted onto {@link EditorConfig} when they are read.
|
||||
*/
|
||||
public Optional<String> editorRunOn() {
|
||||
return editor.allowed() ? Optional.of(editor.runOn()) : Optional.empty();
|
||||
}
|
||||
|
||||
/** The distinct trigger types configured across this policy's inputs (manual inputs aside). */
|
||||
public List<String> triggerTypes() {
|
||||
return inputs.stream()
|
||||
@@ -105,14 +135,14 @@ public record Policy(
|
||||
public Policy withOutput(OutputSpec resolved) {
|
||||
return new Policy(
|
||||
id, name, owner, enabled, required, icon, inputs, steps, resolved, outputIds,
|
||||
teamId);
|
||||
teamId, editor);
|
||||
}
|
||||
|
||||
/** A copy under a different owner (e.g. moving a seed off a placeholder name). */
|
||||
public Policy withOwner(String newOwner) {
|
||||
return new Policy(
|
||||
id, name, newOwner, enabled, required, icon, inputs, steps, output, outputIds,
|
||||
teamId);
|
||||
teamId, editor);
|
||||
}
|
||||
|
||||
/** A copy referencing the given saved output destinations. */
|
||||
@@ -128,7 +158,8 @@ public record Policy(
|
||||
steps,
|
||||
output,
|
||||
newOutputIds,
|
||||
teamId);
|
||||
teamId,
|
||||
editor);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+5
-1
@@ -118,10 +118,14 @@ public class PolicyOverviewService {
|
||||
/**
|
||||
* Summarise a policy's triggers for the overview row: "manual" when no input is triggered,
|
||||
* otherwise the distinct trigger types across its inputs (e.g. "folder-watch, schedule").
|
||||
*
|
||||
* <p>An editor policy has no wire input to trigger, but it is not manual either - it fires in
|
||||
* the editor on every upload or export, so it reports that rather than reading as on-demand.
|
||||
*/
|
||||
private static String triggerSummary(Policy policy) {
|
||||
List<String> types = policy.triggerTypes();
|
||||
return types.isEmpty() ? "manual" : String.join(", ", types);
|
||||
if (!types.isEmpty()) return String.join(", ", types);
|
||||
return policy.editorRunOn().map(runOn -> "editor-" + runOn).orElse("manual");
|
||||
}
|
||||
|
||||
private static String outputSummary(OutputSpec output) {
|
||||
|
||||
+6
-3
@@ -14,6 +14,7 @@ import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.proprietary.model.TeamCreatedEvent;
|
||||
import stirling.software.proprietary.policy.model.EditorConfig;
|
||||
import stirling.software.proprietary.policy.model.OutputSpec;
|
||||
import stirling.software.proprietary.policy.model.PipelineStep;
|
||||
import stirling.software.proprietary.policy.model.Policy;
|
||||
@@ -98,9 +99,8 @@ public class DefaultClassificationPolicySeeder {
|
||||
static Policy defaultPolicy(Long teamId) {
|
||||
Map<String, Object> options = new HashMap<>();
|
||||
options.put("categoryId", CATEGORY);
|
||||
options.put("runOn", "upload");
|
||||
options.put("mode", "new_version");
|
||||
options.put("sources", List.of("editor"));
|
||||
options.put("sources", List.of());
|
||||
options.put("scopeTypes", List.of());
|
||||
options.put("reviewerEmail", "");
|
||||
return new Policy(
|
||||
@@ -113,6 +113,9 @@ public class DefaultClassificationPolicySeeder {
|
||||
List.of(),
|
||||
List.of(new PipelineStep(CLASSIFY_ENDPOINT, Map.of())),
|
||||
new OutputSpec("inline", options),
|
||||
teamId);
|
||||
List.of(),
|
||||
teamId,
|
||||
// Classification runs in the editor on every upload.
|
||||
EditorConfig.onUpload());
|
||||
}
|
||||
}
|
||||
|
||||
+4
-6
@@ -107,14 +107,12 @@ public class SourceOverviewService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a policy runs from the editor. Editor membership is carried in the policy's output
|
||||
* metadata ({@code output.options.sources}) - a client-side list the editor writes when a
|
||||
* policy targets it - rather than as a persisted {@code sourceId}, because the editor is
|
||||
* virtual and has no stored source to reference.
|
||||
* Whether a policy runs from the editor. Read from the policy's first-class {@link
|
||||
* stirling.software.proprietary.policy.model.EditorConfig}, never inferred from a sources list
|
||||
* (the editor is not a real source).
|
||||
*/
|
||||
private static boolean runsFromEditor(Policy policy) {
|
||||
Object sources = policy.output().options().get("sources");
|
||||
return sources instanceof List<?> list && list.contains(EditorSource.ID);
|
||||
return policy.editor().allowed();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+2
-1
@@ -40,7 +40,8 @@ public class InProcessPolicyStore implements PolicyStore {
|
||||
policy.steps(),
|
||||
policy.output(),
|
||||
policy.outputIds(),
|
||||
policy.teamId());
|
||||
policy.teamId(),
|
||||
policy.editor());
|
||||
policies.put(id, stored);
|
||||
// Existing policy keeps its position; a new one appends to the end of its team's queue.
|
||||
sortOrders.computeIfAbsent(id, key -> nextSortOrder(stored.teamId()));
|
||||
|
||||
+65
-2
@@ -3,6 +3,7 @@ package stirling.software.proprietary.policy.store;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -11,8 +12,10 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.proprietary.policy.model.EditorConfig;
|
||||
import stirling.software.proprietary.policy.model.Policy;
|
||||
import stirling.software.proprietary.policy.model.PolicyBinding;
|
||||
import stirling.software.proprietary.policy.source.EditorSource;
|
||||
|
||||
import tools.jackson.databind.DeserializationFeature;
|
||||
import tools.jackson.databind.JsonNode;
|
||||
@@ -51,7 +54,8 @@ public class JpaPolicyStore implements PolicyStore {
|
||||
policy.steps(),
|
||||
policy.output(),
|
||||
policy.outputIds(),
|
||||
policy.teamId());
|
||||
policy.teamId(),
|
||||
policy.editor());
|
||||
|
||||
PolicyEntity entity = new PolicyEntity();
|
||||
entity.setId(id);
|
||||
@@ -151,7 +155,9 @@ public class JpaPolicyStore implements PolicyStore {
|
||||
// One unreadable row must never abort a bulk read or crash startup.
|
||||
private Optional<Policy> toPolicy(PolicyEntity entity) {
|
||||
try {
|
||||
JsonNode node = upgradeLegacyShape(objectMapper.readTree(entity.getPolicyJson()));
|
||||
JsonNode node =
|
||||
liftEditorConfig(
|
||||
upgradeLegacyShape(objectMapper.readTree(entity.getPolicyJson())));
|
||||
// A blob written by an older version won't carry fields added since (e.g. required,
|
||||
// icon). Default absent primitives rather than rejecting the whole policy, so upgrades
|
||||
// don't drop existing pipelines.
|
||||
@@ -201,4 +207,61 @@ public class JpaPolicyStore implements PolicyStore {
|
||||
obj.remove("sourceIds");
|
||||
return obj;
|
||||
}
|
||||
|
||||
/** Categories whose editor moment defaulted to export before it was stored (see runOn.ts). */
|
||||
private static final Set<String> EXPORT_BY_DEFAULT = Set.of("security");
|
||||
|
||||
/**
|
||||
* Derive {@code editor} for a blob written before editor participation had its own field, from
|
||||
* its {@code output.options}: allowed when {@code sources} lists {@code "editor"}, or - for a
|
||||
* catalogue policy - when there is no {@code sources} list at all (an unnarrowed catalogue
|
||||
* policy runs in the editor).
|
||||
*
|
||||
* <p>Runs on every read, deliberately outside {@link #upgradeLegacyShape}'s early return: a
|
||||
* blob written after triggers moved onto {@code inputs} but before this field existed still
|
||||
* needs lifting, and that early return would skip exactly those rows.
|
||||
*/
|
||||
private JsonNode liftEditorConfig(JsonNode root) {
|
||||
if (!(root instanceof ObjectNode obj) || obj.hasNonNull("editor")) {
|
||||
return root;
|
||||
}
|
||||
JsonNode options = obj.path("output").path("options");
|
||||
String categoryId = text(options, "categoryId");
|
||||
JsonNode sources = options.get("sources");
|
||||
boolean listed = sources != null && sources.isArray() && !sources.isEmpty();
|
||||
boolean allowed;
|
||||
if (listed) {
|
||||
// An explicit scope list decides: only the editor's own id puts it on the editor.
|
||||
allowed = false;
|
||||
for (JsonNode source : sources) {
|
||||
if (source.isValueNode() && EditorSource.ID.equals(source.asString())) {
|
||||
allowed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// No list: a catalogue policy ran in the editor by default, but a builder pipeline
|
||||
// (no category) could not reach the editor at all, so silence is not consent there.
|
||||
allowed = !categoryId.isBlank();
|
||||
}
|
||||
ObjectNode editor = objectMapper.createObjectNode();
|
||||
editor.put("allowed", allowed);
|
||||
editor.put("runOn", legacyRunOn(options, categoryId));
|
||||
obj.set("editor", editor);
|
||||
return obj;
|
||||
}
|
||||
|
||||
/** The stored moment, or the category default the client applied when none was stored. */
|
||||
private static String legacyRunOn(JsonNode options, String categoryId) {
|
||||
String stored = text(options, "runOn");
|
||||
if (EditorConfig.EXPORT.equals(stored) || EditorConfig.UPLOAD.equals(stored)) {
|
||||
return stored;
|
||||
}
|
||||
return EXPORT_BY_DEFAULT.contains(categoryId) ? EditorConfig.EXPORT : EditorConfig.UPLOAD;
|
||||
}
|
||||
|
||||
private static String text(JsonNode parent, String field) {
|
||||
JsonNode node = parent.path(field);
|
||||
return node.isValueNode() ? node.asString() : "";
|
||||
}
|
||||
}
|
||||
|
||||
+17
-15
@@ -70,21 +70,23 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
|
||||
|
||||
if (!response.isCommitted()) {
|
||||
if (authentication != null) {
|
||||
if (authentication instanceof Saml2Authentication samlAuthentication) {
|
||||
// Handle SAML2 logout redirection
|
||||
getRedirect_saml2(request, response, samlAuthentication);
|
||||
} else if (authentication instanceof OAuth2AuthenticationToken oAuthToken) {
|
||||
// Handle OAuth2 logout redirection
|
||||
getRedirect_oauth2(request, response, oAuthToken);
|
||||
} else if (authentication instanceof UsernamePasswordAuthenticationToken) {
|
||||
// Handle Username/Password logout
|
||||
getRedirectStrategy().sendRedirect(request, response, LOGOUT_PATH);
|
||||
} else {
|
||||
// Handle unknown authentication types
|
||||
log.error(
|
||||
"Authentication class unknown: {}",
|
||||
authentication.getClass().getSimpleName());
|
||||
getRedirectStrategy().sendRedirect(request, response, LOGOUT_PATH);
|
||||
switch (authentication) {
|
||||
case Saml2Authentication samlAuthentication ->
|
||||
// Handle SAML2 logout redirection
|
||||
getRedirect_saml2(request, response, samlAuthentication);
|
||||
case OAuth2AuthenticationToken oAuthToken ->
|
||||
// Handle OAuth2 logout redirection
|
||||
getRedirect_oauth2(request, response, oAuthToken);
|
||||
case UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken ->
|
||||
// Handle Username/Password logout
|
||||
getRedirectStrategy().sendRedirect(request, response, LOGOUT_PATH);
|
||||
default -> {
|
||||
// Handle unknown authentication types
|
||||
log.error(
|
||||
"Authentication class unknown: {}",
|
||||
authentication.getClass().getSimpleName());
|
||||
getRedirectStrategy().sendRedirect(request, response, LOGOUT_PATH);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (jwtService != null) {
|
||||
|
||||
+34
-34
@@ -392,40 +392,40 @@ public class SecurityConfiguration {
|
||||
// Handle OAUTH2 Logins
|
||||
if (securityProperties.isOauth2Active()) {
|
||||
http.oauth2Login(
|
||||
oauth2 -> {
|
||||
oauth2.loginPage("/login")
|
||||
.authorizationEndpoint(
|
||||
authorizationEndpoint -> {
|
||||
if (clientRegistrationRepository != null) {
|
||||
authorizationEndpoint
|
||||
.authorizationRequestResolver(
|
||||
new TauriAuthorizationRequestResolver(
|
||||
clientRegistrationRepository));
|
||||
}
|
||||
})
|
||||
.successHandler(
|
||||
new CustomOAuth2AuthenticationSuccessHandler(
|
||||
loginAttemptService,
|
||||
securityProperties.getOauth2(),
|
||||
userService,
|
||||
jwtService,
|
||||
licenseSettingsService,
|
||||
applicationProperties))
|
||||
.failureHandler(new CustomOAuth2AuthenticationFailureHandler())
|
||||
// Add existing Authorities from the database
|
||||
.userInfoEndpoint(
|
||||
userInfoEndpoint ->
|
||||
userInfoEndpoint
|
||||
.oidcUserService(
|
||||
new CustomOAuth2UserService(
|
||||
securityProperties
|
||||
.getOauth2(),
|
||||
userService,
|
||||
loginAttemptService))
|
||||
.userAuthoritiesMapper(
|
||||
oAuth2userAuthoritiesMapper))
|
||||
.permitAll();
|
||||
});
|
||||
oauth2 ->
|
||||
oauth2.loginPage("/login")
|
||||
.authorizationEndpoint(
|
||||
authorizationEndpoint -> {
|
||||
if (clientRegistrationRepository != null) {
|
||||
authorizationEndpoint
|
||||
.authorizationRequestResolver(
|
||||
new TauriAuthorizationRequestResolver(
|
||||
clientRegistrationRepository));
|
||||
}
|
||||
})
|
||||
.successHandler(
|
||||
new CustomOAuth2AuthenticationSuccessHandler(
|
||||
loginAttemptService,
|
||||
securityProperties.getOauth2(),
|
||||
userService,
|
||||
jwtService,
|
||||
licenseSettingsService,
|
||||
applicationProperties))
|
||||
.failureHandler(
|
||||
new CustomOAuth2AuthenticationFailureHandler())
|
||||
// Add existing Authorities from the database
|
||||
.userInfoEndpoint(
|
||||
userInfoEndpoint ->
|
||||
userInfoEndpoint
|
||||
.oidcUserService(
|
||||
new CustomOAuth2UserService(
|
||||
securityProperties
|
||||
.getOauth2(),
|
||||
userService,
|
||||
loginAttemptService))
|
||||
.userAuthoritiesMapper(
|
||||
oAuth2userAuthoritiesMapper))
|
||||
.permitAll());
|
||||
}
|
||||
// Handle SAML
|
||||
if (securityProperties.isSaml2Active() && runningProOrHigher) {
|
||||
|
||||
+12
-11
@@ -703,17 +703,18 @@ public class AuthController {
|
||||
}
|
||||
|
||||
private long extractEpochMillis(Object claimValue) {
|
||||
if (claimValue == null) {
|
||||
return -1L;
|
||||
}
|
||||
|
||||
if (claimValue instanceof java.util.Date date) {
|
||||
return date.getTime();
|
||||
}
|
||||
|
||||
if (claimValue instanceof Number number) {
|
||||
long epochSeconds = number.longValue();
|
||||
return epochSeconds * 1000L;
|
||||
switch (claimValue) {
|
||||
case null -> {
|
||||
return -1L;
|
||||
}
|
||||
case java.util.Date date -> {
|
||||
return date.getTime();
|
||||
}
|
||||
case Number number -> {
|
||||
long epochSeconds = number.longValue();
|
||||
return epochSeconds * 1000L;
|
||||
}
|
||||
default -> {}
|
||||
}
|
||||
|
||||
return -1L;
|
||||
|
||||
+8
-8
@@ -760,14 +760,14 @@ public class UserController {
|
||||
for (Object principal : principals) {
|
||||
List<SessionInformation> sessionsInformation =
|
||||
sessionRegistry.getAllSessions(principal, false);
|
||||
if (principal instanceof UserDetails detailsUser) {
|
||||
userNameP = detailsUser.getUsername();
|
||||
} else if (principal instanceof OAuth2User oAuth2User) {
|
||||
userNameP = oAuth2User.getName();
|
||||
} else if (principal instanceof CustomSaml2AuthenticatedPrincipal saml2User) {
|
||||
userNameP = saml2User.name();
|
||||
} else if (principal instanceof String stringUser) {
|
||||
userNameP = stringUser;
|
||||
switch (principal) {
|
||||
case null -> {}
|
||||
case UserDetails detailsUser -> userNameP = detailsUser.getUsername();
|
||||
case OAuth2User oAuth2User -> userNameP = oAuth2User.getName();
|
||||
case CustomSaml2AuthenticatedPrincipal saml2User ->
|
||||
userNameP = saml2User.name();
|
||||
case String stringUser -> userNameP = stringUser;
|
||||
default -> {}
|
||||
}
|
||||
if (userNameP.equalsIgnoreCase(username)) {
|
||||
for (SessionInformation sessionInfo : sessionsInformation) {
|
||||
|
||||
+2
-1
@@ -1,5 +1,6 @@
|
||||
package stirling.software.proprietary.security.model;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
@@ -28,7 +29,7 @@ import lombok.Setter;
|
||||
@Setter
|
||||
public class Authority implements GrantedAuthority, Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
@Serial private static final long serialVersionUID = 1L;
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
|
||||
+2
-1
@@ -1,5 +1,6 @@
|
||||
package stirling.software.proprietary.security.model;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@@ -18,7 +19,7 @@ import lombok.Setter;
|
||||
@Setter
|
||||
public class InviteToken implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
@Serial private static final long serialVersionUID = 1L;
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
|
||||
+52
-47
@@ -36,57 +36,62 @@ public class CustomOAuth2AuthenticationFailureHandler
|
||||
AuthenticationException exception)
|
||||
throws IOException, ServletException {
|
||||
|
||||
if (exception instanceof BadCredentialsException) {
|
||||
log.error("BadCredentialsException", exception);
|
||||
getRedirectStrategy().sendRedirect(request, response, "/login?error=badCredentials");
|
||||
return;
|
||||
}
|
||||
if (exception instanceof DisabledException) {
|
||||
log.error("User is deactivated: ", exception);
|
||||
getRedirectStrategy().sendRedirect(request, response, "/logout?userIsDisabled=true");
|
||||
return;
|
||||
}
|
||||
if (exception instanceof LockedException) {
|
||||
log.error("Account locked: ", exception);
|
||||
getRedirectStrategy().sendRedirect(request, response, "/logout?error=locked");
|
||||
return;
|
||||
}
|
||||
if (exception instanceof OAuth2AuthenticationException oAuth2Exception) {
|
||||
OAuth2Error error = oAuth2Exception.getError();
|
||||
|
||||
String errorCode = error.getErrorCode();
|
||||
|
||||
if ("Password must not be null".equals(error.getErrorCode())) {
|
||||
errorCode = "userAlreadyExistsWeb";
|
||||
switch (exception) {
|
||||
case BadCredentialsException badCredentialsException -> {
|
||||
log.error("BadCredentialsException", exception);
|
||||
getRedirectStrategy()
|
||||
.sendRedirect(request, response, "/login?error=badCredentials");
|
||||
return;
|
||||
}
|
||||
case DisabledException disabledException -> {
|
||||
log.error("User is deactivated: ", exception);
|
||||
getRedirectStrategy()
|
||||
.sendRedirect(request, response, "/logout?userIsDisabled=true");
|
||||
return;
|
||||
}
|
||||
case LockedException lockedException -> {
|
||||
log.error("Account locked: ", exception);
|
||||
getRedirectStrategy().sendRedirect(request, response, "/logout?error=locked");
|
||||
return;
|
||||
}
|
||||
case OAuth2AuthenticationException oAuth2Exception -> {
|
||||
OAuth2Error error = oAuth2Exception.getError();
|
||||
|
||||
log.error(
|
||||
"OAuth2 Authentication error: {}",
|
||||
errorCode != null ? errorCode : exception.getMessage(),
|
||||
exception);
|
||||
String errorValue = errorCode != null ? errorCode : "oauth2AuthenticationError";
|
||||
clearRedirectCookie(response);
|
||||
boolean tauriState = TauriOAuthUtils.isTauriState(request);
|
||||
String redirectUrl;
|
||||
if (tauriState) {
|
||||
String basePath =
|
||||
TauriOAuthUtils.defaultTauriCallbackPath(request.getContextPath());
|
||||
redirectUrl = basePath;
|
||||
String stateParam = request.getParameter("state");
|
||||
if (stateParam != null && !stateParam.isBlank()) {
|
||||
redirectUrl = appendQueryParam(redirectUrl, "state", stateParam);
|
||||
// Extract and pass nonce for CSRF validation
|
||||
String nonce = TauriOAuthUtils.extractNonceFromState(stateParam);
|
||||
if (nonce != null) {
|
||||
redirectUrl = appendQueryParam(redirectUrl, "nonce", nonce);
|
||||
}
|
||||
String errorCode = error.getErrorCode();
|
||||
|
||||
if ("Password must not be null".equals(error.getErrorCode())) {
|
||||
errorCode = "userAlreadyExistsWeb";
|
||||
}
|
||||
redirectUrl = appendQueryParam(redirectUrl, "errorOAuth", errorValue);
|
||||
} else {
|
||||
redirectUrl = buildFailureRedirectUrl(request, errorValue);
|
||||
|
||||
log.error(
|
||||
"OAuth2 Authentication error: {}",
|
||||
errorCode != null ? errorCode : exception.getMessage(),
|
||||
exception);
|
||||
String errorValue = errorCode != null ? errorCode : "oauth2AuthenticationError";
|
||||
clearRedirectCookie(response);
|
||||
boolean tauriState = TauriOAuthUtils.isTauriState(request);
|
||||
String redirectUrl;
|
||||
if (tauriState) {
|
||||
String basePath =
|
||||
TauriOAuthUtils.defaultTauriCallbackPath(request.getContextPath());
|
||||
redirectUrl = basePath;
|
||||
String stateParam = request.getParameter("state");
|
||||
if (stateParam != null && !stateParam.isBlank()) {
|
||||
redirectUrl = appendQueryParam(redirectUrl, "state", stateParam);
|
||||
// Extract and pass nonce for CSRF validation
|
||||
String nonce = TauriOAuthUtils.extractNonceFromState(stateParam);
|
||||
if (nonce != null) {
|
||||
redirectUrl = appendQueryParam(redirectUrl, "nonce", nonce);
|
||||
}
|
||||
}
|
||||
redirectUrl = appendQueryParam(redirectUrl, "errorOAuth", errorValue);
|
||||
} else {
|
||||
redirectUrl = buildFailureRedirectUrl(request, errorValue);
|
||||
}
|
||||
getRedirectStrategy().sendRedirect(request, response, redirectUrl);
|
||||
return;
|
||||
}
|
||||
getRedirectStrategy().sendRedirect(request, response, redirectUrl);
|
||||
return;
|
||||
default -> {}
|
||||
}
|
||||
log.error("Unhandled authentication exception", exception);
|
||||
super.onAuthenticationFailure(request, response, exception);
|
||||
|
||||
+6
-1
@@ -61,7 +61,12 @@ public class CustomSaml2ResponseAuthenticationConverter
|
||||
|
||||
@Override
|
||||
public Saml2Authentication convert(ResponseToken responseToken) {
|
||||
Assertion assertion = responseToken.getResponse().getAssertions().getFirst();
|
||||
List<Assertion> assertions = responseToken.getResponse().getAssertions();
|
||||
if (assertions == null || assertions.isEmpty()) {
|
||||
log.error("SAML response contains no assertions");
|
||||
return null;
|
||||
}
|
||||
Assertion assertion = assertions.getFirst();
|
||||
Map<String, List<Object>> attributes = extractAttributes(assertion);
|
||||
|
||||
// Debug log with actual values
|
||||
|
||||
+5
-2
@@ -213,8 +213,11 @@ public class CustomOAuth2UserService implements OAuth2UserService<OidcUserReques
|
||||
}
|
||||
|
||||
sb.append(
|
||||
"\nWARNING: this block contains PII. Set security.oauth2.debugLogging=false once"
|
||||
+ " troubleshooting is complete.\n");
|
||||
"""
|
||||
|
||||
WARNING: this block contains PII. Set security.oauth2.debugLogging=false once\
|
||||
troubleshooting is complete.
|
||||
""");
|
||||
sb.append("========== [/OAUTH2 DEBUG] ==========");
|
||||
|
||||
if (failure) {
|
||||
|
||||
+3
-1
@@ -132,7 +132,9 @@ public class KeyPersistenceService implements KeyPersistenceServiceInterface {
|
||||
verifyingKeyCache.put(
|
||||
key.getKeyId(), new JwtVerificationKey(key.getKeyId(), key.getVerifyingKey()));
|
||||
}
|
||||
activeKey = new JwtVerificationKey(keys.get(0).getKeyId(), keys.get(0).getVerifyingKey());
|
||||
activeKey =
|
||||
new JwtVerificationKey(
|
||||
keys.getFirst().getKeyId(), keys.getFirst().getVerifyingKey());
|
||||
log.info("Loaded {} JWT key(s) from DB, active key: {}", keys.size(), activeKey.getKeyId());
|
||||
}
|
||||
|
||||
|
||||
+8
-8
@@ -640,14 +640,14 @@ public class UserService implements UserServiceInterface {
|
||||
for (Object principal : sessionRegistry.getAllPrincipals()) {
|
||||
for (SessionInformation sessionsInformation :
|
||||
sessionRegistry.getAllSessions(principal, false)) {
|
||||
if (principal instanceof UserDetails detailsUser) {
|
||||
usernameP = detailsUser.getUsername();
|
||||
} else if (principal instanceof OAuth2User oAuth2User) {
|
||||
usernameP = oAuth2User.getName();
|
||||
} else if (principal instanceof CustomSaml2AuthenticatedPrincipal saml2User) {
|
||||
usernameP = saml2User.name();
|
||||
} else if (principal instanceof String stringUser) {
|
||||
usernameP = stringUser;
|
||||
switch (principal) {
|
||||
case null -> {}
|
||||
case UserDetails detailsUser -> usernameP = detailsUser.getUsername();
|
||||
case OAuth2User oAuth2User -> usernameP = oAuth2User.getName();
|
||||
case CustomSaml2AuthenticatedPrincipal saml2User ->
|
||||
usernameP = saml2User.name();
|
||||
case String stringUser -> usernameP = stringUser;
|
||||
default -> {}
|
||||
}
|
||||
if (usernameP.equalsIgnoreCase(username)) {
|
||||
sessionRegistry.expireSession(sessionsInformation.getSessionId());
|
||||
|
||||
+14
-16
@@ -47,14 +47,13 @@ public class SessionPersistentRegistry implements SessionRegistry {
|
||||
List<SessionInformation> sessionInformations = new ArrayList<>();
|
||||
String principalName = null;
|
||||
|
||||
if (principal instanceof UserDetails detailsUser) {
|
||||
principalName = detailsUser.getUsername();
|
||||
} else if (principal instanceof OAuth2User oAuth2User) {
|
||||
principalName = oAuth2User.getName();
|
||||
} else if (principal instanceof CustomSaml2AuthenticatedPrincipal saml2User) {
|
||||
principalName = saml2User.name();
|
||||
} else if (principal instanceof String stringUser) {
|
||||
principalName = stringUser;
|
||||
switch (principal) {
|
||||
case null -> {}
|
||||
case UserDetails detailsUser -> principalName = detailsUser.getUsername();
|
||||
case OAuth2User oAuth2User -> principalName = oAuth2User.getName();
|
||||
case CustomSaml2AuthenticatedPrincipal saml2User -> principalName = saml2User.name();
|
||||
case String stringUser -> principalName = stringUser;
|
||||
default -> {}
|
||||
}
|
||||
|
||||
if (principalName != null) {
|
||||
@@ -78,14 +77,13 @@ public class SessionPersistentRegistry implements SessionRegistry {
|
||||
public void registerNewSession(String sessionId, Object principal) {
|
||||
String principalName = null;
|
||||
|
||||
if (principal instanceof UserDetails detailsUser) {
|
||||
principalName = detailsUser.getUsername();
|
||||
} else if (principal instanceof OAuth2User oAuth2User) {
|
||||
principalName = oAuth2User.getName();
|
||||
} else if (principal instanceof CustomSaml2AuthenticatedPrincipal saml2User) {
|
||||
principalName = saml2User.name();
|
||||
} else if (principal instanceof String stringUser) {
|
||||
principalName = stringUser;
|
||||
switch (principal) {
|
||||
case null -> {}
|
||||
case UserDetails detailsUser -> principalName = detailsUser.getUsername();
|
||||
case OAuth2User oAuth2User -> principalName = oAuth2User.getName();
|
||||
case CustomSaml2AuthenticatedPrincipal saml2User -> principalName = saml2User.name();
|
||||
case String stringUser -> principalName = stringUser;
|
||||
default -> {}
|
||||
}
|
||||
|
||||
if (principalName != null) {
|
||||
|
||||
+8
-8
@@ -3,16 +3,16 @@ package stirling.software.proprietary.storage.converter;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import jakarta.persistence.AttributeConverter;
|
||||
import jakarta.persistence.Converter;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import tools.jackson.core.JacksonException;
|
||||
import tools.jackson.core.type.TypeReference;
|
||||
import tools.jackson.databind.JsonNode;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
/**
|
||||
* JPA AttributeConverter for storing Map<String, Object> as JSON in database columns.
|
||||
*
|
||||
@@ -33,7 +33,7 @@ public class JsonMapConverter implements AttributeConverter<Map<String, Object>,
|
||||
|
||||
try {
|
||||
return objectMapper.writeValueAsString(attribute);
|
||||
} catch (JsonProcessingException e) {
|
||||
} catch (JacksonException e) {
|
||||
log.error("Failed to convert map to JSON", e);
|
||||
throw new RuntimeException("Failed to convert map to JSON", e);
|
||||
}
|
||||
@@ -48,7 +48,7 @@ public class JsonMapConverter implements AttributeConverter<Map<String, Object>,
|
||||
try {
|
||||
// Try normal parsing first
|
||||
return objectMapper.readValue(dbData, new TypeReference<Map<String, Object>>() {});
|
||||
} catch (JsonProcessingException e) {
|
||||
} catch (JacksonException e) {
|
||||
// Fallback: try double-parsing for legacy double-encoded data
|
||||
// This handles data that was stored as JSON strings instead of JSON objects
|
||||
log.debug("Attempting double-decode fallback for legacy metadata format");
|
||||
@@ -69,7 +69,7 @@ public class JsonMapConverter implements AttributeConverter<Map<String, Object>,
|
||||
return objectMapper.readValue(
|
||||
node.asText(), new TypeReference<Map<String, Object>>() {});
|
||||
}
|
||||
} catch (JsonProcessingException e2) {
|
||||
} catch (JacksonException e2) {
|
||||
log.error("Failed to parse metadata even with double-decode fallback", e2);
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -1,5 +1,6 @@
|
||||
package stirling.software.proprietary.storage.model;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@@ -46,7 +47,7 @@ import stirling.software.proprietary.security.model.User;
|
||||
@Setter
|
||||
public class FileShare implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
@Serial private static final long serialVersionUID = 1L;
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
|
||||
+2
-1
@@ -1,5 +1,6 @@
|
||||
package stirling.software.proprietary.storage.model;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@@ -39,7 +40,7 @@ import stirling.software.proprietary.security.model.User;
|
||||
@Setter
|
||||
public class FileShareAccess implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
@Serial private static final long serialVersionUID = 1L;
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
|
||||
+2
-1
@@ -1,5 +1,6 @@
|
||||
package stirling.software.proprietary.storage.model;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@@ -24,7 +25,7 @@ import lombok.Setter;
|
||||
@Setter
|
||||
public class StorageCleanupEntry implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
@Serial private static final long serialVersionUID = 1L;
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
|
||||
+2
-1
@@ -1,5 +1,6 @@
|
||||
package stirling.software.proprietary.storage.model;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashSet;
|
||||
@@ -45,7 +46,7 @@ import stirling.software.proprietary.workflow.model.WorkflowSession;
|
||||
@Setter
|
||||
public class StoredFile implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
@Serial private static final long serialVersionUID = 1L;
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
|
||||
+2
-1
@@ -1,5 +1,6 @@
|
||||
package stirling.software.proprietary.storage.model;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
@@ -19,7 +20,7 @@ import lombok.Setter;
|
||||
@Setter
|
||||
public class StoredFileBlob implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
@Serial private static final long serialVersionUID = 1L;
|
||||
|
||||
@Id
|
||||
@Column(name = "storage_key", nullable = false, length = 128)
|
||||
|
||||
@@ -7,6 +7,7 @@ import org.slf4j.MDC;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
@@ -64,7 +65,7 @@ public class AuditWebFilter extends OncePerRequestFilter {
|
||||
if (auth != null && auth.getAuthorities() != null) {
|
||||
String roles =
|
||||
auth.getAuthorities().stream()
|
||||
.map(a -> a.getAuthority())
|
||||
.map(GrantedAuthority::getAuthority)
|
||||
.reduce((a, b) -> a + "," + b)
|
||||
.orElse("");
|
||||
MDC.put("userRoles", roles);
|
||||
|
||||
+6
-3
@@ -20,8 +20,6 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
@@ -39,11 +37,14 @@ import stirling.software.proprietary.workflow.dto.CertificateInfo;
|
||||
import stirling.software.proprietary.workflow.dto.CertificateValidationResponse;
|
||||
import stirling.software.proprietary.workflow.dto.ParticipantRequest;
|
||||
import stirling.software.proprietary.workflow.dto.WorkflowCreationRequest;
|
||||
import stirling.software.proprietary.workflow.model.WorkflowParticipant;
|
||||
import stirling.software.proprietary.workflow.model.WorkflowSession;
|
||||
import stirling.software.proprietary.workflow.service.CertificateSubmissionValidator;
|
||||
import stirling.software.proprietary.workflow.service.SigningFinalizationService;
|
||||
import stirling.software.proprietary.workflow.service.WorkflowSessionService;
|
||||
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/security")
|
||||
@@ -259,7 +260,9 @@ public class SigningSessionController {
|
||||
+ "database until manual cleanup.",
|
||||
sessionId,
|
||||
session.getParticipants() != null
|
||||
? session.getParticipants().stream().map(p -> p.getEmail()).toList()
|
||||
? session.getParticipants().stream()
|
||||
.map(WorkflowParticipant::getEmail)
|
||||
.toList()
|
||||
: "unknown",
|
||||
e);
|
||||
throw new ResponseStatusException(
|
||||
|
||||
+2
-1
@@ -5,6 +5,7 @@ import java.nio.charset.StandardCharsets;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.http.ContentDisposition;
|
||||
@@ -429,7 +430,7 @@ public class WorkflowParticipantController {
|
||||
java.util.List<Map<String, Object>> wetSigs =
|
||||
objectMapper.readValue(
|
||||
request.getWetSignaturesData(),
|
||||
new TypeReference<java.util.List<Map<String, Object>>>() {});
|
||||
new TypeReference<List<Map<String, Object>>>() {});
|
||||
if (wetSigs.size() > WetSignatureMetadata.MAX_SIGNATURES_PER_PARTICIPANT) {
|
||||
throw new ResponseStatusException(
|
||||
HttpStatus.BAD_REQUEST, "Too many wet signatures submitted");
|
||||
|
||||
+2
-1
@@ -1,5 +1,6 @@
|
||||
package stirling.software.proprietary.workflow.model;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
@@ -51,7 +52,7 @@ import stirling.software.proprietary.storage.model.ShareAccessRole;
|
||||
@Setter
|
||||
public class WorkflowParticipant implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
@Serial private static final long serialVersionUID = 1L;
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
|
||||
+2
-1
@@ -1,5 +1,6 @@
|
||||
package stirling.software.proprietary.workflow.model;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
@@ -53,7 +54,7 @@ import stirling.software.proprietary.storage.model.StoredFile;
|
||||
@Setter
|
||||
public class WorkflowSession implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
@Serial private static final long serialVersionUID = 1L;
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
|
||||
+4
-10
@@ -217,16 +217,13 @@ public class SigningFinalizationService {
|
||||
wetSignatures.size(),
|
||||
session.getSessionId());
|
||||
|
||||
PDDocument document = pdfDocumentFactory.load(new ByteArrayInputStream(pdfBytes));
|
||||
try {
|
||||
try (PDDocument document = pdfDocumentFactory.load(new ByteArrayInputStream(pdfBytes))) {
|
||||
for (WetSignatureMetadata wetSig : wetSignatures) {
|
||||
applyWetSignatureToPage(document, wetSig);
|
||||
}
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
document.save(baos);
|
||||
return baos.toByteArray();
|
||||
} finally {
|
||||
document.close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -242,11 +239,10 @@ public class SigningFinalizationService {
|
||||
}
|
||||
|
||||
PDPage page = document.getPage(pageIndex);
|
||||
PDPageContentStream contentStream =
|
||||
new PDPageContentStream(
|
||||
document, page, PDPageContentStream.AppendMode.APPEND, true, true);
|
||||
|
||||
try {
|
||||
try (PDPageContentStream contentStream =
|
||||
new PDPageContentStream(
|
||||
document, page, PDPageContentStream.AppendMode.APPEND, true, true)) {
|
||||
// Use WetSignatureMetadata.extractBase64Data() to strip data URL prefix
|
||||
String base64Data = wetSig.extractBase64Data();
|
||||
if (base64Data == null || base64Data.isBlank()) {
|
||||
@@ -279,8 +275,6 @@ public class SigningFinalizationService {
|
||||
pdfY,
|
||||
width,
|
||||
height);
|
||||
} finally {
|
||||
contentStream.close();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+16
-15
@@ -954,21 +954,22 @@ public class WorkflowSessionService {
|
||||
Object pemObject = pemParser.readObject();
|
||||
JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider("BC");
|
||||
PrivateKeyInfo keyInfo;
|
||||
if (pemObject instanceof PKCS8EncryptedPrivateKeyInfo encrypted) {
|
||||
InputDecryptorProvider decryptor =
|
||||
new JceOpenSSLPKCS8DecryptorProviderBuilder().build(password);
|
||||
keyInfo = encrypted.decryptPrivateKeyInfo(decryptor);
|
||||
} else if (pemObject instanceof PEMEncryptedKeyPair encryptedKeyPair) {
|
||||
PEMDecryptorProvider decryptor =
|
||||
new JcePEMDecryptorProviderBuilder().build(password);
|
||||
keyInfo = encryptedKeyPair.decryptKeyPair(decryptor).getPrivateKeyInfo();
|
||||
} else if (pemObject instanceof PEMKeyPair keyPair) {
|
||||
keyInfo = keyPair.getPrivateKeyInfo();
|
||||
} else if (pemObject instanceof PrivateKeyInfo info) {
|
||||
keyInfo = info;
|
||||
} else {
|
||||
throw new ResponseStatusException(
|
||||
HttpStatus.BAD_REQUEST, "Unsupported PEM private key format");
|
||||
switch (pemObject) {
|
||||
case PKCS8EncryptedPrivateKeyInfo encrypted -> {
|
||||
InputDecryptorProvider decryptor =
|
||||
new JceOpenSSLPKCS8DecryptorProviderBuilder().build(password);
|
||||
keyInfo = encrypted.decryptPrivateKeyInfo(decryptor);
|
||||
}
|
||||
case PEMEncryptedKeyPair encryptedKeyPair -> {
|
||||
PEMDecryptorProvider decryptor =
|
||||
new JcePEMDecryptorProviderBuilder().build(password);
|
||||
keyInfo = encryptedKeyPair.decryptKeyPair(decryptor).getPrivateKeyInfo();
|
||||
}
|
||||
case PEMKeyPair keyPair -> keyInfo = keyPair.getPrivateKeyInfo();
|
||||
case PrivateKeyInfo info -> keyInfo = info;
|
||||
case null, default ->
|
||||
throw new ResponseStatusException(
|
||||
HttpStatus.BAD_REQUEST, "Unsupported PEM private key format");
|
||||
}
|
||||
return converter.getPrivateKey(keyInfo);
|
||||
}
|
||||
|
||||
+2
-2
@@ -4,14 +4,14 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import stirling.software.proprietary.workflow.dto.ParticipantResponse;
|
||||
import stirling.software.proprietary.workflow.dto.WetSignatureMetadata;
|
||||
import stirling.software.proprietary.workflow.dto.WorkflowSessionResponse;
|
||||
import stirling.software.proprietary.workflow.model.WorkflowParticipant;
|
||||
import stirling.software.proprietary.workflow.model.WorkflowSession;
|
||||
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
/**
|
||||
* Utility class for mapping workflow entities to DTOs. Centralizes conversion logic for consistent
|
||||
* API responses.
|
||||
|
||||
+45
-3
@@ -15,6 +15,7 @@ import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.service.UserServiceInterface;
|
||||
import stirling.software.proprietary.policy.config.PolicyAccessGuard;
|
||||
import stirling.software.proprietary.policy.config.PolicyManagementAuthority;
|
||||
import stirling.software.proprietary.policy.model.EditorConfig;
|
||||
import stirling.software.proprietary.policy.model.OutputSpec;
|
||||
import stirling.software.proprietary.policy.model.PipelineInput;
|
||||
import stirling.software.proprietary.policy.model.PipelineStep;
|
||||
@@ -156,7 +157,8 @@ class PolicyOverviewServiceTest {
|
||||
List.of(new PipelineStep("/api/v1/security/auto-redact", Map.of())),
|
||||
OutputSpec.inline(),
|
||||
List.of(),
|
||||
null));
|
||||
null,
|
||||
EditorConfig.disabled()));
|
||||
|
||||
PolicyView view = find(service.overview(), "Mandatory redaction");
|
||||
assertTrue(view.required());
|
||||
@@ -177,7 +179,8 @@ class PolicyOverviewServiceTest {
|
||||
List.of(new PipelineStep("/api/v1/misc/compress-pdf", Map.of())),
|
||||
OutputSpec.inline(),
|
||||
List.of(),
|
||||
null));
|
||||
null,
|
||||
EditorConfig.disabled()));
|
||||
// No explicit icon: a template-derived policy falls back to its categoryId marker.
|
||||
policyStore.save(
|
||||
new Policy(
|
||||
@@ -191,7 +194,8 @@ class PolicyOverviewServiceTest {
|
||||
List.of(new PipelineStep("/api/v1/security/auto-redact", Map.of())),
|
||||
new OutputSpec("inline", Map.of("categoryId", "security")),
|
||||
List.of(),
|
||||
null));
|
||||
null,
|
||||
EditorConfig.disabled()));
|
||||
|
||||
assertEquals("shield", find(service.overview(), "Custom with icon").icon());
|
||||
assertEquals("security", find(service.overview(), "Template derived").icon());
|
||||
@@ -278,6 +282,44 @@ class PolicyOverviewServiceTest {
|
||||
teamId));
|
||||
}
|
||||
|
||||
@Test
|
||||
void editorPolicyReportsItsRunMomentRatherThanReadingAsManual() {
|
||||
policyStore.save(
|
||||
new Policy(
|
||||
null,
|
||||
"Editor flatten",
|
||||
"owner",
|
||||
true,
|
||||
List.of(),
|
||||
List.of(new PipelineStep("/api/v1/misc/flatten", Map.of())),
|
||||
OutputSpec.inline(),
|
||||
List.of(),
|
||||
1L,
|
||||
EditorConfig.onUpload()));
|
||||
|
||||
PolicyView view = find(service.overview(), "Editor flatten");
|
||||
|
||||
assertEquals("editor-upload", view.trigger());
|
||||
}
|
||||
|
||||
@Test
|
||||
void sweptPolicyWithNoTriggeredInputIsStillManual() {
|
||||
policyStore.save(
|
||||
new Policy(
|
||||
null,
|
||||
"Swept compress",
|
||||
"owner",
|
||||
true,
|
||||
List.of(),
|
||||
List.of(new PipelineStep("/api/v1/misc/compress-pdf", Map.of())),
|
||||
OutputSpec.inline(),
|
||||
1L));
|
||||
|
||||
PolicyView view = find(service.overview(), "Swept compress");
|
||||
|
||||
assertEquals("manual", view.trigger());
|
||||
}
|
||||
|
||||
private static PolicyView find(PoliciesOverviewResponse response, String name) {
|
||||
return response.pipelines().stream()
|
||||
.filter(view -> view.name().equals(name))
|
||||
|
||||
+18
-2
@@ -64,14 +64,30 @@ class DefaultClassificationPolicySeederTest {
|
||||
assertThat(policy.teamId()).isEqualTo(7L);
|
||||
assertThat(policy.output().type()).isEqualTo("inline");
|
||||
assertThat(policy.output().options().get("categoryId")).isEqualTo("classification");
|
||||
assertThat(policy.output().options().get("runOn")).isEqualTo("upload");
|
||||
assertThat(policy.output().options().get("mode")).isEqualTo("new_version");
|
||||
assertThat(policy.output().options().get("sources")).isEqualTo(List.of("editor"));
|
||||
// Editor participation is the policy's own flag, not a marker in the output options.
|
||||
assertThat(policy.editor().allowed()).isTrue();
|
||||
assertThat(policy.editor().runOn()).isEqualTo("upload");
|
||||
assertThat(policy.steps()).hasSize(1);
|
||||
assertThat(policy.steps().get(0).operation())
|
||||
.isEqualTo("/api/v1/ai/tools/classify-and-label");
|
||||
}
|
||||
|
||||
@Test
|
||||
void marksEditorParticipationOnEditorConfigAndSeedsNoSources() {
|
||||
when(policyStore.findByTeam(7L)).thenReturn(List.of());
|
||||
|
||||
seeder().onTeamCreated(new TeamCreatedEvent(7L, "Acme"));
|
||||
|
||||
ArgumentCaptor<Policy> saved = ArgumentCaptor.forClass(Policy.class);
|
||||
verify(policyStore).save(saved.capture());
|
||||
Policy policy = saved.getValue();
|
||||
// Editor participation is on EditorConfig, not the sources list; the seed carries no
|
||||
// sources.
|
||||
assertThat(policy.editor().allowed()).isTrue();
|
||||
assertThat(policy.output().options().get("sources")).isEqualTo(List.of());
|
||||
}
|
||||
|
||||
@Test
|
||||
void doesNotSeedWhenAClassificationPolicyAlreadyExists() {
|
||||
when(policyStore.findByTeam(7L)).thenReturn(List.of(classificationPolicy(7L)));
|
||||
|
||||
+6
-4
@@ -15,6 +15,7 @@ import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.service.UserServiceInterface;
|
||||
import stirling.software.proprietary.policy.config.PolicyAccessGuard;
|
||||
import stirling.software.proprietary.policy.config.PolicyManagementAuthority;
|
||||
import stirling.software.proprietary.policy.model.EditorConfig;
|
||||
import stirling.software.proprietary.policy.model.OutputSpec;
|
||||
import stirling.software.proprietary.policy.model.PipelineInput;
|
||||
import stirling.software.proprietary.policy.model.PipelineStep;
|
||||
@@ -222,9 +223,7 @@ class SourceOverviewServiceTest {
|
||||
OutputSpec.inline()));
|
||||
}
|
||||
|
||||
/**
|
||||
* A policy that targets the editor: membership rides in its output metadata, not a sourceId.
|
||||
*/
|
||||
/** A policy that targets the editor: membership on its {@link EditorConfig}, not a sourceId. */
|
||||
private void editorPolicy(String name) {
|
||||
policyStore.save(
|
||||
new Policy(
|
||||
@@ -234,7 +233,10 @@ class SourceOverviewServiceTest {
|
||||
true,
|
||||
List.of(),
|
||||
List.of(new PipelineStep("/api/v1/misc/compress-pdf", Map.of())),
|
||||
new OutputSpec("inline", Map.of("sources", List.of("editor")))));
|
||||
OutputSpec.inline(),
|
||||
List.of(),
|
||||
null,
|
||||
EditorConfig.onUpload()));
|
||||
}
|
||||
|
||||
private void teamPolicy(String name, Long teamId, String... sourceIds) {
|
||||
|
||||
+124
@@ -18,6 +18,7 @@ import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import stirling.software.proprietary.policy.model.EditorConfig;
|
||||
import stirling.software.proprietary.policy.model.OutputSpec;
|
||||
import stirling.software.proprietary.policy.model.PipelineInput;
|
||||
import stirling.software.proprietary.policy.model.PipelineStep;
|
||||
@@ -113,6 +114,129 @@ class JpaPolicyStoreTest {
|
||||
upgraded.inputs());
|
||||
}
|
||||
|
||||
/**
|
||||
* The regression this guards: before the editor lift, a blob written by the pre-{@code editor}
|
||||
* seeder deserialized straight onto {@link EditorConfig#disabled()}, silently taking every
|
||||
* upgraded install's Classification policy off the editor.
|
||||
*
|
||||
* <p>The {@code inputs} variant is the important one - {@link
|
||||
* JpaPolicyStore#upgradeLegacyShape} returns early on it, so a lift living inside that method
|
||||
* would miss exactly the rows written between the trigger migration and this field.
|
||||
*/
|
||||
@Test
|
||||
void getLiftsALegacyEditorSourceOntoEditorConfigWhenInputsArePresent() {
|
||||
Policy lifted = readLegacy(legacyJson("\"inputs\":[],", "\"sources\":[\"editor\"],"));
|
||||
|
||||
assertEquals(EditorConfig.onUpload(), lifted.editor());
|
||||
assertEquals(Optional.of("upload"), lifted.editorRunOn());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getLiftsALegacyEditorSourceOnThePreInputsShapeToo() {
|
||||
// Oldest shape: policy-level trigger + sourceIds, so both migrations have to compose.
|
||||
Policy lifted =
|
||||
readLegacy(
|
||||
legacyJson(
|
||||
"\"trigger\":{\"type\":\"schedule\",\"options\":{}},"
|
||||
+ "\"sourceIds\":[\"s1\"],",
|
||||
"\"sources\":[\"editor\"],"));
|
||||
|
||||
assertEquals(EditorConfig.onUpload(), lifted.editor());
|
||||
assertEquals(
|
||||
List.of(new PipelineInput("s1", new TriggerConfig("schedule", Map.of()))),
|
||||
lifted.inputs());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getTreatsAnUnnarrowedCataloguePolicyAsEditorRun() {
|
||||
// Empty and absent both meant "nobody narrowed it", which the editor read as its own.
|
||||
assertTrue(readLegacy(legacyJson("\"inputs\":[],", "\"sources\":[],")).editor().allowed());
|
||||
assertTrue(readLegacy(legacyJson("\"inputs\":[],", "")).editor().allowed());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getLeavesACataloguePolicyScopedElsewhereOffTheEditor() {
|
||||
Policy lifted = readLegacy(legacyJson("\"inputs\":[],", "\"sources\":[\"sharepoint\"],"));
|
||||
|
||||
assertFalse(lifted.editor().allowed());
|
||||
assertEquals(Optional.empty(), lifted.editorRunOn());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getLeavesASourcelessBuilderPipelineOffTheEditor() {
|
||||
// No categoryId: a pipeline built on the Pipelines page, which never reached the editor.
|
||||
String json =
|
||||
"{\"id\":\"p1\",\"name\":\"legacy\",\"enabled\":true,\"inputs\":[],"
|
||||
+ "\"steps\":[],\"output\":{\"type\":\"inline\",\"options\":{}}}";
|
||||
|
||||
assertFalse(readLegacy(json).editor().allowed());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getKeepsTheCategoryDefaultMomentWhenNoRunOnWasStored() {
|
||||
// Security enforced on export before runOn was persisted (frontend runOn.ts
|
||||
// DEFAULT_RUN_ON).
|
||||
String json =
|
||||
"{\"id\":\"p1\",\"name\":\"legacy\",\"enabled\":true,\"inputs\":[],"
|
||||
+ "\"steps\":[],\"output\":{\"type\":\"inline\",\"options\":{"
|
||||
+ "\"categoryId\":\"security\",\"sources\":[\"editor\"]}}}";
|
||||
|
||||
assertEquals(EditorConfig.onExport(), readLegacy(json).editor());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getNeverOverridesAnExplicitlyStoredEditorBlock() {
|
||||
// A deliberate opt-out survives, so the lift stays safe to leave in permanently.
|
||||
String json =
|
||||
"{\"id\":\"p1\",\"name\":\"legacy\",\"enabled\":true,\"inputs\":[],"
|
||||
+ "\"steps\":[],\"editor\":{\"allowed\":false,\"runOn\":\"upload\"},"
|
||||
+ "\"output\":{\"type\":\"inline\",\"options\":{"
|
||||
+ "\"categoryId\":\"classification\",\"sources\":[\"editor\"]}}}";
|
||||
|
||||
assertFalse(readLegacy(json).editor().allowed());
|
||||
}
|
||||
|
||||
/**
|
||||
* Pins the wire shape the stubbed Playwright spec hardcodes: the derived block is additive, so
|
||||
* a real response carries it alongside the untouched legacy options bag.
|
||||
*/
|
||||
@Test
|
||||
void getLeavesTheLegacyOptionsBagIntactSoTheResponseCarriesBoth() {
|
||||
Policy lifted = readLegacy(legacyJson("\"inputs\":[],", "\"sources\":[\"editor\"],"));
|
||||
|
||||
assertEquals(List.of("editor"), lifted.output().options().get("sources"));
|
||||
String wire = objectMapper.writeValueAsString(lifted);
|
||||
assertTrue(
|
||||
wire.contains("\"editor\":{\"allowed\":true,\"runOn\":\"upload\"}"),
|
||||
"expected the derived editor block on the wire, got: " + wire);
|
||||
}
|
||||
|
||||
/**
|
||||
* The blob main's DefaultClassificationPolicySeeder wrote, with the shape bits parameterised.
|
||||
*/
|
||||
private static String legacyJson(String shapeFields, String sourcesField) {
|
||||
return "{\"id\":\"p1\",\"name\":\"Classification Policy\",\"owner\":\"system\","
|
||||
+ "\"enabled\":true,"
|
||||
+ shapeFields
|
||||
+ "\"steps\":[{\"operation\":\"/api/v1/ai/tools/classify-and-label\","
|
||||
+ "\"parameters\":{}}],"
|
||||
+ "\"output\":{\"type\":\"inline\",\"options\":{"
|
||||
+ "\"categoryId\":\"classification\",\"runOn\":\"upload\","
|
||||
+ "\"mode\":\"new_version\","
|
||||
+ sourcesField
|
||||
+ "\"scopeTypes\":[],\"reviewerEmail\":\"\"}},\"teamId\":1}";
|
||||
}
|
||||
|
||||
private Policy readLegacy(String policyJson) {
|
||||
PolicyEntity entity = new PolicyEntity();
|
||||
entity.setId("p1");
|
||||
entity.setName("legacy");
|
||||
entity.setEnabled(true);
|
||||
entity.setPolicyJson(policyJson);
|
||||
when(repository.findById("p1")).thenReturn(Optional.of(entity));
|
||||
return store.get("p1").orElseThrow();
|
||||
}
|
||||
|
||||
@Test
|
||||
void saveDenormalizesTeamIdForScopedQueries() {
|
||||
store.save(
|
||||
|
||||
@@ -26,8 +26,8 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import tools.jackson.core.JacksonException;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
@@ -167,7 +167,7 @@ public class AiCreateController {
|
||||
if (request.constraints() != null) {
|
||||
try {
|
||||
constraintsPayload = objectMapper.writeValueAsString(request.constraints());
|
||||
} catch (JsonProcessingException exc) {
|
||||
} catch (JacksonException exc) {
|
||||
throw new ResponseStatusException(
|
||||
HttpStatus.BAD_REQUEST, "Invalid constraints payload", exc);
|
||||
}
|
||||
@@ -202,7 +202,7 @@ public class AiCreateController {
|
||||
String payload;
|
||||
try {
|
||||
payload = objectMapper.writeValueAsString(request.draftSections());
|
||||
} catch (JsonProcessingException exc) {
|
||||
} catch (JacksonException exc) {
|
||||
throw new ResponseStatusException(
|
||||
HttpStatus.BAD_REQUEST, "Invalid draft sections payload", exc);
|
||||
}
|
||||
@@ -392,7 +392,7 @@ public class AiCreateController {
|
||||
objectMapper
|
||||
.getTypeFactory()
|
||||
.constructCollectionType(List.class, DraftSection.class));
|
||||
} catch (JsonProcessingException exc) {
|
||||
} catch (JacksonException exc) {
|
||||
log.warn("Failed to parse draft sections payload", exc);
|
||||
return null;
|
||||
}
|
||||
@@ -408,7 +408,7 @@ public class AiCreateController {
|
||||
objectMapper
|
||||
.getTypeFactory()
|
||||
.constructMapType(Map.class, String.class, Object.class));
|
||||
} catch (JsonProcessingException exc) {
|
||||
} catch (JacksonException exc) {
|
||||
log.warn("Failed to parse outline constraints payload", exc);
|
||||
return null;
|
||||
}
|
||||
|
||||
+6
-6
@@ -14,8 +14,8 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import tools.jackson.core.JacksonException;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
@@ -61,7 +61,7 @@ public class AiCreateInternalController {
|
||||
try {
|
||||
outlineConstraintsPayload =
|
||||
objectMapper.writeValueAsString(request.outlineConstraints());
|
||||
} catch (JsonProcessingException exc) {
|
||||
} catch (JacksonException exc) {
|
||||
throw new ResponseStatusException(
|
||||
HttpStatus.BAD_REQUEST, "Invalid outline constraints payload", exc);
|
||||
}
|
||||
@@ -70,7 +70,7 @@ public class AiCreateInternalController {
|
||||
if (request.draftSections() != null) {
|
||||
try {
|
||||
draftSectionsPayload = objectMapper.writeValueAsString(request.draftSections());
|
||||
} catch (JsonProcessingException exc) {
|
||||
} catch (JacksonException exc) {
|
||||
throw new ResponseStatusException(
|
||||
HttpStatus.BAD_REQUEST, "Invalid draft sections payload", exc);
|
||||
}
|
||||
@@ -136,7 +136,7 @@ public class AiCreateInternalController {
|
||||
.getTypeFactory()
|
||||
.constructCollectionType(
|
||||
List.class, AiCreateController.DraftSection.class));
|
||||
} catch (JsonProcessingException exc) {
|
||||
} catch (JacksonException exc) {
|
||||
log.warn("Failed to parse draft sections payload", exc);
|
||||
return null;
|
||||
}
|
||||
@@ -152,7 +152,7 @@ public class AiCreateInternalController {
|
||||
objectMapper
|
||||
.getTypeFactory()
|
||||
.constructMapType(Map.class, String.class, Object.class));
|
||||
} catch (JsonProcessingException exc) {
|
||||
} catch (JacksonException exc) {
|
||||
log.warn("Failed to parse outline constraints payload", exc);
|
||||
return null;
|
||||
}
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
|
||||
@@ -13,8 +13,8 @@ import java.util.regex.Pattern;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import tools.jackson.databind.JsonNode;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
|
||||
@@ -56,28 +56,26 @@ public class LegalDocumentRegistry {
|
||||
subprocessorUrl = root.path("subprocessorUrl").asText("");
|
||||
eulaUrl = root.path("eulaUrl").asText("");
|
||||
JsonNode docs = root.path("documents");
|
||||
docs.fieldNames()
|
||||
.forEachRemaining(
|
||||
id -> {
|
||||
JsonNode d = docs.get(id);
|
||||
List<String> parts =
|
||||
objectMapper.convertValue(
|
||||
d.path("parts"),
|
||||
objectMapper
|
||||
.getTypeFactory()
|
||||
.constructCollectionType(
|
||||
List.class, String.class));
|
||||
documents.put(
|
||||
docs.forEachEntry(
|
||||
(id, d) -> {
|
||||
List<String> parts =
|
||||
objectMapper.convertValue(
|
||||
d.path("parts"),
|
||||
objectMapper
|
||||
.getTypeFactory()
|
||||
.constructCollectionType(
|
||||
List.class, String.class));
|
||||
documents.put(
|
||||
id,
|
||||
new LegalDocumentMeta(
|
||||
id,
|
||||
new LegalDocumentMeta(
|
||||
id,
|
||||
d.path("label").asText(id),
|
||||
d.path("displayName").asText(id),
|
||||
d.path("version").asText("0"),
|
||||
d.path("effectiveDate").asText(""),
|
||||
d.path("status").asText("draft"),
|
||||
parts == null ? List.of() : parts));
|
||||
});
|
||||
d.path("label").asText(id),
|
||||
d.path("displayName").asText(id),
|
||||
d.path("version").asText("0"),
|
||||
d.path("effectiveDate").asText(""),
|
||||
d.path("status").asText("draft"),
|
||||
parts == null ? List.of() : parts));
|
||||
});
|
||||
log.info("[legal] loaded {} document(s) from {}", documents.size(), MANIFEST);
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
import io.micrometer.core.instrument.Counter;
|
||||
import io.micrometer.core.instrument.MeterRegistry;
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ import java.util.Map;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
+2
-2
@@ -16,8 +16,8 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import tools.jackson.databind.JsonNode;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
|
||||
+3
-3
@@ -10,8 +10,8 @@ import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import tools.jackson.core.JacksonException;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@@ -707,7 +707,7 @@ public class ProcurementService {
|
||||
private String writeLineItems(QuoteBreakdown breakdown) {
|
||||
try {
|
||||
return OBJECT_MAPPER.writeValueAsString(breakdown.lineItems());
|
||||
} catch (JsonProcessingException e) {
|
||||
} catch (JacksonException e) {
|
||||
log.warn("[procurement] failed to serialise line items", e);
|
||||
return "[]";
|
||||
}
|
||||
|
||||
@@ -113,19 +113,13 @@ public class RateLimitService {
|
||||
public void cleanupExpiredBuckets() {
|
||||
long now = System.currentTimeMillis();
|
||||
|
||||
int hourlyRemoved =
|
||||
(int)
|
||||
hourlyLimits.entrySet().stream()
|
||||
.filter(e -> e.getValue().getResetTime() < now)
|
||||
.peek(e -> hourlyLimits.remove(e.getKey()))
|
||||
.count();
|
||||
int hourlyBefore = hourlyLimits.size();
|
||||
hourlyLimits.entrySet().removeIf(e -> e.getValue().getResetTime() < now);
|
||||
int hourlyRemoved = hourlyBefore - hourlyLimits.size();
|
||||
|
||||
int dailyRemoved =
|
||||
(int)
|
||||
dailyLimits.entrySet().stream()
|
||||
.filter(e -> e.getValue().getResetTime() < now)
|
||||
.peek(e -> dailyLimits.remove(e.getKey()))
|
||||
.count();
|
||||
int dailyBefore = dailyLimits.size();
|
||||
dailyLimits.entrySet().removeIf(e -> e.getValue().getResetTime() < now);
|
||||
int dailyRemoved = dailyBefore - dailyLimits.size();
|
||||
|
||||
if (hourlyRemoved + dailyRemoved > 0) {
|
||||
log.debug(
|
||||
|
||||
+2
-2
@@ -28,8 +28,8 @@ import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import tools.jackson.databind.JsonNode;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
import io.micrometer.core.instrument.MeterRegistry;
|
||||
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
|
||||
|
||||
+2
-2
@@ -38,9 +38,9 @@ ext {
|
||||
gsonVersion = "2.14.0"
|
||||
guavaVersion = "33.6.0-jre"
|
||||
jinjavaVersion = "2.8.4"
|
||||
jackson2Version = "2.22.1"
|
||||
jackson2Version = "2.22.2"
|
||||
bucket4jVersion = "8.19.0"
|
||||
archunitVersion = "1.4.2"
|
||||
archunitVersion = "1.5.0"
|
||||
batikVersion = "1.19"
|
||||
jpdfiumVersion = "1.1.3"
|
||||
jwtVersion = "0.13.0"
|
||||
|
||||
@@ -45,7 +45,7 @@ RUN JPDFIUM_PLATFORM="$([ "$TARGETARCH" = arm64 ] && echo linux-arm64 || echo li
|
||||
--no-daemon
|
||||
|
||||
# Stage 2: Extract Spring Boot Layers
|
||||
FROM eclipse-temurin:25-jre-noble@sha256:fbcf915c585659b30eb766ada4d6d7cfc9ec1040bf521e95bf61b10a25af73db AS jar-extract
|
||||
FROM eclipse-temurin:25-jre-noble@sha256:b4c93a50fc67612798db73d68ca3b0ee4ebdd51736e59cca370e689b9797037e AS jar-extract
|
||||
WORKDIR /tmp
|
||||
COPY --from=app-build /app/app/core/build/libs/*.jar app.jar
|
||||
RUN java -Djarmode=tools -jar app.jar extract --layers --destination /layers
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
# Stage 1: Build and strip Calibre
|
||||
FROM ubuntu:noble@sha256:561618e2c15bf2397621dd04f96926663a3b5616c189cf7e38db7e82f5c538ea AS calibre-build
|
||||
FROM ubuntu:noble@sha256:33ceb71981b602c1a7443a53469e4dba065f7503eab3078a2d7a57a2ab987517 AS calibre-build
|
||||
ARG TARGETPLATFORM
|
||||
ARG CALIBRE_VERSION=9.13.0
|
||||
ARG CALIBRE_STRIP_WEBENGINE=false
|
||||
@@ -274,7 +274,7 @@ RUN if [ "${CALIBRE_STRIP_WEBENGINE}" = "true" ]; then \
|
||||
|
||||
|
||||
# Stage 2: Build Ghostscript from source
|
||||
FROM ubuntu:noble@sha256:561618e2c15bf2397621dd04f96926663a3b5616c189cf7e38db7e82f5c538ea AS gs-build
|
||||
FROM ubuntu:noble@sha256:33ceb71981b602c1a7443a53469e4dba065f7503eab3078a2d7a57a2ab987517 AS gs-build
|
||||
ARG TARGETPLATFORM
|
||||
ARG GS_VERSION=10.07.1
|
||||
|
||||
@@ -298,7 +298,7 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
|
||||
|
||||
|
||||
# Stage 3: Build PDF Tools (QPDF and ImageMagick 7)
|
||||
FROM ubuntu:noble@sha256:561618e2c15bf2397621dd04f96926663a3b5616c189cf7e38db7e82f5c538ea AS pdf-tools-build
|
||||
FROM ubuntu:noble@sha256:33ceb71981b602c1a7443a53469e4dba065f7503eab3078a2d7a57a2ab987517 AS pdf-tools-build
|
||||
ARG TARGETPLATFORM
|
||||
ARG QPDF_VERSION=12.4.0
|
||||
ARG IM_VERSION=7.1.2-29
|
||||
@@ -343,7 +343,7 @@ RUN mkdir -p /magick-export/usr/bin \
|
||||
|
||||
|
||||
# Stage 4: Build Python venv
|
||||
FROM ubuntu:noble@sha256:561618e2c15bf2397621dd04f96926663a3b5616c189cf7e38db7e82f5c538ea AS python-venv-build
|
||||
FROM ubuntu:noble@sha256:33ceb71981b602c1a7443a53469e4dba065f7503eab3078a2d7a57a2ab987517 AS python-venv-build
|
||||
ARG TARGETPLATFORM
|
||||
ARG UNOSERVER_VERSION=3.7
|
||||
|
||||
@@ -368,7 +368,7 @@ RUN --mount=type=cache,target=/root/.cache/pip,sharing=locked \
|
||||
|
||||
|
||||
# Final runtime image - the actual base image
|
||||
FROM eclipse-temurin:25-jre-noble@sha256:fbcf915c585659b30eb766ada4d6d7cfc9ec1040bf521e95bf61b10a25af73db AS runtime
|
||||
FROM eclipse-temurin:25-jre-noble@sha256:b4c93a50fc67612798db73d68ca3b0ee4ebdd51736e59cca370e689b9797037e AS runtime
|
||||
|
||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||
|
||||
|
||||
@@ -48,9 +48,23 @@ ENV STIRLING_FLAVOR=${STIRLING_FLAVOR}
|
||||
# portal or AI layers change; defaults false so normal builds skip the extra app.
|
||||
ARG BUILD_PORTAL=false
|
||||
|
||||
# Which Stirling account the portal connects to. Build-time because Vite inlines VITE_* into the
|
||||
# bundle; there is no runtime override. Empty leaves the committed .env.proprietary defaults, which
|
||||
# is what an ordinary image wants: no Stirling account and no connect flow. The publishable key is
|
||||
# client-side by design, not a secret. Pass the URL and the key from the same Supabase project or
|
||||
# the browser accepts the pair and Supabase rejects it, which surfaces later as "session expired".
|
||||
ARG VITE_SUPABASE_URL=""
|
||||
ARG VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY=""
|
||||
ARG VITE_SAAS_API_URL=""
|
||||
|
||||
# Bundle only the JPDFium native for this image's target arch.
|
||||
ARG TARGETARCH
|
||||
# Exported only when non-empty: Vite reads process.env ahead of the .env files, so exporting an
|
||||
# empty value would blank the committed default rather than fall back to it.
|
||||
RUN JPDFIUM_PLATFORM="$([ "$TARGETARCH" = arm64 ] && echo linux-arm64 || echo linux-x64)" && \
|
||||
if [ -n "${VITE_SUPABASE_URL}" ]; then export VITE_SUPABASE_URL="${VITE_SUPABASE_URL}"; fi; \
|
||||
if [ -n "${VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY}" ]; then export VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY="${VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY}"; fi; \
|
||||
if [ -n "${VITE_SAAS_API_URL}" ]; then export VITE_SAAS_API_URL="${VITE_SAAS_API_URL}"; fi; \
|
||||
STIRLING_FLAVOR=${STIRLING_FLAVOR} \
|
||||
gradle clean build \
|
||||
-PbuildWithFrontend=true \
|
||||
@@ -61,7 +75,7 @@ RUN JPDFIUM_PLATFORM="$([ "$TARGETARCH" = arm64 ] && echo linux-arm64 || echo li
|
||||
--no-daemon
|
||||
|
||||
# Stage 2: Extract Spring Boot Layers
|
||||
FROM eclipse-temurin:25-jre-noble@sha256:fbcf915c585659b30eb766ada4d6d7cfc9ec1040bf521e95bf61b10a25af73db AS jar-extract
|
||||
FROM eclipse-temurin:25-jre-noble@sha256:b4c93a50fc67612798db73d68ca3b0ee4ebdd51736e59cca370e689b9797037e AS jar-extract
|
||||
WORKDIR /tmp
|
||||
COPY --from=app-build /app/app/core/build/libs/*.jar app.jar
|
||||
RUN java -Djarmode=tools -jar app.jar extract --layers --destination /layers
|
||||
|
||||
@@ -61,7 +61,7 @@ RUN --mount=type=cache,id=stirling-pdf-npm-cache,target=/root/.npm,sharing=locke
|
||||
--no-daemon
|
||||
|
||||
# Stage 2: Extract Spring Boot Layers
|
||||
FROM eclipse-temurin:25-jre-noble@sha256:fbcf915c585659b30eb766ada4d6d7cfc9ec1040bf521e95bf61b10a25af73db AS jar-extract
|
||||
FROM eclipse-temurin:25-jre-noble@sha256:b4c93a50fc67612798db73d68ca3b0ee4ebdd51736e59cca370e689b9797037e AS jar-extract
|
||||
WORKDIR /tmp
|
||||
COPY --from=app-build /app/app/core/build/libs/*.jar app.jar
|
||||
RUN java -Djarmode=tools -jar app.jar extract --layers --destination /layers
|
||||
|
||||
@@ -62,7 +62,7 @@ RUN --mount=type=cache,id=stirling-pdf-npm-cache,target=/root/.npm,sharing=locke
|
||||
|
||||
# Stage 2: Runtime image
|
||||
# glibc base (not Alpine/musl): JPDFium's PDFium natives are glibc-linked.
|
||||
FROM eclipse-temurin:25-jre-noble@sha256:fbcf915c585659b30eb766ada4d6d7cfc9ec1040bf521e95bf61b10a25af73db
|
||||
FROM eclipse-temurin:25-jre-noble@sha256:b4c93a50fc67612798db73d68ca3b0ee4ebdd51736e59cca370e689b9797037e
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive \
|
||||
LANG=C.UTF-8 \
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Standalone unoserver image for Stirling-PDF remote UNO mode.
|
||||
# Pinned to unoserver 3.7 to match Stirling-PDF's client (avoids wire mismatch).
|
||||
|
||||
FROM ubuntu:noble@sha256:561618e2c15bf2397621dd04f96926663a3b5616c189cf7e38db7e82f5c538ea
|
||||
FROM ubuntu:noble@sha256:33ceb71981b602c1a7443a53469e4dba065f7503eab3078a2d7a57a2ab987517
|
||||
|
||||
ARG UNOSERVER_VERSION=3.7
|
||||
# ~120 MB of CJK fonts — opt-in.
|
||||
|
||||
@@ -20,7 +20,7 @@ engine = [
|
||||
# No `voyageai` extra either; stirling.documents.voyage speaks its API directly.
|
||||
"pydantic-ai-slim[anthropic,openai]>=1.107.2,<2.0.0",
|
||||
"pydantic-settings>=2.15.0",
|
||||
"python-dotenv>=1.2.2",
|
||||
"python-dotenv>=1.2.3",
|
||||
"sqlite-vec>=0.1.9",
|
||||
"uvicorn>=0.52.3",
|
||||
]
|
||||
@@ -42,7 +42,7 @@ cucumber = [
|
||||
"pillow>=12.3.0",
|
||||
"pypdf[crypto]>=6.15.0",
|
||||
"qrcode[pil]>=8.2",
|
||||
"reportlab>=5.0.0",
|
||||
"reportlab>=5.0.1",
|
||||
"requests>=2.34.2",
|
||||
]
|
||||
# Shared Python utilities used by repository scripts and CI workflows.
|
||||
@@ -51,7 +51,7 @@ tools = [
|
||||
"defusedxml>=0.7.1",
|
||||
"fonttools>=4.63.0",
|
||||
"fpdf2>=2.8.8",
|
||||
"openai>=2.53.0",
|
||||
"openai>=3.3.1",
|
||||
"requests>=2.34.2",
|
||||
"tomli-w>=1.2.0",
|
||||
"tomlkit>=0.15.1",
|
||||
|
||||
Generated
+13
-27
@@ -466,7 +466,7 @@ cucumber = [
|
||||
{ name = "pillow", specifier = ">=12.3.0" },
|
||||
{ name = "pypdf", extras = ["crypto"], specifier = ">=6.15.0" },
|
||||
{ name = "qrcode", extras = ["pil"], specifier = ">=8.2" },
|
||||
{ name = "reportlab", specifier = ">=5.0.0" },
|
||||
{ name = "reportlab", specifier = ">=5.0.1" },
|
||||
{ name = "requests", specifier = ">=2.34.2" },
|
||||
]
|
||||
engine = [
|
||||
@@ -479,7 +479,7 @@ engine = [
|
||||
{ name = "pydantic", specifier = ">=2.13.4" },
|
||||
{ name = "pydantic-ai-slim", extras = ["anthropic", "openai"], specifier = ">=1.107.2,<2.0.0" },
|
||||
{ name = "pydantic-settings", specifier = ">=2.15.0" },
|
||||
{ name = "python-dotenv", specifier = ">=1.2.2" },
|
||||
{ name = "python-dotenv", specifier = ">=1.2.3" },
|
||||
{ name = "sqlite-vec", specifier = ">=0.1.9" },
|
||||
{ name = "uvicorn", specifier = ">=0.52.3" },
|
||||
]
|
||||
@@ -501,7 +501,7 @@ tools = [
|
||||
{ name = "defusedxml", specifier = ">=0.7.1" },
|
||||
{ name = "fonttools", specifier = ">=4.63.0" },
|
||||
{ name = "fpdf2", specifier = ">=2.8.8" },
|
||||
{ name = "openai", specifier = ">=2.53.0" },
|
||||
{ name = "openai", specifier = ">=3.3.1" },
|
||||
{ name = "requests", specifier = ">=2.34.2" },
|
||||
{ name = "tomli-w", specifier = ">=1.2.0" },
|
||||
{ name = "tomlkit", specifier = ">=0.15.1" },
|
||||
@@ -844,21 +844,19 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "openai"
|
||||
version = "2.53.0"
|
||||
version = "3.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "distro" },
|
||||
{ name = "httpx" },
|
||||
{ name = "httpx2" },
|
||||
{ name = "jiter" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "sniffio" },
|
||||
{ name = "tqdm" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ef/cf/36e3e7235fdf6d125c052acc0970924611b17a20a4fe580596faf4566a65/openai-2.53.0.tar.gz", hash = "sha256:baf5802ad08980e1d9d561e1b996e800c8bcd14af5847c6d0e7a5cc59e4d4116", size = 1099435, upload-time = "2026-08-03T21:42:01.664Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7d/9c/ba0c292b4032ede74c249ca314ad64eb1bb5a03a843f6e01facb02f80cd8/openai-3.3.1.tar.gz", hash = "sha256:6f22807de1a976c932cecda620e8172a8c3fdbaeed29c7f21564e0c2410edf56", size = 1282113, upload-time = "2026-08-19T16:31:35.006Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/78/0f/cc6afea3542a5142c5d8fc8211c5e059a8375105d004a41dfa2c7948dbb0/openai-2.53.0-py3-none-any.whl", hash = "sha256:c694ffc747a3c4d1663ef2b07b811315a476164ee5efa3a993967349ebca7618", size = 1659829, upload-time = "2026-08-03T21:41:59.581Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/db/2b7a1b3de659bb82aef979116c74e809982b13e42c057759767552b5155f/openai-3.3.1-py3-none-any.whl", hash = "sha256:9652df7fdf8ee6f5bd58e0a12f2b1d414a18e0f06bb7a9a57c8643a5f5469bd3", size = 1690337, upload-time = "2026-08-19T16:31:32.812Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1262,11 +1260,11 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "python-dotenv"
|
||||
version = "1.2.2"
|
||||
version = "1.2.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6a/53/ed9d74092561d4b01a2ef1349d52cdbc135e526c245f366b089cfca6de49/python_dotenv-1.2.3.tar.gz", hash = "sha256:a20a594dabeaa385725aa239d5244871c143ecb356add8a20fcf23773a6c3a35", size = 58945, upload-time = "2026-08-16T16:54:54.067Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/17/c5c6b53ddc18f297992099b3d9ec16c855c0ccc83263a21fe4d1c625ec6c/python_dotenv-1.2.3-py3-none-any.whl", hash = "sha256:904552145e8bfed22162c09dab1c2b9b54fefa7b23ba780f4f26ca0316b0f0d9", size = 22780, upload-time = "2026-08-16T16:54:52.473Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1373,15 +1371,15 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "reportlab"
|
||||
version = "5.0.0"
|
||||
version = "5.0.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "charset-normalizer" },
|
||||
{ name = "pillow" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/41/d6/4b7b0cf56880eb96533e607967be6a939e344675601e033d113a0bfa1f4e/reportlab-5.0.0.tar.gz", hash = "sha256:e4494a0c6623ae213bb856fba523171b2b54a7bf629fda02d5e525a7b899a784", size = 3701928, upload-time = "2026-06-18T11:34:31.145Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/4a/51/dbe28534ae12c852f61be91f039f343305fd1f34f1c66b8de75afae7a525/reportlab-5.0.1.tar.gz", hash = "sha256:ebd13154be1c8515e665de70bd2d303ae9ddc3ef47e44afd5116441ca0283a26", size = 3945711, upload-time = "2026-08-20T13:48:16.461Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/07/70085c17a369605f15e301d10ab902115019b1126c7253d964afc230c7d6/reportlab-5.0.0-py3-none-any.whl", hash = "sha256:9d5a3affa84919e1111ede580031266a570e93b1ce388219621347965ff1d93c", size = 1956710, upload-time = "2026-06-18T11:34:29.07Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/cb/dacbc268cb68d0428ea2cbd85266195a9ab3e677449589ddae59bd7542ac/reportlab-5.0.1-py3-none-any.whl", hash = "sha256:1c36e6bb0e71780c72331eba60da7f602e8d4389a8723825af71342e49d791e8", size = 1957258, upload-time = "2026-08-20T13:48:14.026Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1566,18 +1564,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/13/bc/8c13eb66537dce1d2bd3a57132902f38d0e7f5bb46fa9f4daed9fe9d76ee/tomlkit-0.15.1-py3-none-any.whl", hash = "sha256:177a05aece5a8ca5266fd3c448abb47b8d352f09d477d3ca8332db4d89b24304", size = 49449, upload-time = "2026-07-17T01:48:05.728Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tqdm"
|
||||
version = "4.70.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/21/3b/6c24bec5be5e743ffd99576daa5cc077722fc7d5bbc00bd133fa0c698dc6/tqdm-4.70.0.tar.gz", hash = "sha256:55b0b0dbd97462d06ebee91e4dac24ed4d4702be82b24f07e6c1d27e08cea220", size = 795438, upload-time = "2026-07-27T11:33:15.271Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/1c/01bfd571a64e7f270e6bab5e33777debe0edc56759233ce84f27dec92d14/tqdm-4.70.0-py3-none-any.whl", hash = "sha256:7f585706bfddbdebf89daac705b2dfcc16890130727d3197ca62c732b4310953", size = 80184, upload-time = "2026-07-27T11:33:13.167Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "truststore"
|
||||
version = "0.10.4"
|
||||
|
||||
@@ -3526,7 +3526,6 @@ label = "إحداثي Y"
|
||||
|
||||
[crop.error]
|
||||
failed = "فشل قصّ PDF"
|
||||
invalidArea = "منطقة القص تتجاوز حدود PDF"
|
||||
|
||||
[crop.preview]
|
||||
title = "معاينة منطقة القص"
|
||||
|
||||
@@ -3526,7 +3526,6 @@ label = "Y mövqeyi"
|
||||
|
||||
[crop.error]
|
||||
failed = "PDF-i kəsmək alınmadı"
|
||||
invalidArea = "Kəsmə sahəsi PDF sərhədlərini aşır"
|
||||
|
||||
[crop.preview]
|
||||
title = "Kəsmə sahəsinin seçimi"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user