Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7e5e004270 | |||
| 09ce2b80d0 | |||
| 476fcce13f | |||
| c030e87213 | |||
| e6dd89e969 | |||
| e85a68a386 | |||
| 0acce6ab08 |
@@ -1,3 +1,3 @@
|
||||
{
|
||||
".": "2.1.1"
|
||||
".": "2.2.0"
|
||||
}
|
||||
|
||||
@@ -5,6 +5,20 @@ 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.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,6 +1,6 @@
|
||||
{
|
||||
"name": "picpeak-backend",
|
||||
"version": "2.1.0",
|
||||
"version": "2.1.1",
|
||||
"description": "Backend for PicPeak event photo sharing platform",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "picpeak-frontend",
|
||||
"private": true,
|
||||
"version": "2.1.0",
|
||||
"version": "2.1.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
Reference in New Issue
Block a user