diff --git a/backend/package-lock.json b/backend/package-lock.json index 7b1d5b5..cbf1dd2 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -13,9 +13,10 @@ "@aws-sdk/s3-request-presigner": "^3.850.0", "adm-zip": "^0.5.16", "archiver": "^5.3.1", - "axios": "^1.10.0", + "axios": "^1.12.2", "bcrypt": "6.0.0", "chokidar": "4.0.3", + "cookie-parser": "^1.4.7", "cors": "^2.8.5", "dotenv": "^16.0.3", "express": "^4.18.2", @@ -3889,13 +3890,13 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz", - "integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", + "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", + "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, @@ -4727,6 +4728,28 @@ "node": ">= 0.6" } }, + "node_modules/cookie-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-parser/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/cookie-signature": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", diff --git a/backend/package.json b/backend/package.json index 24b90da..99771b0 100644 --- a/backend/package.json +++ b/backend/package.json @@ -17,9 +17,10 @@ "@aws-sdk/s3-request-presigner": "^3.850.0", "adm-zip": "^0.5.16", "archiver": "^5.3.1", - "axios": "^1.10.0", + "axios": "^1.12.2", "bcrypt": "6.0.0", "chokidar": "4.0.3", + "cookie-parser": "^1.4.7", "cors": "^2.8.5", "dotenv": "^16.0.3", "express": "^4.18.2", diff --git a/backend/server.js b/backend/server.js index e1091a5..9ceac66 100644 --- a/backend/server.js +++ b/backend/server.js @@ -26,6 +26,11 @@ const { startScheduledBackups } = require('./src/services/databaseBackup'); const { maintenanceMiddleware } = require('./src/middleware/maintenance'); const { sessionTimeoutMiddleware } = require('./src/middleware/sessionTimeout'); const { createRateLimiter, createAuthRateLimiter } = require('./src/services/rateLimitService'); +const cookieParser = require('cookie-parser'); +const { + getAdminTokenFromRequest, + getGalleryTokenFromRequest, +} = require('./src/utils/tokenUtils'); // Import routes const authRoutes = require('./src/routes/auth-enhanced'); @@ -47,14 +52,18 @@ app.set('trust proxy', 'loopback, linklocal, uniquelocal'); const enableHsts = process.env.ENABLE_HSTS === 'true'; const cspDirectives = { defaultSrc: ["'self'"], - scriptSrc: ["'self'", "'unsafe-inline'"], // Required for React + scriptSrc: [ + "'self'", + 'https://www.google.com', + 'https://www.gstatic.com' + ], styleSrc: ["'self'", "'unsafe-inline'", "https:"], // Required for styled components imgSrc: ["'self'", "data:", "https:", "blob:"], // Allow data URLs and external images - connectSrc: ["'self'"], // API connections + connectSrc: ["'self'", 'https://www.google.com', 'https://www.gstatic.com'], // API connections fontSrc: ["'self'", "https:", "data:"], // Web fonts objectSrc: ["'none'"], // Disable plugins mediaSrc: ["'self'"], // Audio/video - frameSrc: ["'none'"], // Disable iframes + frameSrc: ["'self'", 'https://www.google.com'], }; // Only upgrade insecure requests when HSTS explicitly enabled (HTTPS deployment) if (enableHsts) { @@ -62,6 +71,25 @@ if (enableHsts) { cspDirectives.upgradeInsecureRequests = []; } +app.use(cookieParser()); + +app.use((req, res, next) => { + if (!req.headers.authorization) { + const slugMatch = req.path.match(/\/api\/(?:gallery|secure-images)\/([^\/]+)/); + const slug = slugMatch ? slugMatch[1] : req.requestedSlug; + const galleryToken = getGalleryTokenFromRequest(req, slug); + const adminToken = getAdminTokenFromRequest(req); + + if (galleryToken) { + req.headers.authorization = `Bearer ${galleryToken}`; + } else if (adminToken) { + req.headers.authorization = `Bearer ${adminToken}`; + } + } + + next(); +}); + app.use(helmet({ contentSecurityPolicy: { // Avoid helmet adding defaults like upgrade-insecure-requests when not desired diff --git a/backend/src/database/db.js b/backend/src/database/db.js index 67d3612..b7752ac 100644 --- a/backend/src/database/db.js +++ b/backend/src/database/db.js @@ -195,11 +195,24 @@ async function initializeDatabase() { table.string('email_type').notNullable(); // 'creation', 'warning', 'expiration', 'archive_complete' table.json('email_data'); table.string('status').defaultTo('pending'); // 'pending', 'sent', 'failed' + table.datetime('created_at').defaultTo(db.fn.now()); table.datetime('scheduled_at').defaultTo(db.fn.now()); table.datetime('sent_at'); table.text('error_message'); table.integer('retry_count').defaultTo(0); }); + } else { + const hasCreatedAt = await db.schema.hasColumn('email_queue', 'created_at'); + if (!hasCreatedAt) { + await db.schema.alterTable('email_queue', (table) => { + table.datetime('created_at').defaultTo(db.fn.now()); + }); + try { + await db('email_queue').whereNull('created_at').update({ created_at: db.fn.now() }); + } catch (updateError) { + logger.debug('Email queue created_at backfill skipped', { error: updateError.message }); + } + } } // Admin users table @@ -217,6 +230,7 @@ async function initializeDatabase() { table.datetime('updated_at').defaultTo(db.fn.now()); table.datetime('last_login'); table.string('last_login_ip'); + table.string('language', 2).defaultTo('en'); }); } else { // Check if updated_at column exists @@ -252,6 +266,13 @@ async function initializeDatabase() { table.string('last_login_ip'); }); } + + const hasLanguage = await db.schema.hasColumn('admin_users', 'language'); + if (!hasLanguage) { + await db.schema.table('admin_users', (table) => { + table.string('language', 2).defaultTo('en'); + }); + } } // Token revocation tables @@ -327,6 +348,18 @@ async function initializeDatabase() { table.datetime('updated_at').defaultTo(db.fn.now()); }); } + + const defaultLanguageSetting = await db('app_settings') + .where('setting_key', 'default_language') + .first(); + if (!defaultLanguageSetting) { + await db('app_settings').insert({ + setting_key: 'default_language', + setting_value: JSON.stringify('en'), + setting_type: 'general', + updated_at: new Date(), + }); + } // Activity logs table const hasActivityLogsTable = await db.schema.hasTable('activity_logs'); @@ -351,6 +384,86 @@ async function initializeDatabase() { }); } } + + await ensureGlobalCategories(); +} + +// Ensure photo categories exist for new deployments +async function ensureGlobalCategories() { + const hasPhotoCategoriesTable = await db.schema.hasTable('photo_categories'); + if (!hasPhotoCategoriesTable) { + await db.schema.createTable('photo_categories', (table) => { + table.increments('id').primary(); + table.string('name', 100).notNullable(); + table.string('slug', 100).notNullable(); + table.boolean('is_global').defaultTo(true); + table.integer('event_id').references('id').inTable('events').onDelete('CASCADE'); + table.timestamp('created_at').defaultTo(db.fn.now()); + table.unique(['slug', 'event_id']); + }); + } + + const hasCategoryIdColumn = await db.schema.hasColumn('photos', 'category_id'); + if (!hasCategoryIdColumn) { + await db.schema.alterTable('photos', (table) => { + table.integer('category_id').references('id').inTable('photo_categories'); + }); + } + + const hasCmsPagesTable = await db.schema.hasTable('cms_pages'); + if (!hasCmsPagesTable) { + await db.schema.createTable('cms_pages', (table) => { + table.increments('id').primary(); + table.string('slug', 100).unique().notNullable(); + table.text('title_en'); + table.text('title_de'); + table.text('content_en'); + table.text('content_de'); + table.timestamp('updated_at').defaultTo(db.fn.now()); + }); + } + + const categoryCountRow = await db('photo_categories').count({ count: 'id' }).first(); + const categoryCount = categoryCountRow ? Number(categoryCountRow.count) : 0; + if (categoryCount === 0) { + const defaultCategories = [ + { name: 'Ceremony', slug: 'ceremony', is_global: true }, + { name: 'Reception', slug: 'reception', is_global: true }, + { name: 'Portraits', slug: 'portraits', is_global: true }, + { name: 'Group Photos', slug: 'group-photos', is_global: true }, + { name: 'Details', slug: 'details', is_global: true }, + { name: 'Party', slug: 'party', is_global: true }, + ]; + + await db('photo_categories').insert(defaultCategories); + } + + const cmsPages = await db('cms_pages').select('slug'); + const existingSlugs = cmsPages.map((page) => page.slug); + const defaultPages = [ + { + slug: 'impressum', + title_en: 'Legal Notice', + title_de: 'Impressum', + content_en: '
Please edit this content in the admin panel.
', + content_de: 'Bitte bearbeiten Sie diesen Inhalt im Admin-Panel.
', + updated_at: new Date(), + }, + { + slug: 'datenschutz', + title_en: 'Privacy Policy', + title_de: 'Datenschutzerklärung', + content_en: 'Please edit this content in the admin panel.
', + content_de: 'Bitte bearbeiten Sie diesen Inhalt im Admin-Panel.
', + updated_at: new Date(), + }, + ]; + + for (const page of defaultPages) { + if (!existingSlugs.includes(page.slug)) { + await db('cms_pages').insert(page); + } + } } // Helper function to log activities diff --git a/backend/src/middleware/auth-enhanced-v2.js b/backend/src/middleware/auth-enhanced-v2.js index 28176e0..d78ea47 100644 --- a/backend/src/middleware/auth-enhanced-v2.js +++ b/backend/src/middleware/auth-enhanced-v2.js @@ -3,13 +3,14 @@ const { db } = require('../database/db'); const { formatBoolean } = require('../utils/dbCompat'); const { isTokenRevoked } = require('../utils/tokenRevocation'); const logger = require('../utils/logger'); +const { getAdminTokenFromRequest, getGalleryTokenFromRequest } = require('../utils/tokenUtils'); /** * Enhanced admin authentication middleware with revocation checking */ async function adminAuth(req, res, next) { try { - const token = req.headers.authorization?.split(' ')[1]; + const token = getAdminTokenFromRequest(req); if (!token) { return res.status(401).json({ error: 'No token provided' }); } @@ -97,7 +98,8 @@ async function adminAuth(req, res, next) { */ async function galleryAuth(req, res, next) { try { - const token = req.headers.authorization?.split(' ')[1]; + const slug = req.params?.slug || req.requestedSlug; + const token = getGalleryTokenFromRequest(req, slug); if (!token) { return res.status(401).json({ error: 'No token provided' }); } @@ -164,4 +166,4 @@ module.exports = { adminAuth, galleryAuth, // ... other exports -}; \ No newline at end of file +}; diff --git a/backend/src/middleware/auth-enhanced.js b/backend/src/middleware/auth-enhanced.js index 29c1000..db94283 100644 --- a/backend/src/middleware/auth-enhanced.js +++ b/backend/src/middleware/auth-enhanced.js @@ -2,6 +2,7 @@ const jwt = require('jsonwebtoken'); const { db } = require('../database/db'); const { formatBoolean } = require('../utils/dbCompat'); const logger = require('../utils/logger'); +const { getAdminTokenFromRequest, getGalleryTokenFromRequest } = require('../utils/tokenUtils'); /** * Enhanced admin authentication middleware @@ -9,7 +10,7 @@ const logger = require('../utils/logger'); */ async function adminAuth(req, res, next) { try { - const token = req.headers.authorization?.split(' ')[1]; + const token = getAdminTokenFromRequest(req); if (!token) { return res.status(401).json({ error: 'No token provided' }); } @@ -89,7 +90,8 @@ async function adminAuth(req, res, next) { */ async function galleryAuth(req, res, next) { try { - const token = req.headers.authorization?.split(' ')[1]; + const slug = req.params?.slug || req.requestedSlug; + const token = getGalleryTokenFromRequest(req, slug); if (!token) { return res.status(401).json({ error: 'No token provided' }); } @@ -151,7 +153,8 @@ async function galleryAuth(req, res, next) { */ async function photoAuth(req, res, next) { try { - const token = req.headers.authorization?.split(' ')[1]; + const slug = req.params?.slug || req.requestedSlug; + const token = getAdminTokenFromRequest(req) || getGalleryTokenFromRequest(req, slug); if (!token) { return res.status(401).json({ error: 'Authentication required' }); } @@ -235,4 +238,4 @@ module.exports = { galleryAuth, photoAuth, verifyGalleryAccess -}; \ No newline at end of file +}; diff --git a/backend/src/middleware/auth.js b/backend/src/middleware/auth.js index 0270d22..4f855ce 100644 --- a/backend/src/middleware/auth.js +++ b/backend/src/middleware/auth.js @@ -2,10 +2,11 @@ const jwt = require('jsonwebtoken'); const { db } = require('../database/db'); const { formatBoolean } = require('../utils/dbCompat'); const logger = require('../utils/logger'); +const { getAdminTokenFromRequest } = require('../utils/tokenUtils'); async function adminAuth(req, res, next) { try { - const token = req.headers.authorization?.split(' ')[1]; + const token = getAdminTokenFromRequest(req); if (!token) { const clientIp = req.headers['x-forwarded-for']?.split(',')[0]?.trim() || req.headers['x-real-ip'] || diff --git a/backend/src/middleware/gallery.js b/backend/src/middleware/gallery.js index e8d2184..0e25f8f 100644 --- a/backend/src/middleware/gallery.js +++ b/backend/src/middleware/gallery.js @@ -1,17 +1,17 @@ const jwt = require('jsonwebtoken'); const { db, withRetry } = require('../database/db'); const { formatBoolean } = require('../utils/dbCompat'); +const { getGalleryTokenFromRequest } = require('../utils/tokenUtils'); // Middleware to verify gallery access async function verifyGalleryAccess(req, res, next) { try { - const authHeader = req.headers.authorization; - const token = authHeader?.split(' ')[1]; + const requestedSlug = req.params.slug || req.requestedSlug; + const token = getGalleryTokenFromRequest(req, requestedSlug); if (!token) { return res.status(401).json({ error: 'No token provided' }); } - // Try to verify with issuer first, fallback to no issuer for backward compatibility let decoded; try { @@ -29,8 +29,6 @@ async function verifyGalleryAccess(req, res, next) { console.log('[verifyGalleryAccess] Token decoded successfully, eventId:', decoded.eventId); // If we have a slug in the URL params or from pre-middleware, verify it matches - const requestedSlug = req.params.slug || req.requestedSlug; - let event; if (requestedSlug) { // Verify by slug and ensure it matches the token's event @@ -84,10 +82,10 @@ async function verifyGalleryAccess(req, res, next) { next(); } catch (error) { console.error('Error verifying gallery access:', error); - res.status(401).json({ error: 'Invalid token', details: error.message }); + res.status(401).json({ error: 'Invalid token' }); } } module.exports = { verifyGalleryAccess -}; \ No newline at end of file +}; diff --git a/backend/src/middleware/photoAuth.js b/backend/src/middleware/photoAuth.js index 9ba8bc8..2e3546f 100644 --- a/backend/src/middleware/photoAuth.js +++ b/backend/src/middleware/photoAuth.js @@ -2,6 +2,7 @@ const bcrypt = require('bcrypt'); const jwt = require('jsonwebtoken'); const { db } = require('../database/db'); const { formatBoolean } = require('../utils/dbCompat'); +const { getGalleryTokenFromRequest } = require('../utils/tokenUtils'); async function photoAuth(req, res, next) { try { @@ -20,9 +21,9 @@ async function photoAuth(req, res, next) { } // First check for JWT token (from gallery access) - const authHeader = req.headers.authorization; - if (authHeader && authHeader.startsWith('Bearer ')) { - const token = authHeader.replace('Bearer ', ''); + const tokenFromRequest = getGalleryTokenFromRequest(req, eventSlug); + if (tokenFromRequest) { + const token = tokenFromRequest; try { // Try to verify with issuer first, fallback to no issuer for backward compatibility let decoded; @@ -88,7 +89,7 @@ async function photoAuth(req, res, next) { // Check for password header (legacy support) const password = req.headers['x-gallery-password']; - if (!password && !authHeader) { + if (!password && !tokenFromRequest) { return res.status(401).json({ error: 'Authentication required' }); } diff --git a/backend/src/middleware/sessionTimeout.js b/backend/src/middleware/sessionTimeout.js index def5f03..f8fce55 100644 --- a/backend/src/middleware/sessionTimeout.js +++ b/backend/src/middleware/sessionTimeout.js @@ -1,5 +1,6 @@ const jwt = require('jsonwebtoken'); const { db } = require('../database/db'); +const { getAdminTokenFromRequest } = require('../utils/tokenUtils'); // In-memory session tracking (in production, use Redis) const sessions = new Map(); @@ -67,11 +68,7 @@ async function getSessionTimeout() { async function sessionTimeoutMiddleware(req, res, next) { // Skip for non-authenticated routes - if (!req.headers.authorization) { - return next(); - } - - const token = req.headers.authorization.split(' ')[1]; + const token = getAdminTokenFromRequest(req); if (!token) { return next(); } @@ -150,4 +147,4 @@ module.exports = { sessionTimeoutMiddleware, endSession, getActiveSessions -}; \ No newline at end of file +}; diff --git a/backend/src/routes/adminBackup.js b/backend/src/routes/adminBackup.js index bef0e69..203c6e6 100644 --- a/backend/src/routes/adminBackup.js +++ b/backend/src/routes/adminBackup.js @@ -221,7 +221,11 @@ router.post('/test-connection', adminAuth, async (req, res) => { await fs.access(config.path, fs.constants.W_OK); res.json({ success: true, message: 'Local path is writable' }); } catch (error) { - res.json({ success: false, message: 'Cannot write to local path: ' + error.message }); + logger.warn('Local backup path not writable', { + path: config.path, + error: error.message + }); + res.json({ success: false, message: 'Cannot write to local path. Check server logs for details.' }); } break; @@ -243,7 +247,11 @@ router.post('/test-connection', adminAuth, async (req, res) => { const { stdout } = await execAsync(testCommand); res.json({ success: true, message: 'Rsync connection successful' }); } catch (error) { - res.json({ success: false, message: 'Rsync connection failed: ' + error.message }); + logger.warn('Rsync connection test failed', { + destination: config.host || config.destination, + error: error.message + }); + res.json({ success: false, message: 'Rsync connection failed. Check server logs for details.' }); } break; @@ -274,7 +282,7 @@ router.get('/manifest/:backupRunId', adminAuth, async (req, res) => { }); } catch (error) { logger.error('Failed to get backup manifest:', error); - res.status(404).json({ error: error.message || 'Backup manifest not found' }); + res.status(404).json({ error: 'Backup manifest not found' }); } }); @@ -327,7 +335,7 @@ router.get('/manifest/:backupRunId/download', adminAuth, async (req, res) => { } } catch (error) { logger.error('Failed to download backup manifest:', error); - res.status(404).json({ error: error.message || 'Backup manifest not found' }); + res.status(404).json({ error: 'Backup manifest not found' }); } }); @@ -344,7 +352,7 @@ router.get('/manifests/:backupId', adminAuth, async (req, res) => { }); } catch (error) { logger.error('Failed to get backup manifest:', error); - res.status(404).json({ error: error.message || 'Backup manifest not found' }); + res.status(404).json({ error: 'Backup manifest not found' }); } }); @@ -375,7 +383,7 @@ router.get('/manifests/:backupId/download', adminAuth, async (req, res) => { } } catch (error) { logger.error('Failed to download backup manifest:', error); - res.status(404).json({ error: error.message || 'Backup manifest not found' }); + res.status(404).json({ error: 'Backup manifest not found' }); } }); @@ -436,7 +444,7 @@ router.get('/s3/buckets', adminAuth, async (req, res) => { }); } catch (error) { logger.error('Failed to list S3 buckets:', error); - res.status(500).json({ error: 'Failed to list S3 buckets: ' + error.message }); + res.status(500).json({ error: 'Failed to list S3 buckets' }); } }); @@ -473,7 +481,7 @@ router.get('/s3/files', adminAuth, async (req, res) => { }); } catch (error) { logger.error('Failed to list S3 files:', error); - res.status(500).json({ error: 'Failed to list S3 files: ' + error.message }); + res.status(500).json({ error: 'Failed to list S3 files' }); } }); @@ -535,7 +543,7 @@ router.delete('/s3/cleanup', adminAuth, async (req, res) => { }); } catch (error) { logger.error('Failed to cleanup S3 backups:', error); - res.status(500).json({ error: 'Failed to cleanup S3 backups: ' + error.message }); + res.status(500).json({ error: 'Failed to cleanup S3 backups' }); } }); @@ -587,7 +595,7 @@ router.post('/s3/test-upload', adminAuth, async (req, res) => { }); } catch (error) { logger.error('S3 upload test failed:', error); - res.status(500).json({ error: 'S3 upload test failed: ' + error.message }); + res.status(500).json({ error: 'S3 upload test failed' }); } }); @@ -675,7 +683,7 @@ router.get('/download/:backupId', adminAuth, async (req, res) => { } } catch (error) { logger.error('Failed to download backup:', error); - res.status(500).json({ error: 'Failed to download backup: ' + error.message }); + res.status(500).json({ error: 'Failed to download backup' }); } }); @@ -744,7 +752,7 @@ router.get('/checksums', adminAuth, async (req, res) => { }); } catch (error) { logger.error('Failed to get file checksums:', error); - res.status(500).json({ error: 'Failed to get file checksums: ' + error.message }); + res.status(500).json({ error: 'Failed to get file checksums' }); } }); @@ -847,7 +855,7 @@ router.post('/estimate', adminAuth, async (req, res) => { }); } catch (error) { logger.error('Failed to estimate backup size:', error); - res.status(500).json({ error: 'Failed to estimate backup size: ' + error.message }); + res.status(500).json({ error: 'Failed to estimate backup size' }); } }); @@ -945,12 +953,13 @@ async function validateManifestData(manifestData) { } }; } catch (error) { + logger.error('Manifest validation error', { error: error.message }); return { valid: false, - error: `Validation error: ${error.message}`, - details: { error: error.message } + error: 'Validation error encountered while processing manifest', + details: { hint: 'See server logs for diagnostic details.' } }; } } -module.exports = router; \ No newline at end of file +module.exports = router; diff --git a/backend/src/routes/adminEvents.js b/backend/src/routes/adminEvents.js index 3826da1..d42e72e 100644 --- a/backend/src/routes/adminEvents.js +++ b/backend/src/routes/adminEvents.js @@ -515,13 +515,11 @@ router.delete('/:id', adminAuth, async (req, res) => { // Provide more specific error messages if (error.message && error.message.includes('foreign key constraint')) { res.status(500).json({ - error: 'Cannot delete event due to existing references. Please contact support.', - details: error.message + error: 'Cannot delete event due to existing references. Please contact support.' }); } else { res.status(500).json({ - error: 'Failed to delete event', - details: process.env.NODE_ENV === 'development' ? error.message : undefined + error: 'Failed to delete event' }); } } @@ -780,7 +778,7 @@ router.post('/bulk-archive', adminAuth, [ results.failed.push({ id: event.id, name: event.event_name, - error: error.message + error: 'Failed to archive event. Check server logs for details.' }); } } @@ -806,4 +804,4 @@ router.post('/bulk-archive', adminAuth, [ } }); -module.exports = router; \ No newline at end of file +module.exports = router; diff --git a/backend/src/routes/adminExternalMedia.js b/backend/src/routes/adminExternalMedia.js index c6c5102..7927aa9 100644 --- a/backend/src/routes/adminExternalMedia.js +++ b/backend/src/routes/adminExternalMedia.js @@ -4,6 +4,7 @@ const fs = require('fs').promises; const { adminAuth } = require('../middleware/auth'); const { list, resolveExternalPath, getExternalMediaRoot } = require('../services/externalMediaService'); const { db, logActivity } = require('../database/db'); +const logger = require('../utils/logger'); const router = express.Router(); @@ -14,7 +15,11 @@ router.get('/list', adminAuth, async (req, res) => { const result = await list(relPath); res.json(result); } catch (error) { - res.status(400).json({ error: 'Invalid path', details: error.message }); + logger.warn('Invalid external media path requested', { + path: req.query.path, + error: error.message + }); + res.status(400).json({ error: 'Invalid external media path' }); } }); @@ -104,9 +109,13 @@ router.post('/events/:id/import-external', adminAuth, async (req, res) => { res.json({ imported, skipped, thumbnailsQueued: 0 }); } catch (error) { - res.status(500).json({ error: 'Failed to import external media', details: error.message }); + logger.error('External media import failed', { + eventId: req.params.id, + externalPath: req.body?.external_path, + error: error.message + }); + res.status(500).json({ error: 'Failed to import external media' }); } }); module.exports = router; - diff --git a/backend/src/routes/adminFeedback.js b/backend/src/routes/adminFeedback.js index 3cccf7d..fb4ceeb 100644 --- a/backend/src/routes/adminFeedback.js +++ b/backend/src/routes/adminFeedback.js @@ -368,7 +368,7 @@ router.post('/word-filters', res.json({ success: true }); } catch (error) { if (error.message === 'Word filter already exists') { - return res.status(409).json({ error: error.message }); + return res.status(409).json({ error: 'Word filter already exists' }); } logger.error('Error adding word filter:', error); res.status(500).json({ error: 'Failed to add word filter' }); @@ -430,4 +430,4 @@ function convertToCSV(data) { return [csvHeaders, ...csvRows].join('\n'); } -module.exports = router; \ No newline at end of file +module.exports = router; diff --git a/backend/src/routes/adminPhotos.js b/backend/src/routes/adminPhotos.js index f973168..e015467 100644 --- a/backend/src/routes/adminPhotos.js +++ b/backend/src/routes/adminPhotos.js @@ -802,7 +802,8 @@ router.get('/:eventId/debug', adminAuth, async (req, res) => { storagePath: getStoragePath() }); } catch (error) { - res.status(500).json({ error: error.message }); + console.error('Error fetching admin photo debug data:', error); + res.status(500).json({ error: 'Failed to fetch photo debug data' }); } }); diff --git a/backend/src/routes/adminRestore.js b/backend/src/routes/adminRestore.js index 063ff58..b1fe3d6 100644 --- a/backend/src/routes/adminRestore.js +++ b/backend/src/routes/adminRestore.js @@ -82,7 +82,7 @@ router.post('/validate', [ logger.error('Restore validation failed:', error); res.status(400).json({ success: false, - error: error.message, + error: 'Restore validation failed', logs: restoreService.restoreLog }); } @@ -162,7 +162,7 @@ router.post('/start', [ logger.error('Failed to start restore:', error); res.status(500).json({ success: false, - error: error.message + error: 'Failed to start restore operation' }); } }); @@ -458,4 +458,4 @@ async function getBackupConfig() { return config; } -module.exports = router; \ No newline at end of file +module.exports = router; diff --git a/backend/src/routes/adminSystem.js b/backend/src/routes/adminSystem.js index c726ddd..3362397 100644 --- a/backend/src/routes/adminSystem.js +++ b/backend/src/routes/adminSystem.js @@ -5,6 +5,7 @@ const fs = require('fs').promises; const path = require('path'); const os = require('os'); const { formatBoolean } = require('../utils/dbCompat'); +const logger = require('../utils/logger'); const router = express.Router(); // Get system version @@ -208,10 +209,14 @@ router.get('/database', adminAuth, async (req, res) => { }); } catch (error) { // Table might not exist + logger.warn('Failed to retrieve table info', { + table, + error: error.message + }); tableInfo.push({ name: table, rows: 0, - error: error.message + error: 'Unable to retrieve table details' }); } } @@ -226,4 +231,4 @@ router.get('/database', adminAuth, async (req, res) => { } }); -module.exports = router; \ No newline at end of file +module.exports = router; diff --git a/backend/src/routes/auth-enhanced.js b/backend/src/routes/auth-enhanced.js index dbc282d..dced5fc 100644 --- a/backend/src/routes/auth-enhanced.js +++ b/backend/src/routes/auth-enhanced.js @@ -14,6 +14,14 @@ const { } = require('../utils/authSecurity'); const { endSession } = require('../middleware/sessionTimeout'); const logger = require('../utils/logger'); +const { + setAdminAuthCookie, + clearAdminAuthCookie, + setGalleryAuthCookies, + clearGalleryAuthCookies, + getAdminTokenFromRequest, + getGalleryTokenFromRequest, +} = require('../utils/tokenUtils'); const router = express.Router(); // Admin login with enhanced security @@ -91,6 +99,8 @@ router.post('/admin/login', [ expiresIn: '24h', issuer: 'picpeak-auth' }); + + setAdminAuthCookie(res, token); res.json({ token, @@ -110,13 +120,14 @@ router.post('/admin/login', [ // Logout endpoint router.post('/logout', async (req, res) => { try { - const token = req.headers.authorization?.split(' ')[1]; - + const adminToken = getAdminTokenFromRequest(req); + const galleryToken = getGalleryTokenFromRequest(req); + const token = adminToken || galleryToken; + if (token) { // End the session endSession(token); - - // Log the logout + try { const decoded = jwt.verify(token, process.env.JWT_SECRET); logger.info('User logged out', { @@ -124,11 +135,23 @@ router.post('/logout', async (req, res) => { username: decoded.username, type: decoded.type }); + + if (decoded.type === 'admin') { + clearAdminAuthCookie(res); + } else if (decoded.type === 'gallery') { + clearGalleryAuthCookies(res, decoded.eventSlug); + } } catch (err) { - // Token might be invalid, but still process logout + // Token might be invalid, but still process logout and clear cookies + clearAdminAuthCookie(res); + clearGalleryAuthCookies(res); } + } else { + // No token found, but ensure cookies are cleared + clearAdminAuthCookie(res); + clearGalleryAuthCookies(res); } - + res.json({ message: 'Logged out successfully' }); } catch (error) { logger.error('Logout error:', error); @@ -209,6 +232,8 @@ router.post('/gallery/verify', [ expiresIn: '24h', issuer: 'picpeak-auth' }); + + setGalleryAuthCookies(res, token, event.slug); res.json({ token, @@ -230,15 +255,94 @@ router.post('/gallery/verify', [ } }); +// Share link authentication (token-based) +router.post('/gallery/share-login', [ + body('slug').notEmpty().trim(), + body('token').notEmpty() +], async (req, res) => { + try { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ errors: errors.array() }); + } + + const { slug, token } = req.body; + const ipAddress = req.ip || req.connection.remoteAddress; + const userAgent = req.headers['user-agent'] || ''; + + const event = await db('events') + .where({ slug, is_active: formatBoolean(true), is_archived: formatBoolean(false) }) + .first(); + + if (!event) { + return res.status(404).json({ error: 'Gallery not found' }); + } + + let expectedToken = event.share_link; + if (expectedToken && expectedToken.includes('/')) { + expectedToken = expectedToken.split('/').pop(); + } + + if (!expectedToken || token !== expectedToken) { + return res.status(401).json({ error: 'Invalid or expired share link' }); + } + + const jwtToken = jwt.sign({ + eventId: event.id, + eventSlug: event.slug, + type: 'gallery', + ip: ipAddress, + loginTime: Date.now() + }, process.env.JWT_SECRET, { + expiresIn: '24h', + issuer: 'picpeak-auth' + }); + + await trackSuccessfulLogin(`gallery:${slug}:share`, ipAddress, userAgent); + setGalleryAuthCookies(res, jwtToken, event.slug); + + res.json({ + token: jwtToken, + event: { + id: event.id, + event_name: event.event_name, + event_type: event.event_type, + event_date: event.event_date, + welcome_message: event.welcome_message, + color_theme: event.color_theme, + expires_at: event.expires_at, + allow_user_uploads: event.allow_user_uploads, + upload_category_id: event.upload_category_id + } + }); + } catch (error) { + logger.error('Share link authentication error:', error); + res.status(500).json({ error: 'Share link login failed' }); + } +}); + +// Gallery logout to clear cookies +router.post('/gallery/logout', async (req, res) => { + try { + const { slug } = req.body || {}; + clearGalleryAuthCookies(res, slug); + res.json({ message: 'Logged out successfully' }); + } catch (error) { + logger.error('Gallery logout error:', error); + res.status(500).json({ error: 'Logout failed' }); + } +}); + // Get current session info router.get('/session', async (req, res) => { try { - const token = req.headers.authorization?.split(' ')[1]; + const { slug } = req.query; + const token = getAdminTokenFromRequest(req) || getGalleryTokenFromRequest(req, slug); if (!token) { return res.status(401).json({ error: 'No token provided' }); } - + try { const decoded = jwt.verify(token, process.env.JWT_SECRET); @@ -250,7 +354,9 @@ router.get('/session', async (req, res) => { valid: true, type: decoded.type, expiresIn: Math.floor(remainingTime), - user: decoded.username || decoded.eventSlug + user: decoded.username || decoded.eventSlug, + eventSlug: decoded.eventSlug, + adminUsername: decoded.username }); } catch (err) { res.json({ @@ -263,4 +369,4 @@ router.get('/session', async (req, res) => { } }); -module.exports = router; \ No newline at end of file +module.exports = router; diff --git a/backend/src/routes/gallery.js b/backend/src/routes/gallery.js index 4bd0947..8aa18d8 100644 --- a/backend/src/routes/gallery.js +++ b/backend/src/routes/gallery.js @@ -37,7 +37,7 @@ router.get('/:slug/verify-token/:token', async (req, res) => { res.json({ valid: true }); } catch (error) { console.error('Error verifying token:', error); - res.status(500).json({ error: 'Failed to verify token', details: error.message }); + res.status(500).json({ error: 'Failed to verify token' }); } }); @@ -90,7 +90,7 @@ router.get('/:slug/info', async (req, res) => { }); } catch (error) { console.error('Error fetching gallery info:', error); - res.status(500).json({ error: 'Failed to fetch gallery info', details: error.message }); + res.status(500).json({ error: 'Failed to fetch gallery info' }); } }); @@ -239,7 +239,7 @@ router.get('/:slug/photos', verifyGalleryAccess, async (req, res) => { }); } catch (error) { console.error('Error fetching photos:', error); - res.status(500).json({ error: 'Failed to fetch photos', details: error.message }); + res.status(500).json({ error: 'Failed to fetch photos' }); } }); @@ -547,7 +547,7 @@ router.get('/:slug/photo/:photoId', photoId: req.params.photoId, eventId: req.event?.id }); - res.status(500).json({ error: 'Failed to serve photo', details: error.message }); + res.status(500).json({ error: 'Failed to serve photo' }); } } ); diff --git a/backend/src/services/rateLimitService.js b/backend/src/services/rateLimitService.js index 27e0051..2739207 100644 --- a/backend/src/services/rateLimitService.js +++ b/backend/src/services/rateLimitService.js @@ -2,6 +2,7 @@ const rateLimit = require('express-rate-limit'); const jwt = require('jsonwebtoken'); const { db } = require('../database/db'); const logger = require('../utils/logger'); +const { getAdminTokenFromRequest, getGalleryTokenFromRequest } = require('../utils/tokenUtils'); // Cache for rate limit settings let settingsCache = null; @@ -95,12 +96,9 @@ function clearSettingsCache() { */ function isAuthenticated(req) { try { - const authHeader = req.headers.authorization; - if (!authHeader || !authHeader.startsWith('Bearer ')) { - return false; - } - - const token = authHeader.substring(7); + const slugMatch = req.path.match(/\/api\/(?:gallery|secure-images)\/([^\/]+)/); + const slug = slugMatch ? slugMatch[1] : req.requestedSlug; + const token = getAdminTokenFromRequest(req) || getGalleryTokenFromRequest(req, slug); const decoded = jwt.verify(token, process.env.JWT_SECRET); // Check if token is valid @@ -280,4 +278,4 @@ module.exports = { createAuthRateLimiter, isAuthenticated, shouldSkipRateLimit -}; \ No newline at end of file +}; diff --git a/backend/src/utils/tokenUtils.js b/backend/src/utils/tokenUtils.js new file mode 100644 index 0000000..80059b1 --- /dev/null +++ b/backend/src/utils/tokenUtils.js @@ -0,0 +1,122 @@ +const ADMIN_COOKIE_NAME = 'admin_token'; +const GALLERY_COOKIE_NAME = 'gallery_token'; +const GALLERY_COOKIE_PREFIX = 'gallery_token_'; + +const DEFAULT_MAX_AGE_MS = 24 * 60 * 60 * 1000; // 24 hours + +const secureCookie = process.env.COOKIE_SECURE === 'true' || process.env.NODE_ENV === 'production'; +const sameSiteDefault = process.env.COOKIE_SAMESITE || 'Lax'; +const cookieDomain = process.env.COOKIE_DOMAIN; + +function buildCookieBaseOptions() { + const options = { + httpOnly: true, + secure: secureCookie, + sameSite: sameSiteDefault, + path: '/', + }; + + if (cookieDomain) { + options.domain = cookieDomain; + } + + return options; +} + +function buildCookieOptionsWithExpiry(maxAgeMs = DEFAULT_MAX_AGE_MS) { + return { + ...buildCookieBaseOptions(), + maxAge: maxAgeMs, + }; +} + +function sanitizeSlugForCookie(slug = '') { + return String(slug).replace(/[^A-Za-z0-9_-]/g, '_'); +} + +function setAdminAuthCookie(res, token) { + if (!token) return; + res.cookie(ADMIN_COOKIE_NAME, token, buildCookieOptionsWithExpiry()); +} + +function clearAdminAuthCookie(res) { + res.clearCookie(ADMIN_COOKIE_NAME, buildCookieBaseOptions()); +} + +function setGalleryAuthCookies(res, token, slug) { + if (!token) return; + const options = buildCookieOptionsWithExpiry(); + res.cookie(GALLERY_COOKIE_NAME, token, options); + if (slug) { + const cookieName = `${GALLERY_COOKIE_PREFIX}${sanitizeSlugForCookie(slug)}`; + res.cookie(cookieName, token, options); + } +} + +function clearGalleryAuthCookies(res, slug) { + const baseOptions = buildCookieBaseOptions(); + res.clearCookie(GALLERY_COOKIE_NAME, baseOptions); + + const cookies = res.req?.cookies || {}; + + if (slug) { + const cookieName = `${GALLERY_COOKIE_PREFIX}${sanitizeSlugForCookie(slug)}`; + res.clearCookie(cookieName, baseOptions); + } else { + Object.keys(cookies).forEach((name) => { + if (name.startsWith(GALLERY_COOKIE_PREFIX)) { + res.clearCookie(name, baseOptions); + } + }); + } +} + +function getAdminTokenFromRequest(req) { + const header = req.headers?.authorization; + if (header && header.startsWith('Bearer ')) { + return header.substring(7); + } + return req.cookies?.[ADMIN_COOKIE_NAME] || null; +} + +function getGalleryTokenFromRequest(req, slug) { + const header = req.headers?.authorization; + if (header && header.startsWith('Bearer ')) { + return header.substring(7); + } + + if (!req.cookies) { + return null; + } + + if (slug) { + const cookieName = `${GALLERY_COOKIE_PREFIX}${sanitizeSlugForCookie(slug)}`; + if (req.cookies[cookieName]) { + return req.cookies[cookieName]; + } + } + + if (req.cookies[GALLERY_COOKIE_NAME]) { + return req.cookies[GALLERY_COOKIE_NAME]; + } + + const prefixed = Object.keys(req.cookies).find((name) => name.startsWith(GALLERY_COOKIE_PREFIX)); + if (prefixed) { + return req.cookies[prefixed]; + } + + return null; +} + +module.exports = { + ADMIN_COOKIE_NAME, + GALLERY_COOKIE_NAME, + GALLERY_COOKIE_PREFIX, + sanitizeSlugForCookie, + setAdminAuthCookie, + clearAdminAuthCookie, + setGalleryAuthCookies, + clearGalleryAuthCookies, + getAdminTokenFromRequest, + getGalleryTokenFromRequest, +}; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index f2d0cc6..0bfcc29 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -20,14 +20,14 @@ "@types/dompurify": "^3.0.5", "@types/lodash": "^4.17.20", "@types/react-google-recaptcha": "^2.1.9", - "axios": "^1.3.2", + "axios": "^1.12.2", "clsx": "^2.0.0", "date-fns": "4.1.0", "dompurify": "^3.2.6", "i18next": "^25.3.1", "i18next-browser-languagedetector": "^8.2.0", "i18next-http-backend": "^3.0.2", - "js-cookie": "^3.0.5", + "linkifyjs": "^4.3.2", "lodash": "^4.17.21", "lowlight": "^2.9.0", "lucide-react": "0.525.0", @@ -44,7 +44,6 @@ }, "devDependencies": { "@eslint/js": "^9.29.0", - "@types/js-cookie": "^3.0.6", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", "@vitejs/plugin-react": "^4.5.2", @@ -57,7 +56,7 @@ "tailwindcss": "^3.3.0", "typescript": "~5.8.3", "typescript-eslint": "^8.34.1", - "vite": "^7.0.0" + "vite": "^7.1.6" } }, "node_modules/@alloc/quick-lru": { @@ -2014,13 +2013,6 @@ "@types/unist": "^2" } }, - "node_modules/@types/js-cookie": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-3.0.6.tgz", - "integrity": "sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -2549,13 +2541,13 @@ } }, "node_modules/axios": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz", - "integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", + "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", + "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, @@ -3903,15 +3895,6 @@ "jiti": "bin/jiti.js" } }, - "node_modules/js-cookie": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", - "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", - "license": "MIT", - "engines": { - "node": ">=14" - } - }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -4032,9 +4015,9 @@ } }, "node_modules/linkifyjs": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.3.1.tgz", - "integrity": "sha512-DRSlB9DKVW04c4SUdGvKK5FR6be45lTU9M76JnngqPeeGDqPwYc0zdUErtsNVMtxPXgUWV4HbXbnC4sNyBxkYg==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.3.2.tgz", + "integrity": "sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==", "license": "MIT" }, "node_modules/locate-path": { @@ -5524,14 +5507,14 @@ } }, "node_modules/tinyglobby": { - "version": "0.2.14", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", - "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", "dev": true, "license": "MIT", "dependencies": { - "fdir": "^6.4.4", - "picomatch": "^4.0.2" + "fdir": "^6.5.0", + "picomatch": "^4.0.3" }, "engines": { "node": ">=12.0.0" @@ -5541,11 +5524,14 @@ } }, "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.4.6", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", - "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, "peerDependencies": { "picomatch": "^3 || ^4" }, @@ -5731,18 +5717,18 @@ "license": "MIT" }, "node_modules/vite": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.0.5.tgz", - "integrity": "sha512-1mncVwJxy2C9ThLwz0+2GKZyEXuC3MyWtAAlNftlZZXZDP3AJt5FmwcMit/IGGaNZ8ZOB2BNO/HFUB+CpN0NQw==", + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.6.tgz", + "integrity": "sha512-SRYIB8t/isTwNn8vMB3MR6E+EQZM/WG1aKmmIUCfDXfVvKfc20ZpamngWHKzAmmu9ppsgxsg4b2I7c90JZudIQ==", "dev": true, "license": "MIT", "dependencies": { "esbuild": "^0.25.0", - "fdir": "^6.4.6", - "picomatch": "^4.0.2", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", "postcss": "^8.5.6", - "rollup": "^4.40.0", - "tinyglobby": "^0.2.14" + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" }, "bin": { "vite": "bin/vite.js" @@ -5806,11 +5792,14 @@ } }, "node_modules/vite/node_modules/fdir": { - "version": "6.4.6", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", - "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, "peerDependencies": { "picomatch": "^3 || ^4" }, diff --git a/frontend/package.json b/frontend/package.json index 488196d..1de7657 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -23,14 +23,14 @@ "@types/dompurify": "^3.0.5", "@types/lodash": "^4.17.20", "@types/react-google-recaptcha": "^2.1.9", - "axios": "^1.3.2", + "axios": "^1.12.2", "clsx": "^2.0.0", "date-fns": "4.1.0", "dompurify": "^3.2.6", "i18next": "^25.3.1", "i18next-browser-languagedetector": "^8.2.0", "i18next-http-backend": "^3.0.2", - "js-cookie": "^3.0.5", + "linkifyjs": "^4.3.2", "lodash": "^4.17.21", "lowlight": "^2.9.0", "lucide-react": "0.525.0", @@ -47,7 +47,6 @@ }, "devDependencies": { "@eslint/js": "^9.29.0", - "@types/js-cookie": "^3.0.6", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", "@vitejs/plugin-react": "^4.5.2", @@ -60,6 +59,6 @@ "tailwindcss": "^3.3.0", "typescript": "~5.8.3", "typescript-eslint": "^8.34.1", - "vite": "^7.0.0" + "vite": "^7.1.6" } } diff --git a/frontend/src/components/MaintenanceWrapper.tsx b/frontend/src/components/MaintenanceWrapper.tsx index c6b34f0..e1708b0 100644 --- a/frontend/src/components/MaintenanceWrapper.tsx +++ b/frontend/src/components/MaintenanceWrapper.tsx @@ -1,9 +1,9 @@ -import React, { useEffect } from 'react'; +import React, { useEffect, useState } from 'react'; import { useLocation } from 'react-router-dom'; import { useQuery } from '@tanstack/react-query'; import { MaintenanceMode } from './MaintenanceMode'; import { useMaintenanceMode } from '../contexts/MaintenanceContext'; -import { setMaintenanceModeCallback, api, getAuthToken } from '../config/api'; +import { setMaintenanceModeCallback, api } from '../config/api'; interface MaintenanceWrapperProps { children: React.ReactNode; @@ -12,10 +12,38 @@ interface MaintenanceWrapperProps { export const MaintenanceWrapper: React.FC