Compare commits

..

25 Commits

Author SHA1 Message Date
Paul Nothaft 3798662722 Merge pull request #91 from the-luap/release-please--branches--main
Build and Push Docker Images / build-backend (push) Failing after 3m22s
Build and Push Docker Images / build-frontend (push) Failing after 3m0s
Build and Push Docker Images / summary (push) Waiting to run
chore(main): release 2.2.3
2026-01-08 18:26:58 +01:00
github-actions[bot] fa1397cb8c chore(main): release 2.2.3 2026-01-08 17:26:11 +00:00
Paul Nothaft cc1ddfd42c fix(nginx): Add Docker DNS resolver for Swarm/dynamic service discovery (v2.2.3)
Fixes 502 Bad Gateway on root path in Docker Swarm by adding DNS resolver
  configuration (127.0.0.11) and dynamic DNS resolution for all proxy_pass 
  directives. This ensures nginx resolves backend service IPs on each request 
  rather than caching them at startup.
2026-01-08 18:25:54 +01:00
Paul Nothaft 049837f9d6 fix(nginx): add Docker DNS resolver for Swarm/dynamic service discovery
- Add resolver 127.0.0.11 directive for Docker's internal DNS
- Use variable-based proxy_pass to force per-request DNS resolution
- Fix 502 Bad Gateway error on root path in Docker Swarm deployments

The issue was that nginx caches DNS lookups at startup, but in Docker
Swarm where service IPs can change dynamically, this caused stale DNS
entries leading to 502 errors for proxied requests.

