feat: Add comprehensive system enhancements

- Add version display above storage consumption in admin sidebar
- Fix storage consumption to stick to bottom of window using flexbox
- Add user upload settings to events (allow uploads, category selection)
- Enhance disk space tab to comprehensive system status view
- Add localized date formatting for German/English language support
- Remove quick actions from dashboard for cleaner interface
- Create user photo upload functionality for galleries
- Add database migration for user upload settings
- Update all TypeScript types and interfaces

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-07-08 22:47:04 +02:00
parent 69b56ed582
commit 12ba91952e
24 changed files with 1210 additions and 128 deletions
+12 -4
View File
@@ -19,7 +19,9 @@ router.post('/', adminAuth, [
body('password').isLength({ min: 6 }),
body('expiration_days').isInt({ min: 1, max: 365 }).optional(),
body('welcome_message').optional().trim(),
body('color_theme').optional().trim()
body('color_theme').optional().trim(),
body('allow_user_uploads').optional().isBoolean(),
body('upload_category_id').optional().isInt()
], async (req, res) => {
try {
const errors = validationResult(req);
@@ -36,7 +38,9 @@ router.post('/', adminAuth, [
password,
welcome_message = '',
color_theme = null,
expiration_days = 30
expiration_days = 30,
allow_user_uploads = false,
upload_category_id = null
} = req.body;
// Generate unique slug
@@ -79,7 +83,9 @@ router.post('/', adminAuth, [
color_theme,
share_link: shareLink,
expires_at: expires_at.toISOString(),
created_at: new Date().toISOString()
created_at: new Date().toISOString(),
allow_user_uploads,
upload_category_id
});
// Log activity
@@ -256,7 +262,9 @@ router.put('/:id', adminAuth, [
body('is_active').optional().isBoolean(),
body('expires_at').optional().isISO8601(),
body('welcome_message').optional().trim(),
body('color_theme').optional().trim()
body('color_theme').optional().trim(),
body('allow_user_uploads').optional().isBoolean(),
body('upload_category_id').optional().isInt()
], async (req, res) => {
try {
const errors = validationResult(req);
+163
View File
@@ -0,0 +1,163 @@
const express = require('express');
const { db } = require('../database/db');
const { adminAuth } = require('../middleware/auth');
const fs = require('fs').promises;
const path = require('path');
const os = require('os');
const router = express.Router();
// Get system version
router.get('/version', adminAuth, async (req, res) => {
try {
// Read backend version from package.json
const packageJson = require('../../../package.json');
res.json({
backend: packageJson.version,
frontend: '1.0.0', // This will be set by frontend
node: process.version,
environment: process.env.NODE_ENV || 'production'
});
} catch (error) {
console.error('Error fetching version:', error);
res.status(500).json({ error: 'Failed to fetch version information' });
}
});
// Get comprehensive system status
router.get('/status', adminAuth, async (req, res) => {
try {
// Database size
const dbPath = path.join(__dirname, '../../data/photo_sharing.db');
let dbSize = 0;
try {
const stats = await fs.stat(dbPath);
dbSize = stats.size;
} catch (error) {
console.error('Error getting database size:', error);
}
// Count various entities
const [eventsCount] = await db('events').count('* as count');
const [photosCount] = await db('photos').count('* as count');
const [adminsCount] = await db('admin_users').count('* as count');
const [categoriesCount] = await db('photo_categories').count('* as count');
// Email queue status
const [pendingEmails] = await db('email_queue').where('status', 'pending').count('* as count');
const [sentEmails] = await db('email_queue').where('status', 'sent').count('* as count');
const [failedEmails] = await db('email_queue').where('status', 'failed').count('* as count');
// Activity logs count
const [activityCount] = await db('activity_logs').count('* as count');
// System info
const systemInfo = {
platform: os.platform(),
arch: os.arch(),
hostname: os.hostname(),
uptime: Math.floor(process.uptime()),
nodeVersion: process.version,
memory: {
total: os.totalmem(),
free: os.freemem(),
used: os.totalmem() - os.freemem()
},
cpu: {
model: os.cpus()[0]?.model || 'Unknown',
cores: os.cpus().length
}
};
// Build response
const status = {
database: {
size: dbSize,
tables: {
events: eventsCount.count,
photos: photosCount.count,
admins: adminsCount.count,
categories: categoriesCount.count,
activityLogs: activityCount.count
}
},
emailQueue: {
pending: pendingEmails.count,
sent: sentEmails.count,
failed: failedEmails.count
},
system: systemInfo,
services: {
fileWatcher: { status: 'active' }, // These would ideally check actual service status
expirationChecker: { status: 'active' },
emailProcessor: { status: 'active' }
},
timestamp: new Date()
};
res.json(status);
} catch (error) {
console.error('Error fetching system status:', error);
res.status(500).json({ error: 'Failed to fetch system status' });
}
});
// Get database statistics
router.get('/database', adminAuth, async (req, res) => {
try {
// Get table info
const tables = [
'events', 'photos', 'admin_users', 'photo_categories',
'cms_pages', 'email_templates', 'email_queue', 'activity_logs',
'app_settings', 'email_configs', 'access_logs', 'migrations'
];
const tableInfo = [];
for (const table of tables) {
try {
const [count] = await db(table).count('* as count');
// Get last update time
let lastUpdate = null;
try {
const lastRow = await db(table)
.orderBy('updated_at', 'desc')
.orOrderBy('created_at', 'desc')
.orOrderBy('timestamp', 'desc')
.orOrderBy('applied_at', 'desc')
.first();
if (lastRow) {
lastUpdate = lastRow.updated_at || lastRow.created_at || lastRow.timestamp || lastRow.applied_at;
}
} catch (e) {
// Table might not have timestamp columns
}
tableInfo.push({
name: table,
rows: count.count,
lastUpdate
});
} catch (error) {
// Table might not exist
tableInfo.push({
name: table,
rows: 0,
error: error.message
});
}
}
res.json({
tables: tableInfo,
timestamp: new Date()
});
} catch (error) {
console.error('Error fetching database info:', error);
res.status(500).json({ error: 'Failed to fetch database information' });
}
});
module.exports = router;
+73
View File
@@ -304,4 +304,77 @@ router.get('/:slug/stats', verifyGalleryAccess, async (req, res) => {
}
});
// User photo upload endpoint
router.post('/:eventId/upload', verifyGalleryAccess, async (req, res) => {
try {
const eventId = parseInt(req.params.eventId);
// Verify the event matches the token
if (req.event.id !== eventId) {
return res.status(403).json({ error: 'Access denied' });
}
// Check if user uploads are allowed
if (!req.event.allow_user_uploads) {
return res.status(403).json({ error: 'User uploads are not allowed for this event' });
}
// Import multer and photo processing
const multer = require('multer');
const upload = multer({
dest: '/tmp/uploads/',
limits: {
fileSize: 50 * 1024 * 1024, // 50MB
files: 10 // Max 10 files at once
},
fileFilter: (req, file, cb) => {
const allowedTypes = ['image/jpeg', 'image/png', 'image/webp'];
if (allowedTypes.includes(file.mimetype)) {
cb(null, true);
} else {
cb(new Error('Invalid file type'));
}
}
}).array('photos', 10);
// Handle upload
upload(req, res, async (err) => {
if (err) {
console.error('Upload error:', err);
return res.status(400).json({ error: err.message });
}
if (!req.files || req.files.length === 0) {
return res.status(400).json({ error: 'No files uploaded' });
}
const { processUploadedPhotos } = require('../services/photoProcessor');
const categoryId = req.body.category_id || req.event.upload_category_id || null;
try {
// Process uploaded photos
const results = await processUploadedPhotos(req.files, eventId, 'user', categoryId);
// Clean up temp files
const fs = require('fs').promises;
for (const file of req.files) {
await fs.unlink(file.path).catch(console.error);
}
res.json({
message: 'Photos uploaded successfully',
count: results.length,
photos: results
});
} catch (processError) {
console.error('Photo processing error:', processError);
res.status(500).json({ error: 'Failed to process photos' });
}
});
} catch (error) {
console.error('Upload route error:', error);
res.status(500).json({ error: 'Failed to upload photos' });
}
});
module.exports = router;
+110
View File
@@ -0,0 +1,110 @@
const path = require('path');
const fs = require('fs').promises;
const { db } = require('../database/db');
const { generateThumbnail } = require('./imageProcessor');
const { generatePhotoFilename } = require('../utils/filenameSanitizer');
// Get storage path from environment or default
const getStoragePath = () => process.env.STORAGE_PATH || path.join(__dirname, '../../../storage');
async function processUploadedPhotos(files, eventId, uploadedBy = 'admin', categoryId = null) {
const uploadedPhotos = [];
// Get event details
const event = await db('events').where({ id: eventId }).first();
if (!event) {
throw new Error('Event not found');
}
// Process each file
for (const file of files) {
const trx = await db.transaction();
try {
// Get category info if provided
let category = null;
let counter = 1;
const parsedCategoryId = categoryId ? parseInt(categoryId) : null;
if (parsedCategoryId) {
// Get category and update counter
category = await trx('photo_categories')
.where({ id: parsedCategoryId })
.first();
if (category) {
counter = (category.photo_counter || 0) + 1;
await trx('photo_categories')
.where({ id: parsedCategoryId })
.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
);
// Move file to event folder
const destPath = path.join(getStoragePath(), 'events/active', event.slug);
await fs.mkdir(destPath, { recursive: true });
const newPath = path.join(destPath, newFilename);
await fs.rename(file.path, newPath);
// Generate thumbnail
const thumbnailPath = await generateThumbnail(newPath);
// Calculate relative paths
const storagePath = getStoragePath();
const relativePath = path.relative(path.join(storagePath, 'events/active'), newPath);
const relativeThumbPath = thumbnailPath; // thumbnailPath is already relative to storage root
// Add to database with uploaded_by field
const [photoId] = await trx('photos').insert({
event_id: eventId,
filename: newFilename,
path: relativePath,
thumbnail_path: relativeThumbPath,
category_id: parsedCategoryId || null,
type: 'individual',
size_bytes: file.size,
uploaded_by: uploadedBy
});
// Commit transaction
await trx.commit();
uploadedPhotos.push({
id: photoId,
filename: newFilename,
size: file.size,
category_id: parsedCategoryId || null,
uploaded_by: uploadedBy
});
} catch (error) {
console.error(`Error processing file ${file.originalname}:`, error);
if (trx) await trx.rollback();
// Continue with other files
}
}
return uploadedPhotos;
}
module.exports = {
processUploadedPhotos
};