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
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:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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' });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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' });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user