Compare commits

...
Author SHA1 Message Date
Ludy 5f2c4e7205 Merge branch 'main' into backendtests_20260703 2026-07-16 16:38:34 +02:00
Ludy 9c37562029 Merge branch 'main' into backendtests_20260703 2026-07-09 23:15:26 +02:00
Ludy 37a31b3783 Merge branch 'main' into backendtests_20260703 2026-07-09 11:05:31 +02:00
Ludy 87212c8315 Merge branch 'main' into backendtests_20260703 2026-07-07 00:46:21 +02:00
Ludy87 e5f239468b Update backend.yml 2026-07-07 00:45:45 +02:00
Ludy 35db8e6d77 Merge branch 'main' into backendtests_20260703 2026-07-06 21:31:52 +02:00
Ludy87 b235bda9e9 Update backend.yml 2026-07-06 21:30:38 +02:00
Ludy 5ae61ee0ad Merge branch 'main' into backendtests_20260703 2026-07-06 13:53:00 +02:00
Ludy87 34ff6a899b Add license report generation and distribution
Add STIRLING_FLAVOR environment variable to licenses:generate task to ensure the SaaS flavor includes all dependencies in the report.

Add new licenses:generate:copy task to copy the generated license report (index.json) to the static resources directory where it can be served by the application. Includes cross-platform support for Linux/macOS (cp) and Windows (PowerShell).
2026-07-04 15:53:48 +02:00
Ludy87 7e08a8d4a6 Update build.gradle 2026-07-04 13:48:21 +02:00
Ludy87 854804708a Update build.gradle 2026-07-04 13:44:36 +02:00
Ludy87 bff4983265 Update backend.yml 2026-07-04 13:33:33 +02:00
Ludy87 2c233b62f9 Refactor backend tasks to use cross-platform variables
Eliminate platform-specific command duplication in backend.yml by introducing GRADLE/TEST/FORMAT/CLEAN variables. Reorganize test, format, and format:check tasks into matrix patterns that explicitly test all flavor/security combinations. Add PowerShell script for reliable version extraction on Windows.
2026-07-04 13:24:33 +02:00
Ludy87 4792fe1d30 Update build.gradle 2026-07-03 23:01:03 +02:00
Ludy87 554f0cd785 Update build.gradle 2026-07-03 21:22:04 +02:00
Ludy87 791598114e Update build.gradle 2026-07-03 20:58:46 +02:00
Ludy87 86b4fb6c01 Update build.gradle 2026-07-03 20:37:14 +02:00
Ludy87 852c89fed3 Update backend.yml 2026-07-03 19:53:28 +02:00
Ludy87 51950a618b Backend tests 2026-07-03 19:51:06 +02:00
13 changed files with 618 additions and 141 deletions
+106 -69
View File
@@ -15,9 +15,16 @@ version: '3'
# stripping; cmd.exe also requires `.\` (not bare `gradlew.bat`)
# because modern Windows excludes cwd from cmd's search path.
vars:
GRADLE: '{{if eq OS "windows"}}cmd /c ".\gradlew.bat"{{else}}./gradlew{{end}}'
TEST: '{{.GRADLE}} test --no-daemon'
FORMAT: '{{.GRADLE}} spotlessApply'
FORMATCHECK: '{{.GRADLE}} spotlessCheck'
CLEAN: '{{.GRADLE}} clean'
tasks:
dev:
desc: "Start backend dev server"
desc: "Start the backend dev server"
cmds:
- task: dev:proprietary
vars:
@@ -28,7 +35,7 @@ tasks:
SECURITY_ENABLELOGIN: '{{.SECURITY_ENABLELOGIN}}'
dev:proprietary:
desc: "Start backend dev server in proprietary mode"
desc: "Start the backend dev server in proprietary mode"
# `dotenv:` reads from the root Taskfile's directory (".") because this
# subtaskfile is included with `dir: .`. Local overrides in
# .env.proprietary.local win over the committed .env.proprietary defaults.
@@ -43,22 +50,16 @@ tasks:
env:
SERVER_PORT: '{{.PORT}}'
cmds:
- cmd: '{{if .AIENGINE_URL}}AIENGINE_URL={{.AIENGINE_URL}} AIENGINE_ENABLED={{.AIENGINE_ENABLED}} AIENGINE_TIMEOUTSECONDS={{.AIENGINE_TIMEOUTSECONDS}} {{end}}{{if .SECURITY_ENABLELOGIN}}SECURITY_ENABLELOGIN={{.SECURITY_ENABLELOGIN}} {{end}}cmd /c ".\gradlew.bat :stirling-pdf:bootRun"'
platforms: [windows]
- cmd: '{{if .AIENGINE_URL}}AIENGINE_URL={{.AIENGINE_URL}} AIENGINE_ENABLED={{.AIENGINE_ENABLED}} AIENGINE_TIMEOUTSECONDS={{.AIENGINE_TIMEOUTSECONDS}} {{end}}{{if .SECURITY_ENABLELOGIN}}SECURITY_ENABLELOGIN={{.SECURITY_ENABLELOGIN}} {{end}}./gradlew :stirling-pdf:bootRun'
platforms: [linux, darwin]
- '{{if .AIENGINE_URL}}AIENGINE_URL={{.AIENGINE_URL}} AIENGINE_ENABLED={{.AIENGINE_ENABLED}} AIENGINE_TIMEOUTSECONDS={{.AIENGINE_TIMEOUTSECONDS}} {{end}}{{if .SECURITY_ENABLELOGIN}}SECURITY_ENABLELOGIN={{.SECURITY_ENABLELOGIN}} {{end}}{{.GRADLE}} :stirling-pdf:bootRun'
dev:bundled:
desc: "Clean + bootRun with frontend bundled into the backend (single :8080 server)"
desc: "Start the backend with the frontend bundled into the JAR"
ignore_error: true
cmds:
- cmd: cmd /c ".\gradlew.bat clean bootRun -PbuildWithFrontend=true"
platforms: [windows]
- cmd: ./gradlew clean bootRun -PbuildWithFrontend=true
platforms: [linux, darwin]
- '{{.CLEAN}} bootRun -PbuildWithFrontend=true'
dev:saas:
desc: "Start backend in SaaS flavor against Supabase"
desc: "Start the backend in SaaS flavor"
# `dotenv:` reads from the root Taskfile's directory (".") because this
# subtaskfile is included with `dir: .`.
dotenv: ['app/.env.saas.local', 'app/.env.saas']
@@ -77,71 +78,104 @@ tasks:
AIENGINE_ENABLED: '{{.AIENGINE_ENABLED}}'
AIENGINE_TIMEOUTSECONDS: '{{.AIENGINE_TIMEOUTSECONDS}}'
cmds:
- cmd: cmd /c ".\gradlew.bat :stirling-pdf:bootRun {{if .PROFILES}}--args=\"--spring.profiles.include={{.PROFILES}}\"{{end}}"
platforms: [windows]
- cmd: ./gradlew :stirling-pdf:bootRun {{if .PROFILES}}--args='--spring.profiles.include={{.PROFILES}}'{{end}}
platforms: [linux, darwin]
- '{{.GRADLE}} :stirling-pdf:bootRun {{if .PROFILES}}--args=\"--spring.profiles.include={{.PROFILES}}\"{{end}}'
build:
desc: "Full backend build"
desc: "Build the backend"
cmds:
- cmd: cmd /c ".\gradlew.bat clean build"
platforms: [windows]
- cmd: ./gradlew clean build
platforms: [linux, darwin]
- '{{.CLEAN}} build'
build:fast:
desc: "Build without tests"
desc: "Build the backend without running tests"
cmds:
- cmd: cmd /c ".\gradlew.bat clean build -x test"
platforms: [windows]
- cmd: ./gradlew clean build -x test
platforms: [linux, darwin]
- '{{.CLEAN}} build -x test'
build:ci:
desc: "Build for CI (formatting checked separately)"
desc: "Build the backend for CI"
cmds:
- cmd: cmd /c ".\gradlew.bat build -PnoSpotless"
platforms: [windows]
- cmd: ./gradlew build -PnoSpotless
platforms: [linux, darwin]
- '{{.GRADLE}} build -PnoSpotless'
prematrix:*:
desc: "Run the backend test matrix"
vars:
MATRIXNAME: '{{index .MATCH 0}}'
cmds:
- for:
matrix:
FLAVOR: ["proprietary", "core", "saas"]
LOGIN: ["true", "false"]
SEC: ["true", "false"]
task: '{{.MATRIXNAME}}:matrix'
vars:
STIRLING_FLAVOR: '{{.ITEM.FLAVOR}}'
SECURITY_ENABLELOGIN: '{{.ITEM.LOGIN}}'
DOCKER_ENABLE_SECURITY: '{{.ITEM.SEC}}'
test:
desc: "Run backend tests"
desc: "Run the backend test matrix"
cmds:
- cmd: cmd /c ".\gradlew.bat test"
platforms: [windows]
- cmd: ./gradlew test
platforms: [linux, darwin]
- task: prematrix:test
test:matrix:
internal: true
env:
SECURITY_ENABLELOGIN: '{{.SECURITY_ENABLELOGIN}}'
STIRLING_FLAVOR: '{{.STIRLING_FLAVOR}}'
DOCKER_ENABLE_SECURITY: '{{.DOCKER_ENABLE_SECURITY}}'
desc: "Run backend tests for one configuration"
# Cover the backend with the main build/property combinations that change
# Gradle behavior in this repo.
cmds:
- '{{.TEST}}'
- '{{.TEST}} -PbuildWithFrontend=true'
- '{{.TEST}} -PprototypesMode=true'
- '{{.TEST}} -PbuildWithFrontend=true -PprototypesMode=true'
format:
desc: "Auto-fix code formatting"
desc: "Run the backend formatting matrix"
cmds:
- cmd: cmd /c ".\gradlew.bat spotlessApply"
platforms: [windows]
- cmd: ./gradlew spotlessApply
platforms: [linux, darwin]
- task: prematrix:format
format:matrix:
internal: true
desc: "Apply backend formatting for one configuration"
env:
SECURITY_ENABLELOGIN: '{{.SECURITY_ENABLELOGIN}}'
STIRLING_FLAVOR: '{{.STIRLING_FLAVOR}}'
DOCKER_ENABLE_SECURITY: '{{.DOCKER_ENABLE_SECURITY}}'
cmds:
- '{{.FORMAT}}'
- '{{.FORMAT}} -PbuildWithFrontend=true'
- '{{.FORMAT}} -PprototypesMode=true'
format:check:
desc: "Check code formatting"
desc: "Check the backend formatting matrix"
cmds:
- cmd: cmd /c ".\gradlew.bat spotlessCheck"
platforms: [windows]
- cmd: ./gradlew spotlessCheck
platforms: [linux, darwin]
- task: prematrix:format:check
format:check:matrix:
internal: true
desc: "Check backend formatting for one configuration"
env:
SECURITY_ENABLELOGIN: '{{.SECURITY_ENABLELOGIN}}'
STIRLING_FLAVOR: '{{.STIRLING_FLAVOR}}'
DOCKER_ENABLE_SECURITY: '{{.DOCKER_ENABLE_SECURITY}}'
# Mirror the test matrix so formatting checks use the same backend
# configuration variants.
cmds:
- '{{.FORMATCHECK}}'
- '{{.FORMATCHECK}} -PbuildWithFrontend=true'
- '{{.FORMATCHECK}} -PprototypesMode=true'
fix:
desc: "Auto-fix backend"
desc: "Apply backend fixes"
cmds:
- task: format
swagger:
desc: "Generate OpenAPI docs"
desc: "Generate the backend OpenAPI docs"
cmds:
- cmd: cmd /c ".\gradlew.bat :stirling-pdf:copySwaggerDoc"
platforms: [windows]
- cmd: ./gradlew :stirling-pdf:copySwaggerDoc
platforms: [linux, darwin]
- '{{.GRADLE}} :stirling-pdf:copySwaggerDoc'
sources:
- app/core/src/main/java/**/*.java
- app/proprietary/src/main/java/**/*.java
@@ -150,40 +184,43 @@ tasks:
- SwaggerDoc.json
check:
desc: "Backend quality gate"
desc: "Run the backend quality gate"
cmds:
- task: format:check
- task: test
version:
desc: "Print project version"
desc: "Print the backend version"
silent: true
cmds:
- cmd: cmd /c ".\gradlew.bat printVersion --quiet" | tail -1
- cmd: pwsh -NoProfile -File scripts/backend-version.ps1
platforms: [windows]
- cmd: ./gradlew printVersion --quiet | tail -1
platforms: [linux, darwin]
licenses:check:
desc: "Check dependency licenses"
desc: "Check backend dependency licenses"
cmds:
- cmd: cmd /c ".\gradlew.bat checkLicense --no-parallel"
platforms: [windows]
- cmd: ./gradlew checkLicense --no-parallel
platforms: [linux, darwin]
- '{{.GRADLE}} checkLicense --no-parallel'
licenses:generate:
desc: "Check and generate dependency license report"
desc: "Generate the backend dependency license report"
env:
# Use the SaaS flavor so the license report includes all dependencies.
STIRLING_FLAVOR: 'saas'
cmds:
- cmd: cmd /c ".\gradlew.bat checkLicense generateLicenseReport --no-parallel"
platforms: [windows]
- cmd: ./gradlew checkLicense generateLicenseReport --no-parallel
- '{{.GRADLE}} checkLicense generateLicenseReport --no-parallel'
licenses:generate:copy:
desc: "Generate and copy the backend dependency license report"
deps: [licenses:generate]
cmds:
- cmd: cp build/reports/dependency-license/index.json app/core/src/main/resources/static/3rdPartyLicenses.json
platforms: [linux, darwin]
- cmd: powershell -NoProfile -Command "New-Item -ItemType Directory -Force -Path 'app/core/src/main/resources/static' | Out-Null; Copy-Item -Force 'build/reports/dependency-license/index.json' 'app/core/src/main/resources/static/3rdPartyLicenses.json'"
platforms: [windows]
clean:
desc: "Clean build artifacts"
desc: "Clean backend build artifacts"
cmds:
- cmd: cmd /c ".\gradlew.bat clean"
platforms: [windows]
- cmd: ./gradlew clean
platforms: [linux, darwin]
- '{{.CLEAN}}'
+11 -11
View File
@@ -6,7 +6,7 @@ spotless {
java {
target 'src/**/java/**/*.java'
targetExclude 'src/main/java/org/apache/**'
googleJavaFormat(googleJavaFormatVersion).aosp().reorderImports(false)
googleJavaFormat(rootProject.ext.googleJavaFormatVersion).aosp().reorderImports(false)
// google-java-format 1.28.0 bundles Guava 32.x which crashes Spotless lint on JDK 24/25
suppressLintsFor { setStep('google-java-format') }
@@ -29,19 +29,19 @@ spotless {
}
}
dependencies {
api "com.google.guava:guava:${guavaVersion}"
api "com.google.guava:guava:${rootProject.ext.guavaVersion}"
api 'org.springframework.boot:spring-boot-starter-webmvc'
api 'org.springframework.boot:spring-boot-starter-aspectj'
api 'com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer:20260313.1'
api 'com.fathzer:javaluator:3.0.6'
api 'com.posthog.java:posthog:1.2.0'
api "org.apache.commons:commons-lang3:${commonsLang3}"
api "org.apache.commons:commons-lang3:${rootProject.ext.commonsLang3}"
api 'com.drewnoakes:metadata-extractor:2.20.0' // Image metadata extractor
api 'com.vladsch.flexmark:flexmark-html2md-converter:0.64.8'
api "org.apache.pdfbox:pdfbox:$pdfboxVersion"
api "org.apache.pdfbox:pdfbox-io:$pdfboxVersion"
api "org.apache.pdfbox:xmpbox:$pdfboxVersion"
api "org.apache.pdfbox:preflight:$pdfboxVersion"
api "org.apache.pdfbox:pdfbox:${rootProject.ext.pdfboxVersion}"
api "org.apache.pdfbox:pdfbox-io:${rootProject.ext.pdfboxVersion}"
api "org.apache.pdfbox:xmpbox:${rootProject.ext.pdfboxVersion}"
api "org.apache.pdfbox:preflight:${rootProject.ext.pdfboxVersion}"
api 'com.github.junrar:junrar:7.5.10' // RAR archive support for CBR files
api 'jakarta.servlet:jakarta.servlet-api:6.1.0'
api 'org.snakeyaml:snakeyaml-engine:3.0.1'
@@ -60,7 +60,7 @@ dependencies {
exclude group: 'com.google.code.gson', module: 'gson'
}
api "com.stirling:jpdfium:${jpdfiumVersion}"
api "com.stirling:jpdfium:${rootProject.ext.jpdfiumVersion}"
// -PjpdfiumPlatforms=all|<csv of linux-x64,linux-arm64,darwin-x64,darwin-arm64,windows-x64>
def jpdfiumPlatformsProp = (project.findProperty('jpdfiumPlatforms') ?: 'all').toString().trim()
@@ -75,12 +75,12 @@ dependencies {
}
logger.lifecycle("JPDFium native platforms: ${jpdfiumPlatforms.join(', ')}")
jpdfiumPlatforms.each { platform ->
runtimeOnly "com.stirling:jpdfium-natives-${platform}:${jpdfiumVersion}"
runtimeOnly "com.stirling:jpdfium-natives-${platform}:${rootProject.ext.jpdfiumVersion}"
}
// Bucket4j (local in-process token bucket for RateLimitStore default impl)
implementation "com.bucket4j:bucket4j_jdk17-core:${bucket4jVersion}"
implementation "com.bucket4j:bucket4j_jdk17-core:${rootProject.ext.bucket4jVersion}"
// ArchUnit: enforces module dependency direction (see ArchitectureTest)
testImplementation "com.tngtech.archunit:archunit-junit5:${archunitVersion}"
testImplementation "com.tngtech.archunit:archunit-junit5:${rootProject.ext.archunitVersion}"
}
+2
View File
@@ -0,0 +1,2 @@
# Auto-generated by MSW (`msw init`); regenerated verbatim, not hand-formatted.
src/main/resources/static/mockServiceWorker.js
+23 -20
View File
@@ -13,7 +13,7 @@ spotless {
java {
target 'src/**/java/**/*.java'
targetExclude 'src/main/resources/static/**', 'src/main/java/org/apache/**'
googleJavaFormat(googleJavaFormatVersion).aosp().reorderImports(false)
googleJavaFormat(rootProject.ext.googleJavaFormatVersion).aosp().reorderImports(false)
// google-java-format 1.28.0 bundles Guava 32.x which crashes Spotless lint on JDK 24/25
suppressLintsFor { setStep('google-java-format') }
@@ -67,21 +67,21 @@ dependencies {
exclude group: 'com.fasterxml.jackson.jaxrs'
exclude group: 'com.fasterxml.jackson.module', module: 'jackson-module-jaxb-annotations'
}
implementation "commons-io:commons-io:$commonsIoVersion"
implementation "org.bouncycastle:bcprov-jdk18on:$bouncycastleVersion"
implementation "org.bouncycastle:bcpkix-jdk18on:$bouncycastleVersion"
implementation "commons-io:commons-io:${rootProject.ext.commonsIoVersion}"
implementation "org.bouncycastle:bcprov-jdk18on:${rootProject.ext.bouncycastleVersion}"
implementation "org.bouncycastle:bcpkix-jdk18on:${rootProject.ext.bouncycastleVersion}"
implementation 'io.micrometer:micrometer-core'
implementation 'com.google.zxing:core:3.5.4'
implementation "org.commonmark:commonmark:$commonmarkVersion" // https://mvnrepository.com/artifact/org.commonmark/commonmark
implementation "org.commonmark:commonmark-ext-gfm-tables:$commonmarkVersion"
implementation "org.commonmark:commonmark:${rootProject.ext.commonmarkVersion}" // https://mvnrepository.com/artifact/org.commonmark/commonmark
implementation "org.commonmark:commonmark-ext-gfm-tables:${rootProject.ext.commonmarkVersion}"
// General PDF dependencies
implementation "org.apache.pdfbox:preflight:$pdfboxVersion"
implementation "org.apache.pdfbox:xmpbox:$pdfboxVersion"
implementation "org.apache.pdfbox:preflight:${rootProject.ext.pdfboxVersion}"
implementation "org.apache.pdfbox:xmpbox:${rootProject.ext.pdfboxVersion}"
implementation 'org.verapdf:validation-model:1.28.2'
// CVE-2025-66453: Explicit rhino 1.7.15 to override verapdf's 1.7.13
implementation "org.mozilla:rhino:${rhinoVersion}"
implementation "org.mozilla:rhino:${rootProject.ext.rhinoVersion}"
// veraPDF still uses javax.xml.bind, not the new jakarta namespace
implementation 'javax.xml.bind:jaxb-api:2.3.1'
@@ -89,33 +89,33 @@ dependencies {
implementation 'com.sun.xml.bind:jaxb-core:4.0.7'
// CVE-2022-25647: Explicit gson to prevent unsafe deserialization (tabula would pull 2.8.7)
implementation "com.google.code.gson:gson:${gsonVersion}"
implementation "com.google.code.gson:gson:${rootProject.ext.gsonVersion}"
implementation 'org.apache.pdfbox:jbig2-imageio:3.0.4'
implementation 'com.opencsv:opencsv:5.12.0' // https://mvnrepository.com/artifact/com.opencsv/opencsv
implementation 'org.apache.poi:poi-ooxml:5.5.1'
// Batik only bridge module needed (transitively pulls anim, gvt, util, css, dom, svg-dom)
// Replaces batik-all which included unused codec, svggen, transcoder, script modules
implementation "org.apache.xmlgraphics:batik-bridge:${batikVersion}"
implementation "org.apache.xmlgraphics:batik-bridge:${rootProject.ext.batikVersion}"
// Required by TwelveMonkeys imageio-batik SPI (SVGImageReaderSpi) during ImageIO init
runtimeOnly "org.apache.xmlgraphics:batik-transcoder:${batikVersion}"
runtimeOnly "org.apache.xmlgraphics:batik-transcoder:${rootProject.ext.batikVersion}"
// PDFBox Graphics2D bridge for Batik SVG to PDF conversion
implementation 'de.rototor.pdfbox:graphics2d:3.0.5'
// TwelveMonkeys
runtimeOnly "com.twelvemonkeys.imageio:imageio-batik:$imageioVersion"
runtimeOnly "com.twelvemonkeys.imageio:imageio-bmp:$imageioVersion"
runtimeOnly "com.twelvemonkeys.imageio:imageio-jpeg:$imageioVersion"
runtimeOnly "com.twelvemonkeys.imageio:imageio-tiff:$imageioVersion"
runtimeOnly "com.twelvemonkeys.imageio:imageio-webp:$imageioVersion"
runtimeOnly "com.twelvemonkeys.imageio:imageio-batik:${rootProject.ext.imageioVersion}"
runtimeOnly "com.twelvemonkeys.imageio:imageio-bmp:${rootProject.ext.imageioVersion}"
runtimeOnly "com.twelvemonkeys.imageio:imageio-jpeg:${rootProject.ext.imageioVersion}"
runtimeOnly "com.twelvemonkeys.imageio:imageio-tiff:${rootProject.ext.imageioVersion}"
runtimeOnly "com.twelvemonkeys.imageio:imageio-webp:${rootProject.ext.imageioVersion}"
// runtimeOnly "com.twelvemonkeys.imageio:imageio-hdr:$imageioVersion"
// runtimeOnly "com.twelvemonkeys.imageio:imageio-icns:$imageioVersion"
// runtimeOnly "com.twelvemonkeys.imageio:imageio-iff:$imageioVersion"
// runtimeOnly "com.twelvemonkeys.imageio:imageio-pcx:$imageioVersion@
// runtimeOnly "com.twelvemonkeys.imageio:imageio-pict:$imageioVersion"
// runtimeOnly "com.twelvemonkeys.imageio:imageio-pnm:$imageioVersion"
runtimeOnly "com.twelvemonkeys.imageio:imageio-psd:$imageioVersion"
runtimeOnly "com.twelvemonkeys.imageio:imageio-psd:${rootProject.ext.imageioVersion}"
// runtimeOnly "com.twelvemonkeys.imageio:imageio-sgi:$imageioVersion"
// runtimeOnly "com.twelvemonkeys.imageio:imageio-tga:$imageioVersion"
// runtimeOnly "com.twelvemonkeys.imageio:imageio-thumbsdb:$imageioVersion"
@@ -135,7 +135,6 @@ sourceSets {
}
// Disable regular jar
jar {
enabled = false
@@ -292,7 +291,11 @@ tasks.register('npmBuild', Exec) {
group = 'frontend'
description = 'Build editor frontend application'
workingDir file('../..')
commandLine = ['task', frontendBuildTask]
// Pin the repo-root Taskfile explicitly so the Exec task does not inherit
// an unrelated TASKFILE override from the surrounding environment.
commandLine = Os.isFamily(Os.FAMILY_WINDOWS) ?
['cmd', '/c', 'task', '--taskfile', 'Taskfile.yml', frontendBuildTask] :
['task', '--taskfile', 'Taskfile.yml', frontendBuildTask]
inputs.dir(new File(frontendEditorDir, 'src'))
inputs.dir(new File(frontendEditorDir, 'public'))
inputs.file(new File(frontendDir, 'package.json'))
@@ -20,17 +20,18 @@
--cc-separator-border-color: #e0e0e0;
--cc-toggle-on-bg: #007bff;
--cc-toggle-off-bg: #667481;
--cc-toggle-on-knob-bg: #ffffff;
--cc-toggle-off-knob-bg: #ffffff;
/* Toggle colors mirror Mantine Switch (light scheme) */
--cc-toggle-on-bg: var(--mantine-primary-color-filled, #007bff);
--cc-toggle-off-bg: var(--mantine-color-gray-3, #dee2e6);
--cc-toggle-on-knob-bg: var(--mantine-color-white, #ffffff);
--cc-toggle-off-knob-bg: var(--mantine-color-white, #ffffff);
--cc-toggle-enabled-icon-color: #ffffff;
--cc-toggle-disabled-icon-color: #ffffff;
--cc-toggle-readonly-bg: #f1f3f4;
--cc-toggle-readonly-knob-bg: #79747e;
--cc-toggle-readonly-knob-icon-color: #f1f3f4;
--cc-toggle-readonly-bg: var(--mantine-color-disabled, #f1f3f4);
--cc-toggle-readonly-knob-bg: var(--mantine-color-gray-0, #f8f9fa);
--cc-toggle-readonly-knob-icon-color: transparent;
--cc-section-category-border: #e0e0e0;
@@ -69,17 +70,18 @@
--cc-separator-border-color: #555555;
--cc-toggle-on-bg: #4dabf7;
--cc-toggle-off-bg: #667481;
--cc-toggle-on-knob-bg: #2d2d2d;
--cc-toggle-off-knob-bg: #2d2d2d;
/* Toggle colors mirror Mantine Switch (dark scheme) */
--cc-toggle-on-bg: var(--mantine-primary-color-filled, #4dabf7);
--cc-toggle-off-bg: var(--mantine-color-dark-5, #555555);
--cc-toggle-on-knob-bg: var(--mantine-color-white, #ffffff);
--cc-toggle-off-knob-bg: var(--mantine-color-white, #ffffff);
--cc-toggle-enabled-icon-color: #2d2d2d;
--cc-toggle-disabled-icon-color: #2d2d2d;
--cc-toggle-readonly-bg: #555555;
--cc-toggle-readonly-knob-bg: #8e8e8e;
--cc-toggle-readonly-knob-icon-color: #555555;
--cc-toggle-readonly-bg: var(--mantine-color-disabled, #555555);
--cc-toggle-readonly-knob-bg: var(--mantine-color-dark-3, #8e8e8e);
--cc-toggle-readonly-knob-icon-color: transparent;
--cc-section-category-border: #555555;
@@ -176,9 +178,16 @@
color: var(--cc-primary-color) !important;
}
/* Lower z-index so cookie banner appears behind onboarding modals */
/* Banner sits above the chat FAB but behind all modals and onboarding; value
is Z_INDEX_COOKIE_CONSENT_BANNER, set as this variable by useCookieConsent */
#cc-main {
z-index: 100 !important;
z-index: var(--z-index-cookie-consent) !important;
}
/* Preferences dialog sits above the settings modal it opens from; value is
Z_INDEX_COOKIE_PREFERENCES_MODAL, set as this variable by useCookieConsent */
.show--preferences #cc-main {
z-index: var(--z-index-cookie-preferences) !important;
}
/* Ensure consent modal text is visible in both themes */
@@ -203,3 +212,63 @@
#cc-main .cm__link {
color: var(--cc-primary-color) !important;
}
/* ── Category toggles restyled to match Mantine Switch (size sm) ──────────
Mantine sm metrics: 38×20 track, 14px plain thumb, 2.5px inline padding,
150ms ease transitions, no icon inside the thumb. Colors come from the
--cc-toggle-* variables above, which point at the Mantine palette. */
#cc-main .section__toggle,
#cc-main .section__toggle-wrapper,
#cc-main .toggle__icon,
#cc-main .toggle__label {
width: 38px !important;
height: 20px !important;
border-radius: 1000px !important;
}
/* Track: flat fill, no outline ring or border */
#cc-main .toggle__icon {
border: none !important;
box-shadow: none !important;
transition: background-color 150ms ease !important;
}
#cc-main .section__toggle:checked ~ .toggle__icon {
border: none !important;
box-shadow: none !important;
}
/* Always-enabled categories = Mantine disabled switch (must out-prioritise
the !important checked-track rule above) */
#cc-main .section__toggle:checked:disabled ~ .toggle__icon {
background: var(--cc-toggle-readonly-bg) !important;
border: none !important;
box-shadow: none !important;
}
#cc-main .section__toggle:disabled {
cursor: not-allowed !important;
}
/* Thumb: small plain circle, vertically centred, no drop shadow */
#cc-main .toggle__icon-circle {
width: 14px !important;
height: 14px !important;
top: 3px !important;
left: 2.5px !important;
box-shadow: none !important;
transition:
transform 150ms ease,
background-color 150ms ease !important;
}
/* Checked thumb travel: 38 14 2.5 = 21.5px end position */
#cc-main .section__toggle:checked ~ .toggle__icon .toggle__icon-circle {
transform: translateX(19px) !important;
}
/* Mantine switches have no check/cross glyph inside the thumb */
#cc-main .toggle__icon-on,
#cc-main .toggle__icon-off {
display: none !important;
}
@@ -0,0 +1,349 @@
/* eslint-disable */
/* tslint:disable */
/**
* Mock Service Worker.
* @see https://github.com/mswjs/msw
* - Please do NOT modify this file.
*/
const PACKAGE_VERSION = '2.14.6'
const INTEGRITY_CHECKSUM = '4db4a41e972cec1b64cc569c66952d82'
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
const activeClientIds = new Set()
addEventListener('install', function () {
self.skipWaiting()
})
addEventListener('activate', function (event) {
event.waitUntil(self.clients.claim())
})
addEventListener('message', async function (event) {
const clientId = Reflect.get(event.source || {}, 'id')
if (!clientId || !self.clients) {
return
}
const client = await self.clients.get(clientId)
if (!client) {
return
}
const allClients = await self.clients.matchAll({
type: 'window',
})
switch (event.data) {
case 'KEEPALIVE_REQUEST': {
sendToClient(client, {
type: 'KEEPALIVE_RESPONSE',
})
break
}
case 'INTEGRITY_CHECK_REQUEST': {
sendToClient(client, {
type: 'INTEGRITY_CHECK_RESPONSE',
payload: {
packageVersion: PACKAGE_VERSION,
checksum: INTEGRITY_CHECKSUM,
},
})
break
}
case 'MOCK_ACTIVATE': {
activeClientIds.add(clientId)
sendToClient(client, {
type: 'MOCKING_ENABLED',
payload: {
client: {
id: client.id,
frameType: client.frameType,
},
},
})
break
}
case 'CLIENT_CLOSED': {
activeClientIds.delete(clientId)
const remainingClients = allClients.filter((client) => {
return client.id !== clientId
})
// Unregister itself when there are no more clients
if (remainingClients.length === 0) {
self.registration.unregister()
}
break
}
}
})
addEventListener('fetch', function (event) {
const requestInterceptedAt = Date.now()
// Bypass navigation requests.
if (event.request.mode === 'navigate') {
return
}
// Opening the DevTools triggers the "only-if-cached" request
// that cannot be handled by the worker. Bypass such requests.
if (
event.request.cache === 'only-if-cached' &&
event.request.mode !== 'same-origin'
) {
return
}
// Bypass all requests when there are no active clients.
// Prevents the self-unregistered worked from handling requests
// after it's been terminated (still remains active until the next reload).
if (activeClientIds.size === 0) {
return
}
const requestId = crypto.randomUUID()
event.respondWith(handleRequest(event, requestId, requestInterceptedAt))
})
/**
* @param {FetchEvent} event
* @param {string} requestId
* @param {number} requestInterceptedAt
*/
async function handleRequest(event, requestId, requestInterceptedAt) {
const client = await resolveMainClient(event)
const requestCloneForEvents = event.request.clone()
const response = await getResponse(
event,
client,
requestId,
requestInterceptedAt,
)
// Send back the response clone for the "response:*" life-cycle events.
// Ensure MSW is active and ready to handle the message, otherwise
// this message will pend indefinitely.
if (client && activeClientIds.has(client.id)) {
const serializedRequest = await serializeRequest(requestCloneForEvents)
// Clone the response so both the client and the library could consume it.
const responseClone = response.clone()
sendToClient(
client,
{
type: 'RESPONSE',
payload: {
isMockedResponse: IS_MOCKED_RESPONSE in response,
request: {
id: requestId,
...serializedRequest,
},
response: {
type: responseClone.type,
status: responseClone.status,
statusText: responseClone.statusText,
headers: Object.fromEntries(responseClone.headers.entries()),
body: responseClone.body,
},
},
},
responseClone.body ? [serializedRequest.body, responseClone.body] : [],
)
}
return response
}
/**
* Resolve the main client for the given event.
* Client that issues a request doesn't necessarily equal the client
* that registered the worker. It's with the latter the worker should
* communicate with during the response resolving phase.
* @param {FetchEvent} event
* @returns {Promise<Client | undefined>}
*/
async function resolveMainClient(event) {
const client = await self.clients.get(event.clientId)
if (activeClientIds.has(event.clientId)) {
return client
}
if (client?.frameType === 'top-level') {
return client
}
const allClients = await self.clients.matchAll({
type: 'window',
})
return allClients
.filter((client) => {
// Get only those clients that are currently visible.
return client.visibilityState === 'visible'
})
.find((client) => {
// Find the client ID that's recorded in the
// set of clients that have registered the worker.
return activeClientIds.has(client.id)
})
}
/**
* @param {FetchEvent} event
* @param {Client | undefined} client
* @param {string} requestId
* @param {number} requestInterceptedAt
* @returns {Promise<Response>}
*/
async function getResponse(event, client, requestId, requestInterceptedAt) {
// Clone the request because it might've been already used
// (i.e. its body has been read and sent to the client).
const requestClone = event.request.clone()
function passthrough() {
// Cast the request headers to a new Headers instance
// so the headers can be manipulated with.
const headers = new Headers(requestClone.headers)
// Remove the "accept" header value that marked this request as passthrough.
// This prevents request alteration and also keeps it compliant with the
// user-defined CORS policies.
const acceptHeader = headers.get('accept')
if (acceptHeader) {
const values = acceptHeader.split(',').map((value) => value.trim())
const filteredValues = values.filter(
(value) => value !== 'msw/passthrough',
)
if (filteredValues.length > 0) {
headers.set('accept', filteredValues.join(', '))
} else {
headers.delete('accept')
}
}
return fetch(requestClone, { headers })
}
// Bypass mocking when the client is not active.
if (!client) {
return passthrough()
}
// Bypass initial page load requests (i.e. static assets).
// The absence of the immediate/parent client in the map of the active clients
// means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet
// and is not ready to handle requests.
if (!activeClientIds.has(client.id)) {
return passthrough()
}
// Notify the client that a request has been intercepted.
const serializedRequest = await serializeRequest(event.request)
const clientMessage = await sendToClient(
client,
{
type: 'REQUEST',
payload: {
id: requestId,
interceptedAt: requestInterceptedAt,
...serializedRequest,
},
},
[serializedRequest.body],
)
switch (clientMessage.type) {
case 'MOCK_RESPONSE': {
return respondWithMock(clientMessage.data)
}
case 'PASSTHROUGH': {
return passthrough()
}
}
return passthrough()
}
/**
* @param {Client} client
* @param {any} message
* @param {Array<Transferable>} transferrables
* @returns {Promise<any>}
*/
function sendToClient(client, message, transferrables = []) {
return new Promise((resolve, reject) => {
const channel = new MessageChannel()
channel.port1.onmessage = (event) => {
if (event.data && event.data.error) {
return reject(event.data.error)
}
resolve(event.data)
}
client.postMessage(message, [
channel.port2,
...transferrables.filter(Boolean),
])
})
}
/**
* @param {Response} response
* @returns {Response}
*/
function respondWithMock(response) {
// Setting response status code to 0 is a no-op.
// However, when responding with a "Response.error()", the produced Response
// instance will have status code set to 0. Since it's not possible to create
// a Response instance with status code 0, handle that use-case separately.
if (response.status === 0) {
return Response.error()
}
const mockedResponse = new Response(response.body, response)
Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, {
value: true,
enumerable: true,
})
return mockedResponse
}
/**
* @param {Request} request
*/
async function serializeRequest(request) {
return {
url: request.url,
mode: request.mode,
method: request.method,
headers: Object.fromEntries(request.headers.entries()),
cache: request.cache,
credentials: request.credentials,
destination: request.destination,
integrity: request.integrity,
redirect: request.redirect,
referrer: request.referrer,
referrerPolicy: request.referrerPolicy,
body: await request.arrayBuffer(),
keepalive: request.keepalive,
}
}
@@ -147,11 +147,17 @@ class SvgToPdfTest {
ImageIO.write(red, "png", external.toFile());
try {
String rootRelativeExternal =
"/"
+ external.toAbsolutePath()
.toString()
.replace('\\', '/')
.replaceFirst("^/+", "");
String svg =
"<svg xmlns=\"http://www.w3.org/2000/svg\" "
+ "xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"100\" height=\"100\">"
+ "<image x=\"0\" y=\"0\" width=\"100\" height=\"100\" xlink:href=\""
+ external.toUri()
+ rootRelativeExternal
+ "\"/></svg>";
byte[] pdf;
+17 -18
View File
@@ -11,7 +11,7 @@ spotless {
java {
target 'src/**/java/**/*.java'
targetExclude 'src/main/java/org/apache/**'
googleJavaFormat(googleJavaFormatVersion).aosp().reorderImports(false)
googleJavaFormat(rootProject.ext.googleJavaFormatVersion).aosp().reorderImports(false)
// google-java-format 1.28.0 bundles Guava 32.x which crashes Spotless lint on JDK 24/25
suppressLintsFor { setStep('google-java-format') }
@@ -35,13 +35,13 @@ spotless {
}
dependencies {
implementation project(':common')
api "com.google.guava:guava:${guavaVersion}"
api "com.google.guava:guava:${rootProject.ext.guavaVersion}"
api 'org.springframework:spring-jdbc'
api 'org.springframework:spring-webmvc'
api 'org.springframework.session:spring-session-core'
api "org.springframework.security:spring-security-core:$springSecuritySamlVersion"
api "org.springframework.security:spring-security-saml2-service-provider:$springSecuritySamlVersion"
api "org.springframework.security:spring-security-core:${rootProject.ext.springSecuritySamlVersion}"
api "org.springframework.security:spring-security-saml2-service-provider:${rootProject.ext.springSecuritySamlVersion}"
api 'org.springframework.boot:spring-boot-starter-jetty'
api 'org.springframework.boot:spring-boot-starter-security'
api 'org.springframework.boot:spring-boot-starter-data-jpa'
@@ -55,16 +55,16 @@ dependencies {
api 'com.github.ben-manes.caffeine:caffeine'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
api 'io.swagger.core.v3:swagger-core-jakarta:2.2.46'
implementation "com.bucket4j:bucket4j_jdk17-core:${bucket4jVersion}"
implementation "com.bucket4j:bucket4j_jdk17-core:${rootProject.ext.bucket4jVersion}"
// Lettuce-backed Bucket4j ProxyManager used by ValkeyRateLimitStore for cluster-wide
// token-bucket rate limiting (parity with in-process Bucket4j semantics; no fixed-window
// boundary doubling).
implementation "com.bucket4j:bucket4j_jdk17-lettuce:${bucket4jVersion}"
implementation "com.bucket4j:bucket4j_jdk17-lettuce:${rootProject.ext.bucket4jVersion}"
// https://mvnrepository.com/artifact/com.bucket4j/bucket4j_jdk17
implementation "org.bouncycastle:bcprov-jdk18on:$bouncycastleVersion"
implementation "org.bouncycastle:bcprov-jdk18on:${rootProject.ext.bouncycastleVersion}"
implementation "com.google.code.gson:gson:${gsonVersion}"
implementation "com.google.code.gson:gson:${rootProject.ext.gsonVersion}"
// jinjava/jjwt transitively request older Jackson 2 versions; declare the current
// version directly so it is selected consistently (root build.gradle pins are the fallback).
@@ -82,17 +82,17 @@ dependencies {
api 'io.micrometer:micrometer-registry-prometheus'
api "io.jsonwebtoken:jjwt-api:${jwtVersion}"
runtimeOnly "io.jsonwebtoken:jjwt-impl:${jwtVersion}"
runtimeOnly "io.jsonwebtoken:jjwt-jackson:${jwtVersion}"
api "io.jsonwebtoken:jjwt-api:${rootProject.ext.jwtVersion}"
runtimeOnly "io.jsonwebtoken:jjwt-impl:${rootProject.ext.jwtVersion}"
runtimeOnly "io.jsonwebtoken:jjwt-jackson:${rootProject.ext.jwtVersion}"
runtimeOnly 'com.h2database:h2:2.3.232' // Don't upgrade h2database - file format incompatible with 2.4.x, would break existing user databases
runtimeOnly 'org.postgresql:postgresql:42.7.11'
implementation('com.coveo:saml-client:5.0.0') {
exclude group: 'org.opensaml', module: 'opensaml-core'
}
implementation "software.amazon.awssdk:s3:${awsSdkVersion}"
implementation "software.amazon.awssdk:url-connection-client:${awsSdkVersion}"
implementation "software.amazon.awssdk:s3:${rootProject.ext.awsSdkVersion}"
implementation "software.amazon.awssdk:url-connection-client:${rootProject.ext.awsSdkVersion}"
// @DataJpaTest slice (Boot 4 ships test slices as separate starters, like webmvc-test at the
// root) so policy.source repositories can be exercised against embedded H2.
@@ -100,11 +100,10 @@ dependencies {
// Testcontainers: real MinIO/LocalStack (S3) and Valkey for integration tests in CI without
// manually-started instances. Tests skip cleanly when Docker is unavailable.
testImplementation "org.testcontainers:testcontainers:${testcontainersMinioVersion}"
testImplementation "org.testcontainers:minio:${testcontainersMinioVersion}"
testImplementation "org.testcontainers:localstack:${testcontainersMinioVersion}"
testImplementation "org.testcontainers:postgresql:${testcontainersMinioVersion}"
testImplementation "org.testcontainers:junit-jupiter:${testcontainersMinioVersion}"
testImplementation "org.testcontainers:testcontainers:${rootProject.ext.testcontainersMinioVersion}"
testImplementation "org.testcontainers:minio:${rootProject.ext.testcontainersMinioVersion}"
testImplementation "org.testcontainers:localstack:${rootProject.ext.testcontainersMinioVersion}"
testImplementation "org.testcontainers:junit-jupiter:${rootProject.ext.testcontainersMinioVersion}"
}
tasks.register('prepareKotlinBuildScriptModel') {}
@@ -24,7 +24,7 @@ import stirling.software.common.model.ApplicationProperties;
* the full production bean method {@code valkeyConnectionFactory()} so the parse, credential
* wiring, and eager-handshake all run exactly as at boot.
*/
@Testcontainers
@Testcontainers(disabledWithoutDocker = true)
@EnabledIf("isDockerAvailable")
class LiveValkeyAuthIntegrationTest {
@@ -28,7 +28,7 @@ import stirling.software.common.model.ApplicationProperties;
* to reproduce a partition rather than {@code stop} (which would fail fast with
* connection-refused).
*/
@Testcontainers
@Testcontainers(disabledWithoutDocker = true)
@EnabledIf("isDockerAvailable")
class LiveValkeyChaosTest {
@@ -39,7 +39,7 @@ import stirling.software.common.model.ApplicationProperties;
* unavailable - without that guard, {@code @Testcontainers} would throw {@code initializationError}
* (test FAILURE, not skip) on CI runners without Docker.
*/
@Testcontainers
@Testcontainers(disabledWithoutDocker = true)
@EnabledIf("isDockerAvailable")
class LiveValkeyIntegrationTest {
+6 -3
View File
@@ -235,6 +235,7 @@ subprojects {
resolutionStrategy.force "org.bouncycastle:bcprov-jdk18on:${bouncycastleVersion}"
resolutionStrategy.force "org.bouncycastle:bcpkix-jdk18on:${bouncycastleVersion}"
resolutionStrategy.force "org.bouncycastle:bcutil-jdk18on:${bouncycastleVersion}"
resolutionStrategy.force "org.apache.santuario:xmlsec:3.0.5"
}
dependencyManagement {
@@ -285,6 +286,7 @@ subprojects {
def jacocoReport = tasks.named("jacocoTestReport")
tasks.withType(Test).configureEach {
jvmArgs "--enable-native-access=ALL-UNNAMED"
useJUnitPlatform()
finalizedBy(jacocoReport)
}
@@ -297,7 +299,8 @@ subprojects {
html.required.set(true)
}
doLast {
def xmlReport = reports.xml.outputLocation.get().asFile
def reportBaseDir = layout.buildDirectory.dir("reports/jacoco/test").get().asFile
def xmlReport = new File(reportBaseDir, "jacocoTestReport.xml")
if (!xmlReport.exists()) {
logger.lifecycle("Jacoco coverage report not found at ${xmlReport}")
return
@@ -376,7 +379,7 @@ subprojects {
}
logger.lifecycle(separator)
def htmlReport = reports.html.outputLocation.get().asFile
def htmlReport = new File(reportBaseDir, "html")
logger.lifecycle("Detailed HTML report available at: ${htmlReport}")
if (rows.any { it[3] == "FAIL" }) {
logger.lifecycle("Some coverage targets were missed. Please review the detailed report above.")
@@ -666,7 +669,7 @@ tasks.register('compileRestartHelper', JavaCompile) {
source = fileTree(dir: 'scripts', include: 'RestartHelper.java')
classpath = files()
destinationDirectory = layout.buildDirectory.dir("restart-helper-classes")
def restartMajorVersion = project.ext.modernJavaVersion
def restartMajorVersion = rootProject.ext.modernJavaVersion
def restartCompatibility = JavaVersion.toVersion(restartMajorVersion.toString())
sourceCompatibility = restartCompatibility
targetCompatibility = restartCompatibility
+9
View File
@@ -0,0 +1,9 @@
$ErrorActionPreference = 'Stop'
$output = & "$PSScriptRoot/../gradlew.bat" printVersion --quiet 2>&1
$lines = @($output | Where-Object { $_.ToString().Trim() })
if ($lines.Count -eq 0) {
exit 0
}
Write-Output $lines[-1]