Bumps version to 2.2.3
2026-01-08 16:25:05 +01:00
Paul Nothaft 29dc2a3cf1 Merge pull request #89 from the-luap/release-please--branches--main
Build and Push Docker Images / build-backend (push) Failing after 3m26s
Build and Push Docker Images / build-frontend (push) Failing after 3m23s
Build and Push Docker Images / summary (push) Waiting to run
chore(main): release 2.2.2
2026-01-08 15:53:44 +01:00
github-actions[bot] d2cba449b0 chore(main): release 2.2.2 2026-01-08 14:51:24 +00:00
Paul Nothaft e0bd19a74d fix: Align nginx backend port for production Docker deployments (v2.2.2) (#88)
Fix 502 Bad Gateway on root path in production Docker/Traefik deployments.

  - nginx.conf: backend:3001 → backend:3000 (matches production container port)
  - docker-compose.yml: align dev environment to use port 3000
  - Bump version to 2.2.2
2026-01-08 15:51:10 +01:00
Paul Nothaft 0ab8cbde7f chore: bump version to 2.2.2
Includes fix for nginx backend port alignment (3001 → 3000) that caused
502 errors on root path in production Docker deployments.
2026-01-08 15:46:41 +01:00
Paul Nothaft 3a8d53f492 fix: align backend port to 3000 across all configurations
The production docker-compose used port 3000 internally but nginx.conf
was hardcoded to port 3001, causing 502 errors on the root path (/).

Changes:
- Update nginx.conf to use backend:3000
- Update docker-compose.yml to use PORT=3000 for consistency
- Update port mapping and healthcheck to use port 3000
2026-01-08 15:28:35 +01:00
Paul Nothaft 804a964ba0 Merge pull request #87 from the-luap/release-please--branches--main
Build and Push Docker Images / build-backend (push) Failing after 2m58s
Build and Push Docker Images / build-frontend (push) Failing after 3m4s
Build and Push Docker Images / summary (push) Waiting to run
chore(main): release 2.2.1
2026-01-08 14:01:58 +01:00
github-actions[bot] d2cd1aa933 chore(main): release 2.2.1 2026-01-08 13:01:36 +00:00
Paul Nothaft d7ecf83d32 fix: Resolve branding display issues and invitation parsing errors (v2.2.1) (#86)
Fixes #84, Fixes #85
  - Fix uploads proxy routing in nginx and vite dev server
  - Fix logo/favicon state handling in BrandingPage
  - Fix invitation API response field transformation (snake_case → camelCase)  
  - Add hide_powered_by to public settings API
  - Mark Multiple Administrators as implemented in roadmap
  - Bump version to 2.2.1
2026-01-08 14:01:21 +01:00
Paul Nothaft ebb2ce6065 Merge branch 'main' into feature/multiple-administrators 2026-01-08 13:58:41 +01:00
Paul Nothaft 1931d73b60 fix: resolve branding display issues and invitation parsing errors
Fixes #84 - Logo and favicon not displaying on branding page and galleries
Fixes #85 - Invitations showing undefined expiresAt causing parseISO errors

Changes:
- Fix nginx.conf: Add ^~ modifier to /uploads location to prioritize proxy over static file matching
- Fix vite.config.ts: Add /uploads proxy for development environment
- Fix BrandingPage.tsx: Include logo_url from branding settings instead of expecting it from theme
- Fix adminUsers.js: Add transformInvitation() to convert snake_case DB fields to camelCase API response
- Fix publicSettings.js: Add branding_hide_powered_by to public settings API response
- Update README.md: Mark Multiple Administrators feature as implemented
- Bump version to 2.2.1
2026-01-08 13:56:02 +01:00
Paul Nothaft 0d5ce48dcc fix: handle legacy non-JSON logo paths when replacing logo
When uploading a new logo, the code tries to delete the old logo file.
This failed when the old path was stored as a raw path (legacy format)
instead of JSON-serialized. Added check to handle both formats.
2026-01-08 11:44:29 +01:00
Paul Nothaft 4872ef71f8 ci: only build ARM64 images for tagged releases
QEMU emulation of ARM64 on x86 GitHub runners is too slow and
unreliable for npm operations, causing builds to hang or crash
with "Illegal instruction" errors.

Changed platform detection logic to:
- Tagged releases (v*.*.*): Build both amd64 and arm64
- All other builds (branches, PRs): Build amd64 only

This ensures fast CI feedback during development while still
providing multi-arch images for production releases.
2026-01-08 11:33:20 +01:00
Paul Nothaft b83f4272b5 fix: JSON serialize favicon and logo URLs for PostgreSQL storage
Fixes #84

The favicon and logo upload endpoints were storing URL paths directly
without JSON.stringify(), causing PostgreSQL JSON validation errors
("Token '/' is invalid") since paths like "/uploads/favicons/..."
are not valid JSON.

Applied JSON.stringify() to:
- branding_logo_url setting (lines 358, 364)
- branding_favicon_url setting (lines 894, 900)
2026-01-08 11:29:38 +01:00
github-actions[bot] 5df64992c4 chore: sync package.json versions to 2.2.0 2026-01-08 09:28:03 +00:00
Paul Nothaft 7e5e004270 Merge pull request #83 from the-luap/release-please--branches--main
Build and Push Docker Images / build-backend (push) Failing after 3m5s
Build and Push Docker Images / build-frontend (push) Failing after 3m5s
Build and Push Docker Images / summary (push) Waiting to run
chore(main): release 2.2.0
2026-01-08 10:27:40 +01:00
github-actions[bot] 09ce2b80d0 chore(main): release 2.2.0 2026-01-08 09:26:48 +00:00
Paul Nothaft 476fcce13f fix: Add settings translations and fix manual backup process (#82)
- Add i18n translations for settings tabs (Events, Image Security, Moderation, CSS)
  - Fix manual backup when automated backups are disabled
  - Fix PostgreSQL wait-for-db.sh connection check
2026-01-08 10:26:34 +01:00
Paul Nothaft c030e87213 feat(i18n): add translations for settings tabs
- Add settings.events.* keys for Event Creation settings
- Add settings.imageSecurity.* keys for Image Protection settings
- Add settings.moderation.* keys for Word Filter/Moderation settings
- Add cssTemplates.* keys for Custom CSS Templates
- All settings tabs now have proper i18n support
2026-01-07 22:43:36 +01:00
Paul Nothaft e6dd89e969 fix(backup): allow manual backups when automated backups are disabled
- Manual backup button now works regardless of backup_enabled setting
- backup_enabled only controls scheduled/automated backups
- Manual backups only require destination to be configured
- Fixed backup_type to correctly show 'manual' vs 'scheduled'
2026-01-07 22:39:57 +01:00
Paul Nothaft e85a68a386 fix(db): improve PostgreSQL connection check in wait-for-db.sh
- Try connecting to target database first (most common case)
- Fall back to template1 instead of postgres database for checks
- The picpeak user may not have access to postgres system database
- Add better retry logic with max attempts
- Improve error messages
2026-01-07 22:33:40 +01:00
github-actions[bot] 0acce6ab08 chore: sync package.json versions to 2.1.1 2026-01-07 21:24:07 +00:00
16 changed files with 263 additions and 65 deletions
+12 -12
View File
@@ -45,14 +45,14 @@ jobs:
- name: Determine build platforms
id: platforms
run: |
# For PRs, build only amd64 to avoid QEMU emulation issues with Sharp
# For main/develop/tags, build multi-arch
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
echo "platforms=linux/amd64" >> $GITHUB_OUTPUT
echo "skip_qemu=true" >> $GITHUB_OUTPUT
else
# Only build ARM64 for tagged releases (v*.*.*)
# QEMU emulation is too slow/unreliable for npm operations on regular builds
if [[ "${{ github.ref }}" == refs/tags/v* ]]; then
echo "platforms=linux/amd64,linux/arm64" >> $GITHUB_OUTPUT
echo "skip_qemu=false" >> $GITHUB_OUTPUT
else
echo "platforms=linux/amd64" >> $GITHUB_OUTPUT
echo "skip_qemu=true" >> $GITHUB_OUTPUT
fi
- name: Set up QEMU
@@ -142,14 +142,14 @@ jobs:
- name: Determine build platforms
id: platforms
run: |
# For PRs, build only amd64 to avoid QEMU emulation issues
# For main/develop/tags, build multi-arch
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
echo "platforms=linux/amd64" >> $GITHUB_OUTPUT
echo "skip_qemu=true" >> $GITHUB_OUTPUT
else
# Only build ARM64 for tagged releases (v*.*.*)
# QEMU emulation is too slow/unreliable for npm operations on regular builds
if [[ "${{ github.ref }}" == refs/tags/v* ]]; then
echo "platforms=linux/amd64,linux/arm64" >> $GITHUB_OUTPUT
echo "skip_qemu=false" >> $GITHUB_OUTPUT
else
echo "platforms=linux/amd64" >> $GITHUB_OUTPUT
echo "skip_qemu=true" >> $GITHUB_OUTPUT
fi
- name: Set up QEMU
+1 -1
View File
@@ -1,3 +1,3 @@
{
".": "2.1.1"
".": "2.2.3"
}
+40
View File
@@ -5,6 +5,46 @@ 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.2.3](https://github.com/the-luap/picpeak/compare/v2.2.2...v2.2.3) (2026-01-08)
### Bug Fixes
* **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))
## [2.2.2](https://github.com/the-luap/picpeak/compare/v2.2.1...v2.2.2) (2026-01-08)
### Bug Fixes
* 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))
## [2.2.1](https://github.com/the-luap/picpeak/compare/v2.2.0...v2.2.1) (2026-01-08)
### Bug Fixes
* handle legacy non-JSON logo paths when replacing logo ([0d5ce48](https://github.com/the-luap/picpeak/commit/0d5ce48dccf0c61f210725ffae15dafc5e9f7cab))
* JSON serialize favicon and logo URLs for PostgreSQL storage ([b83f427](https://github.com/the-luap/picpeak/commit/b83f4272b584f937fea1f47656182e514b12d980))
* 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))
## [2.2.0](https://github.com/the-luap/picpeak/compare/v2.1.1...v2.2.0) (2026-01-08)
### Features
* **i18n:** add translations for settings tabs ([c030e87](https://github.com/the-luap/picpeak/commit/c030e872135b39701ef1f4bbb2f28bcaf4ce7fae))
### Bug Fixes
* 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))
* **backup:** allow manual backups when automated backups are disabled ([e6dd89e](https://github.com/the-luap/picpeak/commit/e6dd89e969fb7018633159155975bd2bd2fb0409))
* **db:** improve PostgreSQL connection check in wait-for-db.sh ([e85a68a](https://github.com/the-luap/picpeak/commit/e85a68a386c72c276b4958599b5246e60dfac716))
## [2.1.1](https://github.com/the-luap/picpeak/compare/v2.1.0...v2.1.1) (2026-01-07)
+1 -1
View File
@@ -248,7 +248,7 @@ These features are currently in beta testing and may have limited functionality
| **Face Recognition** | AI-powered face detection to help guests find their photos and create automatic person-based albums | Low | 🔄 Open |
| **Gallery Feedback** | Allow guests to like, rate, and comment on photos with admin notifications and moderation | Medium | ✅ Implemented |
| **Video Support** | Upload and display videos alongside photos in galleries with streaming support | Low | ✅ Implemented |
| **Multiple Administrators** | Support for multiple admin accounts with role-based permissions and activity tracking | Low | 📋 Planned |
| **Multiple Administrators** | Support for multiple admin accounts with role-based permissions and activity tracking | Low | ✅ Implemented |
| **Filtering & Export Options** | Filter photos by likes, ratings, comments, or favorites. Search by filename. Sort by date, name, size, or rating. Export filtered selections as ZIP or generate Capture One/Lightroom-compatible file lists for professional workflows | Medium | ✅ Implemented |
**Status Legend:** ✅ Implemented | 🚧 In Progress | 🔄 Open | 📋 Planned
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "picpeak-backend",
"version": "2.1.0",
"version": "2.2.3",
"description": "Backend for PicPeak event photo sharing platform",
"main": "server.js",
"scripts": {
+9 -5
View File
@@ -326,8 +326,12 @@ router.post('/logo', adminAuth, requirePermission('settings.edit'), upload.singl
.first();
if (oldLogoSetting && oldLogoSetting.setting_value) {
const oldPath = JSON.parse(oldLogoSetting.setting_value);
try {
// Handle both JSON-serialized and legacy raw path values
let oldPath = oldLogoSetting.setting_value;
if (oldPath.startsWith('"')) {
oldPath = JSON.parse(oldPath);
}
await fs.unlink(oldPath);
} catch (error) {
console.error('Failed to delete old logo:', error);
@@ -355,13 +359,13 @@ router.post('/logo', adminAuth, requirePermission('settings.edit'), upload.singl
await db('app_settings')
.insert({
setting_key: 'branding_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()
});
@@ -891,13 +895,13 @@ router.post('/favicon', adminAuth, requirePermission('settings.edit'), faviconUp
await db('app_settings')
.insert({
setting_key: 'branding_favicon_url',
setting_value: faviconUrl,
setting_value: JSON.stringify(faviconUrl),
setting_type: 'branding',
updated_at: new Date()
})
.onConflict('setting_key')
.merge({
setting_value: faviconUrl,
setting_value: JSON.stringify(faviconUrl),
updated_at: new Date()
});
+15 -1
View File
@@ -45,6 +45,20 @@ function transformRole(role) {
};
}
/**
* Transform invitation object from snake_case (DB) to camelCase (API)
*/
function transformInvitation(invitation) {
return {
id: invitation.id,
email: invitation.email,
expiresAt: invitation.expires_at,
createdAt: invitation.created_at,
roleName: invitation.role_name,
invitedBy: invitation.invited_by
};
}
/**
* GET /me/permissions
* Get current user's permissions
@@ -81,7 +95,7 @@ router.get('/roles', adminAuth, requirePermission('users.view'), handleAsync(asy
*/
router.get('/invitations', adminAuth, requirePermission('users.view'), handleAsync(async (req, res) => {
const invitations = await userManagementService.getPendingInvitations();
res.json({ invitations });
res.json({ invitations: invitations.map(transformInvitation) });
}));
/**
+1
View File
@@ -58,6 +58,7 @@ router.get('/', async (req, res) => {
branding_logo_display_header: settingsObject.branding_logo_display_header !== false,
branding_logo_display_hero: settingsObject.branding_logo_display_hero !== false,
branding_logo_display_mode: settingsObject.branding_logo_display_mode || 'logo_and_text',
branding_hide_powered_by: settingsObject.branding_hide_powered_by === true,
theme_config: settingsObject.theme_config || null,
default_language: settingsObject.general_default_language || 'en',
enable_analytics: settingsObject.general_enable_analytics !== false,
+14 -5
View File
@@ -710,7 +710,7 @@ async function saveManifestToS3(manifest, manifestFileName, config, result) {
return manifestPath;
}
async function runBackupInternal() {
async function runBackupInternal(isManual = false) {
if (isRunning) {
logger.warn('Backup already running, skipping');
return;
@@ -722,16 +722,25 @@ async function runBackupInternal() {
try {
const config = await resolveConfigWithFallback();
if (!config || !normalizeBoolean(config.backup_enabled)) {
logger.info('Backup is disabled, skipping');
// For scheduled backups, check if backup is enabled
// Manual backups should always be allowed (just need valid destination config)
if (!isManual && (!config || !normalizeBoolean(config.backup_enabled))) {
logger.info('Scheduled backup is disabled, skipping');
return;
}
// For manual backups, just ensure we have a destination configured
if (!config || !config.backup_destination_type) {
logger.warn('Backup destination not configured');
throw new Error('Backup destination not configured. Please configure backup settings first.');
}
const schemaVersion = await getCurrentSchemaVersion();
const insertResult = await db('backup_runs').insert({
started_at: startTime,
status: 'running',
backup_type: 'scheduled',
backup_type: isManual ? 'manual' : 'scheduled',
app_version: packageJson.version,
node_version: process.version,
db_schema_version: schemaVersion
@@ -928,7 +937,7 @@ function stopBackupService() {
async function triggerManualBackup() {
logger.info('Starting manual backup');
await service.runBackup();
await service.runBackup(true); // Pass flag to indicate manual backup
}
async function getBackupStatus(limit = 10) {
+35 -17
View File
@@ -7,7 +7,8 @@ host="${DB_HOST:-postgres}"
port="${DB_PORT:-5432}"
user="${DB_USER:-picpeak}"
target_db="${DB_NAME:-picpeak}"
default_db="${DB_CHECK_DB:-postgres}"
# Use target database for checks - the picpeak user may not have access to 'postgres' database
default_db="${DB_CHECK_DB:-$target_db}"
sanitize_identifier() {
printf '%s' "$1" | sed "s/'/''/g"
@@ -15,27 +16,44 @@ sanitize_identifier() {
echo "Waiting for PostgreSQL at $host:$port..."
# Wait for PostgreSQL server to accept connections (using the default database)
until PGPASSWORD="$DB_PASSWORD" psql -h "$host" -p "$port" -U "$user" -d "$default_db" -c '\q' >/dev/null 2>&1; do
>&2 echo "PostgreSQL is unavailable - sleeping"
# First, wait for PostgreSQL server to be reachable
max_attempts=30
attempt=0
while [ $attempt -lt $max_attempts ]; do
if PGPASSWORD="$DB_PASSWORD" psql -h "$host" -p "$port" -U "$user" -d "$target_db" -c '\q' >/dev/null 2>&1; then
>&2 echo "PostgreSQL is up - database \"$target_db\" is accessible."
break
fi
# If target DB doesn't work, try connecting to 'postgres' or 'template1' to create it
if PGPASSWORD="$DB_PASSWORD" psql -h "$host" -p "$port" -U "$user" -d "template1" -c '\q' >/dev/null 2>&1; then
>&2 echo "PostgreSQL is up - checking if database \"$target_db\" needs to be created..."
# Check if database exists
db_exists=$(PGPASSWORD="$DB_PASSWORD" psql -h "$host" -p "$port" -U "$user" -d "template1" -tAc "SELECT 1 FROM pg_database WHERE datname = '$(sanitize_identifier "$target_db")'" 2>/dev/null || echo 0)
if [ "$db_exists" != "1" ]; then
>&2 echo "Database \"$target_db\" not found. Attempting to create..."
if PGPASSWORD="$DB_PASSWORD" psql -h "$host" -p "$port" -U "$user" -d "template1" -c "CREATE DATABASE \"$target_db\";" >/dev/null 2>&1; then
>&2 echo "Database \"$target_db\" created successfully."
else
>&2 echo "Warning: Could not create database. It may already exist or user lacks permissions."
fi
fi
break
fi
attempt=$((attempt + 1))
>&2 echo "PostgreSQL is unavailable - sleeping (attempt $attempt/$max_attempts)"
sleep 2
done
>&2 echo "PostgreSQL is up - verifying target database \"$target_db\""
# Ensure the target database exists (helps when volumes are reused or DB_NAME is customised)
db_exists=$(PGPASSWORD="$DB_PASSWORD" psql -h "$host" -p "$port" -U "$user" -d "$default_db" -tAc "SELECT 1 FROM pg_database WHERE datname = '$(sanitize_identifier "$target_db")'" 2>/dev/null || echo 0)
if [ "$db_exists" != "1" ]; then
>&2 echo "Database \"$target_db\" not found. Attempting to create..."
if ! PGPASSWORD="$DB_PASSWORD" psql -h "$host" -p "$port" -U "$user" -d "$default_db" -c "CREATE DATABASE \"$target_db\";" >/dev/null 2>&1; then
>&2 echo "Failed to create database \"$target_db\". Please ensure it exists and is accessible."
exit 1
fi
>&2 echo "Database \"$target_db\" created successfully."
if [ $attempt -eq $max_attempts ]; then
>&2 echo "Failed to connect to PostgreSQL after $max_attempts attempts."
exit 1
fi
# Wait until the target database itself is ready to accept connections
# Final verification - wait for target database to accept connections
until PGPASSWORD="$DB_PASSWORD" psql -h "$host" -p "$port" -U "$user" -d "$target_db" -c '\q' >/dev/null 2>&1; do
>&2 echo "Waiting for database \"$target_db\" to accept connections..."
sleep 2
+3 -3
View File
@@ -7,7 +7,7 @@ services:
restart: unless-stopped
environment:
- NODE_ENV=${NODE_ENV:-production}
- PORT=3001
- PORT=3000
- JWT_SECRET=${JWT_SECRET}
- ADMIN_USERNAME=${ADMIN_USERNAME:-admin}
- ADMIN_EMAIL=${ADMIN_EMAIL:-admin@example.com}
@@ -43,12 +43,12 @@ services:
- ./backup:/backup
- ./storage:/app/storage
ports:
- "${BACKEND_PORT:-3001}:3001"
- "${BACKEND_PORT:-3001}:3000"
depends_on:
postgres:
condition: service_healthy
healthcheck:
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://127.0.0.1:3001/health"]
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://127.0.0.1:3000/health"]
interval: 30s
timeout: 10s
retries: 3
+22 -10
View File
@@ -4,6 +4,10 @@ server {
root /usr/share/nginx/html;
index index.html;
# Docker DNS resolver for dynamic service discovery (required for Swarm/Compose)
resolver 127.0.0.11 valid=10s ipv6=off;
resolver_timeout 5s;
# Allow larger file uploads (up to 100MB)
client_max_body_size 100M;
client_body_timeout 300s;
@@ -43,7 +47,9 @@ server {
# API proxy
location /api {
proxy_pass http://backend:3001;
# Use variable to force DNS resolution per request (required for Docker Swarm)
set $backend_upstream backend;
proxy_pass http://$backend_upstream:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
@@ -53,7 +59,7 @@ server {
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
proxy_read_timeout 86400;
# Allow larger uploads for API endpoints
client_max_body_size 100M;
client_body_timeout 300s;
@@ -61,13 +67,14 @@ server {
# Photo serving proxy
location /photos {
proxy_pass http://backend:3001;
set $backend_upstream backend;
proxy_pass http://$backend_upstream:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Cache photos
proxy_cache_valid 200 302 1d;
proxy_cache_valid 404 1m;
@@ -75,27 +82,30 @@ server {
# Thumbnail serving proxy
location /thumbnails {
proxy_pass http://backend:3001;
set $backend_upstream backend;
proxy_pass http://$backend_upstream:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Cache thumbnails
proxy_cache_valid 200 302 7d;
proxy_cache_valid 404 1m;
}
# Uploads serving proxy (logos, favicons, watermarks)
location /uploads {
proxy_pass http://backend:3001;
# ^~ modifier stops regex matching, ensuring uploads are proxied not served locally
location ^~ /uploads {
set $backend_upstream backend;
proxy_pass http://$backend_upstream:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Cache uploads
proxy_cache_valid 200 302 7d;
proxy_cache_valid 404 1m;
@@ -103,7 +113,9 @@ server {
# Delegate root requests to backend for public landing page handling
location = / {
proxy_pass http://backend:3001/;
# Use variable to force DNS resolution per request (required for Docker Swarm)
set $backend_upstream backend;
proxy_pass http://$backend_upstream:3000/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "picpeak-frontend",
"private": true,
"version": "2.1.0",
"version": "2.2.3",
"type": "module",
"scripts": {
"dev": "vite",
+95
View File
@@ -737,6 +737,80 @@
"failed": "Failed",
"lastUpdate": "Last update"
},
"events": {
"title": "Event Creation",
"requiredFields": "Required Fields",
"requiredFieldsDescription": "Configure which contact fields are required when creating new events.",
"requireCustomerName": "Require customer name",
"requireCustomerNameHelp": "Customer name must be provided for new events",
"requireCustomerEmail": "Require customer email",
"requireCustomerEmailHelp": "Customer email must be provided for new events",
"customerEmailWarning": "Required for sending gallery invitations",
"requireAdminEmail": "Require admin email",
"requireAdminEmailHelp": "Admin email must be provided for new events",
"adminEmailWarning": "Required for receiving event notifications",
"saveSettings": "Save Event Settings",
"noteTitle": "Note",
"noteText": "These settings only affect new event creation. Existing events are not affected. Default behavior requires all fields."
},
"imageSecurity": {
"title": "Image Protection",
"saveSuccess": "Image security settings saved",
"saveError": "Failed to save settings",
"loadError": "Failed to load image security settings",
"defaultProtection": "Default Protection Settings",
"defaultProtectionHelp": "These settings apply to all new events. Individual events can override these defaults.",
"protectionLevel": "Default Protection Level",
"imageQuality": "Default Image Quality",
"fragmentationLevel": "Fragmentation Level",
"enableDevtools": "Enable DevTools detection by default",
"enableCanvas": "Enable canvas rendering by default (advanced protection)",
"rateLimiting": "Rate Limiting",
"rateLimitingHelp": "Limit how many images can be requested to prevent scraping.",
"requestsPerMinute": "Requests per minute",
"requestsPer5Minutes": "Requests per 5 min",
"requestsPerHour": "Requests per hour",
"securityMonitoring": "Security Monitoring",
"suspiciousThreshold": "Suspicious activity threshold",
"autoBlockThreshold": "Auto-block threshold",
"enableMonitoring": "Enable security monitoring",
"blockSuspiciousIps": "Automatically block suspicious IPs",
"logEvents": "Log security events to database",
"infoTitle": "About Image Protection",
"infoText": "These protection features help prevent casual downloading and copying but cannot block all methods. Determined users may still find ways to capture images. Consider using watermarks and legal agreements for comprehensive protection."
},
"moderation": {
"title": "Moderation",
"wordFilters": "Word Filters",
"description": "Manage words that should be filtered or blocked in comments",
"addFilter": "Add New Filter",
"enterWord": "Enter word to filter",
"searchFilters": "Search filters...",
"filterAdded": "Word filter added successfully",
"filterExists": "This word filter already exists",
"addError": "Failed to add word filter",
"filterUpdated": "Word filter updated successfully",
"updateError": "Failed to update word filter",
"filterDeleted": "Word filter deleted successfully",
"deleteError": "Failed to delete word filter",
"wordRequired": "Please enter a word to filter",
"confirmDelete": "Are you sure you want to delete this word filter?",
"loading": "Loading word filters...",
"noMatchingFilters": "No matching filters found",
"noFilters": "No word filters configured yet",
"severityLow": "Low",
"severityModerate": "Moderate",
"severityHigh": "High",
"severityBlock": "Block",
"severityLevels": "Severity Levels",
"lowDescription": "Word is flagged for review but not automatically blocked",
"moderateDescription": "Comment requires manual approval before being visible",
"highDescription": "Comment is automatically hidden and requires admin review",
"blockDescription": "Comment is rejected immediately and cannot be submitted"
},
"styling": {
"title": "Custom CSS"
},
"analytics": {
"title": "Analytics",
"umamiIntegration": "Umami Analytics Integration",
@@ -1731,6 +1805,27 @@
"testEmailFailed": "Connection test failed"
}
},
"cssTemplates": {
"title": "Custom CSS Templates",
"template": "Template",
"templateName": "Template Name",
"enableTemplate": "Enable this template",
"enableHint": "Enabled templates can be selected when creating events",
"cssContent": "CSS Content",
"cssHint": "Use .gallery-page to scope styles to the gallery. Available variables: --gallery-bg, --gallery-text, --gallery-accent",
"securityNotice": "Security Notice",
"securityText": "CSS is sanitized to prevent malicious code. External URLs, @import, and JavaScript expressions are blocked.",
"resetToDefault": "Reset to Default",
"resetConfirm": "Reset this template to the default? Your changes will be lost.",
"unsavedChanges": "Unsaved changes",
"saveTemplate": "Save Template",
"lastUpdated": "Last updated",
"saved": "Template saved successfully",
"saveFailed": "Failed to save template",
"sanitizationWarning": "Some CSS patterns were blocked for security",
"reset": "Template reset to default",
"resetFailed": "Failed to reset template"
},
"maintenance": {
"title": "System Maintenance",
"message": "We're currently performing scheduled maintenance to improve our service. We'll be back online shortly.",
+9 -8
View File
@@ -81,9 +81,8 @@ export const BrandingPage: React.FC = () => {
useEffect(() => {
if (settings) {
const formatted = settingsService.formatBrandingSettings(settings);
// Don't set logo_url here - it will be synced from theme
const { logo_url, ...brandingWithoutLogo } = formatted;
setBrandingSettings(prev => ({ ...prev, ...brandingWithoutLogo }));
// Include logo_url from branding settings
setBrandingSettings(prev => ({ ...prev, ...formatted }));
}
}, [settings]);
@@ -91,15 +90,17 @@ export const BrandingPage: React.FC = () => {
useEffect(() => {
if (themeSettings) {
const formatted = settingsService.formatThemeSettings(themeSettings) as ThemeConfig;
if (formatted && Object.keys(formatted).length > 0) {
// Use the theme's logo URL as stored in the theme config
setCurrentTheme(formatted);
setTheme(formatted);
// Always sync the logo URL from theme to branding settings - theme is source of truth
setBrandingSettings(prev => ({ ...prev, logo_url: formatted.logoUrl || '' }));
// Only sync logo URL from theme if it exists there (logo is stored in branding settings)
if (formatted.logoUrl) {
setBrandingSettings(prev => ({ ...prev, logo_url: formatted.logoUrl }));
}
// Try to identify which preset this matches
for (const [key, preset] of Object.entries(GALLERY_THEME_PRESETS)) {
if (JSON.stringify(preset.config) === JSON.stringify(formatted)) {
+4
View File
@@ -36,6 +36,10 @@ const config: VitestUserConfig = {
target: 'http://localhost:7101',
changeOrigin: true,
},
'/uploads': {
target: 'http://localhost:7101',
changeOrigin: true,
},
},
}
}