Fix language setting not being saved to database on admin settings page

- Added default_language field to general settings state in SettingsPage
- Replaced LanguageSelector component with simple select dropdown on settings page
- Fixed public settings endpoint to read general_default_language from database
- Language setting now properly saved when clicking Save Settings button
- Setting is correctly used by gallery login page and legal pages

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

Co-Authored-By: Claude <[email protected]>
This commit is contained in:
2025-07-08 09:49:45 +02:00
co-authored by Claude
parent cfa0b0da69
commit 2012b0bab9
91 changed files with 6183 additions and 577 deletions
+189
View File
@@ -0,0 +1,189 @@
const express = require('express');
const path = require('path');
const { db } = require('../database/db');
const { verifyGalleryAccess } = require('../middleware/gallery');
const watermarkService = require('../services/watermarkService');
const { getStoragePath } = require('../config/storage');
const crypto = require('crypto');
const router = express.Router();
/**
* Generate a signed URL token for image access
*/
function generateImageToken(photoId, expiresIn = 3600) {
const secret = process.env.JWT_SECRET || 'your-secret-key';
const expires = Date.now() + (expiresIn * 1000);
const data = `${photoId}:${expires}`;
const signature = crypto.createHmac('sha256', secret).update(data).digest('hex');
return `${Buffer.from(data).toString('base64')}.${signature}`;
}
/**
* Verify image token
*/
function verifyImageToken(token) {
try {
const secret = process.env.JWT_SECRET || 'your-secret-key';
const [data, signature] = token.split('.');
const decoded = Buffer.from(data, 'base64').toString();
const [photoId, expires] = decoded.split(':');
// Verify signature
const expectedSignature = crypto.createHmac('sha256', secret).update(decoded).digest('hex');
if (signature !== expectedSignature) {
return null;
}
// Check expiration
if (Date.now() > parseInt(expires)) {
return null;
}
return { photoId: parseInt(photoId), expires: parseInt(expires) };
} catch (error) {
return null;
}
}
/**
* Serve watermarked image
*/
router.get('/:slug/photo/:photoId/view', verifyGalleryAccess, async (req, res) => {
try {
const { photoId } = req.params;
// Get photo details
const photo = await db('photos')
.where({
id: photoId,
event_id: req.event.id
})
.first();
if (!photo) {
return res.status(404).json({ error: 'Photo not found' });
}
// Get watermark settings
const watermarkSettings = await watermarkService.getWatermarkSettings();
// Build full path to photo
const photoPath = path.join(getStoragePath(), 'events/active', req.event.slug, photo.path);
// Apply watermark if enabled
const imageBuffer = await watermarkService.applyWatermark(photoPath, watermarkSettings);
// Set appropriate headers
res.set({
'Content-Type': photo.mime_type || 'image/jpeg',
'Content-Length': imageBuffer.length,
'Cache-Control': 'private, max-age=3600',
'X-Content-Type-Options': 'nosniff'
});
// Send the watermarked image
res.send(imageBuffer);
} catch (error) {
console.error('Error serving watermarked image:', error);
res.status(500).json({ error: 'Failed to serve image' });
}
});
/**
* Generate signed URL for image access
*/
router.post('/:slug/photo/:photoId/generate-url', verifyGalleryAccess, async (req, res) => {
try {
const { photoId } = req.params;
// Verify photo belongs to this event
const photo = await db('photos')
.where({
id: photoId,
event_id: req.event.id
})
.first();
if (!photo) {
return res.status(404).json({ error: 'Photo not found' });
}
// Generate signed token
const token = generateImageToken(photoId);
const signedUrl = `/api/images/${req.params.slug}/photo/${photoId}/signed/${token}`;
res.json({
url: signedUrl,
expiresIn: 3600 // 1 hour
});
} catch (error) {
console.error('Error generating signed URL:', error);
res.status(500).json({ error: 'Failed to generate URL' });
}
});
/**
* Serve image with signed URL (no gallery auth required, token is the auth)
*/
router.get('/:slug/photo/:photoId/signed/:token', async (req, res) => {
try {
const { slug, photoId, token } = req.params;
// Verify token
const tokenData = verifyImageToken(token);
if (!tokenData || tokenData.photoId !== parseInt(photoId)) {
return res.status(403).json({ error: 'Invalid or expired token' });
}
// Get event
const event = await db('events')
.where({ slug })
.where('is_active', true)
.first();
if (!event) {
return res.status(404).json({ error: 'Event not found' });
}
// Get photo
const photo = await db('photos')
.where({
id: photoId,
event_id: event.id
})
.first();
if (!photo) {
return res.status(404).json({ error: 'Photo not found' });
}
// Get watermark settings
const watermarkSettings = await watermarkService.getWatermarkSettings();
// Build full path to photo
const photoPath = path.join(getStoragePath(), 'events/active', event.slug, photo.path);
// Apply watermark if enabled
const imageBuffer = await watermarkService.applyWatermark(photoPath, watermarkSettings);
// Set appropriate headers
res.set({
'Content-Type': photo.mime_type || 'image/jpeg',
'Content-Length': imageBuffer.length,
'Cache-Control': 'private, max-age=3600',
'X-Content-Type-Options': 'nosniff'
});
// Send the watermarked image
res.send(imageBuffer);
} catch (error) {
console.error('Error serving signed image:', error);
res.status(500).json({ error: 'Failed to serve image' });
}
});
module.exports = router;