Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 95a6ad505d | |||
| 9dc82c8490 | |||
| 554bf1d43c | |||
| f147bd6cfb | |||
| f564f40ed0 | |||
| b50cc18045 | |||
| 9adb2b04eb | |||
| 5cce58c233 | |||
| c95784eda9 | |||
| f49dbed2d8 | |||
| 1480fa31c7 | |||
| 769dff4364 | |||
| d2e0e75e9a | |||
| 041f3b58f4 | |||
| 0f4db08883 | |||
| e0bd8f0707 | |||
| a8d4500481 | |||
| 12f7901d63 |
+26
-43
@@ -110,50 +110,33 @@ steps:
|
|||||||
|
|
||||||
# -------- NEW: Create GitHub Release --------
|
# -------- NEW: Create GitHub Release --------
|
||||||
- name: github-release
|
- name: github-release
|
||||||
image: plugins/github-release
|
image: curlimages/curl:latest
|
||||||
settings:
|
when:
|
||||||
api_key:
|
event: tag
|
||||||
|
environment:
|
||||||
|
GITHUB_TOKEN:
|
||||||
from_secret: GITHUB_TOKEN
|
from_secret: GITHUB_TOKEN
|
||||||
base_url: https://api.github.com/
|
commands:
|
||||||
upload_url: https://uploads.github.com/
|
- |
|
||||||
repo: the-luap/picpeak
|
# Create release payload
|
||||||
owner: the-luap
|
cat > release.json <<EOF
|
||||||
title: "PicPeak ${DRONE_TAG}"
|
{
|
||||||
note: |
|
"tag_name": "${DRONE_TAG}",
|
||||||
# PicPeak ${DRONE_TAG}
|
"target_commitish": "main",
|
||||||
|
"name": "PicPeak ${DRONE_TAG}",
|
||||||
## 🐳 Docker Images
|
"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,
|
||||||
This release includes Docker images published to GitHub Container Registry:
|
"prerelease": false
|
||||||
|
}
|
||||||
```bash
|
EOF
|
||||||
# Backend
|
- |
|
||||||
docker pull ghcr.io/the-luap/picpeak-backend:${DRONE_TAG}
|
# Create the release on GitHub
|
||||||
docker pull ghcr.io/the-luap/picpeak-backend:latest
|
curl -X POST \
|
||||||
|
-H "Accept: application/vnd.github+json" \
|
||||||
# Frontend
|
-H "Authorization: Bearer $GITHUB_TOKEN" \
|
||||||
docker pull ghcr.io/the-luap/picpeak-frontend:${DRONE_TAG}
|
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||||
docker pull ghcr.io/the-luap/picpeak-frontend:latest
|
https://api.github.com/repos/the-luap/picpeak/releases \
|
||||||
```
|
-d @release.json
|
||||||
|
|
||||||
## 📦 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).
|
|
||||||
|
|
||||||
trigger:
|
trigger:
|
||||||
event:
|
event:
|
||||||
|
|||||||
@@ -48,6 +48,10 @@ coverage/
|
|||||||
*.tmp
|
*.tmp
|
||||||
*.temp
|
*.temp
|
||||||
|
|
||||||
|
# Backup and test directories
|
||||||
|
backups/
|
||||||
|
test-archiver/
|
||||||
|
|
||||||
# Keep directory structure
|
# Keep directory structure
|
||||||
!storage/events/active/.gitkeep
|
!storage/events/active/.gitkeep
|
||||||
!storage/events/archived/.gitkeep
|
!storage/events/archived/.gitkeep
|
||||||
|
|||||||
@@ -8,8 +8,7 @@ exports.up = async function(knex) {
|
|||||||
await knex('app_settings').insert({
|
await knex('app_settings').insert({
|
||||||
setting_key: 'general_default_welcome_message',
|
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_value: JSON.stringify('Thank you for using our photo sharing service! We hope you enjoy your photos.'),
|
||||||
setting_type: 'general',
|
setting_type: 'general'
|
||||||
updated_at: new Date()
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,8 +41,7 @@ exports.up = async function(knex) {
|
|||||||
|
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
await knex('app_settings').insert({
|
await knex('app_settings').insert({
|
||||||
...setting,
|
...setting
|
||||||
updated_at: knex.fn.now()
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
* 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
|
table.string('pre_restore_backup_path', 500); // Path to pre-restore safety backup
|
||||||
|
|
||||||
// Flags
|
// Flags
|
||||||
table.boolean('is_dry_run').defaultTo(formatBoolean(false));
|
table.boolean('is_dry_run').defaultTo(false);
|
||||||
table.boolean('was_rollback_attempted').defaultTo(formatBoolean(false));
|
table.boolean('was_rollback_attempted').defaultTo(false);
|
||||||
table.boolean('was_successful').defaultTo(formatBoolean(false));
|
table.boolean('was_successful').defaultTo(false);
|
||||||
|
|
||||||
// Operator information
|
// Operator information
|
||||||
table.string('operator_type', 50).defaultTo('manual'); // manual, scheduled, api
|
table.string('operator_type', 50).defaultTo('manual'); // manual, scheduled, api
|
||||||
@@ -59,7 +59,7 @@ exports.up = async function(knex) {
|
|||||||
|
|
||||||
table.bigInteger('file_size');
|
table.bigInteger('file_size');
|
||||||
table.string('checksum', 64);
|
table.string('checksum', 64);
|
||||||
table.boolean('checksum_verified').defaultTo(formatBoolean(false));
|
table.boolean('checksum_verified').defaultTo(false);
|
||||||
|
|
||||||
table.text('error_message');
|
table.text('error_message');
|
||||||
table.timestamp('started_at');
|
table.timestamp('started_at');
|
||||||
@@ -92,60 +92,43 @@ exports.up = async function(knex) {
|
|||||||
await knex('app_settings').insert([
|
await knex('app_settings').insert([
|
||||||
{
|
{
|
||||||
setting_key: 'restore_allow_force',
|
setting_key: 'restore_allow_force',
|
||||||
setting_value: formatBoolean(false),
|
setting_value: JSON.stringify(false),
|
||||||
setting_type: 'restore',
|
setting_type: 'restore'
|
||||||
description: 'Allow force restore with warnings',
|
|
||||||
created_at: knex.fn.now(),
|
|
||||||
updated_at: knex.fn.now()
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
setting_key: 'restore_require_pre_backup',
|
setting_key: 'restore_require_pre_backup',
|
||||||
setting_value: formatBoolean(true),
|
setting_value: JSON.stringify(true),
|
||||||
setting_type: 'restore',
|
setting_type: 'restore'
|
||||||
description: 'Require pre-restore backup',
|
|
||||||
created_at: knex.fn.now(),
|
|
||||||
updated_at: knex.fn.now()
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
setting_key: 'restore_max_file_size_mb',
|
setting_key: 'restore_max_file_size_mb',
|
||||||
setting_value: '5000',
|
setting_value: '5000',
|
||||||
setting_type: 'restore',
|
setting_type: 'restore'
|
||||||
description: 'Maximum file size for restore (MB)',
|
|
||||||
created_at: knex.fn.now(),
|
|
||||||
updated_at: knex.fn.now()
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
setting_key: 'restore_verify_checksums',
|
setting_key: 'restore_verify_checksums',
|
||||||
setting_value: formatBoolean(true),
|
setting_value: JSON.stringify(true),
|
||||||
setting_type: 'restore',
|
setting_type: 'restore'
|
||||||
description: 'Verify file checksums during restore',
|
|
||||||
created_at: knex.fn.now(),
|
|
||||||
updated_at: knex.fn.now()
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
setting_key: 'restore_email_on_completion',
|
setting_key: 'restore_email_on_completion',
|
||||||
setting_value: formatBoolean(true),
|
setting_value: JSON.stringify(true),
|
||||||
setting_type: 'restore',
|
setting_type: 'restore'
|
||||||
description: 'Send email on restore completion',
|
|
||||||
created_at: knex.fn.now(),
|
|
||||||
updated_at: knex.fn.now()
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
setting_key: 'restore_retention_days',
|
setting_key: 'restore_retention_days',
|
||||||
setting_value: '30',
|
setting_value: '30',
|
||||||
setting_type: 'restore',
|
setting_type: 'restore'
|
||||||
description: 'Days to retain restore history',
|
|
||||||
created_at: knex.fn.now(),
|
|
||||||
updated_at: knex.fn.now()
|
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Add new email templates for restore notifications
|
// Add new email templates for restore notifications
|
||||||
const emailTemplates = [
|
const emailTemplates = [
|
||||||
{
|
{
|
||||||
name: 'restore_completed',
|
template_key: 'restore_completed',
|
||||||
subject: '✅ Restore Completed Successfully',
|
subject_en: '✅ Restore Completed Successfully',
|
||||||
body: `<h2>Restore Operation Completed</h2>
|
subject_de: '✅ Wiederherstellung erfolgreich abgeschlossen',
|
||||||
|
body_html_en: `<h2>Restore Operation Completed</h2>
|
||||||
<p>A restore operation has completed successfully.</p>
|
<p>A restore operation has completed successfully.</p>
|
||||||
|
|
||||||
<h3>Details:</h3>
|
<h3>Details:</h3>
|
||||||
@@ -158,36 +141,7 @@ exports.up = async function(knex) {
|
|||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<p>Please verify that all systems are functioning correctly after the restore.</p>`,
|
<p>Please verify that all systems are functioning correctly after the restore.</p>`,
|
||||||
language: 'en',
|
body_html_de: `<h2>Wiederherstellungsvorgang abgeschlossen</h2>
|
||||||
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>
|
|
||||||
<p>Ein Wiederherstellungsvorgang wurde erfolgreich abgeschlossen.</p>
|
<p>Ein Wiederherstellungsvorgang wurde erfolgreich abgeschlossen.</p>
|
||||||
|
|
||||||
<h3>Details:</h3>
|
<h3>Details:</h3>
|
||||||
@@ -200,15 +154,50 @@ exports.up = async function(knex) {
|
|||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<p>Bitte überprüfen Sie, ob alle Systeme nach der Wiederherstellung ordnungsgemäß funktionieren.</p>`,
|
<p>Bitte überprüfen Sie, ob alle Systeme nach der Wiederherstellung ordnungsgemäß funktionieren.</p>`,
|
||||||
language: 'de',
|
body_text_en: `Restore Operation Completed
|
||||||
is_active: formatBoolean(true),
|
|
||||||
created_at: knex.fn.now(),
|
A restore operation has completed successfully.
|
||||||
updated_at: knex.fn.now()
|
|
||||||
|
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',
|
template_key: 'restore_failed',
|
||||||
subject: '❌ Wiederherstellungsvorgang fehlgeschlagen',
|
subject_en: '❌ Restore Operation Failed',
|
||||||
body: `<h2>Wiederherstellungsvorgang fehlgeschlagen</h2>
|
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>
|
<p>Ein Wiederherstellungsvorgang ist fehlgeschlagen und erfordert Ihre Aufmerksamkeit.</p>
|
||||||
|
|
||||||
<h3>Details:</h3>
|
<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>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>`,
|
<p><strong>Wichtig:</strong> Falls ein Backup vor der Wiederherstellung erstellt wurde, kann es zur Wiederherstellung verwendet werden.</p>`,
|
||||||
language: 'de',
|
body_text_en: `Restore Operation Failed
|
||||||
is_active: formatBoolean(true),
|
|
||||||
created_at: knex.fn.now(),
|
A restore operation has failed and requires attention.
|
||||||
updated_at: knex.fn.now()
|
|
||||||
|
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) {
|
exports.down = async function(knex) {
|
||||||
// Remove email templates
|
// Remove email templates
|
||||||
await knex('email_templates')
|
await knex('email_templates')
|
||||||
.whereIn('name', ['restore_completed', 'restore_failed'])
|
.whereIn('template_key', ['restore_completed', 'restore_failed'])
|
||||||
.delete();
|
.delete();
|
||||||
|
|
||||||
// Remove settings
|
// Remove settings
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const { formatBoolean } = require('./helpers');
|
// No helpers needed for boolean values
|
||||||
|
|
||||||
exports.up = async function(knex) {
|
exports.up = async function(knex) {
|
||||||
console.log('Adding gallery feedback tables...');
|
console.log('Adding gallery feedback tables...');
|
||||||
@@ -87,8 +87,7 @@ exports.up = async function(knex) {
|
|||||||
{
|
{
|
||||||
setting_key: 'feedback_notification_email',
|
setting_key: 'feedback_notification_email',
|
||||||
setting_value: JSON.stringify(''),
|
setting_value: JSON.stringify(''),
|
||||||
setting_type: 'feedback',
|
setting_type: 'feedback'
|
||||||
updated_at: new Date()
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
setting_key: 'feedback_rate_limits',
|
setting_key: 'feedback_rate_limits',
|
||||||
@@ -97,8 +96,7 @@ exports.up = async function(knex) {
|
|||||||
comment: { max: 20, window: 3600 }, // 20 comments per hour
|
comment: { max: 20, window: 3600 }, // 20 comments per hour
|
||||||
like: { max: 200, window: 3600 } // 200 likes per hour
|
like: { max: 200, window: 3600 } // 200 likes per hour
|
||||||
}),
|
}),
|
||||||
setting_type: 'feedback',
|
setting_type: 'feedback'
|
||||||
updated_at: new Date()
|
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -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 };
|
||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "picpeak-backend",
|
"name": "picpeak-backend",
|
||||||
"version": "1.0.78",
|
"version": "1.0.84",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "picpeak-backend",
|
"name": "picpeak-backend",
|
||||||
"version": "1.0.78",
|
"version": "1.0.84",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-s3": "^3.850.0",
|
"@aws-sdk/client-s3": "^3.850.0",
|
||||||
"@aws-sdk/lib-storage": "^3.850.0",
|
"@aws-sdk/lib-storage": "^3.850.0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "picpeak-backend",
|
"name": "picpeak-backend",
|
||||||
"version": "1.0.78",
|
"version": "1.0.84",
|
||||||
"description": "Backend for PicPeak event photo sharing platform",
|
"description": "Backend for PicPeak event photo sharing platform",
|
||||||
"main": "server.js",
|
"main": "server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ router.get('/', async (req, res) => {
|
|||||||
theme_config: settingsObject.theme_config || null,
|
theme_config: settingsObject.theme_config || null,
|
||||||
default_language: settingsObject.general_default_language || 'en',
|
default_language: settingsObject.general_default_language || 'en',
|
||||||
enable_analytics: settingsObject.general_enable_analytics !== false,
|
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',
|
enable_recaptcha: settingsObject.security_enable_recaptcha === true || settingsObject.security_enable_recaptcha === 'true',
|
||||||
recaptcha_site_key: settingsObject.security_recaptcha_site_key || null,
|
recaptcha_site_key: settingsObject.security_recaptcha_site_key || null,
|
||||||
maintenance_mode: settingsObject.general_maintenance_mode === true || settingsObject.general_maintenance_mode === 'true',
|
maintenance_mode: settingsObject.general_maintenance_mode === true || settingsObject.general_maintenance_mode === 'true',
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "picpeak-frontend",
|
"name": "picpeak-frontend",
|
||||||
"version": "1.0.78",
|
"version": "1.0.84",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "picpeak-frontend",
|
"name": "picpeak-frontend",
|
||||||
"version": "1.0.78",
|
"version": "1.0.84",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tanstack/react-query": "^5.0.0",
|
"@tanstack/react-query": "^5.0.0",
|
||||||
"@tiptap/extension-character-count": "^2.26.1",
|
"@tiptap/extension-character-count": "^2.26.1",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "picpeak-frontend",
|
"name": "picpeak-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.78",
|
"version": "1.0.84",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -83,8 +83,8 @@ api.interceptors.response.use(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (error.response?.status === 401) {
|
if (error.response?.status === 401) {
|
||||||
// Check if it's an admin route
|
// Check if it's an admin route (but not public endpoints)
|
||||||
const isAdminRoute = error.config?.url?.includes('/admin');
|
const isAdminRoute = error.config?.url?.includes('/admin') && !error.config?.url?.includes('/public/');
|
||||||
const currentPath = window.location.pathname;
|
const currentPath = window.location.pathname;
|
||||||
|
|
||||||
if (isAdminRoute) {
|
if (isAdminRoute) {
|
||||||
|
|||||||
@@ -2,16 +2,17 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { format as dateFnsFormat, formatDistanceToNow as dateFnsFormatDistanceToNow } from 'date-fns';
|
import { format as dateFnsFormat, formatDistanceToNow as dateFnsFormatDistanceToNow } from 'date-fns';
|
||||||
import { de, enUS } from 'date-fns/locale';
|
import { de, enUS } from 'date-fns/locale';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { settingsService } from '../services/settings.service';
|
import { publicSettingsService } from '../services/publicSettings.service';
|
||||||
|
|
||||||
export const useLocalizedDate = () => {
|
export const useLocalizedDate = () => {
|
||||||
const { i18n } = useTranslation();
|
const { i18n } = useTranslation();
|
||||||
|
|
||||||
// Fetch admin settings to get the date format
|
// Fetch public settings to get the date format
|
||||||
const { data: settings } = useQuery({
|
const { data: settings } = useQuery({
|
||||||
queryKey: ['admin-settings-general'],
|
queryKey: ['public-settings'],
|
||||||
queryFn: () => settingsService.getSettingsByType('general'),
|
queryFn: () => publicSettingsService.getPublicSettings(),
|
||||||
staleTime: 5 * 60 * 1000, // Cache for 5 minutes
|
staleTime: 5 * 60 * 1000, // Cache for 5 minutes
|
||||||
|
retry: 1, // Only retry once to avoid blocking the UI
|
||||||
});
|
});
|
||||||
|
|
||||||
const getLocale = () => {
|
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;
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user