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 <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,8 @@ const emailRoutes = require('./adminEmail');
|
||||
const settingsRoutes = require('./adminSettings');
|
||||
const eventsRoutes = require('./adminEvents');
|
||||
const photosRoutes = require('./adminPhotos');
|
||||
const categoriesRoutes = require('./adminCategories');
|
||||
const cmsRoutes = require('./adminCMS');
|
||||
|
||||
// Mount sub-routers
|
||||
router.use('/dashboard', dashboardRoutes);
|
||||
@@ -16,5 +18,7 @@ router.use('/email', emailRoutes);
|
||||
router.use('/settings', settingsRoutes);
|
||||
router.use('/events', eventsRoutes);
|
||||
router.use('/events', photosRoutes);
|
||||
router.use('/categories', categoriesRoutes);
|
||||
router.use('/cms', cmsRoutes);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,83 @@
|
||||
const express = require('express');
|
||||
const { body, validationResult } = require('express-validator');
|
||||
const { db, logActivity } = require('../database/db');
|
||||
const { adminAuth } = require('../middleware/auth');
|
||||
const router = express.Router();
|
||||
|
||||
// Get all CMS pages
|
||||
router.get('/pages', adminAuth, async (req, res) => {
|
||||
try {
|
||||
const pages = await db('cms_pages').select('*').orderBy('slug', 'asc');
|
||||
res.json(pages);
|
||||
} catch (error) {
|
||||
console.error('Error fetching CMS pages:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch pages' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get a single CMS page
|
||||
router.get('/pages/:slug', adminAuth, async (req, res) => {
|
||||
try {
|
||||
const { slug } = req.params;
|
||||
const page = await db('cms_pages').where('slug', slug).first();
|
||||
|
||||
if (!page) {
|
||||
return res.status(404).json({ error: 'Page not found' });
|
||||
}
|
||||
|
||||
res.json(page);
|
||||
} catch (error) {
|
||||
console.error('Error fetching CMS page:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch page' });
|
||||
}
|
||||
});
|
||||
|
||||
// Update a CMS page
|
||||
router.put('/pages/:slug', adminAuth, [
|
||||
body('title_en').optional().isString(),
|
||||
body('title_de').optional().isString(),
|
||||
body('content_en').optional().isString(),
|
||||
body('content_de').optional().isString()
|
||||
], async (req, res) => {
|
||||
try {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({ errors: errors.array() });
|
||||
}
|
||||
|
||||
const { slug } = req.params;
|
||||
const { title_en, title_de, content_en, content_de } = req.body;
|
||||
|
||||
const page = await db('cms_pages').where('slug', slug).first();
|
||||
if (!page) {
|
||||
return res.status(404).json({ error: 'Page not found' });
|
||||
}
|
||||
|
||||
// Update the page
|
||||
await db('cms_pages')
|
||||
.where('slug', slug)
|
||||
.update({
|
||||
title_en,
|
||||
title_de,
|
||||
content_en,
|
||||
content_de,
|
||||
updated_at: new Date()
|
||||
});
|
||||
|
||||
const updated = await db('cms_pages').where('slug', slug).first();
|
||||
|
||||
// Log activity
|
||||
await logActivity('cms_page_updated',
|
||||
{ page: slug },
|
||||
null,
|
||||
{ type: 'admin', id: req.admin.id, name: req.admin.username }
|
||||
);
|
||||
|
||||
res.json(updated);
|
||||
} catch (error) {
|
||||
console.error('Error updating CMS page:', error);
|
||||
res.status(500).json({ error: 'Failed to update page' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,182 @@
|
||||
const express = require('express');
|
||||
const { body, validationResult } = require('express-validator');
|
||||
const { db, logActivity } = require('../database/db');
|
||||
const { adminAuth } = require('../middleware/auth');
|
||||
const router = express.Router();
|
||||
|
||||
// Get all global categories
|
||||
router.get('/global', adminAuth, async (req, res) => {
|
||||
try {
|
||||
const categories = await db('photo_categories')
|
||||
.where('is_global', true)
|
||||
.orderBy('name', 'asc');
|
||||
|
||||
res.json(categories);
|
||||
} catch (error) {
|
||||
console.error('Error fetching categories:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch categories' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get categories for a specific event (global + event-specific)
|
||||
router.get('/event/:eventId', adminAuth, async (req, res) => {
|
||||
try {
|
||||
const { eventId } = req.params;
|
||||
|
||||
const categories = await db('photo_categories')
|
||||
.where(function() {
|
||||
this.where('is_global', true)
|
||||
.orWhere('event_id', eventId);
|
||||
})
|
||||
.orderBy('is_global', 'desc')
|
||||
.orderBy('name', 'asc');
|
||||
|
||||
res.json(categories);
|
||||
} catch (error) {
|
||||
console.error('Error fetching event categories:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch categories' });
|
||||
}
|
||||
});
|
||||
|
||||
// Create a new category
|
||||
router.post('/', adminAuth, [
|
||||
body('name').notEmpty().withMessage('Category name is required'),
|
||||
body('slug').optional(),
|
||||
body('is_global').optional().isBoolean(),
|
||||
body('event_id').optional().isInt()
|
||||
], async (req, res) => {
|
||||
try {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({ errors: errors.array() });
|
||||
}
|
||||
|
||||
const { name, slug, is_global = true, event_id = null } = req.body;
|
||||
|
||||
// Generate slug if not provided
|
||||
const categorySlug = slug || name.toLowerCase()
|
||||
.replace(/[^\w\s-]/g, '')
|
||||
.replace(/\s+/g, '-')
|
||||
.replace(/-+/g, '-')
|
||||
.trim();
|
||||
|
||||
// Check if slug already exists for this scope
|
||||
const existing = await db('photo_categories')
|
||||
.where('slug', categorySlug)
|
||||
.where(function() {
|
||||
if (is_global) {
|
||||
this.where('is_global', true);
|
||||
} else {
|
||||
this.where('event_id', event_id);
|
||||
}
|
||||
})
|
||||
.first();
|
||||
|
||||
if (existing) {
|
||||
return res.status(400).json({ error: 'Category with this slug already exists' });
|
||||
}
|
||||
|
||||
// Create category
|
||||
const [categoryId] = await db('photo_categories').insert({
|
||||
name,
|
||||
slug: categorySlug,
|
||||
is_global,
|
||||
event_id: is_global ? null : event_id
|
||||
});
|
||||
|
||||
const category = await db('photo_categories').where('id', categoryId).first();
|
||||
|
||||
// Log activity
|
||||
await logActivity('category_created',
|
||||
{ categoryName: name, isGlobal: is_global },
|
||||
event_id,
|
||||
{ type: 'admin', id: req.admin.id, name: req.admin.username }
|
||||
);
|
||||
|
||||
res.json(category);
|
||||
} catch (error) {
|
||||
console.error('Error creating category:', error);
|
||||
res.status(500).json({ error: 'Failed to create category' });
|
||||
}
|
||||
});
|
||||
|
||||
// Update a category
|
||||
router.put('/:id', adminAuth, [
|
||||
body('name').notEmpty().withMessage('Category name is required')
|
||||
], async (req, res) => {
|
||||
try {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({ errors: errors.array() });
|
||||
}
|
||||
|
||||
const { id } = req.params;
|
||||
const { name } = req.body;
|
||||
|
||||
const category = await db('photo_categories').where('id', id).first();
|
||||
if (!category) {
|
||||
return res.status(404).json({ error: 'Category not found' });
|
||||
}
|
||||
|
||||
await db('photo_categories')
|
||||
.where('id', id)
|
||||
.update({
|
||||
name,
|
||||
slug: name.toLowerCase()
|
||||
.replace(/[^\w\s-]/g, '')
|
||||
.replace(/\s+/g, '-')
|
||||
.replace(/-+/g, '-')
|
||||
.trim()
|
||||
});
|
||||
|
||||
const updated = await db('photo_categories').where('id', id).first();
|
||||
|
||||
// Log activity
|
||||
await logActivity('category_updated',
|
||||
{ categoryName: name },
|
||||
category.event_id,
|
||||
{ type: 'admin', id: req.admin.id, name: req.admin.username }
|
||||
);
|
||||
|
||||
res.json(updated);
|
||||
} catch (error) {
|
||||
console.error('Error updating category:', error);
|
||||
res.status(500).json({ error: 'Failed to update category' });
|
||||
}
|
||||
});
|
||||
|
||||
// Delete a category
|
||||
router.delete('/:id', adminAuth, async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const category = await db('photo_categories').where('id', id).first();
|
||||
if (!category) {
|
||||
return res.status(404).json({ error: 'Category not found' });
|
||||
}
|
||||
|
||||
// Check if category has photos
|
||||
const photoCount = await db('photos').where('category_id', id).count('id as count').first();
|
||||
if (photoCount.count > 0) {
|
||||
return res.status(400).json({
|
||||
error: 'Cannot delete category with photos. Please reassign photos first.'
|
||||
});
|
||||
}
|
||||
|
||||
await db('photo_categories').where('id', id).delete();
|
||||
|
||||
// Log activity
|
||||
await logActivity('category_deleted',
|
||||
{ categoryName: category.name },
|
||||
category.event_id,
|
||||
{ type: 'admin', id: req.admin.id, name: req.admin.username }
|
||||
);
|
||||
|
||||
res.json({ message: 'Category deleted successfully' });
|
||||
} catch (error) {
|
||||
console.error('Error deleting category:', error);
|
||||
res.status(500).json({ error: 'Failed to delete category' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -34,7 +34,7 @@ router.post('/', adminAuth, [
|
||||
admin_email,
|
||||
password,
|
||||
welcome_message = '',
|
||||
color_theme = 'default',
|
||||
color_theme = null,
|
||||
expiration_days = 30
|
||||
} = req.body;
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ const fs = require('fs').promises;
|
||||
const { db, logActivity } = require('../database/db');
|
||||
const { adminAuth } = require('../middleware/auth');
|
||||
const { generateThumbnail } = require('../services/imageProcessor');
|
||||
const { generatePhotoFilename } = require('../utils/filenameSanitizer');
|
||||
const router = express.Router();
|
||||
|
||||
// Get storage path from environment or default
|
||||
@@ -14,7 +15,6 @@ const getStoragePath = () => process.env.STORAGE_PATH || path.join(__dirname, '.
|
||||
const storage = multer.diskStorage({
|
||||
destination: async (req, file, cb) => {
|
||||
const { eventId } = req.params;
|
||||
const { type = 'individual' } = req.body;
|
||||
|
||||
try {
|
||||
// Get event details
|
||||
@@ -23,9 +23,11 @@ const storage = multer.diskStorage({
|
||||
return cb(new Error('Event not found'));
|
||||
}
|
||||
|
||||
// Create destination path
|
||||
const photoType = type === 'collage' ? 'collages' : 'individual';
|
||||
const destPath = path.join(getStoragePath(), 'events/active', event.slug, photoType);
|
||||
// Store event in request for use in filename generation
|
||||
req.eventData = event;
|
||||
|
||||
// Create destination path - now just event folder, no type subfolder
|
||||
const destPath = path.join(getStoragePath(), 'events/active', event.slug);
|
||||
|
||||
// Ensure directory exists
|
||||
await fs.mkdir(destPath, { recursive: true });
|
||||
@@ -35,12 +37,14 @@ const storage = multer.diskStorage({
|
||||
cb(error);
|
||||
}
|
||||
},
|
||||
filename: (req, file, cb) => {
|
||||
// Generate unique filename
|
||||
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
|
||||
const ext = path.extname(file.originalname);
|
||||
const name = path.basename(file.originalname, ext);
|
||||
cb(null, `${name}-${uniqueSuffix}${ext}`);
|
||||
filename: async (req, file, cb) => {
|
||||
try {
|
||||
// Use temporary filename for now, will rename after getting category info
|
||||
const tempName = `temp_${Date.now()}_${Math.round(Math.random() * 1E9)}${path.extname(file.originalname)}`;
|
||||
cb(null, tempName);
|
||||
} catch (error) {
|
||||
cb(error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -67,7 +71,12 @@ const upload = multer({
|
||||
router.post('/:eventId/upload', adminAuth, upload.array('photos', 20), async (req, res) => {
|
||||
try {
|
||||
const { eventId } = req.params;
|
||||
const { type = 'individual' } = req.body;
|
||||
const { category_id } = req.body;
|
||||
|
||||
console.log('Upload request received:');
|
||||
console.log('Body:', req.body);
|
||||
console.log('Files:', req.files ? req.files.length : 'none');
|
||||
console.log('Headers:', req.headers);
|
||||
|
||||
// Verify event exists and admin has access
|
||||
const event = await db('events').where({ id: eventId }).first();
|
||||
@@ -76,40 +85,103 @@ router.post('/:eventId/upload', adminAuth, upload.array('photos', 20), async (re
|
||||
}
|
||||
|
||||
if (!req.files || req.files.length === 0) {
|
||||
console.log('No files in request. req.files:', req.files);
|
||||
return res.status(400).json({ error: 'No files uploaded' });
|
||||
}
|
||||
|
||||
// Get category details if provided
|
||||
let category = null;
|
||||
if (category_id) {
|
||||
category = await db('photo_categories').where({ id: category_id }).first();
|
||||
if (!category) {
|
||||
return res.status(400).json({ error: 'Invalid category' });
|
||||
}
|
||||
}
|
||||
|
||||
const uploadedPhotos = [];
|
||||
|
||||
// Process each uploaded file
|
||||
for (const file of req.files) {
|
||||
let trx;
|
||||
try {
|
||||
// Generate thumbnail
|
||||
// Start transaction for atomic counter update
|
||||
trx = await db.transaction();
|
||||
|
||||
// Get and increment the counter for this category
|
||||
let counter = 1;
|
||||
if (category) {
|
||||
// Lock the category row and get current counter
|
||||
const categoryData = await trx('photo_categories')
|
||||
.where({ id: category_id })
|
||||
.forUpdate()
|
||||
.first();
|
||||
|
||||
counter = (categoryData.photo_counter || 0) + 1;
|
||||
|
||||
// Update counter
|
||||
await trx('photo_categories')
|
||||
.where({ id: category_id })
|
||||
.update({ photo_counter: counter });
|
||||
} else {
|
||||
// For uncategorized photos, count existing uncategorized photos
|
||||
const uncategorizedCount = await trx('photos')
|
||||
.where({ event_id: eventId })
|
||||
.whereNull('category_id')
|
||||
.count('id as count')
|
||||
.first();
|
||||
|
||||
counter = (uncategorizedCount.count || 0) + 1;
|
||||
}
|
||||
|
||||
// Generate new filename
|
||||
const extension = path.extname(file.originalname);
|
||||
const newFilename = generatePhotoFilename(
|
||||
event.event_name,
|
||||
category ? category.name : 'uncategorized',
|
||||
counter,
|
||||
extension
|
||||
);
|
||||
|
||||
// Rename the file
|
||||
const oldPath = file.path;
|
||||
const newPath = path.join(path.dirname(oldPath), newFilename);
|
||||
await fs.rename(oldPath, newPath);
|
||||
|
||||
// Update file object
|
||||
file.filename = newFilename;
|
||||
file.path = newPath;
|
||||
|
||||
// Generate thumbnail with new filename
|
||||
const thumbnailPath = await generateThumbnail(file.path);
|
||||
|
||||
// Calculate relative paths
|
||||
const storagePath = getStoragePath();
|
||||
const relativePath = path.relative(path.join(storagePath, 'events/active'), file.path);
|
||||
const relativeThumbPath = thumbnailPath ? path.relative(path.join(storagePath, 'events/active'), thumbnailPath) : null;
|
||||
const relativeThumbPath = thumbnailPath; // thumbnailPath is already relative to storage root
|
||||
|
||||
// Add to database
|
||||
const [photoId] = await db('photos').insert({
|
||||
const [photoId] = await trx('photos').insert({
|
||||
event_id: eventId,
|
||||
filename: file.filename,
|
||||
path: relativePath,
|
||||
thumbnail_path: relativeThumbPath,
|
||||
type: type === 'collage' ? 'collage' : 'individual',
|
||||
category_id: category_id || null,
|
||||
type: 'individual', // Keep for backwards compatibility
|
||||
size_bytes: file.size
|
||||
});
|
||||
|
||||
// Commit transaction
|
||||
await trx.commit();
|
||||
|
||||
uploadedPhotos.push({
|
||||
id: photoId,
|
||||
filename: file.filename,
|
||||
size: file.size,
|
||||
type
|
||||
category_id: category_id || null
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`Error processing file ${file.filename}:`, error);
|
||||
if (trx) await trx.rollback();
|
||||
// Continue with other files
|
||||
}
|
||||
}
|
||||
@@ -187,23 +259,38 @@ router.delete('/:eventId/photos/:photoId', adminAuth, async (req, res) => {
|
||||
router.get('/:eventId/photos', adminAuth, async (req, res) => {
|
||||
try {
|
||||
const { eventId } = req.params;
|
||||
const { type } = req.query;
|
||||
const { category_id, type } = req.query;
|
||||
|
||||
let query = db('photos').where({ event_id: eventId });
|
||||
let query = db('photos')
|
||||
.leftJoin('photo_categories', 'photos.category_id', 'photo_categories.id')
|
||||
.where({ 'photos.event_id': eventId })
|
||||
.select(
|
||||
'photos.*',
|
||||
'photo_categories.name as category_name',
|
||||
'photo_categories.slug as category_slug'
|
||||
);
|
||||
|
||||
if (type) {
|
||||
query = query.where({ type });
|
||||
if (category_id) {
|
||||
query = query.where({ 'photos.category_id': category_id });
|
||||
}
|
||||
|
||||
const photos = await query.orderBy('uploaded_at', 'desc');
|
||||
// Keep type filter for backwards compatibility
|
||||
if (type) {
|
||||
query = query.where({ 'photos.type': type });
|
||||
}
|
||||
|
||||
const photos = await query.orderBy('photos.uploaded_at', 'desc');
|
||||
|
||||
res.json({
|
||||
photos: photos.map(photo => ({
|
||||
id: photo.id,
|
||||
filename: photo.filename,
|
||||
url: `/photos/${photo.path}`,
|
||||
thumbnail_url: photo.thumbnail_path ? `/photos/${photo.thumbnail_path}` : null,
|
||||
thumbnail_url: photo.thumbnail_path ? `/thumbnails/${photo.thumbnail_path}` : null,
|
||||
type: photo.type,
|
||||
category_id: photo.category_id,
|
||||
category_name: photo.category_name,
|
||||
category_slug: photo.category_slug,
|
||||
size: photo.size_bytes,
|
||||
uploaded_at: photo.uploaded_at
|
||||
}))
|
||||
|
||||
@@ -3,7 +3,7 @@ const multer = require('multer');
|
||||
const path = require('path');
|
||||
const fs = require('fs').promises;
|
||||
const { body, validationResult } = require('express-validator');
|
||||
const { db } = require('../database/db');
|
||||
const { db, logActivity } = require('../database/db');
|
||||
const { adminAuth } = require('../middleware/auth');
|
||||
const router = express.Router();
|
||||
|
||||
@@ -36,6 +36,32 @@ const upload = multer({
|
||||
}
|
||||
});
|
||||
|
||||
// Configure multer for favicon uploads
|
||||
const faviconStorage = multer.diskStorage({
|
||||
destination: async (req, file, cb) => {
|
||||
const uploadDir = path.join(__dirname, '../../storage/uploads/favicons');
|
||||
await fs.mkdir(uploadDir, { recursive: true });
|
||||
cb(null, uploadDir);
|
||||
},
|
||||
filename: (req, file, cb) => {
|
||||
const ext = path.extname(file.originalname);
|
||||
cb(null, `favicon-${Date.now()}${ext}`);
|
||||
}
|
||||
});
|
||||
|
||||
const faviconUpload = multer({
|
||||
storage: faviconStorage,
|
||||
limits: { fileSize: 1 * 1024 * 1024 }, // 1MB
|
||||
fileFilter: (req, file, cb) => {
|
||||
const allowedTypes = ['image/png', 'image/x-icon', 'image/vnd.microsoft.icon'];
|
||||
if (allowedTypes.includes(file.mimetype)) {
|
||||
cb(null, true);
|
||||
} else {
|
||||
cb(new Error('Favicon must be PNG or ICO format'));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Get all settings
|
||||
router.get('/', adminAuth, async (req, res) => {
|
||||
try {
|
||||
@@ -44,9 +70,17 @@ router.get('/', adminAuth, async (req, res) => {
|
||||
// Convert to object format
|
||||
const settingsObject = {};
|
||||
settings.forEach(setting => {
|
||||
settingsObject[setting.setting_key] = setting.setting_value
|
||||
? JSON.parse(setting.setting_value)
|
||||
: null;
|
||||
if (setting.setting_value) {
|
||||
try {
|
||||
// Try to parse as JSON first
|
||||
settingsObject[setting.setting_key] = JSON.parse(setting.setting_value);
|
||||
} catch (e) {
|
||||
// If it's not valid JSON, use the raw value
|
||||
settingsObject[setting.setting_key] = setting.setting_value;
|
||||
}
|
||||
} else {
|
||||
settingsObject[setting.setting_key] = null;
|
||||
}
|
||||
});
|
||||
|
||||
res.json(settingsObject);
|
||||
@@ -67,9 +101,17 @@ router.get('/:type', adminAuth, async (req, res) => {
|
||||
// Convert to object format
|
||||
const settingsObject = {};
|
||||
settings.forEach(setting => {
|
||||
settingsObject[setting.setting_key] = setting.setting_value
|
||||
? JSON.parse(setting.setting_value)
|
||||
: null;
|
||||
if (setting.setting_value) {
|
||||
try {
|
||||
// Try to parse as JSON first
|
||||
settingsObject[setting.setting_key] = JSON.parse(setting.setting_value);
|
||||
} catch (e) {
|
||||
// If it's not valid JSON, use the raw value
|
||||
settingsObject[setting.setting_key] = setting.setting_value;
|
||||
}
|
||||
} else {
|
||||
settingsObject[setting.setting_key] = null;
|
||||
}
|
||||
});
|
||||
|
||||
res.json(settingsObject);
|
||||
@@ -87,7 +129,10 @@ router.put('/branding', adminAuth, async (req, res) => {
|
||||
company_tagline,
|
||||
support_email,
|
||||
footer_text,
|
||||
watermark_enabled
|
||||
watermark_enabled,
|
||||
watermark_position,
|
||||
watermark_opacity,
|
||||
watermark_size
|
||||
} = req.body;
|
||||
|
||||
const brandingSettings = {
|
||||
@@ -95,7 +140,10 @@ router.put('/branding', adminAuth, async (req, res) => {
|
||||
company_tagline,
|
||||
support_email,
|
||||
footer_text,
|
||||
watermark_enabled
|
||||
watermark_enabled,
|
||||
watermark_position,
|
||||
watermark_opacity,
|
||||
watermark_size
|
||||
};
|
||||
|
||||
// Update or insert each setting
|
||||
@@ -172,19 +220,19 @@ router.post('/logo', adminAuth, upload.single('logo'), async (req, res) => {
|
||||
await db('app_settings')
|
||||
.insert({
|
||||
setting_key: 'branding_logo_url',
|
||||
setting_value: JSON.stringify(publicPath),
|
||||
setting_value: publicPath,
|
||||
setting_type: 'branding',
|
||||
updated_at: new Date()
|
||||
})
|
||||
.onConflict('setting_key')
|
||||
.merge({
|
||||
setting_value: JSON.stringify(publicPath),
|
||||
setting_value: publicPath,
|
||||
updated_at: new Date()
|
||||
});
|
||||
|
||||
res.json({
|
||||
message: 'Logo uploaded successfully',
|
||||
logo_url: publicPath
|
||||
logoUrl: publicPath
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Logo upload error:', error);
|
||||
@@ -192,6 +240,68 @@ router.post('/logo', adminAuth, upload.single('logo'), async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Upload watermark logo
|
||||
router.post('/branding/watermark-logo', adminAuth, upload.single('watermarkLogo'), async (req, res) => {
|
||||
try {
|
||||
if (!req.file) {
|
||||
return res.status(400).json({ error: 'No file uploaded' });
|
||||
}
|
||||
|
||||
// Delete old watermark logo if exists
|
||||
const oldWatermarkLogoSetting = await db('app_settings')
|
||||
.where('setting_key', 'branding_watermark_logo_path')
|
||||
.first();
|
||||
|
||||
if (oldWatermarkLogoSetting && oldWatermarkLogoSetting.setting_value) {
|
||||
const oldPath = JSON.parse(oldWatermarkLogoSetting.setting_value);
|
||||
try {
|
||||
await fs.unlink(oldPath);
|
||||
} catch (error) {
|
||||
console.error('Failed to delete old watermark logo:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Save new watermark logo path
|
||||
const logoPath = req.file.path;
|
||||
const publicPath = `/uploads/logos/${req.file.filename}`;
|
||||
|
||||
await db('app_settings')
|
||||
.insert({
|
||||
setting_key: 'branding_watermark_logo_path',
|
||||
setting_value: JSON.stringify(logoPath),
|
||||
setting_type: 'branding',
|
||||
updated_at: new Date()
|
||||
})
|
||||
.onConflict('setting_key')
|
||||
.merge({
|
||||
setting_value: JSON.stringify(logoPath),
|
||||
updated_at: new Date()
|
||||
});
|
||||
|
||||
// Save public URL
|
||||
await db('app_settings')
|
||||
.insert({
|
||||
setting_key: 'branding_watermark_logo_url',
|
||||
setting_value: publicPath,
|
||||
setting_type: 'branding',
|
||||
updated_at: new Date()
|
||||
})
|
||||
.onConflict('setting_key')
|
||||
.merge({
|
||||
setting_value: publicPath,
|
||||
updated_at: new Date()
|
||||
});
|
||||
|
||||
res.json({
|
||||
message: 'Watermark logo uploaded successfully',
|
||||
watermarkLogoUrl: publicPath
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Watermark logo upload error:', error);
|
||||
res.status(500).json({ error: 'Failed to upload watermark logo' });
|
||||
}
|
||||
});
|
||||
|
||||
// Update theme settings
|
||||
router.put('/theme', adminAuth, async (req, res) => {
|
||||
try {
|
||||
@@ -346,4 +456,42 @@ router.get('/storage/info', adminAuth, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Upload favicon endpoint
|
||||
router.post('/favicon', adminAuth, faviconUpload.single('favicon'), async (req, res) => {
|
||||
try {
|
||||
if (!req.file) {
|
||||
return res.status(400).json({ error: 'No favicon file provided' });
|
||||
}
|
||||
|
||||
// The file is already in the correct location from multer
|
||||
const faviconUrl = `/uploads/favicons/${req.file.filename}`;
|
||||
|
||||
// Save to database
|
||||
await db('app_settings')
|
||||
.insert({
|
||||
setting_key: 'branding_favicon_url',
|
||||
setting_value: faviconUrl,
|
||||
setting_type: 'branding',
|
||||
updated_at: new Date()
|
||||
})
|
||||
.onConflict('setting_key')
|
||||
.merge({
|
||||
setting_value: faviconUrl,
|
||||
updated_at: new Date()
|
||||
});
|
||||
|
||||
// Log activity
|
||||
await logActivity('favicon_uploaded',
|
||||
{ faviconUrl },
|
||||
null,
|
||||
{ type: 'admin', id: req.admin.id, name: req.admin.username }
|
||||
);
|
||||
|
||||
res.json({ faviconUrl });
|
||||
} catch (error) {
|
||||
console.error('Error uploading favicon:', error);
|
||||
res.status(500).json({ error: 'Failed to upload favicon' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -87,7 +87,11 @@ router.post('/gallery/verify', [
|
||||
});
|
||||
|
||||
// Generate session token
|
||||
const token = jwt.sign({ eventId: event.id }, process.env.JWT_SECRET, { expiresIn: '24h' });
|
||||
const token = jwt.sign({
|
||||
eventId: event.id,
|
||||
eventSlug: event.slug,
|
||||
type: 'gallery'
|
||||
}, process.env.JWT_SECRET, { expiresIn: '24h' });
|
||||
|
||||
res.json({
|
||||
token,
|
||||
|
||||
@@ -4,6 +4,7 @@ const { db } = require('../database/db');
|
||||
const archiver = require('archiver');
|
||||
const path = require('path');
|
||||
const router = express.Router();
|
||||
const watermarkService = require('../services/watermarkService');
|
||||
|
||||
// Get storage path from environment or default
|
||||
const getStoragePath = () => process.env.STORAGE_PATH || path.join(__dirname, '../../../storage');
|
||||
@@ -26,7 +27,8 @@ async function verifyGalleryAccess(req, res, next) {
|
||||
req.event = event;
|
||||
next();
|
||||
} catch (error) {
|
||||
res.status(401).json({ error: 'Invalid token' });
|
||||
console.error('Error verifying gallery access:', error);
|
||||
res.status(401).json({ error: 'Invalid token', details: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +54,8 @@ router.get('/:slug/verify-token/:token', async (req, res) => {
|
||||
|
||||
res.json({ valid: true });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to verify token' });
|
||||
console.error('Error verifying token:', error);
|
||||
res.status(500).json({ error: 'Failed to verify token', details: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -89,7 +92,8 @@ router.get('/:slug/info', async (req, res) => {
|
||||
requires_password: true
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to fetch gallery info' });
|
||||
console.error('Error fetching gallery info:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch gallery info', details: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -97,8 +101,23 @@ router.get('/:slug/info', async (req, res) => {
|
||||
router.get('/:slug/photos', verifyGalleryAccess, async (req, res) => {
|
||||
try {
|
||||
const photos = await db('photos')
|
||||
.where('event_id', req.event.id)
|
||||
.orderBy('uploaded_at', 'desc');
|
||||
.leftJoin('photo_categories', 'photos.category_id', 'photo_categories.id')
|
||||
.where('photos.event_id', req.event.id)
|
||||
.select(
|
||||
'photos.*',
|
||||
'photo_categories.name as category_name',
|
||||
'photo_categories.slug as category_slug'
|
||||
)
|
||||
.orderBy('photos.uploaded_at', 'desc');
|
||||
|
||||
// Get all categories for this event
|
||||
const categories = await db('photo_categories')
|
||||
.where(function() {
|
||||
this.where('is_global', true)
|
||||
.orWhere('event_id', req.event.id);
|
||||
})
|
||||
.orderBy('is_global', 'desc')
|
||||
.orderBy('name', 'asc');
|
||||
|
||||
// Log view
|
||||
await db('access_logs').insert({
|
||||
@@ -118,18 +137,28 @@ router.get('/:slug/photos', verifyGalleryAccess, async (req, res) => {
|
||||
color_theme: req.event.color_theme,
|
||||
expires_at: req.event.expires_at
|
||||
},
|
||||
categories: categories.map(cat => ({
|
||||
id: cat.id,
|
||||
name: cat.name,
|
||||
slug: cat.slug,
|
||||
is_global: cat.is_global
|
||||
})),
|
||||
photos: photos.map(photo => ({
|
||||
id: photo.id,
|
||||
filename: photo.filename,
|
||||
url: `/photos/${req.event.slug}/${photo.path}`,
|
||||
thumbnail_url: photo.thumbnail_path ? `/photos/${photo.thumbnail_path}` : null,
|
||||
url: `/photos/${photo.path}`,
|
||||
thumbnail_url: photo.thumbnail_path ? `/${photo.thumbnail_path}` : null,
|
||||
type: photo.type,
|
||||
category_id: photo.category_id,
|
||||
category_name: photo.category_name,
|
||||
category_slug: photo.category_slug,
|
||||
size: photo.size_bytes,
|
||||
uploaded_at: photo.uploaded_at
|
||||
}))
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to fetch photos' });
|
||||
console.error('Error fetching photos:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch photos', details: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -159,7 +188,25 @@ router.get('/:slug/download/:photoId', verifyGalleryAccess, async (req, res) =>
|
||||
});
|
||||
|
||||
const filePath = path.join(getStoragePath(), 'events/active', req.event.slug, photo.path);
|
||||
res.download(filePath, photo.filename);
|
||||
|
||||
// Get watermark settings
|
||||
const watermarkSettings = await watermarkService.getWatermarkSettings();
|
||||
|
||||
if (watermarkSettings && watermarkSettings.enabled) {
|
||||
// Apply watermark and send
|
||||
const watermarkedBuffer = await watermarkService.applyWatermark(filePath, watermarkSettings);
|
||||
|
||||
res.set({
|
||||
'Content-Type': photo.mime_type || 'image/jpeg',
|
||||
'Content-Disposition': `attachment; filename="${photo.filename}"`,
|
||||
'Content-Length': watermarkedBuffer.length
|
||||
});
|
||||
|
||||
res.send(watermarkedBuffer);
|
||||
} else {
|
||||
// Send original file
|
||||
res.download(filePath, photo.filename);
|
||||
}
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to download photo' });
|
||||
}
|
||||
@@ -184,10 +231,21 @@ router.get('/:slug/download-all', verifyGalleryAccess, async (req, res) => {
|
||||
|
||||
archive.pipe(res);
|
||||
|
||||
// Get watermark settings
|
||||
const watermarkSettings = await watermarkService.getWatermarkSettings();
|
||||
|
||||
// Add photos to archive
|
||||
for (const photo of photos) {
|
||||
const filePath = path.join(getStoragePath(), 'events/active', req.event.slug, photo.path);
|
||||
archive.file(filePath, { name: photo.path });
|
||||
|
||||
if (watermarkSettings && watermarkSettings.enabled) {
|
||||
// Apply watermark
|
||||
const watermarkedBuffer = await watermarkService.applyWatermark(filePath, watermarkSettings);
|
||||
archive.append(watermarkedBuffer, { name: photo.path });
|
||||
} else {
|
||||
// Add original file
|
||||
archive.file(filePath, { name: photo.path });
|
||||
}
|
||||
}
|
||||
|
||||
await archive.finalize();
|
||||
|
||||
@@ -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;
|
||||
@@ -0,0 +1,33 @@
|
||||
const express = require('express');
|
||||
const { db } = require('../database/db');
|
||||
const router = express.Router();
|
||||
|
||||
// Get public CMS page
|
||||
router.get('/pages/:slug', async (req, res) => {
|
||||
try {
|
||||
const { slug } = req.params;
|
||||
const { lang = 'en' } = req.query;
|
||||
|
||||
const page = await db('cms_pages').where('slug', slug).first();
|
||||
|
||||
if (!page) {
|
||||
return res.status(404).json({ error: 'Page not found' });
|
||||
}
|
||||
|
||||
// Return the appropriate language version
|
||||
const title = lang === 'de' ? page.title_de : page.title_en;
|
||||
const content = lang === 'de' ? page.content_de : page.content_en;
|
||||
|
||||
res.json({
|
||||
title,
|
||||
content,
|
||||
slug: page.slug,
|
||||
updated_at: page.updated_at
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching public CMS page:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch page' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -5,9 +5,9 @@ const router = express.Router();
|
||||
// Get public settings (branding and theme)
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
// Fetch branding and theme settings
|
||||
// Fetch branding, theme, and general settings
|
||||
const settings = await db('app_settings')
|
||||
.whereIn('setting_type', ['branding', 'theme'])
|
||||
.whereIn('setting_type', ['branding', 'theme', 'general'])
|
||||
.select('setting_key', 'setting_value');
|
||||
|
||||
// Convert to object format
|
||||
@@ -30,7 +30,14 @@ router.get('/', async (req, res) => {
|
||||
branding_support_email: settingsObject.branding_support_email || '',
|
||||
branding_footer_text: settingsObject.branding_footer_text || '',
|
||||
branding_watermark_enabled: settingsObject.branding_watermark_enabled || false,
|
||||
theme_config: settingsObject.theme_config || null
|
||||
branding_watermark_logo_url: settingsObject.branding_watermark_logo_url || '',
|
||||
branding_watermark_position: settingsObject.branding_watermark_position || 'bottom-right',
|
||||
branding_watermark_opacity: settingsObject.branding_watermark_opacity || 50,
|
||||
branding_watermark_size: settingsObject.branding_watermark_size || 15,
|
||||
branding_favicon_url: settingsObject.branding_favicon_url || '',
|
||||
branding_logo_url: settingsObject.branding_logo_url || '',
|
||||
theme_config: settingsObject.theme_config || null,
|
||||
default_language: settingsObject.general_default_language || 'en'
|
||||
};
|
||||
|
||||
res.json(publicSettings);
|
||||
|
||||
Reference in New Issue
Block a user