mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2026-09-03 13:20:08 +03:00
Compare commits
123
Commits
openapi
...
LoginV2Test
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
24cabb7070 | ||
|
|
a07f2cbe05 | ||
|
|
03a311e78d | ||
|
|
2de89fb173 | ||
|
|
db815802f7 | ||
|
|
129e4d00e9 | ||
|
|
8149cb157c | ||
|
|
22db7bec76 | ||
|
|
ecf30d1028 | ||
|
|
0ea4410dd3 | ||
|
|
8eeb4c148c | ||
|
|
adf6feea27 | ||
|
|
299e0b2475 | ||
|
|
768ece6921 | ||
|
|
20245c67e0 | ||
|
|
c5328d2aee | ||
|
|
17d9993a53 | ||
|
|
af5a9d1ae1 | ||
|
|
af69256abb | ||
|
|
7c49379a70 | ||
|
|
c1082d9e42 | ||
|
|
507ad1dc61 | ||
|
|
b45d3a43d4 | ||
|
|
7735ea7f48 | ||
|
|
7e3321ee16 | ||
|
|
9861332040 | ||
|
|
f4e4831c0d | ||
|
|
7dedf45b5d | ||
|
|
23aa43bd78 | ||
|
|
2b7671dc63 | ||
|
|
2947abd2fd | ||
|
|
90f0c5826a | ||
|
|
24a9104ebf | ||
|
|
03f86fc59c | ||
|
|
9c9acbfb5b | ||
|
|
8881f19b03 | ||
|
|
8802daf67f | ||
|
|
64a471c3a9 | ||
|
|
fc89ee2c78 | ||
|
|
a6df5e4246 | ||
|
|
989052b099 | ||
|
|
dbe84f7e96 | ||
|
|
8989bdb211 | ||
|
|
1bd166f6fb | ||
|
|
75bd154a82 | ||
|
|
1ce813bcd8 | ||
|
|
3b82602744 | ||
|
|
a9a41b3877 | ||
|
|
5f7e578ff8 | ||
|
|
e4d480c7b3 | ||
|
|
4e5f595951 | ||
|
|
088466b742 | ||
|
|
fd072cccba | ||
|
|
67d3f28f53 | ||
|
|
cdb3ffff78 | ||
|
|
9adb02fcfb | ||
|
|
b8d582a1e3 | ||
|
|
0549c5b191 | ||
|
|
0742364a03 | ||
|
|
9af09c6ac2 | ||
|
|
117d906be3 | ||
|
|
33549c2d67 | ||
|
|
a35956d927 | ||
|
|
3fd52ed564 | ||
|
|
2f977ff250 | ||
|
|
922bbc9076 | ||
|
|
584e2ecee7 | ||
|
|
80e425926e | ||
|
|
58d53c7a6b | ||
|
|
89cda87490 | ||
|
|
af8e065e87 | ||
|
|
21210850ec | ||
|
|
50d745225c | ||
|
|
f446fcd50b | ||
|
|
618485576d | ||
|
|
e75213a206 | ||
|
|
248a14c571 | ||
|
|
62fb5025a9 | ||
|
|
3444aa6525 | ||
|
|
5786421616 | ||
|
|
0090d9f483 | ||
|
|
29916d85b1 | ||
|
|
ffe5b9577b | ||
|
|
2f90220b7b | ||
|
|
bfc679edc5 | ||
|
|
27dd11ba8a | ||
|
|
a51960b199 | ||
|
|
16e56e3208 | ||
|
|
25e9db2570 | ||
|
|
cbc5616a39 | ||
|
|
215bb86a8e | ||
|
|
3ebf75ae6f | ||
|
|
9c410865f9 | ||
|
|
868969192b | ||
|
|
d981968e0f | ||
|
|
ae508730c3 | ||
|
|
bf6a74210f | ||
|
|
e7995a0256 | ||
|
|
7fc850b138 | ||
|
|
ac3da9b7c2 | ||
|
|
5298cb1607 | ||
|
|
41efd80dd5 | ||
|
|
342345cbb9 | ||
|
|
6bf41a4cbe | ||
|
|
1e0949ff2b | ||
|
|
b1584151ba | ||
|
|
0c76bf2c8b | ||
|
|
b5d84db3c8 | ||
|
|
7dd79aa2a1 | ||
|
|
61eac67dc1 | ||
|
|
16f150a203 | ||
|
|
a3c4f1a305 | ||
|
|
0650a6a877 | ||
|
|
09f05ac8d0 | ||
|
|
5f862f55d9 | ||
|
|
15761ae743 | ||
|
|
d216811317 | ||
|
|
41c82b15da | ||
|
|
5f7a4e1664 | ||
|
|
b5e7995554 | ||
|
|
f789533c83 | ||
|
|
d669964975 | ||
|
|
b567f4b110 |
@@ -5,7 +5,12 @@
|
||||
"Bash(mkdir:*)",
|
||||
"Bash(./gradlew:*)",
|
||||
"Bash(grep:*)",
|
||||
"Bash(cat:*)"
|
||||
"Bash(cat:*)",
|
||||
"Bash(find:*)",
|
||||
"Bash(npm test)",
|
||||
"Bash(npm test:*)",
|
||||
"Bash(ls:*)",
|
||||
"Bash(npx tsc:*)"
|
||||
],
|
||||
"deny": []
|
||||
}
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@ indent_size = 2
|
||||
insert_final_newline = false
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.js]
|
||||
[{*.js,*.jsx,*.ts,*.tsx}]
|
||||
indent_size = 2
|
||||
|
||||
[*.css]
|
||||
|
||||
@@ -0,0 +1,505 @@
|
||||
name: Auto PR V2 Deployment
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, closed]
|
||||
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
check-pr:
|
||||
if: github.event.action != 'closed'
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
should_deploy: ${{ steps.check-conditions.outputs.should_deploy }}
|
||||
pr_number: ${{ github.event.number }}
|
||||
pr_repository: ${{ steps.get-pr-info.outputs.repository }}
|
||||
pr_ref: ${{ steps.get-pr-info.outputs.ref }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Check deployment conditions
|
||||
id: check-conditions
|
||||
env:
|
||||
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
|
||||
PR_BRANCH: ${{ github.event.pull_request.head.ref }}
|
||||
run: |
|
||||
echo "PR Title: $PR_TITLE"
|
||||
echo "PR Author: $PR_AUTHOR"
|
||||
echo "PR Branch: $PR_BRANCH"
|
||||
echo "PR Base Branch: ${{ github.event.pull_request.base.ref }}"
|
||||
|
||||
# Define authorized users
|
||||
authorized_users=(
|
||||
"Frooodle"
|
||||
"sf298"
|
||||
"Ludy87"
|
||||
"LaserKaspar"
|
||||
"sbplat"
|
||||
"reecebrowne"
|
||||
"DarioGii"
|
||||
"ConnorYoh"
|
||||
"EthanHealy01"
|
||||
"jbrunton96"
|
||||
)
|
||||
|
||||
# Check if author is in the authorized list
|
||||
is_authorized=false
|
||||
for user in "${authorized_users[@]}"; do
|
||||
if [[ "$PR_AUTHOR" == "$user" ]]; then
|
||||
is_authorized=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# If PR is targeting V2 and user is authorized, deploy unconditionally
|
||||
PR_BASE_BRANCH="${{ github.event.pull_request.base.ref }}"
|
||||
if [[ "$PR_BASE_BRANCH" == "V2" && "$is_authorized" == "true" ]]; then
|
||||
echo "✅ Deployment forced: PR targets V2 and author is authorized."
|
||||
echo "should_deploy=true" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Otherwise, continue with original keyword checks
|
||||
has_v2_keyword=false
|
||||
if [[ "$PR_TITLE" =~ [Vv]2 ]] || [[ "$PR_TITLE" =~ [Vv]ersion.?2 ]] || [[ "$PR_TITLE" =~ [Vv]ersion.?[Tt]wo ]]; then
|
||||
has_v2_keyword=true
|
||||
fi
|
||||
|
||||
has_branch_keyword=false
|
||||
if [[ "$PR_BRANCH" =~ [Vv]2 ]] || [[ "$PR_BRANCH" =~ [Rr]eact ]]; then
|
||||
has_branch_keyword=true
|
||||
fi
|
||||
|
||||
if [[ "$is_authorized" == "true" && ( "$has_v2_keyword" == "true" || "$has_branch_keyword" == "true" ) ]]; then
|
||||
echo "✅ Deployment conditions met"
|
||||
echo "should_deploy=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "❌ Deployment conditions not met"
|
||||
echo " - Authorized user: $is_authorized"
|
||||
echo " - Has V2 keyword in title: $has_v2_keyword"
|
||||
echo " - Has V2/React keyword in branch: $has_branch_keyword"
|
||||
echo "should_deploy=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Get PR repository and ref
|
||||
id: get-pr-info
|
||||
if: steps.check-conditions.outputs.should_deploy == 'true'
|
||||
run: |
|
||||
# For forks, use the full repository name, for internal PRs use the current repo
|
||||
if [[ "${{ github.event.pull_request.head.repo.fork }}" == "true" ]]; then
|
||||
repository="${{ github.event.pull_request.head.repo.full_name }}"
|
||||
else
|
||||
repository="${{ github.repository }}"
|
||||
fi
|
||||
|
||||
echo "repository=$repository" >> $GITHUB_OUTPUT
|
||||
echo "ref=${{ github.event.pull_request.head.ref }}" >> $GITHUB_OUTPUT
|
||||
|
||||
deploy-v2-pr:
|
||||
needs: check-pr
|
||||
runs-on: ubuntu-latest
|
||||
if: needs.check-pr.outputs.should_deploy == 'true'
|
||||
# Concurrency control - only one deployment per PR at a time
|
||||
concurrency:
|
||||
group: v2-deploy-pr-${{ needs.check-pr.outputs.pr_number }}
|
||||
cancel-in-progress: true
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout main repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
repository: ${{ github.repository }}
|
||||
ref: main
|
||||
|
||||
- name: Setup GitHub App Bot
|
||||
if: github.actor != 'dependabot[bot]'
|
||||
id: setup-bot
|
||||
uses: ./.github/actions/setup-bot
|
||||
continue-on-error: true
|
||||
with:
|
||||
app-id: ${{ secrets.GH_APP_ID }}
|
||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Add deployment started comment
|
||||
id: deployment-started
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
with:
|
||||
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||
script: |
|
||||
const { owner, repo } = context.repo;
|
||||
const prNumber = ${{ needs.check-pr.outputs.pr_number }};
|
||||
|
||||
// Delete previous V2 deployment comments to avoid clutter
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: prNumber,
|
||||
per_page: 100
|
||||
});
|
||||
|
||||
const v2Comments = comments.filter(comment =>
|
||||
comment.body.includes('🚀 **Auto-deploying V2 version**') ||
|
||||
comment.body.includes('## 🚀 V2 Auto-Deployment Complete!') ||
|
||||
comment.body.includes('❌ **V2 Auto-deployment failed**')
|
||||
);
|
||||
|
||||
for (const comment of v2Comments) {
|
||||
console.log(`Deleting old V2 comment: ${comment.id}`);
|
||||
await github.rest.issues.deleteComment({
|
||||
owner,
|
||||
repo,
|
||||
comment_id: comment.id
|
||||
});
|
||||
}
|
||||
|
||||
// Create new deployment started comment
|
||||
const { data: newComment } = await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: prNumber,
|
||||
body: `🚀 **Auto-deploying V2 version** for PR #${prNumber}...\n\n_This is an automated deployment triggered by V2/version2 keywords in the PR title or V2/React keywords in the branch name._\n\n⚠️ **Note:** If new commits are pushed during deployment, this build will be cancelled and replaced with the latest version.`
|
||||
});
|
||||
|
||||
return newComment.id;
|
||||
|
||||
- name: Checkout PR
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
repository: ${{ needs.check-pr.outputs.pr_repository }}
|
||||
ref: ${{ needs.check-pr.outputs.pr_ref }}
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
fetch-depth: 0 # Fetch full history for commit hash detection
|
||||
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||
|
||||
- name: Get version number
|
||||
id: versionNumber
|
||||
run: |
|
||||
VERSION=$(grep "^version =" build.gradle | awk -F'"' '{print $2}')
|
||||
echo "versionNumber=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_API }}
|
||||
|
||||
- name: Get commit hashes for frontend and backend
|
||||
id: commit-hashes
|
||||
run: |
|
||||
# Get last commit that touched the frontend folder, docker/frontend, or docker/compose
|
||||
FRONTEND_HASH=$(git log -1 --format="%H" -- frontend/ docker/frontend/ docker/compose/ 2>/dev/null || echo "")
|
||||
if [ -z "$FRONTEND_HASH" ]; then
|
||||
FRONTEND_HASH="no-frontend-changes"
|
||||
fi
|
||||
|
||||
# Get last commit that touched backend code, docker/backend, or docker/compose
|
||||
BACKEND_HASH=$(git log -1 --format="%H" -- app/ docker/backend/ docker/compose/ 2>/dev/null || echo "")
|
||||
if [ -z "$BACKEND_HASH" ]; then
|
||||
BACKEND_HASH="no-backend-changes"
|
||||
fi
|
||||
|
||||
echo "Frontend hash: $FRONTEND_HASH"
|
||||
echo "Backend hash: $BACKEND_HASH"
|
||||
|
||||
echo "frontend_hash=$FRONTEND_HASH" >> $GITHUB_OUTPUT
|
||||
echo "backend_hash=$BACKEND_HASH" >> $GITHUB_OUTPUT
|
||||
|
||||
# Short hashes for tags
|
||||
if [ "$FRONTEND_HASH" = "no-frontend-changes" ]; then
|
||||
echo "frontend_short=no-frontend" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "frontend_short=${FRONTEND_HASH:0:8}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
if [ "$BACKEND_HASH" = "no-backend-changes" ]; then
|
||||
echo "backend_short=no-backend" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "backend_short=${BACKEND_HASH:0:8}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Check if frontend image exists
|
||||
id: check-frontend
|
||||
run: |
|
||||
if docker manifest inspect ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-frontend-${{ steps.commit-hashes.outputs.frontend_short }} >/dev/null 2>&1; then
|
||||
echo "exists=true" >> $GITHUB_OUTPUT
|
||||
echo "Frontend image already exists, skipping build"
|
||||
else
|
||||
echo "exists=false" >> $GITHUB_OUTPUT
|
||||
echo "Frontend image needs to be built"
|
||||
fi
|
||||
|
||||
- name: Check if backend image exists
|
||||
id: check-backend
|
||||
run: |
|
||||
if docker manifest inspect ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-backend-${{ steps.commit-hashes.outputs.backend_short }} >/dev/null 2>&1; then
|
||||
echo "exists=true" >> $GITHUB_OUTPUT
|
||||
echo "Backend image already exists, skipping build"
|
||||
else
|
||||
echo "exists=false" >> $GITHUB_OUTPUT
|
||||
echo "Backend image needs to be built"
|
||||
fi
|
||||
|
||||
- name: Build and push V2 frontend image
|
||||
if: steps.check-frontend.outputs.exists == 'false'
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/frontend/Dockerfile
|
||||
push: true
|
||||
tags: ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-frontend-${{ steps.commit-hashes.outputs.frontend_short }}
|
||||
build-args: VERSION_TAG=v2-alpha
|
||||
platforms: linux/amd64
|
||||
|
||||
- name: Build and push V2 backend image
|
||||
if: steps.check-backend.outputs.exists == 'false'
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/backend/Dockerfile
|
||||
push: true
|
||||
tags: ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-backend-${{ steps.commit-hashes.outputs.backend_short }}
|
||||
build-args: VERSION_TAG=v2-alpha
|
||||
platforms: linux/amd64
|
||||
|
||||
- name: Set up SSH
|
||||
run: |
|
||||
mkdir -p ~/.ssh/
|
||||
echo "${{ secrets.VPS_SSH_KEY }}" > ../private.key
|
||||
sudo chmod 600 ../private.key
|
||||
|
||||
- name: Deploy V2 to VPS
|
||||
id: deploy
|
||||
run: |
|
||||
# Use same port strategy as regular PRs - just the PR number
|
||||
V2_PORT=${{ needs.check-pr.outputs.pr_number }}
|
||||
BACKEND_PORT=$((V2_PORT + 10000)) # Backend on higher port to avoid conflicts
|
||||
|
||||
# Create docker-compose for V2 with separate frontend and backend
|
||||
cat > docker-compose.yml << EOF
|
||||
version: '3.3'
|
||||
services:
|
||||
stirling-pdf-v2-backend:
|
||||
container_name: stirling-pdf-v2-backend-pr-${{ needs.check-pr.outputs.pr_number }}
|
||||
image: ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-backend-${{ steps.commit-hashes.outputs.backend_short }}
|
||||
ports:
|
||||
- "${BACKEND_PORT}:8080" # Backend API port
|
||||
volumes:
|
||||
- /stirling/V2-PR-${{ needs.check-pr.outputs.pr_number }}/data:/usr/share/tessdata:rw
|
||||
- /stirling/V2-PR-${{ needs.check-pr.outputs.pr_number }}/config:/configs:rw
|
||||
- /stirling/V2-PR-${{ needs.check-pr.outputs.pr_number }}/logs:/logs:rw
|
||||
environment:
|
||||
DISABLE_ADDITIONAL_FEATURES: "true"
|
||||
SECURITY_ENABLELOGIN: "false"
|
||||
SYSTEM_DEFAULTLOCALE: en-GB
|
||||
UI_APPNAME: "Stirling-PDF V2 PR#${{ needs.check-pr.outputs.pr_number }}"
|
||||
UI_HOMEDESCRIPTION: "V2 PR#${{ needs.check-pr.outputs.pr_number }} - Frontend/Backend Split Architecture"
|
||||
UI_APPNAMENAVBAR: "V2 PR#${{ needs.check-pr.outputs.pr_number }}"
|
||||
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"
|
||||
restart: on-failure:5
|
||||
|
||||
stirling-pdf-v2-frontend:
|
||||
container_name: stirling-pdf-v2-frontend-pr-${{ needs.check-pr.outputs.pr_number }}
|
||||
image: ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-frontend-${{ steps.commit-hashes.outputs.frontend_short }}
|
||||
ports:
|
||||
- "${V2_PORT}:80" # Frontend port (same as regular PRs)
|
||||
environment:
|
||||
VITE_API_BASE_URL: "http://${{ secrets.VPS_HOST }}:${BACKEND_PORT}"
|
||||
depends_on:
|
||||
- stirling-pdf-v2-backend
|
||||
restart: on-failure:5
|
||||
EOF
|
||||
|
||||
# Deploy to VPS
|
||||
scp -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null docker-compose.yml ${{ secrets.VPS_USERNAME }}@${{ secrets.VPS_HOST }}:/tmp/docker-compose-v2.yml
|
||||
|
||||
ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -T ${{ secrets.VPS_USERNAME }}@${{ secrets.VPS_HOST }} << ENDSSH
|
||||
# Create V2 PR-specific directories
|
||||
mkdir -p /stirling/V2-PR-${{ needs.check-pr.outputs.pr_number }}/{data,config,logs}
|
||||
|
||||
# Move docker-compose file to correct location
|
||||
mv /tmp/docker-compose-v2.yml /stirling/V2-PR-${{ needs.check-pr.outputs.pr_number }}/docker-compose.yml
|
||||
|
||||
# Stop any existing container and clean up
|
||||
cd /stirling/V2-PR-${{ needs.check-pr.outputs.pr_number }}
|
||||
docker-compose down --remove-orphans 2>/dev/null || true
|
||||
|
||||
# Start the new container
|
||||
docker-compose pull
|
||||
docker-compose up -d
|
||||
|
||||
# Clean up unused Docker resources to save space
|
||||
docker system prune -af --volumes || true
|
||||
|
||||
# Clean up old backend/frontend images (older than 2 weeks)
|
||||
docker image prune -af --filter "until=336h" --filter "label!=keep=true" || true
|
||||
ENDSSH
|
||||
|
||||
# Set port for output
|
||||
echo "v2_port=${V2_PORT}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Post V2 deployment URL to PR
|
||||
if: success()
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
with:
|
||||
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||
script: |
|
||||
const { owner, repo } = context.repo;
|
||||
const prNumber = ${{ needs.check-pr.outputs.pr_number }};
|
||||
const v2Port = ${{ steps.deploy.outputs.v2_port }};
|
||||
|
||||
// Delete the "deploying..." comment since we're posting the final result
|
||||
const deploymentStartedId = ${{ steps.deployment-started.outputs.result }};
|
||||
if (deploymentStartedId) {
|
||||
console.log(`Deleting deployment started comment: ${deploymentStartedId}`);
|
||||
try {
|
||||
await github.rest.issues.deleteComment({
|
||||
owner,
|
||||
repo,
|
||||
comment_id: deploymentStartedId
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(`Could not delete deployment started comment: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
const deploymentUrl = `http://${{ secrets.VPS_HOST }}:${v2Port}`;
|
||||
const httpsUrl = `https://${v2Port}.ssl.stirlingpdf.cloud`;
|
||||
|
||||
const commentBody = `## 🚀 V2 Auto-Deployment Complete!\n\n` +
|
||||
`Your V2 PR with the new frontend/backend split architecture has been deployed!\n\n` +
|
||||
`🔗 **Direct Test URL (non-SSL)** [${deploymentUrl}](${deploymentUrl})\n\n` +
|
||||
`🔐 **Secure HTTPS URL**: [${httpsUrl}](${httpsUrl})\n\n` +
|
||||
`_This deployment will be automatically cleaned up when the PR is closed._\n\n` +
|
||||
`🔄 **Auto-deployed** because PR title or branch name contains V2/version2/React keywords.`;
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: prNumber,
|
||||
body: commentBody
|
||||
});
|
||||
|
||||
cleanup-v2-deployment:
|
||||
if: github.event.action == 'closed'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Setup GitHub App Bot
|
||||
if: github.actor != 'dependabot[bot]'
|
||||
id: setup-bot
|
||||
uses: ./.github/actions/setup-bot
|
||||
continue-on-error: true
|
||||
with:
|
||||
app-id: ${{ secrets.GH_APP_ID }}
|
||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Clean up V2 deployment comments
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
with:
|
||||
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||
script: |
|
||||
const { owner, repo } = context.repo;
|
||||
const prNumber = ${{ github.event.pull_request.number }};
|
||||
|
||||
// Find and delete V2 deployment comments
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: prNumber
|
||||
});
|
||||
|
||||
const v2Comments = comments.filter(c =>
|
||||
c.body?.includes("## 🚀 V2 Auto-Deployment Complete!") &&
|
||||
c.user?.type === "Bot"
|
||||
);
|
||||
|
||||
for (const comment of v2Comments) {
|
||||
await github.rest.issues.deleteComment({
|
||||
owner,
|
||||
repo,
|
||||
comment_id: comment.id
|
||||
});
|
||||
console.log(`Deleted V2 deployment comment (ID: ${comment.id})`);
|
||||
}
|
||||
|
||||
- name: Set up SSH
|
||||
run: |
|
||||
mkdir -p ~/.ssh/
|
||||
echo "${{ secrets.VPS_SSH_KEY }}" > ../private.key
|
||||
sudo chmod 600 ../private.key
|
||||
|
||||
- name: Cleanup V2 deployment
|
||||
run: |
|
||||
ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -T ${{ secrets.VPS_USERNAME }}@${{ secrets.VPS_HOST }} << 'ENDSSH'
|
||||
if [ -d "/stirling/V2-PR-${{ github.event.pull_request.number }}" ]; then
|
||||
echo "Found V2 PR directory, proceeding with cleanup..."
|
||||
|
||||
# Stop and remove V2 containers
|
||||
cd /stirling/V2-PR-${{ github.event.pull_request.number }}
|
||||
docker-compose down || true
|
||||
|
||||
# Go back to root before removal
|
||||
cd /
|
||||
|
||||
# Remove V2 PR-specific directories
|
||||
rm -rf /stirling/V2-PR-${{ github.event.pull_request.number }}
|
||||
|
||||
# Clean up V2 containers by name (in case compose cleanup missed them)
|
||||
docker rm -f stirling-pdf-v2-frontend-pr-${{ github.event.pull_request.number }} || true
|
||||
docker rm -f stirling-pdf-v2-backend-pr-${{ github.event.pull_request.number }} || true
|
||||
|
||||
echo "V2 cleanup completed"
|
||||
else
|
||||
echo "V2 PR directory not found, nothing to clean up"
|
||||
fi
|
||||
|
||||
# Clean up old unused images (older than 2 weeks) but keep recent ones for reuse
|
||||
docker image prune -af --filter "until=336h" --filter "label!=keep=true" || true
|
||||
|
||||
# Note: We don't remove the commit-based images since they can be reused across PRs
|
||||
# Only remove PR-specific containers and directories
|
||||
ENDSSH
|
||||
|
||||
- name: Cleanup temporary files
|
||||
if: always()
|
||||
run: |
|
||||
rm -f ../private.key
|
||||
continue-on-error: true
|
||||
|
||||
@@ -29,6 +29,7 @@ jobs:
|
||||
github.event.comment.user.login == 'reecebrowne' ||
|
||||
github.event.comment.user.login == 'DarioGii' ||
|
||||
github.event.comment.user.login == 'EthanHealy01' ||
|
||||
github.event.comment.user.login == 'jbrunton96' ||
|
||||
github.event.comment.user.login == 'ConnorYoh'
|
||||
)
|
||||
outputs:
|
||||
|
||||
+55
-61
@@ -1,11 +1,9 @@
|
||||
name: Build and Test Workflow
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
# push:
|
||||
# branches: ["main"]
|
||||
pull_request:
|
||||
branches: ["main"]
|
||||
branches: ["main", "V2", "V2-gha"]
|
||||
workflow_dispatch:
|
||||
|
||||
# cancel in-progress jobs if a new job is triggered
|
||||
# This is useful to avoid running multiple builds for the same branch if a new commit is pushed
|
||||
@@ -27,60 +25,52 @@ jobs:
|
||||
name: detect what files changed
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 3
|
||||
# Map a step output to a job output
|
||||
outputs:
|
||||
build: ${{ steps.changes.outputs.build }}
|
||||
app: ${{ steps.changes.outputs.app }}
|
||||
project: ${{ steps.changes.outputs.project }}
|
||||
openapi: ${{ steps.changes.outputs.openapi }}
|
||||
steps:
|
||||
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Check for file changes
|
||||
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
uses: dorny/paths-filter@v3.0.2
|
||||
id: changes
|
||||
with:
|
||||
filters: ".github/config/.files.yaml"
|
||||
filters: .github/config/.files.yaml
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
security-events: write
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
jdk-version: [17, 21]
|
||||
spring-security: [true, false]
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up JDK ${{ matrix.jdk-version }}
|
||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
uses: actions/setup-java@v4.7.1
|
||||
with:
|
||||
java-version: ${{ matrix.jdk-version }}
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
|
||||
uses: gradle/actions/setup-gradle@v4.4.1
|
||||
with:
|
||||
gradle-version: 8.14
|
||||
|
||||
- name: Build with Gradle and spring security ${{ matrix.spring-security }}
|
||||
run: ./gradlew clean build
|
||||
run: ./gradlew clean build -PnoSpotless
|
||||
env:
|
||||
DISABLE_ADDITIONAL_FEATURES: ${{ matrix.spring-security }}
|
||||
|
||||
- name: Check Test Reports Exist
|
||||
id: check-reports
|
||||
if: always()
|
||||
run: |
|
||||
declare -a dirs=(
|
||||
@@ -91,69 +81,78 @@ jobs:
|
||||
"app/proprietary/build/reports/tests/"
|
||||
"app/proprietary/build/test-results/"
|
||||
)
|
||||
missing_reports=()
|
||||
for dir in "${dirs[@]}"; do
|
||||
if [ ! -d "$dir" ]; then
|
||||
missing_reports+=("$dir")
|
||||
echo "Missing $dir"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
if [ ${#missing_reports[@]} -gt 0 ]; then
|
||||
echo "ERROR: The following required test report directories are missing:"
|
||||
printf '%s\n' "${missing_reports[@]}"
|
||||
exit 1
|
||||
fi
|
||||
echo "All required test report directories are present"
|
||||
|
||||
- name: Upload Test Reports
|
||||
if: always()
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@v4.6.2
|
||||
with:
|
||||
name: test-reports-jdk-${{ matrix.jdk-version }}-spring-security-${{ matrix.spring-security }}
|
||||
path: |
|
||||
app/core/build/reports/tests/
|
||||
app/core/build/test-results/
|
||||
app/core/build/reports/problems/
|
||||
app/common/build/reports/tests/
|
||||
app/common/build/test-results/
|
||||
app/common/build/reports/problems/
|
||||
app/proprietary/build/reports/tests/
|
||||
app/proprietary/build/test-results/
|
||||
app/proprietary/build/reports/problems/
|
||||
app/**/build/reports/tests/
|
||||
app/**/build/test-results/
|
||||
app/**/build/reports/problems/
|
||||
build/reports/problems/
|
||||
retention-days: 3
|
||||
if-no-files-found: warn
|
||||
|
||||
check-generateOpenApiDocs:
|
||||
if: needs.files-changed.outputs.openapi == 'true'
|
||||
needs: [files-changed, build]
|
||||
needs: [files-changed]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
uses: step-security/harden-runner@v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- uses: actions/checkout@v4.2.2
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
uses: actions/setup-java@v4.7.1
|
||||
with:
|
||||
java-version: "17"
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
|
||||
|
||||
- uses: gradle/actions/setup-gradle@v4.4.1
|
||||
- name: Generate OpenAPI documentation
|
||||
run: ./gradlew :stirling-pdf:generateOpenApiDocs
|
||||
|
||||
- name: Upload OpenAPI Documentation
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@v4.6.2
|
||||
with:
|
||||
name: openapi-docs
|
||||
path: ./SwaggerDoc.json
|
||||
|
||||
frontend-validation:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4.1.0
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
- name: Install frontend dependencies
|
||||
run: cd frontend && npm ci
|
||||
- name: Build frontend
|
||||
run: cd frontend && npm run build
|
||||
- name: Run frontend tests
|
||||
run: cd frontend && npm run test -- --run
|
||||
- name: Upload frontend build artifacts
|
||||
uses: actions/upload-artifact@v4.6.2
|
||||
with:
|
||||
name: frontend-build
|
||||
path: frontend/dist/
|
||||
retention-days: 3
|
||||
|
||||
check-licence:
|
||||
if: needs.files-changed.outputs.build == 'true'
|
||||
needs: [files-changed, build]
|
||||
@@ -163,26 +162,21 @@ jobs:
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
uses: actions/setup-java@v4.7.1
|
||||
with:
|
||||
java-version: "17"
|
||||
distribution: "temurin"
|
||||
|
||||
- name: check the licenses for compatibility
|
||||
run: ./gradlew clean checkLicense
|
||||
|
||||
- name: FAILED - check the licenses for compatibility
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@v4.6.2
|
||||
with:
|
||||
name: dependencies-without-allowed-license.json
|
||||
path: |
|
||||
build/reports/dependency-license/dependencies-without-allowed-license.json
|
||||
path: build/reports/dependency-license/dependencies-without-allowed-license.json
|
||||
retention-days: 3
|
||||
|
||||
docker-compose-tests:
|
||||
@@ -291,7 +285,7 @@ jobs:
|
||||
with:
|
||||
builder: ${{ steps.buildx.outputs.name }}
|
||||
context: .
|
||||
file: ./${{ matrix.docker-rev }}
|
||||
file: ./docker/backend/${{ matrix.docker-rev }}
|
||||
push: false
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
name: Auto V2 Deploy on Push
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- V2
|
||||
- deploy-on-v2-commit
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
deploy-v2-on-push:
|
||||
runs-on: ubuntu-latest
|
||||
concurrency:
|
||||
group: deploy-v2-push-V2
|
||||
cancel-in-progress: true
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Get commit hashes for frontend and backend
|
||||
id: commit-hashes
|
||||
run: |
|
||||
# Get last commit that touched the frontend folder, docker/frontend, or docker/compose
|
||||
FRONTEND_HASH=$(git log -1 --format="%H" -- frontend/ docker/frontend/ docker/compose/ 2>/dev/null || echo "")
|
||||
if [ -z "$FRONTEND_HASH" ]; then
|
||||
FRONTEND_HASH="no-frontend-changes"
|
||||
fi
|
||||
|
||||
# Get last commit that touched backend code, docker/backend, or docker/compose
|
||||
BACKEND_HASH=$(git log -1 --format="%H" -- app/ docker/backend/ docker/compose/ 2>/dev/null || echo "")
|
||||
if [ -z "$BACKEND_HASH" ]; then
|
||||
BACKEND_HASH="no-backend-changes"
|
||||
fi
|
||||
|
||||
echo "Frontend hash: $FRONTEND_HASH"
|
||||
echo "Backend hash: $BACKEND_HASH"
|
||||
|
||||
echo "frontend_hash=$FRONTEND_HASH" >> $GITHUB_OUTPUT
|
||||
echo "backend_hash=$BACKEND_HASH" >> $GITHUB_OUTPUT
|
||||
|
||||
# Short hashes for tags
|
||||
if [ "$FRONTEND_HASH" = "no-frontend-changes" ]; then
|
||||
echo "frontend_short=no-frontend" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "frontend_short=${FRONTEND_HASH:0:8}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
if [ "$BACKEND_HASH" = "no-backend-changes" ]; then
|
||||
echo "backend_short=no-backend" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "backend_short=${BACKEND_HASH:0:8}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Check if frontend image exists
|
||||
id: check-frontend
|
||||
run: |
|
||||
if docker manifest inspect ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-frontend-${{ steps.commit-hashes.outputs.frontend_short }} >/dev/null 2>&1; then
|
||||
echo "exists=true" >> $GITHUB_OUTPUT
|
||||
echo "Frontend image already exists, skipping build"
|
||||
else
|
||||
echo "exists=false" >> $GITHUB_OUTPUT
|
||||
echo "Frontend image needs to be built"
|
||||
fi
|
||||
|
||||
- name: Check if backend image exists
|
||||
id: check-backend
|
||||
run: |
|
||||
if docker manifest inspect ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-backend-${{ steps.commit-hashes.outputs.backend_short }} >/dev/null 2>&1; then
|
||||
echo "exists=true" >> $GITHUB_OUTPUT
|
||||
echo "Backend image already exists, skipping build"
|
||||
else
|
||||
echo "exists=false" >> $GITHUB_OUTPUT
|
||||
echo "Backend image needs to be built"
|
||||
fi
|
||||
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_API }}
|
||||
|
||||
- name: Build and push frontend image
|
||||
if: steps.check-frontend.outputs.exists == 'false'
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/frontend/Dockerfile
|
||||
push: true
|
||||
tags: |
|
||||
${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-frontend-${{ steps.commit-hashes.outputs.frontend_short }}
|
||||
${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-frontend-latest
|
||||
build-args: VERSION_TAG=v2-alpha
|
||||
platforms: linux/amd64
|
||||
|
||||
- name: Build and push backend image
|
||||
if: steps.check-backend.outputs.exists == 'false'
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/backend/Dockerfile
|
||||
push: true
|
||||
tags: |
|
||||
${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-backend-${{ steps.commit-hashes.outputs.backend_short }}
|
||||
${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-backend-latest
|
||||
build-args: VERSION_TAG=v2-alpha
|
||||
platforms: linux/amd64
|
||||
|
||||
|
||||
- name: Set up SSH
|
||||
run: |
|
||||
mkdir -p ~/.ssh/
|
||||
echo "${{ secrets.VPS_SSH_KEY }}" > ../private.key
|
||||
chmod 600 ../private.key
|
||||
|
||||
|
||||
- name: Deploy to VPS on port 3000
|
||||
run: |
|
||||
export UNIQUE_NAME=docker-compose-v2-$GITHUB_RUN_ID.yml
|
||||
|
||||
cat > $UNIQUE_NAME << EOF
|
||||
version: '3.3'
|
||||
services:
|
||||
backend:
|
||||
container_name: stirling-v2-backend
|
||||
image: ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-backend-${{ steps.commit-hashes.outputs.backend_short }}
|
||||
ports:
|
||||
- "13000:8080"
|
||||
volumes:
|
||||
- /stirling/V2/data:/usr/share/tessdata:rw
|
||||
- /stirling/V2/config:/configs:rw
|
||||
- /stirling/V2/logs:/logs:rw
|
||||
environment:
|
||||
DISABLE_ADDITIONAL_FEATURES: "true"
|
||||
SECURITY_ENABLELOGIN: "false"
|
||||
SYSTEM_DEFAULTLOCALE: en-GB
|
||||
UI_APPNAME: "Stirling-PDF V2"
|
||||
UI_HOMEDESCRIPTION: "V2 Frontend/Backend Split"
|
||||
UI_APPNAMENAVBAR: "V2 Deployment"
|
||||
SYSTEM_MAXFILESIZE: "100"
|
||||
METRICS_ENABLED: "true"
|
||||
SYSTEM_GOOGLEVISIBILITY: "false"
|
||||
SWAGGER_SERVER_URL: "https://demo.stirlingpdf.cloud"
|
||||
baseUrl: "https://demo.stirlingpdf.cloud"
|
||||
restart: on-failure:5
|
||||
|
||||
frontend:
|
||||
container_name: stirling-v2-frontend
|
||||
image: ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-frontend-${{ steps.commit-hashes.outputs.frontend_short }}
|
||||
ports:
|
||||
- "3000:80"
|
||||
environment:
|
||||
VITE_API_BASE_URL: "http://${{ secrets.VPS_HOST }}:13000"
|
||||
depends_on:
|
||||
- backend
|
||||
restart: on-failure:5
|
||||
EOF
|
||||
|
||||
# Copy to remote with unique name
|
||||
scp -i ../private.key -o StrictHostKeyChecking=no $UNIQUE_NAME ${{ secrets.VPS_USERNAME }}@${{ secrets.VPS_HOST }}:/tmp/$UNIQUE_NAME
|
||||
|
||||
# SSH and rename/move atomically to avoid interference
|
||||
ssh -i ../private.key -o StrictHostKeyChecking=no ${{ secrets.VPS_USERNAME }}@${{ secrets.VPS_HOST }} << ENDSSH
|
||||
mkdir -p /stirling/V2/{data,config,logs}
|
||||
mv /tmp/$UNIQUE_NAME /stirling/V2/docker-compose.yml
|
||||
cd /stirling/V2
|
||||
docker-compose down || true
|
||||
docker-compose pull
|
||||
docker-compose up -d
|
||||
docker system prune -af --volumes || true
|
||||
docker image prune -af --filter "until=336h" --filter "label!=keep=true" || true
|
||||
ENDSSH
|
||||
|
||||
- name: Cleanup temporary files
|
||||
if: always()
|
||||
run: |
|
||||
rm -f ../private.key
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
name: Frontend License Report Workflow
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- V2
|
||||
paths:
|
||||
- "frontend/package.json"
|
||||
- "frontend/package-lock.json"
|
||||
- "frontend/scripts/generate-licenses.js"
|
||||
pull_request:
|
||||
branches:
|
||||
- V2
|
||||
paths:
|
||||
- "frontend/package.json"
|
||||
- "frontend/package-lock.json"
|
||||
- "frontend/scripts/generate-licenses.js"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
generate-frontend-license-report:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
repository-projects: write # Required for enabling automerge
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Check out code
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup GitHub App Bot
|
||||
id: setup-bot
|
||||
uses: ./.github/actions/setup-bot
|
||||
with:
|
||||
app-id: ${{ secrets.GH_APP_ID }}
|
||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
|
||||
with:
|
||||
node-version: '18'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
|
||||
- name: Install frontend dependencies
|
||||
working-directory: frontend
|
||||
run: npm ci
|
||||
|
||||
- name: Generate frontend license report
|
||||
working-directory: frontend
|
||||
run: npm run generate-licenses
|
||||
|
||||
- name: Check for license warnings
|
||||
run: |
|
||||
if [ -f "frontend/src/assets/license-warnings.json" ]; then
|
||||
echo "LICENSE_WARNINGS_EXIST=true" >> $GITHUB_ENV
|
||||
else
|
||||
echo "LICENSE_WARNINGS_EXIST=false" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
# PR Event: Check licenses and comment on PR
|
||||
- name: Delete previous license check comments
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
with:
|
||||
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||
script: |
|
||||
const { owner, repo } = context.repo;
|
||||
const prNumber = context.issue.number;
|
||||
|
||||
// Get all comments on the PR
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: prNumber,
|
||||
per_page: 100
|
||||
});
|
||||
|
||||
// Filter for license check comments
|
||||
const licenseComments = comments.filter(comment =>
|
||||
comment.body.includes('## ✅ Frontend License Check Passed') ||
|
||||
comment.body.includes('## ❌ Frontend License Check Failed')
|
||||
);
|
||||
|
||||
// Delete old license check comments
|
||||
for (const comment of licenseComments) {
|
||||
console.log(`Deleting old license check comment: ${comment.id}`);
|
||||
await github.rest.issues.deleteComment({
|
||||
owner,
|
||||
repo,
|
||||
comment_id: comment.id
|
||||
});
|
||||
}
|
||||
|
||||
- name: Comment on PR - License Check Results
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
with:
|
||||
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||
script: |
|
||||
const { owner, repo } = context.repo;
|
||||
const prNumber = context.issue.number;
|
||||
const hasWarnings = process.env.LICENSE_WARNINGS_EXIST === 'true';
|
||||
|
||||
let commentBody;
|
||||
|
||||
if (hasWarnings) {
|
||||
// Read warnings file to get specific issues
|
||||
const fs = require('fs');
|
||||
let warningDetails = '';
|
||||
try {
|
||||
const warnings = JSON.parse(fs.readFileSync('frontend/src/assets/license-warnings.json', 'utf8'));
|
||||
warningDetails = warnings.warnings.map(w => `- ${w.message}`).join('\n');
|
||||
} catch (e) {
|
||||
warningDetails = 'Unable to read warning details';
|
||||
}
|
||||
|
||||
commentBody = `## ❌ Frontend License Check Failed
|
||||
|
||||
The frontend license check has detected compatibility warnings that require review:
|
||||
|
||||
${warningDetails}
|
||||
|
||||
**Action Required:** Please review these licenses to ensure they are acceptable for your use case before merging.
|
||||
|
||||
_This check will fail the PR until license issues are resolved._`;
|
||||
} else {
|
||||
commentBody = `## ✅ Frontend License Check Passed
|
||||
|
||||
All frontend licenses have been validated and no compatibility warnings were detected.
|
||||
|
||||
The frontend license report has been updated successfully.`;
|
||||
}
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: prNumber,
|
||||
body: commentBody
|
||||
});
|
||||
|
||||
- name: Fail workflow if license warnings exist (PR only)
|
||||
if: github.event_name == 'pull_request' && env.LICENSE_WARNINGS_EXIST == 'true'
|
||||
run: |
|
||||
echo "❌ License warnings detected. Failing the workflow."
|
||||
exit 1
|
||||
|
||||
# Push Event: Commit license files and create PR
|
||||
- name: Commit changes (Push only)
|
||||
if: github.event_name == 'push'
|
||||
run: |
|
||||
git add frontend/src/assets/3rdPartyLicenses.json
|
||||
# Note: Do NOT commit license-warnings.json - it's only for PR review
|
||||
git diff --staged --quiet || echo "CHANGES_DETECTED=true" >> $GITHUB_ENV
|
||||
|
||||
- name: Prepare PR body (Push only)
|
||||
if: github.event_name == 'push'
|
||||
run: |
|
||||
PR_BODY="Auto-generated by ${{ steps.setup-bot.outputs.app-slug }}[bot]
|
||||
|
||||
This PR updates the frontend license report based on changes to package.json dependencies."
|
||||
|
||||
if [ "${{ env.LICENSE_WARNINGS_EXIST }}" = "true" ]; then
|
||||
PR_BODY="$PR_BODY
|
||||
|
||||
## ⚠️ License Compatibility Warnings
|
||||
|
||||
The following licenses may require review for corporate compatibility:
|
||||
|
||||
$(cat frontend/src/assets/license-warnings.json | jq -r '.warnings[].message')
|
||||
|
||||
Please review these licenses to ensure they are acceptable for your use case."
|
||||
fi
|
||||
|
||||
echo "PR_BODY<<EOF" >> $GITHUB_ENV
|
||||
echo "$PR_BODY" >> $GITHUB_ENV
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
|
||||
- name: Create Pull Request (Push only)
|
||||
id: cpr
|
||||
if: github.event_name == 'push' && env.CHANGES_DETECTED == 'true'
|
||||
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
|
||||
with:
|
||||
token: ${{ steps.setup-bot.outputs.token }}
|
||||
commit-message: "Update Frontend 3rd Party Licenses"
|
||||
committer: ${{ steps.setup-bot.outputs.committer }}
|
||||
author: ${{ steps.setup-bot.outputs.committer }}
|
||||
signoff: true
|
||||
branch: update-frontend-3rd-party-licenses
|
||||
base: V2
|
||||
title: "Update Frontend 3rd Party Licenses"
|
||||
body: ${{ env.PR_BODY }}
|
||||
labels: Licenses,github-actions,frontend
|
||||
draft: false
|
||||
delete-branch: true
|
||||
sign-commits: true
|
||||
|
||||
- name: Enable Pull Request Automerge (Push only)
|
||||
if: github.event_name == 'push' && steps.cpr.outputs.pull-request-operation == 'created' && env.LICENSE_WARNINGS_EXIST == 'false'
|
||||
run: gh pr merge --squash --auto "${{ steps.cpr.outputs.pull-request-number }}"
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.setup-bot.outputs.token }}
|
||||
|
||||
- name: Add review required label (Push only)
|
||||
if: github.event_name == 'push' && steps.cpr.outputs.pull-request-operation == 'created' && env.LICENSE_WARNINGS_EXIST == 'true'
|
||||
run: gh pr edit "${{ steps.cpr.outputs.pull-request-number }}" --add-label "license-review-required"
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.setup-bot.outputs.token }}
|
||||
@@ -27,6 +27,7 @@ clientWebUI/
|
||||
!cucumber/exampleFiles/
|
||||
!cucumber/exampleFiles/example_html.zip
|
||||
exampleYmlFiles/stirling/
|
||||
/stirling/
|
||||
/testing/file_snapshots
|
||||
SwaggerDoc.json
|
||||
|
||||
@@ -196,6 +197,8 @@ id_ed25519.pub
|
||||
.pytest_cache
|
||||
.ipynb_checkpoints
|
||||
|
||||
|
||||
|
||||
**/jcef-bundle/
|
||||
|
||||
# node_modules
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Common Development Commands
|
||||
|
||||
### Build and Test
|
||||
- **Build project**: `./gradlew clean build`
|
||||
- **Run locally**: `./gradlew bootRun`
|
||||
- **Full test suite**: `./test.sh` (builds all Docker variants and runs comprehensive tests)
|
||||
- **Code formatting**: `./gradlew spotlessApply` (runs automatically before compilation)
|
||||
|
||||
### Docker Development
|
||||
- **Build ultra-lite**: `docker build -t stirlingtools/stirling-pdf:latest-ultra-lite -f ./Dockerfile.ultra-lite .`
|
||||
- **Build standard**: `docker build -t stirlingtools/stirling-pdf:latest -f ./Dockerfile .`
|
||||
- **Build fat version**: `docker build -t stirlingtools/stirling-pdf:latest-fat -f ./Dockerfile.fat .`
|
||||
- **Example compose files**: Located in `exampleYmlFiles/` directory
|
||||
|
||||
### Security Mode Development
|
||||
Set `DOCKER_ENABLE_SECURITY=true` environment variable to enable security features during development. This is required for testing the full version locally.
|
||||
|
||||
### Frontend Development
|
||||
- **Frontend dev server**: `cd frontend && npm run dev` (requires backend on localhost:8080)
|
||||
- **Tech Stack**: Vite + React + TypeScript + Mantine UI + TailwindCSS
|
||||
- **Proxy Configuration**: Vite proxies `/api/*` calls to backend (localhost:8080)
|
||||
- **Build Process**: DO NOT run build scripts manually - builds are handled by CI/CD pipelines
|
||||
- **Package Installation**: DO NOT run npm install commands - package management handled separately
|
||||
- **Deployment Options**:
|
||||
- **Desktop App**: `npm run tauri-build` (native desktop application)
|
||||
- **Web Server**: `npm run build` then serve dist/ folder
|
||||
- **Development**: `npm run tauri-dev` for desktop dev mode
|
||||
|
||||
#### Multi-Tool Workflow Architecture
|
||||
Frontend designed for **stateful document processing**:
|
||||
- Users upload PDFs once, then chain tools (split → merge → compress → view)
|
||||
- File state and processing results persist across tool switches
|
||||
- No file reloading between tools - performance critical for large PDFs (up to 100GB+)
|
||||
|
||||
#### FileContext - Central State Management
|
||||
**Location**: `src/contexts/FileContext.tsx`
|
||||
- **Active files**: Currently loaded PDFs and their variants
|
||||
- **Tool navigation**: Current mode (viewer/pageEditor/fileEditor/toolName)
|
||||
- **Memory management**: PDF document cleanup, blob URL lifecycle, Web Worker management
|
||||
- **IndexedDB persistence**: File storage with thumbnail caching
|
||||
- **Preview system**: Tools can preview results (e.g., Split → Viewer → back to Split) without context pollution
|
||||
|
||||
**Critical**: All file operations go through FileContext. Don't bypass with direct file handling.
|
||||
|
||||
#### Processing Services
|
||||
- **enhancedPDFProcessingService**: Background PDF parsing and manipulation
|
||||
- **thumbnailGenerationService**: Web Worker-based with main-thread fallback
|
||||
- **fileStorage**: IndexedDB with LRU cache management
|
||||
|
||||
#### Memory Management Strategy
|
||||
**Why manual cleanup exists**: Large PDFs (up to 100GB+) through multiple tools accumulate:
|
||||
- PDF.js documents that need explicit .destroy() calls
|
||||
- Blob URLs from tool outputs that need revocation
|
||||
- Web Workers that need termination
|
||||
Without cleanup: browser crashes with memory leaks.
|
||||
|
||||
#### Tool Development
|
||||
|
||||
**Architecture**: Modular hook-based system with clear separation of concerns:
|
||||
|
||||
- **useToolOperation** (`frontend/src/hooks/tools/shared/useToolOperation.ts`): Main orchestrator hook
|
||||
- Coordinates all tool operations with consistent interface
|
||||
- Integrates with FileContext for operation tracking
|
||||
- Handles validation, error handling, and UI state management
|
||||
|
||||
- **Supporting Hooks**:
|
||||
- **useToolState**: UI state management (loading, progress, error, files)
|
||||
- **useToolApiCalls**: HTTP requests and file processing
|
||||
- **useToolResources**: Blob URLs, thumbnails, ZIP downloads
|
||||
|
||||
- **Utilities**:
|
||||
- **toolErrorHandler**: Standardized error extraction and i18n support
|
||||
- **toolResponseProcessor**: API response handling (single/zip/custom)
|
||||
- **toolOperationTracker**: FileContext integration utilities
|
||||
|
||||
**Three Tool Patterns**:
|
||||
|
||||
**Pattern 1: Single-File Tools** (Individual processing)
|
||||
- Backend processes one file per API call
|
||||
- Set `multiFileEndpoint: false`
|
||||
- Examples: Compress, Rotate
|
||||
```typescript
|
||||
return useToolOperation({
|
||||
operationType: 'compress',
|
||||
endpoint: '/api/v1/misc/compress-pdf',
|
||||
buildFormData: (params, file: File) => { /* single file */ },
|
||||
multiFileEndpoint: false,
|
||||
filePrefix: 'compressed_'
|
||||
});
|
||||
```
|
||||
|
||||
**Pattern 2: Multi-File Tools** (Batch processing)
|
||||
- Backend accepts `MultipartFile[]` arrays in single API call
|
||||
- Set `multiFileEndpoint: true`
|
||||
- Examples: Split, Merge, Overlay
|
||||
```typescript
|
||||
return useToolOperation({
|
||||
operationType: 'split',
|
||||
endpoint: '/api/v1/general/split-pages',
|
||||
buildFormData: (params, files: File[]) => { /* all files */ },
|
||||
multiFileEndpoint: true,
|
||||
filePrefix: 'split_'
|
||||
});
|
||||
```
|
||||
|
||||
**Pattern 3: Complex Tools** (Custom processing)
|
||||
- Tools with complex routing logic or non-standard processing
|
||||
- Provide `customProcessor` for full control
|
||||
- Examples: Convert, OCR
|
||||
```typescript
|
||||
return useToolOperation({
|
||||
operationType: 'convert',
|
||||
customProcessor: async (params, files) => { /* custom logic */ },
|
||||
filePrefix: 'converted_'
|
||||
});
|
||||
```
|
||||
|
||||
**Benefits**:
|
||||
- **No Timeouts**: Operations run until completion (supports 100GB+ files)
|
||||
- **Consistent**: All tools follow same pattern and interface
|
||||
- **Maintainable**: Single responsibility hooks, easy to test and modify
|
||||
- **i18n Ready**: Built-in internationalization support
|
||||
- **Type Safe**: Full TypeScript support with generic interfaces
|
||||
- **Memory Safe**: Automatic resource cleanup and blob URL management
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
### Project Structure
|
||||
- **Backend**: Spring Boot application with Thymeleaf templating
|
||||
- **Frontend**: React-based SPA in `/frontend` directory (Thymeleaf templates fully replaced)
|
||||
- **File Storage**: IndexedDB for client-side file persistence and thumbnails
|
||||
- **Internationalization**: JSON-based translations (converted from backend .properties)
|
||||
- **PDF Processing**: PDFBox for core PDF operations, LibreOffice for conversions, PDF.js for client-side rendering
|
||||
- **Security**: Spring Security with optional authentication (controlled by `DOCKER_ENABLE_SECURITY`)
|
||||
- **Configuration**: YAML-based configuration with environment variable overrides
|
||||
|
||||
### Controller Architecture
|
||||
- **API Controllers** (`src/main/java/.../controller/api/`): REST endpoints for PDF operations
|
||||
- Organized by function: converters, security, misc, pipeline
|
||||
- Follow pattern: `@RestController` + `@RequestMapping("/api/v1/...")`
|
||||
- **Web Controllers** (`src/main/java/.../controller/web/`): Serve Thymeleaf templates
|
||||
- Pattern: `@Controller` + return template names
|
||||
|
||||
### Key Components
|
||||
- **SPDFApplication.java**: Main application class with desktop UI and browser launching logic
|
||||
- **ConfigInitializer**: Handles runtime configuration and settings files
|
||||
- **Pipeline System**: Automated PDF processing workflows via `PipelineController`
|
||||
- **Security Layer**: Authentication, authorization, and user management (when enabled)
|
||||
|
||||
### Component Architecture
|
||||
- **React Components**: Located in `frontend/src/components/` and `frontend/src/tools/`
|
||||
- **Static Assets**: CSS, JS, and resources in `src/main/resources/static/` (legacy) + `frontend/public/` (modern)
|
||||
- **Internationalization**:
|
||||
- Backend: `messages_*.properties` files
|
||||
- Frontend: JSON files in `frontend/public/locales/` (converted from .properties)
|
||||
- Conversion Script: `scripts/convert_properties_to_json.py`
|
||||
|
||||
### Configuration Modes
|
||||
- **Ultra-lite**: Basic PDF operations only
|
||||
- **Standard**: Full feature set
|
||||
- **Fat**: Pre-downloaded dependencies for air-gapped environments
|
||||
- **Security Mode**: Adds authentication, user management, and enterprise features
|
||||
|
||||
### Testing Strategy
|
||||
- **Integration Tests**: Cucumber tests in `testing/cucumber/`
|
||||
- **Docker Testing**: `test.sh` validates all Docker variants
|
||||
- **Manual Testing**: No unit tests currently - relies on UI and API testing
|
||||
|
||||
## Development Workflow
|
||||
|
||||
1. **Local Development**:
|
||||
- Backend: `./gradlew bootRun` (runs on localhost:8080)
|
||||
- Frontend: `cd frontend && npm run dev` (runs on localhost:5173, proxies to backend)
|
||||
2. **Docker Testing**: Use `./test.sh` before submitting PRs
|
||||
3. **Code Style**: Spotless enforces Google Java Format automatically
|
||||
4. **Translations**:
|
||||
- Backend: Use helper scripts in `/scripts` for multi-language updates
|
||||
- Frontend: Update JSON files in `frontend/public/locales/` or use conversion script
|
||||
5. **Documentation**: API docs auto-generated and available at `/swagger-ui/index.html`
|
||||
|
||||
## Frontend Architecture Status
|
||||
|
||||
- **Core Status**: React SPA architecture complete with multi-tool workflow support
|
||||
- **State Management**: FileContext handles all file operations and tool navigation
|
||||
- **File Processing**: Production-ready with memory management for large PDF workflows (up to 100GB+)
|
||||
- **Tool Integration**: Modular hook architecture with `useToolOperation` orchestrator
|
||||
- Individual hooks: `useToolState`, `useToolApiCalls`, `useToolResources`
|
||||
- Utilities: `toolErrorHandler`, `toolResponseProcessor`, `toolOperationTracker`
|
||||
- Pattern: Each tool creates focused operation hook, UI consumes state/actions
|
||||
- **Preview System**: Tool results can be previewed without polluting file context (Split tool example)
|
||||
- **Performance**: Web Worker thumbnails, IndexedDB persistence, background processing
|
||||
|
||||
## Important Notes
|
||||
|
||||
- **Java Version**: Minimum JDK 17, supports and recommends JDK 21
|
||||
- **Lombok**: Used extensively - ensure IDE plugin is installed
|
||||
- **Desktop Mode**: Set `STIRLING_PDF_DESKTOP_UI=true` for desktop application mode
|
||||
- **File Persistence**:
|
||||
- **Backend**: Designed to be stateless - files are processed in memory/temp locations only
|
||||
- **Frontend**: Uses IndexedDB for client-side file storage and caching (with thumbnails)
|
||||
- **Security**: When `DOCKER_ENABLE_SECURITY=false`, security-related classes are excluded from compilation
|
||||
- **FileContext**: All file operations MUST go through FileContext - never bypass with direct File handling
|
||||
- **Memory Management**: Manual cleanup required for PDF.js documents and blob URLs - don't remove cleanup code
|
||||
- **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)
|
||||
|
||||
## Communication Style
|
||||
- Be direct and to the point
|
||||
- No apologies or conversational filler
|
||||
- Answer questions directly without preamble
|
||||
- Explain reasoning concisely when asked
|
||||
- Avoid unnecessary elaboration
|
||||
|
||||
## Decision Making
|
||||
- Ask clarifying questions before making assumptions
|
||||
- Stop and ask when uncertain about project-specific details
|
||||
- Confirm approach before making structural changes
|
||||
- Request guidance on preferences (cross-platform vs specific tools, etc.)
|
||||
- Verify understanding of requirements before proceeding
|
||||
@@ -0,0 +1,692 @@
|
||||
# Stirling-PDF Developer Guide
|
||||
|
||||
## 1. Introduction
|
||||
|
||||
Stirling-PDF is a robust, locally hosted, web-based PDF manipulation tool. **Stirling 2.0** represents a complete frontend rewrite, replacing the legacy Thymeleaf-based UI with a modern React SPA (Single Page Application).
|
||||
|
||||
This guide focuses on developing for Stirling 2.0, including both the React frontend and Spring Boot backend development workflows.
|
||||
|
||||
## 2. Project Overview
|
||||
|
||||
**Stirling 2.0** is built using:
|
||||
|
||||
**Backend:**
|
||||
- Spring Boot (Java 17+, JDK 21 recommended)
|
||||
- PDFBox for core PDF operations
|
||||
- LibreOffice for document conversions
|
||||
- qpdf for PDF optimization
|
||||
- Spring Security (optional, controlled by `DOCKER_ENABLE_SECURITY`)
|
||||
- Lombok for reducing boilerplate code
|
||||
|
||||
**Frontend (React SPA):**
|
||||
- React + TypeScript
|
||||
- Vite for build tooling and development server
|
||||
- Mantine UI component library
|
||||
- TailwindCSS for styling
|
||||
- PDF.js for client-side PDF rendering
|
||||
- PDF-LIB.js for client-side PDF manipulation
|
||||
- IndexedDB for client-side file storage and thumbnails
|
||||
- i18next for internationalization
|
||||
|
||||
**Infrastructure:**
|
||||
- Docker for containerization
|
||||
- Gradle for build management
|
||||
|
||||
**Legacy (reference only during development):**
|
||||
- Thymeleaf templates (being completely replaced in 2.0)
|
||||
|
||||
## 3. Development Environment Setup
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Docker
|
||||
- Git
|
||||
- Java JDK 17 or later (JDK 21 recommended)
|
||||
- Node.js 18+ and npm (required for frontend development)
|
||||
- Gradle 7.0 or later (Included within the repo)
|
||||
|
||||
### Setup Steps
|
||||
|
||||
1. Clone the repository:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/Stirling-Tools/Stirling-PDF.git
|
||||
cd Stirling-PDF
|
||||
```
|
||||
|
||||
2. Install Docker and JDK17 if not already installed.
|
||||
|
||||
3. Install a recommended Java IDE such as Eclipse, IntelliJ, or VSCode
|
||||
1. Only VSCode
|
||||
1. Open VS Code.
|
||||
2. When prompted, install the recommended extensions.
|
||||
3. Alternatively, open the command palette (`Ctrl + Shift + P` or `Cmd + Shift + P` on macOS) and run:
|
||||
|
||||
```sh
|
||||
Extensions: Show Recommended Extensions
|
||||
```
|
||||
|
||||
4. Install the required extensions from the list.
|
||||
|
||||
4. Lombok Setup
|
||||
Stirling-PDF uses Lombok to reduce boilerplate code. Some IDEs, like Eclipse, don't support Lombok out of the box. To set up Lombok in your development environment:
|
||||
Visit the [Lombok website](https://projectlombok.org/setup/) for installation instructions specific to your IDE.
|
||||
|
||||
5. Add environment variable
|
||||
For local testing, you should generally be testing the full 'Security' version of Stirling PDF. To do this, you must add the environment flag DISABLE_ADDITIONAL_FEATURES=false to your system and/or IDE build/run step.
|
||||
5. **Frontend Setup (Required for Stirling 2.0)**
|
||||
Navigate to the frontend directory and install dependencies using npm.
|
||||
|
||||
## 4. Stirling 2.0 Development Workflow
|
||||
|
||||
### Frontend Development (React)
|
||||
The frontend is a React SPA that runs independently during development:
|
||||
|
||||
1. **Start the backend**: Run the Spring Boot application (serves API endpoints on localhost:8080)
|
||||
2. **Start the frontend dev server**: Navigate to the frontend directory and run the development server (serves UI on localhost:5173)
|
||||
3. **Development flow**: The Vite dev server automatically proxies API calls to the backend
|
||||
|
||||
### File Storage Architecture
|
||||
Stirling 2.0 uses client-side file storage:
|
||||
- **IndexedDB**: Stores files locally in the browser with automatic thumbnail generation
|
||||
- **PDF.js**: Handles client-side PDF rendering and processing
|
||||
- **URL Parameters**: Support for deep linking and tool state persistence
|
||||
|
||||
### Legacy Code Reference
|
||||
The existing Thymeleaf templates remain in the codebase during development as reference material but will be completely removed for the 2.0 release.
|
||||
|
||||
## 5. Project Structure
|
||||
|
||||
```bash
|
||||
Stirling-PDF/
|
||||
├── .github/ # GitHub-specific files (workflows, issue templates)
|
||||
├── configs/ # Configuration files used by stirling at runtime (generated at runtime)
|
||||
├── frontend/ # React SPA frontend (Stirling 2.0)
|
||||
│ ├── src/
|
||||
│ │ ├── components/ # React components
|
||||
│ │ ├── tools/ # Tool-specific React components
|
||||
│ │ ├── hooks/ # Custom React hooks
|
||||
│ │ ├── services/ # API and utility services
|
||||
│ │ ├── types/ # TypeScript type definitions
|
||||
│ │ └── utils/ # Utility functions
|
||||
│ ├── public/
|
||||
│ │ └── locales/ # Internationalization files (JSON)
|
||||
│ ├── package.json # Frontend dependencies
|
||||
│ └── vite.config.ts # Vite configuration
|
||||
├── customFiles/ # Custom static files and templates (generated at runtime used to replace existing files)
|
||||
├── docs/ # Documentation files
|
||||
├── exampleYmlFiles/ # Example YAML configuration files
|
||||
├── images/ # Image assets
|
||||
├── pipeline/ # Pipeline-related files (generated at runtime)
|
||||
├── scripts/ # Utility scripts
|
||||
├── src/ # Source code
|
||||
│ ├── main/
|
||||
│ │ ├── java/
|
||||
│ │ │ └── stirling/
|
||||
│ │ │ └── software/
|
||||
│ │ │ └── SPDF/
|
||||
│ │ │ ├── config/
|
||||
│ │ │ ├── controller/
|
||||
│ │ │ ├── model/
|
||||
│ │ │ ├── repository/
|
||||
│ │ │ ├── service/
|
||||
│ │ │ └── utils/
|
||||
│ │ └── resources/
|
||||
│ │ ├── static/ # Legacy static assets (reference only)
|
||||
│ │ │ ├── css/
|
||||
│ │ │ ├── js/
|
||||
│ │ │ └── pdfjs/
|
||||
│ │ └── templates/ # Legacy Thymeleaf templates (reference only)
|
||||
│ └── test/
|
||||
├── testing/ # Cucumber and integration tests
|
||||
│ └── cucumber/ # Cucumber test files
|
||||
├── build.gradle # Gradle build configuration
|
||||
├── Dockerfile # Main Dockerfile
|
||||
├── Dockerfile.ultra-lite # Dockerfile for ultra-lite version
|
||||
├── Dockerfile.fat # Dockerfile for fat version
|
||||
├── docker-compose.yml # Docker Compose configuration
|
||||
└── test.sh # Test script to deploy all docker versions and run cuke tests
|
||||
```
|
||||
|
||||
## 6. Docker-based Development
|
||||
|
||||
Stirling-PDF offers several Docker versions:
|
||||
|
||||
- Full: All features included
|
||||
- Ultra-Lite: Basic PDF operations only
|
||||
- Fat: Includes additional libraries and fonts predownloaded
|
||||
|
||||
### Example Docker Compose Files
|
||||
|
||||
Stirling-PDF provides several example Docker Compose files in the `exampleYmlFiles` directory, such as:
|
||||
|
||||
- `docker-compose-latest.yml`: Latest version without login and security features
|
||||
- `docker-compose-latest-security.yml`: Latest version with login and security features enabled
|
||||
- `docker-compose-latest-fat-security.yml`: Fat version with login and security features enabled
|
||||
|
||||
These files provide pre-configured setups for different scenarios. For example, here's a snippet from `docker-compose-latest-security.yml`:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
stirling-pdf:
|
||||
container_name: Stirling-PDF-Security
|
||||
image: docker.stirlingpdf.com/stirlingtools/stirling-pdf:latest
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 4G
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status | grep -q 'UP' && curl -fL http://localhost:8080/ | grep -q 'Please sign in'"]
|
||||
interval: 5s
|
||||
timeout: 10s
|
||||
retries: 16
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- ./stirling/latest/data:/usr/share/tessdata:rw
|
||||
- ./stirling/latest/config:/configs:rw
|
||||
- ./stirling/latest/logs:/logs:rw
|
||||
environment:
|
||||
DISABLE_ADDITIONAL_FEATURES: "false"
|
||||
SECURITY_ENABLELOGIN: "true"
|
||||
PUID: 1002
|
||||
PGID: 1002
|
||||
UMASK: "022"
|
||||
SYSTEM_DEFAULTLOCALE: en-US
|
||||
UI_APPNAME: Stirling-PDF
|
||||
UI_HOMEDESCRIPTION: Demo site for Stirling-PDF Latest with Security
|
||||
UI_APPNAMENAVBAR: Stirling-PDF Latest
|
||||
SYSTEM_MAXFILESIZE: "100"
|
||||
METRICS_ENABLED: "true"
|
||||
SYSTEM_GOOGLEVISIBILITY: "true"
|
||||
SHOW_SURVEY: "true"
|
||||
restart: on-failure:5
|
||||
```
|
||||
|
||||
To use these example files, copy the desired file to your project root and rename it to `docker-compose.yml`, or specify the file explicitly when running Docker Compose:
|
||||
|
||||
```bash
|
||||
docker-compose -f exampleYmlFiles/docker-compose-latest-security.yml up
|
||||
```
|
||||
|
||||
### Building Docker Images
|
||||
|
||||
Stirling-PDF uses different Docker images for various configurations. The build process is controlled by environment variables and uses specific Dockerfile variants. Here's how to build the Docker images:
|
||||
|
||||
1. Set the security environment variable:
|
||||
|
||||
```bash
|
||||
export DISABLE_ADDITIONAL_FEATURES=true # or false for to enable login and security features for builds
|
||||
```
|
||||
|
||||
2. Build the project with Gradle:
|
||||
|
||||
```bash
|
||||
./gradlew clean build
|
||||
```
|
||||
|
||||
3. Build the Docker images:
|
||||
|
||||
For the latest version:
|
||||
|
||||
```bash
|
||||
docker build --no-cache --pull --build-arg VERSION_TAG=alpha -t stirlingtools/stirling-pdf:latest -f ./Dockerfile .
|
||||
```
|
||||
|
||||
For the ultra-lite version:
|
||||
|
||||
```bash
|
||||
docker build --no-cache --pull --build-arg VERSION_TAG=alpha -t stirlingtools/stirling-pdf:latest-ultra-lite -f ./Dockerfile.ultra-lite .
|
||||
```
|
||||
|
||||
For the fat version (with login and security features enabled):
|
||||
|
||||
```bash
|
||||
export DISABLE_ADDITIONAL_FEATURES=false
|
||||
docker build --no-cache --pull --build-arg VERSION_TAG=alpha -t stirlingtools/stirling-pdf:latest-fat -f ./Dockerfile.fat .
|
||||
```
|
||||
|
||||
Note: The `--no-cache` and `--pull` flags ensure that the build process uses the latest base images and doesn't use cached layers, which is useful for testing and ensuring reproducible builds. however to improve build times these can often be removed depending on your usecase
|
||||
|
||||
## 7. Testing
|
||||
|
||||
### Comprehensive Testing Script
|
||||
|
||||
Stirling-PDF provides a `test.sh` script in the root directory. This script builds all versions of Stirling-PDF, checks that each version works, and runs Cucumber tests. It's recommended to run this script before submitting a final pull request.
|
||||
|
||||
To run the test script:
|
||||
|
||||
```bash
|
||||
./test.sh
|
||||
```
|
||||
|
||||
This script performs the following actions:
|
||||
|
||||
1. Builds all Docker images (full, ultra-lite, fat).
|
||||
2. Runs each version to ensure it starts correctly.
|
||||
3. Executes Cucumber tests against the main version and ensures feature compatibility. In the event these tests fail, your PR will not be merged.
|
||||
|
||||
Note: The `test.sh` script will run automatically when you raise a PR. However, it's recommended to run it locally first to save resources and catch any issues early.
|
||||
|
||||
### Full Testing with Docker
|
||||
|
||||
1. Build and run the Docker container per the above instructions:
|
||||
|
||||
2. Access the application at `http://localhost:8080` and manually test all features developed.
|
||||
|
||||
### Frontend Development Testing (Stirling 2.0)
|
||||
|
||||
For React frontend development:
|
||||
|
||||
1. Start the backend: Run the Spring Boot application to serve API endpoints on localhost:8080
|
||||
2. Start the frontend dev server: Navigate to the frontend directory and run the development server on localhost:5173
|
||||
3. The Vite dev server automatically proxies API calls to the backend
|
||||
4. Test React components, UI interactions, and IndexedDB file operations using browser developer tools
|
||||
|
||||
### Local Testing (Java and UI Components)
|
||||
|
||||
For quick iterations and development of Java backend, JavaScript, and UI components, you can run and test Stirling-PDF locally without Docker. This approach allows you to work on and verify changes to:
|
||||
|
||||
- Java backend logic
|
||||
- RESTful API endpoints
|
||||
- JavaScript functionality
|
||||
- User interface components and styling
|
||||
- Thymeleaf templates
|
||||
|
||||
To run Stirling-PDF locally:
|
||||
|
||||
1. Compile and run the project using built-in IDE methods or by running:
|
||||
|
||||
```bash
|
||||
./gradlew bootRun
|
||||
```
|
||||
|
||||
2. Access the application at `http://localhost:8080` in your web browser.
|
||||
|
||||
3. Manually test the features you're working on through the UI.
|
||||
|
||||
4. For API changes, use tools like Postman or curl to test endpoints directly.
|
||||
|
||||
Important notes:
|
||||
|
||||
- Local testing doesn't include features that depend on external tools like qpdf, LibreOffice, or Python scripts.
|
||||
- There are currently no automated unit tests. All testing is done manually through the UI or API calls. (You are welcome to add JUnits!)
|
||||
- Always verify your changes in the full Docker environment before submitting pull requests, as some integrations and features will only work in the complete setup.
|
||||
|
||||
## 8. Contributing
|
||||
|
||||
1. Fork the repository on GitHub.
|
||||
2. Create a new branch for your feature or bug fix.
|
||||
3. Make your changes and commit them with clear, descriptive messages and ensure any documentation is updated related to your changes.
|
||||
4. Test your changes thoroughly in the Docker environment.
|
||||
5. Run the `test.sh` script to ensure all versions build correctly and pass the Cucumber tests:
|
||||
|
||||
```bash
|
||||
./test.sh
|
||||
```
|
||||
|
||||
6. Push your changes to your fork.
|
||||
7. Submit a pull request to the main repository.
|
||||
8. See additional [contributing guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md).
|
||||
|
||||
When you raise a PR:
|
||||
|
||||
- The `test.sh` script will run automatically against your PR.
|
||||
- The PR checks will verify versioning and dependency updates.
|
||||
- Documentation will be automatically updated for dependency changes.
|
||||
- Security issues will be checked using Snyk and PixeeBot.
|
||||
|
||||
Address any issues that arise from these checks before finalizing your pull request.
|
||||
|
||||
## 9. API Documentation
|
||||
|
||||
API documentation is available at `/swagger-ui/index.html` when running the application. You can also view the latest API documentation [here](https://app.swaggerhub.com/apis-docs/Stirling-Tools/Stirling-PDF/).
|
||||
|
||||
## 10. Customization
|
||||
|
||||
Stirling-PDF can be customized through environment variables or a `settings.yml` file. Key customization options include:
|
||||
|
||||
- Application name and branding
|
||||
- Security settings
|
||||
- UI customization
|
||||
- Endpoint management
|
||||
|
||||
When using Docker, pass environment variables using the `-e` flag or in your `docker-compose.yml` file.
|
||||
|
||||
Example:
|
||||
|
||||
```bash
|
||||
docker run -p 8080:8080 -e APP_NAME="My PDF Tool" stirling-pdf:full
|
||||
```
|
||||
|
||||
Refer to the main README for a full list of customization options.
|
||||
|
||||
## 11. Language Translations
|
||||
|
||||
For managing language translations that affect multiple files, Stirling-PDF provides a helper script:
|
||||
|
||||
```bash
|
||||
/scripts/replace_translation_line.sh
|
||||
```
|
||||
|
||||
This script helps you make consistent replacements across language files.
|
||||
|
||||
When contributing translations:
|
||||
|
||||
1. Use the helper script for multi-file changes.
|
||||
2. Ensure all language files are updated consistently.
|
||||
3. The PR checks will verify consistency in language file updates.
|
||||
|
||||
Remember to test your changes thoroughly to ensure they don't break any existing functionality.
|
||||
|
||||
## Code examples
|
||||
|
||||
### React Component Development (Stirling 2.0)
|
||||
|
||||
For Stirling 2.0, new features are built as React components instead of Thymeleaf templates:
|
||||
|
||||
#### Creating a New Tool Component
|
||||
|
||||
1. **Create the React Component:**
|
||||
```typescript
|
||||
// frontend/src/tools/NewTool.tsx
|
||||
import { useState } from 'react';
|
||||
import { Button, FileInput, Container } from '@mantine/core';
|
||||
|
||||
interface NewToolProps {
|
||||
params: Record<string, any>;
|
||||
updateParams: (updates: Record<string, any>) => void;
|
||||
}
|
||||
|
||||
export default function NewTool({ params, updateParams }: NewToolProps) {
|
||||
const [files, setFiles] = useState<File[]>([]);
|
||||
|
||||
const handleProcess = async () => {
|
||||
// Process files using API or client-side logic
|
||||
};
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<FileInput
|
||||
multiple
|
||||
accept="application/pdf"
|
||||
onChange={setFiles}
|
||||
/>
|
||||
<Button onClick={handleProcess}>Process</Button>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
2. **Add API Integration:**
|
||||
```typescript
|
||||
// Use existing API endpoints or create new ones
|
||||
const response = await fetch('/api/v1/new-tool', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
```
|
||||
|
||||
3. **Register in Tool Picker:**
|
||||
Update the tool picker component to include the new tool with proper routing and URL parameter support.
|
||||
|
||||
### Legacy Reference: Overview of Thymeleaf
|
||||
|
||||
Thymeleaf is a server-side Java HTML template engine. It is used in Stirling-PDF to render dynamic web pages. Thymeleaf integrates heavily with Spring Boot.
|
||||
|
||||
### Thymeleaf overview
|
||||
|
||||
In Stirling-PDF, Thymeleaf is used to create HTML templates that are rendered on the server side. These templates are located in the `stirling-pdf/src/main/resources/templates` directory. Thymeleaf templates use a combination of HTML and special Thymeleaf attributes to dynamically generate content.
|
||||
|
||||
Some examples of this are:
|
||||
|
||||
```html
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
```
|
||||
or
|
||||
```html
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
```
|
||||
|
||||
Where it uses the `th:block`, `th:` indicating it's a special Thymeleaf element to be used server-side in generating the HTML, and block being the actual element type.
|
||||
In this case, we are inserting the `navbar` entry within the `fragments/navbar.html` fragment into the `th:block` element.
|
||||
|
||||
They can be more complex, such as:
|
||||
|
||||
```html
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{pageExtracter.title}, header=#{pageExtracter.header})}"></th:block>
|
||||
```
|
||||
|
||||
Which is the same as above but passes the parameters title and header into the fragment `common.html` to be used in its HTML generation.
|
||||
|
||||
Thymeleaf can also be used to loop through objects or pass things from the Java side into the HTML side.
|
||||
|
||||
```java
|
||||
@GetMapping
|
||||
public String newFeaturePage(Model model) {
|
||||
model.addAttribute("exampleData", exampleData);
|
||||
return "new-feature";
|
||||
}
|
||||
```
|
||||
|
||||
In the above example, if exampleData is a list of plain java objects of class Person and within it, you had id, name, age, etc. You can reference it like so
|
||||
|
||||
```html
|
||||
<tbody>
|
||||
<!-- Use th:each to iterate over the list -->
|
||||
<tr th:each="person : ${exampleData}">
|
||||
<td th:text="${person.id}"></td>
|
||||
<td th:text="${person.name}"></td>
|
||||
<td th:text="${person.age}"></td>
|
||||
<td th:text="${person.email}"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
```
|
||||
|
||||
This would generate n entries of tr for each person in exampleData
|
||||
|
||||
### Adding a New Feature to the Backend (API)
|
||||
|
||||
1. **Create a New Controller:**
|
||||
- Create a new Java class in the `stirling-pdf/src/main/java/stirling/software/SPDF/controller/api` directory.
|
||||
- Annotate the class with `@RestController` and `@RequestMapping` to define the API endpoint.
|
||||
- Ensure to add API documentation annotations like `@Tag(name = "General", description = "General APIs")` and `@Operation(summary = "Crops a PDF document", description = "This operation takes an input PDF file and crops it according to the given coordinates. Input:PDF Output:PDF Type:SISO")`.
|
||||
|
||||
```java
|
||||
package stirling.software.SPDF.controller.api;
|
||||
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/new-feature")
|
||||
@Tag(name = "General", description = "General APIs")
|
||||
public class NewFeatureController {
|
||||
|
||||
@GetMapping
|
||||
@Operation(summary = "New Feature", description = "This is a new feature endpoint.")
|
||||
public String newFeature() {
|
||||
return "NewFeatureResponse"; // This refers to the NewFeatureResponse.html template presenting the user with the generated html from that file when they navigate to /api/v1/new-feature
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. **Define the Service Layer:** (Not required but often useful)
|
||||
- Create a new service class in the `stirling-pdf/src/main/java/stirling/software/SPDF/service` directory.
|
||||
- Implement the business logic for the new feature.
|
||||
|
||||
```java
|
||||
package stirling.software.SPDF.service;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class NewFeatureService {
|
||||
|
||||
public String getNewFeatureData() {
|
||||
// Implement business logic here
|
||||
return "New Feature Data";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2b. **Integrate the Service with the Controller:**
|
||||
|
||||
- Autowire the service class in the controller and use it to handle the API request.
|
||||
|
||||
```java
|
||||
package stirling.software.SPDF.controller.api;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import stirling.software.SPDF.service.NewFeatureService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/new-feature")
|
||||
@Tag(name = "General", description = "General APIs")
|
||||
public class NewFeatureController {
|
||||
|
||||
@Autowired
|
||||
private NewFeatureService newFeatureService;
|
||||
|
||||
@GetMapping
|
||||
@Operation(summary = "New Feature", description = "This is a new feature endpoint.")
|
||||
public String newFeature() {
|
||||
return newFeatureService.getNewFeatureData();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Adding a New Feature to the Frontend (UI)
|
||||
|
||||
1. **Create a New Thymeleaf Template:**
|
||||
- Create a new HTML file in the `stirling-pdf/src/main/resources/templates` directory.
|
||||
- Use Thymeleaf attributes to dynamically generate content.
|
||||
- Use `extract-page.html` as a base example for the HTML template, which is useful to ensure importing of the general layout, navbar, and footer.
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{newFeature.title}, header=#{newFeature.header})}"></th:block>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<br><br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 bg-card">
|
||||
<div class="tool-header">
|
||||
<span class="material-symbols-rounded tool-header-icon organize">upload</span>
|
||||
<span class="tool-header-text" th:text="#{newFeature.header}"></span>
|
||||
</div>
|
||||
<form th:action="@{'/api/v1/new-feature'}" method="post" enctype="multipart/form-data">
|
||||
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}"></div>
|
||||
<input type="hidden" id="customMode" name="customMode" value="">
|
||||
<div class="mb-3">
|
||||
<label for="featureInput" th:text="#{newFeature.prompt}"></label>
|
||||
<input type="text" class="form-control" id="featureInput" name="featureInput" th:placeholder="#{newFeature.placeholder}" required>
|
||||
</div>
|
||||
|
||||
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{newFeature.submit}"></button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
2. **Create a New Controller for the UI:**
|
||||
- Create a new Java class in the `stirling-pdf/src/main/java/stirling/software/SPDF/controller/ui` directory.
|
||||
- Annotate the class with `@Controller` and `@RequestMapping` to define the UI endpoint.
|
||||
|
||||
```java
|
||||
package stirling.software.SPDF.controller.ui;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import stirling.software.SPDF.service.NewFeatureService;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/new-feature")
|
||||
public class NewFeatureUIController {
|
||||
|
||||
@Autowired
|
||||
private NewFeatureService newFeatureService;
|
||||
|
||||
@GetMapping
|
||||
public String newFeaturePage(Model model) {
|
||||
model.addAttribute("newFeatureData", newFeatureService.getNewFeatureData());
|
||||
return "new-feature";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. **Update the Navigation Bar:**
|
||||
- Add a link to the new feature page in the navigation bar.
|
||||
- Update the `stirling-pdf/src/main/resources/templates/fragments/navbar.html` file.
|
||||
|
||||
```html
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" th:href="@{'/new-feature'}">New Feature</a>
|
||||
</li>
|
||||
```
|
||||
|
||||
## Adding New Translations to Existing Language Files in Stirling-PDF
|
||||
|
||||
When adding a new feature or modifying existing ones in Stirling-PDF, you'll need to add new translation entries to the existing language files. Here's a step-by-step guide:
|
||||
|
||||
### 1. Locate Existing Language Files
|
||||
|
||||
Find the existing `messages.properties` files in the `stirling-pdf/src/main/resources` directory. You'll see files like:
|
||||
|
||||
- `messages.properties` (default, usually English)
|
||||
- `messages_en_GB.properties`
|
||||
- `messages_fr_FR.properties`
|
||||
- `messages_de_DE.properties`
|
||||
- etc.
|
||||
|
||||
### 2. Add New Translation Entries
|
||||
|
||||
Open each of these files and add your new translation entries. For example, if you're adding a new feature called "PDF Splitter",
|
||||
Use descriptive, hierarchical keys (e.g., `feature.element.description`)
|
||||
you might add:
|
||||
|
||||
```properties
|
||||
pdfSplitter.title=PDF Splitter
|
||||
pdfSplitter.description=Split your PDF into multiple documents
|
||||
pdfSplitter.button.split=Split PDF
|
||||
pdfSplitter.input.pages=Enter page numbers to split
|
||||
```
|
||||
|
||||
Add these entries to the default GB language file and any others you wish, translating the values as appropriate for each language.
|
||||
|
||||
### 3. Use Translations in Thymeleaf Templates
|
||||
|
||||
In your Thymeleaf templates, use the `#{key}` syntax to reference the new translations:
|
||||
|
||||
```html
|
||||
<h1 th:text="#{pdfSplitter.title}">PDF Splitter</h1>
|
||||
<p th:text="#{pdfSplitter.description}">Split your PDF into multiple documents</p>
|
||||
<input type="text" th:placeholder="#{pdfSplitter.input.pages}">
|
||||
<button th:text="#{pdfSplitter.button.split}">Split PDF</button>
|
||||
```
|
||||
|
||||
Remember, never hard-code text in your templates or Java code. Always use translation keys to ensure proper localization.
|
||||
@@ -1,60 +0,0 @@
|
||||
# dockerfile.dev
|
||||
|
||||
# Basisimage: Gradle mit JDK 17 (Debian-basiert)
|
||||
FROM gradle:8.14-jdk17
|
||||
|
||||
# Als Root-Benutzer arbeiten, um benötigte Pakete zu installieren
|
||||
USER root
|
||||
|
||||
# Set GRADLE_HOME und füge Gradle zum PATH hinzu
|
||||
ENV GRADLE_HOME=/opt/gradle
|
||||
ENV PATH="$GRADLE_HOME/bin:$PATH"
|
||||
|
||||
# Update und Installation zusätzlicher Pakete (Debian/Ubuntu-basiert)
|
||||
RUN apt-get update && apt-get install -y \
|
||||
sudo \
|
||||
libreoffice \
|
||||
poppler-utils \
|
||||
qpdf \
|
||||
# settings.yml | tessdataDir: /usr/share/tesseract-ocr/5/tessdata
|
||||
tesseract-ocr \
|
||||
tesseract-ocr-eng \
|
||||
fonts-terminus fonts-dejavu fonts-font-awesome fonts-noto fonts-noto-core fonts-noto-cjk fonts-noto-extra fonts-liberation fonts-linuxlibertine fonts-urw-base35 \
|
||||
python3-uno \
|
||||
python3-venv \
|
||||
# ss -tln
|
||||
iproute2 \
|
||||
&& apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Setze die Environment Variable für setuptools
|
||||
ENV SETUPTOOLS_USE_DISTUTILS=local \
|
||||
STIRLING_TEMPFILES_DIRECTORY=/tmp/stirling-pdf \
|
||||
TMPDIR=/tmp/stirling-pdf \
|
||||
TEMP=/tmp/stirling-pdf \
|
||||
TMP=/tmp/stirling-pdf
|
||||
|
||||
# Installation der benötigten Python-Pakete
|
||||
RUN python3 -m venv --system-site-packages /opt/venv \
|
||||
&& . /opt/venv/bin/activate \
|
||||
&& pip install --no-cache-dir --upgrade pip setuptools \
|
||||
&& pip install --no-cache-dir WeasyPrint pdf2image pillow unoserver opencv-python-headless pre-commit
|
||||
|
||||
# Füge den venv-Pfad zur globalen PATH-Variable hinzu, damit die Tools verfügbar sind
|
||||
ENV PATH="/opt/venv/bin:$PATH"
|
||||
|
||||
COPY . /workspace
|
||||
|
||||
RUN mkdir -p /tmp/stirling-pdf \
|
||||
&& fc-cache -f -v \
|
||||
&& adduser --disabled-password --gecos '' devuser \
|
||||
&& chown -R devuser:devuser /home/devuser /workspace /tmp/stirling-pdf
|
||||
RUN echo "devuser ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/devuser \
|
||||
&& chmod 0440 /etc/sudoers.d/devuser
|
||||
|
||||
# Setze das Arbeitsverzeichnis (wird später per Bind-Mount überschrieben)
|
||||
WORKDIR /workspace
|
||||
|
||||
RUN chmod +x /workspace/.devcontainer/git-init.sh /workspace/.devcontainer/init-setup.sh
|
||||
|
||||
# Wechsel zum Nicht‑Root Benutzer
|
||||
USER devuser
|
||||
@@ -29,7 +29,7 @@ spotless {
|
||||
dependencies {
|
||||
api 'org.springframework.boot:spring-boot-starter-web'
|
||||
api 'org.springframework.boot:spring-boot-starter-aop'
|
||||
api 'org.springframework.boot:spring-boot-starter-thymeleaf'
|
||||
// api 'org.springframework.boot:spring-boot-starter-thymeleaf' // Deprecated - UI moved to React frontend
|
||||
api 'com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer:20240325.1'
|
||||
api 'com.fathzer:javaluator:3.0.6'
|
||||
api 'com.posthog.java:posthog:1.2.0'
|
||||
|
||||
@@ -10,7 +10,6 @@ import java.util.Properties;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
@@ -19,9 +18,7 @@ import org.springframework.context.annotation.Scope;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.thymeleaf.spring6.SpringTemplateEngine;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -51,6 +48,7 @@ public class AppConfig {
|
||||
@Value("${server.port:8080}")
|
||||
private String serverPort;
|
||||
|
||||
/* Commented out Thymeleaf template engine bean - to be removed when frontend migration is complete
|
||||
@Bean
|
||||
@ConditionalOnProperty(name = "system.customHTMLFiles", havingValue = "true")
|
||||
public SpringTemplateEngine templateEngine(ResourceLoader resourceLoader) {
|
||||
@@ -58,6 +56,7 @@ public class AppConfig {
|
||||
templateEngine.addTemplateResolver(new FileFallbackTemplateResolver(resourceLoader));
|
||||
return templateEngine;
|
||||
}
|
||||
*/
|
||||
|
||||
@Bean(name = "loginEnabled")
|
||||
public boolean loginEnabled() {
|
||||
|
||||
+3
-13
@@ -1,19 +1,8 @@
|
||||
package stirling.software.common.configuration;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Map;
|
||||
/* Commented out entire FileFallbackTemplateResolver class - Thymeleaf dependency removed
|
||||
* This class will be removed when frontend migration to React is complete
|
||||
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.thymeleaf.IEngineConfiguration;
|
||||
import org.thymeleaf.templateresolver.AbstractConfigurableTemplateResolver;
|
||||
import org.thymeleaf.templateresource.FileTemplateResource;
|
||||
import org.thymeleaf.templateresource.ITemplateResource;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.model.InputStreamTemplateResource;
|
||||
|
||||
@Slf4j
|
||||
public class FileFallbackTemplateResolver extends AbstractConfigurableTemplateResolver {
|
||||
@@ -57,3 +46,4 @@ public class FileFallbackTemplateResolver extends AbstractConfigurableTemplateRe
|
||||
return null;
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
+3
-5
@@ -1,11 +1,8 @@
|
||||
package stirling.software.common.model;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.Reader;
|
||||
/* Commented out entire InputStreamTemplateResource class - Thymeleaf dependency removed
|
||||
* This class will be removed when frontend migration to React is complete
|
||||
|
||||
import org.thymeleaf.templateresource.ITemplateResource;
|
||||
|
||||
public class InputStreamTemplateResource implements ITemplateResource {
|
||||
private InputStream inputStream;
|
||||
@@ -42,3 +39,4 @@ public class InputStreamTemplateResource implements ITemplateResource {
|
||||
return inputStream != null;
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
@@ -10,7 +10,6 @@ import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.core.env.Environment;
|
||||
@@ -23,7 +22,6 @@ import jakarta.annotation.PreDestroy;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.UI.WebBrowser;
|
||||
import stirling.software.common.configuration.AppConfig;
|
||||
import stirling.software.common.configuration.ConfigInitializer;
|
||||
import stirling.software.common.configuration.InstallationPathConfig;
|
||||
@@ -47,17 +45,15 @@ public class SPDFApplication {
|
||||
private final AppConfig appConfig;
|
||||
private final Environment env;
|
||||
private final ApplicationProperties applicationProperties;
|
||||
private final WebBrowser webBrowser;
|
||||
|
||||
// private final WebBrowser webBrowser; // Removed - desktop UI eliminated
|
||||
|
||||
public SPDFApplication(
|
||||
AppConfig appConfig,
|
||||
Environment env,
|
||||
ApplicationProperties applicationProperties,
|
||||
@Autowired(required = false) WebBrowser webBrowser) {
|
||||
AppConfig appConfig, Environment env, ApplicationProperties applicationProperties) {
|
||||
this.appConfig = appConfig;
|
||||
this.env = env;
|
||||
this.applicationProperties = applicationProperties;
|
||||
this.webBrowser = webBrowser;
|
||||
// this.webBrowser = webBrowser; // Removed - desktop UI eliminated
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws IOException, InterruptedException {
|
||||
@@ -151,28 +147,31 @@ public class SPDFApplication {
|
||||
serverPortStatic = serverPort;
|
||||
String url = baseUrl + ":" + getStaticPort() + contextPath;
|
||||
|
||||
if (webBrowser != null
|
||||
&& Boolean.parseBoolean(System.getProperty("STIRLING_PDF_DESKTOP_UI", "false"))) {
|
||||
webBrowser.initWebUI(url);
|
||||
} else {
|
||||
String browserOpenEnv = env.getProperty("BROWSER_OPEN");
|
||||
boolean browserOpen = browserOpenEnv != null && "true".equalsIgnoreCase(browserOpenEnv);
|
||||
if (browserOpen) {
|
||||
try {
|
||||
String os = System.getProperty("os.name").toLowerCase();
|
||||
Runtime rt = Runtime.getRuntime();
|
||||
// Desktop UI initialization removed - webBrowser dependency eliminated
|
||||
// Keep backwards compatibility for STIRLING_PDF_DESKTOP_UI system property
|
||||
if (Boolean.parseBoolean(System.getProperty("STIRLING_PDF_DESKTOP_UI", "false"))) {
|
||||
log.info("Desktop UI mode enabled, but WebBrowser functionality has been removed");
|
||||
// webBrowser.initWebUI(url); // Removed - desktop UI eliminated
|
||||
}
|
||||
|
||||
if (os.contains("win")) {
|
||||
// For Windows
|
||||
SystemCommand.runCommand(rt, "rundll32 url.dll,FileProtocolHandler " + url);
|
||||
} else if (os.contains("mac")) {
|
||||
SystemCommand.runCommand(rt, "open " + url);
|
||||
} else if (os.contains("nix") || os.contains("nux")) {
|
||||
SystemCommand.runCommand(rt, "xdg-open " + url);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.error("Error opening browser: {}", e.getMessage());
|
||||
// Standard browser opening logic
|
||||
String browserOpenEnv = env.getProperty("BROWSER_OPEN");
|
||||
boolean browserOpen = browserOpenEnv != null && "true".equalsIgnoreCase(browserOpenEnv);
|
||||
if (browserOpen) {
|
||||
try {
|
||||
String os = System.getProperty("os.name").toLowerCase();
|
||||
Runtime rt = Runtime.getRuntime();
|
||||
|
||||
if (os.contains("win")) {
|
||||
// For Windows
|
||||
SystemCommand.runCommand(rt, "rundll32 url.dll,FileProtocolHandler " + url);
|
||||
} else if (os.contains("mac")) {
|
||||
SystemCommand.runCommand(rt, "open " + url);
|
||||
} else if (os.contains("nix") || os.contains("nux")) {
|
||||
SystemCommand.runCommand(rt, "xdg-open " + url);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.error("Error opening browser: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -189,9 +188,10 @@ public class SPDFApplication {
|
||||
|
||||
@PreDestroy
|
||||
public void cleanup() {
|
||||
if (webBrowser != null) {
|
||||
webBrowser.cleanup();
|
||||
}
|
||||
// webBrowser cleanup removed - desktop UI eliminated
|
||||
// if (webBrowser != null) {
|
||||
// webBrowser.cleanup();
|
||||
// }
|
||||
}
|
||||
|
||||
private static void printStartupLogs() {
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
package stirling.software.SPDF.UI;
|
||||
|
||||
public interface WebBrowser {
|
||||
void initWebUI(String url);
|
||||
|
||||
void cleanup();
|
||||
}
|
||||
@@ -1,497 +0,0 @@
|
||||
package stirling.software.SPDF.UI.impl;
|
||||
|
||||
import java.awt.AWTException;
|
||||
import java.awt.BorderLayout;
|
||||
import java.awt.Frame;
|
||||
import java.awt.Image;
|
||||
import java.awt.MenuItem;
|
||||
import java.awt.PopupMenu;
|
||||
import java.awt.SystemTray;
|
||||
import java.awt.TrayIcon;
|
||||
import java.awt.event.WindowEvent;
|
||||
import java.awt.event.WindowStateListener;
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.swing.JFrame;
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.SwingUtilities;
|
||||
import javax.swing.Timer;
|
||||
|
||||
import org.cef.CefApp;
|
||||
import org.cef.CefClient;
|
||||
import org.cef.CefSettings;
|
||||
import org.cef.browser.CefBrowser;
|
||||
import org.cef.callback.CefBeforeDownloadCallback;
|
||||
import org.cef.callback.CefDownloadItem;
|
||||
import org.cef.callback.CefDownloadItemCallback;
|
||||
import org.cef.handler.CefDownloadHandlerAdapter;
|
||||
import org.cef.handler.CefLoadHandlerAdapter;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import jakarta.annotation.PreDestroy;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import me.friwi.jcefmaven.CefAppBuilder;
|
||||
import me.friwi.jcefmaven.EnumProgress;
|
||||
import me.friwi.jcefmaven.MavenCefAppHandlerAdapter;
|
||||
import me.friwi.jcefmaven.impl.progress.ConsoleProgressHandler;
|
||||
|
||||
import stirling.software.SPDF.UI.WebBrowser;
|
||||
import stirling.software.common.configuration.InstallationPathConfig;
|
||||
import stirling.software.common.util.UIScaling;
|
||||
|
||||
@Component
|
||||
@Slf4j
|
||||
@ConditionalOnProperty(
|
||||
name = "STIRLING_PDF_DESKTOP_UI",
|
||||
havingValue = "true",
|
||||
matchIfMissing = false)
|
||||
public class DesktopBrowser implements WebBrowser {
|
||||
private static CefApp cefApp;
|
||||
private static CefClient client;
|
||||
private static CefBrowser browser;
|
||||
private static JFrame frame;
|
||||
private static LoadingWindow loadingWindow;
|
||||
private static volatile boolean browserInitialized = false;
|
||||
private static TrayIcon trayIcon;
|
||||
private static SystemTray systemTray;
|
||||
|
||||
public DesktopBrowser() {
|
||||
SwingUtilities.invokeLater(
|
||||
() -> {
|
||||
loadingWindow = new LoadingWindow(null, "Initializing...");
|
||||
loadingWindow.setVisible(true);
|
||||
});
|
||||
}
|
||||
|
||||
public void initWebUI(String url) {
|
||||
CompletableFuture.runAsync(
|
||||
() -> {
|
||||
try {
|
||||
CefAppBuilder builder = new CefAppBuilder();
|
||||
configureCefSettings(builder);
|
||||
builder.setProgressHandler(createProgressHandler());
|
||||
builder.setInstallDir(
|
||||
new File(InstallationPathConfig.getClientWebUIPath()));
|
||||
// Build and initialize CEF
|
||||
cefApp = builder.build();
|
||||
client = cefApp.createClient();
|
||||
|
||||
// Set up download handler
|
||||
setupDownloadHandler();
|
||||
|
||||
// Create browser and frame on EDT
|
||||
SwingUtilities.invokeAndWait(
|
||||
() -> {
|
||||
browser = client.createBrowser(url, false, false);
|
||||
setupMainFrame();
|
||||
setupLoadHandler();
|
||||
|
||||
// Force initialize UI after 7 seconds if not already done
|
||||
Timer timeoutTimer =
|
||||
new Timer(
|
||||
2500,
|
||||
e -> {
|
||||
log.warn(
|
||||
"Loading timeout reached. Forcing"
|
||||
+ " UI transition.");
|
||||
if (!browserInitialized) {
|
||||
// Force UI initialization
|
||||
forceInitializeUI();
|
||||
}
|
||||
});
|
||||
timeoutTimer.setRepeats(false);
|
||||
timeoutTimer.start();
|
||||
});
|
||||
} catch (Exception e) {
|
||||
log.error("Error initializing JCEF browser: ", e);
|
||||
cleanup();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void configureCefSettings(CefAppBuilder builder) {
|
||||
CefSettings settings = builder.getCefSettings();
|
||||
String basePath = InstallationPathConfig.getClientWebUIPath();
|
||||
log.info("basePath " + basePath);
|
||||
settings.cache_path = new File(basePath + "cache").getAbsolutePath();
|
||||
settings.root_cache_path = new File(basePath + "root_cache").getAbsolutePath();
|
||||
// settings.browser_subprocess_path = new File(basePath +
|
||||
// "subprocess").getAbsolutePath();
|
||||
// settings.resources_dir_path = new File(basePath + "resources").getAbsolutePath();
|
||||
// settings.locales_dir_path = new File(basePath + "locales").getAbsolutePath();
|
||||
settings.log_file = new File(basePath, "debug.log").getAbsolutePath();
|
||||
|
||||
settings.persist_session_cookies = true;
|
||||
settings.windowless_rendering_enabled = false;
|
||||
settings.log_severity = CefSettings.LogSeverity.LOGSEVERITY_INFO;
|
||||
|
||||
builder.setAppHandler(
|
||||
new MavenCefAppHandlerAdapter() {
|
||||
@Override
|
||||
public void stateHasChanged(org.cef.CefApp.CefAppState state) {
|
||||
log.info("CEF state changed: " + state);
|
||||
if (state == CefApp.CefAppState.TERMINATED) {
|
||||
System.exit(0);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void setupDownloadHandler() {
|
||||
client.addDownloadHandler(
|
||||
new CefDownloadHandlerAdapter() {
|
||||
@Override
|
||||
public boolean onBeforeDownload(
|
||||
CefBrowser browser,
|
||||
CefDownloadItem downloadItem,
|
||||
String suggestedName,
|
||||
CefBeforeDownloadCallback callback) {
|
||||
callback.Continue("", true);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDownloadUpdated(
|
||||
CefBrowser browser,
|
||||
CefDownloadItem downloadItem,
|
||||
CefDownloadItemCallback callback) {
|
||||
if (downloadItem.isComplete()) {
|
||||
log.info("Download completed: " + downloadItem.getFullPath());
|
||||
} else if (downloadItem.isCanceled()) {
|
||||
log.info("Download canceled: " + downloadItem.getFullPath());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private ConsoleProgressHandler createProgressHandler() {
|
||||
return new ConsoleProgressHandler() {
|
||||
@Override
|
||||
public void handleProgress(EnumProgress state, float percent) {
|
||||
Objects.requireNonNull(state, "state cannot be null");
|
||||
SwingUtilities.invokeLater(
|
||||
() -> {
|
||||
if (loadingWindow != null) {
|
||||
switch (state) {
|
||||
case LOCATING:
|
||||
loadingWindow.setStatus("Locating Files...");
|
||||
loadingWindow.setProgress(0);
|
||||
break;
|
||||
case DOWNLOADING:
|
||||
if (percent >= 0) {
|
||||
loadingWindow.setStatus(
|
||||
String.format(
|
||||
"Downloading additional files: %.0f%%",
|
||||
percent));
|
||||
loadingWindow.setProgress((int) percent);
|
||||
}
|
||||
break;
|
||||
case EXTRACTING:
|
||||
loadingWindow.setStatus("Extracting files...");
|
||||
loadingWindow.setProgress(60);
|
||||
break;
|
||||
case INITIALIZING:
|
||||
loadingWindow.setStatus("Initializing UI...");
|
||||
loadingWindow.setProgress(80);
|
||||
break;
|
||||
case INITIALIZED:
|
||||
loadingWindow.setStatus("Finalising startup...");
|
||||
loadingWindow.setProgress(90);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void setupMainFrame() {
|
||||
frame = new JFrame("Stirling-PDF");
|
||||
frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
|
||||
frame.setUndecorated(true);
|
||||
frame.setOpacity(0.0f);
|
||||
|
||||
JPanel contentPane = new JPanel(new BorderLayout());
|
||||
contentPane.setDoubleBuffered(true);
|
||||
contentPane.add(browser.getUIComponent(), BorderLayout.CENTER);
|
||||
frame.setContentPane(contentPane);
|
||||
|
||||
frame.addWindowListener(
|
||||
new java.awt.event.WindowAdapter() {
|
||||
@Override
|
||||
public void windowClosing(java.awt.event.WindowEvent windowEvent) {
|
||||
cleanup();
|
||||
System.exit(0);
|
||||
}
|
||||
});
|
||||
|
||||
frame.setSize(UIScaling.scaleWidth(1280), UIScaling.scaleHeight(800));
|
||||
frame.setLocationRelativeTo(null);
|
||||
|
||||
loadIcon();
|
||||
}
|
||||
|
||||
private void setupLoadHandler() {
|
||||
final long initStartTime = System.currentTimeMillis();
|
||||
log.info("Setting up load handler at: {}", initStartTime);
|
||||
|
||||
client.addLoadHandler(
|
||||
new CefLoadHandlerAdapter() {
|
||||
@Override
|
||||
public void onLoadingStateChange(
|
||||
CefBrowser browser,
|
||||
boolean isLoading,
|
||||
boolean canGoBack,
|
||||
boolean canGoForward) {
|
||||
log.debug(
|
||||
"Loading state change - isLoading: {}, canGoBack: {}, canGoForward:"
|
||||
+ " {}, browserInitialized: {}, Time elapsed: {}ms",
|
||||
isLoading,
|
||||
canGoBack,
|
||||
canGoForward,
|
||||
browserInitialized,
|
||||
System.currentTimeMillis() - initStartTime);
|
||||
|
||||
if (!isLoading && !browserInitialized) {
|
||||
log.info(
|
||||
"Browser finished loading, preparing to initialize UI"
|
||||
+ " components");
|
||||
browserInitialized = true;
|
||||
SwingUtilities.invokeLater(
|
||||
() -> {
|
||||
try {
|
||||
if (loadingWindow != null) {
|
||||
log.info("Starting UI initialization sequence");
|
||||
|
||||
// Close loading window first
|
||||
loadingWindow.setVisible(false);
|
||||
loadingWindow.dispose();
|
||||
loadingWindow = null;
|
||||
log.info("Loading window disposed");
|
||||
|
||||
// Then setup the main frame
|
||||
frame.setVisible(false);
|
||||
frame.dispose();
|
||||
frame.setOpacity(1.0f);
|
||||
frame.setUndecorated(false);
|
||||
frame.pack();
|
||||
frame.setSize(
|
||||
UIScaling.scaleWidth(1280),
|
||||
UIScaling.scaleHeight(800));
|
||||
frame.setLocationRelativeTo(null);
|
||||
log.debug("Frame reconfigured");
|
||||
|
||||
// Show the main frame
|
||||
frame.setVisible(true);
|
||||
frame.requestFocus();
|
||||
frame.toFront();
|
||||
log.info("Main frame displayed and focused");
|
||||
|
||||
// Focus the browser component
|
||||
Timer focusTimer =
|
||||
new Timer(
|
||||
100,
|
||||
e -> {
|
||||
try {
|
||||
browser.getUIComponent()
|
||||
.requestFocus();
|
||||
log.info(
|
||||
"Browser component"
|
||||
+ " focused");
|
||||
} catch (Exception ex) {
|
||||
log.error(
|
||||
"Error focusing"
|
||||
+ " browser",
|
||||
ex);
|
||||
}
|
||||
});
|
||||
focusTimer.setRepeats(false);
|
||||
focusTimer.start();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Error during UI initialization", e);
|
||||
// Attempt cleanup on error
|
||||
if (loadingWindow != null) {
|
||||
loadingWindow.dispose();
|
||||
loadingWindow = null;
|
||||
}
|
||||
if (frame != null) {
|
||||
frame.setVisible(true);
|
||||
frame.requestFocus();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void setupTrayIcon(Image icon) {
|
||||
if (!SystemTray.isSupported()) {
|
||||
log.warn("System tray is not supported");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
systemTray = SystemTray.getSystemTray();
|
||||
|
||||
// Create popup menu
|
||||
PopupMenu popup = new PopupMenu();
|
||||
|
||||
// Create menu items
|
||||
MenuItem showItem = new MenuItem("Show");
|
||||
showItem.addActionListener(
|
||||
e -> {
|
||||
frame.setVisible(true);
|
||||
frame.setState(Frame.NORMAL);
|
||||
});
|
||||
|
||||
MenuItem exitItem = new MenuItem("Exit");
|
||||
exitItem.addActionListener(
|
||||
e -> {
|
||||
cleanup();
|
||||
System.exit(0);
|
||||
});
|
||||
|
||||
// Add menu items to popup menu
|
||||
popup.add(showItem);
|
||||
popup.addSeparator();
|
||||
popup.add(exitItem);
|
||||
|
||||
// Create tray icon
|
||||
trayIcon = new TrayIcon(icon, "Stirling-PDF", popup);
|
||||
trayIcon.setImageAutoSize(true);
|
||||
|
||||
// Add double-click behavior
|
||||
trayIcon.addActionListener(
|
||||
e -> {
|
||||
frame.setVisible(true);
|
||||
frame.setState(Frame.NORMAL);
|
||||
});
|
||||
|
||||
// Add tray icon to system tray
|
||||
systemTray.add(trayIcon);
|
||||
|
||||
// Modify frame behavior to minimize to tray
|
||||
frame.addWindowStateListener(
|
||||
new WindowStateListener() {
|
||||
public void windowStateChanged(WindowEvent e) {
|
||||
if (e.getNewState() == Frame.ICONIFIED) {
|
||||
frame.setVisible(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
} catch (AWTException e) {
|
||||
log.error("Error setting up system tray icon", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void loadIcon() {
|
||||
try {
|
||||
Image icon = null;
|
||||
String[] iconPaths = {"/static/favicon.ico"};
|
||||
|
||||
for (String path : iconPaths) {
|
||||
if (icon != null) break;
|
||||
try {
|
||||
try (InputStream is = getClass().getResourceAsStream(path)) {
|
||||
if (is != null) {
|
||||
icon = ImageIO.read(is);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.debug("Could not load icon from " + path, e);
|
||||
}
|
||||
}
|
||||
|
||||
if (icon != null) {
|
||||
frame.setIconImage(icon);
|
||||
setupTrayIcon(icon);
|
||||
} else {
|
||||
log.warn("Could not load icon from any source");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Error loading icon", e);
|
||||
}
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void cleanup() {
|
||||
if (browser != null) browser.close(true);
|
||||
if (client != null) client.dispose();
|
||||
if (cefApp != null) cefApp.dispose();
|
||||
if (loadingWindow != null) loadingWindow.dispose();
|
||||
}
|
||||
|
||||
public static void forceInitializeUI() {
|
||||
try {
|
||||
if (loadingWindow != null) {
|
||||
log.info("Forcing start of UI initialization sequence");
|
||||
|
||||
// Close loading window first
|
||||
loadingWindow.setVisible(false);
|
||||
loadingWindow.dispose();
|
||||
loadingWindow = null;
|
||||
log.info("Loading window disposed");
|
||||
|
||||
// Then setup the main frame
|
||||
frame.setVisible(false);
|
||||
frame.dispose();
|
||||
frame.setOpacity(1.0f);
|
||||
frame.setUndecorated(false);
|
||||
frame.pack();
|
||||
frame.setSize(UIScaling.scaleWidth(1280), UIScaling.scaleHeight(800));
|
||||
frame.setLocationRelativeTo(null);
|
||||
log.debug("Frame reconfigured");
|
||||
|
||||
// Show the main frame
|
||||
frame.setVisible(true);
|
||||
frame.requestFocus();
|
||||
frame.toFront();
|
||||
log.info("Main frame displayed and focused");
|
||||
|
||||
// Focus the browser component if available
|
||||
if (browser != null) {
|
||||
Timer focusTimer =
|
||||
new Timer(
|
||||
100,
|
||||
e -> {
|
||||
try {
|
||||
browser.getUIComponent().requestFocus();
|
||||
log.info("Browser component focused");
|
||||
} catch (Exception ex) {
|
||||
log.error(
|
||||
"Error focusing browser during force ui"
|
||||
+ " initialization.",
|
||||
ex);
|
||||
}
|
||||
});
|
||||
focusTimer.setRepeats(false);
|
||||
focusTimer.start();
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Error during Forced UI initialization.", e);
|
||||
// Attempt cleanup on error
|
||||
if (loadingWindow != null) {
|
||||
loadingWindow.dispose();
|
||||
loadingWindow = null;
|
||||
}
|
||||
if (frame != null) {
|
||||
frame.setVisible(true);
|
||||
frame.setOpacity(1.0f);
|
||||
frame.setUndecorated(false);
|
||||
frame.requestFocus();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,351 +0,0 @@
|
||||
package stirling.software.SPDF.UI.impl;
|
||||
|
||||
import java.awt.*;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.swing.*;
|
||||
|
||||
import io.github.pixee.security.BoundedLineReader;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.util.UIScaling;
|
||||
|
||||
@Slf4j
|
||||
public class LoadingWindow extends JDialog {
|
||||
private final JProgressBar progressBar;
|
||||
private final JLabel statusLabel;
|
||||
private final JPanel mainPanel;
|
||||
private final JLabel brandLabel;
|
||||
private long startTime;
|
||||
|
||||
private Timer stuckTimer;
|
||||
private long stuckThreshold = 4000;
|
||||
private long timeAt90Percent = -1;
|
||||
private volatile Process explorerProcess;
|
||||
private static final boolean IS_WINDOWS =
|
||||
System.getProperty("os.name").toLowerCase().contains("win");
|
||||
|
||||
public LoadingWindow(Frame parent, String initialUrl) {
|
||||
super(parent, "Initializing Stirling-PDF", true);
|
||||
startTime = System.currentTimeMillis();
|
||||
log.info("Creating LoadingWindow - initialization started at: {}", startTime);
|
||||
|
||||
// Initialize components
|
||||
mainPanel = new JPanel();
|
||||
mainPanel.setBackground(Color.WHITE);
|
||||
mainPanel.setBorder(BorderFactory.createEmptyBorder(20, 30, 20, 30));
|
||||
mainPanel.setLayout(new GridBagLayout());
|
||||
GridBagConstraints gbc = new GridBagConstraints();
|
||||
|
||||
// Configure GridBagConstraints
|
||||
gbc.gridwidth = GridBagConstraints.REMAINDER;
|
||||
gbc.fill = GridBagConstraints.HORIZONTAL;
|
||||
gbc.insets = new Insets(5, 5, 5, 5);
|
||||
gbc.weightx = 1.0;
|
||||
gbc.weighty = 0.0;
|
||||
|
||||
// Add icon
|
||||
try {
|
||||
try (InputStream is = getClass().getResourceAsStream("/static/favicon.ico")) {
|
||||
if (is != null) {
|
||||
Image img = ImageIO.read(is);
|
||||
if (img != null) {
|
||||
Image scaledImg = UIScaling.scaleIcon(img, 48, 48);
|
||||
JLabel iconLabel = new JLabel(new ImageIcon(scaledImg));
|
||||
iconLabel.setHorizontalAlignment(SwingConstants.CENTER);
|
||||
gbc.gridy = 0;
|
||||
mainPanel.add(iconLabel, gbc);
|
||||
log.info("Icon loaded and scaled successfully");
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to load icon", e);
|
||||
}
|
||||
|
||||
// URL Label with explicit size
|
||||
brandLabel = new JLabel(initialUrl);
|
||||
brandLabel.setHorizontalAlignment(SwingConstants.CENTER);
|
||||
brandLabel.setPreferredSize(new Dimension(300, 25));
|
||||
brandLabel.setText("Stirling-PDF");
|
||||
gbc.gridy = 1;
|
||||
mainPanel.add(brandLabel, gbc);
|
||||
|
||||
// Status label with explicit size
|
||||
statusLabel = new JLabel("Initializing...");
|
||||
statusLabel.setHorizontalAlignment(SwingConstants.CENTER);
|
||||
statusLabel.setPreferredSize(new Dimension(300, 25));
|
||||
gbc.gridy = 2;
|
||||
mainPanel.add(statusLabel, gbc);
|
||||
|
||||
// Progress bar with explicit size
|
||||
progressBar = new JProgressBar(0, 100);
|
||||
progressBar.setStringPainted(true);
|
||||
progressBar.setPreferredSize(new Dimension(300, 25));
|
||||
gbc.gridy = 3;
|
||||
mainPanel.add(progressBar, gbc);
|
||||
|
||||
// Set dialog properties
|
||||
setContentPane(mainPanel);
|
||||
setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE);
|
||||
setResizable(false);
|
||||
setUndecorated(false);
|
||||
|
||||
// Set size and position
|
||||
setSize(UIScaling.scaleWidth(400), UIScaling.scaleHeight(200));
|
||||
|
||||
setLocationRelativeTo(parent);
|
||||
setAlwaysOnTop(true);
|
||||
setProgress(0);
|
||||
setStatus("Starting...");
|
||||
|
||||
log.info(
|
||||
"LoadingWindow initialization completed in {}ms",
|
||||
System.currentTimeMillis() - startTime);
|
||||
}
|
||||
|
||||
private void checkAndRefreshExplorer() {
|
||||
if (!IS_WINDOWS) {
|
||||
return;
|
||||
}
|
||||
if (timeAt90Percent == -1) {
|
||||
timeAt90Percent = System.currentTimeMillis();
|
||||
stuckTimer =
|
||||
new Timer(
|
||||
1000,
|
||||
e -> {
|
||||
long currentTime = System.currentTimeMillis();
|
||||
if (currentTime - timeAt90Percent > stuckThreshold) {
|
||||
try {
|
||||
log.debug(
|
||||
"Attempting Windows explorer refresh due to 90% stuck state");
|
||||
String currentDir = System.getProperty("user.dir");
|
||||
|
||||
// Store current explorer PIDs before we start new one
|
||||
Set<String> existingPids = new HashSet<>();
|
||||
ProcessBuilder listExplorer =
|
||||
new ProcessBuilder(
|
||||
"cmd",
|
||||
"/c",
|
||||
"wmic",
|
||||
"process",
|
||||
"where",
|
||||
"name='explorer.exe'",
|
||||
"get",
|
||||
"ProcessId",
|
||||
"/format:csv");
|
||||
Process process = listExplorer.start();
|
||||
BufferedReader reader =
|
||||
new BufferedReader(
|
||||
new InputStreamReader(
|
||||
process.getInputStream()));
|
||||
String line;
|
||||
while ((line =
|
||||
BoundedLineReader.readLine(
|
||||
reader, 5_000_000))
|
||||
!= null) {
|
||||
if (line.matches(".*\\d+.*")) { // Contains numbers
|
||||
String[] parts = line.trim().split(",");
|
||||
if (parts.length >= 2) {
|
||||
existingPids.add(
|
||||
parts[parts.length - 1].trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
process.waitFor(2, TimeUnit.SECONDS);
|
||||
|
||||
// Start new explorer
|
||||
ProcessBuilder pb =
|
||||
new ProcessBuilder(
|
||||
"cmd",
|
||||
"/c",
|
||||
"start",
|
||||
"/min",
|
||||
"/b",
|
||||
"explorer.exe",
|
||||
currentDir);
|
||||
pb.redirectErrorStream(true);
|
||||
explorerProcess = pb.start();
|
||||
|
||||
// Schedule cleanup
|
||||
Timer cleanupTimer =
|
||||
new Timer(
|
||||
2000,
|
||||
cleanup -> {
|
||||
try {
|
||||
// Find new explorer processes
|
||||
ProcessBuilder findNewExplorer =
|
||||
new ProcessBuilder(
|
||||
"cmd",
|
||||
"/c",
|
||||
"wmic",
|
||||
"process",
|
||||
"where",
|
||||
"name='explorer.exe'",
|
||||
"get",
|
||||
"ProcessId",
|
||||
"/format:csv");
|
||||
Process newProcess =
|
||||
findNewExplorer.start();
|
||||
BufferedReader newReader =
|
||||
new BufferedReader(
|
||||
new InputStreamReader(
|
||||
newProcess
|
||||
.getInputStream()));
|
||||
String newLine;
|
||||
while ((newLine =
|
||||
BoundedLineReader
|
||||
.readLine(
|
||||
newReader,
|
||||
5_000_000))
|
||||
!= null) {
|
||||
if (newLine.matches(
|
||||
".*\\d+.*")) {
|
||||
String[] parts =
|
||||
newLine.trim()
|
||||
.split(",");
|
||||
if (parts.length >= 2) {
|
||||
String pid =
|
||||
parts[
|
||||
parts.length
|
||||
- 1]
|
||||
.trim();
|
||||
if (!existingPids
|
||||
.contains(
|
||||
pid)) {
|
||||
log.debug(
|
||||
"Found new explorer.exe with PID: "
|
||||
+ pid);
|
||||
ProcessBuilder
|
||||
killProcess =
|
||||
new ProcessBuilder(
|
||||
"taskkill",
|
||||
"/PID",
|
||||
pid,
|
||||
"/F");
|
||||
killProcess
|
||||
.redirectErrorStream(
|
||||
true);
|
||||
Process killResult =
|
||||
killProcess
|
||||
.start();
|
||||
killResult.waitFor(
|
||||
2,
|
||||
TimeUnit
|
||||
.SECONDS);
|
||||
log.debug(
|
||||
"Explorer process terminated: "
|
||||
+ pid);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
newProcess.waitFor(
|
||||
2, TimeUnit.SECONDS);
|
||||
} catch (Exception ex) {
|
||||
log.error(
|
||||
"Error cleaning up Windows explorer process",
|
||||
ex);
|
||||
}
|
||||
});
|
||||
cleanupTimer.setRepeats(false);
|
||||
cleanupTimer.start();
|
||||
stuckTimer.stop();
|
||||
} catch (Exception ex) {
|
||||
log.error("Error refreshing Windows explorer", ex);
|
||||
}
|
||||
}
|
||||
});
|
||||
stuckTimer.setRepeats(true);
|
||||
stuckTimer.start();
|
||||
}
|
||||
}
|
||||
|
||||
public void setProgress(final int progress) {
|
||||
SwingUtilities.invokeLater(
|
||||
() -> {
|
||||
try {
|
||||
int validProgress = Math.min(Math.max(progress, 0), 100);
|
||||
log.info(
|
||||
"Setting progress to {}% at {}ms since start",
|
||||
validProgress, System.currentTimeMillis() - startTime);
|
||||
|
||||
// Log additional details when near 90%
|
||||
if (validProgress >= 85 && validProgress <= 95) {
|
||||
log.info(
|
||||
"Near 90% progress - Current status: {}, Window visible: {}, "
|
||||
+ "Progress bar responding: {}, Memory usage: {}MB",
|
||||
statusLabel.getText(),
|
||||
isVisible(),
|
||||
progressBar.isEnabled(),
|
||||
Runtime.getRuntime().totalMemory() / (1024 * 1024));
|
||||
|
||||
// Add thread state logging
|
||||
Thread currentThread = Thread.currentThread();
|
||||
log.info(
|
||||
"Current thread state - Name: {}, State: {}, Priority: {}",
|
||||
currentThread.getName(),
|
||||
currentThread.getState(),
|
||||
currentThread.getPriority());
|
||||
|
||||
if (validProgress >= 90 && validProgress < 95) {
|
||||
checkAndRefreshExplorer();
|
||||
} else {
|
||||
// Reset the timer if we move past 95%
|
||||
if (validProgress >= 95) {
|
||||
if (stuckTimer != null) {
|
||||
stuckTimer.stop();
|
||||
}
|
||||
timeAt90Percent = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
progressBar.setValue(validProgress);
|
||||
progressBar.setString(validProgress + "%");
|
||||
mainPanel.revalidate();
|
||||
mainPanel.repaint();
|
||||
} catch (Exception e) {
|
||||
log.error("Error updating progress to " + progress, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void setStatus(final String status) {
|
||||
log.info(
|
||||
"Status update at {}ms - Setting status to: {}",
|
||||
System.currentTimeMillis() - startTime,
|
||||
status);
|
||||
|
||||
SwingUtilities.invokeLater(
|
||||
() -> {
|
||||
try {
|
||||
String validStatus = status != null ? status : "";
|
||||
statusLabel.setText(validStatus);
|
||||
|
||||
// Log UI state when status changes
|
||||
log.info(
|
||||
"UI State - Window visible: {}, Progress: {}%, Status: {}",
|
||||
isVisible(), progressBar.getValue(), validStatus);
|
||||
|
||||
mainPanel.revalidate();
|
||||
mainPanel.repaint();
|
||||
} catch (Exception e) {
|
||||
log.error("Error updating status to: " + status, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
log.info("LoadingWindow disposing after {}ms", System.currentTimeMillis() - startTime);
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import io.swagger.v3.oas.models.info.Info;
|
||||
import io.swagger.v3.oas.models.info.License;
|
||||
import io.swagger.v3.oas.models.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.models.security.SecurityScheme;
|
||||
import io.swagger.v3.oas.models.servers.Server;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@@ -50,17 +51,25 @@ public class OpenApiConfig {
|
||||
.url("https://www.stirlingpdf.com")
|
||||
.email("contact@stirlingpdf.com"))
|
||||
.description(DEFAULT_DESCRIPTION);
|
||||
|
||||
OpenAPI openAPI = new OpenAPI().info(info);
|
||||
|
||||
// Add server configuration from environment variable
|
||||
String swaggerServerUrl = System.getenv("SWAGGER_SERVER_URL");
|
||||
if (swaggerServerUrl != null && !swaggerServerUrl.trim().isEmpty()) {
|
||||
Server server = new Server().url(swaggerServerUrl).description("API Server");
|
||||
openAPI.addServersItem(server);
|
||||
}
|
||||
|
||||
if (!applicationProperties.getSecurity().getEnableLogin()) {
|
||||
return new OpenAPI().components(new Components()).info(info);
|
||||
return openAPI.components(new Components());
|
||||
} else {
|
||||
SecurityScheme apiKeyScheme =
|
||||
new SecurityScheme()
|
||||
.type(SecurityScheme.Type.APIKEY)
|
||||
.in(SecurityScheme.In.HEADER)
|
||||
.name("X-API-KEY");
|
||||
return new OpenAPI()
|
||||
.components(new Components().addSecuritySchemes("apiKey", apiKeyScheme))
|
||||
.info(info)
|
||||
return openAPI.components(new Components().addSecuritySchemes("apiKey", apiKeyScheme))
|
||||
.addSecurityItem(new SecurityRequirement().addList("apiKey"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,13 +2,10 @@ package stirling.software.SPDF.config;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import stirling.software.common.configuration.InstallationPathConfig;
|
||||
|
||||
@Configuration
|
||||
@RequiredArgsConstructor
|
||||
public class WebMvcConfig implements WebMvcConfigurer {
|
||||
@@ -20,12 +17,12 @@ public class WebMvcConfig implements WebMvcConfigurer {
|
||||
registry.addInterceptor(endpointInterceptor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||
// Handler for external static resources
|
||||
registry.addResourceHandler("/**")
|
||||
.addResourceLocations(
|
||||
"file:" + InstallationPathConfig.getStaticPath(), "classpath:/static/");
|
||||
// .setCachePeriod(0); // Optional: disable caching
|
||||
}
|
||||
// @Override
|
||||
// public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||
// // Handler for external static resources - DISABLED in backend-only mode
|
||||
// registry.addResourceHandler("/**")
|
||||
// .addResourceLocations(
|
||||
// "file:" + InstallationPathConfig.getStaticPath(), "classpath:/static/");
|
||||
// // .setCachePeriod(0); // Optional: disable caching
|
||||
// }
|
||||
}
|
||||
|
||||
+9
-8
@@ -18,6 +18,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.model.api.PDFFile;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
|
||||
@@ -29,7 +30,7 @@ public class AnalysisController {
|
||||
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
|
||||
@PostMapping(value = "/page-count", consumes = "multipart/form-data")
|
||||
@AutoJobPostMapping(value = "/page-count", consumes = "multipart/form-data")
|
||||
@Operation(
|
||||
summary = "Get PDF page count",
|
||||
description = "Returns total number of pages in PDF. Input:PDF Output:JSON Type:SISO")
|
||||
@@ -39,7 +40,7 @@ public class AnalysisController {
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping(value = "/basic-info", consumes = "multipart/form-data")
|
||||
@AutoJobPostMapping(value = "/basic-info", consumes = "multipart/form-data")
|
||||
@Operation(
|
||||
summary = "Get basic PDF information",
|
||||
description = "Returns page count, version, file size. Input:PDF Output:JSON Type:SISO")
|
||||
@@ -53,7 +54,7 @@ public class AnalysisController {
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping(value = "/document-properties", consumes = "multipart/form-data")
|
||||
@AutoJobPostMapping(value = "/document-properties", consumes = "multipart/form-data")
|
||||
@Operation(
|
||||
summary = "Get PDF document properties",
|
||||
description = "Returns title, author, subject, etc. Input:PDF Output:JSON Type:SISO")
|
||||
@@ -76,7 +77,7 @@ public class AnalysisController {
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping(value = "/page-dimensions", consumes = "multipart/form-data")
|
||||
@AutoJobPostMapping(value = "/page-dimensions", consumes = "multipart/form-data")
|
||||
@Operation(
|
||||
summary = "Get page dimensions for all pages",
|
||||
description = "Returns width and height of each page. Input:PDF Output:JSON Type:SISO")
|
||||
@@ -96,7 +97,7 @@ public class AnalysisController {
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping(value = "/form-fields", consumes = "multipart/form-data")
|
||||
@AutoJobPostMapping(value = "/form-fields", consumes = "multipart/form-data")
|
||||
@Operation(
|
||||
summary = "Get form field information",
|
||||
description =
|
||||
@@ -119,7 +120,7 @@ public class AnalysisController {
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping(value = "/annotation-info", consumes = "multipart/form-data")
|
||||
@AutoJobPostMapping(value = "/annotation-info", consumes = "multipart/form-data")
|
||||
@Operation(
|
||||
summary = "Get annotation information",
|
||||
description = "Returns count and types of annotations. Input:PDF Output:JSON Type:SISO")
|
||||
@@ -143,7 +144,7 @@ public class AnalysisController {
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping(value = "/font-info", consumes = "multipart/form-data")
|
||||
@AutoJobPostMapping(value = "/font-info", consumes = "multipart/form-data")
|
||||
@Operation(
|
||||
summary = "Get font information",
|
||||
description =
|
||||
@@ -165,7 +166,7 @@ public class AnalysisController {
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping(value = "/security-info", consumes = "multipart/form-data")
|
||||
@AutoJobPostMapping(value = "/security-info", consumes = "multipart/form-data")
|
||||
@Operation(
|
||||
summary = "Get security information",
|
||||
description =
|
||||
|
||||
@@ -12,7 +12,6 @@ import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@@ -22,6 +21,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import stirling.software.SPDF.model.api.general.CropPdfForm;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
|
||||
@@ -33,7 +33,7 @@ public class CropController {
|
||||
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
|
||||
@PostMapping(value = "/crop", consumes = "multipart/form-data")
|
||||
@AutoJobPostMapping(value = "/crop", consumes = "multipart/form-data")
|
||||
@Operation(
|
||||
summary = "Crops a PDF document",
|
||||
description =
|
||||
|
||||
+3
-3
@@ -14,7 +14,6 @@ import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDOutlin
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
@@ -31,6 +30,7 @@ import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.model.api.EditTableOfContentsRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
|
||||
@@ -44,7 +44,7 @@ public class EditTableOfContentsController {
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
@PostMapping(value = "/extract-bookmarks", consumes = "multipart/form-data")
|
||||
@AutoJobPostMapping(value = "/extract-bookmarks", consumes = "multipart/form-data")
|
||||
@Operation(
|
||||
summary = "Extract PDF Bookmarks",
|
||||
description = "Extracts bookmarks/table of contents from a PDF document as JSON.")
|
||||
@@ -152,7 +152,7 @@ public class EditTableOfContentsController {
|
||||
return bookmark;
|
||||
}
|
||||
|
||||
@PostMapping(value = "/edit-table-of-contents", consumes = "multipart/form-data")
|
||||
@AutoJobPostMapping(value = "/edit-table-of-contents", consumes = "multipart/form-data")
|
||||
@Operation(
|
||||
summary = "Edit Table of Contents",
|
||||
description = "Add or edit bookmarks/table of contents in a PDF document.")
|
||||
|
||||
@@ -22,7 +22,6 @@ import org.apache.pdfbox.pdmodel.interactive.form.PDField;
|
||||
import org.apache.pdfbox.pdmodel.interactive.form.PDSignatureField;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
@@ -34,6 +33,7 @@ import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.model.api.general.MergePdfsRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
@@ -154,7 +154,7 @@ public class MergeController {
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/merge-pdfs")
|
||||
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/merge-pdfs")
|
||||
@Operation(
|
||||
summary = "Merge multiple PDF files into one",
|
||||
description =
|
||||
|
||||
+2
-2
@@ -13,7 +13,6 @@ import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
|
||||
import org.apache.pdfbox.util.Matrix;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
@@ -25,6 +24,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import stirling.software.SPDF.model.api.general.MergeMultiplePagesRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
|
||||
@@ -36,7 +36,7 @@ public class MultiPageLayoutController {
|
||||
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
|
||||
@PostMapping(value = "/multi-page-layout", consumes = "multipart/form-data")
|
||||
@AutoJobPostMapping(value = "/multi-page-layout", consumes = "multipart/form-data")
|
||||
@Operation(
|
||||
summary = "Merge multiple pages of a PDF document into a single page",
|
||||
description =
|
||||
|
||||
+2
-2
@@ -6,7 +6,6 @@ import java.io.IOException;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@@ -16,6 +15,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import stirling.software.SPDF.service.PdfImageRemovalService;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.model.api.PDFFile;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
@@ -46,7 +46,7 @@ public class PdfImageRemovalController {
|
||||
* content type and filename.
|
||||
* @throws IOException If an error occurs while processing the PDF file.
|
||||
*/
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/remove-image-pdf")
|
||||
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/remove-image-pdf")
|
||||
@Operation(
|
||||
summary = "Remove images from file to reduce the file size.",
|
||||
description =
|
||||
|
||||
+2
-2
@@ -15,7 +15,6 @@ import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
@@ -27,6 +26,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import stirling.software.SPDF.model.api.general.OverlayPdfsRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
@@ -39,7 +39,7 @@ public class PdfOverlayController {
|
||||
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
|
||||
@PostMapping(value = "/overlay-pdfs", consumes = "multipart/form-data")
|
||||
@AutoJobPostMapping(value = "/overlay-pdfs", consumes = "multipart/form-data")
|
||||
@Operation(
|
||||
summary = "Overlay PDF files in various modes",
|
||||
description =
|
||||
|
||||
+3
-3
@@ -9,7 +9,6 @@ import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
@@ -24,6 +23,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import stirling.software.SPDF.model.SortTypes;
|
||||
import stirling.software.SPDF.model.api.PDFWithPageNums;
|
||||
import stirling.software.SPDF.model.api.general.RearrangePagesRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
@@ -38,7 +38,7 @@ public class RearrangePagesPDFController {
|
||||
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/remove-pages")
|
||||
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/remove-pages")
|
||||
@Operation(
|
||||
summary = "Remove pages from a PDF file",
|
||||
description =
|
||||
@@ -237,7 +237,7 @@ public class RearrangePagesPDFController {
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/rearrange-pages")
|
||||
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/rearrange-pages")
|
||||
@Operation(
|
||||
summary = "Rearrange pages in a PDF file",
|
||||
description =
|
||||
|
||||
+2
-2
@@ -7,7 +7,6 @@ import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.apache.pdfbox.pdmodel.PDPageTree;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
@@ -19,6 +18,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import stirling.software.SPDF.model.api.general.RotatePDFRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
@@ -31,7 +31,7 @@ public class RotationController {
|
||||
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/rotate-pdf")
|
||||
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/rotate-pdf")
|
||||
@Operation(
|
||||
summary = "Rotate a PDF file",
|
||||
description =
|
||||
|
||||
+2
-2
@@ -14,7 +14,6 @@ import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
|
||||
import org.apache.pdfbox.util.Matrix;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
@@ -26,6 +25,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import stirling.software.SPDF.model.api.general.ScalePagesRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
@@ -38,7 +38,7 @@ public class ScalePagesController {
|
||||
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
|
||||
@PostMapping(value = "/scale-pages", consumes = "multipart/form-data")
|
||||
@AutoJobPostMapping(value = "/scale-pages", consumes = "multipart/form-data")
|
||||
@Operation(
|
||||
summary = "Change the size of a PDF page/document",
|
||||
description =
|
||||
|
||||
+2
-2
@@ -7,7 +7,6 @@ import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
|
||||
@@ -17,6 +16,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import stirling.software.SPDF.config.EndpointConfiguration;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.configuration.InstallationPathConfig;
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
@@ -31,7 +31,7 @@ public class SettingsController {
|
||||
private final ApplicationProperties applicationProperties;
|
||||
private final EndpointConfiguration endpointConfiguration;
|
||||
|
||||
@PostMapping("/update-enable-analytics")
|
||||
@AutoJobPostMapping("/update-enable-analytics")
|
||||
@Hidden
|
||||
public ResponseEntity<String> updateApiKey(@RequestBody Boolean enabled) throws IOException {
|
||||
if (applicationProperties.getSystem().getEnableAnalytics() != null) {
|
||||
|
||||
+2
-2
@@ -15,7 +15,6 @@ import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
@@ -28,6 +27,7 @@ import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.model.api.PDFWithPageNums;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
@@ -41,7 +41,7 @@ public class SplitPDFController {
|
||||
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/split-pages")
|
||||
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/split-pages")
|
||||
@Operation(
|
||||
summary = "Split a PDF file into separate documents",
|
||||
description =
|
||||
|
||||
+2
-2
@@ -15,7 +15,6 @@ import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDOutlin
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
@@ -32,6 +31,7 @@ import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.model.api.SplitPdfByChaptersRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.model.PdfMetadata;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.service.PdfMetadataService;
|
||||
@@ -117,7 +117,7 @@ public class SplitPdfByChaptersController {
|
||||
return bookmarks;
|
||||
}
|
||||
|
||||
@PostMapping(value = "/split-pdf-by-chapters", consumes = "multipart/form-data")
|
||||
@AutoJobPostMapping(value = "/split-pdf-by-chapters", consumes = "multipart/form-data")
|
||||
@Operation(
|
||||
summary = "Split PDFs by Chapters",
|
||||
description = "Splits a PDF into chapters and returns a ZIP file.")
|
||||
|
||||
+2
-2
@@ -20,7 +20,6 @@ import org.apache.pdfbox.util.Matrix;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
@@ -32,6 +31,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import stirling.software.SPDF.model.api.SplitPdfBySectionsRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
|
||||
@@ -43,7 +43,7 @@ public class SplitPdfBySectionsController {
|
||||
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
|
||||
@PostMapping(value = "/split-pdf-by-sections", consumes = "multipart/form-data")
|
||||
@AutoJobPostMapping(value = "/split-pdf-by-sections", consumes = "multipart/form-data")
|
||||
@Operation(
|
||||
summary = "Split PDF pages into smaller sections",
|
||||
description =
|
||||
|
||||
+2
-2
@@ -12,7 +12,6 @@ import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
@@ -25,6 +24,7 @@ import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.model.api.general.SplitPdfBySizeOrCountRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
@@ -39,7 +39,7 @@ public class SplitPdfBySizeController {
|
||||
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
|
||||
@PostMapping(value = "/split-by-size-or-count", consumes = "multipart/form-data")
|
||||
@AutoJobPostMapping(value = "/split-by-size-or-count", consumes = "multipart/form-data")
|
||||
@Operation(
|
||||
summary = "Auto split PDF pages into separate documents based on size or count",
|
||||
description =
|
||||
|
||||
+2
-2
@@ -12,7 +12,6 @@ import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@@ -21,6 +20,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.model.api.PDFFile;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
@@ -33,7 +33,7 @@ public class ToSinglePageController {
|
||||
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/pdf-to-single-page")
|
||||
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/pdf-to-single-page")
|
||||
@Operation(
|
||||
summary = "Convert a multi-page PDF into a single long page PDF",
|
||||
description =
|
||||
|
||||
@@ -0,0 +1,300 @@
|
||||
package stirling.software.SPDF.controller.api;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.*;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.model.Dependency;
|
||||
import stirling.software.SPDF.model.SignatureFile;
|
||||
import stirling.software.SPDF.service.SignatureService;
|
||||
import stirling.software.common.configuration.InstallationPathConfig;
|
||||
import stirling.software.common.configuration.RuntimePathConfig;
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.service.UserServiceInterface;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/ui-data")
|
||||
@Tag(name = "UI Data", description = "APIs for React UI data")
|
||||
public class UIDataController {
|
||||
|
||||
private final ApplicationProperties applicationProperties;
|
||||
private final SignatureService signatureService;
|
||||
private final UserServiceInterface userService;
|
||||
private final ResourceLoader resourceLoader;
|
||||
private final RuntimePathConfig runtimePathConfig;
|
||||
|
||||
public UIDataController(
|
||||
ApplicationProperties applicationProperties,
|
||||
SignatureService signatureService,
|
||||
@Autowired(required = false) UserServiceInterface userService,
|
||||
ResourceLoader resourceLoader,
|
||||
RuntimePathConfig runtimePathConfig) {
|
||||
this.applicationProperties = applicationProperties;
|
||||
this.signatureService = signatureService;
|
||||
this.userService = userService;
|
||||
this.resourceLoader = resourceLoader;
|
||||
this.runtimePathConfig = runtimePathConfig;
|
||||
}
|
||||
|
||||
@GetMapping("/home")
|
||||
@Operation(summary = "Get home page data")
|
||||
public ResponseEntity<HomeData> getHomeData() {
|
||||
String showSurvey = System.getenv("SHOW_SURVEY");
|
||||
boolean showSurveyValue = showSurvey == null || "true".equalsIgnoreCase(showSurvey);
|
||||
|
||||
HomeData data = new HomeData();
|
||||
data.setShowSurveyFromDocker(showSurveyValue);
|
||||
|
||||
return ResponseEntity.ok(data);
|
||||
}
|
||||
|
||||
@GetMapping("/licenses")
|
||||
@Operation(summary = "Get third-party licenses data")
|
||||
public ResponseEntity<LicensesData> getLicensesData() {
|
||||
LicensesData data = new LicensesData();
|
||||
Resource resource = new ClassPathResource("static/3rdPartyLicenses.json");
|
||||
|
||||
try {
|
||||
InputStream is = resource.getInputStream();
|
||||
String json = new String(is.readAllBytes(), StandardCharsets.UTF_8);
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
Map<String, List<Dependency>> licenseData =
|
||||
mapper.readValue(json, new TypeReference<>() {});
|
||||
data.setDependencies(licenseData.get("dependencies"));
|
||||
} catch (IOException e) {
|
||||
log.error("Failed to load licenses data", e);
|
||||
data.setDependencies(Collections.emptyList());
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(data);
|
||||
}
|
||||
|
||||
@GetMapping("/pipeline")
|
||||
@Operation(summary = "Get pipeline configuration data")
|
||||
public ResponseEntity<PipelineData> getPipelineData() {
|
||||
PipelineData data = new PipelineData();
|
||||
List<String> pipelineConfigs = new ArrayList<>();
|
||||
List<Map<String, String>> pipelineConfigsWithNames = new ArrayList<>();
|
||||
|
||||
if (new java.io.File(runtimePathConfig.getPipelineDefaultWebUiConfigs()).exists()) {
|
||||
try (Stream<Path> paths =
|
||||
Files.walk(Paths.get(runtimePathConfig.getPipelineDefaultWebUiConfigs()))) {
|
||||
List<Path> jsonFiles =
|
||||
paths.filter(Files::isRegularFile)
|
||||
.filter(p -> p.toString().endsWith(".json"))
|
||||
.toList();
|
||||
|
||||
for (Path jsonFile : jsonFiles) {
|
||||
String content = Files.readString(jsonFile, StandardCharsets.UTF_8);
|
||||
pipelineConfigs.add(content);
|
||||
}
|
||||
|
||||
for (String config : pipelineConfigs) {
|
||||
Map<String, Object> jsonContent =
|
||||
new ObjectMapper()
|
||||
.readValue(config, new TypeReference<Map<String, Object>>() {});
|
||||
String name = (String) jsonContent.get("name");
|
||||
if (name == null || name.length() < 1) {
|
||||
String filename =
|
||||
jsonFiles
|
||||
.get(pipelineConfigs.indexOf(config))
|
||||
.getFileName()
|
||||
.toString();
|
||||
name = filename.substring(0, filename.lastIndexOf('.'));
|
||||
}
|
||||
Map<String, String> configWithName = new HashMap<>();
|
||||
configWithName.put("json", config);
|
||||
configWithName.put("name", name);
|
||||
pipelineConfigsWithNames.add(configWithName);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.error("Failed to load pipeline configs", e);
|
||||
}
|
||||
}
|
||||
|
||||
if (pipelineConfigsWithNames.isEmpty()) {
|
||||
Map<String, String> configWithName = new HashMap<>();
|
||||
configWithName.put("json", "");
|
||||
configWithName.put("name", "No preloaded configs found");
|
||||
pipelineConfigsWithNames.add(configWithName);
|
||||
}
|
||||
|
||||
data.setPipelineConfigsWithNames(pipelineConfigsWithNames);
|
||||
data.setPipelineConfigs(pipelineConfigs);
|
||||
|
||||
return ResponseEntity.ok(data);
|
||||
}
|
||||
|
||||
@GetMapping("/sign")
|
||||
@Operation(summary = "Get signature form data")
|
||||
public ResponseEntity<SignData> getSignData() {
|
||||
String username = "";
|
||||
if (userService != null) {
|
||||
username = userService.getCurrentUsername();
|
||||
}
|
||||
|
||||
List<SignatureFile> signatures = signatureService.getAvailableSignatures(username);
|
||||
List<FontResource> fonts = getFontNames();
|
||||
|
||||
SignData data = new SignData();
|
||||
data.setSignatures(signatures);
|
||||
data.setFonts(fonts);
|
||||
|
||||
return ResponseEntity.ok(data);
|
||||
}
|
||||
|
||||
@GetMapping("/ocr-pdf")
|
||||
@Operation(summary = "Get OCR PDF data")
|
||||
public ResponseEntity<OcrData> getOcrPdfData() {
|
||||
List<String> languages = getAvailableTesseractLanguages();
|
||||
|
||||
OcrData data = new OcrData();
|
||||
data.setLanguages(languages);
|
||||
|
||||
return ResponseEntity.ok(data);
|
||||
}
|
||||
|
||||
private List<String> getAvailableTesseractLanguages() {
|
||||
String tessdataDir = applicationProperties.getSystem().getTessdataDir();
|
||||
java.io.File[] files = new java.io.File(tessdataDir).listFiles();
|
||||
if (files == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return Arrays.stream(files)
|
||||
.filter(file -> file.getName().endsWith(".traineddata"))
|
||||
.map(file -> file.getName().replace(".traineddata", ""))
|
||||
.filter(lang -> !"osd".equalsIgnoreCase(lang))
|
||||
.sorted()
|
||||
.toList();
|
||||
}
|
||||
|
||||
private List<FontResource> getFontNames() {
|
||||
List<FontResource> fontNames = new ArrayList<>();
|
||||
fontNames.addAll(getFontNamesFromLocation("classpath:static/fonts/*.woff2"));
|
||||
fontNames.addAll(
|
||||
getFontNamesFromLocation(
|
||||
"file:"
|
||||
+ InstallationPathConfig.getStaticPath()
|
||||
+ "fonts"
|
||||
+ java.io.File.separator
|
||||
+ "*"));
|
||||
return fontNames;
|
||||
}
|
||||
|
||||
private List<FontResource> getFontNamesFromLocation(String locationPattern) {
|
||||
try {
|
||||
Resource[] resources =
|
||||
GeneralUtils.getResourcesFromLocationPattern(locationPattern, resourceLoader);
|
||||
return Arrays.stream(resources)
|
||||
.map(
|
||||
resource -> {
|
||||
try {
|
||||
String filename = resource.getFilename();
|
||||
if (filename != null) {
|
||||
int lastDotIndex = filename.lastIndexOf('.');
|
||||
if (lastDotIndex != -1) {
|
||||
String name = filename.substring(0, lastDotIndex);
|
||||
String extension = filename.substring(lastDotIndex + 1);
|
||||
return new FontResource(name, extension);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
} catch (Exception e) {
|
||||
throw ExceptionUtils.createRuntimeException(
|
||||
"error.fontLoadingFailed",
|
||||
"Error processing font file",
|
||||
e);
|
||||
}
|
||||
})
|
||||
.filter(Objects::nonNull)
|
||||
.toList();
|
||||
} catch (Exception e) {
|
||||
throw ExceptionUtils.createRuntimeException(
|
||||
"error.fontDirectoryReadFailed", "Failed to read font directory", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Data classes
|
||||
@Data
|
||||
public static class HomeData {
|
||||
private boolean showSurveyFromDocker;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class LicensesData {
|
||||
private List<Dependency> dependencies;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class PipelineData {
|
||||
private List<Map<String, String>> pipelineConfigsWithNames;
|
||||
private List<String> pipelineConfigs;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class SignData {
|
||||
private List<SignatureFile> signatures;
|
||||
private List<FontResource> fonts;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class OcrData {
|
||||
private List<String> languages;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class FontResource {
|
||||
private String name;
|
||||
private String extension;
|
||||
private String type;
|
||||
|
||||
public FontResource(String name, String extension) {
|
||||
this.name = name;
|
||||
this.extension = extension;
|
||||
this.type = getFormatFromExtension(extension);
|
||||
}
|
||||
|
||||
private static String getFormatFromExtension(String extension) {
|
||||
switch (extension) {
|
||||
case "ttf":
|
||||
return "truetype";
|
||||
case "woff":
|
||||
return "woff";
|
||||
case "woff2":
|
||||
return "woff2";
|
||||
case "eot":
|
||||
return "embedded-opentype";
|
||||
case "svg":
|
||||
return "svg";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -8,7 +8,6 @@ import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
@@ -20,6 +19,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.configuration.RuntimePathConfig;
|
||||
import stirling.software.common.model.api.converters.EmlToPdfRequest;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
@@ -40,7 +40,7 @@ public class ConvertEmlToPDF {
|
||||
private final TempFileManager tempFileManager;
|
||||
private final CustomHtmlSanitizer customHtmlSanitizer;
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/eml/pdf")
|
||||
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/eml/pdf")
|
||||
@Operation(
|
||||
summary = "Convert EML to PDF",
|
||||
description =
|
||||
|
||||
+3
-2
@@ -2,7 +2,6 @@ package stirling.software.SPDF.controller.api.converters;
|
||||
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
@@ -13,6 +12,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.configuration.RuntimePathConfig;
|
||||
import stirling.software.common.model.api.converters.HTMLToPdfRequest;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
@@ -36,7 +36,8 @@ public class ConvertHtmlToPDF {
|
||||
|
||||
private final CustomHtmlSanitizer customHtmlSanitizer;
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/html/pdf")
|
||||
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/html/pdf")
|
||||
|
||||
@Operation(
|
||||
summary = "Convert an HTML or ZIP (containing HTML and CSS) to PDF",
|
||||
description =
|
||||
|
||||
+3
-3
@@ -19,7 +19,6 @@ import org.apache.pdfbox.rendering.ImageType;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
@@ -33,6 +32,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.model.api.converters.ConvertToImageRequest;
|
||||
import stirling.software.SPDF.model.api.converters.ConvertToPdfRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.CheckProgramInstall;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
@@ -51,7 +51,7 @@ public class ConvertImgPDFController {
|
||||
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/pdf/img")
|
||||
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/pdf/img")
|
||||
@Operation(
|
||||
summary = "Convert PDF to image(s)",
|
||||
description =
|
||||
@@ -211,7 +211,7 @@ public class ConvertImgPDFController {
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/img/pdf")
|
||||
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/img/pdf")
|
||||
@Operation(
|
||||
summary = "Convert images to a PDF file",
|
||||
description =
|
||||
|
||||
+3
-2
@@ -12,7 +12,6 @@ import org.commonmark.renderer.html.AttributeProvider;
|
||||
import org.commonmark.renderer.html.HtmlRenderer;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
@@ -23,6 +22,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.configuration.RuntimePathConfig;
|
||||
import stirling.software.common.model.api.GeneralFile;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
@@ -45,7 +45,8 @@ public class ConvertMarkdownToPdf {
|
||||
|
||||
private final CustomHtmlSanitizer customHtmlSanitizer;
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/markdown/pdf")
|
||||
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/markdown/pdf")
|
||||
|
||||
@Operation(
|
||||
summary = "Convert a Markdown file to PDF",
|
||||
description =
|
||||
|
||||
+2
-2
@@ -13,7 +13,6 @@ import org.apache.commons.io.FilenameUtils;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
@@ -24,6 +23,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.configuration.RuntimePathConfig;
|
||||
import stirling.software.common.model.api.GeneralFile;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
@@ -97,7 +97,7 @@ public class ConvertOfficeController {
|
||||
return fileExtension.matches(extensionPattern);
|
||||
}
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/file/pdf")
|
||||
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/file/pdf")
|
||||
@Operation(
|
||||
summary = "Convert a file to a PDF using LibreOffice",
|
||||
description =
|
||||
|
||||
+2
-2
@@ -2,7 +2,6 @@ package stirling.software.SPDF.controller.api.converters;
|
||||
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
@@ -10,6 +9,7 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.model.api.PDFFile;
|
||||
import stirling.software.common.util.PDFToFile;
|
||||
|
||||
@@ -18,7 +18,7 @@ import stirling.software.common.util.PDFToFile;
|
||||
@RequestMapping("/api/v1/convert")
|
||||
public class ConvertPDFToHtml {
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/pdf/html")
|
||||
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/pdf/html")
|
||||
@Operation(
|
||||
summary = "Convert PDF to HTML",
|
||||
description =
|
||||
|
||||
+5
-5
@@ -7,7 +7,6 @@ import org.apache.pdfbox.text.PDFTextStripper;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
@@ -21,6 +20,7 @@ import lombok.RequiredArgsConstructor;
|
||||
import stirling.software.SPDF.model.api.converters.PdfToPresentationRequest;
|
||||
import stirling.software.SPDF.model.api.converters.PdfToTextOrRTFRequest;
|
||||
import stirling.software.SPDF.model.api.converters.PdfToWordRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.model.api.PDFFile;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.PDFToFile;
|
||||
@@ -34,7 +34,7 @@ public class ConvertPDFToOffice {
|
||||
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/pdf/presentation")
|
||||
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/pdf/presentation")
|
||||
@Operation(
|
||||
summary = "Convert PDF to Presentation format",
|
||||
description =
|
||||
@@ -49,7 +49,7 @@ public class ConvertPDFToOffice {
|
||||
return pdfToFile.processPdfToOfficeFormat(inputFile, outputFormat, "impress_pdf_import");
|
||||
}
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/pdf/text")
|
||||
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/pdf/text")
|
||||
@Operation(
|
||||
summary = "Convert PDF to Text or RTF format",
|
||||
description =
|
||||
@@ -77,7 +77,7 @@ public class ConvertPDFToOffice {
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/pdf/word")
|
||||
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/pdf/word")
|
||||
@Operation(
|
||||
summary = "Convert PDF to Word document",
|
||||
description =
|
||||
@@ -91,7 +91,7 @@ public class ConvertPDFToOffice {
|
||||
return pdfToFile.processPdfToOfficeFormat(inputFile, outputFormat, "writer_pdf_import");
|
||||
}
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/pdf/xml")
|
||||
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/pdf/xml")
|
||||
@Operation(
|
||||
summary = "Convert PDF to XML",
|
||||
description =
|
||||
|
||||
+2
-2
@@ -55,7 +55,6 @@ import org.apache.xmpbox.xml.XmpSerializer;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
@@ -67,6 +66,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.model.api.converters.PdfToPdfARequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.ProcessExecutor;
|
||||
import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
|
||||
@@ -78,7 +78,7 @@ import stirling.software.common.util.WebResponseUtils;
|
||||
@Tag(name = "Convert", description = "Convert APIs")
|
||||
public class ConvertPDFToPDFA {
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/pdf/pdfa")
|
||||
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/pdf/pdfa")
|
||||
@Operation(
|
||||
summary = "Convert a PDF to a PDF/A",
|
||||
description =
|
||||
|
||||
+2
-2
@@ -9,7 +9,6 @@ import java.util.List;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@@ -20,6 +19,7 @@ import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.model.api.converters.UrlToPdfRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.configuration.RuntimePathConfig;
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
@@ -40,7 +40,7 @@ public class ConvertWebsiteToPDF {
|
||||
private final RuntimePathConfig runtimePathConfig;
|
||||
private final ApplicationProperties applicationProperties;
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/url/pdf")
|
||||
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/url/pdf")
|
||||
@Operation(
|
||||
summary = "Convert a URL to a PDF",
|
||||
description =
|
||||
|
||||
+2
-2
@@ -18,7 +18,6 @@ import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@@ -30,6 +29,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.model.api.PDFWithPageNums;
|
||||
import stirling.software.SPDF.pdf.FlexibleCSVWriter;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
|
||||
import technology.tabula.ObjectExtractor;
|
||||
@@ -46,7 +46,7 @@ public class ExtractCSVController {
|
||||
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
|
||||
@PostMapping(value = "/pdf/csv", consumes = "multipart/form-data")
|
||||
@AutoJobPostMapping(value = "/pdf/csv", consumes = "multipart/form-data")
|
||||
@Operation(
|
||||
summary = "Extracts a CSV document from a PDF",
|
||||
description =
|
||||
|
||||
+7
-7
@@ -7,7 +7,6 @@ import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
@@ -24,6 +23,7 @@ import stirling.software.SPDF.model.api.filter.ContainsTextRequest;
|
||||
import stirling.software.SPDF.model.api.filter.FileSizeRequest;
|
||||
import stirling.software.SPDF.model.api.filter.PageRotationRequest;
|
||||
import stirling.software.SPDF.model.api.filter.PageSizeRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.PdfUtils;
|
||||
@@ -37,7 +37,7 @@ public class FilterController {
|
||||
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/filter-contains-text")
|
||||
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/filter-contains-text")
|
||||
@Operation(
|
||||
summary = "Checks if a PDF contains set text, returns true if does",
|
||||
description = "Input:PDF Output:Boolean Type:SISO")
|
||||
@@ -55,7 +55,7 @@ public class FilterController {
|
||||
}
|
||||
|
||||
// TODO
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/filter-contains-image")
|
||||
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/filter-contains-image")
|
||||
@Operation(
|
||||
summary = "Checks if a PDF contains an image",
|
||||
description = "Input:PDF Output:Boolean Type:SISO")
|
||||
@@ -71,7 +71,7 @@ public class FilterController {
|
||||
return null;
|
||||
}
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/filter-page-count")
|
||||
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/filter-page-count")
|
||||
@Operation(
|
||||
summary = "Checks if a PDF is greater, less or equal to a setPageCount",
|
||||
description = "Input:PDF Output:Boolean Type:SISO")
|
||||
@@ -104,7 +104,7 @@ public class FilterController {
|
||||
return null;
|
||||
}
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/filter-page-size")
|
||||
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/filter-page-size")
|
||||
@Operation(
|
||||
summary = "Checks if a PDF is of a certain size",
|
||||
description = "Input:PDF Output:Boolean Type:SISO")
|
||||
@@ -147,7 +147,7 @@ public class FilterController {
|
||||
return null;
|
||||
}
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/filter-file-size")
|
||||
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/filter-file-size")
|
||||
@Operation(
|
||||
summary = "Checks if a PDF is a set file size",
|
||||
description = "Input:PDF Output:Boolean Type:SISO")
|
||||
@@ -180,7 +180,7 @@ public class FilterController {
|
||||
return null;
|
||||
}
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/filter-page-rotation")
|
||||
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/filter-page-rotation")
|
||||
@Operation(
|
||||
summary = "Checks if a PDF is of a certain rotation",
|
||||
description = "Input:PDF Output:Boolean Type:SISO")
|
||||
|
||||
+2
-2
@@ -6,7 +6,6 @@ import java.util.List;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
@@ -20,6 +19,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.model.api.misc.AddAttachmentRequest;
|
||||
import stirling.software.SPDF.service.AttachmentServiceInterface;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
|
||||
@@ -34,7 +34,7 @@ public class AttachmentController {
|
||||
|
||||
private final AttachmentServiceInterface pdfAttachmentService;
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/add-attachments")
|
||||
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/add-attachments")
|
||||
@Operation(
|
||||
summary = "Add attachments to PDF",
|
||||
description =
|
||||
|
||||
+2
-2
@@ -10,7 +10,6 @@ import org.apache.pdfbox.text.PDFTextStripper;
|
||||
import org.apache.pdfbox.text.TextPosition;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
@@ -23,6 +22,7 @@ import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.model.api.misc.ExtractHeaderRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
|
||||
@@ -38,7 +38,7 @@ public class AutoRenameController {
|
||||
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/auto-rename")
|
||||
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/auto-rename")
|
||||
@Operation(
|
||||
summary = "Extract header from PDF file",
|
||||
description =
|
||||
|
||||
+2
-2
@@ -19,7 +19,6 @@ import org.apache.pdfbox.rendering.PDFRenderer;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
@@ -35,6 +34,7 @@ import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.model.api.misc.AutoSplitPdfRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
|
||||
@@ -102,7 +102,7 @@ public class AutoSplitPdfController {
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping(value = "/auto-split-pdf", consumes = "multipart/form-data")
|
||||
@AutoJobPostMapping(value = "/auto-split-pdf", consumes = "multipart/form-data")
|
||||
@Operation(
|
||||
summary = "Auto split PDF pages into separate documents",
|
||||
description =
|
||||
|
||||
+2
-2
@@ -17,7 +17,6 @@ import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
@@ -30,6 +29,7 @@ import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.model.api.misc.RemoveBlankPagesRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.PdfUtils;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
@@ -69,7 +69,7 @@ public class BlankPageController {
|
||||
return whitePixelPercentage >= whitePercent;
|
||||
}
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/remove-blanks")
|
||||
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/remove-blanks")
|
||||
@Operation(
|
||||
summary = "Remove blank pages from a PDF file",
|
||||
description =
|
||||
|
||||
+2
-2
@@ -34,7 +34,6 @@ import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
|
||||
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
@@ -52,6 +51,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.config.EndpointConfiguration;
|
||||
import stirling.software.SPDF.model.api.misc.OptimizePdfRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
@@ -658,7 +658,7 @@ public class CompressController {
|
||||
};
|
||||
}
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/compress-pdf")
|
||||
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/compress-pdf")
|
||||
@Operation(
|
||||
summary = "Optimize PDF file",
|
||||
description =
|
||||
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
package stirling.software.SPDF.controller.api.misc;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import stirling.software.SPDF.config.EndpointConfiguration;
|
||||
import stirling.software.common.configuration.AppConfig;
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
|
||||
@RestController
|
||||
@Tag(name = "Config", description = "Configuration APIs")
|
||||
@RequestMapping("/api/v1/config")
|
||||
@RequiredArgsConstructor
|
||||
@Hidden
|
||||
public class ConfigController {
|
||||
|
||||
private final ApplicationProperties applicationProperties;
|
||||
private final ApplicationContext applicationContext;
|
||||
private final EndpointConfiguration endpointConfiguration;
|
||||
|
||||
@GetMapping("/app-config")
|
||||
public ResponseEntity<Map<String, Object>> getAppConfig() {
|
||||
Map<String, Object> configData = new HashMap<>();
|
||||
|
||||
try {
|
||||
// Get AppConfig bean
|
||||
AppConfig appConfig = applicationContext.getBean(AppConfig.class);
|
||||
|
||||
// Extract key configuration values from AppConfig
|
||||
configData.put("baseUrl", appConfig.getBaseUrl());
|
||||
configData.put("contextPath", appConfig.getContextPath());
|
||||
configData.put("serverPort", appConfig.getServerPort());
|
||||
|
||||
// Extract values from ApplicationProperties
|
||||
configData.put("appName", applicationProperties.getUi().getAppName());
|
||||
configData.put("appNameNavbar", applicationProperties.getUi().getAppNameNavbar());
|
||||
configData.put("homeDescription", applicationProperties.getUi().getHomeDescription());
|
||||
configData.put("languages", applicationProperties.getUi().getLanguages());
|
||||
|
||||
// Security settings
|
||||
configData.put("enableLogin", applicationProperties.getSecurity().getEnableLogin());
|
||||
|
||||
// System settings
|
||||
configData.put(
|
||||
"enableAlphaFunctionality",
|
||||
applicationProperties.getSystem().getEnableAlphaFunctionality());
|
||||
configData.put(
|
||||
"enableAnalytics", applicationProperties.getSystem().getEnableAnalytics());
|
||||
|
||||
// Premium/Enterprise settings
|
||||
configData.put("premiumEnabled", applicationProperties.getPremium().isEnabled());
|
||||
|
||||
// Legal settings
|
||||
configData.put(
|
||||
"termsAndConditions", applicationProperties.getLegal().getTermsAndConditions());
|
||||
configData.put("privacyPolicy", applicationProperties.getLegal().getPrivacyPolicy());
|
||||
configData.put("cookiePolicy", applicationProperties.getLegal().getCookiePolicy());
|
||||
configData.put("impressum", applicationProperties.getLegal().getImpressum());
|
||||
configData.put(
|
||||
"accessibilityStatement",
|
||||
applicationProperties.getLegal().getAccessibilityStatement());
|
||||
|
||||
// Try to get EEAppConfig values if available
|
||||
try {
|
||||
if (applicationContext.containsBean("runningProOrHigher")) {
|
||||
configData.put(
|
||||
"runningProOrHigher",
|
||||
applicationContext.getBean("runningProOrHigher", Boolean.class));
|
||||
}
|
||||
if (applicationContext.containsBean("runningEE")) {
|
||||
configData.put(
|
||||
"runningEE", applicationContext.getBean("runningEE", Boolean.class));
|
||||
}
|
||||
if (applicationContext.containsBean("license")) {
|
||||
configData.put("license", applicationContext.getBean("license", String.class));
|
||||
}
|
||||
if (applicationContext.containsBean("GoogleDriveEnabled")) {
|
||||
configData.put(
|
||||
"GoogleDriveEnabled",
|
||||
applicationContext.getBean("GoogleDriveEnabled", Boolean.class));
|
||||
}
|
||||
if (applicationContext.containsBean("SSOAutoLogin")) {
|
||||
configData.put(
|
||||
"SSOAutoLogin",
|
||||
applicationContext.getBean("SSOAutoLogin", Boolean.class));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// EE features not available, continue without them
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(configData);
|
||||
|
||||
} catch (Exception e) {
|
||||
// Return basic config if there are any issues
|
||||
configData.put("error", "Unable to retrieve full configuration");
|
||||
return ResponseEntity.ok(configData);
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/endpoint-enabled")
|
||||
public ResponseEntity<Boolean> isEndpointEnabled(
|
||||
@RequestParam(name = "endpoint") String endpoint) {
|
||||
boolean enabled = endpointConfiguration.isEndpointEnabled(endpoint);
|
||||
return ResponseEntity.ok(enabled);
|
||||
}
|
||||
|
||||
@GetMapping("/endpoints-enabled")
|
||||
public ResponseEntity<Map<String, Boolean>> areEndpointsEnabled(
|
||||
@RequestParam(name = "endpoints") String endpoints) {
|
||||
Map<String, Boolean> result = new HashMap<>();
|
||||
String[] endpointArray = endpoints.split(",");
|
||||
for (String endpoint : endpointArray) {
|
||||
String trimmedEndpoint = endpoint.trim();
|
||||
result.put(trimmedEndpoint, endpointConfiguration.isEndpointEnabled(trimmedEndpoint));
|
||||
}
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -13,7 +13,6 @@ import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
@@ -24,6 +23,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.model.api.PDFFile;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
@@ -38,7 +38,7 @@ public class DecompressPdfController {
|
||||
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
|
||||
@PostMapping(value = "/decompress-pdf", consumes = "multipart/form-data")
|
||||
@AutoJobPostMapping(value = "/decompress-pdf", consumes = "multipart/form-data")
|
||||
@Operation(
|
||||
summary = "Decompress PDF streams",
|
||||
description = "Fully decompresses all PDF streams including text content")
|
||||
|
||||
+2
-2
@@ -19,7 +19,6 @@ import org.apache.pdfbox.rendering.PDFRenderer;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
@@ -31,6 +30,7 @@ import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.model.api.misc.ExtractImageScansRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.CheckProgramInstall;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
@@ -50,7 +50,7 @@ public class ExtractImageScansController {
|
||||
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/extract-image-scans")
|
||||
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/extract-image-scans")
|
||||
@Operation(
|
||||
summary = "Extract image scans from an input file",
|
||||
description =
|
||||
|
||||
+2
-2
@@ -27,7 +27,6 @@ import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
@@ -40,6 +39,7 @@ import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.model.api.PDFExtractImagesRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.ImageProcessingUtils;
|
||||
@@ -54,7 +54,7 @@ public class ExtractImagesController {
|
||||
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/extract-images")
|
||||
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/extract-images")
|
||||
@Operation(
|
||||
summary = "Extract images from a PDF file",
|
||||
description =
|
||||
|
||||
+2
-2
@@ -13,7 +13,6 @@ import org.apache.pdfbox.rendering.ImageType;
|
||||
import org.apache.pdfbox.rendering.PDFRenderer;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
@@ -26,6 +25,7 @@ import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.model.api.misc.FlattenRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
|
||||
@@ -38,7 +38,7 @@ public class FlattenController {
|
||||
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/flatten")
|
||||
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/flatten")
|
||||
@Operation(
|
||||
summary = "Flatten PDF form fields or full page",
|
||||
description =
|
||||
|
||||
+2
-1
@@ -23,6 +23,7 @@ import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.model.api.misc.MetadataRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
import stirling.software.common.util.propertyeditor.StringToMapPropertyEditor;
|
||||
@@ -51,7 +52,7 @@ public class MetadataController {
|
||||
binder.registerCustomEditor(Map.class, "allRequestParams", new StringToMapPropertyEditor());
|
||||
}
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/update-metadata")
|
||||
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/update-metadata")
|
||||
@Operation(
|
||||
summary = "Update metadata of a PDF file",
|
||||
description =
|
||||
|
||||
+2
-2
@@ -18,7 +18,6 @@ import org.apache.pdfbox.text.PDFTextStripper;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
@@ -32,6 +31,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.config.EndpointConfiguration;
|
||||
import stirling.software.SPDF.model.api.misc.ProcessPdfWithOcrRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
@@ -76,7 +76,7 @@ public class OCRController {
|
||||
.toList();
|
||||
}
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/ocr-pdf")
|
||||
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/ocr-pdf")
|
||||
@Operation(
|
||||
summary = "Process a PDF file with OCR",
|
||||
description =
|
||||
|
||||
+2
-2
@@ -5,7 +5,6 @@ import java.io.IOException;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
@@ -18,6 +17,7 @@ import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.model.api.misc.OverlayImageRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.PdfUtils;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
@@ -31,7 +31,7 @@ public class OverlayImageController {
|
||||
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/add-image")
|
||||
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/add-image")
|
||||
@Operation(
|
||||
summary = "Overlay image onto a PDF file",
|
||||
description =
|
||||
|
||||
+2
-2
@@ -13,7 +13,6 @@ import org.apache.pdfbox.pdmodel.font.Standard14Fonts;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
@@ -25,6 +24,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import stirling.software.SPDF.model.api.misc.AddPageNumbersRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
@@ -37,7 +37,7 @@ public class PageNumbersController {
|
||||
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
|
||||
@PostMapping(value = "/add-page-numbers", consumes = "multipart/form-data")
|
||||
@AutoJobPostMapping(value = "/add-page-numbers", consumes = "multipart/form-data")
|
||||
@Operation(
|
||||
summary = "Add page numbers to a PDF document",
|
||||
description =
|
||||
|
||||
+1
-1
@@ -37,7 +37,7 @@ import stirling.software.SPDF.model.api.misc.PrintFileRequest;
|
||||
public class PrintFileController {
|
||||
|
||||
// TODO
|
||||
// @PostMapping(value = "/print-file", consumes = "multipart/form-data")
|
||||
// @AutoJobPostMapping(value = "/print-file", consumes = "multipart/form-data")
|
||||
// @Operation(
|
||||
// summary = "Prints PDF/Image file to a set printer",
|
||||
// description =
|
||||
|
||||
+2
-2
@@ -6,7 +6,6 @@ import java.util.List;
|
||||
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
@@ -19,6 +18,7 @@ import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.config.EndpointConfiguration;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.model.api.PDFFile;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ProcessExecutor;
|
||||
@@ -46,7 +46,7 @@ public class RepairController {
|
||||
return endpointConfiguration.isGroupEnabled("qpdf");
|
||||
}
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/repair")
|
||||
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/repair")
|
||||
@Operation(
|
||||
summary = "Repair a PDF file",
|
||||
description =
|
||||
|
||||
+2
-2
@@ -7,7 +7,6 @@ import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@@ -18,6 +17,7 @@ import lombok.RequiredArgsConstructor;
|
||||
|
||||
import stirling.software.SPDF.model.api.misc.ReplaceAndInvertColorRequest;
|
||||
import stirling.software.SPDF.service.misc.ReplaceAndInvertColorService;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/misc")
|
||||
@@ -27,7 +27,7 @@ public class ReplaceAndInvertColorController {
|
||||
|
||||
private final ReplaceAndInvertColorService replaceAndInvertColorService;
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/replace-invert-pdf")
|
||||
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/replace-invert-pdf")
|
||||
@Operation(
|
||||
summary = "Replace-Invert Color PDF",
|
||||
description =
|
||||
|
||||
+2
-2
@@ -19,7 +19,6 @@ import org.apache.pdfbox.rendering.PDFRenderer;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
@@ -34,6 +33,7 @@ import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.model.api.misc.ScannerEffectRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
|
||||
@@ -52,7 +52,7 @@ public class ScannerEffectController {
|
||||
private static final int MAX_IMAGE_HEIGHT = 8192;
|
||||
private static final long MAX_IMAGE_PIXELS = 16_777_216; // 4096x4096
|
||||
|
||||
@PostMapping(value = "/scanner-effect", consumes = "multipart/form-data")
|
||||
@AutoJobPostMapping(value = "/scanner-effect", consumes = "multipart/form-data")
|
||||
@Operation(
|
||||
summary = "Apply scanner effect to PDF",
|
||||
description =
|
||||
|
||||
+2
-2
@@ -9,7 +9,6 @@ import org.apache.pdfbox.pdmodel.interactive.action.PDActionJavaScript;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
@@ -20,6 +19,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.model.api.PDFFile;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
@@ -32,7 +32,7 @@ public class ShowJavascript {
|
||||
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/show-javascript")
|
||||
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/show-javascript")
|
||||
@Operation(
|
||||
summary = "Grabs all JS from a PDF and returns a single JS file with all code",
|
||||
description = "desc. Input:PDF Output:JS Type:SISO")
|
||||
|
||||
+2
-2
@@ -26,7 +26,6 @@ import org.apache.pdfbox.util.Matrix;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
@@ -38,6 +37,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import stirling.software.SPDF.model.api.misc.AddStampRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.TempFile;
|
||||
import stirling.software.common.util.TempFileManager;
|
||||
@@ -52,7 +52,7 @@ public class StampController {
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
private final TempFileManager tempFileManager;
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/add-stamp")
|
||||
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/add-stamp")
|
||||
@Operation(
|
||||
summary = "Add stamp to a PDF file",
|
||||
description =
|
||||
|
||||
+2
-2
@@ -12,7 +12,6 @@ import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
|
||||
import org.apache.pdfbox.pdmodel.interactive.form.PDField;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@@ -22,6 +21,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.model.api.PDFFile;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
@@ -37,7 +37,7 @@ public class UnlockPDFFormsController {
|
||||
this.pdfDocumentFactory = pdfDocumentFactory;
|
||||
}
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/unlock-pdf-forms")
|
||||
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/unlock-pdf-forms")
|
||||
@Operation(
|
||||
summary = "Remove read-only property from form fields",
|
||||
description =
|
||||
|
||||
+2
-2
@@ -12,7 +12,6 @@ import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
@@ -30,6 +29,7 @@ import stirling.software.SPDF.model.PipelineConfig;
|
||||
import stirling.software.SPDF.model.PipelineOperation;
|
||||
import stirling.software.SPDF.model.PipelineResult;
|
||||
import stirling.software.SPDF.model.api.HandleDataRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.service.PostHogService;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
|
||||
@@ -46,7 +46,7 @@ public class PipelineController {
|
||||
|
||||
private final PostHogService postHogService;
|
||||
|
||||
@PostMapping(value = "/handleData", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@AutoJobPostMapping(value = "/handleData", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
public ResponseEntity<byte[]> handleData(@ModelAttribute HandleDataRequest request)
|
||||
throws JsonMappingException, JsonProcessingException {
|
||||
MultipartFile[] files = request.getFileInput();
|
||||
|
||||
+2
-2
@@ -59,7 +59,6 @@ import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.WebDataBinder;
|
||||
import org.springframework.web.bind.annotation.InitBinder;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
@@ -73,6 +72,7 @@ import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.model.api.security.SignPDFWithCertRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
@@ -138,7 +138,7 @@ public class CertSignController {
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping(
|
||||
@AutoJobPostMapping(
|
||||
consumes = {
|
||||
MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
MediaType.APPLICATION_FORM_URLENCODED_VALUE
|
||||
|
||||
+2
-2
@@ -46,7 +46,6 @@ import org.apache.xmpbox.xml.XmpSerializer;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
@@ -61,6 +60,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.model.api.PDFFile;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
@@ -188,7 +188,7 @@ public class GetInfoOnPDF {
|
||||
return false;
|
||||
}
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/get-info-on-pdf")
|
||||
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/get-info-on-pdf")
|
||||
@Operation(summary = "Summary here", description = "desc. Input:PDF Output:JSON Type:SISO")
|
||||
public ResponseEntity<byte[]> getPdfInfo(@ModelAttribute PDFFile request) throws IOException {
|
||||
MultipartFile inputFile = request.getFileInput();
|
||||
|
||||
+3
-3
@@ -7,7 +7,6 @@ import org.apache.pdfbox.pdmodel.encryption.AccessPermission;
|
||||
import org.apache.pdfbox.pdmodel.encryption.StandardProtectionPolicy;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
@@ -20,6 +19,7 @@ import lombok.RequiredArgsConstructor;
|
||||
|
||||
import stirling.software.SPDF.model.api.security.AddPasswordRequest;
|
||||
import stirling.software.SPDF.model.api.security.PDFPasswordRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
@@ -32,7 +32,7 @@ public class PasswordController {
|
||||
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/remove-password")
|
||||
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/remove-password")
|
||||
@Operation(
|
||||
summary = "Remove password from a PDF file",
|
||||
description =
|
||||
@@ -58,7 +58,7 @@ public class PasswordController {
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/add-password")
|
||||
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/add-password")
|
||||
@Operation(
|
||||
summary = "Add password to a PDF file",
|
||||
description =
|
||||
|
||||
+3
-3
@@ -18,7 +18,6 @@ import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.WebDataBinder;
|
||||
import org.springframework.web.bind.annotation.InitBinder;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
@@ -34,6 +33,7 @@ import stirling.software.SPDF.model.PDFText;
|
||||
import stirling.software.SPDF.model.api.security.ManualRedactPdfRequest;
|
||||
import stirling.software.SPDF.model.api.security.RedactPdfRequest;
|
||||
import stirling.software.SPDF.pdf.TextFinder;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.model.api.security.RedactionArea;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
@@ -56,7 +56,7 @@ public class RedactController {
|
||||
List.class, "redactions", new StringToArrayListPropertyEditor());
|
||||
}
|
||||
|
||||
@PostMapping(value = "/redact", consumes = "multipart/form-data")
|
||||
@AutoJobPostMapping(value = "/redact", consumes = "multipart/form-data")
|
||||
@Operation(
|
||||
summary = "Redacts areas and pages in a PDF document",
|
||||
description =
|
||||
@@ -190,7 +190,7 @@ public class RedactController {
|
||||
return pageNumbers;
|
||||
}
|
||||
|
||||
@PostMapping(value = "/auto-redact", consumes = "multipart/form-data")
|
||||
@AutoJobPostMapping(value = "/auto-redact", consumes = "multipart/form-data")
|
||||
@Operation(
|
||||
summary = "Redacts listOfText in a PDF document",
|
||||
description =
|
||||
|
||||
+2
-2
@@ -9,7 +9,6 @@ import org.apache.pdfbox.pdmodel.interactive.form.PDField;
|
||||
import org.apache.pdfbox.pdmodel.interactive.form.PDSignatureField;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
@@ -20,6 +19,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.model.api.PDFFile;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
@@ -32,7 +32,7 @@ public class RemoveCertSignController {
|
||||
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/remove-cert-sign")
|
||||
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/remove-cert-sign")
|
||||
@Operation(
|
||||
summary = "Remove digital signature from PDF",
|
||||
description =
|
||||
|
||||
+2
-2
@@ -23,7 +23,6 @@ import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
|
||||
import org.apache.pdfbox.pdmodel.interactive.form.PDField;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
@@ -35,6 +34,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import stirling.software.SPDF.model.api.security.SanitizePdfRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
|
||||
@@ -46,7 +46,7 @@ public class SanitizeController {
|
||||
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/sanitize-pdf")
|
||||
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/sanitize-pdf")
|
||||
@Operation(
|
||||
summary = "Sanitize a PDF file",
|
||||
description =
|
||||
|
||||
+4
-2
@@ -27,7 +27,6 @@ import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.WebDataBinder;
|
||||
import org.springframework.web.bind.annotation.InitBinder;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
@@ -40,6 +39,7 @@ import lombok.RequiredArgsConstructor;
|
||||
import stirling.software.SPDF.model.api.security.SignatureValidationRequest;
|
||||
import stirling.software.SPDF.model.api.security.SignatureValidationResult;
|
||||
import stirling.software.SPDF.service.CertificateValidationService;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
|
||||
@@ -69,7 +69,9 @@ public class ValidateSignatureController {
|
||||
description =
|
||||
"Validates the digital signatures in a PDF file against default or custom"
|
||||
+ " certificates. Input:PDF Output:JSON Type:SISO")
|
||||
@PostMapping(value = "/validate-signature", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@AutoJobPostMapping(
|
||||
value = "/validate-signature",
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
public ResponseEntity<List<SignatureValidationResult>> validateSignature(
|
||||
@ModelAttribute SignatureValidationRequest request) throws IOException {
|
||||
List<SignatureValidationResult> results = new ArrayList<>();
|
||||
|
||||
+2
-2
@@ -28,7 +28,6 @@ import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.WebDataBinder;
|
||||
import org.springframework.web.bind.annotation.InitBinder;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
@@ -40,6 +39,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import stirling.software.SPDF.model.api.security.AddWatermarkRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.PdfUtils;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
@@ -64,7 +64,7 @@ public class WatermarkController {
|
||||
});
|
||||
}
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/add-watermark")
|
||||
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/add-watermark")
|
||||
@Operation(
|
||||
summary = "Add watermark to a PDF file",
|
||||
description =
|
||||
|
||||
+31
-18
@@ -1,8 +1,6 @@
|
||||
package stirling.software.SPDF.controller.web;
|
||||
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
@@ -12,46 +10,52 @@ import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.util.ApplicationContextProvider;
|
||||
import stirling.software.common.util.CheckProgramInstall;
|
||||
|
||||
@Controller
|
||||
// @Controller // Disabled - Backend-only mode, no Thymeleaf UI
|
||||
@Tag(name = "Convert", description = "Convert APIs")
|
||||
public class ConverterWebController {
|
||||
|
||||
@GetMapping("/img-to-pdf")
|
||||
@Deprecated
|
||||
// @GetMapping("/img-to-pdf")
|
||||
@Hidden
|
||||
public String convertImgToPdfForm(Model model) {
|
||||
model.addAttribute("currentPage", "img-to-pdf");
|
||||
return "convert/img-to-pdf";
|
||||
}
|
||||
|
||||
@GetMapping("/html-to-pdf")
|
||||
@Deprecated
|
||||
// @GetMapping("/html-to-pdf")
|
||||
@Hidden
|
||||
public String convertHTMLToPdfForm(Model model) {
|
||||
model.addAttribute("currentPage", "html-to-pdf");
|
||||
return "convert/html-to-pdf";
|
||||
}
|
||||
|
||||
@GetMapping("/markdown-to-pdf")
|
||||
@Deprecated
|
||||
// @GetMapping("/markdown-to-pdf")
|
||||
@Hidden
|
||||
public String convertMarkdownToPdfForm(Model model) {
|
||||
model.addAttribute("currentPage", "markdown-to-pdf");
|
||||
return "convert/markdown-to-pdf";
|
||||
}
|
||||
|
||||
@GetMapping("/pdf-to-markdown")
|
||||
@Deprecated
|
||||
// @GetMapping("/pdf-to-markdown")
|
||||
@Hidden
|
||||
public String convertPdfToMarkdownForm(Model model) {
|
||||
model.addAttribute("currentPage", "pdf-to-markdown");
|
||||
return "convert/pdf-to-markdown";
|
||||
}
|
||||
|
||||
@GetMapping("/url-to-pdf")
|
||||
@Deprecated
|
||||
// @GetMapping("/url-to-pdf")
|
||||
@Hidden
|
||||
public String convertURLToPdfForm(Model model) {
|
||||
model.addAttribute("currentPage", "url-to-pdf");
|
||||
return "convert/url-to-pdf";
|
||||
}
|
||||
|
||||
@GetMapping("/file-to-pdf")
|
||||
@Deprecated
|
||||
// @GetMapping("/file-to-pdf")
|
||||
@Hidden
|
||||
public String convertToPdfForm(Model model) {
|
||||
model.addAttribute("currentPage", "file-to-pdf");
|
||||
@@ -60,7 +64,8 @@ public class ConverterWebController {
|
||||
|
||||
// PDF TO......
|
||||
|
||||
@GetMapping("/pdf-to-img")
|
||||
@Deprecated
|
||||
// @GetMapping("/pdf-to-img")
|
||||
@Hidden
|
||||
public String pdfToimgForm(Model model) {
|
||||
boolean isPython = CheckProgramInstall.isPythonAvailable();
|
||||
@@ -76,7 +81,8 @@ public class ConverterWebController {
|
||||
return "convert/pdf-to-img";
|
||||
}
|
||||
|
||||
@GetMapping("/pdf-to-html")
|
||||
@Deprecated
|
||||
// @GetMapping("/pdf-to-html")
|
||||
@Hidden
|
||||
public ModelAndView pdfToHTML() {
|
||||
ModelAndView modelAndView = new ModelAndView("convert/pdf-to-html");
|
||||
@@ -84,7 +90,8 @@ public class ConverterWebController {
|
||||
return modelAndView;
|
||||
}
|
||||
|
||||
@GetMapping("/pdf-to-presentation")
|
||||
@Deprecated
|
||||
// @GetMapping("/pdf-to-presentation")
|
||||
@Hidden
|
||||
public ModelAndView pdfToPresentation() {
|
||||
ModelAndView modelAndView = new ModelAndView("convert/pdf-to-presentation");
|
||||
@@ -92,7 +99,8 @@ public class ConverterWebController {
|
||||
return modelAndView;
|
||||
}
|
||||
|
||||
@GetMapping("/pdf-to-text")
|
||||
@Deprecated
|
||||
// @GetMapping("/pdf-to-text")
|
||||
@Hidden
|
||||
public ModelAndView pdfToText() {
|
||||
ModelAndView modelAndView = new ModelAndView("convert/pdf-to-text");
|
||||
@@ -100,7 +108,8 @@ public class ConverterWebController {
|
||||
return modelAndView;
|
||||
}
|
||||
|
||||
@GetMapping("/pdf-to-word")
|
||||
@Deprecated
|
||||
// @GetMapping("/pdf-to-word")
|
||||
@Hidden
|
||||
public ModelAndView pdfToWord() {
|
||||
ModelAndView modelAndView = new ModelAndView("convert/pdf-to-word");
|
||||
@@ -108,7 +117,8 @@ public class ConverterWebController {
|
||||
return modelAndView;
|
||||
}
|
||||
|
||||
@GetMapping("/pdf-to-xml")
|
||||
@Deprecated
|
||||
// @GetMapping("/pdf-to-xml")
|
||||
@Hidden
|
||||
public ModelAndView pdfToXML() {
|
||||
ModelAndView modelAndView = new ModelAndView("convert/pdf-to-xml");
|
||||
@@ -116,7 +126,8 @@ public class ConverterWebController {
|
||||
return modelAndView;
|
||||
}
|
||||
|
||||
@GetMapping("/pdf-to-csv")
|
||||
@Deprecated
|
||||
// @GetMapping("/pdf-to-csv")
|
||||
@Hidden
|
||||
public ModelAndView pdfToCSV() {
|
||||
ModelAndView modelAndView = new ModelAndView("convert/pdf-to-csv");
|
||||
@@ -124,14 +135,16 @@ public class ConverterWebController {
|
||||
return modelAndView;
|
||||
}
|
||||
|
||||
@GetMapping("/pdf-to-pdfa")
|
||||
@Deprecated
|
||||
// @GetMapping("/pdf-to-pdfa")
|
||||
@Hidden
|
||||
public String pdfToPdfAForm(Model model) {
|
||||
model.addAttribute("currentPage", "pdf-to-pdfa");
|
||||
return "convert/pdf-to-pdfa";
|
||||
}
|
||||
|
||||
@GetMapping("/eml-to-pdf")
|
||||
@Deprecated
|
||||
// @GetMapping("/eml-to-pdf")
|
||||
@Hidden
|
||||
public String convertEmlToPdfForm(Model model) {
|
||||
model.addAttribute("currentPage", "eml-to-pdf");
|
||||
|
||||
+43
-24
@@ -12,9 +12,7 @@ import java.util.stream.Stream;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
@@ -32,7 +30,7 @@ import stirling.software.common.service.UserServiceInterface;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
|
||||
@Controller
|
||||
// @Controller // Disabled - Backend-only mode, no Thymeleaf UI
|
||||
@Tag(name = "General", description = "General APIs")
|
||||
@Slf4j
|
||||
public class GeneralWebController {
|
||||
@@ -53,7 +51,8 @@ public class GeneralWebController {
|
||||
this.runtimePathConfig = runtimePathConfig;
|
||||
}
|
||||
|
||||
@GetMapping("/pipeline")
|
||||
@Deprecated
|
||||
// @GetMapping("/pipeline")
|
||||
@Hidden
|
||||
public String pipelineForm(Model model) {
|
||||
model.addAttribute("currentPage", "pipeline");
|
||||
@@ -103,91 +102,104 @@ public class GeneralWebController {
|
||||
return "pipeline";
|
||||
}
|
||||
|
||||
@GetMapping("/merge-pdfs")
|
||||
@Deprecated
|
||||
// @GetMapping("/merge-pdfs")
|
||||
@Hidden
|
||||
public String mergePdfForm(Model model) {
|
||||
model.addAttribute("currentPage", "merge-pdfs");
|
||||
return "merge-pdfs";
|
||||
}
|
||||
|
||||
@GetMapping("/split-pdf-by-sections")
|
||||
@Deprecated
|
||||
// @GetMapping("/split-pdf-by-sections")
|
||||
@Hidden
|
||||
public String splitPdfBySections(Model model) {
|
||||
model.addAttribute("currentPage", "split-pdf-by-sections");
|
||||
return "split-pdf-by-sections";
|
||||
}
|
||||
|
||||
@GetMapping("/split-pdf-by-chapters")
|
||||
@Deprecated
|
||||
// @GetMapping("/split-pdf-by-chapters")
|
||||
@Hidden
|
||||
public String splitPdfByChapters(Model model) {
|
||||
model.addAttribute("currentPage", "split-pdf-by-chapters");
|
||||
return "split-pdf-by-chapters";
|
||||
}
|
||||
|
||||
@GetMapping("/view-pdf")
|
||||
@Deprecated
|
||||
// @GetMapping("/view-pdf")
|
||||
@Hidden
|
||||
public String ViewPdfForm2(Model model) {
|
||||
model.addAttribute("currentPage", "view-pdf");
|
||||
return "view-pdf";
|
||||
}
|
||||
|
||||
@GetMapping("/edit-table-of-contents")
|
||||
@Deprecated
|
||||
// @GetMapping("/edit-table-of-contents")
|
||||
@Hidden
|
||||
public String editTableOfContents(Model model) {
|
||||
model.addAttribute("currentPage", "edit-table-of-contents");
|
||||
return "edit-table-of-contents";
|
||||
}
|
||||
|
||||
@GetMapping("/multi-tool")
|
||||
@Deprecated
|
||||
// @GetMapping("/multi-tool")
|
||||
@Hidden
|
||||
public String multiToolForm(Model model) {
|
||||
model.addAttribute("currentPage", "multi-tool");
|
||||
return "multi-tool";
|
||||
}
|
||||
|
||||
@GetMapping("/remove-pages")
|
||||
@Deprecated
|
||||
// @GetMapping("/remove-pages")
|
||||
@Hidden
|
||||
public String pageDeleter(Model model) {
|
||||
model.addAttribute("currentPage", "remove-pages");
|
||||
return "remove-pages";
|
||||
}
|
||||
|
||||
@GetMapping("/pdf-organizer")
|
||||
@Deprecated
|
||||
// @GetMapping("/pdf-organizer")
|
||||
@Hidden
|
||||
public String pageOrganizer(Model model) {
|
||||
model.addAttribute("currentPage", "pdf-organizer");
|
||||
return "pdf-organizer";
|
||||
}
|
||||
|
||||
@GetMapping("/extract-page")
|
||||
@Deprecated
|
||||
// @GetMapping("/extract-page")
|
||||
@Hidden
|
||||
public String extractPages(Model model) {
|
||||
model.addAttribute("currentPage", "extract-page");
|
||||
return "extract-page";
|
||||
}
|
||||
|
||||
@GetMapping("/pdf-to-single-page")
|
||||
@Deprecated
|
||||
// @GetMapping("/pdf-to-single-page")
|
||||
@Hidden
|
||||
public String pdfToSinglePage(Model model) {
|
||||
model.addAttribute("currentPage", "pdf-to-single-page");
|
||||
return "pdf-to-single-page";
|
||||
}
|
||||
|
||||
@GetMapping("/rotate-pdf")
|
||||
@Deprecated
|
||||
// @GetMapping("/rotate-pdf")
|
||||
@Hidden
|
||||
public String rotatePdfForm(Model model) {
|
||||
model.addAttribute("currentPage", "rotate-pdf");
|
||||
return "rotate-pdf";
|
||||
}
|
||||
|
||||
@GetMapping("/split-pdfs")
|
||||
@Deprecated
|
||||
// @GetMapping("/split-pdfs")
|
||||
@Hidden
|
||||
public String splitPdfForm(Model model) {
|
||||
model.addAttribute("currentPage", "split-pdfs");
|
||||
return "split-pdfs";
|
||||
}
|
||||
|
||||
@GetMapping("/sign")
|
||||
@Deprecated
|
||||
// @GetMapping("/sign")
|
||||
@Hidden
|
||||
public String signForm(Model model) {
|
||||
String username = "";
|
||||
@@ -202,28 +214,32 @@ public class GeneralWebController {
|
||||
return "sign";
|
||||
}
|
||||
|
||||
@GetMapping("/multi-page-layout")
|
||||
@Deprecated
|
||||
// @GetMapping("/multi-page-layout")
|
||||
@Hidden
|
||||
public String multiPageLayoutForm(Model model) {
|
||||
model.addAttribute("currentPage", "multi-page-layout");
|
||||
return "multi-page-layout";
|
||||
}
|
||||
|
||||
@GetMapping("/scale-pages")
|
||||
@Deprecated
|
||||
// @GetMapping("/scale-pages")
|
||||
@Hidden
|
||||
public String scalePagesFrom(Model model) {
|
||||
model.addAttribute("currentPage", "scale-pages");
|
||||
return "scale-pages";
|
||||
}
|
||||
|
||||
@GetMapping("/split-by-size-or-count")
|
||||
@Deprecated
|
||||
// @GetMapping("/split-by-size-or-count")
|
||||
@Hidden
|
||||
public String splitBySizeOrCount(Model model) {
|
||||
model.addAttribute("currentPage", "split-by-size-or-count");
|
||||
return "split-by-size-or-count";
|
||||
}
|
||||
|
||||
@GetMapping("/overlay-pdf")
|
||||
@Deprecated
|
||||
// @GetMapping("/overlay-pdf")
|
||||
@Hidden
|
||||
public String overlayPdf(Model model) {
|
||||
model.addAttribute("currentPage", "overlay-pdf");
|
||||
@@ -296,21 +312,24 @@ public class GeneralWebController {
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/crop")
|
||||
@Deprecated
|
||||
// @GetMapping("/crop")
|
||||
@Hidden
|
||||
public String cropForm(Model model) {
|
||||
model.addAttribute("currentPage", "crop");
|
||||
return "crop";
|
||||
}
|
||||
|
||||
@GetMapping("/auto-split-pdf")
|
||||
@Deprecated
|
||||
// @GetMapping("/auto-split-pdf")
|
||||
@Hidden
|
||||
public String autoSPlitPDFForm(Model model) {
|
||||
model.addAttribute("currentPage", "auto-split-pdf");
|
||||
return "auto-split-pdf";
|
||||
}
|
||||
|
||||
@GetMapping("/remove-image-pdf")
|
||||
@Deprecated
|
||||
// @GetMapping("/remove-image-pdf")
|
||||
@Hidden
|
||||
public String removeImagePdfForm(Model model) {
|
||||
model.addAttribute("currentPage", "remove-image-pdf");
|
||||
|
||||
+13
-8
@@ -9,7 +9,6 @@ import java.util.Map;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
@@ -26,20 +25,22 @@ import stirling.software.SPDF.model.Dependency;
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
|
||||
@Slf4j
|
||||
@Controller
|
||||
// @Controller // Disabled - Backend-only mode, no Thymeleaf UI
|
||||
@RequiredArgsConstructor
|
||||
public class HomeWebController {
|
||||
|
||||
private final ApplicationProperties applicationProperties;
|
||||
|
||||
@GetMapping("/about")
|
||||
@Deprecated
|
||||
// @GetMapping("/about")
|
||||
@Hidden
|
||||
public String gameForm(Model model) {
|
||||
model.addAttribute("currentPage", "about");
|
||||
return "about";
|
||||
}
|
||||
|
||||
@GetMapping("/licenses")
|
||||
@Deprecated
|
||||
// @GetMapping("/licenses")
|
||||
@Hidden
|
||||
public String licensesForm(Model model) {
|
||||
model.addAttribute("currentPage", "licenses");
|
||||
@@ -56,12 +57,14 @@ public class HomeWebController {
|
||||
return "licenses";
|
||||
}
|
||||
|
||||
@GetMapping("/releases")
|
||||
@Deprecated
|
||||
// @GetMapping("/releases")
|
||||
public String getReleaseNotes(Model model) {
|
||||
return "releases";
|
||||
}
|
||||
|
||||
@GetMapping("/")
|
||||
@Deprecated
|
||||
// @GetMapping("/")
|
||||
public String home(Model model) {
|
||||
model.addAttribute("currentPage", "home");
|
||||
String showSurvey = System.getenv("SHOW_SURVEY");
|
||||
@@ -70,12 +73,14 @@ public class HomeWebController {
|
||||
return "home";
|
||||
}
|
||||
|
||||
@GetMapping("/home")
|
||||
@Deprecated
|
||||
// @GetMapping("/home")
|
||||
public String root(Model model) {
|
||||
return "redirect:/";
|
||||
}
|
||||
|
||||
@GetMapping("/home-legacy")
|
||||
@Deprecated
|
||||
// @GetMapping("/home-legacy")
|
||||
public String redirectHomeLegacy() {
|
||||
return "redirect:/";
|
||||
}
|
||||
|
||||
+45
-25
@@ -5,9 +5,7 @@ import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
@@ -18,28 +16,31 @@ import lombok.RequiredArgsConstructor;
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.util.CheckProgramInstall;
|
||||
|
||||
@Controller
|
||||
// @Controller // Disabled - Backend-only mode, no Thymeleaf UI
|
||||
@Tag(name = "Misc", description = "Miscellaneous APIs")
|
||||
@RequiredArgsConstructor
|
||||
public class OtherWebController {
|
||||
|
||||
private final ApplicationProperties applicationProperties;
|
||||
|
||||
@GetMapping("/compress-pdf")
|
||||
@Deprecated
|
||||
// @GetMapping("/compress-pdf")
|
||||
@Hidden
|
||||
public String compressPdfForm(Model model) {
|
||||
model.addAttribute("currentPage", "compress-pdf");
|
||||
return "misc/compress-pdf";
|
||||
}
|
||||
|
||||
@GetMapping("/replace-and-invert-color-pdf")
|
||||
@Deprecated
|
||||
// @GetMapping("/replace-and-invert-color-pdf")
|
||||
@Hidden
|
||||
public String replaceAndInvertColorPdfForm(Model model) {
|
||||
model.addAttribute("currentPage", "replace-invert-color-pdf");
|
||||
return "misc/replace-color";
|
||||
}
|
||||
|
||||
@GetMapping("/extract-image-scans")
|
||||
@Deprecated
|
||||
// @GetMapping("/extract-image-scans")
|
||||
@Hidden
|
||||
public ModelAndView extractImageScansForm() {
|
||||
ModelAndView modelAndView = new ModelAndView("misc/extract-image-scans");
|
||||
@@ -49,70 +50,80 @@ public class OtherWebController {
|
||||
return modelAndView;
|
||||
}
|
||||
|
||||
@GetMapping("/show-javascript")
|
||||
@Deprecated
|
||||
// @GetMapping("/show-javascript")
|
||||
@Hidden
|
||||
public String extractJavascriptForm(Model model) {
|
||||
model.addAttribute("currentPage", "show-javascript");
|
||||
return "misc/show-javascript";
|
||||
}
|
||||
|
||||
@GetMapping("/stamp")
|
||||
@Deprecated
|
||||
// @GetMapping("/stamp")
|
||||
@Hidden
|
||||
public String stampForm(Model model) {
|
||||
model.addAttribute("currentPage", "stamp");
|
||||
return "misc/stamp";
|
||||
}
|
||||
|
||||
@GetMapping("/add-page-numbers")
|
||||
@Deprecated
|
||||
// @GetMapping("/add-page-numbers")
|
||||
@Hidden
|
||||
public String addPageNumbersForm(Model model) {
|
||||
model.addAttribute("currentPage", "add-page-numbers");
|
||||
return "misc/add-page-numbers";
|
||||
}
|
||||
|
||||
@GetMapping("/scanner-effect")
|
||||
@Deprecated
|
||||
// @GetMapping("/scanner-effect")
|
||||
@Hidden
|
||||
public String scannerEffectForm(Model model) {
|
||||
model.addAttribute("currentPage", "scanner-effect");
|
||||
return "misc/scanner-effect";
|
||||
}
|
||||
|
||||
@GetMapping("/extract-images")
|
||||
@Deprecated
|
||||
// @GetMapping("/extract-images")
|
||||
@Hidden
|
||||
public String extractImagesForm(Model model) {
|
||||
model.addAttribute("currentPage", "extract-images");
|
||||
return "misc/extract-images";
|
||||
}
|
||||
|
||||
@GetMapping("/flatten")
|
||||
@Deprecated
|
||||
// @GetMapping("/flatten")
|
||||
@Hidden
|
||||
public String flattenForm(Model model) {
|
||||
model.addAttribute("currentPage", "flatten");
|
||||
return "misc/flatten";
|
||||
}
|
||||
|
||||
@GetMapping("/change-metadata")
|
||||
@Deprecated
|
||||
// @GetMapping("/change-metadata")
|
||||
@Hidden
|
||||
public String addWatermarkForm(Model model) {
|
||||
model.addAttribute("currentPage", "change-metadata");
|
||||
return "misc/change-metadata";
|
||||
}
|
||||
|
||||
@GetMapping("/unlock-pdf-forms")
|
||||
@Deprecated
|
||||
// @GetMapping("/unlock-pdf-forms")
|
||||
@Hidden
|
||||
public String unlockPDFForms(Model model) {
|
||||
model.addAttribute("currentPage", "unlock-pdf-forms");
|
||||
return "misc/unlock-pdf-forms";
|
||||
}
|
||||
|
||||
@GetMapping("/compare")
|
||||
@Deprecated
|
||||
// @GetMapping("/compare")
|
||||
@Hidden
|
||||
public String compareForm(Model model) {
|
||||
model.addAttribute("currentPage", "compare");
|
||||
return "misc/compare";
|
||||
}
|
||||
|
||||
@GetMapping("/print-file")
|
||||
@Deprecated
|
||||
// @GetMapping("/print-file")
|
||||
@Hidden
|
||||
public String printFileForm(Model model) {
|
||||
model.addAttribute("currentPage", "print-file");
|
||||
@@ -133,7 +144,8 @@ public class OtherWebController {
|
||||
.toList();
|
||||
}
|
||||
|
||||
@GetMapping("/ocr-pdf")
|
||||
@Deprecated
|
||||
// @GetMapping("/ocr-pdf")
|
||||
@Hidden
|
||||
public ModelAndView ocrPdfPage() {
|
||||
ModelAndView modelAndView = new ModelAndView("misc/ocr-pdf");
|
||||
@@ -143,56 +155,64 @@ public class OtherWebController {
|
||||
return modelAndView;
|
||||
}
|
||||
|
||||
@GetMapping("/add-image")
|
||||
@Deprecated
|
||||
// @GetMapping("/add-image")
|
||||
@Hidden
|
||||
public String overlayImage(Model model) {
|
||||
model.addAttribute("currentPage", "add-image");
|
||||
return "misc/add-image";
|
||||
}
|
||||
|
||||
@GetMapping("/adjust-contrast")
|
||||
@Deprecated
|
||||
// @GetMapping("/adjust-contrast")
|
||||
@Hidden
|
||||
public String contrast(Model model) {
|
||||
model.addAttribute("currentPage", "adjust-contrast");
|
||||
return "misc/adjust-contrast";
|
||||
}
|
||||
|
||||
@GetMapping("/repair")
|
||||
@Deprecated
|
||||
// @GetMapping("/repair")
|
||||
@Hidden
|
||||
public String repairForm(Model model) {
|
||||
model.addAttribute("currentPage", "repair");
|
||||
return "misc/repair";
|
||||
}
|
||||
|
||||
@GetMapping("/remove-blanks")
|
||||
@Deprecated
|
||||
// @GetMapping("/remove-blanks")
|
||||
@Hidden
|
||||
public String removeBlanksForm(Model model) {
|
||||
model.addAttribute("currentPage", "remove-blanks");
|
||||
return "misc/remove-blanks";
|
||||
}
|
||||
|
||||
@GetMapping("/remove-annotations")
|
||||
@Deprecated
|
||||
// @GetMapping("/remove-annotations")
|
||||
@Hidden
|
||||
public String removeAnnotationsForm(Model model) {
|
||||
model.addAttribute("currentPage", "remove-annotations");
|
||||
return "misc/remove-annotations";
|
||||
}
|
||||
|
||||
@GetMapping("/auto-crop")
|
||||
@Deprecated
|
||||
// @GetMapping("/auto-crop")
|
||||
@Hidden
|
||||
public String autoCropForm(Model model) {
|
||||
model.addAttribute("currentPage", "auto-crop");
|
||||
return "misc/auto-crop";
|
||||
}
|
||||
|
||||
@GetMapping("/auto-rename")
|
||||
@Deprecated
|
||||
// @GetMapping("/auto-rename")
|
||||
@Hidden
|
||||
public String autoRenameForm(Model model) {
|
||||
model.addAttribute("currentPage", "auto-rename");
|
||||
return "misc/auto-rename";
|
||||
}
|
||||
|
||||
@GetMapping("/add-attachments")
|
||||
@Deprecated
|
||||
// @GetMapping("/add-attachments")
|
||||
@Hidden
|
||||
public String attachmentsForm(Model model) {
|
||||
model.addAttribute("currentPage", "add-attachments");
|
||||
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package stirling.software.SPDF.controller.web;
|
||||
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
// @Controller // Disabled - Backend-only mode, no Thymeleaf UI
|
||||
public class ReactRoutingController {
|
||||
|
||||
@GetMapping("/{path:^(?!api|static|robots\\.txt|favicon\\.ico)[^\\.]*$}")
|
||||
public String forwardRootPaths() {
|
||||
return "forward:/index.html";
|
||||
}
|
||||
|
||||
@GetMapping("/{path:^(?!api|static)[^\\.]*}/{subpath:^(?!.*\\.).*$}")
|
||||
public String forwardNestedPaths() {
|
||||
return "forward:/index.html";
|
||||
}
|
||||
}
|
||||
+23
-14
@@ -1,86 +1,95 @@
|
||||
package stirling.software.SPDF.controller.web;
|
||||
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
@Controller
|
||||
// @Controller // Disabled - Backend-only mode, no Thymeleaf UI
|
||||
@Tag(name = "Security", description = "Security APIs")
|
||||
public class SecurityWebController {
|
||||
|
||||
@GetMapping("/auto-redact")
|
||||
@Deprecated
|
||||
// @GetMapping("/auto-redact")
|
||||
@Hidden
|
||||
public String autoRedactForm(Model model) {
|
||||
model.addAttribute("currentPage", "auto-redact");
|
||||
return "security/auto-redact";
|
||||
}
|
||||
|
||||
@GetMapping("/redact")
|
||||
@Deprecated
|
||||
// @GetMapping("/redact")
|
||||
public String redactForm(Model model) {
|
||||
model.addAttribute("currentPage", "redact");
|
||||
return "security/redact";
|
||||
}
|
||||
|
||||
@GetMapping("/add-password")
|
||||
@Deprecated
|
||||
// @GetMapping("/add-password")
|
||||
@Hidden
|
||||
public String addPasswordForm(Model model) {
|
||||
model.addAttribute("currentPage", "add-password");
|
||||
return "security/add-password";
|
||||
}
|
||||
|
||||
@GetMapping("/change-permissions")
|
||||
@Deprecated
|
||||
// @GetMapping("/change-permissions")
|
||||
@Hidden
|
||||
public String permissionsForm(Model model) {
|
||||
model.addAttribute("currentPage", "change-permissions");
|
||||
return "security/change-permissions";
|
||||
}
|
||||
|
||||
@GetMapping("/remove-password")
|
||||
@Deprecated
|
||||
// @GetMapping("/remove-password")
|
||||
@Hidden
|
||||
public String removePasswordForm(Model model) {
|
||||
model.addAttribute("currentPage", "remove-password");
|
||||
return "security/remove-password";
|
||||
}
|
||||
|
||||
@GetMapping("/add-watermark")
|
||||
@Deprecated
|
||||
// @GetMapping("/add-watermark")
|
||||
@Hidden
|
||||
public String addWatermarkForm(Model model) {
|
||||
model.addAttribute("currentPage", "add-watermark");
|
||||
return "security/add-watermark";
|
||||
}
|
||||
|
||||
@GetMapping("/cert-sign")
|
||||
@Deprecated
|
||||
// @GetMapping("/cert-sign")
|
||||
@Hidden
|
||||
public String certSignForm(Model model) {
|
||||
model.addAttribute("currentPage", "cert-sign");
|
||||
return "security/cert-sign";
|
||||
}
|
||||
|
||||
@GetMapping("/validate-signature")
|
||||
@Deprecated
|
||||
// @GetMapping("/validate-signature")
|
||||
@Hidden
|
||||
public String certSignVerifyForm(Model model) {
|
||||
model.addAttribute("currentPage", "validate-signature");
|
||||
return "security/validate-signature";
|
||||
}
|
||||
|
||||
@GetMapping("/remove-cert-sign")
|
||||
@Deprecated
|
||||
// @GetMapping("/remove-cert-sign")
|
||||
@Hidden
|
||||
public String certUnSignForm(Model model) {
|
||||
model.addAttribute("currentPage", "remove-cert-sign");
|
||||
return "security/remove-cert-sign";
|
||||
}
|
||||
|
||||
@GetMapping("/sanitize-pdf")
|
||||
@Deprecated
|
||||
// @GetMapping("/sanitize-pdf")
|
||||
@Hidden
|
||||
public String sanitizeForm(Model model) {
|
||||
model.addAttribute("currentPage", "sanitize-pdf");
|
||||
return "security/sanitize-pdf";
|
||||
}
|
||||
|
||||
@GetMapping("/get-info-on-pdf")
|
||||
@Deprecated
|
||||
// @GetMapping("/get-info-on-pdf")
|
||||
@Hidden
|
||||
public String getInfo(Model model) {
|
||||
model.addAttribute("currentPage", "get-info-on-pdf");
|
||||
|
||||
+1
-2
@@ -6,7 +6,6 @@ import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
@@ -14,7 +13,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import stirling.software.SPDF.service.SignatureService;
|
||||
import stirling.software.common.service.UserServiceInterface;
|
||||
|
||||
@Controller
|
||||
// @Controller // Disabled - Backend-only mode, no Thymeleaf UI
|
||||
@RequestMapping("/api/v1/general")
|
||||
public class SignatureController {
|
||||
|
||||
|
||||
+2
-2
@@ -2,7 +2,6 @@ package stirling.software.SPDF.model.api.converters;
|
||||
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
@@ -10,6 +9,7 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.model.api.PDFFile;
|
||||
import stirling.software.common.util.PDFToFile;
|
||||
|
||||
@@ -18,7 +18,7 @@ import stirling.software.common.util.PDFToFile;
|
||||
@RequestMapping("/api/v1/convert")
|
||||
public class ConvertPDFToMarkdown {
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/pdf/markdown")
|
||||
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/pdf/markdown")
|
||||
@Operation(
|
||||
summary = "Convert PDF to Markdown",
|
||||
description =
|
||||
|
||||
@@ -9,7 +9,7 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.thymeleaf.util.StringUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@@ -40,7 +40,7 @@ public class SignatureService {
|
||||
List<SignatureFile> signatures = new ArrayList<>();
|
||||
|
||||
// Get signatures from user's personal folder
|
||||
if (!StringUtils.isEmptyOrWhitespace(username)) {
|
||||
if (StringUtils.hasText(username)) {
|
||||
Path userFolder = Paths.get(SIGNATURE_BASE_PATH, username);
|
||||
if (Files.exists(userFolder)) {
|
||||
try {
|
||||
|
||||
@@ -3,9 +3,9 @@ logging.level.org.springframework=WARN
|
||||
logging.level.org.hibernate=WARN
|
||||
logging.level.org.eclipse.jetty=WARN
|
||||
#logging.level.org.springframework.security.saml2=TRACE
|
||||
#logging.level.org.springframework.security=DEBUG
|
||||
logging.level.org.springframework.security=DEBUG
|
||||
#logging.level.org.opensaml=DEBUG
|
||||
#logging.level.stirling.software.SPDF.config.security: DEBUG
|
||||
logging.level.stirling.software: DEBUG
|
||||
logging.level.com.zaxxer.hikari=WARN
|
||||
spring.jpa.open-in-view=false
|
||||
server.forward-headers-strategy=NATIVE
|
||||
@@ -25,7 +25,7 @@ server.servlet.context-path=${SYSTEM_ROOTURIPATH:/}
|
||||
spring.devtools.restart.enabled=true
|
||||
spring.devtools.livereload.enabled=true
|
||||
spring.devtools.restart.exclude=stirling.software.proprietary.security/**
|
||||
spring.thymeleaf.encoding=UTF-8
|
||||
# spring.thymeleaf.encoding=UTF-8 # Disabled - React frontend replaces Thymeleaf
|
||||
spring.web.resources.mime-mappings.webmanifest=application/manifest+json
|
||||
spring.mvc.async.request-timeout=${SYSTEM_CONNECTIONTIMEOUTMILLISECONDS:1200000}
|
||||
|
||||
@@ -35,6 +35,14 @@ spring.datasource.username=sa
|
||||
spring.datasource.password=
|
||||
spring.h2.console.enabled=false
|
||||
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
|
||||
# using database initialization scripts or tools.
|
||||
spring.jpa.defer-datasource-initialization=true
|
||||
|
||||
# Disable SQL logging to avoid cluttering the logs in production. Enable this
|
||||
# property during development if you need to debug SQL queries.
|
||||
spring.jpa.show-sql=false
|
||||
server.servlet.session.timeout:30m
|
||||
# Change the default URL path for OpenAPI JSON
|
||||
springdoc.api-docs.path=/v1/api-docs
|
||||
@@ -47,4 +55,14 @@ posthog.host=https://eu.i.posthog.com
|
||||
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}
|
||||
java.io.tmpdir=${stirling.tempfiles.directory:${java.io.tmpdir}/stirling-pdf}
|
||||
|
||||
|
||||
#spring.security.oauth2.resourceserver.jwt.issuer-uri=https://nrlkjfznsavsbmweiyqu.supabase.co/auth/v1
|
||||
|
||||
spring.security.oauth2.resourceserver.jwk-set-uri=https://nrlkjfznsavsbmweiyqu.supabase.co/auth/v1/.well-known/jwks.json
|
||||
spring.security.oauth2.resourceserver.audience=authenticated
|
||||
logging:
|
||||
level:
|
||||
org.springframework.security: DEBUG
|
||||
your.project.pkg.security: DEBUG
|
||||
@@ -1844,22 +1844,6 @@ cookieBanner.preferencesModal.necessary.description=These cookies are essential
|
||||
cookieBanner.preferencesModal.analytics.title=Analytics
|
||||
cookieBanner.preferencesModal.analytics.description=These cookies help us understand how our tools are being used, so we can focus on building the features our community values most. Rest assured—Stirling PDF cannot and will never track the content of the documents you work with.
|
||||
|
||||
#scannerEffect
|
||||
scannerEffect.title=Scanner Effect
|
||||
scannerEffect.header=Scanner Effect
|
||||
scannerEffect.description=Create a PDF that looks like it was scanned
|
||||
scannerEffect.selectPDF=Select PDF:
|
||||
scannerEffect.quality=Scan Quality
|
||||
scannerEffect.quality.low=Low
|
||||
scannerEffect.quality.medium=Medium
|
||||
scannerEffect.quality.high=High
|
||||
scannerEffect.rotation=Rotation Angle
|
||||
scannerEffect.rotation.none=None
|
||||
scannerEffect.rotation.slight=Slight
|
||||
scannerEffect.rotation.moderate=Moderate
|
||||
scannerEffect.rotation.severe=Severe
|
||||
scannerEffect.submit=Create Scanner Effect
|
||||
|
||||
#home.scannerEffect
|
||||
home.scannerEffect.title=Scanner Effect
|
||||
home.scannerEffect.desc=Create a PDF that looks like it was scanned
|
||||
@@ -1902,3 +1886,73 @@ editTableOfContents.desc.1=This tool allows you to add or edit the table of cont
|
||||
editTableOfContents.desc.2=You can create a hierarchical structure by adding child bookmarks to parent bookmarks.
|
||||
editTableOfContents.desc.3=Each bookmark requires a title and target page number.
|
||||
editTableOfContents.submit=Apply Table of Contents
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
####################
|
||||
# React Frontend #
|
||||
####################
|
||||
# Common UI elements
|
||||
removeMetadata.submit=Remove Metadata
|
||||
sidebar.toggle=Toggle Sidebar
|
||||
theme.toggle=Toggle Theme
|
||||
view.viewer=Viewer
|
||||
view.pageEditor=Page Editor
|
||||
view.fileManager=File Manager
|
||||
|
||||
# File management
|
||||
fileManager.dragDrop=Drag & Drop files here
|
||||
fileManager.clickToUpload=Click to upload files
|
||||
fileManager.selectedFiles=Selected Files
|
||||
fileManager.clearAll=Clear All
|
||||
fileManager.storage=Storage
|
||||
fileManager.filesStored=files stored
|
||||
fileManager.storageError=Storage error occurred
|
||||
fileManager.storageLow=Storage is running low. Consider removing old files.
|
||||
fileManager.uploadError=Failed to upload some files.
|
||||
fileManager.supportMessage=Powered by browser database storage for unlimited capacity
|
||||
|
||||
# Page Editor
|
||||
pageEditor.title=Page Editor
|
||||
pageEditor.save=Save Changes
|
||||
pageEditor.noPdfLoaded=No PDF loaded. Please upload a PDF to edit.
|
||||
pageEditor.rotatedLeft=Rotated left:
|
||||
pageEditor.rotatedRight=Rotated right:
|
||||
pageEditor.deleted=Deleted:
|
||||
pageEditor.movedLeft=Moved left:
|
||||
pageEditor.movedRight=Moved right:
|
||||
pageEditor.splitAt=Split at:
|
||||
pageEditor.insertedPageBreak=Inserted page break at:
|
||||
pageEditor.addFileNotImplemented=Add file not implemented in demo
|
||||
pageEditor.closePdf=Close PDF
|
||||
|
||||
# Viewer
|
||||
viewer.noPdfLoaded=No PDF loaded. Click to upload a PDF.
|
||||
viewer.choosePdf=Choose PDF
|
||||
viewer.noPagesToDisplay=No pages to display.
|
||||
viewer.singlePageView=Single Page View
|
||||
viewer.dualPageView=Dual Page View
|
||||
viewer.hideSidebars=Hide Sidebars
|
||||
viewer.showSidebars=Show Sidebars
|
||||
viewer.zoomOut=Zoom out
|
||||
viewer.zoomIn=Zoom in
|
||||
|
||||
# Tool Picker
|
||||
toolPicker.searchPlaceholder=Search tools...
|
||||
toolPicker.noToolsFound=No tools found
|
||||
pageEditor.reset=Reset Changes
|
||||
pageEditor.zoomIn=Zoom In
|
||||
pageEditor.zoomOut=Zoom Out
|
||||
pageEditor.fitToWidth=Fit to Width
|
||||
pageEditor.actualSize=Actual Size
|
||||
|
||||
# Viewer
|
||||
viewer.previousPage=Previous Page
|
||||
viewer.nextPage=Next Page
|
||||
viewer.pageNavigation=Page Navigation
|
||||
viewer.currentPage=Current Page
|
||||
viewer.totalPages=Total Pages
|
||||
|
||||
@@ -35,6 +35,10 @@ dependencies {
|
||||
api 'org.springframework:spring-webmvc'
|
||||
api 'org.springframework.session:spring-session-core'
|
||||
api "org.springframework.security:spring-security-core:$springSecuritySamlVersion"
|
||||
api "org.springframework.security:spring-security-web:$springSecuritySamlVersion"
|
||||
api "org.springframework.security:spring-security-config:$springSecuritySamlVersion"
|
||||
api("org.springframework.boot:spring-boot-starter-oauth2-resource-server:3.5.4")
|
||||
api "org.springframework.security:spring-security-oauth2-jose:$springSecuritySamlVersion"
|
||||
api "org.springframework.security:spring-security-saml2-service-provider:$springSecuritySamlVersion"
|
||||
api 'org.springframework.boot:spring-boot-starter-jetty'
|
||||
api 'org.springframework.boot:spring-boot-starter-security'
|
||||
@@ -47,7 +51,7 @@ dependencies {
|
||||
// https://mvnrepository.com/artifact/com.bucket4j/bucket4j_jdk17
|
||||
implementation 'org.bouncycastle:bcprov-jdk18on:1.81'
|
||||
|
||||
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5:3.1.3.RELEASE'
|
||||
// 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'
|
||||
runtimeOnly 'com.h2database:h2:2.3.232' // Don't upgrade h2database
|
||||
|
||||
+1
-2
@@ -22,7 +22,6 @@ import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
@@ -46,7 +45,7 @@ import stirling.software.proprietary.security.config.EnterpriseEndpoint;
|
||||
|
||||
/** Controller for the audit dashboard. Admin-only access. */
|
||||
@Slf4j
|
||||
@Controller
|
||||
// @Controller // Disabled - Backend-only mode, no Thymeleaf UI
|
||||
@RequestMapping("/audit")
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
@RequiredArgsConstructor
|
||||
|
||||
+484
@@ -0,0 +1,484 @@
|
||||
package stirling.software.proprietary.controller.api;
|
||||
|
||||
import static stirling.software.common.util.ProviderUtils.validateProvider;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.*;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.model.ApplicationProperties.Security;
|
||||
import stirling.software.common.model.ApplicationProperties.Security.OAUTH2;
|
||||
import stirling.software.common.model.ApplicationProperties.Security.OAUTH2.Client;
|
||||
import stirling.software.common.model.ApplicationProperties.Security.SAML2;
|
||||
import stirling.software.common.model.FileInfo;
|
||||
import stirling.software.common.model.enumeration.Role;
|
||||
import stirling.software.common.model.oauth2.GitHubProvider;
|
||||
import stirling.software.common.model.oauth2.GoogleProvider;
|
||||
import stirling.software.common.model.oauth2.KeycloakProvider;
|
||||
import stirling.software.proprietary.audit.AuditEventType;
|
||||
import stirling.software.proprietary.audit.AuditLevel;
|
||||
import stirling.software.proprietary.config.AuditConfigurationProperties;
|
||||
import stirling.software.proprietary.model.Team;
|
||||
import stirling.software.proprietary.model.dto.TeamWithUserCountDTO;
|
||||
import stirling.software.proprietary.security.config.EnterpriseEndpoint;
|
||||
import stirling.software.proprietary.security.database.repository.SessionRepository;
|
||||
import stirling.software.proprietary.security.database.repository.UserRepository;
|
||||
import stirling.software.proprietary.security.model.Authority;
|
||||
import stirling.software.proprietary.security.model.SessionEntity;
|
||||
import stirling.software.proprietary.security.model.User;
|
||||
import stirling.software.proprietary.security.repository.TeamRepository;
|
||||
import stirling.software.proprietary.security.saml2.CustomSaml2AuthenticatedPrincipal;
|
||||
import stirling.software.proprietary.security.service.DatabaseService;
|
||||
import stirling.software.proprietary.security.service.TeamService;
|
||||
import stirling.software.proprietary.security.session.SessionPersistentRegistry;
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/proprietary/ui-data")
|
||||
@Tag(name = "Proprietary UI Data", description = "APIs for React UI data (Proprietary features)")
|
||||
@EnterpriseEndpoint
|
||||
public class ProprietaryUIDataController {
|
||||
|
||||
private final ApplicationProperties applicationProperties;
|
||||
private final AuditConfigurationProperties auditConfig;
|
||||
private final SessionPersistentRegistry sessionPersistentRegistry;
|
||||
private final UserRepository userRepository;
|
||||
private final TeamRepository teamRepository;
|
||||
private final SessionRepository sessionRepository;
|
||||
private final DatabaseService databaseService;
|
||||
private final boolean runningEE;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public ProprietaryUIDataController(
|
||||
ApplicationProperties applicationProperties,
|
||||
AuditConfigurationProperties auditConfig,
|
||||
SessionPersistentRegistry sessionPersistentRegistry,
|
||||
UserRepository userRepository,
|
||||
TeamRepository teamRepository,
|
||||
SessionRepository sessionRepository,
|
||||
DatabaseService databaseService,
|
||||
ObjectMapper objectMapper,
|
||||
@Qualifier("runningEE") boolean runningEE) {
|
||||
this.applicationProperties = applicationProperties;
|
||||
this.auditConfig = auditConfig;
|
||||
this.sessionPersistentRegistry = sessionPersistentRegistry;
|
||||
this.userRepository = userRepository;
|
||||
this.teamRepository = teamRepository;
|
||||
this.sessionRepository = sessionRepository;
|
||||
this.databaseService = databaseService;
|
||||
this.objectMapper = objectMapper;
|
||||
this.runningEE = runningEE;
|
||||
}
|
||||
|
||||
@GetMapping("/audit-dashboard")
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
@Operation(summary = "Get audit dashboard data")
|
||||
public ResponseEntity<AuditDashboardData> getAuditDashboardData() {
|
||||
AuditDashboardData data = new AuditDashboardData();
|
||||
data.setAuditEnabled(auditConfig.isEnabled());
|
||||
data.setAuditLevel(auditConfig.getAuditLevel());
|
||||
data.setAuditLevelInt(auditConfig.getLevel());
|
||||
data.setRetentionDays(auditConfig.getRetentionDays());
|
||||
data.setAuditLevels(AuditLevel.values());
|
||||
data.setAuditEventTypes(AuditEventType.values());
|
||||
|
||||
return ResponseEntity.ok(data);
|
||||
}
|
||||
|
||||
@GetMapping("/login")
|
||||
@Operation(summary = "Get login page data")
|
||||
public ResponseEntity<LoginData> getLoginData() {
|
||||
LoginData data = new LoginData();
|
||||
Map<String, String> providerList = new HashMap<>();
|
||||
Security securityProps = applicationProperties.getSecurity();
|
||||
OAUTH2 oauth = securityProps.getOauth2();
|
||||
|
||||
if (oauth != null && oauth.getEnabled()) {
|
||||
if (oauth.isSettingsValid()) {
|
||||
String firstChar = String.valueOf(oauth.getProvider().charAt(0));
|
||||
String clientName =
|
||||
oauth.getProvider().replaceFirst(firstChar, firstChar.toUpperCase());
|
||||
providerList.put("/oauth2/authorization/" + oauth.getProvider(), clientName);
|
||||
}
|
||||
|
||||
Client client = oauth.getClient();
|
||||
if (client != null) {
|
||||
GoogleProvider google = client.getGoogle();
|
||||
if (validateProvider(google)) {
|
||||
providerList.put(
|
||||
"/oauth2/authorization/" + google.getName(), google.getClientName());
|
||||
}
|
||||
|
||||
GitHubProvider github = client.getGithub();
|
||||
if (validateProvider(github)) {
|
||||
providerList.put(
|
||||
"/oauth2/authorization/" + github.getName(), github.getClientName());
|
||||
}
|
||||
|
||||
KeycloakProvider keycloak = client.getKeycloak();
|
||||
if (validateProvider(keycloak)) {
|
||||
providerList.put(
|
||||
"/oauth2/authorization/" + keycloak.getName(),
|
||||
keycloak.getClientName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SAML2 saml2 = securityProps.getSaml2();
|
||||
if (securityProps.isSaml2Active()
|
||||
&& applicationProperties.getSystem().getEnableAlphaFunctionality()
|
||||
&& applicationProperties.getPremium().isEnabled()) {
|
||||
String samlIdp = saml2.getProvider();
|
||||
String saml2AuthenticationPath = "/saml2/authenticate/" + saml2.getRegistrationId();
|
||||
|
||||
if (!applicationProperties.getPremium().getProFeatures().isSsoAutoLogin()) {
|
||||
providerList.put(saml2AuthenticationPath, samlIdp + " (SAML 2)");
|
||||
}
|
||||
}
|
||||
|
||||
// Remove null entries
|
||||
providerList
|
||||
.entrySet()
|
||||
.removeIf(entry -> entry.getKey() == null || entry.getValue() == null);
|
||||
|
||||
data.setProviderList(providerList);
|
||||
data.setLoginMethod(securityProps.getLoginMethod());
|
||||
data.setAltLogin(!providerList.isEmpty() && securityProps.isAltLogin());
|
||||
|
||||
return ResponseEntity.ok(data);
|
||||
}
|
||||
|
||||
@GetMapping("/admin-settings")
|
||||
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||
@Operation(summary = "Get admin settings data")
|
||||
public ResponseEntity<AdminSettingsData> getAdminSettingsData(Authentication authentication) {
|
||||
List<User> allUsers = userRepository.findAllWithTeam();
|
||||
Iterator<User> iterator = allUsers.iterator();
|
||||
Map<String, String> roleDetails = Role.getAllRoleDetails();
|
||||
|
||||
Map<String, Boolean> userSessions = new HashMap<>();
|
||||
Map<String, Date> userLastRequest = new HashMap<>();
|
||||
int activeUsers = 0;
|
||||
int disabledUsers = 0;
|
||||
|
||||
while (iterator.hasNext()) {
|
||||
User user = iterator.next();
|
||||
if (user != null) {
|
||||
boolean shouldRemove = false;
|
||||
|
||||
// Check if user is an INTERNAL_API_USER
|
||||
for (Authority authority : user.getAuthorities()) {
|
||||
if (authority.getAuthority().equals(Role.INTERNAL_API_USER.getRoleId())) {
|
||||
shouldRemove = true;
|
||||
roleDetails.remove(Role.INTERNAL_API_USER.getRoleId());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if user is part of the Internal team
|
||||
if (user.getTeam() != null
|
||||
&& user.getTeam().getName().equals(TeamService.INTERNAL_TEAM_NAME)) {
|
||||
shouldRemove = true;
|
||||
}
|
||||
|
||||
if (shouldRemove) {
|
||||
iterator.remove();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Session status and last request time
|
||||
int maxInactiveInterval = sessionPersistentRegistry.getMaxInactiveInterval();
|
||||
boolean hasActiveSession = false;
|
||||
Date lastRequest = null;
|
||||
Optional<SessionEntity> latestSession =
|
||||
sessionPersistentRegistry.findLatestSession(user.getUsername());
|
||||
|
||||
if (latestSession.isPresent()) {
|
||||
SessionEntity sessionEntity = latestSession.get();
|
||||
Date lastAccessedTime = sessionEntity.getLastRequest();
|
||||
Instant now = Instant.now();
|
||||
Instant expirationTime =
|
||||
lastAccessedTime
|
||||
.toInstant()
|
||||
.plus(maxInactiveInterval, ChronoUnit.SECONDS);
|
||||
|
||||
if (now.isAfter(expirationTime)) {
|
||||
sessionPersistentRegistry.expireSession(sessionEntity.getSessionId());
|
||||
} else {
|
||||
hasActiveSession = !sessionEntity.isExpired();
|
||||
}
|
||||
lastRequest = sessionEntity.getLastRequest();
|
||||
} else {
|
||||
lastRequest = new Date(0);
|
||||
}
|
||||
|
||||
userSessions.put(user.getUsername(), hasActiveSession);
|
||||
userLastRequest.put(user.getUsername(), lastRequest);
|
||||
|
||||
if (hasActiveSession) activeUsers++;
|
||||
if (!user.isEnabled()) disabledUsers++;
|
||||
}
|
||||
}
|
||||
|
||||
// Sort users by active status and last request date
|
||||
List<User> sortedUsers =
|
||||
allUsers.stream()
|
||||
.sorted(
|
||||
(u1, u2) -> {
|
||||
boolean u1Active = userSessions.get(u1.getUsername());
|
||||
boolean u2Active = userSessions.get(u2.getUsername());
|
||||
if (u1Active && !u2Active) return -1;
|
||||
if (!u1Active && u2Active) return 1;
|
||||
|
||||
Date u1LastRequest =
|
||||
userLastRequest.getOrDefault(
|
||||
u1.getUsername(), new Date(0));
|
||||
Date u2LastRequest =
|
||||
userLastRequest.getOrDefault(
|
||||
u2.getUsername(), new Date(0));
|
||||
return u2LastRequest.compareTo(u1LastRequest);
|
||||
})
|
||||
.toList();
|
||||
|
||||
List<Team> allTeams =
|
||||
teamRepository.findAll().stream()
|
||||
.filter(team -> !team.getName().equals(TeamService.INTERNAL_TEAM_NAME))
|
||||
.toList();
|
||||
|
||||
AdminSettingsData data = new AdminSettingsData();
|
||||
data.setUsers(sortedUsers);
|
||||
data.setCurrentUsername(authentication.getName());
|
||||
data.setRoleDetails(roleDetails);
|
||||
data.setUserSessions(userSessions);
|
||||
data.setUserLastRequest(userLastRequest);
|
||||
data.setTotalUsers(allUsers.size());
|
||||
data.setActiveUsers(activeUsers);
|
||||
data.setDisabledUsers(disabledUsers);
|
||||
data.setTeams(allTeams);
|
||||
data.setMaxPaidUsers(applicationProperties.getPremium().getMaxUsers());
|
||||
|
||||
return ResponseEntity.ok(data);
|
||||
}
|
||||
|
||||
@GetMapping("/account")
|
||||
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
||||
@Operation(summary = "Get account page data")
|
||||
public ResponseEntity<AccountData> getAccountData(Authentication authentication) {
|
||||
if (authentication == null || !authentication.isAuthenticated()) {
|
||||
return ResponseEntity.status(401).build();
|
||||
}
|
||||
|
||||
Object principal = authentication.getPrincipal();
|
||||
String username = null;
|
||||
boolean isOAuth2Login = false;
|
||||
boolean isSaml2Login = false;
|
||||
|
||||
if (principal instanceof UserDetails detailsUser) {
|
||||
username = detailsUser.getUsername();
|
||||
} else if (principal instanceof OAuth2User oAuth2User) {
|
||||
username = oAuth2User.getName();
|
||||
isOAuth2Login = true;
|
||||
} else if (principal instanceof CustomSaml2AuthenticatedPrincipal saml2User) {
|
||||
username = saml2User.name();
|
||||
isSaml2Login = true;
|
||||
}
|
||||
|
||||
if (username == null) {
|
||||
return ResponseEntity.status(401).build();
|
||||
}
|
||||
|
||||
Optional<User> user = userRepository.findByUsernameIgnoreCaseWithSettings(username);
|
||||
if (user.isEmpty()) {
|
||||
return ResponseEntity.status(404).build();
|
||||
}
|
||||
|
||||
String settingsJson;
|
||||
try {
|
||||
settingsJson = objectMapper.writeValueAsString(user.get().getSettings());
|
||||
} catch (JsonProcessingException e) {
|
||||
log.error("Error converting settings map", e);
|
||||
return ResponseEntity.status(500).build();
|
||||
}
|
||||
|
||||
AccountData data = new AccountData();
|
||||
data.setUsername(username);
|
||||
data.setRole(user.get().getRolesAsString());
|
||||
data.setSettings(settingsJson);
|
||||
data.setChangeCredsFlag(user.get().isFirstLogin());
|
||||
data.setOAuth2Login(isOAuth2Login);
|
||||
data.setSaml2Login(isSaml2Login);
|
||||
|
||||
return ResponseEntity.ok(data);
|
||||
}
|
||||
|
||||
@GetMapping("/teams")
|
||||
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||
@Operation(summary = "Get teams list data")
|
||||
public ResponseEntity<TeamsData> getTeamsData() {
|
||||
List<TeamWithUserCountDTO> allTeamsWithCounts = teamRepository.findAllTeamsWithUserCount();
|
||||
List<TeamWithUserCountDTO> teamsWithCounts =
|
||||
allTeamsWithCounts.stream()
|
||||
.filter(team -> !team.getName().equals(TeamService.INTERNAL_TEAM_NAME))
|
||||
.toList();
|
||||
|
||||
List<Object[]> teamActivities = sessionRepository.findLatestActivityByTeam();
|
||||
Map<Long, Date> teamLastRequest = new HashMap<>();
|
||||
for (Object[] result : teamActivities) {
|
||||
Long teamId = (Long) result[0];
|
||||
Date lastActivity = (Date) result[1];
|
||||
teamLastRequest.put(teamId, lastActivity);
|
||||
}
|
||||
|
||||
TeamsData data = new TeamsData();
|
||||
data.setTeamsWithCounts(teamsWithCounts);
|
||||
data.setTeamLastRequest(teamLastRequest);
|
||||
|
||||
return ResponseEntity.ok(data);
|
||||
}
|
||||
|
||||
@GetMapping("/teams/{id}")
|
||||
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||
@Operation(summary = "Get team details data")
|
||||
public ResponseEntity<TeamDetailsData> getTeamDetailsData(@PathVariable("id") Long id) {
|
||||
Team team =
|
||||
teamRepository
|
||||
.findById(id)
|
||||
.orElseThrow(() -> new RuntimeException("Team not found"));
|
||||
|
||||
if (team.getName().equals(TeamService.INTERNAL_TEAM_NAME)) {
|
||||
return ResponseEntity.status(403).build();
|
||||
}
|
||||
|
||||
List<User> teamUsers = userRepository.findAllByTeamId(id);
|
||||
List<User> allUsers = userRepository.findAllWithTeam();
|
||||
List<User> availableUsers =
|
||||
allUsers.stream()
|
||||
.filter(
|
||||
user ->
|
||||
(user.getTeam() == null
|
||||
|| !user.getTeam().getId().equals(id))
|
||||
&& (user.getTeam() == null
|
||||
|| !user.getTeam()
|
||||
.getName()
|
||||
.equals(
|
||||
TeamService
|
||||
.INTERNAL_TEAM_NAME)))
|
||||
.toList();
|
||||
|
||||
List<Object[]> userSessions = sessionRepository.findLatestSessionByTeamId(id);
|
||||
Map<String, Date> userLastRequest = new HashMap<>();
|
||||
for (Object[] result : userSessions) {
|
||||
String username = (String) result[0];
|
||||
Date lastRequest = (Date) result[1];
|
||||
userLastRequest.put(username, lastRequest);
|
||||
}
|
||||
|
||||
TeamDetailsData data = new TeamDetailsData();
|
||||
data.setTeam(team);
|
||||
data.setTeamUsers(teamUsers);
|
||||
data.setAvailableUsers(availableUsers);
|
||||
data.setUserLastRequest(userLastRequest);
|
||||
|
||||
return ResponseEntity.ok(data);
|
||||
}
|
||||
|
||||
@GetMapping("/database")
|
||||
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||
@Operation(summary = "Get database management data")
|
||||
public ResponseEntity<DatabaseData> getDatabaseData() {
|
||||
List<FileInfo> backupList = databaseService.getBackupList();
|
||||
String dbVersion = databaseService.getH2Version();
|
||||
boolean isVersionUnknown = "Unknown".equalsIgnoreCase(dbVersion);
|
||||
|
||||
DatabaseData data = new DatabaseData();
|
||||
data.setBackupFiles(backupList);
|
||||
data.setDatabaseVersion(dbVersion);
|
||||
data.setVersionUnknown(isVersionUnknown);
|
||||
|
||||
return ResponseEntity.ok(data);
|
||||
}
|
||||
|
||||
// Data classes
|
||||
@Data
|
||||
public static class AuditDashboardData {
|
||||
private boolean auditEnabled;
|
||||
private AuditLevel auditLevel;
|
||||
private int auditLevelInt;
|
||||
private int retentionDays;
|
||||
private AuditLevel[] auditLevels;
|
||||
private AuditEventType[] auditEventTypes;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class LoginData {
|
||||
private Map<String, String> providerList;
|
||||
private String loginMethod;
|
||||
private boolean altLogin;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class AdminSettingsData {
|
||||
private List<User> users;
|
||||
private String currentUsername;
|
||||
private Map<String, String> roleDetails;
|
||||
private Map<String, Boolean> userSessions;
|
||||
private Map<String, Date> userLastRequest;
|
||||
private int totalUsers;
|
||||
private int activeUsers;
|
||||
private int disabledUsers;
|
||||
private List<Team> teams;
|
||||
private int maxPaidUsers;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class AccountData {
|
||||
private String username;
|
||||
private String role;
|
||||
private String settings;
|
||||
private boolean changeCredsFlag;
|
||||
private boolean oAuth2Login;
|
||||
private boolean saml2Login;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class TeamsData {
|
||||
private List<TeamWithUserCountDTO> teamsWithCounts;
|
||||
private Map<Long, Date> teamLastRequest;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class TeamDetailsData {
|
||||
private Team team;
|
||||
private List<User> teamUsers;
|
||||
private List<User> availableUsers;
|
||||
private Map<String, Date> userLastRequest;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class DatabaseData {
|
||||
private List<FileInfo> backupFiles;
|
||||
private String databaseVersion;
|
||||
private boolean versionUnknown;
|
||||
}
|
||||
}
|
||||
-11
@@ -7,14 +7,3 @@ import lombok.RequiredArgsConstructor;
|
||||
|
||||
import stirling.software.proprietary.security.filter.IPRateLimitingFilter;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class RateLimitResetScheduler {
|
||||
|
||||
private final IPRateLimitingFilter rateLimitingFilter;
|
||||
|
||||
@Scheduled(cron = "0 0 0 * * MON") // At 00:00 every Monday TODO: configurable
|
||||
public void resetRateLimit() {
|
||||
rateLimitingFilter.resetRequestCounts();
|
||||
}
|
||||
}
|
||||
|
||||
+10
-13
@@ -12,13 +12,10 @@ import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
@@ -48,7 +45,7 @@ import stirling.software.proprietary.security.saml2.CustomSaml2AuthenticatedPrin
|
||||
import stirling.software.proprietary.security.service.TeamService;
|
||||
import stirling.software.proprietary.security.session.SessionPersistentRegistry;
|
||||
|
||||
@Controller
|
||||
// @Controller // Disabled - Backend-only mode, no Thymeleaf UI
|
||||
@Slf4j
|
||||
@Tag(name = "Account Security", description = "Account Security APIs")
|
||||
public class AccountWebController {
|
||||
@@ -75,7 +72,7 @@ public class AccountWebController {
|
||||
this.teamRepository = teamRepository;
|
||||
}
|
||||
|
||||
@GetMapping("/login")
|
||||
// @GetMapping("/login")
|
||||
public String login(HttpServletRequest request, Model model, Authentication authentication) {
|
||||
// If the user is already authenticated, redirect them to the home page.
|
||||
if (authentication != null && authentication.isAuthenticated()) {
|
||||
@@ -203,8 +200,8 @@ public class AccountWebController {
|
||||
return "login";
|
||||
}
|
||||
|
||||
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||
@GetMapping("/usage")
|
||||
// @PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||
// @GetMapping("/usage")
|
||||
public String showUsage() {
|
||||
if (!runningEE) {
|
||||
return "error";
|
||||
@@ -212,8 +209,8 @@ public class AccountWebController {
|
||||
return "usage";
|
||||
}
|
||||
|
||||
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||
@GetMapping("/adminSettings")
|
||||
// @PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||
// @GetMapping("/adminSettings")
|
||||
public String showAddUserForm(
|
||||
HttpServletRequest request, Model model, Authentication authentication) {
|
||||
List<User> allUsers = userRepository.findAllWithTeam();
|
||||
@@ -371,8 +368,8 @@ public class AccountWebController {
|
||||
return "adminSettings";
|
||||
}
|
||||
|
||||
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
||||
@GetMapping("/account")
|
||||
// @PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
||||
// @GetMapping("/account")
|
||||
public String account(HttpServletRequest request, Model model, Authentication authentication) {
|
||||
if (authentication == null || !authentication.isAuthenticated()) {
|
||||
return "redirect:/";
|
||||
@@ -436,8 +433,8 @@ public class AccountWebController {
|
||||
return "account";
|
||||
}
|
||||
|
||||
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
||||
@GetMapping("/change-creds")
|
||||
// @PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
||||
// @GetMapping("/change-creds")
|
||||
public String changeCreds(
|
||||
HttpServletRequest request, Model model, Authentication authentication) {
|
||||
if (authentication == null || !authentication.isAuthenticated()) {
|
||||
|
||||
+311
@@ -0,0 +1,311 @@
|
||||
package stirling.software.proprietary.security.configuration;
|
||||
|
||||
|
||||
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||
import org.springframework.security.authentication.event.AbstractAuthenticationEvent;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
|
||||
import org.springframework.security.oauth2.server.resource.InvalidBearerTokenException;
|
||||
import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationEntryPoint;
|
||||
import org.springframework.security.oauth2.server.resource.web.access.BearerTokenAccessDeniedHandler;
|
||||
import org.springframework.security.oauth2.server.resource.web.authentication.BearerTokenAuthenticationFilter;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
import org.springframework.web.cors.CorsConfigurationSource;
|
||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Security configuration for Supabase-issued JWTs only.
|
||||
*
|
||||
* Requires:
|
||||
*
|
||||
* spring:
|
||||
* security:
|
||||
* oauth2:
|
||||
* resourceserver:
|
||||
* jwt:
|
||||
* issuer-uri: https://<project-id>.supabase.co/auth/v1
|
||||
*
|
||||
* Optional logging (application.yml):
|
||||
* logging:
|
||||
* level:
|
||||
* org.springframework.security: DEBUG
|
||||
* your.project.pkg.security: DEBUG
|
||||
*/
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
public class SecurityConfig {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(SecurityConfig.class);
|
||||
|
||||
/** Your Supabase project ref, e.g. abcd1234efgh5678ijkl */
|
||||
@Value("${app.supabase.project-ref:nrlkjfznsavsbmweiyqu}")
|
||||
private String projectRef;
|
||||
|
||||
/** Optional audience to enforce (leave empty to skip) */
|
||||
@Value("${app.jwt.expected-aud:}")
|
||||
private String expectedAud;
|
||||
|
||||
/** Clock skew in seconds for exp validation */
|
||||
@Value("${app.jwt.clock-skew-seconds:120}")
|
||||
private long clockSkewSeconds;
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain securityFilterChain(HttpSecurity http, JwtDecoder jwtDecoder) throws Exception {
|
||||
http
|
||||
.csrf(AbstractHttpConfigurer::disable)
|
||||
.cors(Customizer.withDefaults())
|
||||
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
// allow CORS preflight only
|
||||
.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
|
||||
// public endpoints
|
||||
.requestMatchers(
|
||||
new AntPathRequestMatcher("/actuator/health"),
|
||||
new AntPathRequestMatcher("/public/**"),
|
||||
new AntPathRequestMatcher("/images/**"),
|
||||
new AntPathRequestMatcher("/css/**"),
|
||||
new AntPathRequestMatcher("/js/**")
|
||||
).permitAll()
|
||||
// everything else requires auth
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.addFilterBefore(new VerboseAuthLoggingFilter(), BearerTokenAuthenticationFilter.class)
|
||||
.exceptionHandling(ex -> ex
|
||||
.authenticationEntryPoint(new BearerTokenAuthenticationEntryPoint())
|
||||
.accessDeniedHandler(new BearerTokenAccessDeniedHandler())
|
||||
)
|
||||
.oauth2ResourceServer(oauth -> oauth
|
||||
.jwt(jwt -> jwt
|
||||
.decoder(jwtDecoder)
|
||||
.jwtAuthenticationConverter(this::toAuthentication)
|
||||
)
|
||||
);
|
||||
|
||||
return http.build();
|
||||
}
|
||||
|
||||
/** CORS config so browser can send Authorization on real requests. */
|
||||
@Bean
|
||||
CorsConfigurationSource corsConfigurationSource() {
|
||||
CorsConfiguration cfg = new CorsConfiguration();
|
||||
cfg.setAllowedOrigins(List.of(
|
||||
"http://localhost:3000", // dev
|
||||
"http://localhost:5173",
|
||||
"http://localhost:8080",
|
||||
"https://your-frontend.example"// prod
|
||||
));
|
||||
cfg.setAllowedMethods(List.of("GET","POST","PUT","PATCH","DELETE","OPTIONS"));
|
||||
cfg.setAllowedHeaders(List.of("Authorization","Content-Type","X-Requested-With","Accept","Origin"));
|
||||
cfg.setExposedHeaders(List.of("WWW-Authenticate"));
|
||||
cfg.setAllowCredentials(true);
|
||||
cfg.setMaxAge(3600L);
|
||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||
source.registerCorsConfiguration("/**", cfg);
|
||||
return source;
|
||||
}
|
||||
|
||||
/** JWKS-based decoder + custom validator (issuer/exp/aud). */
|
||||
@Bean
|
||||
JwtDecoder jwtDecoder() {
|
||||
String issuer = "https://" + projectRef + ".supabase.co/auth/v1"; // no trailing slash
|
||||
String jwks = issuer + "/.well-known/jwks.json";
|
||||
|
||||
log.info("Configuring JWT decoder with JWKS: {}", jwks);
|
||||
NimbusJwtDecoder decoder = NimbusJwtDecoder.withJwkSetUri(jwks).build();
|
||||
decoder.setJwtValidator(new CompositeValidator(issuer, expectedAud, Duration.ofSeconds(clockSkewSeconds)));
|
||||
|
||||
if (expectedAud == null || expectedAud.isBlank()) {
|
||||
log.info("JWT validation: enforcing issuer='{}' and exp (skew {}s)", issuer, clockSkewSeconds);
|
||||
} else {
|
||||
log.info("JWT validation: enforcing issuer='{}', audience='{}', and exp (skew {}s)",
|
||||
issuer, expectedAud, clockSkewSeconds);
|
||||
}
|
||||
return decoder;
|
||||
}
|
||||
|
||||
/** Map claims -> authorities. DEBUG: hotmail.com => ROLE_ADMIN. */
|
||||
private AbstractAuthenticationToken toAuthentication(Jwt jwt) {
|
||||
List<GrantedAuthority> authorities = new ArrayList<>();
|
||||
|
||||
// Supabase default role -> ROLE_authenticated (optional but handy)
|
||||
String supabaseRole = jwt.getClaimAsString("role"); // often "authenticated"
|
||||
if (supabaseRole != null && !supabaseRole.isBlank()) {
|
||||
authorities.add(new SimpleGrantedAuthority("ROLE_" + supabaseRole));
|
||||
}
|
||||
|
||||
// Your custom app_role (if you add it via Access Token Hook) -> ROLE_*
|
||||
String appRole = jwt.getClaimAsString("app_role");
|
||||
if (appRole != null && !appRole.isBlank()) {
|
||||
authorities.add(new SimpleGrantedAuthority("ROLE_" + appRole.toUpperCase()));
|
||||
}
|
||||
|
||||
// DEBUG RULE: email domain → admin
|
||||
String email = jwt.getClaimAsString("email");
|
||||
if (email != null && email.toLowerCase(Locale.ROOT).endsWith("@hotmail.com")) {
|
||||
authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
|
||||
} else {
|
||||
// Give a basic user role for convenience while debugging (optional)
|
||||
authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
|
||||
}
|
||||
|
||||
// Optional permissions array -> PERM_*
|
||||
List<String> perms = jwt.getClaimAsStringList("permissions");
|
||||
if (perms != null) {
|
||||
perms.stream()
|
||||
.filter(p -> p != null && !p.isBlank())
|
||||
.map(p -> new SimpleGrantedAuthority("PERM_" + p))
|
||||
.forEach(authorities::add);
|
||||
}
|
||||
|
||||
String principalName = (email != null && !email.isBlank()) ? email : jwt.getSubject();
|
||||
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("JWT accepted: sub='{}', email='{}', supabase.role='{}', app_role='{}', permissions={}",
|
||||
jwt.getSubject(), email, supabaseRole, appRole, perms);
|
||||
log.debug("Granted authorities: {}", authorities.stream()
|
||||
.map(GrantedAuthority::getAuthority).collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
return new JwtAuthenticationToken(jwt, authorities, principalName);
|
||||
}
|
||||
|
||||
/** Logs authentication lifecycle events (success/failure). */
|
||||
@Bean
|
||||
ApplicationListener<AbstractAuthenticationEvent> authenticationEventsLogger() {
|
||||
return event -> {
|
||||
try {
|
||||
if (event.getSource() instanceof AbstractAuthenticationToken auth) {
|
||||
String type = event.getClass().getSimpleName();
|
||||
String name = auth.getName();
|
||||
String authorities = auth.getAuthorities().stream()
|
||||
.map(GrantedAuthority::getAuthority).collect(Collectors.joining(","));
|
||||
log.debug("[AuthEvent] {} principal='{}' authorities='{}' details={}",
|
||||
type, name, authorities, auth.getDetails());
|
||||
} else {
|
||||
log.debug("[AuthEvent] {} source={}", event.getClass().getSimpleName(), event.getSource());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to log authentication event", e);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** Super-chatty per-request logger around bearer processing; never logs raw token. */
|
||||
static class VerboseAuthLoggingFilter extends OncePerRequestFilter {
|
||||
private static final Logger flog = LoggerFactory.getLogger(VerboseAuthLoggingFilter.class);
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(
|
||||
@NonNull HttpServletRequest request,
|
||||
@NonNull HttpServletResponse response,
|
||||
@NonNull FilterChain filterChain
|
||||
) throws ServletException, IOException {
|
||||
|
||||
String authHeader = request.getHeader("Authorization");
|
||||
boolean hasBearer = authHeader != null && authHeader.startsWith("Bearer ");
|
||||
if (flog.isDebugEnabled()) {
|
||||
flog.debug("[REQ] {} {} AuthorizationHeaderPresent={} (token hidden)",
|
||||
request.getMethod(), request.getRequestURI(), hasBearer);
|
||||
}
|
||||
|
||||
try {
|
||||
filterChain.doFilter(request, response);
|
||||
} catch (InvalidBearerTokenException ibte) {
|
||||
flog.warn("[AUTH] Invalid bearer token: {}", ibte.getMessage());
|
||||
throw ibte;
|
||||
} catch (Exception ex) {
|
||||
flog.error("[AUTH] Unexpected auth error: {}", ex.toString(), ex);
|
||||
throw ex;
|
||||
}
|
||||
|
||||
var auth = org.springframework.security.core.context.SecurityContextHolder.getContext().getAuthentication();
|
||||
if (auth instanceof JwtAuthenticationToken jwtAuth) {
|
||||
String authorities = jwtAuth.getAuthorities().stream()
|
||||
.map(GrantedAuthority::getAuthority).collect(Collectors.joining(","));
|
||||
flog.debug("[AUTH] OK principal='{}' authorities='{}'", jwtAuth.getName(), authorities);
|
||||
} else {
|
||||
flog.debug("[AUTH] No authentication established");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Validator: issuer == expected, not expired (with skew), optional audience. */
|
||||
static final class CompositeValidator implements OAuth2TokenValidator<Jwt> {
|
||||
private final String expectedIssuer; // not null
|
||||
private final String expectedAudienceOrNull; // may be null/blank
|
||||
private final Duration skew;
|
||||
|
||||
CompositeValidator(String expectedIssuer, String expectedAudienceOrNull, Duration skew) {
|
||||
this.expectedIssuer = Objects.requireNonNull(expectedIssuer);
|
||||
this.expectedAudienceOrNull = (expectedAudienceOrNull != null && !expectedAudienceOrNull.isBlank())
|
||||
? expectedAudienceOrNull : null;
|
||||
this.skew = Objects.requireNonNull(skew);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OAuth2TokenValidatorResult validate(Jwt token) {
|
||||
List<OAuth2Error> errors = new ArrayList<>();
|
||||
|
||||
String iss = token.getIssuer() != null ? token.getIssuer().toString() : null;
|
||||
if (iss == null || !iss.equals(expectedIssuer)) {
|
||||
errors.add(new OAuth2Error("invalid_token", "Invalid issuer: " + iss, null));
|
||||
}
|
||||
|
||||
Instant exp = token.getExpiresAt();
|
||||
if (exp == null) {
|
||||
errors.add(new OAuth2Error("invalid_token", "Missing exp claim", null));
|
||||
} else if (exp.isBefore(Instant.now().minus(skew))) {
|
||||
errors.add(new OAuth2Error("invalid_token", "Token expired at " + exp, null));
|
||||
}
|
||||
|
||||
if (expectedAudienceOrNull != null) {
|
||||
List<String> aud = token.getAudience();
|
||||
if (aud == null || !aud.contains(expectedAudienceOrNull)) {
|
||||
errors.add(new OAuth2Error("invalid_token", "Missing/invalid audience: " + expectedAudienceOrNull, null));
|
||||
}
|
||||
}
|
||||
|
||||
if (!errors.isEmpty()) {
|
||||
errors.forEach(e -> log.warn("JWT validation error: {} - {}", e.getErrorCode(), e.getDescription()));
|
||||
return OAuth2TokenValidatorResult.failure(errors);
|
||||
}
|
||||
return OAuth2TokenValidatorResult.success();
|
||||
}
|
||||
}
|
||||
}
|
||||
-326
@@ -1,326 +0,0 @@
|
||||
package stirling.software.proprietary.security.configuration;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.DependsOn;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.security.authentication.ProviderManager;
|
||||
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
|
||||
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
|
||||
import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml4AuthenticationRequestResolver;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
|
||||
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
|
||||
import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler;
|
||||
import org.springframework.security.web.savedrequest.NullRequestCache;
|
||||
import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.configuration.AppConfig;
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.proprietary.security.CustomAuthenticationFailureHandler;
|
||||
import stirling.software.proprietary.security.CustomAuthenticationSuccessHandler;
|
||||
import stirling.software.proprietary.security.CustomLogoutSuccessHandler;
|
||||
import stirling.software.proprietary.security.database.repository.JPATokenRepositoryImpl;
|
||||
import stirling.software.proprietary.security.database.repository.PersistentLoginRepository;
|
||||
import stirling.software.proprietary.security.filter.FirstLoginFilter;
|
||||
import stirling.software.proprietary.security.filter.IPRateLimitingFilter;
|
||||
import stirling.software.proprietary.security.filter.UserAuthenticationFilter;
|
||||
import stirling.software.proprietary.security.model.User;
|
||||
import stirling.software.proprietary.security.oauth2.CustomOAuth2AuthenticationFailureHandler;
|
||||
import stirling.software.proprietary.security.oauth2.CustomOAuth2AuthenticationSuccessHandler;
|
||||
import stirling.software.proprietary.security.saml2.CustomSaml2AuthenticationFailureHandler;
|
||||
import stirling.software.proprietary.security.saml2.CustomSaml2AuthenticationSuccessHandler;
|
||||
import stirling.software.proprietary.security.saml2.CustomSaml2ResponseAuthenticationConverter;
|
||||
import stirling.software.proprietary.security.service.CustomOAuth2UserService;
|
||||
import stirling.software.proprietary.security.service.CustomUserDetailsService;
|
||||
import stirling.software.proprietary.security.service.LoginAttemptService;
|
||||
import stirling.software.proprietary.security.service.UserService;
|
||||
import stirling.software.proprietary.security.session.SessionPersistentRegistry;
|
||||
|
||||
@Slf4j
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@EnableMethodSecurity
|
||||
@DependsOn("runningProOrHigher")
|
||||
public class SecurityConfiguration {
|
||||
|
||||
private final CustomUserDetailsService userDetailsService;
|
||||
private final UserService userService;
|
||||
private final boolean loginEnabledValue;
|
||||
private final boolean runningProOrHigher;
|
||||
|
||||
private final ApplicationProperties applicationProperties;
|
||||
private final AppConfig appConfig;
|
||||
private final UserAuthenticationFilter userAuthenticationFilter;
|
||||
private final LoginAttemptService loginAttemptService;
|
||||
private final FirstLoginFilter firstLoginFilter;
|
||||
private final SessionPersistentRegistry sessionRegistry;
|
||||
private final PersistentLoginRepository persistentLoginRepository;
|
||||
private final GrantedAuthoritiesMapper oAuth2userAuthoritiesMapper;
|
||||
private final RelyingPartyRegistrationRepository saml2RelyingPartyRegistrations;
|
||||
private final OpenSaml4AuthenticationRequestResolver saml2AuthenticationRequestResolver;
|
||||
|
||||
public SecurityConfiguration(
|
||||
PersistentLoginRepository persistentLoginRepository,
|
||||
CustomUserDetailsService userDetailsService,
|
||||
@Lazy UserService userService,
|
||||
@Qualifier("loginEnabled") boolean loginEnabledValue,
|
||||
@Qualifier("runningProOrHigher") boolean runningProOrHigher,
|
||||
AppConfig appConfig,
|
||||
ApplicationProperties applicationProperties,
|
||||
UserAuthenticationFilter userAuthenticationFilter,
|
||||
LoginAttemptService loginAttemptService,
|
||||
FirstLoginFilter firstLoginFilter,
|
||||
SessionPersistentRegistry sessionRegistry,
|
||||
@Autowired(required = false) GrantedAuthoritiesMapper oAuth2userAuthoritiesMapper,
|
||||
@Autowired(required = false)
|
||||
RelyingPartyRegistrationRepository saml2RelyingPartyRegistrations,
|
||||
@Autowired(required = false)
|
||||
OpenSaml4AuthenticationRequestResolver saml2AuthenticationRequestResolver) {
|
||||
this.userDetailsService = userDetailsService;
|
||||
this.userService = userService;
|
||||
this.loginEnabledValue = loginEnabledValue;
|
||||
this.runningProOrHigher = runningProOrHigher;
|
||||
this.appConfig = appConfig;
|
||||
this.applicationProperties = applicationProperties;
|
||||
this.userAuthenticationFilter = userAuthenticationFilter;
|
||||
this.loginAttemptService = loginAttemptService;
|
||||
this.firstLoginFilter = firstLoginFilter;
|
||||
this.sessionRegistry = sessionRegistry;
|
||||
this.persistentLoginRepository = persistentLoginRepository;
|
||||
this.oAuth2userAuthoritiesMapper = oAuth2userAuthoritiesMapper;
|
||||
this.saml2RelyingPartyRegistrations = saml2RelyingPartyRegistrations;
|
||||
this.saml2AuthenticationRequestResolver = saml2AuthenticationRequestResolver;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
if (applicationProperties.getSecurity().getCsrfDisabled() || !loginEnabledValue) {
|
||||
http.csrf(csrf -> csrf.disable());
|
||||
}
|
||||
|
||||
if (loginEnabledValue) {
|
||||
http.addFilterBefore(
|
||||
userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
||||
if (!applicationProperties.getSecurity().getCsrfDisabled()) {
|
||||
CookieCsrfTokenRepository cookieRepo =
|
||||
CookieCsrfTokenRepository.withHttpOnlyFalse();
|
||||
CsrfTokenRequestAttributeHandler requestHandler =
|
||||
new CsrfTokenRequestAttributeHandler();
|
||||
requestHandler.setCsrfRequestAttributeName(null);
|
||||
http.csrf(
|
||||
csrf ->
|
||||
csrf.ignoringRequestMatchers(
|
||||
request -> {
|
||||
String apiKey = request.getHeader("X-API-KEY");
|
||||
// If there's no API key, don't ignore CSRF
|
||||
// (return false)
|
||||
if (apiKey == null || apiKey.trim().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
// Validate API key using existing UserService
|
||||
try {
|
||||
Optional<User> user =
|
||||
userService.getUserByApiKey(apiKey);
|
||||
// If API key is valid, ignore CSRF (return
|
||||
// true)
|
||||
// If API key is invalid, don't ignore CSRF
|
||||
// (return false)
|
||||
return user.isPresent();
|
||||
} catch (Exception e) {
|
||||
// If there's any error validating the API
|
||||
// key, don't ignore CSRF
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.csrfTokenRepository(cookieRepo)
|
||||
.csrfTokenRequestHandler(requestHandler));
|
||||
}
|
||||
http.addFilterBefore(rateLimitingFilter(), UsernamePasswordAuthenticationFilter.class);
|
||||
http.addFilterAfter(firstLoginFilter, UsernamePasswordAuthenticationFilter.class);
|
||||
http.sessionManagement(
|
||||
sessionManagement ->
|
||||
sessionManagement
|
||||
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
|
||||
.maximumSessions(10)
|
||||
.maxSessionsPreventsLogin(false)
|
||||
.sessionRegistry(sessionRegistry)
|
||||
.expiredUrl("/login?logout=true"));
|
||||
http.authenticationProvider(daoAuthenticationProvider());
|
||||
http.requestCache(requestCache -> requestCache.requestCache(new NullRequestCache()));
|
||||
http.logout(
|
||||
logout ->
|
||||
logout.logoutRequestMatcher(
|
||||
PathPatternRequestMatcher.withDefaults()
|
||||
.matcher("/logout"))
|
||||
.logoutSuccessHandler(
|
||||
new CustomLogoutSuccessHandler(
|
||||
applicationProperties, appConfig))
|
||||
.clearAuthentication(true)
|
||||
.invalidateHttpSession(true)
|
||||
.deleteCookies("JSESSIONID", "remember-me"));
|
||||
http.rememberMe(
|
||||
rememberMeConfigurer -> // Use the configurator directly
|
||||
rememberMeConfigurer
|
||||
.tokenRepository(persistentTokenRepository())
|
||||
.tokenValiditySeconds( // 14 days
|
||||
14 * 24 * 60 * 60)
|
||||
.userDetailsService( // Your existing UserDetailsService
|
||||
userDetailsService)
|
||||
.useSecureCookie( // Enable secure cookie
|
||||
true)
|
||||
.rememberMeParameter( // Form parameter name
|
||||
"remember-me")
|
||||
.rememberMeCookieName( // Cookie name
|
||||
"remember-me")
|
||||
.alwaysRemember(false));
|
||||
http.authorizeHttpRequests(
|
||||
authz ->
|
||||
authz.requestMatchers(
|
||||
req -> {
|
||||
String uri = req.getRequestURI();
|
||||
String contextPath = req.getContextPath();
|
||||
// Remove the context path from the URI
|
||||
String trimmedUri =
|
||||
uri.startsWith(contextPath)
|
||||
? uri.substring(
|
||||
contextPath.length())
|
||||
: uri;
|
||||
return trimmedUri.startsWith("/login")
|
||||
|| trimmedUri.startsWith("/oauth")
|
||||
|| trimmedUri.startsWith("/saml2")
|
||||
|| trimmedUri.endsWith(".svg")
|
||||
|| trimmedUri.startsWith("/register")
|
||||
|| trimmedUri.startsWith("/error")
|
||||
|| trimmedUri.startsWith("/images/")
|
||||
|| trimmedUri.startsWith("/public/")
|
||||
|| trimmedUri.startsWith("/css/")
|
||||
|| trimmedUri.startsWith("/fonts/")
|
||||
|| trimmedUri.startsWith("/js/")
|
||||
|| trimmedUri.startsWith(
|
||||
"/api/v1/info/status");
|
||||
})
|
||||
.permitAll()
|
||||
.anyRequest()
|
||||
.authenticated());
|
||||
// Handle User/Password Logins
|
||||
if (applicationProperties.getSecurity().isUserPass()) {
|
||||
http.formLogin(
|
||||
formLogin ->
|
||||
formLogin
|
||||
.loginPage("/login")
|
||||
.successHandler(
|
||||
new CustomAuthenticationSuccessHandler(
|
||||
loginAttemptService, userService))
|
||||
.failureHandler(
|
||||
new CustomAuthenticationFailureHandler(
|
||||
loginAttemptService, userService))
|
||||
.defaultSuccessUrl("/")
|
||||
.permitAll());
|
||||
}
|
||||
// Handle OAUTH2 Logins
|
||||
if (applicationProperties.getSecurity().isOauth2Active()) {
|
||||
http.oauth2Login(
|
||||
oauth2 ->
|
||||
oauth2.loginPage("/oauth2")
|
||||
/*
|
||||
This Custom handler is used to check if the OAUTH2 user trying to log in, already exists in the database.
|
||||
If user exists, login proceeds as usual. If user does not exist, then it is auto-created but only if 'OAUTH2AutoCreateUser'
|
||||
is set as true, else login fails with an error message advising the same.
|
||||
*/
|
||||
.successHandler(
|
||||
new CustomOAuth2AuthenticationSuccessHandler(
|
||||
loginAttemptService,
|
||||
applicationProperties,
|
||||
userService))
|
||||
.failureHandler(
|
||||
new CustomOAuth2AuthenticationFailureHandler())
|
||||
. // Add existing Authorities from the database
|
||||
userInfoEndpoint(
|
||||
userInfoEndpoint ->
|
||||
userInfoEndpoint
|
||||
.oidcUserService(
|
||||
new CustomOAuth2UserService(
|
||||
applicationProperties,
|
||||
userService,
|
||||
loginAttemptService))
|
||||
.userAuthoritiesMapper(
|
||||
oAuth2userAuthoritiesMapper))
|
||||
.permitAll());
|
||||
}
|
||||
// Handle SAML
|
||||
if (applicationProperties.getSecurity().isSaml2Active() && runningProOrHigher) {
|
||||
// Configure the authentication provider
|
||||
OpenSaml4AuthenticationProvider authenticationProvider =
|
||||
new OpenSaml4AuthenticationProvider();
|
||||
authenticationProvider.setResponseAuthenticationConverter(
|
||||
new CustomSaml2ResponseAuthenticationConverter(userService));
|
||||
http.authenticationProvider(authenticationProvider)
|
||||
.saml2Login(
|
||||
saml2 -> {
|
||||
try {
|
||||
saml2.loginPage("/saml2")
|
||||
.relyingPartyRegistrationRepository(
|
||||
saml2RelyingPartyRegistrations)
|
||||
.authenticationManager(
|
||||
new ProviderManager(authenticationProvider))
|
||||
.successHandler(
|
||||
new CustomSaml2AuthenticationSuccessHandler(
|
||||
loginAttemptService,
|
||||
applicationProperties,
|
||||
userService))
|
||||
.failureHandler(
|
||||
new CustomSaml2AuthenticationFailureHandler())
|
||||
.authenticationRequestResolver(
|
||||
saml2AuthenticationRequestResolver);
|
||||
} catch (Exception e) {
|
||||
log.error("Error configuring SAML 2 login", e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
log.debug("Login is not enabled.");
|
||||
http.authorizeHttpRequests(authz -> authz.anyRequest().permitAll());
|
||||
}
|
||||
return http.build();
|
||||
}
|
||||
|
||||
public DaoAuthenticationProvider daoAuthenticationProvider() {
|
||||
DaoAuthenticationProvider provider = new DaoAuthenticationProvider(userDetailsService);
|
||||
provider.setPasswordEncoder(passwordEncoder());
|
||||
return provider;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public IPRateLimitingFilter rateLimitingFilter() {
|
||||
// Example limit TODO add config level
|
||||
int maxRequestsPerIp = 1000000;
|
||||
return new IPRateLimitingFilter(maxRequestsPerIp, maxRequestsPerIp);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public PersistentTokenRepository persistentTokenRepository() {
|
||||
return new JPATokenRepositoryImpl(persistentLoginRepository);
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -43,7 +43,7 @@ import stirling.software.proprietary.security.model.api.admin.UpdateSettingsRequ
|
||||
@Tag(name = "Admin Settings", description = "Admin-only Settings Management APIs")
|
||||
@RequestMapping("/api/v1/admin/settings")
|
||||
@RequiredArgsConstructor
|
||||
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
@Slf4j
|
||||
public class AdminSettingsController {
|
||||
|
||||
|
||||
+2
-2
@@ -5,7 +5,6 @@ import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.mail.MailSendException;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@@ -18,6 +17,7 @@ import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.proprietary.security.model.api.Email;
|
||||
import stirling.software.proprietary.security.service.EmailService;
|
||||
|
||||
@@ -42,7 +42,7 @@ public class EmailController {
|
||||
* attachment.
|
||||
* @return ResponseEntity with success or error message.
|
||||
*/
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/send-email")
|
||||
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/send-email")
|
||||
@Operation(
|
||||
summary = "Send an email with an attachment",
|
||||
description =
|
||||
|
||||
+3
-9
@@ -105,9 +105,7 @@ public class UserController {
|
||||
if (user.getUsername().equals(newUsername)) {
|
||||
return new RedirectView("/account?messageType=usernameExists", true);
|
||||
}
|
||||
if (!userService.isPasswordCorrect(user, currentPassword)) {
|
||||
return new RedirectView("/account?messageType=incorrectPassword", true);
|
||||
}
|
||||
|
||||
if (!user.getUsername().equals(newUsername) && userService.usernameExists(newUsername)) {
|
||||
return new RedirectView("/account?messageType=usernameExists", true);
|
||||
}
|
||||
@@ -141,9 +139,7 @@ public class UserController {
|
||||
return new RedirectView("/change-creds?messageType=userNotFound", true);
|
||||
}
|
||||
User user = userOpt.get();
|
||||
if (!userService.isPasswordCorrect(user, currentPassword)) {
|
||||
return new RedirectView("/change-creds?messageType=incorrectPassword", true);
|
||||
}
|
||||
|
||||
userService.changePassword(user, newPassword);
|
||||
userService.changeFirstUse(user, false);
|
||||
// Logout using Spring's utility
|
||||
@@ -169,9 +165,7 @@ public class UserController {
|
||||
return new RedirectView("/account?messageType=userNotFound", true);
|
||||
}
|
||||
User user = userOpt.get();
|
||||
if (!userService.isPasswordCorrect(user, currentPassword)) {
|
||||
return new RedirectView("/account?messageType=incorrectPassword", true);
|
||||
}
|
||||
|
||||
userService.changePassword(user, newPassword);
|
||||
// Logout using Spring's utility
|
||||
new SecurityContextLogoutHandler().logout(request, response, null);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user