Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ffcfd9766d | |||
| 3501a52f0e | |||
| 5ca598b80a | |||
| 4e2075c638 |
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "picpeak-backend",
|
"name": "picpeak-backend",
|
||||||
"version": "1.0.58",
|
"version": "1.0.60",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "picpeak-backend",
|
"name": "picpeak-backend",
|
||||||
"version": "1.0.58",
|
"version": "1.0.60",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"adm-zip": "^0.5.16",
|
"adm-zip": "^0.5.16",
|
||||||
"archiver": "^5.3.1",
|
"archiver": "^5.3.1",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "picpeak-backend",
|
"name": "picpeak-backend",
|
||||||
"version": "1.0.58",
|
"version": "1.0.60",
|
||||||
"description": "Backend for PicPeak event photo sharing platform",
|
"description": "Backend for PicPeak event photo sharing platform",
|
||||||
"main": "server.js",
|
"main": "server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
+72
-1
@@ -97,6 +97,38 @@ app.use(cors(corsOptions));
|
|||||||
const limiter = rateLimit({
|
const limiter = rateLimit({
|
||||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||||
max: process.env.NODE_ENV === 'development' ? 1000 : 100, // More lenient in development
|
max: process.env.NODE_ENV === 'development' ? 1000 : 100, // More lenient in development
|
||||||
|
// Use correct client IP when behind proxy
|
||||||
|
keyGenerator: (req) => {
|
||||||
|
// Get the real client IP from proxy headers
|
||||||
|
const clientIp = req.headers['x-forwarded-for']?.split(',')[0]?.trim() ||
|
||||||
|
req.headers['x-real-ip'] ||
|
||||||
|
req.connection.remoteAddress ||
|
||||||
|
req.ip;
|
||||||
|
|
||||||
|
// Log rate limit key for debugging (only in development)
|
||||||
|
if (process.env.NODE_ENV === 'development' && req.path.includes('/api/')) {
|
||||||
|
logger.debug('Rate limit key generated', {
|
||||||
|
path: req.path,
|
||||||
|
clientIp,
|
||||||
|
headers: {
|
||||||
|
'x-forwarded-for': req.headers['x-forwarded-for'],
|
||||||
|
'x-real-ip': req.headers['x-real-ip']
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return clientIp;
|
||||||
|
},
|
||||||
|
handler: (req, res) => {
|
||||||
|
logger.warn('Rate limit exceeded', {
|
||||||
|
ip: req.headers['x-forwarded-for']?.split(',')[0]?.trim() || req.ip,
|
||||||
|
path: req.path,
|
||||||
|
method: req.method
|
||||||
|
});
|
||||||
|
res.status(429).json({
|
||||||
|
error: 'Too many requests, please try again later.'
|
||||||
|
});
|
||||||
|
},
|
||||||
skip: (req) => {
|
skip: (req) => {
|
||||||
// Skip rate limiting for authenticated admin users
|
// Skip rate limiting for authenticated admin users
|
||||||
if (req.path.startsWith('/api/admin/') && req.headers.authorization) {
|
if (req.path.startsWith('/api/admin/') && req.headers.authorization) {
|
||||||
@@ -118,7 +150,24 @@ const limiter = rateLimit({
|
|||||||
|
|
||||||
const authLimiter = rateLimit({
|
const authLimiter = rateLimit({
|
||||||
windowMs: 15 * 60 * 1000,
|
windowMs: 15 * 60 * 1000,
|
||||||
max: 5 // limit auth attempts
|
max: 5, // limit auth attempts
|
||||||
|
// Use correct client IP when behind proxy
|
||||||
|
keyGenerator: (req) => {
|
||||||
|
// Get the real client IP from proxy headers
|
||||||
|
return req.headers['x-forwarded-for']?.split(',')[0]?.trim() ||
|
||||||
|
req.headers['x-real-ip'] ||
|
||||||
|
req.connection.remoteAddress ||
|
||||||
|
req.ip;
|
||||||
|
},
|
||||||
|
handler: (req, res) => {
|
||||||
|
logger.warn('Auth rate limit exceeded', {
|
||||||
|
ip: req.headers['x-forwarded-for']?.split(',')[0]?.trim() || req.ip,
|
||||||
|
path: req.path
|
||||||
|
});
|
||||||
|
res.status(429).json({
|
||||||
|
error: 'Too many authentication attempts, please try again later.'
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Apply rate limiting - admin routes check will skip for valid admin tokens
|
// Apply rate limiting - admin routes check will skip for valid admin tokens
|
||||||
@@ -158,6 +207,28 @@ app.use('/thumbnails', require('./src/middleware/photoAuth'), setCorsHeaders, se
|
|||||||
// Static file serving for uploads (public - logos, favicons)
|
// Static file serving for uploads (public - logos, favicons)
|
||||||
app.use('/uploads', setCorsHeaders, secureStatic(path.join(storagePath, 'uploads')));
|
app.use('/uploads', setCorsHeaders, secureStatic(path.join(storagePath, 'uploads')));
|
||||||
|
|
||||||
|
// Debug endpoint to check IP detection (only in development)
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
app.get('/api/debug/ip', (req, res) => {
|
||||||
|
const clientIp = req.headers['x-forwarded-for']?.split(',')[0]?.trim() ||
|
||||||
|
req.headers['x-real-ip'] ||
|
||||||
|
req.connection.remoteAddress ||
|
||||||
|
req.ip;
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
detectedIp: clientIp,
|
||||||
|
reqIp: req.ip,
|
||||||
|
headers: {
|
||||||
|
'x-forwarded-for': req.headers['x-forwarded-for'],
|
||||||
|
'x-real-ip': req.headers['x-real-ip'],
|
||||||
|
'x-forwarded-proto': req.headers['x-forwarded-proto'],
|
||||||
|
'x-forwarded-host': req.headers['x-forwarded-host']
|
||||||
|
},
|
||||||
|
trustProxy: app.get('trust proxy')
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Health check endpoint
|
// Health check endpoint
|
||||||
app.get('/health', async (req, res) => {
|
app.get('/health', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ function validateEnvironment() {
|
|||||||
if (name === 'JWT_SECRET' && value) {
|
if (name === 'JWT_SECRET' && value) {
|
||||||
// Check for the insecure default value
|
// Check for the insecure default value
|
||||||
if (value === 'your-secret-key') {
|
if (value === 'your-secret-key') {
|
||||||
errors.push(`CRITICAL: JWT_SECRET is set to the insecure default value. Please set a secure secret key.`);
|
errors.push('CRITICAL: JWT_SECRET is set to the insecure default value. Please set a secure secret key.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check minimum length (should be at least 32 characters for security)
|
// Check minimum length (should be at least 32 characters for security)
|
||||||
|
|||||||
@@ -59,9 +59,9 @@ async function initializeDatabase() {
|
|||||||
)
|
)
|
||||||
`);
|
`);
|
||||||
|
|
||||||
await db.raw(`INSERT INTO events_new SELECT * FROM events`);
|
await db.raw('INSERT INTO events_new SELECT * FROM events');
|
||||||
await db.raw(`DROP TABLE events`);
|
await db.raw('DROP TABLE events');
|
||||||
await db.raw(`ALTER TABLE events_new RENAME TO events`);
|
await db.raw('ALTER TABLE events_new RENAME TO events');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// If the migration fails, it might already have been applied
|
// If the migration fails, it might already have been applied
|
||||||
console.log('Color theme migration may have already been applied');
|
console.log('Color theme migration may have already been applied');
|
||||||
|
|||||||
@@ -251,7 +251,7 @@ router.post('/:id/restore', adminAuth, async (req, res) => {
|
|||||||
} catch (statError) {
|
} catch (statError) {
|
||||||
console.error(`Failed to stat file: ${actualFilePath}`);
|
console.error(`Failed to stat file: ${actualFilePath}`);
|
||||||
console.error(`Entry name was: ${entry.entryName}`);
|
console.error(`Entry name was: ${entry.entryName}`);
|
||||||
console.error(`Error:`, statError.message);
|
console.error('Error:', statError.message);
|
||||||
// Skip this file if we can't stat it
|
// Skip this file if we can't stat it
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ const path = require('path');
|
|||||||
const { archiveEvent } = require('../services/archiveService');
|
const { archiveEvent } = require('../services/archiveService');
|
||||||
const { queueEmail } = require('../services/emailProcessor');
|
const { queueEmail } = require('../services/emailProcessor');
|
||||||
const { escapeLikePattern } = require('../utils/sqlSecurity');
|
const { escapeLikePattern } = require('../utils/sqlSecurity');
|
||||||
const { formatDate } = require('../utils/dateFormatter');
|
// formatDate import removed - dates are formatted by email processor
|
||||||
const { validatePasswordInContext, getBcryptRounds } = require('../utils/passwordValidation');
|
const { validatePasswordInContext, getBcryptRounds } = require('../utils/passwordValidation');
|
||||||
const { formatBoolean } = require('../utils/dbCompat');
|
const { formatBoolean } = require('../utils/dbCompat');
|
||||||
|
|
||||||
@@ -135,8 +135,7 @@ router.post('/', adminAuth, [
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Queue creation email
|
// Queue creation email
|
||||||
// Determine language based on email domain
|
// Language detection is handled by email processor
|
||||||
const emailLang = host_email.endsWith('.de') ? 'de' : 'en';
|
|
||||||
|
|
||||||
await db('email_queue').insert({
|
await db('email_queue').insert({
|
||||||
event_id: eventId,
|
event_id: eventId,
|
||||||
@@ -145,10 +144,10 @@ router.post('/', adminAuth, [
|
|||||||
email_data: JSON.stringify({
|
email_data: JSON.stringify({
|
||||||
host_name: host_name,
|
host_name: host_name,
|
||||||
event_name,
|
event_name,
|
||||||
event_date: await formatDate(event_date, emailLang),
|
event_date: event_date, // Pass raw date - will be formatted by email processor
|
||||||
gallery_link: shareLink,
|
gallery_link: shareLink,
|
||||||
gallery_password: password,
|
gallery_password: password,
|
||||||
expiry_date: await formatDate(expires_at, emailLang),
|
expiry_date: expires_at.toISOString(), // Pass ISO string - will be formatted by email processor
|
||||||
welcome_message: welcome_message || ''
|
welcome_message: welcome_message || ''
|
||||||
}),
|
}),
|
||||||
status: 'pending',
|
status: 'pending',
|
||||||
@@ -553,10 +552,10 @@ router.post('/:id/reset-password', adminAuth, async (req, res) => {
|
|||||||
await queueEmail(id, event.host_email, 'gallery_created', {
|
await queueEmail(id, event.host_email, 'gallery_created', {
|
||||||
host_name: event.host_email.split('@')[0],
|
host_name: event.host_email.split('@')[0],
|
||||||
event_name: event.event_name,
|
event_name: event.event_name,
|
||||||
event_date: new Date(event.event_date).toLocaleDateString(),
|
event_date: event.event_date, // Pass raw date - will be formatted by email processor
|
||||||
gallery_link: event.share_link,
|
gallery_link: event.share_link,
|
||||||
gallery_password: newPassword,
|
gallery_password: newPassword,
|
||||||
expiry_date: new Date(event.expires_at).toLocaleDateString()
|
expiry_date: event.expires_at // Pass raw date - will be formatted by email processor
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -603,18 +602,16 @@ router.post('/:id/resend-email', adminAuth, async (req, res) => {
|
|||||||
galleryPassword = '{{password_security_message}}';
|
galleryPassword = '{{password_security_message}}';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format dates in a neutral format - the email processor will localize them
|
// Dates will be formatted by the email processor based on recipient language
|
||||||
const eventDate = new Date(event.event_date);
|
|
||||||
const expiryDate = new Date(event.expires_at);
|
|
||||||
|
|
||||||
// Queue the email
|
// Queue the email
|
||||||
await queueEmail(id, event.host_email, 'gallery_created', {
|
await queueEmail(id, event.host_email, 'gallery_created', {
|
||||||
host_name: event.host_name || event.host_email.split('@')[0],
|
host_name: event.host_name || event.host_email.split('@')[0],
|
||||||
event_name: event.event_name,
|
event_name: event.event_name,
|
||||||
event_date: eventDate.toISOString().split('T')[0], // YYYY-MM-DD format
|
event_date: event.event_date, // Pass raw date - will be formatted by email processor
|
||||||
gallery_link: event.share_link,
|
gallery_link: event.share_link,
|
||||||
gallery_password: galleryPassword,
|
gallery_password: galleryPassword,
|
||||||
expiry_date: expiryDate.toISOString().split('T')[0], // YYYY-MM-DD format
|
expiry_date: event.expires_at, // Pass raw date - will be formatted by email processor
|
||||||
welcome_message: event.welcome_message || '',
|
welcome_message: event.welcome_message || '',
|
||||||
eventId: id,
|
eventId: id,
|
||||||
isResend: true // Flag to indicate this is a resend
|
isResend: true // Flag to indicate this is a resend
|
||||||
|
|||||||
@@ -87,10 +87,10 @@ router.post('/', adminAuth, [
|
|||||||
await queueEmail(eventId, host_email, 'gallery_created', {
|
await queueEmail(eventId, host_email, 'gallery_created', {
|
||||||
host_name: host_email.split('@')[0], // Extract name from email
|
host_name: host_email.split('@')[0], // Extract name from email
|
||||||
event_name,
|
event_name,
|
||||||
event_date: new Date(event_date).toLocaleDateString(),
|
event_date: event_date, // Pass raw date - will be formatted by email processor
|
||||||
gallery_link: shareLink,
|
gallery_link: shareLink,
|
||||||
gallery_password: password,
|
gallery_password: password,
|
||||||
expiry_date: expires_at.toLocaleDateString(),
|
expiry_date: expires_at.toISOString(), // Pass ISO string - will be formatted by email processor
|
||||||
welcome_message: welcome_message || ''
|
welcome_message: welcome_message || ''
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ function escapeLikePattern(input) {
|
|||||||
.replace(/\\/g, '\\\\') // Escape backslashes first
|
.replace(/\\/g, '\\\\') // Escape backslashes first
|
||||||
.replace(/%/g, '\\%') // Escape percent signs
|
.replace(/%/g, '\\%') // Escape percent signs
|
||||||
.replace(/_/g, '\\_') // Escape underscores
|
.replace(/_/g, '\\_') // Escape underscores
|
||||||
.replace(/'/g, "''"); // Escape single quotes for safety
|
.replace(/'/g, '\'\''); // Escape single quotes for safety
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user