Harden auth cookies and fix native schema for event creation
Mirror to GitHub / mirror (push) Successful in 45s
Test and Lint / backend-test (push) Successful in 1m37s
Test and Lint / frontend-test (push) Successful in 2m8s
Version and Release / version-bump (push) Successful in 1m0s
Version and Release / trigger-drone (push) Successful in 3s

This commit is contained in:
2025-09-18 15:58:58 +02:00
parent bda76ff513
commit 71e7179145
35 changed files with 1055 additions and 381 deletions
+25 -16
View File
@@ -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;
module.exports = router;
+4 -6
View File
@@ -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;
module.exports = router;
+12 -3
View File
@@ -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;
+2 -2
View File
@@ -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;
module.exports = router;
+2 -1
View File
@@ -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' });
}
});
+3 -3
View File
@@ -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;
module.exports = router;
+7 -2
View File
@@ -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;
module.exports = router;
+116 -10
View File
@@ -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;
module.exports = router;
+4 -4
View File
@@ -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' });
}
}
);