Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 41f80fc898 | |||
| 0f7551ab5b | |||
| 7c58749806 | |||
| 050ed37819 | |||
| 83a4344a01 | |||
| 9b50f3d6b7 | |||
| e945bc9413 | |||
| 3b720ed56e | |||
| ce8587b24d | |||
| fe772b52d6 | |||
| f29b77998b | |||
| f843e4c25c | |||
| ea20446a79 | |||
| 41f9b6d45d | |||
| 7b5916d3b9 | |||
| 657c205a4d | |||
| fe7d45dd12 | |||
| c05ae5b0b9 | |||
| dab012c3d1 | |||
| 32492c5a91 | |||
| 2add85eccf | |||
| 5edfb44776 | |||
| eedb0fe49c | |||
| 3c7dc2013f | |||
| 617e778a48 | |||
| e3c3c4c951 | |||
| 0e3b50d1b6 | |||
| bd8b885f7f | |||
| 3cdc0ea715 | |||
| a2ff9eae3f | |||
| 3f7631cd95 | |||
| 53b8764ed7 | |||
| 082d8ab205 | |||
| 749100c92a |
@@ -50,6 +50,16 @@ VITE_API_URL=/api
|
||||
# DB_PORT=5432
|
||||
# REDIS_PORT=6379
|
||||
|
||||
# Release Channel
|
||||
# Options: 'stable' (default), 'beta', or specific version like 'v2.3.0'
|
||||
# 'stable' uses the :stable tag (same as :latest on main)
|
||||
# 'beta' uses the :beta tag for pre-release versions
|
||||
PICPEAK_CHANNEL=stable
|
||||
|
||||
# Update Check Configuration
|
||||
# Set to 'false' to disable update notifications in admin UI
|
||||
UPDATE_CHECK_ENABLED=true
|
||||
|
||||
# Timezone
|
||||
TZ=UTC
|
||||
|
||||
|
||||
@@ -8,10 +8,10 @@ name: Build and Push Docker Images
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, develop ]
|
||||
tags: [ 'v*.*.*' ] # Triggered by Release Please tags
|
||||
branches: [ main, beta ]
|
||||
tags: [ 'v*.*.*', 'v*.*.*-beta.*' ] # Triggered by Release Please tags (stable and beta)
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
branches: [ main, beta ]
|
||||
release:
|
||||
types: [ published ] # Triggered when Release Please creates a release
|
||||
workflow_dispatch:
|
||||
@@ -42,6 +42,18 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Determine build context
|
||||
id: context
|
||||
run: |
|
||||
# Determine if this is a beta or stable release
|
||||
if [[ "${{ github.ref }}" == refs/tags/v*-beta* ]] || [[ "${{ github.ref }}" == refs/heads/beta ]]; then
|
||||
echo "channel=beta" >> $GITHUB_OUTPUT
|
||||
echo "is_prerelease=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "channel=stable" >> $GITHUB_OUTPUT
|
||||
echo "is_prerelease=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Determine build platforms
|
||||
id: platforms
|
||||
run: |
|
||||
@@ -88,10 +100,12 @@ jobs:
|
||||
type=ref,event=branch
|
||||
type=ref,event=pr
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
type=semver,pattern={{major}}.{{minor}},enable=${{ steps.context.outputs.is_prerelease == 'false' }}
|
||||
type=semver,pattern={{major}},enable=${{ steps.context.outputs.is_prerelease == 'false' }}
|
||||
type=sha,format=short
|
||||
type=raw,value=latest,enable={{is_default_branch}}
|
||||
type=raw,value=stable,enable=${{ github.ref == 'refs/heads/main' || (startsWith(github.ref, 'refs/tags/v') && steps.context.outputs.is_prerelease == 'false') }}
|
||||
type=raw,value=beta,enable=${{ github.ref == 'refs/heads/beta' || steps.context.outputs.is_prerelease == 'true' }}
|
||||
|
||||
- name: Build and push Backend Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
@@ -123,7 +137,7 @@ jobs:
|
||||
|
||||
- name: Upload Trivy scan results to GitHub Security tab
|
||||
if: github.event_name != 'pull_request' && steps.login-ghcr.outcome == 'success'
|
||||
uses: github/codeql-action/upload-sarif@v3
|
||||
uses: github/codeql-action/upload-sarif@v4
|
||||
with:
|
||||
sarif_file: 'trivy-backend.sarif'
|
||||
category: 'backend-vulnerabilities'
|
||||
@@ -139,6 +153,18 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Determine build context
|
||||
id: context
|
||||
run: |
|
||||
# Determine if this is a beta or stable release
|
||||
if [[ "${{ github.ref }}" == refs/tags/v*-beta* ]] || [[ "${{ github.ref }}" == refs/heads/beta ]]; then
|
||||
echo "channel=beta" >> $GITHUB_OUTPUT
|
||||
echo "is_prerelease=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "channel=stable" >> $GITHUB_OUTPUT
|
||||
echo "is_prerelease=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Determine build platforms
|
||||
id: platforms
|
||||
run: |
|
||||
@@ -185,10 +211,12 @@ jobs:
|
||||
type=ref,event=branch
|
||||
type=ref,event=pr
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
type=semver,pattern={{major}}.{{minor}},enable=${{ steps.context.outputs.is_prerelease == 'false' }}
|
||||
type=semver,pattern={{major}},enable=${{ steps.context.outputs.is_prerelease == 'false' }}
|
||||
type=sha,format=short
|
||||
type=raw,value=latest,enable={{is_default_branch}}
|
||||
type=raw,value=stable,enable=${{ github.ref == 'refs/heads/main' || (startsWith(github.ref, 'refs/tags/v') && steps.context.outputs.is_prerelease == 'false') }}
|
||||
type=raw,value=beta,enable=${{ github.ref == 'refs/heads/beta' || steps.context.outputs.is_prerelease == 'true' }}
|
||||
|
||||
- name: Build and push Frontend Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
@@ -220,7 +248,7 @@ jobs:
|
||||
|
||||
- name: Upload Trivy scan results to GitHub Security tab
|
||||
if: github.event_name != 'pull_request' && steps.login-ghcr.outcome == 'success'
|
||||
uses: github/codeql-action/upload-sarif@v3
|
||||
uses: github/codeql-action/upload-sarif@v4
|
||||
with:
|
||||
sarif_file: 'trivy-frontend.sarif'
|
||||
category: 'frontend-vulnerabilities'
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
name: Release Please (Beta)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [beta]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
release-please:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
release_created: ${{ steps.release.outputs.release_created }}
|
||||
tag_name: ${{ steps.release.outputs.tag_name }}
|
||||
version: ${{ steps.release.outputs.version }}
|
||||
steps:
|
||||
- name: Run Release Please
|
||||
uses: googleapis/release-please-action@v4
|
||||
id: release
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
config-file: release-please-config-beta.json
|
||||
manifest-file: .release-please-manifest-beta.json
|
||||
target-branch: beta
|
||||
|
||||
- name: Output Release Info
|
||||
if: ${{ steps.release.outputs.release_created }}
|
||||
run: |
|
||||
echo "## Beta Release Created!" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Tag:** ${{ steps.release.outputs.tag_name }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Version:** ${{ steps.release.outputs.version }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Docker images will be built and tagged with this beta version." >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
@@ -34,38 +34,3 @@ jobs:
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Docker images will be built and tagged with this version." >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# Sync version to package.json files after release
|
||||
sync-versions:
|
||||
needs: release-please
|
||||
if: ${{ needs.release-please.outputs.release_created }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: main
|
||||
|
||||
- name: Update package.json versions
|
||||
run: |
|
||||
VERSION="${{ needs.release-please.outputs.version }}"
|
||||
echo "Updating package.json files to version $VERSION"
|
||||
|
||||
# Update backend package.json
|
||||
cd backend
|
||||
npm version $VERSION --no-git-tag-version --allow-same-version
|
||||
cd ..
|
||||
|
||||
# Update frontend package.json
|
||||
cd frontend
|
||||
npm version $VERSION --no-git-tag-version --allow-same-version
|
||||
cd ..
|
||||
|
||||
- name: Commit version updates
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git add backend/package.json frontend/package.json
|
||||
git diff --staged --quiet || git commit -m "chore: sync package.json versions to ${{ needs.release-please.outputs.version }}"
|
||||
git push
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
".": "3.0.0-beta.0"
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
".": "2.2.3"
|
||||
".": "2.3.4"
|
||||
}
|
||||
|
||||
+231
@@ -5,6 +5,237 @@ All notable changes to PicPeak will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [2.3.4](https://github.com/the-luap/picpeak/compare/v2.3.3...v2.3.4) (2026-01-15)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add lightbox loading spinner and watermark cache invalidation ([050ed37](https://github.com/the-luap/picpeak/commit/050ed378199eb3b15c7c7f243792f68f858803f5))
|
||||
* database migration restart bug, lightbox loading spinner, and watermark cache invalidation ([7c58749](https://github.com/the-luap/picpeak/commit/7c5874980640ae8c3d1050ce24daeb0a2aeab7a3))
|
||||
* prevent database migration restart failures ([83a4344](https://github.com/the-luap/picpeak/commit/83a4344a01de4f65c5024fdf2d177a04457ccd2f)), closes [#107](https://github.com/the-luap/picpeak/issues/107)
|
||||
|
||||
## [2.3.3](https://github.com/the-luap/picpeak/compare/v2.3.2...v2.3.3) (2026-01-15)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* lightbox watermark loading, white label translations, and dynamic footer year ([3b720ed](https://github.com/the-luap/picpeak/commit/3b720ed56ecd2ded6aec57309f8c408c63a617ef))
|
||||
* lightbox watermark loading, white label translations, and dynamic footer year ([ce8587b](https://github.com/the-luap/picpeak/commit/ce8587b24df3f53a11a74348eff8b5c5b96c5488))
|
||||
* lightbox watermark loading, white label translations, and dynamic footer year ([#108](https://github.com/the-luap/picpeak/issues/108)) ([3b720ed](https://github.com/the-luap/picpeak/commit/3b720ed56ecd2ded6aec57309f8c408c63a617ef))
|
||||
|
||||
## [2.3.2](https://github.com/the-luap/picpeak/compare/v2.3.1...v2.3.2) (2026-01-15)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* watermark thumbnails, custom logo display, and German translations ([f843e4c](https://github.com/the-luap/picpeak/commit/f843e4c25cef02eef354fd3ee25824e20e4f8fc8))
|
||||
* watermark thumbnails, custom logo display, and German translations ([ea20446](https://github.com/the-luap/picpeak/commit/ea20446a797a00cf45dbe7bf6f06574a79c4d8a6))
|
||||
|
||||
## [2.3.1](https://github.com/the-luap/picpeak/compare/v2.3.0...v2.3.1) (2026-01-15)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* CI workflow fixes for protected branches ([657c205](https://github.com/the-luap/picpeak/commit/657c205a4d8ca49070b69973f4c7a3d1418633af))
|
||||
* use Release Please extra-files instead of sync-versions job ([fe7d45d](https://github.com/the-luap/picpeak/commit/fe7d45dd122b2dca1b2a21ba5c86d32b9a193074))
|
||||
|
||||
## [3.0.0-beta.0](https://github.com/the-luap/picpeak/compare/v2.3.0-beta.0...v3.0.0-beta.0) (2026-01-15)
|
||||
|
||||
|
||||
### ⚠ BREAKING CHANGES
|
||||
|
||||
* Deployment now requires external reverse proxy for SSL/HTTPS
|
||||
|
||||
### Features
|
||||
|
||||
* add Apple Liquid Glass templates, image security settings, and automated releases ([6033461](https://github.com/the-luap/picpeak/commit/6033461be118ce78277ec568e1ef1ceeff7311c8))
|
||||
* add complete translation support for backup admin page ([e9f92e6](https://github.com/the-luap/picpeak/commit/e9f92e66d08ac7001c31a3ee8f43ee8306bc79a9))
|
||||
* Add CSS template system with custom gallery styling support ([0da45e6](https://github.com/the-luap/picpeak/commit/0da45e699ad998031aa56a92f2da5ee61a04e285))
|
||||
* add event management, gallery customization, and release automationFeature/event rename ([40ee671](https://github.com/the-luap/picpeak/commit/40ee67171d41522037bf9d4e7675b62ec564346d))
|
||||
* add feedback management enhancements ([0064122](https://github.com/the-luap/picpeak/commit/0064122eff12029300ab7f95078b5710c3c2d08c))
|
||||
* add GitHub Actions workflow for Docker image builds ([4029559](https://github.com/the-luap/picpeak/commit/40295599547b86af7fea3359c7486918d2cd0236))
|
||||
* add multi-administrator support with RBAC and fix backup/restore for S3 ([892e47d](https://github.com/the-luap/picpeak/commit/892e47d017064d7922536f8e138bbb290a45cdc9))
|
||||
* **admin:** external media import modal + thumbnail fixes for reference events\n\n- Photos tab: replace inline external folder picker with a modal opened via "Import from External Folder" button next to "Upload Photos"; add info that all pictures in the selected folder will be imported.\n- Admin thumbnails: align list endpoint to /api/admin/photos/:eventId/photos and always return thumbnail_url to trigger on-demand generation; normalize external paths to avoid duplicated folder segments (e.g., individual/individual) that broke resolver; improve thumbnail logging.\n- Use authenticated image fetching on admin feedback pages to prevent 401s in automation.\n- i18n: add backup.external.warning strings; complete German backup/restore coverage; add common keys (notSet, of, up, select, selected).\n- Docs: add Local (npm) setup for EXTERNAL_MEDIA_ROOT in deployment guide.\n\nRefs [#17](https://github.com/the-luap/picpeak/issues/17) – gallery feature request: https://github.com/the-luap/picpeak/issues/17 ([49c7778](https://github.com/the-luap/picpeak/commit/49c77785e7a776890f15c0c541dcd18b74a86c6e))
|
||||
* **admin:** refine header layout and logo placement ([d64e7d0](https://github.com/the-luap/picpeak/commit/d64e7d08deae7ad1b6f744f447fe546115427942))
|
||||
* allow admin email updates in UI ([#36](https://github.com/the-luap/picpeak/issues/36)) ([3c2a79a](https://github.com/the-luap/picpeak/commit/3c2a79a31a0f1a44c8ec4f9a87f6fbcea9be651c))
|
||||
* beta/stable release channels with update notifications and bug fixes ([3c7dc20](https://github.com/the-luap/picpeak/commit/3c7dc2013fc3b57712ddf16db85f495b3cc7bfd7))
|
||||
* beta/stable release channels with update notifications and bug fixes ([#98](https://github.com/the-luap/picpeak/issues/98)) ([3c7dc20](https://github.com/the-luap/picpeak/commit/3c7dc2013fc3b57712ddf16db85f495b3cc7bfd7))
|
||||
* completely rewrite GitHub mirror to create new history from target commit ([febacb7](https://github.com/the-luap/picpeak/commit/febacb79ad86d35a222ec86a1e7da65747bbe19a))
|
||||
* consolidate setup scripts and guides into unified solution ([29a8ff9](https://github.com/the-luap/picpeak/commit/29a8ff914cf838918ab827280e4415afbce5ca8d))
|
||||
* **docker:** add PUID/PGID and user mapping to avoid bind mount permission issues; feat(setup): prompt for admin email interactively; docs: PUID/PGID in .env.example ([410a33f](https://github.com/the-luap/picpeak/commit/410a33fecf1693cc75816c53ac460ec20089e2a1))
|
||||
* enhance mirror-to-github workflow with commit-based history filtering ([b4b09c1](https://github.com/the-luap/picpeak/commit/b4b09c16504ca64ce265c7bd0bf0c901dbbd0638))
|
||||
* **events:** add CSS template selector to event edit page ([6a6c2cd](https://github.com/the-luap/picpeak/commit/6a6c2cd34db26a53b5fb96415650e8136a74e47f))
|
||||
* exclude Claude contributor from GitHub mirror workflow ([abbcdb1](https://github.com/the-luap/picpeak/commit/abbcdb11136afd8cf4eb21c2103e81d22b9c886f))
|
||||
* fix analytics dashboard and implement complete Umami integration ([45ce988](https://github.com/the-luap/picpeak/commit/45ce98806d4c87ddce8c400d07cc667bde435d75))
|
||||
* **gallery/filters:** add Rated and Commented filters (UI + backend).\n\n- UI: add star (Rated) and message (Commented) buttons to feedback filter bars (desktop + mobile)\n- Backend: support filter=rated, commented, and combinations via aggregate counts/queries ([b03760a](https://github.com/the-luap/picpeak/commit/b03760ab01e21feb3578f90d065945d437d03452))
|
||||
* **gallery:** add quick Like/Favorite actions on thumbnails across layouts ([6368f10](https://github.com/the-luap/picpeak/commit/6368f1027f96107ba64964eb126911bfe185f54a))
|
||||
* **gallery:** always-visible feedback indicators on grid tiles; fallback image rendering in lightbox/hero; auto-auth from shared-link token; fix external photo resolver\n\n- GridGallery: bottom-left icons for like/rated/comment on every tile\n- Hero layout grid: added same indicators (non-intrusive icons)\n- Lightbox/Hero: add fallbackSrc to display thumbnail if original fails\n- GalleryAuth: auto-store token from /gallery/:slug/:token and hydrate event\n- Backend gallery photo route: use resolvePhotoFilePath for external-media\n\nfix(admin): move photo feedback badges to bottom-right on admin grid tiles\n\nfix(dashboard): add missing i18n keys for activity types + fallback to formatter\n\nfix(admin/feedback): correct thumbnail URL base + robust date parsing\n\nRefs: [#19](https://github.com/the-luap/picpeak/issues/19) ([6948aaa](https://github.com/the-luap/picpeak/commit/6948aaa92afc29609f85cf7fd631095f3e32ad3f))
|
||||
* **gallery:** compact vertical icon-only feedback filter in PhotoFilterBar; remove wide buttons to prevent overflow\n\n- Desktop: vertical icon stack (All/Grid, Likes, Favorites) outside scroll area\n- Mobile: vertical icon stack below categories\n- Keeps existing category bar layout and count\n\nRefs: [#19](https://github.com/the-luap/picpeak/issues/19) ([465f997](https://github.com/the-luap/picpeak/commit/465f997752fc930ac0a3ae530e9e57a378877d53))
|
||||
* **i18n:** add translations for settings tabs ([c030e87](https://github.com/the-luap/picpeak/commit/c030e872135b39701ef1f4bbb2f28bcaf4ce7fae))
|
||||
* implement 4 new features with bug fixes and refactoring plan ([77a4bfd](https://github.com/the-luap/picpeak/commit/77a4bfd49975551bf509354097f280cab3e48c7a))
|
||||
* implement beta/stable release channels with update notifications ([617e778](https://github.com/the-luap/picpeak/commit/617e778a48e0f0c24fcb8441d00ed2a816f19c03))
|
||||
* implement comprehensive backup and restore system with S3 support ([f6a79c8](https://github.com/the-luap/picpeak/commit/f6a79c815e3085a56cbe7bac2964dd135f5e88bb))
|
||||
* implement feedback filter for liked/favorited photos (Issue [#17](https://github.com/the-luap/picpeak/issues/17)) ([41857ec](https://github.com/the-luap/picpeak/commit/41857ec499e2aab4347173cb031db246b9a032f6))
|
||||
* implement gallery feedback system with version tracking for backups ([dc1419c](https://github.com/the-luap/picpeak/commit/dc1419c051dae44532bfc2b2c2bc00942577dc22))
|
||||
* implement gallery logo customization (Issue [#17](https://github.com/the-luap/picpeak/issues/17)) ([909e760](https://github.com/the-luap/picpeak/commit/909e760447c76bb35dbffa553a4665edc5ebccd9))
|
||||
* **lightbox:** keep feedback usable while navigating ([6368f10](https://github.com/the-luap/picpeak/commit/6368f1027f96107ba64964eb126911bfe185f54a)), closes [#19](https://github.com/the-luap/picpeak/issues/19)
|
||||
* Multi-administrator RBAC, CSS templates & security hardening ([#78](https://github.com/the-luap/picpeak/issues/78)) ([16b3ab0](https://github.com/the-luap/picpeak/commit/16b3ab039ae95f5641dc15a4811eb2b503f1791c))
|
||||
* **native:** auto-serve SPA when dist exists (unless SERVE_FRONTEND=false); add clear logging; serve index.html for /admin ([fb16b7b](https://github.com/the-luap/picpeak/commit/fb16b7bbb8225192160c08050f1b164c36c8dc74))
|
||||
* **native:** build frontend and serve SPA from backend (SERVE_FRONTEND); fix Cannot GET /admin on native installs ([9fe10bc](https://github.com/the-luap/picpeak/commit/9fe10bcce2871a48f2409b4936d95c00249deb51))
|
||||
* **native:** serve built frontend from backend; build frontend during install/update; ensure env flags (SERVE_FRONTEND, FRONTEND_DIR) ([61ad2d6](https://github.com/the-luap/picpeak/commit/61ad2d61c137196c229817989f991e50fa389a6e))
|
||||
* overhaul public landing page and backup tooling ([2a4d388](https://github.com/the-luap/picpeak/commit/2a4d38813f7ab64a6bbb3a666f3c98a29443488d))
|
||||
* **select:** add per-tile checkbox selection in Admin grid and all gallery layouts; tile click opens viewer; checkbox toggles selection; auto-enable selection mode; add testids ([9fda54b](https://github.com/the-luap/picpeak/commit/9fda54bd06d37cd8f8f71056bf4f59e158cd8112))
|
||||
* **setup/docker:** auto-set PUID/PGID from invoking user and chown bind-mount folders; create missing data/events dirs ([0618b78](https://github.com/the-luap/picpeak/commit/0618b78725e85f97f0a4b4e834c17811c033c8f4))
|
||||
* **setup:** remove --admin-password; print admin credentials from ADMIN_CREDENTIALS.txt; fix ADMIN_URL to avoid /admin/admin; update native service commands ([84d0f63](https://github.com/the-luap/picpeak/commit/84d0f63d36c68532fea83e7087b1afeaa9b82f39))
|
||||
* support per-gallery password toggle ([5d6c061](https://github.com/the-luap/picpeak/commit/5d6c061f1c4fd20581b1e74fa114c96530b5de53))
|
||||
* update GitHub mirror workflow to start history from specific commit ([08da01f](https://github.com/the-luap/picpeak/commit/08da01f021788a1b81a3a3aabf120636c4e1a90a))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add missing route for feedback management page ([517128f](https://github.com/the-luap/picpeak/commit/517128fd99863ea203e39268ffa6c1ff093bcbd0))
|
||||
* add missing translations and fix BackupHistory useTranslation error ([99e4778](https://github.com/the-luap/picpeak/commit/99e47785e4a53c7ef9f95421413a2704b15b456d))
|
||||
* Add settings translations and fix manual backup process ([#82](https://github.com/the-luap/picpeak/issues/82)) ([476fcce](https://github.com/the-luap/picpeak/commit/476fcce13f30f9f2d2f98a0c87c25fba09e9eebc))
|
||||
* **admin/feedback:** use correct event id when rendering photo thumbnails ([4c7b49a](https://github.com/the-luap/picpeak/commit/4c7b49a5f69a3fce4f9a0e837a082b56bb7e47d6)), closes [#19](https://github.com/the-luap/picpeak/issues/19)
|
||||
* **admin:** prevent category badge overlap in grid ([d64e7d0](https://github.com/the-luap/picpeak/commit/d64e7d08deae7ad1b6f744f447fe546115427942))
|
||||
* align backend port to 3000 across all configurations ([3a8d53f](https://github.com/the-luap/picpeak/commit/3a8d53f4927f577c4031c4bc3531e08191dc632a))
|
||||
* Align nginx backend port for production Docker deployments (v2.2.2) ([#88](https://github.com/the-luap/picpeak/issues/88)) ([e0bd19a](https://github.com/the-luap/picpeak/commit/e0bd19a74dd81bdd45be2384820830bd96769e1c))
|
||||
* auto-convert old date formats to new date-fns syntax ([e1aca6b](https://github.com/the-luap/picpeak/commit/e1aca6b00c5affb914a0db44a6264c8e54fdffd6))
|
||||
* **backup:** add lastBackup alias and totalBackups for frontend compatibility ([749100c](https://github.com/the-luap/picpeak/commit/749100c92abd2bb123b137e3d3c6bb342b8f5f00))
|
||||
* **backup:** allow manual backups when automated backups are disabled ([e6dd89e](https://github.com/the-luap/picpeak/commit/e6dd89e969fb7018633159155975bd2bd2fb0409))
|
||||
* **ci:** add QEMU setup for multi-arch builds and skip for PRs ([0d36a27](https://github.com/the-luap/picpeak/commit/0d36a273bb58ffd0172efacd828e7171d954b41c))
|
||||
* clear notifications via API ([#35](https://github.com/the-luap/picpeak/issues/35)) ([013be18](https://github.com/the-luap/picpeak/commit/013be18d982986333e2ac24c7ede907de49690bc))
|
||||
* complete backup page translations and improve UI ([7387a5e](https://github.com/the-luap/picpeak/commit/7387a5e9f90965a6cfb75589b2338bf28263b840))
|
||||
* complete restore page translations and fix structure ([618e269](https://github.com/the-luap/picpeak/commit/618e2695fdf844cc0ae961b50a9b6eb99bc46a03))
|
||||
* configure github-release plugin to use GitHub API instead of Gitea ([2624ea6](https://github.com/the-luap/picpeak/commit/2624ea6130a38224597f0c4d3f3d0341c334472f))
|
||||
* correct GitHub repository path in Drone CI release config ([247e154](https://github.com/the-luap/picpeak/commit/247e154afefd3aef285e459bb7fc39ea460e53e2))
|
||||
* correct import statements for api in backup JSX files ([30f6780](https://github.com/the-luap/picpeak/commit/30f678048417aeffe6eabefc7bed5e4dc2267f25))
|
||||
* correct malformed gallery URLs in admin panel View Gallery links ([3074748](https://github.com/the-luap/picpeak/commit/3074748bbc6a8cb8fc0e95d2f24d626f0d0444d0))
|
||||
* correct password generator function name in reset password route ([65d796b](https://github.com/the-luap/picpeak/commit/65d796b9f09417f85bb3209c5e5fbe597a4bb2d3))
|
||||
* correct script name in Gitea mirror workflow ([828d6bc](https://github.com/the-luap/picpeak/commit/828d6bc456175007b72998db7116eec993750435))
|
||||
* **cors:** scope CORS to /api only and avoid throwing on disallowed origins; prevents static asset 500s on native ([90bb21e](https://github.com/the-luap/picpeak/commit/90bb21e38bf1ba97e3fb8185b8d05f1296d745ee))
|
||||
* critical database connection pool exhaustion issues ([8588133](https://github.com/the-luap/picpeak/commit/8588133a4e35774e46f7c605638758e5b2a4a9e2))
|
||||
* **db:** improve PostgreSQL connection check in wait-for-db.sh ([e85a68a](https://github.com/the-luap/picpeak/commit/e85a68a386c72c276b4958599b5246e60dfac716))
|
||||
* display new password after admin password reset ([bd8b885](https://github.com/the-luap/picpeak/commit/bd8b885f7f060160eb852870d143f25ce628f3db))
|
||||
* Docker Swarm DNS resolution and backup status display (v2.2.3) ([082d8ab](https://github.com/the-luap/picpeak/commit/082d8ab2054416b2a4f9e0438aa2bda0a8f4277e))
|
||||
* Docker Swarm DNS resolution and backup status display (v2.2.3) ([082d8ab](https://github.com/the-luap/picpeak/commit/082d8ab2054416b2a4f9e0438aa2bda0a8f4277e))
|
||||
* force github-release plugin to use GitHub API instead of Gitea ([558a966](https://github.com/the-luap/picpeak/commit/558a966f8509ac7b77c732f8cc5855c9f88a4bab))
|
||||
* **frontend:** add missing externalMedia service and mount admin external-media routes; verify Vite build ([ab324f1](https://github.com/the-luap/picpeak/commit/ab324f192859204a3ea3c129530ccfe8f5a36968))
|
||||
* gallery thumbnails not loading (404 errors) [#96](https://github.com/the-luap/picpeak/issues/96) ([e3c3c4c](https://github.com/the-luap/picpeak/commit/e3c3c4c951c52de99bd0afd95b08d119153997b4))
|
||||
* **gallery/filters:** always apply global liked/favorited filters by aggregate counts (ignore guest_id); resolves mismatch between client guest_id and server identifier ([526dcd8](https://github.com/the-luap/picpeak/commit/526dcd8dfc030d86143cee799a88a1004d96b116))
|
||||
* **gallery/filters:** make feedback filters work globally when no guest_id is provided; remove guest_id from client photos query\n\n- Backend /api/gallery/:slug/photos: if filter present and guest_id missing, filter by like_count/favorite_count\n- Frontend useGalleryPhotos: stop passing random guestId (does not match server guest_identifier)\n\nThis makes Liked/Favorited filters reflect photos with aggregate feedback counts as expected. ([5b2561b](https://github.com/the-luap/picpeak/commit/5b2561b6f1da2665d6092ba954f8ff26df3959a4))
|
||||
* **gallery/sidebar:** compact icon-only feedback filter in sidebar (vertical, small) to avoid overflow; use GalleryFilter variant=compact ([ff89f96](https://github.com/the-luap/picpeak/commit/ff89f96e31130f75bcd7a406c5d895eac17b65de))
|
||||
* **gallery:** feedback filter headline + horizontal icons in sidebar (compact variant); ensure sidebar content scrolls (flex-col container) ([3a6d061](https://github.com/the-luap/picpeak/commit/3a6d06192a280ead8bd5d1fbfe06554e63f3346e))
|
||||
* handle auth errors and JSON parsing in admin panel ([b2ae5f1](https://github.com/the-luap/picpeak/commit/b2ae5f18ad4622ea9cb0b5b593dad19e5d14cf60))
|
||||
* handle legacy non-JSON logo paths when replacing logo ([0d5ce48](https://github.com/the-luap/picpeak/commit/0d5ce48dccf0c61f210725ffae15dafc5e9f7cab))
|
||||
* harden gallery downloads and per-gallery auth ([fc1bf53](https://github.com/the-luap/picpeak/commit/fc1bf534129092ca3638e4a4bc47274cd297fa5f))
|
||||
* implement 9 production enhancements and security fixes ([c584369](https://github.com/the-luap/picpeak/commit/c584369d5d5c33fd794cf82a2aea8089bd10e514))
|
||||
* improve admin credentials display and configuration ([ad495a9](https://github.com/the-luap/picpeak/commit/ad495a92c46d02849ce0d9176cff43c83c5c4b57))
|
||||
* improve version bump workflow with better conflict resolution ([c787510](https://github.com/the-luap/picpeak/commit/c7875102c5196a9ef3038c2d5e0ee313fbb2782a))
|
||||
* JSON serialize favicon and logo URLs for PostgreSQL storage ([b83f427](https://github.com/the-luap/picpeak/commit/b83f4272b584f937fea1f47656182e514b12d980))
|
||||
* Multi-administrator RBAC, CSS templates & security hardening ([#80](https://github.com/the-luap/picpeak/issues/80)) ([37d4e1c](https://github.com/the-luap/picpeak/commit/37d4e1cb6132346699a90aebfbaec83d84f931f4))
|
||||
* multiple improvements and CI/CD updates ([bf70567](https://github.com/the-luap/picpeak/commit/bf705674d505b0cb1b82fecc74aa8d95edd50a47))
|
||||
* **native/http:** disable CSP upgrade-insecure-requests and HSTS unless ENABLE_HSTS=true; prevents HTTPS upgrades on HTTP installs ([24b4a31](https://github.com/the-luap/picpeak/commit/24b4a314a9e97b6c640ca29067e95028a23a8973))
|
||||
* **native:** correct setup paths to /opt/picpeak/app, update repo URL, add sqlite prod support; docs path fixes ([b992b15](https://github.com/the-luap/picpeak/commit/b992b151d3ca6ccb4a9b2434d94edcdc90ada3b0))
|
||||
* **native:** remove obsolete workers service; restart only backend; add API request logging and preflight handler; keep static assets outside CORS ([f3604b4](https://github.com/the-luap/picpeak/commit/f3604b438b37e5f2bddf98e79f458bfa2367cb75))
|
||||
* **nginx:** add Docker DNS resolver for Swarm/dynamic service discovery ([049837f](https://github.com/the-luap/picpeak/commit/049837f9d675ff5a4d93c02e5eb771bf65bc2616))
|
||||
* **nginx:** Add Docker DNS resolver for Swarm/dynamic service discovery (v2.2.3) ([cc1ddfd](https://github.com/the-luap/picpeak/commit/cc1ddfd42cccac07d5869fe2ee19c25a9ffa50e8))
|
||||
* **photos:** category changes now persist and display correctly ([#77](https://github.com/the-luap/picpeak/issues/77)) ([d9da98c](https://github.com/the-luap/picpeak/commit/d9da98c355011c247c526b28e6f07b329a632b55))
|
||||
* **photos:** resolve upload category selection and improve feedback buttons ([#77](https://github.com/the-luap/picpeak/issues/77)) ([856d533](https://github.com/the-luap/picpeak/commit/856d53343c6805706e1498892a29b120938f8547))
|
||||
* prefer admin token on admin routes ([#23](https://github.com/the-luap/picpeak/issues/23) [#28](https://github.com/the-luap/picpeak/issues/28)) ([d4404e3](https://github.com/the-luap/picpeak/commit/d4404e39bd7953649da02d3e300ffef46573ac97))
|
||||
* prevent unnecessary image recompression and fix SQLite migration [#95](https://github.com/the-luap/picpeak/issues/95) ([3cdc0ea](https://github.com/the-luap/picpeak/commit/3cdc0ea7152e63cd72124a91394741a6e6904af3))
|
||||
* remove description field from migration 035 app_settings inserts ([22cc406](https://github.com/the-luap/picpeak/commit/22cc40617f88e1f0a636fc049c78601fc1f38c33))
|
||||
* remove file requirement from GitHub release in Drone CI ([8335916](https://github.com/the-luap/picpeak/commit/833591681adf29d99a1dfa7c43d5aee7a6cb98ba))
|
||||
* remove formatBoolean calls from migration 032 - critical production fix ([0502ed3](https://github.com/the-luap/picpeak/commit/0502ed34c9fe76acacc2aecd02151564d109cf0b))
|
||||
* remove unnecessary publish-manifest job from Docker workflow ([986b101](https://github.com/the-luap/picpeak/commit/986b101040674f2253fcdfda99a9e603535daaa0))
|
||||
* remove unused formatBoolean import from migration 033 ([1238db5](https://github.com/the-luap/picpeak/commit/1238db58c25e97513c9bdcb5dcc26b1034e9f074))
|
||||
* remove updated_at field from password reset query ([ed0243e](https://github.com/the-luap/picpeak/commit/ed0243ec398acca26490ef27cbe3cfe5fa9b95a6))
|
||||
* remove updated_at from app_settings inserts in multiple migrations ([4c42b4c](https://github.com/the-luap/picpeak/commit/4c42b4c60157755b770bea3b78d42fe6abd60afa))
|
||||
* replace github-release plugin with direct curl API call ([76a466c](https://github.com/the-luap/picpeak/commit/76a466c0776eeabe3eac6a480bd699c2ae5c60bc))
|
||||
* resolve backend startup errors in development ([f8fb1c3](https://github.com/the-luap/picpeak/commit/f8fb1c3f4b2b5de53182e987a9dfe042704320b9))
|
||||
* resolve branding display issues and invitation parsing errors ([1931d73](https://github.com/the-luap/picpeak/commit/1931d73b60d3419203cc8b420841abbfc9e14d2d))
|
||||
* Resolve branding display issues and invitation parsing errors (v2.2.1) ([#86](https://github.com/the-luap/picpeak/issues/86)) ([d7ecf83](https://github.com/the-luap/picpeak/commit/d7ecf83d32ec6608280b96e6cdee48e9a0ad0afa))
|
||||
* resolve CI/CD version bump race condition ([0bf4764](https://github.com/the-luap/picpeak/commit/0bf4764a0720f6f199442a738a885a2edaae2a4d))
|
||||
* resolve database connection error for analytics settings ([95939d5](https://github.com/the-luap/picpeak/commit/95939d57e6857646d261b0f049bdda752602caeb))
|
||||
* resolve date formatting error in event creation ([c51d756](https://github.com/the-luap/picpeak/commit/c51d7565035146cc3f689c0cc4b508b78d9bb5ee))
|
||||
* resolve development environment issues ([61299a3](https://github.com/the-luap/picpeak/commit/61299a33c4f92730fe8b14f6035f61325d952b94))
|
||||
* resolve duplicate logger declaration and syntax error in rate limit service ([0fe6d73](https://github.com/the-luap/picpeak/commit/0fe6d738b222555b27cbf8a36f455b1c15c4f4e8))
|
||||
* resolve feedback validation issues from GitHub issue [#16](https://github.com/the-luap/picpeak/issues/16) ([f26beca](https://github.com/the-luap/picpeak/commit/f26becad1dfa72c62b6ecec491be025644426d67))
|
||||
* resolve feedback validation issues from GitHub issue [#16](https://github.com/the-luap/picpeak/issues/16) ([67ff415](https://github.com/the-luap/picpeak/commit/67ff4158404347bc7c13dee5b4e13260eb0e743d))
|
||||
* resolve GitHub issues [#4](https://github.com/the-luap/picpeak/issues/4), [#8](https://github.com/the-luap/picpeak/issues/8), [#9](https://github.com/the-luap/picpeak/issues/9), and [#10](https://github.com/the-luap/picpeak/issues/10) ([934d6dd](https://github.com/the-luap/picpeak/commit/934d6ddc5847f65db6371a4043b764f6d4cd6c8b))
|
||||
* resolve GitHub mirror workflow cherry-pick failure with merge commits ([d6adde4](https://github.com/the-luap/picpeak/commit/d6adde4e093537aeecf8b190513a1171c3ecc82c))
|
||||
* resolve language-specific column issues in core migrations ([62617f6](https://github.com/the-luap/picpeak/commit/62617f627f56aedd132fa20528b1d7c7e272c85c))
|
||||
* resolve migration conflicts and duplicate numbering ([a401fbd](https://github.com/the-luap/picpeak/commit/a401fbdc54f30c18b5aa2440d7b6887ca12e00eb))
|
||||
* resolve multiple feedback management issues ([ad75818](https://github.com/the-luap/picpeak/commit/ad758185666bf4ac52965f16d1c0e1e052887ac2))
|
||||
* resolve multiple issues from GitHub issue [#14](https://github.com/the-luap/picpeak/issues/14) ([e91209f](https://github.com/the-luap/picpeak/commit/e91209f7cb38a5b840e74ed6acd8d490ef9d2294))
|
||||
* resolve port configuration issues and database column mismatch ([6de64a1](https://github.com/the-luap/picpeak/commit/6de64a1df18932badd7bb1b9928d09e9477f0c3f))
|
||||
* resolve PostgreSQL migration issues for development environment ([ee855a3](https://github.com/the-luap/picpeak/commit/ee855a3502ecd1a5556e378e9995de86e3548de1))
|
||||
* resolve production UI and API issues ([d5790ad](https://github.com/the-luap/picpeak/commit/d5790ad635596842926a358753932e5c422590d6))
|
||||
* resolve SIGPIPE error in GitHub mirror workflow file cleanup ([b7c8953](https://github.com/the-luap/picpeak/commit/b7c8953cb4d4a2541dcb38865c8a7beef0edf494))
|
||||
* resolve translation interpolation issue for download button ([c1e10f1](https://github.com/the-luap/picpeak/commit/c1e10f14a30797c76169c2531de5d04976ee4888))
|
||||
* **security:** upgrade Alpine base image to fix libpng and c-ares CVEs ([b706eeb](https://github.com/the-luap/picpeak/commit/b706eeb5d332e9618706193976a7241aee53d879))
|
||||
* **setup/native:** correct repo URL, paths, and systemd for native install; support sqlite in production knex config ([87b8414](https://github.com/the-luap/picpeak/commit/87b8414e449802db6dc9f762453f7672616b83c9))
|
||||
* **setup/native:** Debian 12 compatibility (reliable RAM detection, sudo-less run_as_user, git safe.directory); ensure SQLite data dir; use user for migrate ([dc482e6](https://github.com/the-luap/picpeak/commit/dc482e614a5fbac44c6570d812669511301a4403))
|
||||
* **setup/native:** handle forced updates safely by fetch+checkout/reset instead of pull; stable on rewritten histories ([3697344](https://github.com/the-luap/picpeak/commit/3697344cd0add28b4da71c3b33e2ccc0a96f50f9))
|
||||
* **setup/update:** detect native installs first (/opt/picpeak/app/backend or systemd unit); avoid false docker updates on root ([adf576f](https://github.com/the-luap/picpeak/commit/adf576fbe17f40c13c1d77dd9751f2e9dbf523a1))
|
||||
* simplify Drone github-release step to avoid shell parsing issues ([94f10e1](https://github.com/the-luap/picpeak/commit/94f10e164502e6848cd720ee5a5c2822abbde46f))
|
||||
* stabilize uploads and guest feedback filters ([aaaf598](https://github.com/the-luap/picpeak/commit/aaaf59817b3978635d2282c006853e183ab944d4))
|
||||
* update all deployment guide links in README.md ([6389b9d](https://github.com/the-luap/picpeak/commit/6389b9df3f616c09a9bbbf1a2988764b0c3aeb77))
|
||||
* update deployment guide with critical URL configuration and nginx port fixes ([1cadce1](https://github.com/the-luap/picpeak/commit/1cadce196bb04a0575d83437618454d4ca5bcdac))
|
||||
* update form-data and multer to address security vulnerabilities ([7750170](https://github.com/the-luap/picpeak/commit/7750170832dddf81a33c7c2409b37b0b7bc1f290))
|
||||
* update Gitea mirror workflow to selectively remove scripts ([296430e](https://github.com/the-luap/picpeak/commit/296430e4d7e01a6be031dbb89dd25f563b163a97))
|
||||
* update GitHub mirror action to support fine-grained personal access tokens ([827eb48](https://github.com/the-luap/picpeak/commit/827eb4819b7da6171d48613963d176399cad80c6))
|
||||
* use admin API for Umami config in analytics page ([a54a2c0](https://github.com/the-luap/picpeak/commit/a54a2c0fdaa28193d1359da73bc7fb61476e2a58))
|
||||
* use plugins/gitea-release for Drone CI/CD ([0c783c6](https://github.com/the-luap/picpeak/commit/0c783c66d0dfe8cb637f0db7349ae9637d7bf787))
|
||||
* use plugins/github-release for Drone CI/CD ([f926cd3](https://github.com/the-luap/picpeak/commit/f926cd3adf513858bc7b291582c7ca2efdf93ff8))
|
||||
* watermark upload JSON parsing and image quality preservation ([0e3b50d](https://github.com/the-luap/picpeak/commit/0e3b50d1b6a2dc532ebdc0981f81f77722e8f23a))
|
||||
|
||||
|
||||
### Documentation
|
||||
|
||||
* add minimum system requirements section to README ([4615a5d](https://github.com/the-luap/picpeak/commit/4615a5d795b415367edf4882628377936b29ab32))
|
||||
* add PUID/PGID note for Docker bind mounts to avoid permission issues ([0178e71](https://github.com/the-luap/picpeak/commit/0178e71c67f198c6013ece52b0a2da0e2f1a6b2a))
|
||||
* add transparency note about AI-assisted development ([35e360d](https://github.com/the-luap/picpeak/commit/35e360dcf7ac68833bec81f2e79f4a11a76a0e87))
|
||||
* add warnings about $ character in Docker Compose passwords ([87d1761](https://github.com/the-luap/picpeak/commit/87d1761091bb97747821aa810a58f3978a59d08f))
|
||||
* clarify VITE_API_URL usage; remove FRONTEND_API_URL; add storage vars; simplify compose mounts and external DB example (refs [#18](https://github.com/the-luap/picpeak/issues/18)) ([758c085](https://github.com/the-luap/picpeak/commit/758c085467e579e9f6b16df2298747fdddf2b205))
|
||||
* **compose:** fix backend healthcheck path; remove frontend VITE_API_URL env and document /api proxy (refs [#18](https://github.com/the-luap/picpeak/issues/18)) ([ecbc488](https://github.com/the-luap/picpeak/commit/ecbc48815ded99a052ef057e69427c823cd34ece))
|
||||
* fix deployment/admin routing and CORS guidance; add AGENTS.md; ignore AGENTS.md (refs [#18](https://github.com/the-luap/picpeak/issues/18)) ([dad1787](https://github.com/the-luap/picpeak/commit/dad1787aad8763637373e8eb87a47728d3d568cc))
|
||||
* follow-up on PR [#15](https://github.com/the-luap/picpeak/issues/15) — clarify VITE_API_URL usage, compose mounts, and admin routing (refs [#15](https://github.com/the-luap/picpeak/issues/15)) ([e9171c7](https://github.com/the-luap/picpeak/commit/e9171c71159cb41b91a099621bd2d7a7985dd239))
|
||||
* **readme:** reflect new External Media reference mode and update roadmap (gallery feedback status) ([ee13556](https://github.com/the-luap/picpeak/commit/ee13556c5cb4f24fe88e14fd00b821acf65b11cb))
|
||||
* replace email addresses with GitHub issue links ([0c989ce](https://github.com/the-luap/picpeak/commit/0c989ce08699ce68b131a9cc4ba4f14e06e3d221))
|
||||
* update deployment guide with GitHub Container Registry images ([2c9a56f](https://github.com/the-luap/picpeak/commit/2c9a56f217218f0700817d150b3de115e9503baa))
|
||||
|
||||
|
||||
### Code Refactoring
|
||||
|
||||
* simplify deployment structure with direct port exposure ([6492cb9](https://github.com/the-luap/picpeak/commit/6492cb9ec8f8b811297aa71c153b9fe6a00e947a))
|
||||
|
||||
## [2.3.0](https://github.com/the-luap/picpeak/compare/v2.2.4...v2.3.0) (2026-01-15)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* beta/stable release channels with update notifications and bug fixes ([3c7dc20](https://github.com/the-luap/picpeak/commit/3c7dc2013fc3b57712ddf16db85f495b3cc7bfd7))
|
||||
* beta/stable release channels with update notifications and bug fixes ([#98](https://github.com/the-luap/picpeak/issues/98)) ([3c7dc20](https://github.com/the-luap/picpeak/commit/3c7dc2013fc3b57712ddf16db85f495b3cc7bfd7))
|
||||
* implement beta/stable release channels with update notifications ([617e778](https://github.com/the-luap/picpeak/commit/617e778a48e0f0c24fcb8441d00ed2a816f19c03))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* display new password after admin password reset ([bd8b885](https://github.com/the-luap/picpeak/commit/bd8b885f7f060160eb852870d143f25ce628f3db))
|
||||
* gallery thumbnails not loading (404 errors) [#96](https://github.com/the-luap/picpeak/issues/96) ([e3c3c4c](https://github.com/the-luap/picpeak/commit/e3c3c4c951c52de99bd0afd95b08d119153997b4))
|
||||
* prevent unnecessary image recompression and fix SQLite migration [#95](https://github.com/the-luap/picpeak/issues/95) ([3cdc0ea](https://github.com/the-luap/picpeak/commit/3cdc0ea7152e63cd72124a91394741a6e6904af3))
|
||||
* watermark upload JSON parsing and image quality preservation ([0e3b50d](https://github.com/the-luap/picpeak/commit/0e3b50d1b6a2dc532ebdc0981f81f77722e8f23a))
|
||||
|
||||
## [2.2.4](https://github.com/the-luap/picpeak/compare/v2.2.3...v2.2.4) (2026-01-08)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **backup:** add lastBackup alias and totalBackups for frontend compatibility ([749100c](https://github.com/the-luap/picpeak/commit/749100c92abd2bb123b137e3d3c6bb342b8f5f00))
|
||||
* Docker Swarm DNS resolution and backup status display (v2.2.3) ([082d8ab](https://github.com/the-luap/picpeak/commit/082d8ab2054416b2a4f9e0438aa2bda0a8f4277e))
|
||||
* Docker Swarm DNS resolution and backup status display (v2.2.3) ([082d8ab](https://github.com/the-luap/picpeak/commit/082d8ab2054416b2a4f9e0438aa2bda0a8f4277e))
|
||||
|
||||
## [2.2.3](https://github.com/the-luap/picpeak/compare/v2.2.2...v2.2.3) (2026-01-08)
|
||||
|
||||
|
||||
|
||||
+116
-148
@@ -2,9 +2,24 @@
|
||||
|
||||
This guide covers multiple deployment options for PicPeak, from simple local setups to production-ready configurations.
|
||||
|
||||
## 🎯 Quick Start - Simple Setup (Recommended for Beginners)
|
||||
## 📋 Table of Contents
|
||||
|
||||
For the easiest installation without Docker or complex configurations, use our **unified setup script**:
|
||||
- [Quick Start](#-quick-start)
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [Configuration](#-configuration)
|
||||
- [Deployment](#-deployment)
|
||||
- [First Login](#-first-login)
|
||||
- [Release Channels](#-release-channels)
|
||||
- [Reverse Proxy Setup](#-reverse-proxy-setup)
|
||||
- [External Media Library](#external-media-library)
|
||||
- [Maintenance](#-maintenance)
|
||||
- [Troubleshooting](#-troubleshooting)
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
### Option 1: Automated Setup Script (Easiest)
|
||||
|
||||
For the simplest installation, use our unified setup script:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/the-luap/picpeak/main/scripts/picpeak-setup.sh -o picpeak-setup.sh && \
|
||||
@@ -12,27 +27,11 @@ chmod +x picpeak-setup.sh && \
|
||||
sudo ./picpeak-setup.sh
|
||||
```
|
||||
|
||||
This automated script handles everything including:
|
||||
- Choice between Docker or Native installation
|
||||
- OS detection and dependency installation
|
||||
- Database setup and service configuration
|
||||
- SSL/HTTPS setup (optional)
|
||||
|
||||
Perfect for:
|
||||
- Small to medium deployments
|
||||
- Local or VPS installations
|
||||
- Users new to server management
|
||||
- Quick testing and evaluation
|
||||
This script handles Docker/Native installation choice, OS detection, dependencies, database setup, and optional SSL.
|
||||
|
||||
👉 **See [SIMPLE_SETUP.md](./SIMPLE_SETUP.md) for detailed instructions.**
|
||||
|
||||
---
|
||||
|
||||
## 🐳 Docker Compose Deployment
|
||||
|
||||
### Option 1: Using Pre-built Images (Recommended)
|
||||
|
||||
PicPeak provides official Docker images via GitHub Container Registry for quick deployment without building:
|
||||
### Option 2: Docker with Pre-built Images (Recommended)
|
||||
|
||||
```bash
|
||||
# Clone repository for configuration files
|
||||
@@ -43,35 +42,40 @@ cd picpeak
|
||||
cp .env.example .env
|
||||
nano .env # Edit with your values
|
||||
|
||||
# Use pre-built images deployment
|
||||
# Create required directories
|
||||
mkdir -p events/active events/archived data logs backup storage
|
||||
chmod -R 755 events data logs backup storage
|
||||
|
||||
# Deploy using pre-built images
|
||||
docker compose -f docker-compose.production.yml up -d
|
||||
|
||||
# Check logs
|
||||
docker compose -f docker-compose.production.yml logs -f
|
||||
```
|
||||
|
||||
The production compose file uses:
|
||||
- **Backend**: `ghcr.io/the-luap/picpeak/backend:latest`
|
||||
- **Frontend**: `ghcr.io/the-luap/picpeak/frontend:latest`
|
||||
**Available image tags:**
|
||||
| Channel | Tags | Description |
|
||||
|---------|------|-------------|
|
||||
| Stable | `stable`, `latest`, `v2.3.0` | Production-ready releases |
|
||||
| Beta | `beta`, `v2.3.0-beta.1` | Early access to new features |
|
||||
| Branch | `main`, `beta` | Latest from each branch |
|
||||
|
||||
Available tags:
|
||||
- `latest` - Latest stable release
|
||||
- `main` - Latest main branch build
|
||||
- `develop` - Development branch (may be unstable)
|
||||
- `v1.0.0` - Specific version tags
|
||||
To select a channel, set `PICPEAK_CHANNEL` in your `.env` file (see [Release Channels](#release-channels) section)
|
||||
|
||||
### Option 2: Building from Source
|
||||
### Option 3: Build from Source
|
||||
|
||||
If you need to customize the application or the pre-built images aren't available, you can build locally:
|
||||
```bash
|
||||
git clone https://github.com/the-luap/picpeak.git
|
||||
cd picpeak
|
||||
cp .env.example .env
|
||||
nano .env # Edit with your values
|
||||
|
||||
## 📋 Table of Contents
|
||||
mkdir -p events/active events/archived data logs backup storage
|
||||
chmod -R 755 events data logs backup storage
|
||||
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [Quick Start](#quick-start)
|
||||
- [Configuration](#configuration)
|
||||
- [Deployment](#deployment)
|
||||
- [First Login](#first-login)
|
||||
- [Reverse Proxy Setup](#reverse-proxy-setup)
|
||||
- [Maintenance](#maintenance)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
- [External Media Library](#external-media-library)
|
||||
docker compose build
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
## Prerequisites
|
||||
|
||||
@@ -80,106 +84,6 @@ If you need to customize the application or the pre-built images aren't availabl
|
||||
- SMTP server credentials for emails
|
||||
- At least 2GB RAM and 20GB storage
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
### Method 1: Using Pre-built Images (Fastest)
|
||||
|
||||
1. **Clone the repository for configs**
|
||||
```bash
|
||||
git clone https://github.com/the-luap/picpeak.git
|
||||
cd picpeak
|
||||
```
|
||||
|
||||
2. **Set up environment**
|
||||
```bash
|
||||
cp .env.example .env
|
||||
nano .env # Edit with your values
|
||||
```
|
||||
|
||||
3. **Create required directories**
|
||||
```bash
|
||||
mkdir -p events/active events/archived data logs backup storage
|
||||
chmod -R 755 events data logs backup storage
|
||||
```
|
||||
|
||||
4. **Deploy using pre-built images**
|
||||
```bash
|
||||
docker compose -f docker-compose.production.yml up -d
|
||||
```
|
||||
|
||||
5. **Check logs**
|
||||
```bash
|
||||
docker compose -f docker-compose.production.yml logs -f
|
||||
```
|
||||
|
||||
## External Media Library
|
||||
|
||||
PicPeak can reference an existing, read‑only media library mounted into the backend container. This avoids copying originals into PicPeak storage.
|
||||
|
||||
- Map your host library path to the container as read‑only in `docker-compose.production.yml`:
|
||||
- Add volume under `backend`: `- ${EXTERNAL_MEDIA}:/external-media:ro`
|
||||
- Add backend env: `EXTERNAL_MEDIA_ROOT=/external-media`
|
||||
- In `.env`, set:
|
||||
- `EXTERNAL_MEDIA=/mnt/photos` (example host path)
|
||||
- `EXTERNAL_MEDIA_ROOT=/external-media`
|
||||
|
||||
Usage:
|
||||
- In Admin → Events, set “Source Mode” to “Reference (external folder)”, select a folder under `/external-media`, then import to index and generate thumbnails. Originals stay in your library.
|
||||
|
||||
Backups and Archives:
|
||||
- Backups only include data under `STORAGE_PATH` and exclude external originals. The backup manifest includes `metadata.external_references = { excluded: true, events: N, photos: M }` and the Admin UI surfaces a warning.
|
||||
- Archiving reference events creates a manifest‑only ZIP and deletes thumbnails for that event. External originals are never moved or deleted.
|
||||
|
||||
Local (npm) setup (no Docker):
|
||||
|
||||
1. Create or choose a folder that contains your external originals, e.g. `/Users/you/Pictures/picpeak-external` (macOS/Linux) or `C:\\Pictures\\picpeak-external` (Windows).
|
||||
2. In `backend/.env` (or your shell), set:
|
||||
- `EXTERNAL_MEDIA_ROOT=/absolute/path/to/picpeak-external`
|
||||
- Ensure `STORAGE_PATH` points to your PicPeak storage (defaults to `./storage`).
|
||||
3. Start services from source:
|
||||
- Backend: `cd backend && npm install && npm run migrate && JWT_SECRET=... npm start`
|
||||
- Frontend: `cd frontend && npm install && npm run dev` (or build + serve)
|
||||
4. In Admin → Events:
|
||||
- Create an event, set “Source Mode” to “Reference (external folder)”.
|
||||
- Use the folder picker to browse under your `EXTERNAL_MEDIA_ROOT` and select the subfolder to reference.
|
||||
- Click “Import from selected folder” to index files and generate thumbnails on demand.
|
||||
|
||||
Notes:
|
||||
- PicPeak only reads from `EXTERNAL_MEDIA_ROOT`; it never modifies or deletes your originals there.
|
||||
- Thumbnails are generated under `STORAGE_PATH/thumbnails` and are included in backups; originals in `EXTERNAL_MEDIA_ROOT` are excluded.
|
||||
- On Windows, use absolute paths (e.g., `C:\\Photos\\Library`) for `EXTERNAL_MEDIA_ROOT`.
|
||||
|
||||
### Method 2: Building from Source
|
||||
|
||||
1. **Clone the repository**
|
||||
```bash
|
||||
git clone https://github.com/the-luap/picpeak.git
|
||||
cd picpeak
|
||||
```
|
||||
|
||||
2. **Set up environment**
|
||||
```bash
|
||||
cp .env.example .env
|
||||
nano .env # Edit with your values
|
||||
```
|
||||
|
||||
3. **Create required directories**
|
||||
```bash
|
||||
mkdir -p events/active events/archived data logs backup storage
|
||||
chmod -R 755 events data logs backup storage
|
||||
```
|
||||
|
||||
4. **Build and deploy**
|
||||
```bash
|
||||
docker compose build
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
5. **Check logs**
|
||||
```bash
|
||||
docker compose logs -f
|
||||
```
|
||||
|
||||
## 🔧 Configuration
|
||||
|
||||
### Essential Environment Variables
|
||||
@@ -358,14 +262,16 @@ docker exec picpeak-backend cat data/ADMIN_CREDENTIALS.txt
|
||||
# Show current admin username and email (password is hidden)
|
||||
docker exec picpeak-backend node scripts/show-admin-credentials.js
|
||||
|
||||
# Reset the admin password to a new random password
|
||||
# Reset the admin password to a new random password (displays new password in console)
|
||||
docker exec picpeak-backend node scripts/show-admin-credentials.js --reset
|
||||
```
|
||||
|
||||
> **Note:** When using `--reset`, the new password will be displayed in the console output. Save it immediately - it will not be shown again!
|
||||
|
||||
#### Important Security Notes
|
||||
|
||||
- **Login requires the email address**, not username
|
||||
- The admin password is only displayed once during initial setup
|
||||
- When resetting password, the new password is displayed once in the console - save it immediately
|
||||
- **Password change is MANDATORY** on first login - the system will force you to change it
|
||||
- If you lose the password before first login, use the `--reset` option to generate a new one
|
||||
- New password requirements: minimum 12 characters, mixed case, numbers, and special characters
|
||||
@@ -446,6 +352,61 @@ ADMIN_EMAIL=your-email@yourdomain.com
|
||||
|
||||
**Note**: This only works on first deployment. To change the admin email after deployment, you'll need to update it in the database or create a new admin user through the admin panel.
|
||||
|
||||
## 🔄 Release Channels
|
||||
|
||||
PicPeak offers two release channels for different needs:
|
||||
|
||||
### Stable Channel (Recommended)
|
||||
- Production-ready releases
|
||||
- Thoroughly tested before release
|
||||
- Docker tags: `stable`, `latest`, or specific version like `v2.3.0`
|
||||
|
||||
### Beta Channel
|
||||
- Early access to new features
|
||||
- May contain bugs or incomplete functionality
|
||||
- Docker tags: `beta` or specific version like `v2.3.0-beta.1`
|
||||
|
||||
### Configuring Your Channel
|
||||
|
||||
Set the `PICPEAK_CHANNEL` environment variable in your `.env` file:
|
||||
|
||||
```bash
|
||||
# For stable releases (default)
|
||||
PICPEAK_CHANNEL=stable
|
||||
|
||||
# For beta releases
|
||||
PICPEAK_CHANNEL=beta
|
||||
|
||||
# For a specific version
|
||||
PICPEAK_CHANNEL=v2.3.0
|
||||
```
|
||||
|
||||
The `docker-compose.production.yml` uses this variable for both backend and frontend images:
|
||||
```yaml
|
||||
image: ghcr.io/the-luap/picpeak/backend:${PICPEAK_CHANNEL:-stable}
|
||||
```
|
||||
|
||||
### Switching Channels
|
||||
|
||||
To switch between channels:
|
||||
|
||||
```bash
|
||||
# Edit your .env file
|
||||
nano .env
|
||||
# Change PICPEAK_CHANNEL=stable to PICPEAK_CHANNEL=beta (or vice versa)
|
||||
|
||||
# Pull the new images and restart
|
||||
docker compose -f docker-compose.production.yml pull
|
||||
docker compose -f docker-compose.production.yml up -d
|
||||
```
|
||||
|
||||
### Update Notifications
|
||||
|
||||
The admin dashboard automatically notifies you when updates are available for your channel. This feature:
|
||||
- Checks GitHub releases hourly (cached to avoid rate limits)
|
||||
- Shows updates relevant to your current channel (stable or beta)
|
||||
- Can be disabled by setting `UPDATE_CHECK_ENABLED=false` in your `.env`
|
||||
|
||||
## 🔒 Reverse Proxy Setup
|
||||
|
||||
For production deployments, you should use a reverse proxy for SSL/HTTPS. The application exposes ports directly, allowing you to use any reverse proxy solution.
|
||||
@@ -656,20 +617,27 @@ docker compose up -d
|
||||
docker compose ps
|
||||
```
|
||||
|
||||
#### Specific Version Updates
|
||||
#### Specific Version or Channel Updates
|
||||
|
||||
To use a specific version of the images:
|
||||
To use a specific version or switch channels, update your `.env` file:
|
||||
|
||||
```bash
|
||||
# Edit docker-compose.production.yml to specify version tags
|
||||
# Change: ghcr.io/the-luap/picpeak/backend:latest
|
||||
# To: ghcr.io/the-luap/picpeak/backend:v1.0.0
|
||||
# Edit .env to change the channel or pin to a specific version
|
||||
nano .env
|
||||
|
||||
# Options for PICPEAK_CHANNEL:
|
||||
# - stable (recommended, production-ready)
|
||||
# - beta (early access to new features)
|
||||
# - v2.3.0 (pin to specific stable version)
|
||||
# - v2.3.0-beta.1 (pin to specific beta version)
|
||||
|
||||
# Then pull and restart
|
||||
docker compose -f docker-compose.production.yml pull
|
||||
docker compose -f docker-compose.production.yml up -d
|
||||
```
|
||||
|
||||
The admin dashboard will notify you when updates are available for your configured channel.
|
||||
|
||||
### Database Migrations
|
||||
|
||||
Migrations run automatically on startup, but you can run them manually:
|
||||
|
||||
@@ -79,7 +79,51 @@ Note on Docker file permissions (PUID/PGID)
|
||||
- Example in `.env`:
|
||||
- `PUID=1000`
|
||||
- `PGID=1000`
|
||||
- Without this, creating events, uploads, thumbnails, or logs can fail with “Permission denied”.
|
||||
- Without this, creating events, uploads, thumbnails, or logs can fail with "Permission denied".
|
||||
|
||||
## 🔄 Release Channels
|
||||
|
||||
PicPeak offers two release channels for different needs:
|
||||
|
||||
### Stable Channel (Recommended)
|
||||
- Production-ready releases
|
||||
- Thoroughly tested before release
|
||||
- Docker tags: `stable`, `latest`, or specific version like `v2.3.0`
|
||||
|
||||
### Beta Channel
|
||||
- Early access to new features
|
||||
- May contain bugs or incomplete functionality
|
||||
- Docker tags: `beta` or specific version like `v2.3.0-beta.1`
|
||||
|
||||
### Switching Channels
|
||||
|
||||
Set the `PICPEAK_CHANNEL` environment variable in your `.env` file:
|
||||
|
||||
```bash
|
||||
# For stable releases (default)
|
||||
PICPEAK_CHANNEL=stable
|
||||
|
||||
# For beta releases
|
||||
PICPEAK_CHANNEL=beta
|
||||
|
||||
# For a specific version
|
||||
PICPEAK_CHANNEL=v2.3.0
|
||||
```
|
||||
|
||||
Then update your containers:
|
||||
|
||||
```bash
|
||||
docker-compose -f docker-compose.production.yml pull
|
||||
docker-compose -f docker-compose.production.yml up -d
|
||||
```
|
||||
|
||||
### Update Notifications
|
||||
|
||||
The admin dashboard automatically notifies you when updates are available for your channel. To disable update checks, set:
|
||||
|
||||
```bash
|
||||
UPDATE_CHECK_ENABLED=false
|
||||
```
|
||||
|
||||
## 📖 Documentation
|
||||
|
||||
|
||||
@@ -451,6 +451,8 @@ cd /opt/picpeak/app/backend
|
||||
sudo -u picpeak node scripts/reset-admin-password.js
|
||||
```
|
||||
|
||||
> **Note:** The new password will be displayed in the console output and saved to `ADMIN_PASSWORD_RESET.txt`. Save it immediately!
|
||||
|
||||
### Getting Help
|
||||
|
||||
1. **Check logs:**
|
||||
|
||||
@@ -3,6 +3,81 @@
|
||||
* These templates support the RBAC (Role-Based Access Control) feature
|
||||
*/
|
||||
exports.up = async function(knex) {
|
||||
// First, ensure the email_templates table has multilingual columns
|
||||
// This is needed for fresh installations where legacy migrations don't run
|
||||
const columnInfo = await knex('email_templates').columnInfo();
|
||||
|
||||
if (!columnInfo.subject_en) {
|
||||
// Need to add multilingual columns
|
||||
console.log('Adding multilingual columns to email_templates table...');
|
||||
|
||||
// Check if we're using SQLite or PostgreSQL
|
||||
const client = knex.client.config.client;
|
||||
const isSqlite = client === 'sqlite3' || client === 'better-sqlite3';
|
||||
|
||||
if (isSqlite) {
|
||||
// SQLite doesn't support column rename directly in all versions
|
||||
// We need to recreate the table with new structure
|
||||
|
||||
// Get existing data
|
||||
const existingData = await knex('email_templates').select('*');
|
||||
|
||||
// Drop the old table
|
||||
await knex.schema.dropTable('email_templates');
|
||||
|
||||
// Create new table with multilingual columns
|
||||
await knex.schema.createTable('email_templates', (table) => {
|
||||
table.increments('id').primary();
|
||||
table.string('template_key').unique().notNullable();
|
||||
table.string('subject_en');
|
||||
table.string('subject_de');
|
||||
table.text('body_html_en');
|
||||
table.text('body_html_de');
|
||||
table.text('body_text_en');
|
||||
table.text('body_text_de');
|
||||
table.json('variables');
|
||||
table.datetime('updated_at').defaultTo(knex.fn.now());
|
||||
});
|
||||
|
||||
// Re-insert existing data with column mapping
|
||||
for (const row of existingData) {
|
||||
await knex('email_templates').insert({
|
||||
template_key: row.template_key,
|
||||
subject_en: row.subject,
|
||||
subject_de: row.subject, // Copy to German as default
|
||||
body_html_en: row.body_html,
|
||||
body_html_de: row.body_html,
|
||||
body_text_en: row.body_text,
|
||||
body_text_de: row.body_text,
|
||||
variables: row.variables,
|
||||
updated_at: row.updated_at
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Migrated email_templates table to multilingual structure');
|
||||
} else {
|
||||
// PostgreSQL supports ALTER TABLE for column operations
|
||||
await knex.schema.alterTable('email_templates', (table) => {
|
||||
table.renameColumn('subject', 'subject_en');
|
||||
table.renameColumn('body_html', 'body_html_en');
|
||||
table.renameColumn('body_text', 'body_text_en');
|
||||
});
|
||||
|
||||
await knex.schema.alterTable('email_templates', (table) => {
|
||||
table.string('subject_de');
|
||||
table.text('body_html_de');
|
||||
table.text('body_text_de');
|
||||
});
|
||||
|
||||
// Copy English values to German as defaults
|
||||
await knex('email_templates').update({
|
||||
subject_de: knex.raw('subject_en'),
|
||||
body_html_de: knex.raw('body_html_en'),
|
||||
body_text_de: knex.raw('body_text_en')
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Check which templates already exist
|
||||
const existingTemplates = await knex('email_templates')
|
||||
.select('template_key')
|
||||
|
||||
@@ -1,22 +1,27 @@
|
||||
exports.up = async function(knex) {
|
||||
// Add photo_counter column to photo_categories table
|
||||
await knex.schema.alterTable('photo_categories', function(table) {
|
||||
table.integer('photo_counter').defaultTo(0).notNullable();
|
||||
});
|
||||
// Check if photo_counter column already exists to make migration idempotent
|
||||
const hasPhotoCounter = await knex.schema.hasColumn('photo_categories', 'photo_counter');
|
||||
|
||||
// Initialize counters based on existing photos
|
||||
const categories = await knex('photo_categories').select('id');
|
||||
|
||||
for (const category of categories) {
|
||||
const photoCount = await knex('photos')
|
||||
.where('category_id', category.id)
|
||||
.count('id as count')
|
||||
.first();
|
||||
|
||||
if (photoCount && photoCount.count > 0) {
|
||||
await knex('photo_categories')
|
||||
.where('id', category.id)
|
||||
.update({ photo_counter: photoCount.count });
|
||||
if (!hasPhotoCounter) {
|
||||
// Add photo_counter column to photo_categories table
|
||||
await knex.schema.alterTable('photo_categories', function(table) {
|
||||
table.integer('photo_counter').defaultTo(0).notNullable();
|
||||
});
|
||||
|
||||
// Initialize counters based on existing photos
|
||||
const categories = await knex('photo_categories').select('id');
|
||||
|
||||
for (const category of categories) {
|
||||
const photoCount = await knex('photos')
|
||||
.where('category_id', category.id)
|
||||
.count('id as count')
|
||||
.first();
|
||||
|
||||
if (photoCount && photoCount.count > 0) {
|
||||
await knex('photo_categories')
|
||||
.where('id', category.id)
|
||||
.update({ photo_counter: photoCount.count });
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,23 +1,33 @@
|
||||
exports.up = async function(knex) {
|
||||
// Add language-specific columns to email_templates
|
||||
await knex.schema.alterTable('email_templates', function(table) {
|
||||
// Add English versions (rename existing columns for consistency)
|
||||
table.renameColumn('subject', 'subject_en');
|
||||
table.renameColumn('body_html', 'body_html_en');
|
||||
table.renameColumn('body_text', 'body_text_en');
|
||||
|
||||
// Add German versions
|
||||
table.string('subject_de');
|
||||
table.text('body_html_de');
|
||||
table.text('body_text_de');
|
||||
});
|
||||
// Check which columns already exist to make migration idempotent
|
||||
const hasSubjectEn = await knex.schema.hasColumn('email_templates', 'subject_en');
|
||||
const hasSubjectDe = await knex.schema.hasColumn('email_templates', 'subject_de');
|
||||
const hasSubjectOriginal = await knex.schema.hasColumn('email_templates', 'subject');
|
||||
|
||||
// Copy existing values to German columns as defaults
|
||||
await knex('email_templates').update({
|
||||
subject_de: knex.raw('subject_en'),
|
||||
body_html_de: knex.raw('body_html_en'),
|
||||
body_text_de: knex.raw('body_text_en')
|
||||
});
|
||||
// Only rename columns if they haven't been renamed yet
|
||||
if (hasSubjectOriginal && !hasSubjectEn) {
|
||||
await knex.schema.alterTable('email_templates', function(table) {
|
||||
table.renameColumn('subject', 'subject_en');
|
||||
table.renameColumn('body_html', 'body_html_en');
|
||||
table.renameColumn('body_text', 'body_text_en');
|
||||
});
|
||||
}
|
||||
|
||||
// Only add German columns if they don't exist
|
||||
if (!hasSubjectDe) {
|
||||
await knex.schema.alterTable('email_templates', function(table) {
|
||||
table.string('subject_de');
|
||||
table.text('body_html_de');
|
||||
table.text('body_text_de');
|
||||
});
|
||||
|
||||
// Copy existing values to German columns as defaults
|
||||
await knex('email_templates').update({
|
||||
subject_de: knex.raw('subject_en'),
|
||||
body_html_de: knex.raw('body_html_en'),
|
||||
body_text_de: knex.raw('body_text_en')
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
exports.down = async function(knex) {
|
||||
|
||||
@@ -67,26 +67,37 @@ async function runMigrationSafely(filepath) {
|
||||
const migrationPath = path.join(__dirname, filepath);
|
||||
const migration = require(migrationPath);
|
||||
const filename = path.basename(filepath);
|
||||
|
||||
|
||||
if (migration.up) {
|
||||
console.log(`Running migration: ${filepath}`);
|
||||
|
||||
|
||||
// Run migration in a transaction if possible
|
||||
// IMPORTANT: Include the migrations table insert INSIDE the transaction
|
||||
// to ensure atomicity between schema changes and tracking
|
||||
if (db.client.config.client === 'pg') {
|
||||
await db.transaction(async (trx) => {
|
||||
await migration.up(trx);
|
||||
// Insert migration record inside transaction for atomicity
|
||||
await trx('migrations').insert({ filename });
|
||||
});
|
||||
} else {
|
||||
await migration.up(db);
|
||||
await db('migrations').insert({ filename });
|
||||
}
|
||||
|
||||
await db('migrations').insert({ filename });
|
||||
|
||||
console.log(`Migration ${filepath} completed successfully`);
|
||||
}
|
||||
} catch (error) {
|
||||
// Check if error is because schema already exists
|
||||
if (error.code === '42P07' || // PostgreSQL: relation already exists
|
||||
error.code === 'SQLITE_ERROR' && error.message.includes('already exists')) {
|
||||
// PostgreSQL error codes:
|
||||
// - 42P07: duplicate_table (relation already exists)
|
||||
// - 42701: duplicate_column (column already exists)
|
||||
// - 42710: duplicate_object (constraint, index, etc. already exists)
|
||||
// - 23505: unique_violation (migration record already exists)
|
||||
const schemaExistsErrors = ['42P07', '42701', '42710', '23505'];
|
||||
const isSQLiteAlreadyExists = error.code === 'SQLITE_ERROR' && error.message.includes('already exists');
|
||||
|
||||
if (schemaExistsErrors.includes(error.code) || isSQLiteAlreadyExists) {
|
||||
console.log(`Migration ${filepath} - schema already exists, marking as applied`);
|
||||
await markMigrationAsApplied(path.basename(filepath));
|
||||
} else {
|
||||
|
||||
@@ -26,11 +26,22 @@ async function runMigration(filepath) {
|
||||
const migrationPath = path.join(__dirname, filepath);
|
||||
const migration = require(migrationPath);
|
||||
const filename = path.basename(filepath);
|
||||
|
||||
|
||||
if (migration.up) {
|
||||
console.log(`Running migration: ${filepath}`);
|
||||
await migration.up(db);
|
||||
await db('migrations').insert({ filename });
|
||||
|
||||
// Run migration in a transaction if PostgreSQL to ensure atomicity
|
||||
// between schema changes and migration tracking
|
||||
if (db.client.config.client === 'pg') {
|
||||
await db.transaction(async (trx) => {
|
||||
await migration.up(trx);
|
||||
await trx('migrations').insert({ filename });
|
||||
});
|
||||
} else {
|
||||
await migration.up(db);
|
||||
await db('migrations').insert({ filename });
|
||||
}
|
||||
|
||||
console.log(`Migration ${filepath} completed`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "picpeak-backend",
|
||||
"version": "2.2.3",
|
||||
"version": "2.3.4",
|
||||
"description": "Backend for PicPeak event photo sharing platform",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -36,12 +36,14 @@ async function showAdminCredentials(resetPassword = false) {
|
||||
.where('id', admin.id)
|
||||
.update({
|
||||
password_hash: passwordHash,
|
||||
must_change_password: true,
|
||||
updated_at: new Date()
|
||||
});
|
||||
|
||||
// Password logging removed for security - check logs or database if needed
|
||||
console.log('Password: [NEWLY RESET - stored in database]');
|
||||
console.log('\n⚠️ IMPORTANT: New password has been set in database!');
|
||||
|
||||
console.log(`Password: ${newPassword}`);
|
||||
console.log('\n⚠️ IMPORTANT:');
|
||||
console.log('1. Save this password securely - it will not be shown again');
|
||||
console.log('2. You will be required to change it on next login');
|
||||
} else {
|
||||
console.log('Password: [hidden - use --reset flag to generate new password]');
|
||||
}
|
||||
|
||||
@@ -392,11 +392,21 @@ router.post('/branding/watermark-logo', adminAuth, requirePermission('settings.e
|
||||
.first();
|
||||
|
||||
if (oldWatermarkLogoSetting && oldWatermarkLogoSetting.setting_value) {
|
||||
const oldPath = JSON.parse(oldWatermarkLogoSetting.setting_value);
|
||||
let oldPath;
|
||||
try {
|
||||
await fs.unlink(oldPath);
|
||||
} catch (error) {
|
||||
console.error('Failed to delete old watermark logo:', error);
|
||||
// Try to parse as JSON first (for JSON-stringified paths)
|
||||
oldPath = JSON.parse(oldWatermarkLogoSetting.setting_value);
|
||||
} catch (e) {
|
||||
// If it's not valid JSON, use the raw value
|
||||
oldPath = oldWatermarkLogoSetting.setting_value;
|
||||
}
|
||||
|
||||
if (oldPath && typeof oldPath === 'string') {
|
||||
try {
|
||||
await fs.unlink(oldPath);
|
||||
} catch (error) {
|
||||
console.error('Failed to delete old watermark logo:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -421,13 +431,13 @@ router.post('/branding/watermark-logo', adminAuth, requirePermission('settings.e
|
||||
await db('app_settings')
|
||||
.insert({
|
||||
setting_key: 'branding_watermark_logo_url',
|
||||
setting_value: publicPath,
|
||||
setting_value: JSON.stringify(publicPath),
|
||||
setting_type: 'branding',
|
||||
updated_at: new Date()
|
||||
})
|
||||
.onConflict('setting_key')
|
||||
.merge({
|
||||
setting_value: publicPath,
|
||||
setting_value: JSON.stringify(publicPath),
|
||||
updated_at: new Date()
|
||||
});
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ const path = require('path');
|
||||
const os = require('os');
|
||||
const { formatBoolean } = require('../utils/dbCompat');
|
||||
const logger = require('../utils/logger');
|
||||
const { checkForUpdates, getCurrentChannel } = require('../services/updateCheckService');
|
||||
const router = express.Router();
|
||||
|
||||
// Get system version
|
||||
@@ -22,12 +23,15 @@ router.get('/version', adminAuth, requirePermission('settings.view'), async (req
|
||||
} catch (err) {
|
||||
console.error('Could not read package.json:', err);
|
||||
}
|
||||
|
||||
|
||||
const channel = getCurrentChannel(backendVersion);
|
||||
|
||||
res.json({
|
||||
backend: backendVersion,
|
||||
frontend: '1.0.0', // This will be set by frontend
|
||||
node: process.version,
|
||||
environment: process.env.NODE_ENV || 'production'
|
||||
environment: process.env.NODE_ENV || 'production',
|
||||
channel: channel
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching version:', error);
|
||||
@@ -35,6 +39,32 @@ router.get('/version', adminAuth, requirePermission('settings.view'), async (req
|
||||
}
|
||||
});
|
||||
|
||||
// Check for updates
|
||||
router.get('/updates', adminAuth, requirePermission('settings.view'), async (req, res) => {
|
||||
try {
|
||||
// Check if update checking is enabled
|
||||
const updateCheckEnabled = process.env.UPDATE_CHECK_ENABLED !== 'false';
|
||||
|
||||
if (!updateCheckEnabled) {
|
||||
return res.json({
|
||||
enabled: false,
|
||||
message: 'Update checking is disabled'
|
||||
});
|
||||
}
|
||||
|
||||
const forceRefresh = req.query.refresh === 'true';
|
||||
const updateInfo = await checkForUpdates(forceRefresh);
|
||||
|
||||
res.json({
|
||||
enabled: true,
|
||||
...updateInfo
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Error checking for updates:', error);
|
||||
res.status(500).json({ error: 'Failed to check for updates' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get comprehensive system status
|
||||
router.get('/status', adminAuth, requirePermission('settings.view'), async (req, res) => {
|
||||
try {
|
||||
|
||||
@@ -12,6 +12,7 @@ const { resolvePhotoFilePath } = require('../services/photoResolver');
|
||||
const { getEventShareToken, resolveShareIdentifier, buildShareLinkVariants } = require('../services/shareLinkService');
|
||||
const { handleAsync } = require('../utils/routeHelpers');
|
||||
const { NotFoundError } = require('../utils/errors');
|
||||
const { ensureThumbnail } = require('../services/imageProcessor');
|
||||
|
||||
// Get storage path from environment or default
|
||||
const getStoragePath = () => process.env.STORAGE_PATH || path.join(__dirname, '../../storage');
|
||||
@@ -171,7 +172,13 @@ router.get('/:slug/photos', verifyGalleryAccess, async (req, res) => {
|
||||
try {
|
||||
// Get filter parameters from query
|
||||
const { filter, guest_id } = req.query;
|
||||
|
||||
|
||||
// Get watermark settings to generate cache-busting version for URLs
|
||||
const watermarkSettings = await watermarkService.getWatermarkSettings();
|
||||
const wmVersion = watermarkSettings?.enabled
|
||||
? `wm=${watermarkSettings.opacity}${watermarkSettings.position}${watermarkSettings.size}`
|
||||
: '';
|
||||
|
||||
// First get all photos
|
||||
let photos = await db('photos')
|
||||
.where('photos.event_id', req.event.id)
|
||||
@@ -326,15 +333,17 @@ router.get('/:slug/photos', verifyGalleryAccess, async (req, res) => {
|
||||
categories: categories,
|
||||
photos: photos.map(photo => {
|
||||
const useJwtUrl = (protectionSettings.protection_level === 'basic' || protectionSettings.protection_level === 'standard');
|
||||
const photoUrl = useJwtUrl ?
|
||||
`/api/gallery/${req.params.slug}/photo/${photo.id}` :
|
||||
// Add watermark version to URLs for cache busting when settings change
|
||||
const wmQuery = wmVersion ? `?${wmVersion}` : '';
|
||||
const photoUrl = useJwtUrl ?
|
||||
`/api/gallery/${req.params.slug}/photo/${photo.id}${wmQuery}` :
|
||||
`/api/secure-images/${req.params.slug}/secure/${photo.id}/{{token}}`;
|
||||
|
||||
|
||||
return {
|
||||
id: photo.id,
|
||||
filename: photo.filename,
|
||||
url: photoUrl,
|
||||
thumbnail_url: photo.thumbnail_path ? `/api/gallery/${req.params.slug}/thumbnail/${photo.id}` : null,
|
||||
thumbnail_url: photo.thumbnail_path ? `/api/gallery/${req.params.slug}/thumbnail/${photo.id}${wmQuery}` : null,
|
||||
secure_url_template: `/api/secure-images/${req.params.slug}/secure/${photo.id}/{{token}}`,
|
||||
download_url_template: `/api/secure-images/${req.params.slug}/secure-download/${photo.id}/{{token}}`,
|
||||
type: photo.type,
|
||||
@@ -753,6 +762,20 @@ router.get('/:slug/photo/:photoId',
|
||||
// Get watermark settings
|
||||
const watermarkSettings = await watermarkService.getWatermarkSettings();
|
||||
|
||||
// Generate ETag based on photo id, modification time, and watermark settings
|
||||
// This ensures cache invalidation when watermark settings change
|
||||
const fs = require('fs');
|
||||
const stat = fs.statSync(filePath);
|
||||
const watermarkHash = watermarkSettings?.enabled
|
||||
? `-wm${watermarkSettings.opacity}${watermarkSettings.position}${watermarkSettings.size}`
|
||||
: '-nowm';
|
||||
const etag = `"${photoId}-${stat.mtime.getTime()}${watermarkHash}"`;
|
||||
|
||||
// Check if client has valid cached version
|
||||
if (req.headers['if-none-match'] === etag) {
|
||||
return res.status(304).end();
|
||||
}
|
||||
|
||||
if (watermarkSettings && watermarkSettings.enabled) {
|
||||
// Apply watermark and send
|
||||
const watermarkedBuffer = await watermarkService.applyWatermark(filePath, watermarkSettings);
|
||||
@@ -760,6 +783,7 @@ router.get('/:slug/photo/:photoId',
|
||||
res.set({
|
||||
'Content-Type': photo.mime_type || 'image/jpeg',
|
||||
'Cache-Control': 'private, max-age=1800', // Cache for 30 minutes
|
||||
'ETag': etag,
|
||||
'X-Protection-Level': 'basic'
|
||||
});
|
||||
|
||||
@@ -768,6 +792,7 @@ router.get('/:slug/photo/:photoId',
|
||||
// Send original file with basic protection headers
|
||||
res.set({
|
||||
'Cache-Control': 'private, max-age=1800',
|
||||
'ETag': etag,
|
||||
'X-Protection-Level': 'basic'
|
||||
});
|
||||
// Ensure absolute path for res.sendFile
|
||||
@@ -787,30 +812,30 @@ router.get('/:slug/photo/:photoId',
|
||||
);
|
||||
|
||||
// Serve thumbnail
|
||||
router.get('/:slug/thumbnail/:photoId',
|
||||
verifyGalleryAccess,
|
||||
router.get('/:slug/thumbnail/:photoId',
|
||||
verifyGalleryAccess,
|
||||
async (req, res) => {
|
||||
try {
|
||||
const { photoId } = req.params;
|
||||
|
||||
|
||||
const photo = await db('photos')
|
||||
.where({ id: photoId, event_id: req.event.id })
|
||||
.first();
|
||||
|
||||
if (!photo || !photo.thumbnail_path) {
|
||||
return res.status(404).json({ error: 'Thumbnail not found' });
|
||||
|
||||
if (!photo) {
|
||||
return res.status(404).json({ error: 'Photo not found' });
|
||||
}
|
||||
|
||||
const thumbPath = path.join(getStoragePath(), photo.thumbnail_path);
|
||||
|
||||
// Check if file exists
|
||||
const fs = require('fs').promises;
|
||||
try {
|
||||
await fs.access(thumbPath);
|
||||
} catch (error) {
|
||||
return res.status(404).json({ error: 'Thumbnail file not found' });
|
||||
|
||||
// Ensure thumbnail exists and is valid, regenerate if needed
|
||||
const thumbnailPath = await ensureThumbnail(photo);
|
||||
|
||||
if (!thumbnailPath) {
|
||||
logger.error(`Failed to generate thumbnail for photo ${photoId}`);
|
||||
return res.status(404).json({ error: 'Thumbnail generation failed' });
|
||||
}
|
||||
|
||||
const thumbPath = path.join(getStoragePath(), thumbnailPath);
|
||||
|
||||
// Log thumbnail access
|
||||
await secureImageService.logImageAccess(
|
||||
photoId,
|
||||
@@ -818,18 +843,41 @@ router.get('/:slug/thumbnail/:photoId',
|
||||
req.clientInfo,
|
||||
'thumbnail'
|
||||
);
|
||||
|
||||
|
||||
// Check if watermarks are enabled and apply to thumbnail
|
||||
const watermarkSettings = await watermarkService.getWatermarkSettings();
|
||||
|
||||
// Generate ETag based on photo id, thumbnail modification time, and watermark settings
|
||||
const fs = require('fs');
|
||||
const stat = fs.statSync(thumbPath);
|
||||
const watermarkHash = watermarkSettings?.enabled
|
||||
? `-wm${watermarkSettings.opacity}${watermarkSettings.position}${watermarkSettings.size}`
|
||||
: '-nowm';
|
||||
const etag = `"thumb-${photoId}-${stat.mtime.getTime()}${watermarkHash}"`;
|
||||
|
||||
// Check if client has valid cached version
|
||||
if (req.headers['if-none-match'] === etag) {
|
||||
return res.status(304).end();
|
||||
}
|
||||
|
||||
// Set appropriate headers with enhanced security
|
||||
res.set({
|
||||
'Content-Type': 'image/jpeg',
|
||||
'Cache-Control': 'private, max-age=1800', // Reduced cache time
|
||||
'Cross-Origin-Resource-Policy': 'cross-origin',
|
||||
'X-Content-Type-Options': 'nosniff',
|
||||
'X-Protected-Thumbnail': 'true'
|
||||
'X-Protected-Thumbnail': 'true',
|
||||
'ETag': etag
|
||||
});
|
||||
|
||||
// Send file
|
||||
res.sendFile(path.resolve(thumbPath));
|
||||
|
||||
if (watermarkSettings && watermarkSettings.enabled) {
|
||||
// Apply watermark to thumbnail
|
||||
const watermarkedBuffer = await watermarkService.applyWatermark(thumbPath, watermarkSettings);
|
||||
res.send(watermarkedBuffer);
|
||||
} else {
|
||||
// Send file without watermark
|
||||
res.sendFile(path.resolve(thumbPath));
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error serving thumbnail:', {
|
||||
error: error.message,
|
||||
|
||||
@@ -90,38 +90,50 @@ router.get('/:slug/photo/:photoId/view', verifyGalleryAccess, async (req, res) =
|
||||
}, 'view');
|
||||
|
||||
// Get protection settings from event
|
||||
const eventProtectionLevel = req.event.protection_level || protectionLevel;
|
||||
const protectionSettings = {
|
||||
protectionLevel: req.event.protection_level || protectionLevel,
|
||||
protectionLevel: eventProtectionLevel,
|
||||
quality: req.event.image_quality || 85,
|
||||
addFingerprint: req.event.add_fingerprint !== false,
|
||||
fragmentImage: protectionLevel === 'maximum'
|
||||
fragmentImage: eventProtectionLevel === 'maximum'
|
||||
};
|
||||
|
||||
|
||||
// Build full path to photo
|
||||
const photoPath = path.join(getStoragePath(), 'events/active', req.event.slug, photo.path);
|
||||
|
||||
// Process image with protection
|
||||
const processedImage = await secureImageService.processProtectedImage(photoPath, protectionSettings);
|
||||
|
||||
// Apply watermark if enabled
|
||||
|
||||
// For basic/standard protection without special features, serve original file
|
||||
// This avoids unnecessary recompression
|
||||
const needsProcessing = eventProtectionLevel === 'enhanced' ||
|
||||
eventProtectionLevel === 'maximum' ||
|
||||
protectionSettings.addFingerprint;
|
||||
|
||||
let finalImage;
|
||||
if (processedImage.type === 'fragmented') {
|
||||
// Return fragmented image data for canvas reconstruction
|
||||
return res.json({
|
||||
type: 'fragmented',
|
||||
fragments: processedImage.fragments.map(f => ({
|
||||
index: f.index,
|
||||
row: f.row,
|
||||
col: f.col,
|
||||
data: f.buffer.toString('base64'),
|
||||
position: f.position
|
||||
})),
|
||||
dimensions: processedImage.originalDimensions,
|
||||
fragmentDimensions: processedImage.fragmentDimensions
|
||||
});
|
||||
|
||||
if (!needsProcessing) {
|
||||
// Serve original file without processing
|
||||
const fs = require('fs').promises;
|
||||
finalImage = await fs.readFile(photoPath);
|
||||
} else {
|
||||
const watermarkSettings = await watermarkService.getWatermarkSettings();
|
||||
finalImage = await watermarkService.applyWatermark(photoPath, watermarkSettings);
|
||||
// Process image with protection measures
|
||||
const processedImage = await secureImageService.processProtectedImage(photoPath, protectionSettings);
|
||||
|
||||
if (processedImage.type === 'fragmented') {
|
||||
// Return fragmented image data for canvas reconstruction
|
||||
return res.json({
|
||||
type: 'fragmented',
|
||||
fragments: processedImage.fragments.map(f => ({
|
||||
index: f.index,
|
||||
row: f.row,
|
||||
col: f.col,
|
||||
data: f.buffer.toString('base64'),
|
||||
position: f.position
|
||||
})),
|
||||
dimensions: processedImage.originalDimensions,
|
||||
fragmentDimensions: processedImage.fragmentDimensions
|
||||
});
|
||||
}
|
||||
|
||||
finalImage = processedImage;
|
||||
}
|
||||
|
||||
// Set security headers
|
||||
|
||||
@@ -993,12 +993,16 @@ async function getBackupStatus(limit = 10) {
|
||||
}
|
||||
}
|
||||
|
||||
const lastRunWithManifest = lastRun ? { ...lastRun, manifestValid } : null;
|
||||
|
||||
return {
|
||||
isRunning,
|
||||
isHealthy: Boolean(lastRun && lastRun.status === 'completed'),
|
||||
lastRun: lastRun ? { ...lastRun, manifestValid } : null,
|
||||
lastRun: lastRunWithManifest,
|
||||
lastBackup: lastRunWithManifest, // Alias for frontend compatibility
|
||||
recentRuns: runs,
|
||||
recentBackups: runs, // Alias for frontend compatibility
|
||||
totalBackups: runs.filter(r => r.status === 'completed').length,
|
||||
nextScheduledRun: getNextScheduledRun()
|
||||
};
|
||||
} catch (error) {
|
||||
|
||||
@@ -157,6 +157,8 @@ class SecureImageService {
|
||||
|
||||
/**
|
||||
* Process image with protection measures
|
||||
* For basic/standard protection without fingerprinting, returns original file
|
||||
* For enhanced/maximum protection, applies quality reduction and fingerprinting
|
||||
*/
|
||||
async processProtectedImage(imagePath, options = {}) {
|
||||
const {
|
||||
@@ -169,11 +171,58 @@ class SecureImageService {
|
||||
} = options;
|
||||
|
||||
try {
|
||||
// For basic protection level, always return original file without processing
|
||||
if (protectionLevel === 'basic') {
|
||||
return await fs.readFile(imagePath);
|
||||
}
|
||||
|
||||
// For standard protection without fingerprinting, return original file
|
||||
// This avoids unnecessary recompression when no protection features are needed
|
||||
if (protectionLevel === 'standard' && !addFingerprint && !fragmentImage) {
|
||||
return await fs.readFile(imagePath);
|
||||
}
|
||||
|
||||
// Get metadata to check if processing is actually needed
|
||||
const metadata = await sharp(imagePath).metadata();
|
||||
|
||||
// For standard protection with fingerprint only (no resize needed, no quality change),
|
||||
// we can add fingerprint without full recompression by preserving format
|
||||
const needsResize = metadata.width > maxWidth || metadata.height > maxHeight;
|
||||
const needsQualityReduction = protectionLevel === 'enhanced' || protectionLevel === 'maximum';
|
||||
|
||||
// If standard protection and only fingerprinting is needed, and image doesn't need resize,
|
||||
// just add metadata without recompressing
|
||||
if (protectionLevel === 'standard' && addFingerprint && !needsResize) {
|
||||
let image = sharp(imagePath);
|
||||
|
||||
// Add fingerprint to metadata without changing image quality
|
||||
const fingerprint = crypto.randomBytes(16).toString('hex');
|
||||
|
||||
// Preserve original format with high quality
|
||||
const format = metadata.format || 'jpeg';
|
||||
if (format === 'png') {
|
||||
image = image.png({ compressionLevel: 6 });
|
||||
} else if (format === 'webp') {
|
||||
image = image.webp({ quality: 95 });
|
||||
} else {
|
||||
image = image.jpeg({ quality: 100, mozjpeg: true });
|
||||
}
|
||||
|
||||
image = image.withMetadata({
|
||||
exif: {
|
||||
[sharp.EXIF.IFD0.ImageDescription]: `Protected:${fingerprint}`
|
||||
}
|
||||
});
|
||||
|
||||
return await image.toBuffer();
|
||||
}
|
||||
|
||||
// For enhanced/maximum protection or when resize is needed, do full processing
|
||||
let image = sharp(imagePath);
|
||||
const metadata = await image.metadata();
|
||||
let effectiveQuality = quality;
|
||||
|
||||
// Resize if too large
|
||||
if (metadata.width > maxWidth || metadata.height > maxHeight) {
|
||||
if (needsResize) {
|
||||
image = image.resize(maxWidth, maxHeight, {
|
||||
fit: 'inside',
|
||||
withoutEnlargement: true
|
||||
@@ -182,18 +231,26 @@ class SecureImageService {
|
||||
|
||||
// Apply quality reduction for protection
|
||||
if (protectionLevel === 'enhanced') {
|
||||
quality = Math.min(quality, 70);
|
||||
effectiveQuality = Math.min(quality, 70);
|
||||
} else if (protectionLevel === 'maximum') {
|
||||
quality = Math.min(quality, 60);
|
||||
effectiveQuality = Math.min(quality, 60);
|
||||
}
|
||||
|
||||
// Convert to appropriate format
|
||||
image = image.jpeg({ quality, progressive: true });
|
||||
// Preserve original format when possible, apply quality settings
|
||||
const format = metadata.format || 'jpeg';
|
||||
if (format === 'png' && !needsQualityReduction) {
|
||||
image = image.png({ compressionLevel: 6 });
|
||||
} else if (format === 'webp') {
|
||||
image = image.webp({ quality: effectiveQuality });
|
||||
} else {
|
||||
// JPEG or when quality reduction is needed (convert to JPEG)
|
||||
image = image.jpeg({ quality: effectiveQuality, progressive: true });
|
||||
}
|
||||
|
||||
// Add invisible watermark/fingerprint
|
||||
if (addFingerprint) {
|
||||
const fingerprint = crypto.randomBytes(16).toString('hex');
|
||||
|
||||
|
||||
// Embed fingerprint in metadata
|
||||
image = image.withMetadata({
|
||||
exif: {
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
const axios = require('axios');
|
||||
const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
// Cache for version info (avoid hitting GitHub API too often)
|
||||
let versionCache = null;
|
||||
let lastCheck = 0;
|
||||
const CACHE_TTL = 60 * 60 * 1000; // 1 hour cache
|
||||
|
||||
/**
|
||||
* Get current installed version from package.json
|
||||
*/
|
||||
async function getCurrentVersion() {
|
||||
try {
|
||||
const packagePath = path.join(__dirname, '../../package.json');
|
||||
const packageContent = await fs.readFile(packagePath, 'utf8');
|
||||
const packageJson = JSON.parse(packageContent);
|
||||
return packageJson.version || '0.0.0';
|
||||
} catch (err) {
|
||||
logger.error('Could not read package.json for version:', err);
|
||||
return '0.0.0';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine current release channel from version or environment
|
||||
*/
|
||||
function getCurrentChannel(version) {
|
||||
// Check environment variable first
|
||||
const envChannel = process.env.PICPEAK_RELEASE_CHANNEL;
|
||||
if (envChannel && ['stable', 'beta'].includes(envChannel)) {
|
||||
return envChannel;
|
||||
}
|
||||
|
||||
// Infer from version string
|
||||
if (version && version.includes('-beta')) {
|
||||
return 'beta';
|
||||
}
|
||||
return 'stable';
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse version string into comparable parts
|
||||
*/
|
||||
function parseVersion(version) {
|
||||
if (!version) return null;
|
||||
|
||||
// Handle versions like "2.3.0" or "2.3.0-beta.1"
|
||||
const match = version.match(/^(\d+)\.(\d+)\.(\d+)(?:-beta\.(\d+))?$/);
|
||||
if (!match) return null;
|
||||
|
||||
return {
|
||||
major: parseInt(match[1], 10),
|
||||
minor: parseInt(match[2], 10),
|
||||
patch: parseInt(match[3], 10),
|
||||
beta: match[4] ? parseInt(match[4], 10) : null,
|
||||
isBeta: !!match[4]
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two versions
|
||||
* Returns: 1 if a > b, -1 if a < b, 0 if equal
|
||||
*/
|
||||
function compareVersions(a, b) {
|
||||
const va = parseVersion(a);
|
||||
const vb = parseVersion(b);
|
||||
|
||||
if (!va || !vb) return 0;
|
||||
|
||||
// Compare major.minor.patch
|
||||
if (va.major !== vb.major) return va.major > vb.major ? 1 : -1;
|
||||
if (va.minor !== vb.minor) return va.minor > vb.minor ? 1 : -1;
|
||||
if (va.patch !== vb.patch) return va.patch > vb.patch ? 1 : -1;
|
||||
|
||||
// Handle beta vs stable
|
||||
if (va.isBeta && !vb.isBeta) return -1; // beta < stable
|
||||
if (!va.isBeta && vb.isBeta) return 1; // stable > beta
|
||||
|
||||
// Both are beta - compare beta numbers
|
||||
if (va.isBeta && vb.isBeta) {
|
||||
if (va.beta !== vb.beta) return va.beta > vb.beta ? 1 : -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch available versions from GitHub Releases
|
||||
* Uses GitHub Releases API which is publicly accessible without authentication
|
||||
*/
|
||||
async function fetchAvailableVersions() {
|
||||
try {
|
||||
// Use GitHub Releases API (public, no auth required)
|
||||
const response = await axios.get(
|
||||
'https://api.github.com/repos/the-luap/picpeak/releases',
|
||||
{
|
||||
headers: {
|
||||
'Accept': 'application/vnd.github+json',
|
||||
'X-GitHub-Api-Version': '2022-11-28',
|
||||
'User-Agent': 'PicPeak-Update-Checker'
|
||||
},
|
||||
timeout: 10000
|
||||
}
|
||||
);
|
||||
|
||||
// Extract version tags from releases
|
||||
const versions = {
|
||||
stable: [],
|
||||
beta: []
|
||||
};
|
||||
|
||||
for (const release of response.data) {
|
||||
const tag = release.tag_name;
|
||||
if (!tag) continue;
|
||||
|
||||
// Remove 'v' prefix if present
|
||||
const version = tag.startsWith('v') ? tag.substring(1) : tag;
|
||||
|
||||
if (version.match(/^\d+\.\d+\.\d+$/)) {
|
||||
// Stable version
|
||||
versions.stable.push(version);
|
||||
} else if (version.match(/^\d+\.\d+\.\d+-beta\.\d+$/)) {
|
||||
// Beta version
|
||||
versions.beta.push(version);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort versions descending (newest first)
|
||||
versions.stable.sort((a, b) => compareVersions(b, a));
|
||||
versions.beta.sort((a, b) => compareVersions(b, a));
|
||||
|
||||
return versions;
|
||||
} catch (error) {
|
||||
logger.error('Failed to fetch available versions from GitHub:', error.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for available updates
|
||||
*/
|
||||
async function checkForUpdates(forceRefresh = false) {
|
||||
const now = Date.now();
|
||||
|
||||
// Use cache if available and not expired
|
||||
if (!forceRefresh && versionCache && (now - lastCheck) < CACHE_TTL) {
|
||||
return versionCache;
|
||||
}
|
||||
|
||||
const currentVersion = await getCurrentVersion();
|
||||
const currentChannel = getCurrentChannel(currentVersion);
|
||||
const availableVersions = await fetchAvailableVersions();
|
||||
|
||||
if (!availableVersions) {
|
||||
return {
|
||||
current: currentVersion,
|
||||
channel: currentChannel,
|
||||
updateAvailable: false,
|
||||
error: 'Unable to check for updates'
|
||||
};
|
||||
}
|
||||
|
||||
// Determine latest version for current channel
|
||||
const latestStable = availableVersions.stable[0] || currentVersion;
|
||||
const latestBeta = availableVersions.beta[0] || currentVersion;
|
||||
const latestForChannel = currentChannel === 'beta' ? latestBeta : latestStable;
|
||||
|
||||
const updateAvailable = compareVersions(latestForChannel, currentVersion) > 0;
|
||||
|
||||
// Also check if there's a newer beta for stable users who want to preview
|
||||
const newerBetaAvailable = currentChannel === 'stable' &&
|
||||
availableVersions.beta.length > 0 &&
|
||||
compareVersions(latestBeta, currentVersion) > 0;
|
||||
|
||||
const result = {
|
||||
current: currentVersion,
|
||||
channel: currentChannel,
|
||||
latest: {
|
||||
stable: latestStable,
|
||||
beta: latestBeta,
|
||||
forChannel: latestForChannel
|
||||
},
|
||||
updateAvailable,
|
||||
newerBetaAvailable,
|
||||
lastChecked: new Date().toISOString()
|
||||
};
|
||||
|
||||
// Update cache
|
||||
versionCache = result;
|
||||
lastCheck = now;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the version cache (useful for testing)
|
||||
*/
|
||||
function clearCache() {
|
||||
versionCache = null;
|
||||
lastCheck = 0;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
checkForUpdates,
|
||||
getCurrentVersion,
|
||||
getCurrentChannel,
|
||||
compareVersions,
|
||||
parseVersion,
|
||||
clearCache
|
||||
};
|
||||
@@ -177,14 +177,25 @@ class WatermarkService {
|
||||
settings.position
|
||||
);
|
||||
|
||||
// Apply watermark
|
||||
const watermarkedBuffer = await image
|
||||
.composite([{
|
||||
input: watermarkBuffer,
|
||||
top: position.top,
|
||||
left: position.left
|
||||
}])
|
||||
.toBuffer();
|
||||
// Apply watermark with high quality output to preserve original image quality
|
||||
let watermarkedImage = image.composite([{
|
||||
input: watermarkBuffer,
|
||||
top: position.top,
|
||||
left: position.left
|
||||
}]);
|
||||
|
||||
// Preserve original format with high quality settings
|
||||
const format = metadata.format || 'jpeg';
|
||||
let watermarkedBuffer;
|
||||
|
||||
if (format === 'png') {
|
||||
watermarkedBuffer = await watermarkedImage.png({ quality: 100, compressionLevel: 6 }).toBuffer();
|
||||
} else if (format === 'webp') {
|
||||
watermarkedBuffer = await watermarkedImage.webp({ quality: 95, lossless: false }).toBuffer();
|
||||
} else {
|
||||
// Default to JPEG with maximum quality (100) to prevent recompression
|
||||
watermarkedBuffer = await watermarkedImage.jpeg({ quality: 100, mozjpeg: true }).toBuffer();
|
||||
}
|
||||
|
||||
// Cache the result
|
||||
this.cache.set(cacheKey, {
|
||||
|
||||
@@ -38,7 +38,8 @@ services:
|
||||
|
||||
backend:
|
||||
# Use pre-built image from GitHub Container Registry
|
||||
image: ghcr.io/the-luap/picpeak/backend:latest
|
||||
# PICPEAK_CHANNEL: 'stable' (default), 'beta', or specific version like 'v2.3.0'
|
||||
image: ghcr.io/the-luap/picpeak/backend:${PICPEAK_CHANNEL:-stable}
|
||||
container_name: picpeak-backend
|
||||
env_file: .env
|
||||
environment:
|
||||
@@ -46,6 +47,7 @@ services:
|
||||
- DB_HOST=${DB_HOST:-postgres}
|
||||
- REDIS_HOST=redis
|
||||
- PHOTOS_DIR=/app/storage/events
|
||||
- PICPEAK_RELEASE_CHANNEL=${PICPEAK_CHANNEL:-stable}
|
||||
volumes:
|
||||
- ${APP_STORAGE}:/app/storage
|
||||
- ${LOGS}:/app/logs
|
||||
@@ -69,7 +71,8 @@ services:
|
||||
|
||||
frontend:
|
||||
# Use pre-built image from GitHub Container Registry
|
||||
image: ghcr.io/the-luap/picpeak/frontend:latest
|
||||
# Uses same channel as backend for consistency
|
||||
image: ghcr.io/the-luap/picpeak/frontend:${PICPEAK_CHANNEL:-stable}
|
||||
container_name: picpeak-frontend
|
||||
# Note: Pre-built frontend uses Nginx to proxy /api to backend:3001.
|
||||
# Prefer keeping API base as '/api' in builds to avoid CORS.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "picpeak-frontend",
|
||||
"private": true,
|
||||
"version": "2.2.3",
|
||||
"version": "2.3.4",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ArrowUpCircle, X, ExternalLink } from 'lucide-react';
|
||||
import { api } from '../../config/api';
|
||||
|
||||
interface UpdateInfo {
|
||||
enabled: boolean;
|
||||
current: string;
|
||||
channel: 'stable' | 'beta';
|
||||
latest: {
|
||||
stable: string;
|
||||
beta: string;
|
||||
forChannel: string;
|
||||
};
|
||||
updateAvailable: boolean;
|
||||
newerBetaAvailable?: boolean;
|
||||
lastChecked: string;
|
||||
error?: string;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
async function fetchUpdateInfo(): Promise<UpdateInfo> {
|
||||
const response = await api.get<UpdateInfo>('/admin/system/updates');
|
||||
return response.data;
|
||||
}
|
||||
|
||||
interface UpdateNotificationProps {
|
||||
onDismiss?: () => void;
|
||||
}
|
||||
|
||||
export const UpdateNotification: React.FC<UpdateNotificationProps> = ({ onDismiss }) => {
|
||||
const { t } = useTranslation();
|
||||
const [dismissed, setDismissed] = useState(false);
|
||||
|
||||
const { data: updateInfo } = useQuery({
|
||||
queryKey: ['update-check'],
|
||||
queryFn: fetchUpdateInfo,
|
||||
staleTime: 60 * 60 * 1000, // 1 hour
|
||||
retry: false,
|
||||
refetchOnWindowFocus: false
|
||||
});
|
||||
|
||||
// Don't render if no update available, not enabled, or dismissed
|
||||
if (!updateInfo?.enabled || !updateInfo?.updateAvailable || dismissed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleDismiss = () => {
|
||||
setDismissed(true);
|
||||
onDismiss?.();
|
||||
};
|
||||
|
||||
const channelLabel = updateInfo.channel === 'beta'
|
||||
? t('admin.updates.channelBeta', 'Beta')
|
||||
: t('admin.updates.channelStable', 'Stable');
|
||||
|
||||
return (
|
||||
<div className="bg-blue-50 border-l-4 border-blue-500 p-4 mb-4 rounded-r-lg">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-start">
|
||||
<ArrowUpCircle className="w-5 h-5 text-blue-500 mt-0.5 mr-3 flex-shrink-0" />
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold text-blue-800">
|
||||
{t('admin.updates.available', 'Update Available')}
|
||||
</h4>
|
||||
<p className="text-sm text-blue-700 mt-1">
|
||||
{t('admin.updates.newVersion', 'Version {{version}} is available', {
|
||||
version: updateInfo.latest.forChannel
|
||||
})}
|
||||
<span className="text-blue-500 ml-2">
|
||||
({t('admin.updates.currentVersion', 'Current: {{version}}', {
|
||||
version: updateInfo.current
|
||||
})})
|
||||
</span>
|
||||
</p>
|
||||
<p className="text-xs text-blue-600 mt-1">
|
||||
{t('admin.updates.channel', 'Channel: {{channel}}', {
|
||||
channel: channelLabel
|
||||
})}
|
||||
</p>
|
||||
<a
|
||||
href="https://github.com/the-luap/picpeak/releases"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center text-xs text-blue-600 hover:text-blue-800 mt-2"
|
||||
>
|
||||
{t('admin.updates.viewReleaseNotes', 'View Release Notes')}
|
||||
<ExternalLink className="w-3 h-3 ml-1" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleDismiss}
|
||||
className="text-blue-400 hover:text-blue-600 p-1"
|
||||
aria-label={t('common.close', 'Close')}
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Info } from 'lucide-react';
|
||||
import { Info, ArrowUpCircle } from 'lucide-react';
|
||||
import { api } from '../../config/api';
|
||||
import packageJson from '../../../package.json';
|
||||
|
||||
@@ -13,6 +13,15 @@ interface SystemVersion {
|
||||
frontend: string;
|
||||
node: string;
|
||||
environment: string;
|
||||
channel?: 'stable' | 'beta';
|
||||
}
|
||||
|
||||
interface UpdateInfo {
|
||||
enabled: boolean;
|
||||
updateAvailable: boolean;
|
||||
latest?: {
|
||||
forChannel: string;
|
||||
};
|
||||
}
|
||||
|
||||
async function fetchSystemVersion(): Promise<SystemVersion> {
|
||||
@@ -20,6 +29,11 @@ async function fetchSystemVersion(): Promise<SystemVersion> {
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function fetchUpdateInfo(): Promise<UpdateInfo> {
|
||||
const response = await api.get<UpdateInfo>('/admin/system/updates');
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export const VersionInfo: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { data: versionInfo } = useQuery({
|
||||
@@ -28,11 +42,25 @@ export const VersionInfo: React.FC = () => {
|
||||
staleTime: 5 * 60 * 1000, // Cache for 5 minutes
|
||||
});
|
||||
|
||||
const { data: updateInfo } = useQuery({
|
||||
queryKey: ['update-check'],
|
||||
queryFn: fetchUpdateInfo,
|
||||
staleTime: 60 * 60 * 1000, // 1 hour
|
||||
retry: false
|
||||
});
|
||||
|
||||
const channelBadge = versionInfo?.channel === 'beta' ? (
|
||||
<span className="ml-1 px-1.5 py-0.5 text-xs bg-amber-100 text-amber-700 rounded">
|
||||
{t('admin.updates.beta', 'BETA')}
|
||||
</span>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<div className="px-4 py-3 border-t border-neutral-200">
|
||||
<div className="flex items-center gap-2 text-xs text-neutral-600">
|
||||
<Info className="w-3 h-3" />
|
||||
<span className="font-medium">{t('admin.version')}</span>
|
||||
{channelBadge}
|
||||
</div>
|
||||
<div className="mt-1 space-y-0.5 text-xs text-neutral-500">
|
||||
<div>Frontend: v{FRONTEND_VERSION}</div>
|
||||
@@ -40,6 +68,16 @@ export const VersionInfo: React.FC = () => {
|
||||
<div>Backend: v{versionInfo.backend}</div>
|
||||
)}
|
||||
</div>
|
||||
{updateInfo?.enabled && updateInfo?.updateAvailable && (
|
||||
<div className="mt-2 flex items-center gap-1 text-xs text-blue-600">
|
||||
<ArrowUpCircle className="w-3 h-3" />
|
||||
<span>
|
||||
{t('admin.updates.updateAvailableShort', 'v{{version}} available', {
|
||||
version: updateInfo.latest?.forChannel
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
resolveSlugFromRequestUrl,
|
||||
} from '../../utils/galleryAuthStorage';
|
||||
|
||||
interface AuthenticatedImageProps extends React.ImgHTMLAttributes<HTMLImageElement> {
|
||||
interface AuthenticatedImageProps extends Omit<React.ImgHTMLAttributes<HTMLImageElement>, 'onLoad'> {
|
||||
src: string;
|
||||
fallbackSrc?: string;
|
||||
useWatermark?: boolean;
|
||||
@@ -29,6 +29,7 @@ interface AuthenticatedImageProps extends React.ImgHTMLAttributes<HTMLImageEleme
|
||||
detectDevTools?: boolean;
|
||||
protectionLevel?: 'basic' | 'standard' | 'enhanced' | 'maximum';
|
||||
useEnhancedProtection?: boolean;
|
||||
onLoad?: () => void;
|
||||
}
|
||||
|
||||
export const AuthenticatedImage: React.FC<AuthenticatedImageProps> = ({
|
||||
@@ -54,6 +55,7 @@ export const AuthenticatedImage: React.FC<AuthenticatedImageProps> = ({
|
||||
detectDevTools,
|
||||
protectionLevel,
|
||||
useEnhancedProtection,
|
||||
onLoad,
|
||||
...props
|
||||
}) => {
|
||||
const unusedProps = {
|
||||
@@ -221,6 +223,7 @@ export const AuthenticatedImage: React.FC<AuthenticatedImageProps> = ({
|
||||
img.onload = () => {
|
||||
imageRef.current = img;
|
||||
drawToCanvas();
|
||||
onLoad?.();
|
||||
};
|
||||
|
||||
img.onerror = (e) => {
|
||||
@@ -235,7 +238,7 @@ export const AuthenticatedImage: React.FC<AuthenticatedImageProps> = ({
|
||||
img.onload = null;
|
||||
img.onerror = null;
|
||||
};
|
||||
}, [imageSrc, useCanvasRendering, drawToCanvas]);
|
||||
}, [imageSrc, useCanvasRendering, drawToCanvas, onLoad]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
@@ -282,5 +285,5 @@ export const AuthenticatedImage: React.FC<AuthenticatedImageProps> = ({
|
||||
);
|
||||
}
|
||||
|
||||
return <img src={imageSrc} alt={alt} {...props} />;
|
||||
return <img src={imageSrc} alt={alt} onLoad={onLoad} {...props} />;
|
||||
};
|
||||
|
||||
@@ -366,7 +366,10 @@ export const GalleryLayout: React.FC<GalleryLayoutProps> = ({
|
||||
className={`${heroLogoSize.className} w-auto object-contain mx-auto`}
|
||||
style={{
|
||||
...(heroLogoSize.style || {}),
|
||||
filter: 'brightness(0) invert(1) drop-shadow(0 2px 4px rgba(0, 0, 0, 0.3))'
|
||||
// Only apply brightness/invert filter to default logo; custom logos display as-is
|
||||
filter: brandingSettings?.logo_url
|
||||
? 'drop-shadow(0 2px 4px rgba(0, 0, 0, 0.3))'
|
||||
: 'brightness(0) invert(1) drop-shadow(0 2px 4px rgba(0, 0, 0, 0.3))'
|
||||
}}
|
||||
/>
|
||||
{shouldShowCompanyName() && brandingSettings?.company_name && (
|
||||
@@ -441,7 +444,7 @@ export const GalleryLayout: React.FC<GalleryLayoutProps> = ({
|
||||
</p>
|
||||
)}
|
||||
<p className="text-xs sm:text-sm text-neutral-500">
|
||||
{brandingSettings?.footer_text || '© 2024 Your Company. All rights reserved.'}
|
||||
{brandingSettings?.footer_text || `© ${new Date().getFullYear()}${brandingSettings?.company_name ? ` ${brandingSettings.company_name}` : ''}. All rights reserved.`}
|
||||
{!brandingSettings?.hide_powered_by && (
|
||||
<> | Powered by <span className="font-semibold">PicPeak</span></>
|
||||
)}
|
||||
|
||||
@@ -203,7 +203,7 @@ export const GalleryView: React.FC<GalleryViewProps> = ({ slug, event }) => {
|
||||
company_name: settingsData.branding_company_name || '',
|
||||
company_tagline: settingsData.branding_company_tagline || '',
|
||||
support_email: settingsData.branding_support_email || '',
|
||||
footer_text: settingsData.branding_footer_text || '© 2024 Your Company. All rights reserved.',
|
||||
footer_text: settingsData.branding_footer_text || '',
|
||||
watermark_enabled: settingsData.branding_watermark_enabled || false,
|
||||
logo_url: settingsData.branding_logo_url || null,
|
||||
logo_size: settingsData.branding_logo_size || 'medium',
|
||||
@@ -380,12 +380,12 @@ export const GalleryView: React.FC<GalleryViewProps> = ({ slug, event }) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Transform URLs for watermarks if enabled
|
||||
// Transform full-size URLs for watermarks if enabled
|
||||
// Note: Thumbnails are watermarked server-side at the thumbnail endpoint
|
||||
if (watermarkEnabled) {
|
||||
photos = photos.map(photo => ({
|
||||
...photo,
|
||||
url: `/gallery/${slug}/photo/${photo.id}`,
|
||||
thumbnail_url: `/gallery/${slug}/photo/${photo.id}` // Use watermarked version for thumbnails too
|
||||
url: `/api/gallery/${slug}/photo/${photo.id}`
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useDevToolsProtection } from '../../hooks/useDevToolsProtection';
|
||||
import { X, ChevronLeft, ChevronRight, Download, ZoomIn, ZoomOut, MessageSquare, Heart, Star } from 'lucide-react';
|
||||
import { X, ChevronLeft, ChevronRight, Download, ZoomIn, ZoomOut, MessageSquare, Heart, Star, Loader2 } from 'lucide-react';
|
||||
import type { Photo } from '../../types';
|
||||
import { useDownloadPhoto } from '../../hooks/useGallery';
|
||||
import { AuthenticatedImage } from '../common';
|
||||
@@ -62,12 +62,18 @@ export const PhotoLightbox: React.FC<PhotoLightboxProps> = ({
|
||||
const [savedIdentity, setSavedIdentity] = useState<{ name: string; email: string } | null>(null);
|
||||
const [showIdentityModal, setShowIdentityModal] = useState(false);
|
||||
const [pendingAction, setPendingAction] = useState<null | { type: 'like' | 'rating'; rating?: number }>(null);
|
||||
const [imageLoaded, setImageLoaded] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const onResize = () => setIsSmallScreen(window.innerWidth < 640);
|
||||
window.addEventListener('resize', onResize);
|
||||
return () => window.removeEventListener('resize', onResize);
|
||||
}, []);
|
||||
|
||||
// Reset image loaded state when changing photos
|
||||
useEffect(() => {
|
||||
setImageLoaded(false);
|
||||
}, [currentIndex]);
|
||||
|
||||
const downloadPhotoMutation = useDownloadPhoto();
|
||||
const currentPhoto = photos[currentIndex];
|
||||
@@ -488,6 +494,13 @@ export const PhotoLightbox: React.FC<PhotoLightboxProps> = ({
|
||||
right: isDesktopFeedback ? `${desktopFeedbackWidth}px` : 0,
|
||||
}}
|
||||
>
|
||||
{/* Loading spinner */}
|
||||
{!imageLoaded && currentPhoto.media_type !== 'video' && (
|
||||
<div className="absolute inset-0 flex items-center justify-center z-10">
|
||||
<Loader2 className="w-12 h-12 text-white animate-spin" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{currentPhoto.media_type === 'video' ? (
|
||||
<VideoPlayer
|
||||
src={currentPhoto.url}
|
||||
@@ -505,8 +518,10 @@ export const PhotoLightbox: React.FC<PhotoLightboxProps> = ({
|
||||
style={{
|
||||
transform: `scale(${zoom}) translate(${dragOffset.x / zoom}px, ${dragOffset.y / zoom}px)`,
|
||||
transition: isDragging ? 'none' : 'transform 0.2s',
|
||||
opacity: imageLoaded ? 1 : 0,
|
||||
}}
|
||||
draggable={false}
|
||||
onLoad={() => setImageLoaded(true)}
|
||||
useWatermark={useEnhancedProtection}
|
||||
watermarkText={useEnhancedProtection ? `${currentPhoto.filename} - Protected` : undefined}
|
||||
isGallery={true}
|
||||
@@ -524,7 +539,7 @@ export const PhotoLightbox: React.FC<PhotoLightboxProps> = ({
|
||||
detectDevTools={protectionLevel === 'enhanced' || protectionLevel === 'maximum'}
|
||||
onProtectionViolation={(violationType) => {
|
||||
console.warn(`Protection violation in lightbox for photo ${currentPhoto.id}: ${violationType}`);
|
||||
|
||||
|
||||
// Track analytics
|
||||
if (typeof window !== 'undefined' && (window as any).umami) {
|
||||
(window as any).umami.track('lightbox_protection_violation', {
|
||||
@@ -534,7 +549,7 @@ export const PhotoLightbox: React.FC<PhotoLightboxProps> = ({
|
||||
zoom
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// For maximum protection, close lightbox on violation
|
||||
if (protectionLevel === 'maximum' &&
|
||||
['devtools_detected', 'print_screen_detected', 'canvas_access_blocked'].includes(violationType)) {
|
||||
|
||||
@@ -142,8 +142,11 @@ export const HeroGalleryLayout: React.FC<HeroGalleryLayoutProps> = ({
|
||||
}
|
||||
alt="Event logo"
|
||||
className="h-20 sm:h-24 lg:h-32 mx-auto"
|
||||
style={{
|
||||
filter: 'brightness(0) invert(1) drop-shadow(0 4px 6px rgba(0, 0, 0, 0.5))'
|
||||
style={{
|
||||
// Only apply brightness/invert filter to default logo; custom logos display as-is
|
||||
filter: eventLogo
|
||||
? 'drop-shadow(0 4px 6px rgba(0, 0, 0, 0.5))'
|
||||
: 'brightness(0) invert(1) drop-shadow(0 4px 6px rgba(0, 0, 0, 0.5))'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -778,6 +778,10 @@
|
||||
"enableDevtoolsProtection": "Entwicklertools erkennen",
|
||||
"useCanvasRendering": "Canvas-Rendering (erweiterter Schutz)",
|
||||
"protectionInfo": "Schutzfunktionen helfen, unerlaubte Downloads zu verhindern, können jedoch nicht alle Methoden blockieren.",
|
||||
"protectionLevelBasic": "Einfach - Nur Rechtsklick-Sperre",
|
||||
"protectionLevelStandard": "Standard - Tastenkombinationen blockiert",
|
||||
"protectionLevelEnhanced": "Erweitert - Bildschirmfoto-Erkennung",
|
||||
"protectionLevelMaximum": "Maximum - DevTools-Erkennung & Canvas-Rendering",
|
||||
"heroPhoto": "Hero-Foto",
|
||||
"heroPhotoHelp": "Wählen Sie ein hervorgehobenes Foto für das Hero-Galerie-Layout",
|
||||
"selectHeroPhoto": "Hero-Foto auswählen",
|
||||
@@ -1032,6 +1036,48 @@
|
||||
"saveSettings": "Sicherheitseinstellungen speichern",
|
||||
"saveSecuritySettings": "Sicherheitseinstellungen speichern"
|
||||
},
|
||||
"events": {
|
||||
"title": "Veranstaltungserstellung",
|
||||
"requiredFields": "Pflichtfelder",
|
||||
"requiredFieldsDescription": "Konfigurieren Sie, welche Kontaktfelder beim Erstellen neuer Veranstaltungen erforderlich sind.",
|
||||
"requireCustomerName": "Kundenname erforderlich",
|
||||
"requireCustomerNameHelp": "Kundenname muss für neue Veranstaltungen angegeben werden",
|
||||
"requireCustomerEmail": "Kunden-E-Mail erforderlich",
|
||||
"requireCustomerEmailHelp": "Kunden-E-Mail muss für neue Veranstaltungen angegeben werden",
|
||||
"customerEmailWarning": "Erforderlich für das Versenden von Galerie-Einladungen",
|
||||
"requireAdminEmail": "Admin-E-Mail erforderlich",
|
||||
"requireAdminEmailHelp": "Admin-E-Mail muss für neue Veranstaltungen angegeben werden",
|
||||
"adminEmailWarning": "Erforderlich für den Erhalt von Veranstaltungsbenachrichtigungen",
|
||||
"saveSettings": "Veranstaltungseinstellungen speichern",
|
||||
"noteTitle": "Hinweis",
|
||||
"noteText": "Diese Einstellungen betreffen nur die Erstellung neuer Veranstaltungen. Bestehende Veranstaltungen sind nicht betroffen. Standardmäßig sind alle Felder erforderlich."
|
||||
},
|
||||
"imageSecurity": {
|
||||
"title": "Bildschutz",
|
||||
"saveSuccess": "Bildschutz-Einstellungen gespeichert",
|
||||
"saveError": "Fehler beim Speichern der Einstellungen",
|
||||
"loadError": "Fehler beim Laden der Bildschutz-Einstellungen",
|
||||
"defaultProtection": "Standard-Schutzeinstellungen",
|
||||
"defaultProtectionHelp": "Diese Einstellungen gelten für alle neuen Veranstaltungen. Einzelne Veranstaltungen können diese Standardwerte überschreiben.",
|
||||
"protectionLevel": "Standard-Schutzstufe",
|
||||
"imageQuality": "Standard-Bildqualität",
|
||||
"fragmentationLevel": "Fragmentierungsstufe",
|
||||
"enableDevtools": "DevTools-Erkennung standardmäßig aktivieren",
|
||||
"enableCanvas": "Canvas-Rendering standardmäßig aktivieren (erweiterter Schutz)",
|
||||
"rateLimiting": "Ratenbegrenzung",
|
||||
"rateLimitingHelp": "Begrenzen Sie, wie viele Bilder angefordert werden können, um Scraping zu verhindern.",
|
||||
"requestsPerMinute": "Anfragen pro Minute",
|
||||
"requestsPer5Minutes": "Anfragen pro 5 Min.",
|
||||
"requestsPerHour": "Anfragen pro Stunde",
|
||||
"securityMonitoring": "Sicherheitsüberwachung",
|
||||
"suspiciousThreshold": "Schwellenwert für verdächtige Aktivitäten",
|
||||
"autoBlockThreshold": "Automatische Sperrschwelle",
|
||||
"enableMonitoring": "Sicherheitsüberwachung aktivieren",
|
||||
"blockSuspiciousIps": "Verdächtige IPs automatisch blockieren",
|
||||
"logEvents": "Sicherheitsereignisse in Datenbank protokollieren",
|
||||
"infoTitle": "Über Bildschutz",
|
||||
"infoText": "Diese Schutzfunktionen helfen, gelegentliches Herunterladen und Kopieren zu verhindern, können aber nicht alle Methoden blockieren. Entschlossene Benutzer finden möglicherweise trotzdem Wege, Bilder zu erfassen. Erwägen Sie die Verwendung von Wasserzeichen und rechtlichen Vereinbarungen für umfassenden Schutz."
|
||||
},
|
||||
"categories": {
|
||||
"title": "Kategorien",
|
||||
"about": "Über Fotokategorien",
|
||||
@@ -1209,7 +1255,31 @@
|
||||
"customizeTheme": "Design anpassen",
|
||||
"saveTheme": "Design speichern",
|
||||
"previewLayout": "Vorschau-Layout",
|
||||
"livePreview": "Live-Vorschau"
|
||||
"livePreview": "Live-Vorschau",
|
||||
"whiteLabel": "White Label",
|
||||
"hidePoweredBy": "\"Powered by PicPeak\" Branding ausblenden",
|
||||
"hidePoweredByHelp": "Entfernen Sie die PicPeak-Kennzeichnung aus Galerie-Fußzeilen für ein vollständig personalisiertes Erscheinungsbild",
|
||||
"logoCustomization": "Logo-Anpassung",
|
||||
"changeLogo": "Logo ändern",
|
||||
"logoSizeSmall": "Klein (32px)",
|
||||
"logoSizeMedium": "Mittel (48px)",
|
||||
"logoSizeLarge": "Groß (64px)",
|
||||
"logoSizeXLarge": "Extra Groß (96px)",
|
||||
"logoSizeCustom": "Benutzerdefiniert",
|
||||
"logoMaxHeight": "Maximale Höhe (Pixel)",
|
||||
"logoMaxHeightHelp": "Legen Sie eine benutzerdefinierte maximale Höhe für das Logo fest (20-200 Pixel)",
|
||||
"logoPosition": "Logo-Position im Header",
|
||||
"positionLeft": "Links",
|
||||
"positionCenter": "Mitte",
|
||||
"positionRight": "Rechts",
|
||||
"logoDisplayMode": "Anzeigemodus",
|
||||
"logoOnly": "Nur Logo",
|
||||
"textOnly": "Nur Firmenname",
|
||||
"logoAndText": "Logo und Firmenname",
|
||||
"showLogoInHeader": "Logo im Galerie-Header anzeigen",
|
||||
"showLogoInHeaderHelp": "Logo in der Hauptkopfzeile anzeigen",
|
||||
"showLogoInHero": "Logo im Hero-Bereich anzeigen",
|
||||
"showLogoInHeroHelp": "Logo in Hero-Bereichen anzeigen (für Nicht-Raster-Layouts)"
|
||||
},
|
||||
"admin": {
|
||||
"title": "Admin-Panel",
|
||||
@@ -1232,6 +1302,20 @@
|
||||
"error": "Fehler",
|
||||
"checking": "Prüfe..."
|
||||
},
|
||||
"updates": {
|
||||
"available": "Update verfügbar",
|
||||
"newVersion": "Version {{version}} ist verfügbar",
|
||||
"currentVersion": "Aktuell: {{version}}",
|
||||
"channel": "Kanal: {{channel}}",
|
||||
"channelStable": "Stabil",
|
||||
"channelBeta": "Beta",
|
||||
"beta": "BETA",
|
||||
"viewReleaseNotes": "Versionshinweise anzeigen",
|
||||
"updateAvailableShort": "v{{version}} verfügbar",
|
||||
"checkForUpdates": "Nach Updates suchen",
|
||||
"upToDate": "Alles aktuell",
|
||||
"lastChecked": "Zuletzt geprüft: {{time}}"
|
||||
},
|
||||
"notifications": "Benachrichtigungen",
|
||||
"viewAllNotifications": "Alle Benachrichtigungen anzeigen",
|
||||
"noNotifications": "Keine neuen Benachrichtigungen",
|
||||
|
||||
@@ -452,6 +452,10 @@
|
||||
"enableDevtoolsProtection": "Detect developer tools",
|
||||
"useCanvasRendering": "Canvas rendering (advanced protection)",
|
||||
"protectionInfo": "Protection features help prevent unauthorized downloads but cannot block all methods.",
|
||||
"protectionLevelBasic": "Basic - Right-click blocking only",
|
||||
"protectionLevelStandard": "Standard - Keyboard shortcuts blocked",
|
||||
"protectionLevelEnhanced": "Enhanced - Print screen detection",
|
||||
"protectionLevelMaximum": "Maximum - DevTools detection & canvas rendering",
|
||||
"heroPhoto": "Hero Photo",
|
||||
"heroPhotoHelp": "Select a featured photo for the hero gallery layout",
|
||||
"selectHeroPhoto": "Select Hero Photo",
|
||||
@@ -1014,7 +1018,31 @@
|
||||
"customizeTheme": "Customize Theme",
|
||||
"saveTheme": "Save Theme",
|
||||
"previewLayout": "Preview Layout",
|
||||
"livePreview": "Live Preview"
|
||||
"livePreview": "Live Preview",
|
||||
"whiteLabel": "White Label",
|
||||
"hidePoweredBy": "Hide \"Powered by PicPeak\" branding",
|
||||
"hidePoweredByHelp": "Remove the PicPeak attribution from gallery footers for a fully white-labeled experience",
|
||||
"logoCustomization": "Logo Customization",
|
||||
"changeLogo": "Change Logo",
|
||||
"logoSizeSmall": "Small (32px)",
|
||||
"logoSizeMedium": "Medium (48px)",
|
||||
"logoSizeLarge": "Large (64px)",
|
||||
"logoSizeXLarge": "Extra Large (96px)",
|
||||
"logoSizeCustom": "Custom",
|
||||
"logoMaxHeight": "Maximum Height (pixels)",
|
||||
"logoMaxHeightHelp": "Set a custom maximum height for the logo (20-200 pixels)",
|
||||
"logoPosition": "Logo Position in Header",
|
||||
"positionLeft": "Left",
|
||||
"positionCenter": "Center",
|
||||
"positionRight": "Right",
|
||||
"logoDisplayMode": "Display Mode",
|
||||
"logoOnly": "Logo Only",
|
||||
"textOnly": "Company Name Only",
|
||||
"logoAndText": "Logo and Company Name",
|
||||
"showLogoInHeader": "Show logo in gallery header",
|
||||
"showLogoInHeaderHelp": "Display the logo in the main header bar",
|
||||
"showLogoInHero": "Show logo in hero section",
|
||||
"showLogoInHeroHelp": "Display the logo in hero sections (for non-grid layouts)"
|
||||
},
|
||||
"admin": {
|
||||
"title": "Admin Panel",
|
||||
@@ -1037,6 +1065,20 @@
|
||||
"error": "Error",
|
||||
"checking": "Checking..."
|
||||
},
|
||||
"updates": {
|
||||
"available": "Update Available",
|
||||
"newVersion": "Version {{version}} is available",
|
||||
"currentVersion": "Current: {{version}}",
|
||||
"channel": "Channel: {{channel}}",
|
||||
"channelStable": "Stable",
|
||||
"channelBeta": "Beta",
|
||||
"beta": "BETA",
|
||||
"viewReleaseNotes": "View Release Notes",
|
||||
"updateAvailableShort": "v{{version}} available",
|
||||
"checkForUpdates": "Check for Updates",
|
||||
"upToDate": "You're up to date",
|
||||
"lastChecked": "Last checked: {{time}}"
|
||||
},
|
||||
"notifications": "Notifications",
|
||||
"viewAllNotifications": "View all notifications",
|
||||
"noNotifications": "No new notifications",
|
||||
|
||||
@@ -17,6 +17,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useLocalizedDate } from '../../hooks/useLocalizedDate';
|
||||
|
||||
import { Button, Card, Loading } from '../../components/common';
|
||||
import { UpdateNotification } from '../../components/admin/UpdateNotification';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { eventsService } from '../../services/events.service';
|
||||
import { adminService } from '../../services/admin.service';
|
||||
@@ -140,6 +141,9 @@ export const AdminDashboard: React.FC = () => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Update Notification */}
|
||||
<UpdateNotification />
|
||||
|
||||
{/* Page Header */}
|
||||
<div className="flex justify-between items-center mb-8">
|
||||
<div>
|
||||
|
||||
@@ -15,7 +15,7 @@ export const BrandingPage: React.FC = () => {
|
||||
const [brandingSettings, setBrandingSettings] = useState<BrandingSettings>({
|
||||
company_name: '',
|
||||
company_tagline: '',
|
||||
footer_text: '© 2024 Your Company. All rights reserved.',
|
||||
footer_text: '',
|
||||
support_email: '',
|
||||
watermark_enabled: false,
|
||||
watermark_position: 'bottom-right',
|
||||
@@ -307,7 +307,7 @@ export const BrandingPage: React.FC = () => {
|
||||
onChange={(e) => handleBrandingChange('footer_text', e.target.value)}
|
||||
className="w-full px-3 py-2 border border-neutral-300 rounded-lg focus:ring-2 focus:ring-primary-500"
|
||||
rows={2}
|
||||
placeholder="© 2024 Your Company. All rights reserved."
|
||||
placeholder={`© ${new Date().getFullYear()} Your Company. All rights reserved.`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -144,7 +144,7 @@ export const LegalPage: React.FC = () => {
|
||||
</Link>
|
||||
</div>
|
||||
<p className="text-sm text-neutral-500 mt-4">
|
||||
© 2024 PicPeak. All rights reserved.
|
||||
© {new Date().getFullYear()} PicPeak. All rights reserved.
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@@ -12,10 +12,10 @@ interface PhotoUrlOptions {
|
||||
*/
|
||||
export function getPhotoUrl({ slug, photo, watermarkEnabled = false, token }: PhotoUrlOptions): string {
|
||||
if (watermarkEnabled && token) {
|
||||
// Use the watermarked photo endpoint
|
||||
return `/gallery/${slug}/photo/${photo.id}`;
|
||||
// Use the watermarked photo endpoint (needs /api prefix)
|
||||
return `/api/gallery/${slug}/photo/${photo.id}`;
|
||||
}
|
||||
|
||||
|
||||
// Use the static photo URL
|
||||
return photo.url;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json",
|
||||
"release-type": "simple",
|
||||
"bump-minor-pre-major": true,
|
||||
"bump-patch-for-minor-pre-major": true,
|
||||
"include-component-in-tag": false,
|
||||
"include-v-in-tag": true,
|
||||
"prerelease": true,
|
||||
"prerelease-type": "beta",
|
||||
"changelog-sections": [
|
||||
{ "type": "feat", "section": "Features", "hidden": false },
|
||||
{ "type": "fix", "section": "Bug Fixes", "hidden": false },
|
||||
{ "type": "perf", "section": "Performance Improvements", "hidden": false },
|
||||
{ "type": "revert", "section": "Reverts", "hidden": false },
|
||||
{ "type": "docs", "section": "Documentation", "hidden": false },
|
||||
{ "type": "style", "section": "Styles", "hidden": true },
|
||||
{ "type": "chore", "section": "Miscellaneous", "hidden": true },
|
||||
{ "type": "refactor", "section": "Code Refactoring", "hidden": true },
|
||||
{ "type": "test", "section": "Tests", "hidden": true },
|
||||
{ "type": "build", "section": "Build System", "hidden": true },
|
||||
{ "type": "ci", "section": "CI/CD", "hidden": true }
|
||||
],
|
||||
"packages": {
|
||||
".": {
|
||||
"release-type": "simple",
|
||||
"changelog-path": "CHANGELOG.md",
|
||||
"extra-files": [
|
||||
{
|
||||
"type": "json",
|
||||
"path": "backend/package.json",
|
||||
"jsonpath": "$.version"
|
||||
},
|
||||
{
|
||||
"type": "json",
|
||||
"path": "frontend/package.json",
|
||||
"jsonpath": "$.version"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,18 @@
|
||||
".": {
|
||||
"release-type": "simple",
|
||||
"changelog-path": "CHANGELOG.md",
|
||||
"extra-files": []
|
||||
"extra-files": [
|
||||
{
|
||||
"type": "json",
|
||||
"path": "backend/package.json",
|
||||
"jsonpath": "$.version"
|
||||
},
|
||||
{
|
||||
"type": "json",
|
||||
"path": "frontend/package.json",
|
||||
"jsonpath": "$.version"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user