feat: support per-gallery password toggle

This commit is contained in:
2025-10-01 14:18:14 +02:00
parent 45e835a51a
commit 5d6c061f1c
23 changed files with 1019 additions and 349 deletions
+40 -6
View File
@@ -2,13 +2,48 @@ const jwt = require('jsonwebtoken');
const { db, withRetry } = require('../database/db');
const { formatBoolean } = require('../utils/dbCompat');
const { getGalleryTokenFromRequest } = require('../utils/tokenUtils');
const logger = require('../utils/logger');
// Middleware to verify gallery access
async function verifyGalleryAccess(req, res, next) {
try {
const requestedSlug = req.params.slug || req.requestedSlug;
const token = getGalleryTokenFromRequest(req, requestedSlug);
let event;
if (!token) {
if (!requestedSlug) {
return res.status(401).json({ error: 'No token provided' });
}
event = await withRetry(async () => {
return await db('events')
.where({
slug: requestedSlug,
is_active: formatBoolean(true),
is_archived: formatBoolean(false)
})
.select('*')
.first();
});
if (!event) {
return res.status(404).json({ error: 'Gallery not found or expired' });
}
const requiresPassword = !(event.require_password === false || event.require_password === 0 || event.require_password === '0');
if (!requiresPassword) {
req.event = event;
req.sessionID = `gallery_public_${event.id}_${Date.now()}`;
req.clientInfo = {
ip: req.ip || req.connection.remoteAddress || 'unknown',
userAgent: req.get('User-Agent') || 'unknown',
fingerprint: `${req.ip}-${req.get('User-Agent')}`.substring(0, 32),
timestamp: Date.now()
};
return next();
}
return res.status(401).json({ error: 'No token provided' });
}
@@ -26,10 +61,9 @@ async function verifyGalleryAccess(req, res, next) {
throw error;
}
}
console.log('[verifyGalleryAccess] Token decoded successfully, eventId:', decoded.eventId);
logger.debug('[verifyGalleryAccess] Token decoded successfully', { eventId: decoded.eventId, slug: requestedSlug });
// If we have a slug in the URL params or from pre-middleware, verify it matches
let event;
if (requestedSlug) {
// Verify by slug and ensure it matches the token's event
event = await withRetry(async () => {
@@ -62,11 +96,11 @@ async function verifyGalleryAccess(req, res, next) {
}
if (!event) {
console.log('[verifyGalleryAccess] Event not found for slug:', requestedSlug || 'no-slug', 'eventId:', decoded.eventId);
logger.warn('[verifyGalleryAccess] Event not found for slug', { slug: requestedSlug || 'no-slug', tokenEventId: decoded.eventId });
return res.status(404).json({ error: 'Gallery not found or expired' });
}
console.log('[verifyGalleryAccess] Event found:', event.id, event.slug);
logger.debug('[verifyGalleryAccess] Event located', { eventId: event.id, slug: event.slug });
req.event = event;
req.sessionID = decoded.sessionId || `gallery_${event.id}_${Date.now()}`;
@@ -78,10 +112,10 @@ async function verifyGalleryAccess(req, res, next) {
timestamp: Date.now()
};
console.log('[verifyGalleryAccess] Access granted for event:', event.id);
logger.debug('[verifyGalleryAccess] Access granted', { eventId: event.id, slug: event.slug });
next();
} catch (error) {
console.error('Error verifying gallery access:', error);
logger.error('Error verifying gallery access', { error: error.message, stack: error.stack });
res.status(401).json({ error: 'Invalid token' });
}
}
+18 -12
View File
@@ -3,14 +3,13 @@ const jwt = require('jsonwebtoken');
const { db } = require('../database/db');
const { formatBoolean } = require('../utils/dbCompat');
const { getGalleryTokenFromRequest } = require('../utils/tokenUtils');
const logger = require('../utils/logger');
async function photoAuth(req, res, next) {
try {
// Extract event slug from the path
let eventSlug;
console.log('PhotoAuth middleware - path:', req.path);
// For thumbnails, we need to parse the filename to get the event info
if (req.path.startsWith('/thumb_')) {
// For now, we'll rely on JWT token for thumbnail access
@@ -80,29 +79,36 @@ async function photoAuth(req, res, next) {
// For both thumbnails and photos with admin token, allow access
return next();
}
} catch (err) {
// Token invalid, fall through to password check
console.error('JWT verification failed:', err.message);
} catch (err) {
// Token invalid, fall through to password check
logger.warn('JWT verification failed in photoAuth', { error: err.message });
}
}
// Check for password header (legacy support)
const password = req.headers['x-gallery-password'];
if (!password && !tokenFromRequest) {
return res.status(401).json({ error: 'Authentication required' });
}
// If no eventSlug (thumbnails), and we don't have valid auth yet, deny access
if (!eventSlug && !password) {
if (!eventSlug && !password && !tokenFromRequest) {
return res.status(401).json({ error: 'Authentication required for thumbnails' });
}
const event = await db('events').where({ slug: eventSlug, is_active: formatBoolean(true) }).first();
if (!event) {
return res.status(404).json({ error: 'Gallery not found' });
}
const requiresPassword = !(event.require_password === false || event.require_password === 0 || event.require_password === '0');
if (!requiresPassword) {
req.event = event;
return next();
}
if (!password && !tokenFromRequest) {
return res.status(401).json({ error: 'Authentication required' });
}
if (password) {
const validPassword = await bcrypt.compare(password, event.password_hash);
if (!validPassword) {
@@ -122,7 +128,7 @@ async function photoAuth(req, res, next) {
req.event = event;
next();
} catch (error) {
console.error('Photo auth error:', error);
logger.error('Photo auth error', { error: error.message, stack: error.stack });
res.status(500).json({ error: 'Authentication error' });
}
}