Compare commits

...

18 Commits

Author SHA1 Message Date
Gitea Actions Bot 95a6ad505d chore: bump version to 1.0.84 (backend + frontend)
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is failing
2025-07-23 07:36:39 +00:00
paul 9dc82c8490 fix: replace github-release plugin with direct curl API call
Mirror to GitHub / mirror (push) Successful in 37s
Test and Lint / backend-test (push) Successful in 1m30s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m7s
The github-release plugin was incorrectly detecting and using the
Gitea API instead of GitHub's API. Replaced with direct curl command
that explicitly calls GitHub API to create releases.

This approach:
- Uses curlimages/curl image for lightweight execution
- Directly calls GitHub API v3 with proper authentication
- Avoids any auto-detection issues from the plugin
- Creates releases with full markdown formatting

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-23 09:35:36 +02:00
paul 554bf1d43c CRITICAL FIX: prevent gallery pages redirecting to admin login
Mirror to GitHub / mirror (push) Successful in 48s
Test and Lint / backend-test (push) Successful in 1m39s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m14s
Version and Release / version-bump (push) Successful in 52s
Version and Release / trigger-drone (push) Successful in 3s
Users were being redirected from gallery pages to admin login due to
useLocalizedDate hook trying to fetch admin settings. Fixed by:

1. Added general_date_format to public settings endpoint
2. Created publicSettingsService for unauthenticated access
3. Updated useLocalizedDate to use public settings instead of admin
4. Fixed API interceptor to not redirect on public endpoint 401s
5. Added backups/ and test-archiver/ to .gitignore

This restores gallery access for all users.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-23 09:30:35 +02:00
Gitea Actions Bot f147bd6cfb chore: bump backend version to 1.0.83
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is failing
2025-07-23 07:13:22 +00:00
paul f564f40ed0 CRITICAL FIX: correct email_templates column names in migration 032
Mirror to GitHub / mirror (push) Successful in 34s
Test and Lint / backend-test (push) Successful in 1m28s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m4s
Version and Release / version-bump (push) Successful in 41s
Version and Release / trigger-drone (push) Successful in 3s
Production failing because email_templates table has different columns.
Fixed column names:
- name → template_key
- subject → subject_en, subject_de
- body → body_html_en, body_html_de, body_text_en, body_text_de
- Added missing 'variables' field
- Removed language and is_active fields (not in schema)

Also fixed the down() function to use template_key instead of name.

URGENT: Production is still down.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-23 09:08:13 +02:00
Gitea Actions Bot b50cc18045 chore: bump backend version to 1.0.82
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is failing
2025-07-23 06:55:08 +00:00
paul 9adb2b04eb fix: remove description field from migration 035 app_settings inserts
Mirror to GitHub / mirror (push) Successful in 35s
Test and Lint / backend-test (push) Successful in 1m22s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m6s
Version and Release / version-bump (push) Successful in 41s
Version and Release / trigger-drone (push) Successful in 3s
The app_settings table doesn't have a description column.
Removed all description fields to prevent migration failures.

This completes the fix for all app_settings inserts across migrations.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-23 08:49:56 +02:00
paul 5cce58c233 CRITICAL FIX: remove description field from app_settings inserts
Mirror to GitHub / mirror (push) Successful in 31s
Test and Lint / frontend-test (push) Has been cancelled
Test and Lint / backend-test (push) Has been cancelled
Version and Release / version-bump (push) Has been cancelled
Version and Release / trigger-drone (push) Has been cancelled
continuous-integration/drone/push Build is passing
Production failing with "column description does not exist" error.
The app_settings table only has: id, setting_key, setting_value, setting_type, updated_at
Removed all description fields from migration 032.

URGENT: Production is down - this is blocking the backend from starting.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-23 08:48:23 +02:00
Gitea Actions Bot c95784eda9 chore: bump backend version to 1.0.81
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is failing
2025-07-23 06:35:20 +00:00
paul f49dbed2d8 fix: remove updated_at from app_settings inserts in multiple migrations
Mirror to GitHub / mirror (push) Successful in 32s
Test and Lint / backend-test (push) Successful in 1m25s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m2s
Version and Release / version-bump (push) Successful in 36s
Version and Release / trigger-drone (push) Successful in 3s
The app_settings table in production doesn't have created_at/updated_at columns.
Fixed inconsistent usage across migrations:
- Migration 014: removed updated_at: new Date()
- Migration 027: removed updated_at: knex.fn.now()
- Migration 033: removed updated_at: new Date()

This ensures all migrations are consistent and won't fail in production.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-23 08:30:19 +02:00
paul 1480fa31c7 CRITICAL FIX: remove created_at/updated_at from migration 032 inserts
Mirror to GitHub / mirror (push) Successful in 31s
Test and Lint / backend-test (push) Successful in 1m16s
Version and Release / version-bump (push) Has been cancelled
Test and Lint / frontend-test (push) Has been cancelled
Version and Release / trigger-drone (push) Has been cancelled
continuous-integration/drone/push Build is passing
Production was failing because app_settings and email_templates
tables don't have created_at/updated_at columns. Removed these
fields from all insert statements to restore service.

