Compare commits

..
23 Commits
Author SHA1 Message Date
Anthony Stirling 7ebe3133d2 test new func with api credits (NOT WORKING) 2025-08-14 14:56:47 +01:00
Anthony Stirling 30937d204e init limits for users and non Authed Users 2025-08-12 09:40:59 +01:00
Anthony Stirling 1a51a43962 conflicts 2025-08-11 22:04:26 +01:00
Anthony Stirling 877caee396 Merge branch 'main' into org 2025-08-09 15:41:28 +01:00
Anthony Stirling 0dd0e0c71e formatting and fixes 2025-08-06 16:35:09 +01:00
Anthony Stirling d1c8802890 cleanup and fixes 2025-08-06 16:21:39 +01:00
a 31c397d016 Merge branch 'org' of git@github.com:Stirling-Tools/Stirling-PDF.git into org 2025-08-06 15:53:55 +01:00
Anthony Stirling e418e06ace preauth 2025-08-06 15:53:39 +01:00
Anthony Stirling 9e78057a3e disable console 2025-08-06 15:06:19 +01:00
Anthony Stirling 4437e1cf30 fix removal for V1 2025-08-06 15:05:15 +01:00
Anthony Stirling 3f81adaf56 Merge branch 'main' into org 2025-08-06 14:57:14 +01:00
Anthony Stirling cac817ed56 remove other roles 2025-08-06 14:43:01 +01:00
Anthony Stirling 3da9ae46b6 Merge branch 'main' into org 2025-08-05 15:47:25 +01:00
Anthony Stirling 771ce3acfe formating and lang 2025-08-01 21:14:12 +01:00
Anthony Stirling a9def611f6 admin permission switch 2025-08-01 20:34:11 +01:00
Anthony Stirling b049638f49 debug TODO: TO REMOVE before merge 2025-08-01 15:19:22 +01:00
Anthony Stirling 5761bfbffa formatting 2025-08-01 14:48:37 +01:00
Anthony Stirling 3c6786f719 logging 2025-08-01 14:48:25 +01:00
a 7af18cb10c Merge branch 'org' of git@github.com:Stirling-Tools/Stirling-PDF.git into org 2025-08-01 14:46:46 +01:00
Anthony Stirling 42f5e68f48 h2 console stuff 2025-08-01 14:46:22 +01:00
Anthony Stirling 92e7538182 Merge branch 'main' into org 2025-07-31 12:50:55 +01:00
Anthony Stirling 5e5f2dda83 org init 2025-07-31 12:16:01 +01:00
Anthony Stirling 0966b919cb remove file locks plus formatting 2025-07-29 19:00:48 +01:00
631 changed files with 15255 additions and 47402 deletions
+3 -6
View File
@@ -11,12 +11,9 @@
"Bash(npm test:*)",
"Bash(ls:*)",
"Bash(npx tsc:*)",
"Bash(node:*)",
"Bash(npm run dev:*)",
"Bash(sed:*)",
"Bash(npm run typecheck:*)"
"Bash(echo)",
"Bash(rm:*)"
],
"deny": [],
"defaultMode": "acceptEdits"
"deny": []
}
}
+1 -1
View File
@@ -24,7 +24,7 @@ indent_size = 2
insert_final_newline = false
trim_trailing_whitespace = false
[{*.js,*.jsx,*.mjs,*.ts,*.tsx}]
[{*.js,*.jsx,*.ts,*.tsx}]
indent_size = 2
[*.css]
+1 -5
View File
@@ -46,9 +46,6 @@ labels:
- label: 'API'
title: '.*openapi.*|.*swagger.*|.*api.*'
- label: 'v2'
base-branch: 'V2'
- label: 'Translation'
files:
- 'app/core/src/main/resources/messages_[a-zA-Z_]{2}_[a-zA-Z_]{2,7}.properties'
@@ -65,7 +62,6 @@ labels:
- 'app/core/src/main/java/stirling/software/SPDF/controller/web/.*'
- 'app/core/src/main/java/stirling/software/SPDF/UI/.*'
- 'app/proprietary/src/main/java/stirling/software/proprietary/security/controller/web/.*'
- 'frontend/**'
- label: 'Java'
files:
@@ -124,7 +120,6 @@ labels:
- 'scripts/installFonts.sh'
- 'test.sh'
- 'test2.sh'
- 'docker/**'
- label: 'Devtools'
files:
@@ -136,6 +131,7 @@ labels:
- '.github/workflows/pre_commit.yml'
- 'devGuide/.*'
- 'devTools/.*'
- 'devTools/.*'
- label: 'Test'
files:
-1
View File
@@ -83,7 +83,6 @@
color: "DEDEDE"
- name: "v2"
color: "FFFF00"
description: "Issues or pull requests related to the v2 branch"
- name: "wontfix"
description: "This will not be worked on"
color: "FFFFFF"
+1 -2
View File
@@ -318,8 +318,7 @@ jobs:
SYSTEM_MAXFILESIZE: "100"
METRICS_ENABLED: "true"
SYSTEM_GOOGLEVISIBILITY: "false"
SWAGGER_SERVER_URL: "https://${V2_PORT}.ssl.stirlingpdf.cloud"
baseUrl: "https://${V2_PORT}.ssl.stirlingpdf.cloud"
SWAGGER_SERVER_URL: "http://${{ secrets.VPS_HOST }}:${V2_PORT}"
restart: on-failure:5
stirling-pdf-v2-frontend:
@@ -47,7 +47,7 @@ jobs:
egress-policy: audit
- name: Checkout PR
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup GitHub App Bot
if: github.actor != 'dependabot[bot]'
@@ -158,7 +158,7 @@ jobs:
egress-policy: audit
- name: Checkout PR
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup GitHub App Bot
if: github.actor != 'dependabot[bot]'
@@ -170,7 +170,7 @@ jobs:
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- name: Checkout PR
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
repository: ${{ needs.check-comment.outputs.pr_repository }}
ref: ${{ needs.check-comment.outputs.pr_ref }}
+1 -1
View File
@@ -26,7 +26,7 @@ jobs:
egress-policy: audit
- name: Checkout PR
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup GitHub App Bot
if: github.actor != 'dependabot[bot]'
+2 -2
View File
@@ -23,7 +23,7 @@ jobs:
with:
egress-policy: audit
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0
@@ -87,7 +87,7 @@ jobs:
- name: AI PR Title Analysis
if: steps.actor.outputs.is_repo_dev == 'true'
id: ai-title-analysis
uses: actions/ai-inference@b81b2afb8390ee6839b494a404766bef6493c7d9 # v1.2.8
uses: actions/ai-inference@0cbed4a10641c75090de5968e66d70eb4660f751 # v1.2.7
with:
model: openai/gpt-4o
system-prompt-file: ".github/config/system-prompt.txt"
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
with:
egress-policy: audit
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup GitHub App Bot
id: setup-bot
+12 -19
View File
@@ -31,7 +31,8 @@ jobs:
project: ${{ steps.changes.outputs.project }}
openapi: ${{ steps.changes.outputs.openapi }}
steps:
- uses: actions/checkout@v4.3.0
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Check for file changes
uses: dorny/paths-filter@v3.0.2
@@ -55,15 +56,14 @@ jobs:
with:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@v4.3.0
uses: actions/checkout@v4.2.2
- name: Set up JDK ${{ matrix.jdk-version }}
uses: actions/setup-java@v4.7.1
with:
java-version: ${{ matrix.jdk-version }}
distribution: "temurin"
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4.4.2
uses: gradle/actions/setup-gradle@v4.4.1
with:
gradle-version: 8.14
- name: Build with Gradle and spring security ${{ matrix.spring-security }}
@@ -106,24 +106,19 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@v2.13.0
uses: step-security/harden-runner@v2.12.2
with:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@v4.3.0
- uses: actions/checkout@v4.2.2
- name: Set up JDK 17
uses: actions/setup-java@v4.7.1
with:
java-version: "17"
distribution: "temurin"
- uses: gradle/actions/setup-gradle@v4.4.2
- uses: gradle/actions/setup-gradle@v4.4.1
- name: Generate OpenAPI documentation
run: ./gradlew :stirling-pdf:generateOpenApiDocs
env:
DISABLE_ADDITIONAL_FEATURES: true
- name: Upload OpenAPI Documentation
uses: actions/upload-artifact@v4.6.2
with:
@@ -147,8 +142,6 @@ jobs:
cache-dependency-path: frontend/package-lock.json
- name: Install frontend dependencies
run: cd frontend && npm ci
- name: Lint frontend
run: cd frontend && npm run lint
- name: Build frontend
run: cd frontend && npm run build
- name: Run frontend tests
@@ -170,7 +163,7 @@ jobs:
with:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@v4.3.0
uses: actions/checkout@v4.2.2
- name: Set up JDK 17
uses: actions/setup-java@v4.7.1
with:
@@ -212,7 +205,7 @@ jobs:
egress-policy: audit
- name: Checkout Repository
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set up Java 17
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
@@ -261,7 +254,7 @@ jobs:
egress-policy: audit
- name: Checkout Repository
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set up JDK 17
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
@@ -270,7 +263,7 @@ jobs:
distribution: "temurin"
- name: Set up Gradle
uses: gradle/actions/setup-gradle@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
with:
gradle-version: 8.14
+1 -1
View File
@@ -35,7 +35,7 @@ jobs:
egress-policy: audit
- name: Checkout main branch first
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup GitHub App Bot
id: setup-bot
+1 -1
View File
@@ -22,6 +22,6 @@ jobs:
egress-policy: audit
- name: "Checkout Repository"
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: "Dependency Review"
uses: actions/dependency-review-action@da24556b548a50705dd671f47852072ea4c105d9 # v4.7.1
+1 -2
View File
@@ -151,8 +151,7 @@ jobs:
SYSTEM_MAXFILESIZE: "100"
METRICS_ENABLED: "true"
SYSTEM_GOOGLEVISIBILITY: "false"
SWAGGER_SERVER_URL: "https://demo.stirlingpdf.cloud"
baseUrl: "https://demo.stirlingpdf.cloud"
SWAGGER_SERVER_URL: "http://${{ secrets.VPS_HOST }}:3000"
restart: on-failure:5
frontend:
+2 -2
View File
@@ -36,7 +36,7 @@ jobs:
egress-policy: audit
- name: Check out code
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0
@@ -54,7 +54,7 @@ jobs:
distribution: "temurin"
- name: Setup Gradle
uses: gradle/actions/setup-gradle@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
- name: Check licenses for compatibility
run: ./gradlew clean checkLicense
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
egress-policy: audit
- name: Check out the repository
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Run Labeler
uses: crazy-max/ghaction-github-labeler@24d110aa46a59976b8a7f35518cb7f14f434c916 # v5.3.0
+8 -8
View File
@@ -25,7 +25,7 @@ jobs:
with:
egress-policy: audit
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set up JDK
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
@@ -64,7 +64,7 @@ jobs:
with:
egress-policy: audit
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set up JDK 21
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
@@ -72,7 +72,7 @@ jobs:
java-version: "21"
distribution: "temurin"
- uses: gradle/actions/setup-gradle@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
- uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
with:
gradle-version: 8.14
@@ -115,7 +115,7 @@ jobs:
egress-policy: audit
- name: Download build artifacts
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: stirling-${{ matrix.file_suffix }}binaries
@@ -152,7 +152,7 @@ jobs:
with:
egress-policy: audit
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set up JDK 21
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
@@ -160,7 +160,7 @@ jobs:
java-version: "21"
distribution: "temurin"
- uses: gradle/actions/setup-gradle@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
- uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
with:
gradle-version: 8.14
@@ -243,7 +243,7 @@ jobs:
egress-policy: audit
- name: Download build artifacts
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: ${{ matrix.platform }}binaries
@@ -306,7 +306,7 @@ jobs:
egress-policy: audit
- name: Download signed artifacts
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
- name: Display structure of downloaded files
run: ls -R
- name: Upload binaries, attestations and signatures to Release and create GitHub Release
+1 -1
View File
@@ -22,7 +22,7 @@ jobs:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0
+2 -2
View File
@@ -34,7 +34,7 @@ jobs:
with:
egress-policy: audit
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set up JDK 17
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
@@ -42,7 +42,7 @@ jobs:
java-version: "17"
distribution: "temurin"
- uses: gradle/actions/setup-gradle@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
- uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
with:
gradle-version: 8.14
+4 -4
View File
@@ -27,7 +27,7 @@ jobs:
with:
egress-policy: audit
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set up JDK 17
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
@@ -35,7 +35,7 @@ jobs:
java-version: "17"
distribution: "temurin"
- uses: gradle/actions/setup-gradle@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
- uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
with:
gradle-version: 8.14
@@ -88,7 +88,7 @@ jobs:
egress-policy: audit
- name: Download build artifacts
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: binaries${{ matrix.file_suffix }}
- name: Display structure of downloaded files
@@ -166,7 +166,7 @@ jobs:
egress-policy: audit
- name: Download signed artifacts
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: signed${{ matrix.file_suffix }}
+2 -2
View File
@@ -39,7 +39,7 @@ jobs:
egress-policy: audit
- name: "Checkout code"
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
@@ -74,6 +74,6 @@ jobs:
# Upload the results to GitHub's code scanning dashboard.
- name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@76621b61decf072c1cee8dd1ce2d2a82d33c17ed # v3.29.5
uses: github/codeql-action/upload-sarif@51f77329afa6477de8c49fc9c7046c15b9a4e79d # v3.29.5
with:
sarif_file: results.sarif
+2 -2
View File
@@ -34,12 +34,12 @@ jobs:
with:
egress-policy: audit
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0
- name: Setup Gradle
uses: gradle/actions/setup-gradle@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
- name: Build and analyze with Gradle
env:
+2 -2
View File
@@ -30,7 +30,7 @@ jobs:
with:
egress-policy: audit
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set up JDK 17
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
@@ -38,7 +38,7 @@ jobs:
java-version: "17"
distribution: "temurin"
- uses: gradle/actions/setup-gradle@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
- uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
- name: Generate Swagger documentation
run: ./gradlew :stirling-pdf:generateOpenApiDocs
+1 -1
View File
@@ -36,7 +36,7 @@ jobs:
with:
egress-policy: audit
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup GitHub App Bot
id: setup-bot
+3 -3
View File
@@ -29,7 +29,7 @@ jobs:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set up JDK
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
@@ -38,7 +38,7 @@ jobs:
distribution: 'temurin'
- name: Setup Gradle
uses: gradle/actions/setup-gradle@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
with:
gradle-version: 8.14
@@ -126,7 +126,7 @@ jobs:
with:
egress-policy: audit
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set up Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
-1
View File
@@ -19,6 +19,5 @@
"yzhang.markdown-all-in-one", // Markdown All-in-One extension for enhanced Markdown editing
"stylelint.vscode-stylelint", // Stylelint extension for CSS and SCSS linting
"redhat.vscode-yaml", // YAML extension for Visual Studio Code
"dbaeumer.vscode-eslint", // ESLint extension for TypeScript linting
]
}
+1 -4
View File
@@ -139,8 +139,5 @@
"app/core/src/main/java",
"app/common/src/main/java",
"app/proprietary/src/main/java"
],
"[typescript]": {
"editor.defaultFormatter": "vscode.typescript-language-features"
}
]
}
-300
View File
@@ -1,300 +0,0 @@
# Adding New React Tools to Stirling PDF
This guide covers how to add new PDF tools to the React frontend, either by migrating existing Thymeleaf templates or creating entirely new tools.
## Overview
When adding tools, follow this systematic approach using the established patterns and architecture.
## 1. Create Tool Structure
Create these files in the correct directories:
```
frontend/src/hooks/tools/[toolName]/
├── use[ToolName]Parameters.ts # Parameter definitions and validation
└── use[ToolName]Operation.ts # Tool operation logic using useToolOperation
frontend/src/components/tools/[toolName]/
└── [ToolName]Settings.tsx # Settings UI component (if needed)
frontend/src/tools/
└── [ToolName].tsx # Main tool component
```
## 2. Implementation Pattern
Use `useBaseTool` for simplified hook management. This is the recommended approach for all new tools:
**Parameters Hook** (`use[ToolName]Parameters.ts`):
```typescript
import { BaseParameters } from '../../../types/parameters';
import { useBaseParameters, BaseParametersHook } from '../shared/useBaseParameters';
export interface [ToolName]Parameters extends BaseParameters {
// Define your tool-specific parameters here
someOption: boolean;
}
export const defaultParameters: [ToolName]Parameters = {
someOption: false,
};
export const use[ToolName]Parameters = (): BaseParametersHook<[ToolName]Parameters> => {
return useBaseParameters({
defaultParameters,
endpointName: 'your-endpoint-name',
validateFn: (params) => true, // Add validation logic
});
};
```
**Operation Hook** (`use[ToolName]Operation.ts`):
```typescript
import { useTranslation } from 'react-i18next';
import { ToolType, useToolOperation } from '../shared/useToolOperation';
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
export const build[ToolName]FormData = (parameters: [ToolName]Parameters, file: File): FormData => {
const formData = new FormData();
formData.append('fileInput', file);
// Add parameters to formData
return formData;
};
export const [toolName]OperationConfig = {
toolType: ToolType.singleFile, // or ToolType.multiFile (buildFormData's file parameter will need to be updated)
buildFormData: build[ToolName]FormData,
operationType: '[toolName]',
endpoint: '/api/v1/category/endpoint-name',
filePrefix: 'processed_', // Will be overridden with translation
defaultParameters,
} as const;
export const use[ToolName]Operation = () => {
const { t } = useTranslation();
return useToolOperation({
...[toolName]OperationConfig,
filePrefix: t('[toolName].filenamePrefix', 'processed') + '_',
getErrorMessage: createStandardErrorHandler(t('[toolName].error.failed', 'Operation failed'))
});
};
```
**Main Component** (`[ToolName].tsx`):
```typescript
import { useTranslation } from "react-i18next";
import { createToolFlow } from "../components/tools/shared/createToolFlow";
import { use[ToolName]Parameters } from "../hooks/tools/[toolName]/use[ToolName]Parameters";
import { use[ToolName]Operation } from "../hooks/tools/[toolName]/use[ToolName]Operation";
import { useBaseTool } from "../hooks/tools/shared/useBaseTool";
import { BaseToolProps, ToolComponent } from "../types/tool";
const [ToolName] = (props: BaseToolProps) => {
const { t } = useTranslation();
const base = useBaseTool('[toolName]', use[ToolName]Parameters, use[ToolName]Operation, props);
return createToolFlow({
files: {
selectedFiles: base.selectedFiles,
isCollapsed: base.hasResults,
placeholder: t("[toolName].files.placeholder", "Select files to get started"),
},
steps: [
// Add settings steps if needed
],
executeButton: {
text: t("[toolName].submit", "Process"),
isVisible: !base.hasResults,
loadingText: t("loading"),
onClick: base.handleExecute,
disabled: !base.params.validateParameters() || !base.hasFiles || !base.endpointEnabled,
},
review: {
isVisible: base.hasResults,
operation: base.operation,
title: t("[toolName].results.title", "Results"),
onFileClick: base.handleThumbnailClick,
onUndo: base.handleUndo,
},
});
};
[ToolName].tool = () => use[ToolName]Operation;
export default [ToolName] as ToolComponent;
```
**Note**: Some existing tools (like AddPassword, Compress) use a legacy pattern with manual hook management. **Always use the Modern Pattern above for new tools** - it's cleaner, more maintainable, and includes automation support.
## 3. Register Tool in System
Update these files to register your new tool:
**Tool Registry** (`frontend/src/data/useTranslatedToolRegistry.tsx`):
1. Add imports at the top:
```typescript
import [ToolName] from "../tools/[ToolName]";
import { [toolName]OperationConfig } from "../hooks/tools/[toolName]/use[ToolName]Operation";
import [ToolName]Settings from "../components/tools/[toolName]/[ToolName]Settings";
```
2. Add tool entry in the `allTools` object:
```typescript
[toolName]: {
icon: <LocalIcon icon="your-icon-name" width="1.5rem" height="1.5rem" />,
name: t("home.[toolName].title", "Tool Name"),
component: [ToolName],
description: t("home.[toolName].desc", "Tool description"),
categoryId: ToolCategoryId.STANDARD_TOOLS, // or appropriate category
subcategoryId: SubcategoryId.APPROPRIATE_SUBCATEGORY,
maxFiles: -1, // or specific number
endpoints: ["endpoint-name"],
operationConfig: [toolName]OperationConfig,
settingsComponent: [ToolName]Settings, // if settings exist
},
```
## 4. Add Tooltips (Optional but Recommended)
Create user-friendly tooltips to help non-technical users understand your tool. **Use simple, clear language - avoid technical jargon:**
**Tooltip Hook** (`frontend/src/components/tooltips/use[ToolName]Tips.ts`):
```typescript
import { useTranslation } from 'react-i18next';
import { TooltipContent } from '../../types/tips';
export const use[ToolName]Tips = (): TooltipContent => {
const { t } = useTranslation();
return {
header: {
title: t("[toolName].tooltip.header.title", "Tool Overview")
},
tips: [
{
title: t("[toolName].tooltip.description.title", "What does this tool do?"),
description: t("[toolName].tooltip.description.text", "Simple explanation in everyday language that non-technical users can understand."),
bullets: [
t("[toolName].tooltip.description.bullet1", "Easy-to-understand benefit 1"),
t("[toolName].tooltip.description.bullet2", "Easy-to-understand benefit 2")
]
}
// Add more tip sections as needed
]
};
};
```
**Add tooltip to your main component:**
```typescript
import { use[ToolName]Tips } from "../components/tooltips/use[ToolName]Tips";
const [ToolName] = (props: BaseToolProps) => {
const tips = use[ToolName]Tips();
// In your steps array:
steps: [
{
title: t("[toolName].steps.settings", "Settings"),
tooltip: tips, // Add this line
content: <[ToolName]Settings ... />
}
]
```
## 5. Add Translations
Update translation files. **Important: Only update `en-GB` files** - other languages are handled separately.
**File to update:** `frontend/public/locales/en-GB/translation.json`
**Required Translation Keys**:
```json
{
"home": {
"[toolName]": {
"title": "Tool Name",
"desc": "Tool description"
}
},
"[toolName]": {
"title": "Tool Name",
"submit": "Process",
"filenamePrefix": "processed",
"files": {
"placeholder": "Select files to get started"
},
"steps": {
"settings": "Settings"
},
"options": {
"title": "Tool Options",
"someOption": "Option Label",
"someOption.desc": "Option description",
"note": "General information about the tool."
},
"results": {
"title": "Results"
},
"error": {
"failed": "Operation failed"
},
"tooltip": {
"header": {
"title": "Tool Overview"
},
"description": {
"title": "What does this tool do?",
"text": "Simple explanation in everyday language",
"bullet1": "Easy-to-understand benefit 1",
"bullet2": "Easy-to-understand benefit 2"
}
}
}
}
```
**Translation Notes:**
- **Only update `en-GB/translation.json`** - other locale files are managed separately
- Use descriptive keys that match your component's `t()` calls
- Include tooltip translations if you created tooltip hooks
- Add `options.*` keys if your tool has settings with descriptions
**Tooltip Writing Guidelines:**
- **Use simple, everyday language** - avoid technical terms like "converts interactive elements"
- **Focus on benefits** - explain what the user gains, not how it works internally
- **Use concrete examples** - "text boxes become regular text" vs "form fields are flattened"
- **Answer user questions** - "What does this do?", "When should I use this?", "What's this option for?"
- **Keep descriptions concise** - 1-2 sentences maximum per section
- **Use bullet points** for multiple benefits or features
## 6. Migration from Thymeleaf
When migrating existing Thymeleaf templates:
1. **Identify Form Parameters**: Look at the original `<form>` inputs to determine parameter structure
2. **Extract Translation Keys**: Find `#{key.name}` references and add them to JSON translations (For many tools these translations will already exist but some parts will be missing)
3. **Map API Endpoint**: Note the `th:action` URL for the operation hook
4. **Preserve Functionality**: Ensure all original form behaviour is replicated which is applicable to V2 react UI
## 7. Testing Your Tool
- Verify tool appears in UI with correct icon and description
- Test with various file sizes and types
- Confirm translations work
- Check error handling
- Test undo functionality
- Verify results display correctly
## Tool Development Patterns
### Three Tool Patterns:
**Pattern 1: Single-File Tools** (Individual processing)
- Backend processes one file per API call
- Set `multiFileEndpoint: false`
- Examples: Compress, Rotate
**Pattern 2: Multi-File Tools** (Batch processing)
- Backend accepts `MultipartFile[]` arrays in single API call
- Set `multiFileEndpoint: true`
- Examples: Split, Merge, Overlay
**Pattern 3: Complex Tools** (Custom processing)
- Tools with complex routing logic or non-standard processing
- Provide `customProcessor` for full control
- Examples: Convert, OCR
-1
View File
@@ -208,7 +208,6 @@ return useToolOperation({
- **Tool Development**: New tools should follow `useToolOperation` hook pattern (see `useCompressOperation.ts`)
- **Performance Target**: Must handle PDFs up to 100GB+ without browser crashes
- **Preview System**: Tools can preview results without polluting main file context (see Split tool implementation)
- **Adding Tools**: See `ADDING_TOOLS.md` for complete guide to creating new PDF tools
## Communication Style
- Be direct and to the point
@@ -7,7 +7,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
/**
* Shortcut for a POST endpoint that is executed through the Stirling "autojob" framework.
* Shortcut for a POST endpoint that is executed through the Stirling "auto-job" framework.
*
* <p>Behaviour notes:
*
@@ -16,7 +16,7 @@ import org.springframework.web.bind.annotation.RequestMethod;
* multipart/form-data} unless you override {@link #consumes()}.
* <li>When the client supplies {@code ?async=true} the call is handed to {@link
* stirling.software.common.service.JobExecutorService JobExecutorService} where it may be
* queued, retried, tracked and subject to timeouts. For synchronous (default) invocations
* queued, retried, tracked and subject to time-outs. For synchronous (default) invocations
* these advanced options are ignored.
* <li>Progress information (see {@link #trackProgress()}) is stored in {@link
* stirling.software.common.service.TaskManager TaskManager} and can be polled via <code>
@@ -48,8 +48,8 @@ public @interface AutoJobPostMapping {
long timeout() default -1;
/**
* Total number of attempts (initial + retries). Must be at least&nbsp;1. Retries are executed
* with exponential backoff.
* Total number of attempts (initial + retries). Must be at least 1. Retries are executed
* with exponential back-off.
*
* <p>Only honoured when {@code async=true}.
*/
@@ -71,8 +71,9 @@ public @interface AutoJobPostMapping {
boolean queueable() default false;
/**
* Relative resource weight (1100) used by the scheduler to prioritise / throttle jobs. Values
* below 1 are clamped to&nbsp;1, values above 100 to&nbsp;100.
* Credit cost for this endpoint in the API credit system. Also used as relative resource weight
* (1-100) by the scheduler to prioritise / throttle jobs. Values below 1 are clamped to 1,
* values above 100 to 100.
*/
int resourceWeight() default 50;
int resourceWeight() default 1;
}
@@ -8,7 +8,6 @@ import java.util.List;
import java.util.Locale;
import java.util.Properties;
import java.util.function.Predicate;
import java.util.stream.Stream;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
@@ -49,14 +48,6 @@ public class AppConfig {
@Value("${server.port:8080}")
private String serverPort;
@Value("${v2}")
public boolean v2Enabled;
@Bean
public boolean v2Enabled() {
return v2Enabled;
}
/* Commented out Thymeleaf template engine bean - to be removed when frontend migration is complete
@Bean
@ConditionalOnProperty(name = "system.customHTMLFiles", havingValue = "true")
@@ -128,7 +119,7 @@ public class AppConfig {
public boolean rateLimit() {
String rateLimit = System.getProperty("rateLimit");
if (rateLimit == null) rateLimit = System.getenv("rateLimit");
return Boolean.parseBoolean(rateLimit);
return (rateLimit != null) ? Boolean.valueOf(rateLimit) : false;
}
@Bean(name = "RunningInDocker")
@@ -148,8 +139,8 @@ public class AppConfig {
if (!Files.exists(mountInfo)) {
return true;
}
try (Stream<String> lines = Files.lines(mountInfo)) {
return lines.anyMatch(line -> line.contains(" /configs "));
try {
return Files.lines(mountInfo).anyMatch(line -> line.contains(" /configs "));
} catch (IOException e) {
return false;
}
@@ -25,7 +25,6 @@ public class InstallationPathConfig {
private static final String STATIC_PATH;
private static final String TEMPLATES_PATH;
private static final String SIGNATURES_PATH;
private static final String PRIVATE_KEY_PATH;
static {
BASE_PATH = initializeBasePath();
@@ -46,7 +45,6 @@ public class InstallationPathConfig {
STATIC_PATH = CUSTOM_FILES_PATH + "static" + File.separator;
TEMPLATES_PATH = CUSTOM_FILES_PATH + "templates" + File.separator;
SIGNATURES_PATH = CUSTOM_FILES_PATH + "signatures" + File.separator;
PRIVATE_KEY_PATH = CONFIG_PATH + "db" + File.separator + "keys" + File.separator;
}
private static String initializeBasePath() {
@@ -122,8 +120,4 @@ public class InstallationPathConfig {
public static String getSignaturesPath() {
return SIGNATURES_PATH;
}
public static String getPrivateKeyPath() {
return PRIVATE_KEY_PATH;
}
}
@@ -119,7 +119,6 @@ public class ApplicationProperties {
private long loginResetTimeMinutes;
private String loginMethod = "all";
private String customGlobalAPIKey;
private Jwt jwt = new Jwt();
public Boolean isAltLogin() {
return saml2.getEnabled() || oauth2.getEnabled();
@@ -299,15 +298,6 @@ public class ApplicationProperties {
}
}
}
@Data
public static class Jwt {
private boolean enableKeystore = true;
private boolean enableKeyRotation = false;
private boolean enableKeyCleanup = true;
private int keyRetentionDays = 7;
private boolean secureCookie;
}
}
@Data
@@ -372,8 +362,7 @@ public class ApplicationProperties {
public String getBaseTmpDir() {
return baseTmpDir != null && !baseTmpDir.isEmpty()
? baseTmpDir
: java.lang.System.getProperty("java.io.tmpdir").replaceAll("/+$", "")
+ "/stirling-pdf";
: java.lang.System.getProperty("java.io.tmpdir") + "/stirling-pdf";
}
@JsonIgnore
@@ -10,21 +10,25 @@ import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
public enum Role {
// Unlimited access
// System-wide administrator - can manage all organizations
SYSTEM_ADMIN(
"ROLE_SYSTEM_ADMIN",
Integer.MAX_VALUE,
Integer.MAX_VALUE,
"adminUserSettings.systemAdmin"),
// Organization administrator - can manage their organization and all its teams
ORG_ADMIN("ROLE_ORG_ADMIN", Integer.MAX_VALUE, Integer.MAX_VALUE, "adminUserSettings.orgAdmin"),
// Team leader - can manage users in their specific team
TEAM_LEAD("ROLE_TEAM_LEAD", Integer.MAX_VALUE, Integer.MAX_VALUE, "adminUserSettings.teamLead"),
// Legacy admin role - equivalent to SYSTEM_ADMIN for backward compatibility
ADMIN("ROLE_ADMIN", Integer.MAX_VALUE, Integer.MAX_VALUE, "adminUserSettings.admin"),
// Unlimited access
// Regular user with unlimited access within their team/org
USER("ROLE_USER", Integer.MAX_VALUE, Integer.MAX_VALUE, "adminUserSettings.user"),
// 40 API calls Per Day, 40 web calls
LIMITED_API_USER("ROLE_LIMITED_API_USER", 40, 40, "adminUserSettings.apiUser"),
// 20 API calls Per Day, 20 web calls
EXTRA_LIMITED_API_USER("ROLE_EXTRA_LIMITED_API_USER", 20, 20, "adminUserSettings.extraApiUser"),
// 0 API calls per day and 20 web calls
WEB_ONLY_USER("ROLE_WEB_ONLY_USER", 0, 20, "adminUserSettings.webOnlyUser"),
INTERNAL_API_USER(
"STIRLING-PDF-BACKEND-API-USER",
Integer.MAX_VALUE,
@@ -63,4 +67,35 @@ public enum Role {
}
throw new IllegalArgumentException("No Role defined for id: " + roleId);
}
/** Checks if this role can manage users across all organizations */
public boolean isSystemAdmin() {
return this == SYSTEM_ADMIN || this == ADMIN; // ADMIN for backward compatibility
}
/** Checks if this role can manage an organization and all its teams */
public boolean isOrgAdmin() {
return isSystemAdmin() || this == ORG_ADMIN;
}
/** Checks if this role can manage a specific team */
public boolean isTeamLead() {
return isOrgAdmin() || this == TEAM_LEAD;
}
/** Gets the hierarchy level of this role (higher number = more permissions) */
public int getHierarchyLevel() {
return switch (this) {
case SYSTEM_ADMIN, ADMIN -> 4;
case ORG_ADMIN -> 3;
case TEAM_LEAD -> 2;
case USER -> 1;
default -> 0; // Limited users
};
}
/** Checks if this role has higher or equal authority than another role */
public boolean hasAuthorityOver(Role otherRole) {
return this.getHierarchyLevel() >= otherRole.getHierarchyLevel();
}
}
@@ -14,10 +14,8 @@ public class RequestUriUtils {
|| requestURI.startsWith(contextPath + "/images/")
|| requestURI.startsWith(contextPath + "/public/")
|| requestURI.startsWith(contextPath + "/pdfjs/")
|| requestURI.startsWith(contextPath + "/pdfjs-legacy/")
|| requestURI.startsWith(contextPath + "/login")
|| requestURI.startsWith(contextPath + "/error")
|| requestURI.startsWith(contextPath + "/favicon")
|| requestURI.endsWith(".svg")
|| requestURI.endsWith(".png")
|| requestURI.endsWith(".ico")
@@ -45,6 +45,11 @@ public class CleanUrlInterceptor implements HandlerInterceptor {
String queryString = request.getQueryString();
if (queryString != null && !queryString.isEmpty()) {
// Reuse the requestURI variable from above
if (requestURI.contains("/api/")) {
return true;
}
Map<String, String> allowedParameters = new HashMap<>();
// Keep only the allowed parameters
@@ -1,11 +1,12 @@
multipart.enabled=true
logging.level.org.springframework=WARN
logging.level.org.hibernate=WARN
#logging.level.root=DEBUG
logging.level.org.eclipse.jetty=WARN
#logging.level.org.springframework.security.saml2=TRACE
#logging.level.org.springframework.security=DEBUG
#logging.level.org.opensaml=DEBUG
#logging.level.stirling.software.proprietary.security=DEBUG
#logging.level.stirling.software.SPDF.config.security: DEBUG
logging.level.com.zaxxer.hikari=WARN
spring.jpa.open-in-view=false
server.forward-headers-strategy=NATIVE
@@ -34,6 +35,7 @@ spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.h2.console.enabled=false
spring.h2.console.path=/h2-console
spring.jpa.hibernate.ddl-auto=update
# Defer datasource initialization to ensure that the database is fully set up
# before Hibernate attempts to access it. This is particularly useful when
@@ -57,5 +59,11 @@ spring.main.allow-bean-definition-overriding=true
# Set up a consistent temporary directory location
java.io.tmpdir=${stirling.tempfiles.directory:${java.io.tmpdir}/stirling-pdf}
# V2 features
v2=false
# API Credit System Configuration
api.credit-system.enabled=true
api.credit-system.anonymous.enabled=true
api.credit-system.anonymous.monthly-credits=10
api.credit-system.anonymous.abuse-threshold=3
api.credit-system.exclude-settings=true
api.credit-system.exclude-actuator=true
api.credit-system.default-credit-cost=1
@@ -455,9 +455,6 @@ adminUserSettings.confirmChangeUserStatus=هل يجب تعطيل/تمكين ال
adminUserSettings.usernameInfo=يمكن أن يحتوي اسم المستخدم فقط على أحرف وأرقام والرموز الخاصة التالية @._+- أو يجب أن يكون عنوان بريد إلكتروني صالح.
adminUserSettings.role=الدور
adminUserSettings.actions=الإجراءات
adminUserSettings.apiUser=مستخدم API محدود
adminUserSettings.extraApiUser=مستخدم API محدود إضافي
adminUserSettings.webOnlyUser=مستخدم الويب فقط
adminUserSettings.demoUser=مستخدم تجريبي (بدون إعدادات مخصصة)
adminUserSettings.internalApiUser=مستخدم API داخلي
adminUserSettings.forceChange=إجبار المستخدم على تغيير كلمة المرور عند تسجيل الدخول
@@ -908,7 +905,6 @@ login.alreadyLoggedIn=لقد تسجل دخولًا إلى
login.alreadyLoggedIn2=أجهزة أخرى. يرجى تسجيل الخروج من الأجهزة وحاول مرة أخرى.
login.toManySessions=لديك عدة جلسات نشطة
login.logoutMessage=You have been logged out.
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
#auto-redact
autoRedact.title=حجب تلقائي
@@ -455,9 +455,6 @@ adminUserSettings.confirmChangeUserStatus=İstifadəçi aktivləşdirilməli/dea
adminUserSettings.usernameInfo=İstifadəçi adı sadəcə hərflərdən, rəqəmlərdən və @._+- xüsusi simvollarından ibarət ola bilər və ya düzgün email ünvanı olmalıdır.
adminUserSettings.role=Rol
adminUserSettings.actions=Fəaliyyətlər
adminUserSettings.apiUser=Məhdudlaşdırılmış API İstifadəçisi
adminUserSettings.extraApiUser=Əlavə Məhdudlaşdırılmış API İstifadəçisi
adminUserSettings.webOnlyUser=Yalnız Veb İstifadəçisi
adminUserSettings.demoUser=Demo İstifadəçisi (Fərdi parametrlər yoxdur)
adminUserSettings.internalApiUser=Daxili API İstifadəçisi
adminUserSettings.forceChange=İstifadəçini giriş zamanı parolu dəyişməyə məcbur et
@@ -908,7 +905,6 @@ login.alreadyLoggedIn=Siz artıq daxil olmusunuz
login.alreadyLoggedIn2=cihazlar. Zəhmət olmasa, cihazlardan çıxış edin və yenidən cəhd edin.
login.toManySessions=Həddindən artıq aktiv sessiyanız var
login.logoutMessage=You have been logged out.
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
#auto-redact
autoRedact.title=Avtomatik Gizlətmə
@@ -455,9 +455,6 @@ adminUserSettings.confirmChangeUserStatus=Трябва ли потребител
adminUserSettings.usernameInfo=Потребителското име може да съдържа само букви, цифри и следните специални символи @._+- или трябва да е валиден имейл адрес.
adminUserSettings.role=Роля
adminUserSettings.actions=Действия
adminUserSettings.apiUser=Ограничен API потребител
adminUserSettings.extraApiUser=Допълнителен ограничен API потребител
adminUserSettings.webOnlyUser=Само за уеб-потребител
adminUserSettings.demoUser=Демо потребител (без персонализирани настройки)
adminUserSettings.internalApiUser=Вътрешен API потребител
adminUserSettings.forceChange=Принудете потребителя да промени потребителското име/парола при влизане
@@ -908,7 +905,6 @@ login.alreadyLoggedIn=Вече сте влезли в
login.alreadyLoggedIn2=устройства. Моля, излезте от устройствата и опитайте отново.
login.toManySessions=Имате твърде много активни сесии
login.logoutMessage=You have been logged out.
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
#auto-redact
autoRedact.title=Автоматично редактиране
@@ -455,9 +455,6 @@ adminUserSettings.confirmChangeUserStatus=སྤྱོད་མཁན་སྤ
adminUserSettings.usernameInfo=སྤྱོད་མཁན་མིང་ནང་ཡི་གེ་དང་ཨང་ཀི། དམིགས་བསལ་མཚོན་རྟགས་ @._+- ཡང་ན་གློག་འཕྲིན་ཁ་བྱང་ཚད་ལྡན་ཞིག་དགོས།
adminUserSettings.role=འགན་འཁུར།
adminUserSettings.actions=བྱ་སྤྱོད།
adminUserSettings.apiUser=ཚད་བཀག་ཅན་གྱི་ API སྤྱོད་མཁན།
adminUserSettings.extraApiUser=ཚད་བཀག་ཅན་གྱི་ API སྤྱོད་མཁན་འཕར་མ།
adminUserSettings.webOnlyUser=དྲ་ཚིགས་ཁོ་ནའི་སྤྱོད་མཁན།
adminUserSettings.demoUser=བརྟག་དཔྱད་སྤྱོད་མཁན། (རང་སྒྲིག་མེད་པ།)
adminUserSettings.internalApiUser=ནང་ཁུལ་ API སྤྱོད་མཁན།
adminUserSettings.forceChange=ནང་འཛུལ་སྐབས་གསང་ཚིག་བསྒྱུར་དགོས་པ་བཟོ་བ།
@@ -908,7 +905,6 @@ login.alreadyLoggedIn=ཁྱེད་རང་
login.alreadyLoggedIn2=སྒྲིག་ཆས་ནང་ནང་འཛུལ་བྱས་ཟིན། སྒྲིག་ཆས་ནས་ཕྱིར་འཐེན་བྱས་ནས་ཡང་བསྐྱར་ཚོད་ལྟ་བྱེད་རོགས།
login.toManySessions=ཁྱེད་ལ་འཛུལ་ཞུགས་བྱས་པའི་གནས་སྐབས་མང་དྲགས་འདུག
login.logoutMessage=You have been logged out.
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
#auto-redact
autoRedact.title=རང་འགུལ་སྒྲིབ་སྲུང་།
@@ -455,9 +455,6 @@ adminUserSettings.confirmChangeUserStatus=Vols deshabilitar/habilitar aquest usu
adminUserSettings.usernameInfo=El nom d'usuari només pot contenir lletres, números i els següents caràcters especials: @._+- o ha de ser una adreça de correu electrònic vàlida.
adminUserSettings.role=Rol
adminUserSettings.actions=Accions
adminUserSettings.apiUser=Usuari amb API limitada
adminUserSettings.extraApiUser=Usuari Addicional amb API limitada
adminUserSettings.webOnlyUser=Usuari només WEB
adminUserSettings.demoUser=Usuari de Demo (Sense configuracions personalitzades)
adminUserSettings.internalApiUser=Usuari d'API Interna
adminUserSettings.forceChange=Força l'usuari a canviar la contrasenya en iniciar sessió
@@ -908,7 +905,6 @@ login.alreadyLoggedIn=Ja has iniciat sessió a
login.alreadyLoggedIn2=dispositius. Si us plau, tanca la sessió en els dispositius i torna-ho a intentar.
login.toManySessions=Tens massa sessions actives
login.logoutMessage=You have been logged out.
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
#auto-redact
autoRedact.title=Redacció Automàtica
@@ -455,9 +455,6 @@ adminUserSettings.confirmChangeUserStatus=Má být uživatel deaktivován/aktivo
adminUserSettings.usernameInfo=Uživatelské jméno může obsahovat pouze písmena, číslice a následující speciální znaky @._+- nebo musí být platná e-mailová adresa.
adminUserSettings.role=Role
adminUserSettings.actions=Akce
adminUserSettings.apiUser=Omezený API uživatel
adminUserSettings.extraApiUser=Další omezený API uživatel
adminUserSettings.webOnlyUser=Pouze webový uživatel
adminUserSettings.demoUser=Demo uživatel (Bez vlastních nastavení)
adminUserSettings.internalApiUser=Interní API uživatel
adminUserSettings.forceChange=Vynutit změnu hesla při přihlášení
@@ -908,7 +905,6 @@ login.alreadyLoggedIn=Již jste přihlášeni na
login.alreadyLoggedIn2=zařízeních. Odhlaste se prosím z těchto zařízení a zkuste to znovu.
login.toManySessions=Máte příliš mnoho aktivních relací
login.logoutMessage=You have been logged out.
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
#auto-redact
autoRedact.title=Automatické začernění
@@ -455,9 +455,6 @@ adminUserSettings.confirmChangeUserStatus=Skal brugeren deaktiveres/aktiveres?
adminUserSettings.usernameInfo=Brugernavn må kun indeholde bogstaver, tal og følgende specialtegn @._+- eller skal være en gyldig e-mailadresse.
adminUserSettings.role=Rolle
adminUserSettings.actions=Handlinger
adminUserSettings.apiUser=Begrænset API-bruger
adminUserSettings.extraApiUser=Yderligere Begrænset API-bruger
adminUserSettings.webOnlyUser=Kun Web-bruger
adminUserSettings.demoUser=Demo-bruger (Ingen brugerdefinerede indstillinger)
adminUserSettings.internalApiUser=Intern API-bruger
adminUserSettings.forceChange=Tving bruger til at ændre adgangskode ved login
@@ -908,7 +905,6 @@ login.alreadyLoggedIn=Du er allerede logget ind på
login.alreadyLoggedIn2=enheder. Log ud af disse enheder og prøv igen.
login.toManySessions=Du har for mange aktive sessoner
login.logoutMessage=You have been logged out.
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
#auto-redact
autoRedact.title=Auto Rediger
@@ -455,9 +455,6 @@ adminUserSettings.confirmChangeUserStatus=Soll der Benutzer deaktiviert/aktivier
adminUserSettings.usernameInfo=Der Benutzername darf nur Buchstaben, Zahlen und die folgenden Sonderzeichen @._+- enthalten oder muss eine gültige E-Mail-Adresse sein.
adminUserSettings.role=Rolle
adminUserSettings.actions=Aktionen
adminUserSettings.apiUser=Eingeschränkter API-Benutzer
adminUserSettings.extraApiUser=Zusätzlicher eingeschränkter API-Benutzer
adminUserSettings.webOnlyUser=Nur Web-Benutzer
adminUserSettings.demoUser=Demo-Benutzer (Keine benutzerdefinierten Einstellungen)
adminUserSettings.internalApiUser=Interner API-Benutzer
adminUserSettings.forceChange=Benutzer dazu zwingen, Benutzernamen/Passwort bei der Anmeldung zu ändern
@@ -908,7 +905,6 @@ login.alreadyLoggedIn=Sie sind bereits an
login.alreadyLoggedIn2=Geräten angemeldet. Bitte melden Sie sich dort ab und versuchen es dann erneut.
login.toManySessions=Sie haben zu viele aktive Sitzungen
login.logoutMessage=Sie wurden erfolgreich abgemeldet.
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
#auto-redact
autoRedact.title=Automatisch zensieren/schwärzen
@@ -455,9 +455,6 @@ adminUserSettings.confirmChangeUserStatus=Θέλετε να απενεργοπο
adminUserSettings.usernameInfo=Το όνομα χρήστη μπορεί να περιέχει μόνο γράμματα, αριθμούς και τους ειδικούς χαρακτήρες @._+- ή πρέπει να είναι έγκυρη διεύθυνση email.
adminUserSettings.role=Ρόλος
adminUserSettings.actions=Ενέργειες
adminUserSettings.apiUser=Περιορισμένος χρήστης API
adminUserSettings.extraApiUser=Επιπλέον περιορισμένος χρήστης API
adminUserSettings.webOnlyUser=Χρήστης μόνο web
adminUserSettings.demoUser=Δοκιμαστικός χρήστης (Χωρίς προσαρμοσμένες ρυθμίσεις)
adminUserSettings.internalApiUser=Εσωτερικός χρήστης API
adminUserSettings.forceChange=Υποχρεωτική αλλαγή κωδικού κατά τη σύνδεση
@@ -908,7 +905,6 @@ login.alreadyLoggedIn=Είστε ήδη συνδεδεμένοι σε
login.alreadyLoggedIn2=συσκευές. Παρακαλώ αποσυνδεθείτε από τις συσκευές και προσπαθήστε ξανά.
login.toManySessions=Έχετε πάρα πολλές ενεργές συνεδρίες
login.logoutMessage=You have been logged out.
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
#auto-redact
autoRedact.title=Αυτόματη απόκρυψη
@@ -446,6 +446,9 @@ account.adminNotif=You have admin privileges. Access system settings and user ma
adminUserSettings.title=User Control Settings
adminUserSettings.header=Admin User Control Settings
adminUserSettings.systemAdmin=System Administrator
adminUserSettings.orgAdmin=Organisation Administrator
adminUserSettings.teamLead=Team Leader
adminUserSettings.admin=Admin
adminUserSettings.user=User
adminUserSettings.addUser=Add New User
@@ -455,9 +458,6 @@ adminUserSettings.confirmChangeUserStatus=Should the user be disabled/enabled?
adminUserSettings.usernameInfo=Username can only contain letters, numbers and the following special characters @._+- or must be a valid email address.
adminUserSettings.role=Role
adminUserSettings.actions=Actions
adminUserSettings.apiUser=Limited API User
adminUserSettings.extraApiUser=Additional Limited API User
adminUserSettings.webOnlyUser=Web Only User
adminUserSettings.demoUser=Demo User (No custom settings)
adminUserSettings.internalApiUser=Internal API User
adminUserSettings.forceChange=Force user to change password on login
@@ -893,7 +893,7 @@ login.rememberme=Remember me
login.invalid=Invalid username or password.
login.locked=Your account has been locked.
login.signinTitle=Please sign in
login.ssoSignIn=Login via Single Sign-On
login.ssoSignIn=Login via Single Sign-on
login.oAuth2AutoCreateDisabled=OAUTH2 Auto-Create User Disabled
login.oAuth2AdminBlockedUser=Registration or logging in of non-registered users is currently blocked. Please contact the administrator.
login.oauth2RequestNotFound=Authorization request not found
@@ -908,7 +908,6 @@ login.alreadyLoggedIn=You are already logged in to
login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
login.toManySessions=You have too many active sessions
login.logoutMessage=You have been logged out.
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
#auto-redact
autoRedact.title=Auto Redact
@@ -455,9 +455,6 @@ adminUserSettings.confirmChangeUserStatus=Should the user be disabled/enabled?
adminUserSettings.usernameInfo=Username can only contain letters, numbers and the following special characters @._+- or must be a valid email address.
adminUserSettings.role=Role
adminUserSettings.actions=Actions
adminUserSettings.apiUser=Limited API User
adminUserSettings.extraApiUser=Additional Limited API User
adminUserSettings.webOnlyUser=Web Only User
adminUserSettings.demoUser=Demo User (No custom settings)
adminUserSettings.internalApiUser=Internal API User
adminUserSettings.forceChange=Force user to change password on login
@@ -908,7 +905,6 @@ login.alreadyLoggedIn=You are already logged in to
login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
login.toManySessions=You have too many active sessions
login.logoutMessage=You have been logged out.
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
#auto-redact
autoRedact.title=Auto Redact
@@ -455,9 +455,6 @@ adminUserSettings.confirmChangeUserStatus=¿Se debe habilitar/deshabilitar el us
adminUserSettings.usernameInfo=El nombre de usuario solo puede contener letras, números y los siguientes caracteres especiales @._+- o debe ser una dirección de correo electrónico válida.
adminUserSettings.role=Rol
adminUserSettings.actions=Acciones
adminUserSettings.apiUser=Usuario limitado de API
adminUserSettings.extraApiUser=Otro usuario limitado de API
adminUserSettings.webOnlyUser=Usuario solo web
adminUserSettings.demoUser=Usuario Demo (Sin ajustes personalizados)
adminUserSettings.internalApiUser=Usuario interno de API
adminUserSettings.forceChange=Forzar usuario a cambiar usuario/contraseña en el acceso
@@ -908,7 +905,6 @@ login.alreadyLoggedIn=Ya ha iniciado sesión en
login.alreadyLoggedIn2=dispositivos. Cierre sesión en los dispositivos y vuelva a intentarlo.
login.toManySessions=Tiene demasiadas sesiones activas
login.logoutMessage=You have been logged out.
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
#auto-redact
autoRedact.title=Auto Censurar Texto
@@ -455,9 +455,6 @@ adminUserSettings.confirmChangeUserStatus=Should the user be disabled/enabled?
adminUserSettings.usernameInfo=Username can only contain letters, numbers and the following special characters @._+- or must be a valid email address.
adminUserSettings.role=Rol
adminUserSettings.actions=Ekintzak
adminUserSettings.apiUser=APIren erabiltzaile mugatua
adminUserSettings.extraApiUser=Additional Limited API User
adminUserSettings.webOnlyUser=Web-erabiltzailea bakarrik
adminUserSettings.demoUser=Demo User (No custom settings)
adminUserSettings.internalApiUser=Internal API User
adminUserSettings.forceChange=Force user to change password on login
@@ -908,7 +905,6 @@ login.alreadyLoggedIn=You are already logged in to
login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
login.toManySessions=You have too many active sessions
login.logoutMessage=You have been logged out.
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
#auto-redact
autoRedact.title=Auto Idatzi
@@ -455,9 +455,6 @@ adminUserSettings.confirmChangeUserStatus=آیا باید وضعیت کاربر
adminUserSettings.usernameInfo=نام کاربری فقط می‌تواند شامل حروف، اعداد و کاراکترهای خاص @._+- باشد یا باید یک آدرس ایمیل معتبر باشد.
adminUserSettings.role=نقش
adminUserSettings.actions=اقدامات
adminUserSettings.apiUser=کاربر محدود API
adminUserSettings.extraApiUser=کاربر محدود اضافی API
adminUserSettings.webOnlyUser=فقط کاربر وب
adminUserSettings.demoUser=کاربر دمو (بدون تنظیمات سفارشی)
adminUserSettings.internalApiUser=کاربر داخلی API
adminUserSettings.forceChange=مجبور کردن کاربر به تغییر رمز عبور هنگام ورود
@@ -908,7 +905,6 @@ login.alreadyLoggedIn=شما قبلاً وارد شده‌اید در
login.alreadyLoggedIn2=دستگاه‌ها. لطفاً از دستگاه‌ها خارج شده و دوباره تلاش کنید.
login.toManySessions=شما تعداد زیادی نشست فعال دارید.
login.logoutMessage=You have been logged out.
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
#auto-redact
autoRedact.title=سانسور خودکار
@@ -455,9 +455,6 @@ adminUserSettings.confirmChangeUserStatus=Voulez vous vraiment déactiver/réact
adminUserSettings.usernameInfo=Le nom d'utilisateur ne peut contenir que des lettres, des chiffres et les caractères spéciaux suivants @._+- ou doit être une adresse e-mail valide.
adminUserSettings.role=Rôle
adminUserSettings.actions=Actions
adminUserSettings.apiUser=Utilisateur API limité
adminUserSettings.extraApiUser=Utilisateur limité supplémentaire de l'API
adminUserSettings.webOnlyUser=Utilisateur Web uniquement
adminUserSettings.demoUser=Demo User (Paramètres par défaut)
adminUserSettings.internalApiUser=Utilisateur de l'API interne
adminUserSettings.forceChange=Forcer l'utilisateur à changer son nom d'utilisateur/mot de passe lors de la connexion
@@ -908,7 +905,6 @@ login.alreadyLoggedIn=Vous êtes déjà connecté sur
login.alreadyLoggedIn2=appareils. Veuillez vous déconnecter des appareils et réessayer.
login.toManySessions=Vous avez trop de sessions actives.
login.logoutMessage=Vous avez été déconnecté.
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
#auto-redact
autoRedact.title=Caviarder automatiquement
@@ -455,9 +455,6 @@ adminUserSettings.confirmChangeUserStatus=Ar cheart an t-úsáideoir a dhíchuma
adminUserSettings.usernameInfo=Ní féidir ach litreacha, uimhreacha agus na carachtair speisialta seo a leanas @._+- a bheith san ainm úsáideora nó ní mór gur seoladh ríomhphoist bailí é.
adminUserSettings.role=Ról
adminUserSettings.actions=Gníomhartha
adminUserSettings.apiUser=Úsáideoir API Teoranta
adminUserSettings.extraApiUser=Úsáideoir API Teoranta breise
adminUserSettings.webOnlyUser=Úsáideoir Gréasáin Amháin
adminUserSettings.demoUser=Úsáideoir Taispeána (Gan socruithe saincheaptha)
adminUserSettings.internalApiUser=Úsáideoir API Inmheánach
adminUserSettings.forceChange=Cuir iallach ar an úsáideoir pasfhocal a athrú ar logáil isteach
@@ -908,7 +905,6 @@ login.alreadyLoggedIn=Tá tú logáilte isteach cheana
login.alreadyLoggedIn2=gléasanna. Logáil amach as na gléasanna agus bain triail eile as.
login.toManySessions=Tá an iomarca seisiún gníomhach agat
login.logoutMessage=You have been logged out.
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
#auto-redact
autoRedact.title=Auto Redact
@@ -455,9 +455,6 @@ adminUserSettings.confirmChangeUserStatus=क्या उपयोगकर्
adminUserSettings.usernameInfo=उपयोगकर्ता नाम में केवल अक्षर, संख्याएं और निम्नलिखित विशेष वर्ण @._+- हो सकते हैं या एक वैध ईमेल पता होना चाहिए।
adminUserSettings.role=भूमिका
adminUserSettings.actions=कार्रवाइयां
adminUserSettings.apiUser=सीमित API उपयोगकर्ता
adminUserSettings.extraApiUser=अतिरिक्त सीमित API उपयोगकर्ता
adminUserSettings.webOnlyUser=केवल वेब उपयोगकर्ता
adminUserSettings.demoUser=डेमो उपयोगकर्ता (कोई कस्टम सेटिंग्स नहीं)
adminUserSettings.internalApiUser=आंतरिक API उपयोगकर्ता
adminUserSettings.forceChange=लॉगिन पर उपयोगकर्ता को पासवर्ड बदलने के लिए मजबूर करें
@@ -908,7 +905,6 @@ login.alreadyLoggedIn=आप पहले से ही
login.alreadyLoggedIn2=उपकरणों में लॉग इन हैं। कृपया उपकरणों से लॉग आउट करें और पुनः प्रयास करें।
login.toManySessions=आपके बहुत सारे सक्रिय सत्र हैं
login.logoutMessage=You have been logged out.
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
#auto-redact
autoRedact.title=स्वतः गोपनीयकरण
@@ -455,9 +455,6 @@ adminUserSettings.confirmChangeUserStatus=Treba li isključiti/uključiti ovog k
adminUserSettings.usernameInfo=Korisničko ime može sadržavati samo slova, brojke i sljedeće posebne znakove @._+- ili mora biti važeća adresa e-pošte.
adminUserSettings.role=Uloga
adminUserSettings.actions=Akcije
adminUserSettings.apiUser=Korisnik s ograničenim API pristupom
adminUserSettings.extraApiUser=Dodatni korisnik s ograničenim API pristupom
adminUserSettings.webOnlyUser=Web Korisnik
adminUserSettings.demoUser=Demo korisnik (Bez prilagođenih Postavki)
adminUserSettings.internalApiUser=Interni API Korisnik
adminUserSettings.forceChange=Prisiliti korisnika da promijeni lozinku prilikom prijave
@@ -908,7 +905,6 @@ login.alreadyLoggedIn=Već ste se prijavili na
login.alreadyLoggedIn2=ure. Odjavite se s ure i pokušajte ponovo.
login.toManySessions=Imate preko mrežne sesije aktivnih
login.logoutMessage=You have been logged out.
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
#auto-redact
autoRedact.title=Automatsko uređivanje
@@ -455,9 +455,6 @@ adminUserSettings.confirmChangeUserStatus=Biztosan módosítja a felhasználó
adminUserSettings.usernameInfo=A felhasználónév csak betűket, számokat és a következő speciális karaktereket tartalmazhatja: @._+- vagy érvényes e-mail címnek kell lennie.
adminUserSettings.role=Szerepkör
adminUserSettings.actions=Műveletek
adminUserSettings.apiUser=Korlátozott API felhasználó
adminUserSettings.extraApiUser=További korlátozott API felhasználó
adminUserSettings.webOnlyUser=Csak webes felhasználó
adminUserSettings.demoUser=Demo felhasználó (egyedi beállítások nélkül)
adminUserSettings.internalApiUser=Belső API felhasználó
adminUserSettings.forceChange=Jelszóváltoztatás kikényszerítése bejelentkezéskor
@@ -908,7 +905,6 @@ login.alreadyLoggedIn=Már be van jelentkezve
login.alreadyLoggedIn2=eszközön. Kérjük, jelentkezzen ki az eszközökről és próbálja újra.
login.toManySessions=Túl sok aktív munkamenet
login.logoutMessage=Sikeresen kijelentkezett.
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
#auto-redact
autoRedact.title=Automatikus kitakarás
@@ -455,9 +455,6 @@ adminUserSettings.confirmChangeUserStatus=Haruskah pengguna dinonaktifkan/diakti
adminUserSettings.usernameInfo=Nama pengguna hanya boleh mengandung huruf, angka, dan karakter khusus berikut @._+- atau harus berupa alamat email yang valid.
adminUserSettings.role=Peran
adminUserSettings.actions=Tindakan
adminUserSettings.apiUser=Pengguna API Terbatas
adminUserSettings.extraApiUser=Pengguna API Terbatas Tambahan
adminUserSettings.webOnlyUser=Pengguna Khusus Web
adminUserSettings.demoUser=Pengguna Demo (Tanpa pengaturan kustom)
adminUserSettings.internalApiUser=Pengguna API Internal
adminUserSettings.forceChange=Memaksa pengguna untuk mengubah nama pengguna/kata sandi saat masuk
@@ -908,7 +905,6 @@ login.alreadyLoggedIn=Anda sudah login ke
login.alreadyLoggedIn2=perangkat. Silakan keluar dari perangkat dan coba lagi.
login.toManySessions=Anda memiliki terlalu banyak sesi aktif
login.logoutMessage=You have been logged out.
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
#auto-redact
autoRedact.title=Redaksional Otomatis
@@ -455,9 +455,6 @@ adminUserSettings.confirmChangeUserStatus=L'utente dovrebbe essere disabilitato/
adminUserSettings.usernameInfo=Il nome utente può contenere solo lettere, numeri e i seguenti caratteri speciali @._+- oppure deve essere un indirizzo email valido.
adminUserSettings.role=Ruolo
adminUserSettings.actions=Azioni
adminUserSettings.apiUser=Utente API limitato
adminUserSettings.extraApiUser=API utente limitato aggiuntivo
adminUserSettings.webOnlyUser=Utente solo Web
adminUserSettings.demoUser=Utente demo (nessuna impostazione personalizzata)
adminUserSettings.internalApiUser=API utente interna
adminUserSettings.forceChange=Forza l'utente a cambiare nome utente/password all'accesso
@@ -908,7 +905,6 @@ login.alreadyLoggedIn=Hai già effettuato l'accesso a
login.alreadyLoggedIn2=dispositivi. Esci dai dispositivi e riprova.
login.toManySessions=Hai troppe sessioni attive
login.logoutMessage=Sei stato disconnesso.
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
#auto-redact
autoRedact.title=Redazione automatica
@@ -1774,7 +1770,7 @@ audit.dashboard.filter.userPlaceholder=Filtra per utente
audit.dashboard.filter.startDate=Data di inizio
audit.dashboard.filter.endDate=Data di fine
audit.dashboard.filter.apply=Applica filtri
audit.dashboard.filter.reset=Resetta filtri
audit.dashboard.filter.reset=Resetta Filtri
# Table Headers
audit.dashboard.table.id=ID
@@ -1864,7 +1860,7 @@ scannerEffect.submit=Crea una falsa scansione
#home.scannerEffect
home.scannerEffect.title=Falsa scansione
home.scannerEffect.desc=Crea un PDF che sembra scansionato
scannerEffect.tags=scansiona,simula,realistico,converti
scannerEffect.tags=scansiona, simula, realistico, converti
# ScannerEffect advanced settings (frontend)
scannerEffect.advancedSettings=Abilita impostazioni di scansione avanzate
@@ -1886,7 +1882,7 @@ scannerEffect.resolution=Risoluzione (DPI)
home.editTableOfContents.title=Modifica indice
home.editTableOfContents.desc=Aggiungi o modifica segnalibri e sommario nei documenti PDF
editTableOfContents.tags=segnalibri,indice,navigazione,indice analitico,sommario,capitoli,sezioni,struttura
editTableOfContents.tags=segnalibri, indice, navigazione, indice analitico, sommario, capitoli, sezioni, struttura
editTableOfContents.title=Modifica indice
editTableOfContents.header=Aggiungi o modifica sommario PDF
editTableOfContents.replaceExisting=Sostituisci i segnalibri esistenti (deseleziona per aggiungerli a quelli esistenti)
@@ -455,9 +455,6 @@ adminUserSettings.confirmChangeUserStatus=ユーザーを無効/有効にする
adminUserSettings.usernameInfo=ユーザー名には、文字、数字、および次の特殊文字 @._+- のみを含めることができます。または、有効な電子メール アドレスである必要があります。
adminUserSettings.role=ロール
adminUserSettings.actions=アクション
adminUserSettings.apiUser=限定されたAPIユーザー
adminUserSettings.extraApiUser=追加の制限付きAPIユーザー
adminUserSettings.webOnlyUser=ウェブ専用ユーザー
adminUserSettings.demoUser=デモユーザー (カスタム設定なし)
adminUserSettings.internalApiUser=内部APIユーザー
adminUserSettings.forceChange=ログイン時にユーザー名/パスワードを強制的に変更する
@@ -908,7 +905,6 @@ login.alreadyLoggedIn=すでにログインしています
login.alreadyLoggedIn2=デバイスからログアウトしてもう一度お試しください。
login.toManySessions=アクティブなセッションが多すぎます
login.logoutMessage=ログアウトしました
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
#auto-redact
autoRedact.title=自動墨消し
@@ -455,9 +455,6 @@ adminUserSettings.confirmChangeUserStatus=사용자를 비활성화/활성화하
adminUserSettings.usernameInfo=사용자 이름은 문자, 숫자 및 @._+- 특수문자만 포함하거나 유효한 이메일 주소여야 합니다.
adminUserSettings.role=역할
adminUserSettings.actions=작업
adminUserSettings.apiUser=제한된 API 사용자
adminUserSettings.extraApiUser=추가 제한된 API 사용자
adminUserSettings.webOnlyUser=웹 전용 사용자
adminUserSettings.demoUser=데모 사용자 (사용자 지정 설정 없음)
adminUserSettings.internalApiUser=내부 API 사용자
adminUserSettings.forceChange=로그인 시 사용자 비밀번호 변경 강제
@@ -908,7 +905,6 @@ login.alreadyLoggedIn=이미 다음에 로그인되어 있습니다
login.alreadyLoggedIn2=개의 기기. 해당 기기에서 로그아웃한 후 다시 시도하세요.
login.toManySessions=활성 세션이 너무 많습니다
login.logoutMessage=You have been logged out.
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
#auto-redact
autoRedact.title=자동 검열
@@ -455,9 +455,6 @@ adminUserSettings.confirmChangeUserStatus=ഉപയോക്താവിനെ
adminUserSettings.usernameInfo=ഉപയോക്തൃനാമത്തിൽ അക്ഷരങ്ങൾ, അക്കങ്ങൾ, താഴെ പറയുന്ന പ്രത്യേക പ്രതീകങ്ങൾ @._+- എന്നിവ മാത്രമേ ഉണ്ടാകാവൂ അല്ലെങ്കിൽ സാധുവായ ഒരു ഇമെയിൽ വിലാസം ആയിരിക്കണം.
adminUserSettings.role=റോൾ
adminUserSettings.actions=പ്രവർത്തനങ്ങൾ
adminUserSettings.apiUser=പരിമിതമായ API ഉപയോക്താവ്
adminUserSettings.extraApiUser=അധിക പരിമിതമായ API ഉപയോക്താവ്
adminUserSettings.webOnlyUser=വെബ് മാത്രം ഉപയോക്താവ്
adminUserSettings.demoUser=ഡെമോ ഉപയോക്താവ് (ഇഷ്ടാനുസൃത ക്രമീകരണങ്ങളില്ല)
adminUserSettings.internalApiUser=ആന്തരിക API ഉപയോക്താവ്
adminUserSettings.forceChange=ലോഗിൻ ചെയ്യുമ്പോൾ പാസ്വേഡ് മാറ്റാൻ ഉപയോക്താവിനെ നിർബന്ധിക്കുക
@@ -908,7 +905,6 @@ login.alreadyLoggedIn=നിങ്ങൾ ഇതിനകം ലോഗിൻ ച
login.alreadyLoggedIn2=ഉപകരണങ്ങളിൽ. ദയവായി ഉപകരണങ്ങളിൽ നിന്ന് ലോഗ് ഔട്ട് ചെയ്ത് വീണ്ടും ശ്രമിക്കുക.
login.toManySessions=നിങ്ങൾക്ക് വളരെയധികം സജീവ സെഷനുകൾ ഉണ്ട്
login.logoutMessage=നിങ്ങൾ ലോഗ് ഔട്ട് ചെയ്തു.
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
#auto-redact
autoRedact.title=സ്വയം റെഡാക്റ്റ് ചെയ്യുക
@@ -455,9 +455,6 @@ adminUserSettings.confirmChangeUserStatus=Should the user be disabled/enabled?
adminUserSettings.usernameInfo=Gebruikersnaam kan alleen letters, nummers en de volgende speciale tekens @._+- bevatten of moet een geldig emailadres zijn.
adminUserSettings.role=Rol
adminUserSettings.actions=Acties
adminUserSettings.apiUser=Beperkte API gebruiker
adminUserSettings.extraApiUser=Extra beperkte API gebruiker
adminUserSettings.webOnlyUser=Alleen web gebruiker
adminUserSettings.demoUser=Demogebruiker (geen aangepaste instellingen)
adminUserSettings.internalApiUser=Interne API gebruiker
adminUserSettings.forceChange=Forceer gebruiker om gebruikersnaam/wachtwoord te wijzigen bij inloggen
@@ -908,7 +905,6 @@ login.alreadyLoggedIn=U zit reeds ingelogd bij
login.alreadyLoggedIn2=apparaten. U moet u a.u.b. uitloggen van de apparaten en opnieuw proberen.
login.toManySessions=U heeft te veel actieve sessies
login.logoutMessage=You have been logged out.
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
#auto-redact
autoRedact.title=Automatisch censureren
@@ -455,9 +455,6 @@ adminUserSettings.confirmChangeUserStatus=Skal brukeren deaktiveres/aktiveres?
adminUserSettings.usernameInfo=Brukernavn kan bare inneholde bokstaver, tall og følgende spesialtegn @._+- eller må være en gyldig e-postadresse.
adminUserSettings.role=Rolle
adminUserSettings.actions=Handlinger
adminUserSettings.apiUser=Begrenset API Bruker
adminUserSettings.extraApiUser=Ekstra Begrenset API Bruker
adminUserSettings.webOnlyUser=Kun Web Bruker
adminUserSettings.demoUser=Demo Bruker (Ingen tilpassede innstillinger)
adminUserSettings.internalApiUser=Intern API Bruker
adminUserSettings.forceChange=Tving bruker til å endre passord ved innlogging
@@ -908,7 +905,6 @@ login.alreadyLoggedIn=Du er allerede innlogget på
login.alreadyLoggedIn2=enheter. Logg ut og forsøk igjen
login.toManySessions=Du har for mange aktive økter
login.logoutMessage=You have been logged out.
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
#auto-redact
autoRedact.title=Automatisk Sensurering
@@ -455,9 +455,6 @@ adminUserSettings.confirmChangeUserStatus=Czy użytkownik powinien zostać wył
adminUserSettings.usernameInfo=Niewłaściwa nazwa użytkownika - musi zawierać litery, cyfry i @._+- LUB być adresem email.
adminUserSettings.role=Rola
adminUserSettings.actions=Akcje
adminUserSettings.apiUser=Ograniczony Użytkownik API
adminUserSettings.extraApiUser=Dodatkowy ograniczony Użytkownik API
adminUserSettings.webOnlyUser=Użytkownik tylko WEB
adminUserSettings.demoUser=Użytkownik DEMO
adminUserSettings.internalApiUser=Wewnętrzny użytkownik API
adminUserSettings.forceChange=Wymuś zmianę hasło po zalogowaniu
@@ -908,7 +905,6 @@ login.alreadyLoggedIn=Jesteś już zalogowany na
login.alreadyLoggedIn2=urządzeniach. Wyloguj się z tych urządzeń i spróbuj ponownie.
login.toManySessions=Masz zbyt wiele aktywnych sesji
login.logoutMessage=You have been logged out.
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
#auto-redact
autoRedact.title=Automatyczne zaciemnienie
@@ -455,9 +455,6 @@ adminUserSettings.confirmChangeUserStatus=O usuário deve ser desabilitado/habil
adminUserSettings.usernameInfo=Nome de usuário só pode incluir letras, números e os seguintes caracteres especiais @._+- ou deve ser um e-mail válido.
adminUserSettings.role=Função
adminUserSettings.actions=Ações
adminUserSettings.apiUser=Usuário de API limitado
adminUserSettings.extraApiUser=Usuário de API limitado adicional
adminUserSettings.webOnlyUser=Usuário web apenas
adminUserSettings.demoUser=Usuário demo (Sem configurações personalizadas)
adminUserSettings.internalApiUser=Usuário de API interno
adminUserSettings.forceChange=Forçar usuário a trocar a senha ao iniciar sessão
@@ -908,7 +905,6 @@ login.alreadyLoggedIn=Você já está conectado em
login.alreadyLoggedIn2=aparelhos. Por favor saia dos aparelhos e tente novamente.
login.toManySessions=Você tem muitas sessões ativas
login.logoutMessage=Você foi desconectado.
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
#auto-redact
autoRedact.title=Ocultação de Texto Automática
@@ -455,9 +455,6 @@ adminUserSettings.confirmChangeUserStatus=Deve o utilizador ser desativado/ativa
adminUserSettings.usernameInfo=O nome de utilizador só pode conter letras, números e os seguintes caracteres especiais @._+- ou deve ser um endereço de email válido.
adminUserSettings.role=Função
adminUserSettings.actions=Ações
adminUserSettings.apiUser=Utilizador API Limitado
adminUserSettings.extraApiUser=Utilizador API Limitado Adicional
adminUserSettings.webOnlyUser=Utilizador Apenas Web
adminUserSettings.demoUser=Utilizador Demo (Sem Definições Personalizadas)
adminUserSettings.internalApiUser=Utilizador API Interno
adminUserSettings.forceChange=Forçar utilizador a alterar palavra-passe no login
@@ -908,7 +905,6 @@ login.alreadyLoggedIn=Já tem sessão iniciada em
login.alreadyLoggedIn2=dispositivos. Por favor termine sessão nesses dispositivos e tente novamente.
login.toManySessions=Tem demasiadas sessões ativas
login.logoutMessage=You have been logged out.
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
#auto-redact
autoRedact.title=Redação Automática
@@ -455,9 +455,6 @@ adminUserSettings.confirmChangeUserStatus=Ar trebui dezactivat/activat utilizato
adminUserSettings.usernameInfo=Numele de utilizator poate conține doar litere, numere și următoarele caractere speciale @._+- sau trebuie să fie o adresă de email validă.
adminUserSettings.role=Rol
adminUserSettings.actions=Acțiuni
adminUserSettings.apiUser=Utilizator API Limitat
adminUserSettings.extraApiUser=Utilizator API Limitat Suplimentar
adminUserSettings.webOnlyUser=Utilizator Doar Web
adminUserSettings.demoUser=Utilizator Demo (Fără setări personalizate)
adminUserSettings.internalApiUser=Utilizator API Intern
adminUserSettings.forceChange=Forțează utilizatorul să schimbe parola la conectare
@@ -908,7 +905,6 @@ login.alreadyLoggedIn=You are already logged in to
login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
login.toManySessions=You have too many active sessions
login.logoutMessage=You have been logged out.
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
#auto-redact
autoRedact.title=Redactare Automată
@@ -455,9 +455,6 @@ adminUserSettings.confirmChangeUserStatus=Отключить/включить п
adminUserSettings.usernameInfo=Имя пользователя может содержать только буквы, цифры и следующие специальные символы @._+- или должно быть действительным адресом электронной почты.
adminUserSettings.role=Роль
adminUserSettings.actions=Действия
adminUserSettings.apiUser=Ограниченный пользователь API
adminUserSettings.extraApiUser=Дополнительный ограниченный пользователь API
adminUserSettings.webOnlyUser=Только веб-пользователь
adminUserSettings.demoUser=Демо-пользователь (без настраиваемых параметров)
adminUserSettings.internalApiUser=Внутренний пользователь API
adminUserSettings.forceChange=Требовать смену пароля при входе
@@ -908,7 +905,6 @@ login.alreadyLoggedIn=Вы уже вошли в
login.alreadyLoggedIn2=устройств(а). Пожалуйста, выйдите из этих устройств и попробуйте снова.
login.toManySessions=У вас слишком много активных сессий
login.logoutMessage=Вы вышли из системы.
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
#auto-redact
autoRedact.title=Автоматическое редактирование
@@ -455,9 +455,6 @@ adminUserSettings.confirmChangeUserStatus=Should the user be disabled/enabled?
adminUserSettings.usernameInfo=Používateľské meno musí obsahovať iba písmená a čísla, žiadne medzery alebo špeciálne znaky.
adminUserSettings.role=Rola
adminUserSettings.actions=Akcie
adminUserSettings.apiUser=Obmedzený API používateľ
adminUserSettings.extraApiUser=Ďalší obmedzený API používateľ
adminUserSettings.webOnlyUser=Používateľ iba pre web
adminUserSettings.demoUser=Demo používateľ (Bez vlastných nastavení)
adminUserSettings.internalApiUser=Interný API používateľ
adminUserSettings.forceChange=Donútiť používateľa zmeniť heslo pri prihlásení
@@ -908,7 +905,6 @@ login.alreadyLoggedIn=You are already logged in to
login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
login.toManySessions=You have too many active sessions
login.logoutMessage=You have been logged out.
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
#auto-redact
autoRedact.title=Automatické redigovanie
@@ -455,9 +455,6 @@ adminUserSettings.confirmChangeUserStatus=Ali naj bo uporabnik onemogočen/omogo
adminUserSettings.usernameInfo=Uporabniško ime lahko vsebuje samo črke, številke in naslednje posebne znake @._+- ali mora biti veljaven e-poštni naslov.
adminUserSettings.role=Vloga
adminUserSettings.actions=Dejanja
adminUserSettings.apiUser=Omejen uporabnik API-ja
adminUserSettings.extraApiUser=Dodatni omejeni uporabnik API-ja
adminUserSettings.webOnlyUser=Samo spletni uporabnik
adminUserSettings.demoUser=Demo uporabnik (brez nastavitev po meri)
adminUserSettings.internalApiUser=Notranji uporabnik API-ja
adminUserSettings.forceChange=Prisili uporabnika, da spremeni geslo ob prijavi
@@ -908,7 +905,6 @@ login.alreadyLoggedIn=Prijavljeni ste že v
login.alreadyLoggedIn2=naprave. Odjavite se iz naprav in poskusite znova.
login.toManySessions=Imate preveč aktivnih sej
login.logoutMessage=You have been logged out.
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
#auto-redact
autoRedact.title=Samodejno redigiraj
@@ -455,9 +455,6 @@ adminUserSettings.confirmChangeUserStatus=Da onemogućim/omogućim korisnika?
adminUserSettings.usernameInfo=Korisničko ime može sadržati samo slova, brojeve i specijalne karaktere @._+- ili mora biti validna email adresa.
adminUserSettings.role=Uloga
adminUserSettings.actions=Akcije
adminUserSettings.apiUser=Korisnik s ograničenim API pristupom
adminUserSettings.extraApiUser=Dodatni ograničeni API korisnik
adminUserSettings.webOnlyUser=Korisnik samo za web
adminUserSettings.demoUser=Demo korisnik (Bez prilagođenih podešavanja)
adminUserSettings.internalApiUser=Interni API korisnik
adminUserSettings.forceChange=Prisili korisnika da promeni korisničko ime/lozinku pri prijavi
@@ -908,7 +905,6 @@ login.alreadyLoggedIn=Već si prijavljen na
login.alreadyLoggedIn2=uređaja. Odjavi se sa uređaja i pokušaj ponovo.
login.toManySessions=Imaš previše aktivnih sesija
login.logoutMessage=Odjavljen si.
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
#auto-redact
autoRedact.title=Automatsko cenzurisanje
@@ -455,9 +455,6 @@ adminUserSettings.confirmChangeUserStatus=Ska användaren inaktiveras/aktiveras?
adminUserSettings.usernameInfo=Användarnamn kan endast innehålla bokstäver, siffror och följande specialtecken @._+- eller måste vara en giltig e-postadress.
adminUserSettings.role=Roll
adminUserSettings.actions=Åtgärder
adminUserSettings.apiUser=Begränsad API-användare
adminUserSettings.extraApiUser=Ytterligare begränsad API-användare
adminUserSettings.webOnlyUser=Endast webbanvändare
adminUserSettings.demoUser=Demoanvändare (Inga anpassade inställningar)
adminUserSettings.internalApiUser=Intern API-användare
adminUserSettings.forceChange=Tvinga användare att ändra lösenord vid inloggning
@@ -908,7 +905,6 @@ login.alreadyLoggedIn=Du är redan inloggad på
login.alreadyLoggedIn2=enheter. Logga ut från enheterna och försök igen.
login.toManySessions=Du har för många aktiva sessioner
login.logoutMessage=You have been logged out.
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
#auto-redact
autoRedact.title=Auto-redigera
@@ -455,9 +455,6 @@ adminUserSettings.confirmChangeUserStatus=ผู้ใช้นี้ควร
adminUserSettings.usernameInfo=ชื่อผู้ใช้สามารถประกอบด้วยตัวอักษร ตัวเลข และอักขระพิเศษต่อไปนี้ @._+- หรือจะต้องเป็นที่อยู่อีเมลที่ถูกต้อง
adminUserSettings.role=บทบาท
adminUserSettings.actions=การดำเนินการ
adminUserSettings.apiUser=ผู้ใช้ API จำกัด
adminUserSettings.extraApiUser=ผู้ใช้ API เพิ่มเติม
adminUserSettings.webOnlyUser=ผู้ใช้เว็บเท่านั้น
adminUserSettings.demoUser=ผู้ใช้ทดลอง (ไม่มีการตั้งค่าปรับแต่ง)
adminUserSettings.internalApiUser=ผู้ใช้ API ภายใน
adminUserSettings.forceChange=บังคับให้ผู้ใช้เปลี่ยนรหัสผ่านในการเข้าสู่ระบบ
@@ -908,7 +905,6 @@ login.alreadyLoggedIn=คุณได้เข้าสู่ระบบใน
login.alreadyLoggedIn2=อุปกรณ์แล้ว กรุณาออกจากระบบจากอุปกรณ์ที่ใช้งานอยู่แล้ว จากนั้นลองใหม่อีกครั้ง
login.toManySessions=คุณมีการเข้าสู่ระบบพร้อมกันเกินกว่ากำหนด
login.logoutMessage=You have been logged out.
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
#auto-redact
autoRedact.title=ซ่อนข้อมูลอัตโนมัติ
@@ -455,9 +455,6 @@ adminUserSettings.confirmChangeUserStatus=Kullanıcı devre dışı bırakılmal
adminUserSettings.usernameInfo=Kullanıcı adı yalnızca harf, rakam ve aşağıdaki özel karakterleri @._+- içerebilir veya geçerli bir e-posta adresi olmalıdır.
adminUserSettings.role=Rol
adminUserSettings.actions=Eylemler
adminUserSettings.apiUser=Sınırlı API Kullanıcısı
adminUserSettings.extraApiUser=Ek Sınırlı API Kullanıcısı
adminUserSettings.webOnlyUser=Sadece Web Kullanıcısı
adminUserSettings.demoUser=Demo Kullanıcısı (Özel ayar yok)
adminUserSettings.internalApiUser=Dahili API Kullanıcısı
adminUserSettings.forceChange=Kullanıcının girişte kullanıcı adı/şifre değiştirmesini zorla
@@ -908,7 +905,6 @@ login.alreadyLoggedIn=Zaten şu cihazlarda oturum açılmış:
login.alreadyLoggedIn2=Lütfen bu cihazlardan çıkış yaparak tekrar deneyin.
login.toManySessions=Çok fazla aktif oturumunuz var
login.logoutMessage=You have been logged out.
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
#auto-redact
autoRedact.title=Otomatik Karartma
@@ -455,9 +455,6 @@ adminUserSettings.confirmChangeUserStatus=Чи потрібно вимкнути
adminUserSettings.usernameInfo=Ім’я користувача може містити лише літери, цифри та наступні спеціальні символи @._+- або має бути дійсною електронною адресою.
adminUserSettings.role=Роль
adminUserSettings.actions=Дії
adminUserSettings.apiUser=Обмежений користувач API
adminUserSettings.extraApiUser=Додатковий обмежений користувач API
adminUserSettings.webOnlyUser=Тільки веб-користувач
adminUserSettings.demoUser=Демо-користувач (без налаштованих параметрів)
adminUserSettings.internalApiUser=Внутрішній користувач API
adminUserSettings.forceChange=Примусити користувача змінити пароль при вході в систему
@@ -908,7 +905,6 @@ login.alreadyLoggedIn=Ви вже увійшли до
login.alreadyLoggedIn2=пристроїв (а). Будь ласка, вийдіть із цих пристроїв і спробуйте знову.
login.toManySessions=У вас дуже багато активних сесій
login.logoutMessage=You have been logged out.
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
#auto-redact
autoRedact.title=Автоматичне редагування
@@ -455,9 +455,6 @@ adminUserSettings.confirmChangeUserStatus=Should the user be disabled/enabled?
adminUserSettings.usernameInfo=Tên người dùng chỉ có thể chứa chữ cái, số và các ký tự đặc biệt sau @._+- hoặc phải là một địa chỉ email hợp lệ.
adminUserSettings.role=Vai trò
adminUserSettings.actions=Hành động
adminUserSettings.apiUser=Người dùng API giới hạn
adminUserSettings.extraApiUser=Người dùng API giới hạn bổ sung
adminUserSettings.webOnlyUser=Chỉ người dùng web
adminUserSettings.demoUser=Người dùng demo (Không có cài đặt tùy chỉnh)
adminUserSettings.internalApiUser=Người dùng API nội bộ
adminUserSettings.forceChange=Buộc người dùng thay đổi mật khẩu khi đăng nhập
@@ -908,7 +905,6 @@ login.alreadyLoggedIn=You are already logged in to
login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
login.toManySessions=You have too many active sessions
login.logoutMessage=You have been logged out.
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
#auto-redact
autoRedact.title=Tự động biên tập
@@ -455,9 +455,6 @@ adminUserSettings.confirmChangeUserStatus=是否应禁用/启用该用户?
adminUserSettings.usernameInfo=用户名只能包含字母、数字和以下特殊字符@._+-,或者必须是有效的电子邮件地址。
adminUserSettings.role=角色
adminUserSettings.actions=操作
adminUserSettings.apiUser=受限制的 API 用户
adminUserSettings.extraApiUser=额外受限制的 API 用户
adminUserSettings.webOnlyUser=仅限 Web 用户
adminUserSettings.demoUser=演示用户(无自定义设置)
adminUserSettings.internalApiUser=内部 API 用户
adminUserSettings.forceChange=强制用户在登录时更改用户名/密码
@@ -908,7 +905,6 @@ login.alreadyLoggedIn=您已经登录到了
login.alreadyLoggedIn2=设备,请注销设备后重试。
login.toManySessions=你已经有太多的会话了。请注销一些设备后重试。
login.logoutMessage=您已退出登录。
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
#auto-redact
autoRedact.title=自动删除
@@ -455,9 +455,6 @@ adminUserSettings.confirmChangeUserStatus=是否要停用/啟用此使用者?
adminUserSettings.usernameInfo=使用者名稱只能包含字母、數字和以下特殊字元 @._+- 或必須是有效的電子郵件地址。
adminUserSettings.role=角色
adminUserSettings.actions=操作
adminUserSettings.apiUser=受限制的 API 使用者
adminUserSettings.extraApiUser=額外受限制的 API 使用者
adminUserSettings.webOnlyUser=僅網頁版使用者
adminUserSettings.demoUser=示範使用者(無自訂設定)
adminUserSettings.internalApiUser=內部 API 使用者
adminUserSettings.forceChange=強制使用者在登入時變更密碼
@@ -908,7 +905,6 @@ login.alreadyLoggedIn=您已經登入了
login.alreadyLoggedIn2=部裝置。請先從這些裝置登出後再試一次。
login.toManySessions=您有太多使用中的工作階段
login.logoutMessage=您已登出。
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
#auto-redact
autoRedact.title=自動塗黑
@@ -59,17 +59,12 @@ security:
idpCert: classpath:okta.cert # The certificate your Provider will use to authenticate your app's SAML authentication requests. Provided by your Provider
privateKey: classpath:saml-private-key.key # Your private key. Generated from your keypair
spCert: classpath:saml-public-cert.crt # Your signing certificate. Generated from your keypair
jwt: # This feature is currently under development and not yet fully supported. Do not use in production.
persistence: true # Set to 'true' to enable JWT key store
enableKeyRotation: true # Set to 'true' to enable key pair rotation
enableKeyCleanup: true # Set to 'true' to enable key pair cleanup
keyRetentionDays: 7 # Number of days to retain old keys. The default is 7 days.
secureCookie: false # Set to 'true' to use secure cookies for JWTs
premium:
key: 00000000-0000-0000-0000-000000000000
enabled: false # Enable license key checks for pro/enterprise features
proFeatures:
database: true # Enable database features
SSOAutoLogin: false
CustomMetadata:
autoUpdateMetadata: false
@@ -132,13 +132,6 @@
"moduleLicense": "Apache License, Version 2.0",
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt"
},
{
"moduleName": "com.github.ben-manes.caffeine:caffeine",
"moduleUrl": "https://github.com/ben-manes/caffeine",
"moduleVersion": "3.2.2",
"moduleLicense": "Apache License, Version 2.0",
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt"
},
{
"moduleName": "com.github.jai-imageio:jai-imageio-core",
"moduleUrl": "https://github.com/jai-imageio/jai-imageio-core",
@@ -175,7 +168,7 @@
{
"moduleName": "com.google.errorprone:error_prone_annotations",
"moduleUrl": "https://errorprone.info/error_prone_annotations",
"moduleVersion": "2.40.0",
"moduleVersion": "2.38.0",
"moduleLicense": "Apache 2.0",
"moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt"
},
@@ -556,27 +549,6 @@
"moduleLicense": "MIT License",
"moduleLicenseUrl": "http://www.opensource.org/licenses/mit-license.php"
},
{
"moduleName": "io.jsonwebtoken:jjwt-api",
"moduleUrl": "https://github.com/jwtk/jjwt",
"moduleVersion": "0.12.6",
"moduleLicense": "Apache License, Version 2.0",
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
},
{
"moduleName": "io.jsonwebtoken:jjwt-impl",
"moduleUrl": "https://github.com/jwtk/jjwt",
"moduleVersion": "0.12.6",
"moduleLicense": "Apache License, Version 2.0",
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
},
{
"moduleName": "io.jsonwebtoken:jjwt-jackson",
"moduleUrl": "https://github.com/jwtk/jjwt",
"moduleVersion": "0.12.6",
"moduleLicense": "Apache License, Version 2.0",
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
},
{
"moduleName": "io.micrometer:micrometer-commons",
"moduleUrl": "https://github.com/micrometer-metrics/micrometer",
@@ -1535,13 +1507,6 @@
"moduleLicense": "Apache License, Version 2.0",
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
},
{
"moduleName": "org.springframework.boot:spring-boot-starter-cache",
"moduleUrl": "https://spring.io/projects/spring-boot",
"moduleVersion": "3.5.4",
"moduleLicense": "Apache License, Version 2.0",
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
},
{
"moduleName": "org.springframework.boot:spring-boot-starter-data-jpa",
"moduleUrl": "https://spring.io/projects/spring-boot",
@@ -46,9 +46,10 @@ export class DecryptFile {
formData.append('password', password);
}
// Send decryption request
const response = await fetchWithCsrf('/api/v1/security/remove-password', {
const response = await fetch('/api/v1/security/remove-password', {
method: 'POST',
body: formData,
headers: csrfToken ? {'X-XSRF-TOKEN': csrfToken} : undefined,
});
if (response.ok) {
@@ -218,7 +218,7 @@
formData.append('password', password);
// Use handleSingleDownload to send the request
const decryptionResult = await fetchWithCsrf(removePasswordUrl, {method: 'POST', body: formData});
const decryptionResult = await fetch(removePasswordUrl, {method: 'POST', body: formData});
if (decryptionResult && decryptionResult.blob) {
const decryptedBlob = await decryptionResult.blob();
@@ -1,29 +1,3 @@
// Authentication utility for cookie-based JWT
window.JWTManager = {
// Logout - clear cookies and redirect to login
logout: function() {
// Clear JWT cookie manually (fallback)
document.cookie = 'stirling_jwt=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT; SameSite=None; Secure';
// Perform logout request to clear server-side session
fetch('/logout', {
method: 'POST',
credentials: 'include'
}).then(response => {
if (response.redirected) {
window.location.href = response.url;
} else {
window.location.href = '/login?logout=true';
}
}).catch(() => {
// If logout fails, let server handle it
window.location.href = '/logout';
});
}
};
window.fetchWithCsrf = async function(url, options = {}) {
function getCsrfToken() {
const cookieValue = document.cookie
@@ -50,18 +24,5 @@ window.fetchWithCsrf = async function(url, options = {}) {
fetchOptions.headers['X-XSRF-TOKEN'] = csrfToken;
}
// Always include credentials to send JWT cookies
fetchOptions.credentials = 'include';
// Make the request
const response = await fetch(url, fetchOptions);
// Handle 401 responses (unauthorized)
if (response.status === 401) {
console.warn('Authentication failed, redirecting to login');
window.JWTManager.logout();
return response;
}
return response;
return fetch(url, fetchOptions);
}
@@ -1,44 +0,0 @@
// JWT Authentication Management Script
// This script handles cookie-based JWT authentication and page access control
(function() {
// Clean up JWT token from URL parameters after OAuth/Login flows
function cleanupTokenFromUrl() {
const urlParams = new URLSearchParams(window.location.search);
const hasToken = urlParams.get('jwt') || urlParams.get('token');
if (hasToken) {
// Clean up URL by removing token parameter
// Token should now be set as cookie by server
urlParams.delete('jwt');
urlParams.delete('token');
const newUrl = window.location.pathname + (urlParams.toString() ? '?' + urlParams.toString() : '');
window.history.replaceState({}, '', newUrl);
}
}
// Initialize JWT handling when page loads
function initializeJWT() {
// Clean up any JWT tokens from URL (OAuth flow)
cleanupTokenFromUrl();
// Authentication is handled server-side
// If user is not authenticated, server will redirect to login
console.log('JWT initialization complete - authentication handled server-side');
}
// No form enhancement needed for cookie-based JWT
// Cookies are automatically sent with form submissions
function enhanceFormSubmissions() {
// Cookie-based JWT is automatically included in form submissions
// No additional processing needed
}
// Initialize when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function() {
initializeJWT();
});
} else {
initializeJWT();
}
})();
@@ -138,19 +138,5 @@ document.addEventListener('DOMContentLoaded', () => {
tooltipSetup();
setupDropdowns();
fixNavbarDropdownStyles();
// Setup logout button functionality
const logoutButton = document.querySelector('a[href="/logout"]');
if (logoutButton) {
logoutButton.addEventListener('click', function(event) {
event.preventDefault();
if (window.JWTManager) {
window.JWTManager.logout();
} else {
// Fallback if JWTManager is not available
window.location.href = '/logout';
}
});
}
});
window.addEventListener('resize', fixNavbarDropdownStyles);
@@ -102,7 +102,7 @@ async function fetchEndpointData() {
refreshBtn.classList.add('refreshing');
refreshBtn.disabled = true;
const response = await fetchWithCsrf('/api/v1/info/load/all');
const response = await fetch('/api/v1/info/load/all');
if (!response.ok) {
throw new Error('Network response was not ok');
}
@@ -32,7 +32,7 @@
</div>
<!-- Admin Settings Banner (for admins only) -->
<div th:if="${role == 'ROLE_ADMIN'}" class="data-panel data-mb-3" style="background-color: var(--md-sys-color-secondary-container);">
<div th:if="${isSystemAdmin}" class="data-panel data-mb-3" style="background-color: var(--md-sys-color-secondary-container);">
<div class="data-body" style="display: flex; align-items: center; justify-content: space-between; padding: 1rem 1.5rem; background-color: var(--md-sys-color-secondary-container);">
<div style="display: flex; align-items: center; gap: 1rem;">
<span class="material-symbols-rounded" style="font-size: 2rem; color: var(--md-sys-color-secondary);">
@@ -390,13 +390,8 @@
key.includes('clientSubmissionOrder') ||
key.includes('lastSubmitTime') ||
key.includes('lastClientId') ||
key.includes('stirling_jwt') ||
key.includes('JSESSIONID') ||
key.includes('XSRF-TOKEN') ||
key.includes('remember-me') ||
key.includes('auth') ||
key.includes('token') ||
key.includes('session') ||
key.includes('posthog') || key.includes('ssoRedirectAttempts') || key.includes('lastRedirectAttempt') || key.includes('surveyVersion') ||
key.includes('pageViews');
}
-12
View File
@@ -1,15 +1,9 @@
repositories {
maven { url = "https://build.shibboleth.net/maven/releases" }
}
ext {
jwtVersion = '0.12.6'
}
bootRun {
enabled = false
}
spotless {
java {
target 'src/**/java/**/*.java'
@@ -49,8 +43,6 @@ dependencies {
api 'org.springframework.boot:spring-boot-starter-data-jpa'
api 'org.springframework.boot:spring-boot-starter-oauth2-client'
api 'org.springframework.boot:spring-boot-starter-mail'
api 'org.springframework.boot:spring-boot-starter-cache'
api 'com.github.ben-manes.caffeine:caffeine'
api 'io.swagger.core.v3:swagger-core-jakarta:2.2.35'
implementation 'com.bucket4j:bucket4j_jdk17-core:8.14.0'
@@ -60,10 +52,6 @@ dependencies {
// implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5:3.1.3.RELEASE' // Removed - UI moved to React frontend
api 'io.micrometer:micrometer-registry-prometheus'
implementation 'com.unboundid.product.scim2:scim2-sdk-client:4.0.0'
api "io.jsonwebtoken:jjwt-api:$jwtVersion"
runtimeOnly "io.jsonwebtoken:jjwt-impl:$jwtVersion"
runtimeOnly "io.jsonwebtoken:jjwt-jackson:$jwtVersion"
runtimeOnly 'com.h2database:h2:2.3.232' // Don't upgrade h2database
runtimeOnly 'org.postgresql:postgresql:42.7.7'
constraints {
@@ -0,0 +1,71 @@
package stirling.software.proprietary.config;
import java.util.Map;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import stirling.software.common.model.enumeration.Role;
import stirling.software.proprietary.service.ApiCreditService;
@Configuration
@ConfigurationProperties(prefix = "api.credit-system")
@Data
@Slf4j
public class ApiCreditConfiguration {
private boolean enabled = true;
private boolean excludeSettings = true;
private boolean excludeActuator = true;
private int defaultCreditCost = 1;
/**
* Default monthly credit limits by role. Override in application.yml/properties. Note:
* Integer.MAX_VALUE is treated as "unlimited".
*/
private Map<String, Integer> defaultCreditLimits =
Map.of(
"ROLE_SYSTEM_ADMIN", Integer.MAX_VALUE,
"ROLE_ORG_ADMIN", 10000,
"ROLE_TEAM_LEAD", 5000,
"ROLE_ADMIN", Integer.MAX_VALUE,
"ROLE_USER", 50,
"ROLE_DEMO_USER", 20,
"STIRLING-PDF-BACKEND-API-USER", Integer.MAX_VALUE);
@Bean
public CommandLineRunner initializeDefaultCreditLimits(ApiCreditService creditService) {
return args -> {
if (!enabled) {
log.info("API credit system is disabled");
return;
}
log.info("Initializing default API credit limits...");
initializeDefaults(creditService);
log.info("Default API credit limits initialized successfully");
};
}
private void initializeDefaults(ApiCreditService creditService) {
for (Map.Entry<String, Integer> entry : defaultCreditLimits.entrySet()) {
String roleName = entry.getKey();
Integer creditLimit = entry.getValue();
try {
Role.fromString(roleName);
creditService.createOrUpdateRoleDefault(roleName, creditLimit);
log.debug(
"Set default credit limit for role {} to {}/month",
roleName,
creditLimit == Integer.MAX_VALUE ? "unlimited" : creditLimit);
} catch (IllegalArgumentException e) {
log.warn("Skipping unknown role in credit system config: {}", roleName);
}
}
}
}
@@ -1,17 +0,0 @@
package stirling.software.proprietary.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.transaction.annotation.EnableTransactionManagement;
/** Configuration to explicitly enable JPA repositories and scheduling for the audit system. */
@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(basePackages = "stirling.software.proprietary.repository")
@EnableScheduling
public class AuditJpaConfig {
// This configuration enables JPA repositories in the specified package
// and enables scheduling for audit cleanup tasks
// No additional beans or methods needed
}
@@ -18,7 +18,7 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import stirling.software.proprietary.model.security.PersistentAuditEvent;
import stirling.software.proprietary.repository.PersistentAuditEventRepository;
import stirling.software.proprietary.security.repository.PersistentAuditEventRepository;
import stirling.software.proprietary.util.SecretMasker;
@Component
@@ -30,13 +30,13 @@ public class CustomAuditEventRepository implements AuditEventRepository {
private final PersistentAuditEventRepository repo;
private final ObjectMapper mapper;
/* ── READ side intentionally inert (endpoint disabled) ── */
/* READ side intentionally inert (endpoint disabled) */
@Override
public List<AuditEvent> find(String p, Instant after, String type) {
return List.of();
}
/* ── WRITE side (async) ───────────────────────────────── */
/* WRITE side (async) */
@Async("auditExecutor")
@Override
public void add(AuditEvent ev) {
@@ -33,7 +33,7 @@ public class AdminJobController {
* @return Job statistics
*/
@GetMapping("/api/v1/admin/job/stats")
@PreAuthorize("hasRole('ROLE_ADMIN')")
@PreAuthorize("@roleBasedAuthorizationService.canManageAllUsers()")
public ResponseEntity<JobStats> getJobStats() {
JobStats stats = taskManager.getJobStats();
log.info(
@@ -49,7 +49,7 @@ public class AdminJobController {
* @return Queue statistics
*/
@GetMapping("/api/v1/admin/job/queue/stats")
@PreAuthorize("hasRole('ROLE_ADMIN')")
@PreAuthorize("@roleBasedAuthorizationService.canManageAllUsers()")
public ResponseEntity<?> getQueueStats() {
Map<String, Object> queueStats = jobQueue.getQueueStats();
log.info("Admin requested queue stats: {} queued jobs", queueStats.get("queuedJobs"));
@@ -62,7 +62,7 @@ public class AdminJobController {
* @return A response indicating how many jobs were cleaned up
*/
@PostMapping("/api/v1/admin/job/cleanup")
@PreAuthorize("hasRole('ROLE_ADMIN')")
@PreAuthorize("@roleBasedAuthorizationService.canManageAllUsers()")
public ResponseEntity<?> cleanupOldJobs() {
int beforeCount = taskManager.getJobStats().getTotalJobs();
taskManager.cleanupOldJobs();
@@ -40,14 +40,14 @@ import stirling.software.proprietary.audit.AuditEventType;
import stirling.software.proprietary.audit.AuditLevel;
import stirling.software.proprietary.config.AuditConfigurationProperties;
import stirling.software.proprietary.model.security.PersistentAuditEvent;
import stirling.software.proprietary.repository.PersistentAuditEventRepository;
import stirling.software.proprietary.security.repository.PersistentAuditEventRepository;
import stirling.software.proprietary.security.config.EnterpriseEndpoint;
/** Controller for the audit dashboard. Admin-only access. */
@Slf4j
// @Controller // Disabled - Backend-only mode, no Thymeleaf UI
@RequestMapping("/audit")
@PreAuthorize("hasRole('ADMIN')")
@PreAuthorize("@roleBasedAuthorizationService.canManageAllUsers()")
@RequiredArgsConstructor
@EnterpriseEndpoint
public class AuditDashboardController {
@@ -0,0 +1,173 @@
package stirling.software.proprietary.controller.api;
import java.time.YearMonth;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import stirling.software.proprietary.security.model.User;
import stirling.software.proprietary.security.service.UserService;
import stirling.software.proprietary.service.ApiCreditService;
@RestController
@RequestMapping("/api/v1/credits")
@RequiredArgsConstructor
@Slf4j
@Tag(
name = "API Credits",
description = "Endpoints for managing and viewing API credit limits and usage")
public class ApiCreditController {
private final ApiCreditService creditService;
private final UserService userService;
public record CreditMetricsResponse(
int creditsConsumed,
int monthlyCredits,
int remaining,
String scope,
String month,
boolean isPooled,
long resetEpochMillis) {}
public record UpdateCreditLimitRequest(int monthlyCredits, Boolean isActive) {}
public record CreateUserCreditConfigRequest(
String username, int monthlyCredits, boolean isActive) {}
public record CreateOrgCreditConfigRequest(
String organizationName, int monthlyCredits, boolean isPooled, boolean isActive) {}
@Operation(
summary = "Get current user's credit metrics",
description =
"Returns the current user's credit consumption, limits, and remaining credits for the current month")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "Credit metrics retrieved successfully"),
@ApiResponse(responseCode = "401", description = "User not authenticated")
})
@GetMapping("/my-usage")
public ResponseEntity<CreditMetricsResponse> getMyCredits(Authentication authentication) {
if (authentication == null || !authentication.isAuthenticated()) {
return ResponseEntity.status(401).build();
}
User user =
userService
.findByUsername(authentication.getName())
.orElseThrow(() -> new RuntimeException("User not found"));
ApiCreditService.CreditMetrics metrics = creditService.getUserCreditMetrics(user);
YearMonth nextMonth = metrics.month().plusMonths(1);
ZonedDateTime resetTime = nextMonth.atDay(1).atStartOfDay(ZoneOffset.UTC);
long resetEpochMillis = resetTime.toInstant().toEpochMilli();
CreditMetricsResponse response =
new CreditMetricsResponse(
metrics.creditsConsumed(),
metrics.monthlyCredits(),
metrics.remaining(),
metrics.scope(),
metrics.month().toString(),
metrics.isPooled(),
resetEpochMillis);
return ResponseEntity.ok(response);
}
@Operation(
summary = "Get user credit metrics",
description = "Returns credit metrics for a specific user (admin only)")
@PreAuthorize("@roleBasedAuthorizationService.canManageAllUsers()")
@GetMapping("/user/{username}")
public ResponseEntity<CreditMetricsResponse> getUserCredits(
@Parameter(description = "Username to get credit metrics for") @PathVariable
String username) {
User user = userService.findByUsername(username).orElse(null);
if (user == null) {
return ResponseEntity.notFound().build();
}
ApiCreditService.CreditMetrics metrics = creditService.getUserCreditMetrics(user);
YearMonth nextMonth = metrics.month().plusMonths(1);
ZonedDateTime resetTime = nextMonth.atDay(1).atStartOfDay(ZoneOffset.UTC);
long resetEpochMillis = resetTime.toInstant().toEpochMilli();
CreditMetricsResponse response =
new CreditMetricsResponse(
metrics.creditsConsumed(),
metrics.monthlyCredits(),
metrics.remaining(),
metrics.scope(),
metrics.month().toString(),
metrics.isPooled(),
resetEpochMillis);
return ResponseEntity.ok(response);
}
@Operation(
summary = "Create user-specific credit limit",
description = "Create a credit limit configuration for a specific user (admin only)")
@PreAuthorize("@roleBasedAuthorizationService.canManageAllUsers()")
@PostMapping("/config/user")
public ResponseEntity<?> createUserCreditConfig(
@RequestBody CreateUserCreditConfigRequest request) {
try {
User user = userService.findByUsername(request.username()).orElse(null);
if (user == null) {
return ResponseEntity.badRequest().body("User not found: " + request.username());
}
creditService.createUserCreditConfig(user, request.monthlyCredits(), request.isActive());
return ResponseEntity.ok().body("User credit configuration created successfully");
} catch (Exception e) {
log.error("Error creating user credit config", e);
return ResponseEntity.badRequest().body("Error: " + e.getMessage());
}
}
@Operation(
summary = "Update role default credit limits",
description = "Update the default credit limit for a specific role (admin only)")
@PreAuthorize("@roleBasedAuthorizationService.canManageAllUsers()")
@PutMapping("/config/role/{roleName}")
public ResponseEntity<?> updateRoleDefault(
@Parameter(description = "Role name to update") @PathVariable String roleName,
@RequestBody UpdateCreditLimitRequest request) {
try {
creditService.createOrUpdateRoleDefault(roleName, request.monthlyCredits());
return ResponseEntity.ok().body("Role default updated successfully");
} catch (Exception e) {
log.error("Error updating role default", e);
return ResponseEntity.badRequest().body("Error: " + e.getMessage());
}
}
@Operation(
summary = "Get credit system status",
description = "Returns basic information about the credit system configuration")
@GetMapping("/status")
public ResponseEntity<?> getCreditSystemStatus() {
return ResponseEntity.ok()
.body(
"API Credit System is active. Use /api/v1/credits/my-usage to check your credit balance.");
}
}
@@ -0,0 +1,119 @@
package stirling.software.proprietary.model;
import java.io.Serializable;
import java.time.Instant;
import java.time.YearMonth;
import java.util.HashSet;
import java.util.Set;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import jakarta.persistence.*;
import jakarta.validation.constraints.NotNull;
import lombok.*;
import stirling.software.proprietary.model.converter.YearMonthStringConverter;
@Entity
@Table(
name = "anonymous_api_credit_usage",
uniqueConstraints = {
@UniqueConstraint(
name = "uq_anon_credit_fingerprint_month",
columnNames = {"fingerprint", "month"})
},
indexes = {
@Index(name = "idx_anon_credit_fingerprint", columnList = "fingerprint"),
@Index(name = "idx_anon_credit_month", columnList = "month"),
@Index(name = "idx_anon_credit_ip", columnList = "ip_address")
})
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Getter
@Setter
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
@ToString(onlyExplicitlyIncluded = true)
public class AnonymousCreditUsage implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
@EqualsAndHashCode.Include
@ToString.Include
private Long id;
@NotNull
@Column(name = "fingerprint", nullable = false, length = 128)
@ToString.Include
private String fingerprint;
@NotNull
@Column(name = "month", nullable = false, length = 7)
@Convert(converter = YearMonthStringConverter.class)
private YearMonth month;
@NotNull
@Column(name = "credits_consumed", nullable = false)
@Builder.Default
private Integer creditsConsumed = 0;
@NotNull
@Column(name = "credits_allocated", nullable = false)
@Builder.Default
private Integer creditsAllocated = 0;
@Column(name = "ip_address", length = 45)
private String ipAddress;
@Column(name = "user_agent", length = 512)
private String userAgent;
@ElementCollection(fetch = FetchType.EAGER)
@CollectionTable(
name = "anonymous_api_related_fingerprints",
joinColumns = @JoinColumn(name = "usage_id"))
@Column(name = "related_fingerprint")
@Builder.Default
private Set<String> relatedFingerprints = new HashSet<>();
@Column(name = "abuse_score")
@Builder.Default
private Integer abuseScore = 0;
@Column(name = "is_blocked")
@Builder.Default
private Boolean isBlocked = false;
@Column(name = "last_access")
private Instant lastAccess;
@CreationTimestamp
@Column(name = "created_at", updatable = false, nullable = false)
private Instant createdAt;
@UpdateTimestamp
@Column(name = "updated_at", nullable = false)
private Instant updatedAt;
@Version
@Column(name = "version")
private Long version;
@PreUpdate
public void preUpdate() {
this.lastAccess = Instant.now();
}
public int getRemainingCredits() {
return Math.max(0, creditsAllocated - creditsConsumed);
}
public boolean hasCreditsRemaining(int creditCost) {
return getRemainingCredits() >= creditCost;
}
}
@@ -0,0 +1,132 @@
package stirling.software.proprietary.model;
import java.io.Serializable;
import java.time.Instant;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import jakarta.persistence.*;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import lombok.*;
import stirling.software.proprietary.security.model.User;
@Entity
@Table(
name = "api_credit_configs",
uniqueConstraints = {
@UniqueConstraint(
name = "uq_user_credit_cfg",
columnNames = {"scope_type", "user_id"}),
@UniqueConstraint(
name = "uq_org_credit_cfg",
columnNames = {"scope_type", "org_id"}),
@UniqueConstraint(
name = "uq_role_credit_cfg",
columnNames = {"scope_type", "role_name"})
},
indexes = {
@Index(name = "idx_credit_cfg_user", columnList = "user_id"),
@Index(name = "idx_credit_cfg_org", columnList = "org_id"),
@Index(name = "idx_credit_cfg_role", columnList = "role_name"),
@Index(name = "idx_credit_cfg_scope", columnList = "scope_type")
})
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Getter
@Setter
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
@ToString(onlyExplicitlyIncluded = true)
public class ApiCreditConfig implements Serializable {
private static final long serialVersionUID = 1L;
public enum ScopeType {
USER,
ORGANIZATION,
ROLE_DEFAULT
}
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
@EqualsAndHashCode.Include
@ToString.Include
private Long id;
@NotNull
@Column(name = "scope_type", nullable = false, length = 20)
@Enumerated(EnumType.STRING)
private ScopeType scopeType;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = true)
private User user;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "org_id", nullable = true)
private Organization organization;
@Column(name = "role_name", nullable = true, length = 50)
private String roleName;
@NotNull
@Min(0)
@Column(name = "monthly_credits", nullable = false)
private Integer monthlyCredits;
@NotNull
@Builder.Default
@Column(name = "is_pooled", nullable = false)
private Boolean isPooled = false;
@NotNull
@Builder.Default
@Column(name = "is_active", nullable = false)
private Boolean isActive = true;
@CreationTimestamp
@Column(name = "created_at", updatable = false, nullable = false)
private Instant createdAt;
@UpdateTimestamp
@Column(name = "updated_at", nullable = false)
private Instant updatedAt;
@Version
@Column(name = "version")
private Long version;
@PrePersist
@PreUpdate
protected void validateScope() {
int nonNullCount = 0;
if (user != null) nonNullCount++;
if (organization != null) nonNullCount++;
if (roleName != null) nonNullCount++;
if (nonNullCount != 1) {
throw new IllegalStateException(
"Exactly one of user, organization, or roleName must be set");
}
if (user != null && scopeType != ScopeType.USER) {
throw new IllegalStateException("ScopeType must be USER when user is set");
}
if (organization != null && scopeType != ScopeType.ORGANIZATION) {
throw new IllegalStateException(
"ScopeType must be ORGANIZATION when organization is set");
}
if (roleName != null && scopeType != ScopeType.ROLE_DEFAULT) {
throw new IllegalStateException("ScopeType must be ROLE_DEFAULT when roleName is set");
}
if (Boolean.TRUE.equals(isPooled) && scopeType != ScopeType.ORGANIZATION) {
throw new IllegalStateException("isPooled can only be true for ORGANIZATION scope");
}
}
}
@@ -0,0 +1,127 @@
package stirling.software.proprietary.model;
import java.io.Serializable;
import java.time.Instant;
import java.time.YearMonth;
import java.time.ZoneOffset;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import jakarta.persistence.*;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import lombok.*;
import stirling.software.proprietary.security.model.User;
@Entity
@Table(
name = "api_credit_usage",
uniqueConstraints = {
@UniqueConstraint(
name = "uq_credit_user_month",
columnNames = {"user_id", "month_key"}),
@UniqueConstraint(
name = "uq_credit_org_month",
columnNames = {"org_id", "month_key"})
},
indexes = {
@Index(name = "idx_credit_usage_user_month", columnList = "user_id, month_key"),
@Index(name = "idx_credit_usage_org_month", columnList = "org_id, month_key"),
@Index(name = "idx_credit_usage_month", columnList = "month_key")
})
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Getter
@Setter
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
@ToString(onlyExplicitlyIncluded = true)
public class ApiCreditUsage implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
@EqualsAndHashCode.Include
@ToString.Include
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = true)
private User user;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "org_id", nullable = true)
private Organization organization;
@NotNull
@Column(name = "month_key", nullable = false, length = 7)
@ToString.Include
private YearMonth monthKey;
@NotNull
@Min(0)
@Builder.Default
@Column(name = "credits_consumed", nullable = false)
private Integer creditsConsumed = 0;
@NotNull
@Min(0)
@Builder.Default
@Column(name = "credits_allocated", nullable = false)
private Integer creditsAllocated = 0;
@CreationTimestamp
@Column(name = "created_at", updatable = false, nullable = false)
private Instant createdAt;
@UpdateTimestamp
@Column(name = "updated_at", nullable = false)
private Instant updatedAt;
@Version
@Column(name = "version")
private Long version;
@PrePersist
@PreUpdate
protected void validateScope() {
if ((user == null && organization == null) || (user != null && organization != null)) {
throw new IllegalStateException("Exactly one of user or organization must be set");
}
}
public static YearMonth getCurrentMonth() {
return YearMonth.now(ZoneOffset.UTC);
}
public static ApiCreditUsage forUser(User user, int creditsAllocated) {
return ApiCreditUsage.builder()
.user(user)
.monthKey(getCurrentMonth())
.creditsConsumed(0)
.creditsAllocated(creditsAllocated)
.build();
}
public static ApiCreditUsage forOrganization(Organization org, int creditsAllocated) {
return ApiCreditUsage.builder()
.organization(org)
.monthKey(getCurrentMonth())
.creditsConsumed(0)
.creditsAllocated(creditsAllocated)
.build();
}
public int getRemainingCredits() {
return Math.max(0, creditsAllocated - creditsConsumed);
}
public boolean hasCreditsRemaining(int creditCost) {
return getRemainingCredits() >= creditCost;
}
}
@@ -0,0 +1,83 @@
package stirling.software.proprietary.model;
import java.util.UUID;
import jakarta.servlet.http.HttpServletResponse;
import stirling.software.proprietary.security.model.User;
/** Holds context information for credit-based API requests */
public class CreditRequestContext {
private final String requestId;
private final User user;
private final String ipAddress;
private final String userAgent;
private final int creditCost;
private final String endpoint;
private boolean creditsPreChecked = false;
private HttpServletResponse httpResponse;
public CreditRequestContext(
String requestId,
User user,
String ipAddress,
String userAgent,
int creditCost,
String endpoint) {
this.requestId = requestId;
this.user = user;
this.ipAddress = ipAddress;
this.userAgent = userAgent;
this.creditCost = creditCost;
this.endpoint = endpoint;
}
public String getRequestId() {
return requestId;
}
public User getUser() {
return user;
}
public String getIpAddress() {
return ipAddress;
}
public String getUserAgent() {
return userAgent;
}
public int getCreditCost() {
return creditCost;
}
public String getEndpoint() {
return endpoint;
}
public boolean isCreditsPreChecked() {
return creditsPreChecked;
}
public void setCreditsPreChecked(boolean preChecked) {
this.creditsPreChecked = preChecked;
}
public boolean isAnonymous() {
return user == null;
}
public HttpServletResponse getHttpResponse() {
return httpResponse;
}
public void setHttpResponse(HttpServletResponse httpResponse) {
this.httpResponse = httpResponse;
}
/** Generate a unique identifier for this request */
public static String generateRequestId() {
return UUID.randomUUID().toString();
}
}
@@ -0,0 +1,20 @@
package stirling.software.proprietary.model;
/** Classification of API call failures for credit charging purposes */
public enum FailureType {
/**
* Client-side errors that don't consume credits and don't count toward failure limit Examples:
* 400 Bad Request, 401 Unauthorized, 403 Forbidden, 422 Unprocessable Entity
*/
CLIENT_ERROR,
/**
* Processing errors that occur after validation passes - these count toward consecutive
* failures Examples: 500 Internal Server Error, processing exceptions, timeout during PDF
* manipulation
*/
PROCESSING_ERROR,
/** Successful processing - resets failure counter and consumes credits */
SUCCESS
}
@@ -0,0 +1,45 @@
package stirling.software.proprietary.model;
import java.io.Serializable;
import java.util.HashSet;
import java.util.Set;
import jakarta.persistence.*;
import lombok.*;
@Entity
@Table(name = "organizations")
@NoArgsConstructor
@Getter
@Setter
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
@ToString(onlyExplicitlyIncluded = true)
public class Organization implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "org_id")
private Long id;
@Column(name = "name", unique = true, nullable = false)
private String name;
@Column(name = "description")
private String description;
@OneToMany(mappedBy = "organization", cascade = CascadeType.ALL, orphanRemoval = true)
private Set<Team> teams = new HashSet<>();
public void addTeam(Team team) {
teams.add(team);
team.setOrganization(this);
}
public void removeTeam(Team team) {
teams.remove(team);
team.setOrganization(null);
}
}
@@ -11,7 +11,7 @@ import lombok.*;
import stirling.software.proprietary.security.model.User;
@Entity
@Table(name = "teams")
@Table(name = "teams", uniqueConstraints = @UniqueConstraint(columnNames = {"name", "org_id"}))
@NoArgsConstructor
@Getter
@Setter
@@ -26,9 +26,15 @@ public class Team implements Serializable {
@Column(name = "team_id")
private Long id;
@Column(name = "name", unique = true, nullable = false)
@Column(name = "name", nullable = false)
private String name;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(
name = "org_id",
nullable = true) // Nullable for backward compatibility during migration
private Organization organization;
@OneToMany(mappedBy = "team", cascade = CascadeType.ALL, orphanRemoval = true)
private Set<User> users = new HashSet<>();

Some files were not shown because too many files have changed in this diff Show More