fix: show hero image in thumbnail grid on hero gallery layout
Test and Lint / backend-test (push) Successful in 1m12s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m16s
Version and Release / version-bump (push) Successful in 35s
Version and Release / trigger-drone (push) Successful in 3s

This commit is contained in:
2025-07-13 20:03:27 +02:00
parent 10649691de
commit f38014099e
29 changed files with 2028 additions and 18 deletions
+1 -1
View File
@@ -2,7 +2,7 @@ const express = require('express');
const path = require('path');
const fs = require('fs').promises;
const { db } = require('../database/db');
const { adminAuth } = require('../middleware/auth');
const { adminAuth } = require('../middleware/auth-enhanced-v2');
const archiver = require('archiver');
const AdmZip = require('adm-zip');
const router = express.Router();
+1 -1
View File
@@ -2,7 +2,7 @@ const express = require('express');
const bcrypt = require('bcrypt');
const { body, validationResult } = require('express-validator');
const { db, logActivity } = require('../database/db');
const { adminAuth } = require('../middleware/auth');
const { adminAuth } = require('../middleware/auth-enhanced-v2');
const { endSession } = require('../middleware/sessionTimeout');
const { validatePasswordStrength } = require('../utils/passwordGenerator');
const router = express.Router();
+1 -1
View File
@@ -1,7 +1,7 @@
const express = require('express');
const { body, validationResult } = require('express-validator');
const { db, logActivity } = require('../database/db');
const { adminAuth } = require('../middleware/auth');
const { adminAuth } = require('../middleware/auth-enhanced-v2');
const router = express.Router();
// Get all CMS pages
+1 -1
View File
@@ -1,7 +1,7 @@
const express = require('express');
const { body, validationResult } = require('express-validator');
const { db, logActivity } = require('../database/db');
const { adminAuth } = require('../middleware/auth');
const { adminAuth } = require('../middleware/auth-enhanced-v2');
const router = express.Router();
// Get all global categories
+1 -1
View File
@@ -1,6 +1,6 @@
const express = require('express');
const { db } = require('../database/db');
const { adminAuth } = require('../middleware/auth');
const { adminAuth } = require('../middleware/auth-enhanced-v2');
const { sanitizeDays, addDateRangeCondition } = require('../utils/sqlSecurity');
const router = express.Router();
+1 -1
View File
@@ -2,7 +2,7 @@ const express = require('express');
const nodemailer = require('nodemailer');
const { body, validationResult } = require('express-validator');
const { db, logActivity } = require('../database/db');
const { adminAuth } = require('../middleware/auth');
const { adminAuth } = require('../middleware/auth-enhanced-v2');
const router = express.Router();
// Get email configuration
+121
View File
@@ -0,0 +1,121 @@
// This is a partial file showing the enhanced event creation with password validation
// Only the relevant parts are shown - merge with existing adminEvents.js
const { validatePasswordInContext, getBcryptRounds } = require('../utils/passwordValidation');
// Enhanced event creation with password validation
router.post('/', adminAuth, [
body('event_type').isIn(['wedding', 'birthday', 'corporate', 'other']),
body('event_name').notEmpty().trim(),
body('event_date').isDate(),
body('host_email').isEmail().normalizeEmail(),
body('admin_email').isEmail().normalizeEmail(),
body('password').notEmpty(), // Remove the weak isLength validation
body('expiration_days').isInt({ min: 1, max: 365 }).optional(),
body('welcome_message').optional().trim(),
body('color_theme').optional().trim(),
body('allow_user_uploads').optional().isBoolean().toBoolean(),
body('upload_category_id').optional({ nullable: true, checkFalsy: true }).isInt(),
body('host_name').notEmpty().trim()
], async (req, res) => {
try {
console.log('Create event request body:', req.body);
const errors = validationResult(req);
if (!errors.isEmpty()) {
console.error('Validation errors:', errors.array());
return res.status(400).json({ errors: errors.array() });
}
const {
event_type,
event_name,
event_date,
host_name,
host_email,
admin_email,
password,
welcome_message = '',
color_theme = null,
expiration_days = 30,
allow_user_uploads = false,
upload_category_id = null
} = req.body;
// Validate password strength for gallery
const passwordValidation = validatePasswordInContext(password, 'gallery', {
eventName: event_name
});
if (!passwordValidation.valid) {
return res.status(400).json({
error: 'Password does not meet security requirements',
details: passwordValidation.errors,
score: passwordValidation.score,
feedback: passwordValidation.feedback
});
}
// Generate unique slug
const baseSlug = `${event_type}-${event_name.toLowerCase().replace(/[^a-z0-9]/g, '-')}-${event_date}`;
let slug = baseSlug;
let counter = 1;
while (await db('events').where({ slug }).first()) {
slug = `${baseSlug}-${counter}`;
counter++;
}
// Generate share link
const shareToken = crypto.randomBytes(16).toString('hex');
const shareLink = `${process.env.FRONTEND_URL}/gallery/${slug}/${shareToken}`;
// Hash password with configurable rounds
const password_hash = await bcrypt.hash(password, getBcryptRounds());
// Calculate expiration date (days after event date)
const expires_at = new Date(event_date);
expires_at.setDate(expires_at.getDate() + parseInt(expiration_days, 10));
// Create folder structure
const storagePath = process.env.STORAGE_PATH || path.join(__dirname, '../../../storage');
const eventPath = path.join(storagePath, 'events/active', slug);
await fs.mkdir(path.join(eventPath, 'collages'), { recursive: true });
await fs.mkdir(path.join(eventPath, 'individual'), { recursive: true });
// Insert into database
const [eventId] = await db('events').insert({
slug,
event_type,
event_name,
event_date,
host_name,
host_email,
admin_email,
password_hash,
welcome_message,
color_theme,
share_link: shareLink,
expires_at: expires_at.toISOString(),
created_at: new Date().toISOString(),
allow_user_uploads,
upload_category_id
});
// Log activity
await logActivity('event_created',
{
event_type,
expires_at,
password_strength: passwordValidation.score
},
eventId,
{ type: 'admin', id: req.admin.id, name: req.admin.username }
);
// Rest of the implementation remains the same...
// Queue creation email, etc.
} catch (error) {
console.error('Error creating event:', error);
res.status(500).json({ error: 'Failed to create event' });
}
});
+18 -3
View File
@@ -1,7 +1,7 @@
const express = require('express');
const { body, query, validationResult } = require('express-validator');
const { db, logActivity } = require('../database/db');
const { adminAuth } = require('../middleware/auth');
const { adminAuth } = require('../middleware/auth-enhanced-v2');
const router = express.Router();
const bcrypt = require('bcrypt');
const crypto = require('crypto');
@@ -10,6 +10,7 @@ const path = require('path');
const { archiveEvent } = require('../services/archiveService');
const { escapeLikePattern } = require('../utils/sqlSecurity');
const { formatDate } = require('../utils/dateFormatter');
const { validatePasswordInContext, getBcryptRounds } = require('../utils/passwordValidation');
// Create new event
router.post('/', adminAuth, [
@@ -49,6 +50,20 @@ router.post('/', adminAuth, [
upload_category_id = null
} = req.body;
// Validate password strength
const passwordValidation = validatePasswordInContext(password, 'gallery', {
eventName: event_name
});
if (!passwordValidation.valid) {
return res.status(400).json({
error: 'Password does not meet security requirements',
details: passwordValidation.errors,
score: passwordValidation.score,
feedback: passwordValidation.feedback
});
}
// Generate unique slug
const baseSlug = `${event_type}-${event_name.toLowerCase().replace(/[^a-z0-9]/g, '-')}-${event_date}`;
let slug = baseSlug;
@@ -63,8 +78,8 @@ router.post('/', adminAuth, [
const shareToken = crypto.randomBytes(16).toString('hex');
const shareLink = `${process.env.FRONTEND_URL}/gallery/${slug}/${shareToken}`;
// Hash password
const password_hash = await bcrypt.hash(password, 10);
// Hash password with configurable rounds
const password_hash = await bcrypt.hash(password, getBcryptRounds());
// Calculate expiration date (days after event date)
const expires_at = new Date(event_date);
+1 -1
View File
@@ -1,6 +1,6 @@
const express = require('express');
const { db, logActivity } = require('../database/db');
const { adminAuth } = require('../middleware/auth');
const { adminAuth } = require('../middleware/auth-enhanced-v2');
const router = express.Router();
// Get notifications (unread activity logs)
+1 -1
View File
@@ -1,6 +1,6 @@
const express = require('express');
const { db } = require('../database/db');
const { adminAuth } = require('../middleware/auth');
const { adminAuth } = require('../middleware/auth-enhanced-v2');
const fs = require('fs').promises;
const path = require('path');
const os = require('os');
+374
View File
@@ -0,0 +1,374 @@
const express = require('express');
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const { body, validationResult } = require('express-validator');
const { db } = require('../database/db');
const { verifyRecaptcha } = require('../services/recaptcha');
const {
trackFailedAttempt,
trackSuccessfulLogin,
checkAccountLockout,
checkSuspiciousActivity,
getGenericAuthError
} = require('../utils/authSecurity');
const {
validatePasswordInContext,
getBcryptRounds,
logPasswordValidationFailure
} = require('../utils/passwordValidation');
const { endSession } = require('../middleware/sessionTimeout');
const logger = require('../utils/logger');
const router = express.Router();
// Admin login with enhanced security
router.post('/admin/login', [
body('username').notEmpty().trim(),
body('password').notEmpty()
], async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const { username, password, recaptchaToken } = req.body;
const ipAddress = req.ip || req.connection.remoteAddress;
const userAgent = req.headers['user-agent'] || '';
// Check account lockout first
const lockoutStatus = await checkAccountLockout(username);
if (lockoutStatus.isLocked) {
logger.warn('Login attempt on locked account', { username, ipAddress });
return res.status(423).json({
error: 'Account temporarily locked due to too many failed attempts',
retryAfter: lockoutStatus.remainingTime
});
}
// Verify reCAPTCHA
const recaptchaValid = await verifyRecaptcha(recaptchaToken);
if (!recaptchaValid) {
await trackFailedAttempt(username, ipAddress, userAgent);
return res.status(400).json({ error: 'reCAPTCHA verification failed' });
}
// Check for suspicious activity
const isSuspicious = await checkSuspiciousActivity(username, ipAddress);
if (isSuspicious) {
// Still allow login but log it
logger.warn('Suspicious login pattern detected', { username, ipAddress });
}
const admin = await db('admin_users')
.where({ username })
.orWhere({ email: username })
.first();
// Use generic error to prevent user enumeration
if (!admin || !await bcrypt.compare(password, admin.password_hash)) {
await trackFailedAttempt(username, ipAddress, userAgent);
return res.status(401).json({ error: getGenericAuthError() });
}
if (!admin.is_active) {
await trackFailedAttempt(username, ipAddress, userAgent);
return res.status(401).json({ error: getGenericAuthError() });
}
// Successful login
await trackSuccessfulLogin(username, ipAddress, userAgent);
// Update last login and login metadata
await db('admin_users').where('id', admin.id).update({
last_login: new Date(),
last_login_ip: ipAddress
});
// Generate token with additional claims
const token = jwt.sign({
id: admin.id,
username: admin.username,
type: 'admin',
ip: ipAddress,
loginTime: Date.now()
}, process.env.JWT_SECRET, {
expiresIn: '24h',
issuer: 'picpeak-auth'
});
res.json({
token,
user: {
id: admin.id,
username: admin.username,
email: admin.email,
mustChangePassword: admin.must_change_password || false
}
});
} catch (error) {
logger.error('Login error:', error);
res.status(500).json({ error: 'Login failed' });
}
});
// Admin password change with validation
router.post('/admin/change-password', [
body('currentPassword').notEmpty(),
body('newPassword').notEmpty(),
body('confirmPassword').notEmpty()
.custom((value, { req }) => value === req.body.newPassword)
.withMessage('Passwords do not match')
], async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const { currentPassword, newPassword } = req.body;
const adminId = req.admin.id; // From auth middleware
// Get admin user
const admin = await db('admin_users').where({ id: adminId }).first();
if (!admin) {
return res.status(404).json({ error: 'User not found' });
}
// Verify current password
const validPassword = await bcrypt.compare(currentPassword, admin.password_hash);
if (!validPassword) {
return res.status(401).json({ error: 'Current password is incorrect' });
}
// Validate new password
const passwordValidation = validatePasswordInContext(newPassword, 'admin', {
username: admin.username,
email: admin.email
});
if (!passwordValidation.valid) {
logPasswordValidationFailure('admin_password_change', passwordValidation.errors, {
userId: adminId,
username: admin.username
});
return res.status(400).json({
error: 'Password does not meet security requirements',
details: passwordValidation.errors,
score: passwordValidation.score,
feedback: passwordValidation.feedback
});
}
// Hash new password with configurable rounds
const hashedPassword = await bcrypt.hash(newPassword, getBcryptRounds());
// Update password and track change time
await db('admin_users').where('id', adminId).update({
password_hash: hashedPassword,
password_changed_at: new Date(),
must_change_password: false
});
// Log password change
logger.info('Admin password changed', {
userId: adminId,
username: admin.username,
ip: req.ip
});
res.json({
message: 'Password changed successfully',
score: passwordValidation.score
});
} catch (error) {
logger.error('Password change error:', error);
res.status(500).json({ error: 'Failed to change password' });
}
});
// Logout endpoint
router.post('/logout', async (req, res) => {
try {
const token = req.headers.authorization?.split(' ')[1];
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', {
userId: decoded.id,
username: decoded.username,
type: decoded.type
});
} catch (err) {
// Token might be invalid, but still process logout
}
}
res.json({ message: 'Logged out successfully' });
} catch (error) {
logger.error('Logout error:', error);
res.status(500).json({ error: 'Logout failed' });
}
});
// Gallery password verification with enhanced security
router.post('/gallery/verify', [
body('slug').notEmpty().trim(),
body('password').notEmpty()
], async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const { slug, password, recaptchaToken } = req.body;
const ipAddress = req.ip || req.connection.remoteAddress;
const userAgent = req.headers['user-agent'] || '';
// Check gallery-specific lockout
const lockoutStatus = await checkAccountLockout(`gallery:${slug}`);
if (lockoutStatus.isLocked) {
logger.warn('Gallery access attempt on locked gallery', { slug, ipAddress });
return res.status(423).json({
error: 'Too many failed attempts. Please try again later.',
retryAfter: lockoutStatus.remainingTime
});
}
// Verify reCAPTCHA
const recaptchaValid = await verifyRecaptcha(recaptchaToken);
if (!recaptchaValid) {
await trackFailedAttempt(`gallery:${slug}`, ipAddress, userAgent);
return res.status(400).json({ error: 'reCAPTCHA verification failed' });
}
const event = await db('events').where({ slug, is_active: true, is_archived: false }).first();
if (!event) {
// Don't reveal if gallery exists
await trackFailedAttempt(`gallery:${slug}`, ipAddress, userAgent);
return res.status(401).json({ error: 'Invalid gallery or password' });
}
const validPassword = await bcrypt.compare(password, event.password_hash);
if (!validPassword) {
await trackFailedAttempt(`gallery:${slug}`, ipAddress, userAgent);
await db('access_logs').insert({
event_id: event.id,
ip_address: ipAddress,
user_agent: userAgent,
action: 'login_fail'
});
return res.status(401).json({ error: 'Invalid gallery or password' });
}
// Successful access
await trackSuccessfulLogin(`gallery:${slug}`, ipAddress, userAgent);
// Log successful access
await db('access_logs').insert({
event_id: event.id,
ip_address: ipAddress,
user_agent: userAgent,
action: 'login_success'
});
// Generate session token with additional security info
const token = jwt.sign({
eventId: event.id,
eventSlug: event.slug,
type: 'gallery',
ip: ipAddress,
loginTime: Date.now()
}, process.env.JWT_SECRET, {
expiresIn: '24h',
issuer: 'picpeak-auth'
});
res.json({
token,
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('Gallery verification error:', error);
res.status(500).json({ error: 'Verification failed' });
}
});
// Get current session info
router.get('/session', async (req, res) => {
try {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'No token provided' });
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
// Calculate remaining time
const now = Date.now() / 1000;
const remainingTime = Math.max(0, decoded.exp - now);
res.json({
valid: true,
type: decoded.type,
expiresIn: Math.floor(remainingTime),
user: decoded.username || decoded.eventSlug
});
} catch (err) {
res.json({
valid: false,
error: 'Invalid or expired token'
});
}
} catch (error) {
res.status(500).json({ error: 'Session check failed' });
}
});
// Password strength check endpoint (for real-time validation)
router.post('/password-strength', [
body('password').notEmpty(),
body('context').isIn(['admin', 'gallery']).optional()
], async (req, res) => {
try {
const { password, context = 'gallery' } = req.body;
// Get user data if available (for context-aware validation)
const userData = {};
if (context === 'admin' && req.admin) {
userData.username = req.admin.username;
userData.email = req.admin.email;
}
const validation = validatePasswordInContext(password, context, userData);
res.json({
valid: validation.valid,
score: validation.score,
errors: validation.errors,
feedback: validation.feedback
});
} catch (error) {
res.status(500).json({ error: 'Failed to check password strength' });
}
});
module.exports = router;
+1 -1
View File
@@ -3,7 +3,7 @@ const { body, validationResult } = require('express-validator');
const bcrypt = require('bcrypt');
const crypto = require('crypto');
const { db } = require('../database/db');
const { adminAuth } = require('../middleware/auth');
const { adminAuth } = require('../middleware/auth-enhanced-v2');
const fs = require('fs').promises;
const path = require('path');
const router = express.Router();