This is a critical production fix - system was down.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-23 08:28:03 +02:00
Gitea Actions Bot 769dff4364 chore: bump backend version to 1.0.80
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is failing
2025-07-23 05:30:06 +00:00
paul d2e0e75e9a fix: force github-release plugin to use GitHub API instead of Gitea
Mirror to GitHub / mirror (push) Successful in 35s
Test and Lint / backend-test (push) Successful in 1m29s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m3s
The plugin was auto-detecting the Gitea instance and using its API
instead of GitHub's. Fixed by:
- Adding explicit environment variables to override detection
- Removing deprecated github_url/github_upload_url parameters
- Setting DRONE_REMOTE_URL to point to GitHub

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-23 07:29:14 +02:00
paul 041f3b58f4 fix: remove unused formatBoolean import from migration 033
Mirror to GitHub / mirror (push) Successful in 33s
Test and Lint / backend-test (push) Successful in 1m24s
Test and Lint / frontend-test (push) Has been cancelled
Version and Release / version-bump (push) Successful in 42s
continuous-integration/drone/push Build is passing
Version and Release / trigger-drone (push) Successful in 3s
Removed unnecessary import that could cause issues if helpers.js
doesn't define formatBoolean. Migration already uses correct
boolean syntax without the helper.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-23 07:26:30 +02:00
paul 0f4db08883 fix: remove formatBoolean calls from migration 032 - critical production fix
Mirror to GitHub / mirror (push) Successful in 30s
Test and Lint / frontend-test (push) Has been cancelled
Test and Lint / backend-test (push) Has been cancelled
Version and Release / version-bump (push) Has been cancelled
Version and Release / trigger-drone (push) Has been cancelled
continuous-integration/drone/push Build is passing
Migration was failing with "formatBoolean is not a function" error,
preventing backend startup. Fixed by:
- Removing formatBoolean import
- Using direct boolean values for column defaults
- Using JSON.stringify for setting values

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-23 07:25:10 +02:00
Gitea Actions Bot e0bd8f0707 chore: bump backend version to 1.0.79
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is failing
2025-07-22 20:36:13 +00:00
paul a8d4500481 fix: resolve migration conflicts and duplicate numbering
Mirror to GitHub / mirror (push) Successful in 33s
Test and Lint / backend-test (push) Successful in 1m24s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m15s
Version and Release / version-bump (push) Successful in 41s
Version and Release / trigger-drone (push) Successful in 3s
- Rename conflicting migrations to sequential numbers
- Update 035_enhance_backup_system.js to check for existing columns
- Prevent 'column already exists' errors during migration
- Add proper column existence checks before alterations
2025-07-22 22:30:59 +02:00
paul 12f7901d63 fix: correct GitHub repository path in Drone CI release config
Mirror to GitHub / mirror (push) Successful in 33s
Test and Lint / backend-test (push) Successful in 1m18s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m2s
- Remove deprecated base_url and upload_url parameters
- Use correct GitHub repository: the-luap/picpeak
- This should resolve the 404 error when creating releases
2025-07-22 15:31:24 +02:00
16 changed files with 392 additions and 273 deletions
+26 -43
View File
@@ -110,50 +110,33 @@ steps:
# -------- NEW: Create GitHub Release --------
- name: github-release
image: plugins/github-release
settings:
api_key:
image: curlimages/curl:latest
when:
event: tag
environment:
GITHUB_TOKEN:
from_secret: GITHUB_TOKEN
base_url: https://api.github.com/
upload_url: https://uploads.github.com/
repo: the-luap/picpeak
owner: the-luap
title: "PicPeak ${DRONE_TAG}"
note: |
# PicPeak ${DRONE_TAG}
## 🐳 Docker Images
This release includes Docker images published to GitHub Container Registry:
```bash
# Backend
docker pull ghcr.io/the-luap/picpeak-backend:${DRONE_TAG}
docker pull ghcr.io/the-luap/picpeak-backend:latest
# Frontend
docker pull ghcr.io/the-luap/picpeak-frontend:${DRONE_TAG}
docker pull ghcr.io/the-luap/picpeak-frontend:latest
```
## 📦 What's New
See the [README](https://github.com/the-luap/picpeak#readme) for features and documentation.
## 🚀 Quick Start
```bash
# Clone and deploy
git clone https://github.com/the-luap/picpeak.git
cd picpeak
# Use the tagged version
docker-compose -f docker-compose.prod.yml up -d
```
---
For detailed deployment instructions, see the [Deployment Guide](https://github.com/the-luap/picpeak/blob/main/DEPLOYMENT.md).
commands:
- |
# Create release payload
cat > release.json <<EOF
{
"tag_name": "${DRONE_TAG}",
"target_commitish": "main",
"name": "PicPeak ${DRONE_TAG}",
"body": "# PicPeak ${DRONE_TAG}\n\n## 🐳 Docker Images\n\nThis release includes Docker images published to GitHub Container Registry:\n\n\`\`\`bash\n# Backend\ndocker pull ghcr.io/the-luap/picpeak-backend:${DRONE_TAG}\ndocker pull ghcr.io/the-luap/picpeak-backend:latest\n\n# Frontend\ndocker pull ghcr.io/the-luap/picpeak-frontend:${DRONE_TAG}\ndocker pull ghcr.io/the-luap/picpeak-frontend:latest\n\`\`\`\n\n## 📦 What's New\n\nSee the [README](https://github.com/the-luap/picpeak#readme) for features and documentation.\n\n## 🚀 Quick Start\n\n\`\`\`bash\n# Clone and deploy\ngit clone https://github.com/the-luap/picpeak.git\ncd picpeak\n\n# Use the tagged version\ndocker-compose -f docker-compose.prod.yml up -d\n\`\`\`\n\n---\n\nFor detailed deployment instructions, see the [Deployment Guide](https://github.com/the-luap/picpeak/blob/main/DEPLOYMENT.md).",
"draft": false,
"prerelease": false
}
EOF
- |
# Create the release on GitHub
curl -X POST \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer $GITHUB_TOKEN" \
-H "X-GitHub-Api-Version: 2022-11-28" \
https://api.github.com/repos/the-luap/picpeak/releases \
-d @release.json
trigger:
event:
+4
View File
@@ -48,6 +48,10 @@ coverage/
*.tmp
*.temp
# Backup and test directories
backups/
test-archiver/
# Keep directory structure
!storage/events/active/.gitkeep
!storage/events/archived/.gitkeep
@@ -8,8 +8,7 @@ exports.up = async function(knex) {
await knex('app_settings').insert({
setting_key: 'general_default_welcome_message',
setting_value: JSON.stringify('Thank you for using our photo sharing service! We hope you enjoy your photos.'),
setting_type: 'general',
updated_at: new Date()
setting_type: 'general'
});
}
@@ -41,8 +41,7 @@ exports.up = async function(knex) {
if (!exists) {
await knex('app_settings').insert({
...setting,
updated_at: knex.fn.now()
...setting
});
}
}
@@ -1,131 +0,0 @@
exports.up = function(knex) {
return knex.schema
// Add new settings to app_settings table
.table('app_settings', table => {
// S3 configuration enhancements
table.boolean('backup_s3_force_path_style').defaultTo(false).comment('Force path-style S3 URLs (for MinIO/self-hosted)');
table.boolean('backup_s3_ssl_enabled').defaultTo(true).comment('Enable SSL/TLS for S3 connections');
table.string('backup_s3_prefix', 255).comment('S3 key prefix for organizing backups');
// Backup features
table.boolean('backup_incremental').defaultTo(false).comment('Enable incremental backups');
table.boolean('backup_include_database').defaultTo(true).comment('Include database dumps in backups');
table.boolean('backup_manifest_enabled').defaultTo(true).comment('Generate backup manifests');
table.enum('backup_manifest_format', ['json', 'yaml']).defaultTo('json').comment('Manifest file format');
table.boolean('backup_encryption_enabled').defaultTo(false).comment('Enable backup encryption');
table.string('backup_database_schedule', 100).comment('Separate cron schedule for database-only backups');
})
// Enhance backup_runs table
.table('backup_runs', table => {
// Manifest tracking
table.string('manifest_path', 500).comment('Path to backup manifest file');
table.uuid('manifest_id').comment('Unique identifier for the manifest');
table.enum('manifest_format', ['json', 'yaml']).comment('Format of the manifest file');
// Incremental backup support
table.integer('parent_backup_id').unsigned().references('id').inTable('backup_runs').onDelete('SET NULL').comment('Parent backup for incremental backups');
table.enum('backup_mode', ['full', 'incremental', 'database']).defaultTo('full').comment('Type of backup performed');
// Add indexes for better query performance
table.index(['backup_mode', 'status'], 'idx_backup_runs_mode_status');
table.index(['parent_backup_id'], 'idx_backup_runs_parent');
table.index(['created_at', 'backup_mode'], 'idx_backup_runs_created_mode');
})
// Create backup_manifest table for storing detailed manifest metadata
.createTable('backup_manifest', table => {
table.increments('id').primary();
table.integer('backup_run_id').unsigned().notNullable().references('id').inTable('backup_runs').onDelete('CASCADE');
table.uuid('manifest_id').notNullable().unique().comment('Unique identifier matching backup_runs.manifest_id');
table.string('version', 20).notNullable().defaultTo('1.0.0').comment('Manifest schema version');
table.enum('format', ['json', 'yaml']).notNullable().defaultTo('json');
// Backup metadata
table.timestamp('backup_start').notNullable();
table.timestamp('backup_end').notNullable();
table.bigInteger('total_size').unsigned().comment('Total size of backup in bytes');
table.integer('file_count').unsigned().comment('Number of files in backup');
table.integer('photo_count').unsigned().comment('Number of photos backed up');
table.integer('event_count').unsigned().comment('Number of events backed up');
// Incremental backup metadata
table.boolean('is_incremental').defaultTo(false);
table.uuid('parent_manifest_id').comment('Parent manifest ID for incremental backups');
table.timestamp('incremental_since').comment('Timestamp for incremental backup baseline');
// Content checksums
table.string('checksum_algorithm', 50).defaultTo('sha256').comment('Algorithm used for checksums');
table.text('manifest_checksum').comment('Checksum of the manifest file itself');
// Storage information
table.string('storage_location', 500).comment('Primary storage location (local path or S3 URI)');
table.string('storage_provider', 50).comment('Storage provider (local, s3, etc.)');
// Encryption metadata
table.boolean('is_encrypted').defaultTo(false);
table.string('encryption_algorithm', 100).comment('Encryption algorithm used');
table.string('encryption_key_id', 255).comment('ID of encryption key used');
// Additional metadata as JSON
table.json('metadata').comment('Additional metadata as JSON');
// Timestamps
table.timestamps(true, true);
// Indexes
table.index(['backup_run_id'], 'idx_manifest_backup_run');
table.index(['manifest_id'], 'idx_manifest_uuid');
table.index(['parent_manifest_id'], 'idx_manifest_parent');
table.index(['backup_start', 'backup_end'], 'idx_manifest_time_range');
table.index(['is_incremental', 'created_at'], 'idx_manifest_incremental_created');
})
// Add composite indexes for common query patterns
.raw(`
CREATE INDEX IF NOT EXISTS idx_backup_runs_recent_successful
ON backup_runs(created_at DESC)
WHERE status = 'completed' AND backup_mode = 'full';
`)
.raw(`
CREATE INDEX IF NOT EXISTS idx_backup_runs_incremental_chain
ON backup_runs(parent_backup_id, created_at)
WHERE backup_mode = 'incremental';
`);
};
exports.down = function(knex) {
return knex.schema
// Drop indexes first
.raw('DROP INDEX IF EXISTS idx_backup_runs_incremental_chain;')
.raw('DROP INDEX IF EXISTS idx_backup_runs_recent_successful;')
// Drop backup_manifest table
.dropTableIfExists('backup_manifest')
// Remove columns from backup_runs table
.table('backup_runs', table => {
table.dropIndex(['backup_mode', 'status'], 'idx_backup_runs_mode_status');
table.dropIndex(['parent_backup_id'], 'idx_backup_runs_parent');
table.dropIndex(['created_at', 'backup_mode'], 'idx_backup_runs_created_mode');
table.dropColumn('manifest_path');
table.dropColumn('manifest_id');
table.dropColumn('manifest_format');
table.dropColumn('parent_backup_id');
table.dropColumn('backup_mode');
})
// Remove columns from app_settings table
.table('app_settings', table => {
table.dropColumn('backup_s3_force_path_style');
table.dropColumn('backup_s3_ssl_enabled');
table.dropColumn('backup_s3_prefix');
table.dropColumn('backup_incremental');
table.dropColumn('backup_include_database');
table.dropColumn('backup_manifest_enabled');
table.dropColumn('backup_manifest_format');
table.dropColumn('backup_encryption_enabled');
table.dropColumn('backup_database_schedule');
});
};
@@ -1,4 +1,4 @@
const { formatBoolean, parseBoolean } = require('./helpers');
// No helpers needed for this migration
/**
* Add restore_runs table for tracking restore operations
@@ -30,9 +30,9 @@ exports.up = async function(knex) {
table.string('pre_restore_backup_path', 500); // Path to pre-restore safety backup
// Flags
table.boolean('is_dry_run').defaultTo(formatBoolean(false));
table.boolean('was_rollback_attempted').defaultTo(formatBoolean(false));
table.boolean('was_successful').defaultTo(formatBoolean(false));
table.boolean('is_dry_run').defaultTo(false);
table.boolean('was_rollback_attempted').defaultTo(false);
table.boolean('was_successful').defaultTo(false);
// Operator information
table.string('operator_type', 50).defaultTo('manual'); // manual, scheduled, api
@@ -59,7 +59,7 @@ exports.up = async function(knex) {
table.bigInteger('file_size');
table.string('checksum', 64);
table.boolean('checksum_verified').defaultTo(formatBoolean(false));
table.boolean('checksum_verified').defaultTo(false);
table.text('error_message');
table.timestamp('started_at');
@@ -92,60 +92,43 @@ exports.up = async function(knex) {
await knex('app_settings').insert([
{
setting_key: 'restore_allow_force',
setting_value: formatBoolean(false),
setting_type: 'restore',
description: 'Allow force restore with warnings',
created_at: knex.fn.now(),
updated_at: knex.fn.now()
setting_value: JSON.stringify(false),
setting_type: 'restore'
},
{
setting_key: 'restore_require_pre_backup',
setting_value: formatBoolean(true),
setting_type: 'restore',
description: 'Require pre-restore backup',
created_at: knex.fn.now(),
updated_at: knex.fn.now()
setting_value: JSON.stringify(true),
setting_type: 'restore'
},
{
setting_key: 'restore_max_file_size_mb',
setting_value: '5000',
setting_type: 'restore',
description: 'Maximum file size for restore (MB)',
created_at: knex.fn.now(),
updated_at: knex.fn.now()
setting_type: 'restore'
},
{
setting_key: 'restore_verify_checksums',
setting_value: formatBoolean(true),
setting_type: 'restore',
description: 'Verify file checksums during restore',
created_at: knex.fn.now(),
updated_at: knex.fn.now()
setting_value: JSON.stringify(true),
setting_type: 'restore'
},
{
setting_key: 'restore_email_on_completion',
setting_value: formatBoolean(true),
setting_type: 'restore',
description: 'Send email on restore completion',
created_at: knex.fn.now(),
updated_at: knex.fn.now()
setting_value: JSON.stringify(true),
setting_type: 'restore'
},
{
setting_key: 'restore_retention_days',
setting_value: '30',
setting_type: 'restore',
description: 'Days to retain restore history',
created_at: knex.fn.now(),
updated_at: knex.fn.now()
setting_type: 'restore'
}
]);
// Add new email templates for restore notifications
const emailTemplates = [
{
name: 'restore_completed',
subject: '✅ Restore Completed Successfully',
body: `<h2>Restore Operation Completed</h2>
template_key: 'restore_completed',
subject_en: '✅ Restore Completed Successfully',
subject_de: '✅ Wiederherstellung erfolgreich abgeschlossen',
body_html_en: `<h2>Restore Operation Completed</h2>
<p>A restore operation has completed successfully.</p>
<h3>Details:</h3>
@@ -158,36 +141,7 @@ exports.up = async function(knex) {
</ul>
<p>Please verify that all systems are functioning correctly after the restore.</p>`,
language: 'en',
is_active: formatBoolean(true),
created_at: knex.fn.now(),
updated_at: knex.fn.now()
},
{
name: 'restore_failed',
subject: '❌ Restore Operation Failed',
body: `<h2>Restore Operation Failed</h2>
<p>A restore operation has failed and requires attention.</p>
<h3>Details:</h3>
<ul>
<li><strong>Restore Type:</strong> {{restore_type}}</li>
<li><strong>Error:</strong> {{error_message}}</li>
<li><strong>Timestamp:</strong> {{timestamp}}</li>
</ul>
<p>Please check the system logs for more details and take appropriate action.</p>
<p><strong>Important:</strong> If a pre-restore backup was created, it may be used for recovery.</p>`,
language: 'en',
is_active: formatBoolean(true),
created_at: knex.fn.now(),
updated_at: knex.fn.now()
},
{
name: 'restore_completed',
subject: '✅ Wiederherstellung erfolgreich abgeschlossen',
body: `<h2>Wiederherstellungsvorgang abgeschlossen</h2>
body_html_de: `<h2>Wiederherstellungsvorgang abgeschlossen</h2>
<p>Ein Wiederherstellungsvorgang wurde erfolgreich abgeschlossen.</p>
<h3>Details:</h3>
@@ -200,15 +154,50 @@ exports.up = async function(knex) {
</ul>
<p>Bitte überprüfen Sie, ob alle Systeme nach der Wiederherstellung ordnungsgemäß funktionieren.</p>`,
language: 'de',
is_active: formatBoolean(true),
created_at: knex.fn.now(),
updated_at: knex.fn.now()
body_text_en: `Restore Operation Completed
A restore operation has completed successfully.
Details:
- Restore Type: {{restore_type}}
- Duration: {{duration}}
- Files Restored: {{files_restored}}
- Backup ID: {{backup_id}}
- Timestamp: {{timestamp}}
Please verify that all systems are functioning correctly after the restore.`,
body_text_de: `Wiederherstellungsvorgang abgeschlossen
Ein Wiederherstellungsvorgang wurde erfolgreich abgeschlossen.
Details:
- Wiederherstellungstyp: {{restore_type}}
- Dauer: {{duration}}
- Wiederhergestellte Dateien: {{files_restored}}
- Backup-ID: {{backup_id}}
- Zeitstempel: {{timestamp}}
Bitte überprüfen Sie, ob alle Systeme nach der Wiederherstellung ordnungsgemäß funktionieren.`,
variables: JSON.stringify(['restore_type', 'duration', 'files_restored', 'backup_id', 'timestamp'])
},
{
name: 'restore_failed',
subject: '❌ Wiederherstellungsvorgang fehlgeschlagen',
body: `<h2>Wiederherstellungsvorgang fehlgeschlagen</h2>
template_key: 'restore_failed',
subject_en: '❌ Restore Operation Failed',
subject_de: '❌ Wiederherstellungsvorgang fehlgeschlagen',
body_html_en: `<h2>Restore Operation Failed</h2>
<p>A restore operation has failed and requires attention.</p>
<h3>Details:</h3>
<ul>
<li><strong>Restore Type:</strong> {{restore_type}}</li>
<li><strong>Error:</strong> {{error_message}}</li>
<li><strong>Timestamp:</strong> {{timestamp}}</li>
</ul>
<p>Please check the system logs for more details and take appropriate action.</p>
<p><strong>Important:</strong> If a pre-restore backup was created, it may be used for recovery.</p>`,
body_html_de: `<h2>Wiederherstellungsvorgang fehlgeschlagen</h2>
<p>Ein Wiederherstellungsvorgang ist fehlgeschlagen und erfordert Ihre Aufmerksamkeit.</p>
<h3>Details:</h3>
@@ -221,10 +210,31 @@ exports.up = async function(knex) {
<p>Bitte überprüfen Sie die Systemprotokolle für weitere Details und ergreifen Sie entsprechende Maßnahmen.</p>
<p><strong>Wichtig:</strong> Falls ein Backup vor der Wiederherstellung erstellt wurde, kann es zur Wiederherstellung verwendet werden.</p>`,
language: 'de',
is_active: formatBoolean(true),
created_at: knex.fn.now(),
updated_at: knex.fn.now()
body_text_en: `Restore Operation Failed
A restore operation has failed and requires attention.
Details:
- Restore Type: {{restore_type}}
- Error: {{error_message}}
- Timestamp: {{timestamp}}
Please check the system logs for more details and take appropriate action.
Important: If a pre-restore backup was created, it may be used for recovery.`,
body_text_de: `Wiederherstellungsvorgang fehlgeschlagen
Ein Wiederherstellungsvorgang ist fehlgeschlagen und erfordert Ihre Aufmerksamkeit.
Details:
- Wiederherstellungstyp: {{restore_type}}
- Fehler: {{error_message}}
- Zeitstempel: {{timestamp}}
Bitte überprüfen Sie die Systemprotokolle für weitere Details und ergreifen Sie entsprechende Maßnahmen.
Wichtig: Falls ein Backup vor der Wiederherstellung erstellt wurde, kann es zur Wiederherstellung verwendet werden.`,
variables: JSON.stringify(['restore_type', 'error_message', 'timestamp'])
}
];
@@ -234,7 +244,7 @@ exports.up = async function(knex) {
exports.down = async function(knex) {
// Remove email templates
await knex('email_templates')
.whereIn('name', ['restore_completed', 'restore_failed'])
.whereIn('template_key', ['restore_completed', 'restore_failed'])
.delete();
// Remove settings
@@ -1,4 +1,4 @@
const { formatBoolean } = require('./helpers');
// No helpers needed for boolean values
exports.up = async function(knex) {
console.log('Adding gallery feedback tables...');
@@ -87,8 +87,7 @@ exports.up = async function(knex) {
{
setting_key: 'feedback_notification_email',
setting_value: JSON.stringify(''),
setting_type: 'feedback',
updated_at: new Date()
setting_type: 'feedback'
},
{
setting_key: 'feedback_rate_limits',
@@ -97,8 +96,7 @@ exports.up = async function(knex) {
comment: { max: 20, window: 3600 }, // 20 comments per hour
like: { max: 200, window: 3600 } // 200 likes per hour
}),
setting_type: 'feedback',
updated_at: new Date()
setting_type: 'feedback'
}
]);
@@ -0,0 +1,221 @@
const { db } = require('../src/database/db');
async function up() {
console.log('Enhancing backup system...');
// Add new settings to app_settings table if they don't exist
const backupSettings = [
{
setting_key: 'backup_s3_force_path_style',
setting_value: JSON.stringify(false),
setting_type: 'backup'
},
{
setting_key: 'backup_s3_ssl_enabled',
setting_value: JSON.stringify(true),
setting_type: 'backup'
},
{
setting_key: 'backup_s3_prefix',
setting_value: JSON.stringify(''),
setting_type: 'backup'
},
{
setting_key: 'backup_incremental',
setting_value: JSON.stringify(false),
setting_type: 'backup'
},
{
setting_key: 'backup_include_database',
setting_value: JSON.stringify(true),
setting_type: 'backup'
},
{
setting_key: 'backup_encryption_enabled',
setting_value: JSON.stringify(false),
setting_type: 'backup'
},
{
setting_key: 'backup_database_schedule',
setting_value: JSON.stringify(''),
setting_type: 'backup'
}
];
// Insert settings if they don't exist
for (const setting of backupSettings) {
const exists = await db('app_settings')
.where('setting_key', setting.setting_key)
.first();
if (!exists) {
await db('app_settings').insert(setting);
}
}
// Check and add columns to backup_runs table
const hasBackupRunsTable = await db.schema.hasTable('backup_runs');
if (hasBackupRunsTable) {
// Check for existing columns before adding
const hasManifestPath = await db.schema.hasColumn('backup_runs', 'manifest_path');
const hasManifestId = await db.schema.hasColumn('backup_runs', 'manifest_id');
const hasManifestFormat = await db.schema.hasColumn('backup_runs', 'manifest_format');
const hasParentBackupId = await db.schema.hasColumn('backup_runs', 'parent_backup_id');
const hasBackupMode = await db.schema.hasColumn('backup_runs', 'backup_mode');
if (!hasManifestPath || !hasManifestId || !hasManifestFormat || !hasParentBackupId || !hasBackupMode) {
await db.schema.alterTable('backup_runs', (table) => {
if (!hasManifestPath) {
table.string('manifest_path', 500).comment('Path to backup manifest file');
}
if (!hasManifestId) {
table.uuid('manifest_id').comment('Unique identifier for the manifest');
}
if (!hasManifestFormat) {
table.enum('manifest_format', ['json', 'yaml']).comment('Format of the manifest file');
}
if (!hasParentBackupId) {
table.integer('parent_backup_id').unsigned().references('id').inTable('backup_runs').onDelete('SET NULL').comment('Parent backup for incremental backups');
}
if (!hasBackupMode) {
table.enum('backup_mode', ['full', 'incremental', 'database']).defaultTo('full').comment('Type of backup performed');
}
});
}
// Add indexes if they don't exist
try {
await db.raw('CREATE INDEX IF NOT EXISTS idx_backup_runs_mode_status ON backup_runs(backup_mode, status)');
await db.raw('CREATE INDEX IF NOT EXISTS idx_backup_runs_parent ON backup_runs(parent_backup_id)');
await db.raw('CREATE INDEX IF NOT EXISTS idx_backup_runs_created_mode ON backup_runs(created_at, backup_mode)');
} catch (error) {
console.log('Note: Some indexes may already exist, continuing...');
}
}
// Create backup_manifest table if it doesn't exist
const hasManifestTable = await db.schema.hasTable('backup_manifest');
if (!hasManifestTable) {
await db.schema.createTable('backup_manifest', (table) => {
table.increments('id').primary();
table.integer('backup_run_id').unsigned().notNullable().references('id').inTable('backup_runs').onDelete('CASCADE');
table.uuid('manifest_id').notNullable().unique().comment('Unique identifier matching backup_runs.manifest_id');
table.string('version', 20).notNullable().defaultTo('1.0.0').comment('Manifest schema version');
table.enum('format', ['json', 'yaml']).notNullable().defaultTo('json');
// Backup metadata
table.timestamp('backup_start').notNullable();
table.timestamp('backup_end').notNullable();
table.bigInteger('total_size').unsigned().comment('Total size of backup in bytes');
table.integer('file_count').unsigned().comment('Number of files in backup');
table.integer('photo_count').unsigned().comment('Number of photos backed up');
table.integer('event_count').unsigned().comment('Number of events backed up');
// Incremental backup metadata
table.boolean('is_incremental').defaultTo(false);
table.uuid('parent_manifest_id').comment('Parent manifest ID for incremental backups');
table.timestamp('incremental_since').comment('Timestamp for incremental backup baseline');
// Content checksums
table.string('checksum_algorithm', 50).defaultTo('sha256').comment('Algorithm used for checksums');
table.text('manifest_checksum').comment('Checksum of the manifest file itself');
// Storage information
table.string('storage_location', 500).comment('Primary storage location (local path or S3 URI)');
table.string('storage_provider', 50).comment('Storage provider (local, s3, etc.)');
// Encryption metadata
table.boolean('is_encrypted').defaultTo(false);
table.string('encryption_algorithm', 100).comment('Encryption algorithm used');
table.string('encryption_key_id', 255).comment('ID of encryption key used');
// Additional metadata as JSON
table.json('metadata').comment('Additional metadata as JSON');
// Timestamps
table.timestamps(true, true);
// Indexes
table.index(['backup_run_id'], 'idx_manifest_backup_run');
table.index(['manifest_id'], 'idx_manifest_uuid');
table.index(['parent_manifest_id'], 'idx_manifest_parent');
table.index(['backup_start', 'backup_end'], 'idx_manifest_time_range');
table.index(['is_incremental', 'created_at'], 'idx_manifest_incremental_created');
});
}
// Add composite indexes for common query patterns
try {
await db.raw(`
CREATE INDEX IF NOT EXISTS idx_backup_runs_recent_successful
ON backup_runs(created_at DESC)
WHERE status = 'completed' AND backup_mode = 'full';
`);
await db.raw(`
CREATE INDEX IF NOT EXISTS idx_backup_runs_incremental_chain
ON backup_runs(parent_backup_id, created_at)
WHERE backup_mode = 'incremental';
`);
} catch (error) {
console.log('Note: Some composite indexes may already exist, continuing...');
}
console.log('Backup system enhancements completed');
}
async function down() {
// Drop indexes first
try {
await db.raw('DROP INDEX IF EXISTS idx_backup_runs_incremental_chain;');
await db.raw('DROP INDEX IF EXISTS idx_backup_runs_recent_successful;');
} catch (error) {
// Ignore errors if indexes don't exist
}
// Drop backup_manifest table
await db.schema.dropTableIfExists('backup_manifest');
// Remove columns from backup_runs if they exist
const hasBackupRunsTable = await db.schema.hasTable('backup_runs');
if (hasBackupRunsTable) {
const hasBackupMode = await db.schema.hasColumn('backup_runs', 'backup_mode');
const hasParentBackupId = await db.schema.hasColumn('backup_runs', 'parent_backup_id');
const hasManifestFormat = await db.schema.hasColumn('backup_runs', 'manifest_format');
const hasManifestId = await db.schema.hasColumn('backup_runs', 'manifest_id');
const hasManifestPath = await db.schema.hasColumn('backup_runs', 'manifest_path');
if (hasBackupMode || hasParentBackupId || hasManifestFormat || hasManifestId || hasManifestPath) {
await db.schema.alterTable('backup_runs', (table) => {
if (hasBackupMode) table.dropColumn('backup_mode');
if (hasParentBackupId) table.dropColumn('parent_backup_id');
if (hasManifestFormat) table.dropColumn('manifest_format');
if (hasManifestId) table.dropColumn('manifest_id');
if (hasManifestPath) table.dropColumn('manifest_path');
});
}
// Drop indexes
try {
await db.raw('DROP INDEX IF EXISTS idx_backup_runs_mode_status');
await db.raw('DROP INDEX IF EXISTS idx_backup_runs_parent');
await db.raw('DROP INDEX IF EXISTS idx_backup_runs_created_mode');
} catch (error) {
// Ignore errors if indexes don't exist
}
}
// Remove settings
await db('app_settings')
.whereIn('setting_key', [
'backup_s3_force_path_style',
'backup_s3_ssl_enabled',
'backup_s3_prefix',
'backup_incremental',
'backup_include_database',
'backup_encryption_enabled',
'backup_database_schedule'
])
.delete();
}
module.exports = { up, down };
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "picpeak-backend",
"version": "1.0.78",
"version": "1.0.84",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "picpeak-backend",
"version": "1.0.78",
"version": "1.0.84",
"dependencies": {
"@aws-sdk/client-s3": "^3.850.0",
"@aws-sdk/lib-storage": "^3.850.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "picpeak-backend",
"version": "1.0.78",
"version": "1.0.84",
"description": "Backend for PicPeak event photo sharing platform",
"main": "server.js",
"scripts": {
+1
View File
@@ -45,6 +45,7 @@ router.get('/', async (req, res) => {
theme_config: settingsObject.theme_config || null,
default_language: settingsObject.general_default_language || 'en',
enable_analytics: settingsObject.general_enable_analytics !== false,
general_date_format: settingsObject.general_date_format || 'PPP',
enable_recaptcha: settingsObject.security_enable_recaptcha === true || settingsObject.security_enable_recaptcha === 'true',
recaptcha_site_key: settingsObject.security_recaptcha_site_key || null,
maintenance_mode: settingsObject.general_maintenance_mode === true || settingsObject.general_maintenance_mode === 'true',
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "picpeak-frontend",
"version": "1.0.78",
"version": "1.0.84",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "picpeak-frontend",
"version": "1.0.78",
"version": "1.0.84",
"dependencies": {
"@tanstack/react-query": "^5.0.0",
"@tiptap/extension-character-count": "^2.26.1",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "picpeak-frontend",
"private": true,
"version": "1.0.78",
"version": "1.0.84",
"type": "module",
"scripts": {
"dev": "vite",
+2 -2
View File
@@ -83,8 +83,8 @@ api.interceptors.response.use(
}
if (error.response?.status === 401) {
// Check if it's an admin route
const isAdminRoute = error.config?.url?.includes('/admin');
// Check if it's an admin route (but not public endpoints)
const isAdminRoute = error.config?.url?.includes('/admin') && !error.config?.url?.includes('/public/');
const currentPath = window.location.pathname;
if (isAdminRoute) {
+5 -4
View File
@@ -2,16 +2,17 @@ import { useTranslation } from 'react-i18next';
import { format as dateFnsFormat, formatDistanceToNow as dateFnsFormatDistanceToNow } from 'date-fns';
import { de, enUS } from 'date-fns/locale';
import { useQuery } from '@tanstack/react-query';
import { settingsService } from '../services/settings.service';
import { publicSettingsService } from '../services/publicSettings.service';
export const useLocalizedDate = () => {
const { i18n } = useTranslation();
// Fetch admin settings to get the date format
// Fetch public settings to get the date format
const { data: settings } = useQuery({
queryKey: ['admin-settings-general'],
queryFn: () => settingsService.getSettingsByType('general'),
queryKey: ['public-settings'],
queryFn: () => publicSettingsService.getPublicSettings(),
staleTime: 5 * 60 * 1000, // Cache for 5 minutes
retry: 1, // Only retry once to avoid blocking the UI
});
const getLocale = () => {
@@ -0,0 +1,34 @@
import { api } from '../config/api';
export interface PublicSettings {
branding_company_name: string;
branding_company_tagline: string;
branding_support_email: string;
branding_footer_text: string;
branding_watermark_enabled: boolean;
branding_watermark_logo_url: string;
branding_watermark_position: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left' | 'center';
branding_watermark_opacity: number;
branding_watermark_size: number;
branding_favicon_url: string;
branding_logo_url: string;
theme_config: any;
default_language: string;
enable_analytics: boolean;
general_date_format: string;
enable_recaptcha: boolean;
recaptcha_site_key: string | null;
maintenance_mode: boolean;
umami_enabled: boolean;
umami_url: string | null;
umami_website_id: string | null;
umami_share_url: string | null;
}
export const publicSettingsService = {
// Get public settings (no authentication required)
async getPublicSettings(): Promise<PublicSettings> {
const response = await api.get<PublicSettings>('/public/settings');
return response.data;
}
};