diff --git a/README.md b/README.md index 22b8c0c..0cb80e0 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,18 @@ # 📸 PicPeak - Open Source Photo Sharing for Events -[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -[![Docker](https://img.shields.io/badge/docker-%230db7ed.svg?style=flat&logo=docker&logoColor=white)](https://www.docker.com/) -[![Node.js](https://img.shields.io/badge/node.js-6DA55F?style=flat&logo=node.js&logoColor=white)](https://nodejs.org/) -[![React](https://img.shields.io/badge/react-%2320232a.svg?style=flat&logo=react&logoColor=%2361DAFB)](https://reactjs.org/) +
+ PicPeak Logo + + [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) + [![Docker](https://img.shields.io/badge/docker-%230db7ed.svg?style=flat&logo=docker&logoColor=white)](https://www.docker.com/) + [![Node.js](https://img.shields.io/badge/node.js-6DA55F?style=flat&logo=node.js&logoColor=white)](https://nodejs.org/) + [![React](https://img.shields.io/badge/react-%2320232a.svg?style=flat&logo=react&logoColor=%2361DAFB)](https://reactjs.org/) +
**PicPeak** is a powerful, self-hosted open-source alternative to commercial photo-sharing platforms like PicDrop.com and Scrapbook.de. Designed specifically for photographers and event organizers, PicPeak makes it simple to share beautiful, time-limited photo galleries with clients while maintaining full control over your data and branding. ![PicPeak Gallery Preview](https://github.com/the-luap/picpeak/assets/placeholder-hero.png) +> 📸 *Gallery preview will be updated soon with latest interface* ## 🌟 Why Choose PicPeak? @@ -124,17 +129,34 @@ Found a security issue? Please email security@example.com ## 📸 Screenshots +### 🎛️ **Admin Dashboard** +Get a complete overview of your photo galleries, analytics, and system status. + +PicPeak Admin Dashboard + +### 📊 **Analytics & Insights** +Track gallery performance, view statistics, and monitor user engagement. + +PicPeak Analytics Dashboard + +### 📁 **Event Management** +Organize and manage your photo galleries with intuitive event management tools. + +PicPeak Events Management + +### ✨ **Key Interface Highlights** +
-View Gallery Examples +👆 Click to see more interface details -### Admin Dashboard -![Admin Dashboard](https://github.com/the-luap/picpeak/assets/placeholder-admin.png) +#### What makes PicPeak's interface special: -### Client Gallery View -![Gallery View](https://github.com/the-luap/picpeak/assets/placeholder-gallery.png) - -### Mobile Experience -![Mobile View](https://github.com/the-luap/picpeak/assets/placeholder-mobile.png) +- **🎨 Clean Design**: Modern, photographer-friendly interface +- **📱 Responsive**: Perfect on desktop, tablet, and mobile +- **⚡ Fast Loading**: Optimized for quick photo browsing +- **🔒 Secure Access**: Password-protected galleries with expiration +- **📤 Easy Uploads**: Drag & drop functionality for effortless photo management +- **🎯 Client-Focused**: Intuitive gallery experience for your clients
diff --git a/backend/src/middleware/auth-enhanced-v2.js b/backend/src/middleware/auth-enhanced-v2.js index d0661d7..28176e0 100644 --- a/backend/src/middleware/auth-enhanced-v2.js +++ b/backend/src/middleware/auth-enhanced-v2.js @@ -1,5 +1,6 @@ const jwt = require('jsonwebtoken'); const { db } = require('../database/db'); +const { formatBoolean } = require('../utils/dbCompat'); const { isTokenRevoked } = require('../utils/tokenRevocation'); const logger = require('../utils/logger'); @@ -57,7 +58,7 @@ async function adminAuth(req, res, next) { // Check if admin still exists and is active const admin = await db('admin_users') - .where({ id: decoded.id, is_active: true }) + .where({ id: decoded.id, is_active: formatBoolean(true) }) .first(); if (!admin) { diff --git a/backend/src/middleware/auth-enhanced.js b/backend/src/middleware/auth-enhanced.js index acbbf3d..29c1000 100644 --- a/backend/src/middleware/auth-enhanced.js +++ b/backend/src/middleware/auth-enhanced.js @@ -1,5 +1,6 @@ const jwt = require('jsonwebtoken'); const { db } = require('../database/db'); +const { formatBoolean } = require('../utils/dbCompat'); const logger = require('../utils/logger'); /** @@ -50,7 +51,7 @@ async function adminAuth(req, res, next) { // Check if admin still exists and is active const admin = await db('admin_users') - .where({ id: decoded.id, is_active: true }) + .where({ id: decoded.id, is_active: formatBoolean(true) }) .first(); if (!admin) { @@ -165,7 +166,7 @@ async function photoAuth(req, res, next) { // Allow both admin and gallery tokens if (decoded.type === 'admin') { const admin = await db('admin_users') - .where({ id: decoded.id, is_active: true }) + .where({ id: decoded.id, is_active: formatBoolean(true) }) .first(); if (!admin) { diff --git a/backend/src/middleware/auth.js b/backend/src/middleware/auth.js index 1bc3912..d218573 100644 --- a/backend/src/middleware/auth.js +++ b/backend/src/middleware/auth.js @@ -1,5 +1,6 @@ const jwt = require('jsonwebtoken'); const { db } = require('../database/db'); +const { formatBoolean } = require('../utils/dbCompat'); async function adminAuth(req, res, next) { try { @@ -9,7 +10,7 @@ async function adminAuth(req, res, next) { } const decoded = jwt.verify(token, process.env.JWT_SECRET); - const admin = await db('admin_users').where({ id: decoded.id, is_active: true }).first(); + const admin = await db('admin_users').where({ id: decoded.id, is_active: formatBoolean(true) }).first(); if (!admin) { return res.status(401).json({ error: 'Invalid token' }); diff --git a/backend/src/middleware/gallery.js b/backend/src/middleware/gallery.js index b07be1d..99482cb 100644 --- a/backend/src/middleware/gallery.js +++ b/backend/src/middleware/gallery.js @@ -1,5 +1,6 @@ const jwt = require('jsonwebtoken'); const { db } = require('../database/db'); +const { formatBoolean } = require('../utils/dbCompat'); // Middleware to verify gallery access async function verifyGalleryAccess(req, res, next) { @@ -10,7 +11,7 @@ async function verifyGalleryAccess(req, res, next) { } const decoded = jwt.verify(token, process.env.JWT_SECRET); - const event = await db('events').where({ id: decoded.eventId, is_active: true }).first(); + const event = await db('events').where({ id: decoded.eventId, is_active: formatBoolean(true) }).first(); if (!event) { return res.status(404).json({ error: 'Gallery not found or expired' }); diff --git a/backend/src/middleware/photoAuth.js b/backend/src/middleware/photoAuth.js index b729432..e4fc2cd 100644 --- a/backend/src/middleware/photoAuth.js +++ b/backend/src/middleware/photoAuth.js @@ -1,6 +1,7 @@ const bcrypt = require('bcrypt'); const jwt = require('jsonwebtoken'); const { db } = require('../database/db'); +const { formatBoolean } = require('../utils/dbCompat'); async function photoAuth(req, res, next) { try { @@ -27,7 +28,7 @@ async function photoAuth(req, res, next) { if (decoded.type === 'gallery') { // For thumbnails, we accept any valid gallery token if (!eventSlug) { - const event = await db('events').where({ slug: decoded.eventSlug, is_active: true }).first(); + const event = await db('events').where({ slug: decoded.eventSlug, is_active: formatBoolean(true) }).first(); if (event) { req.event = event; return next(); @@ -35,7 +36,7 @@ async function photoAuth(req, res, next) { } // For regular photos, check if token matches the event else if (decoded.eventSlug === eventSlug) { - const event = await db('events').where({ slug: eventSlug, is_active: true }).first(); + const event = await db('events').where({ slug: eventSlug, is_active: formatBoolean(true) }).first(); if (event) { req.event = event; return next(); @@ -72,7 +73,7 @@ async function photoAuth(req, res, next) { return res.status(401).json({ error: 'Authentication required for thumbnails' }); } - const event = await db('events').where({ slug: eventSlug, is_active: true }).first(); + const event = await db('events').where({ slug: eventSlug, is_active: formatBoolean(true) }).first(); if (!event) { return res.status(404).json({ error: 'Gallery not found' }); } diff --git a/backend/src/routes/adminArchives.js b/backend/src/routes/adminArchives.js index bc5505a..a052d1c 100644 --- a/backend/src/routes/adminArchives.js +++ b/backend/src/routes/adminArchives.js @@ -2,6 +2,7 @@ const express = require('express'); const path = require('path'); const fs = require('fs').promises; const { db } = require('../database/db'); +const { formatBoolean } = require('../utils/dbCompat'); const { adminAuth } = require('../middleware/auth-enhanced-v2'); const archiver = require('archiver'); const AdmZip = require('adm-zip'); @@ -16,7 +17,7 @@ router.get('/', adminAuth, async (req, res) => { // Get total count const totalCount = await db('events') - .where('is_archived', true) + .where('is_archived', formatBoolean(true)) .count('id as count') .first(); @@ -28,7 +29,7 @@ router.get('/', adminAuth, async (req, res) => { db.raw('SUM(photos.size_bytes) as total_size') ) .leftJoin('photos', 'events.id', 'photos.event_id') - .where('events.is_archived', true) + .where('events.is_archived', formatBoolean(true)) .groupBy('events.id') .orderBy('events.archived_at', 'desc') .limit(limit) @@ -84,7 +85,7 @@ router.get('/:id', adminAuth, async (req, res) => { try { const archive = await db('events') .where('id', req.params.id) - .where('is_archived', true) + .where('is_archived', formatBoolean(true)) .first(); if (!archive) { @@ -140,7 +141,7 @@ router.post('/:id/restore', adminAuth, async (req, res) => { try { const archive = await db('events') .where('id', req.params.id) - .where('is_archived', true) + .where('is_archived', formatBoolean(true)) .first(); if (!archive) { @@ -303,7 +304,7 @@ router.get('/:id/download', adminAuth, async (req, res) => { try { const archive = await db('events') .where('id', req.params.id) - .where('is_archived', true) + .where('is_archived', formatBoolean(true)) .first(); if (!archive) { @@ -352,7 +353,7 @@ router.delete('/:id', adminAuth, async (req, res) => { try { const archive = await db('events') .where('id', req.params.id) - .where('is_archived', true) + .where('is_archived', formatBoolean(true)) .first(); if (!archive) { diff --git a/backend/src/routes/adminCategories.js b/backend/src/routes/adminCategories.js index a2efd14..f3d9000 100644 --- a/backend/src/routes/adminCategories.js +++ b/backend/src/routes/adminCategories.js @@ -1,6 +1,7 @@ const express = require('express'); const { body, validationResult } = require('express-validator'); const { db, logActivity } = require('../database/db'); +const { formatBoolean } = require('../utils/dbCompat'); const { adminAuth } = require('../middleware/auth-enhanced-v2'); const router = express.Router(); @@ -8,7 +9,7 @@ const router = express.Router(); router.get('/global', adminAuth, async (req, res) => { try { const categories = await db('photo_categories') - .where('is_global', true) + .where('is_global', formatBoolean(true)) .orderBy('name', 'asc'); res.json(categories); @@ -25,7 +26,7 @@ router.get('/event/:eventId', adminAuth, async (req, res) => { const categories = await db('photo_categories') .where(function() { - this.where('is_global', true) + this.where('is_global', formatBoolean(true)) .orWhere('event_id', eventId); }) .orderBy('is_global', 'desc') @@ -65,7 +66,7 @@ router.post('/', adminAuth, [ .where('slug', categorySlug) .where(function() { if (is_global) { - this.where('is_global', true); + this.where('is_global', formatBoolean(true)); } else { this.where('event_id', event_id); } diff --git a/backend/src/routes/adminSettings.js b/backend/src/routes/adminSettings.js index a579b74..010e929 100644 --- a/backend/src/routes/adminSettings.js +++ b/backend/src/routes/adminSettings.js @@ -4,6 +4,7 @@ const path = require('path'); const fs = require('fs').promises; const { body, validationResult } = require('express-validator'); const { db, logActivity } = require('../database/db'); +const { formatBoolean } = require('../utils/dbCompat'); const { adminAuth } = require('../middleware/auth'); const { clearMaintenanceCache } = require('../middleware/maintenance'); const router = express.Router(); @@ -513,7 +514,7 @@ router.get('/storage/info', adminAuth, async (req, res) => { // Get archive storage const archives = await db('events') - .where('is_archived', true) + .where('is_archived', formatBoolean(true)) .whereNotNull('archive_path') .select('archive_path'); diff --git a/backend/src/routes/auth-enhanced-v2.js b/backend/src/routes/auth-enhanced-v2.js index 8a67188..2f8eba7 100644 --- a/backend/src/routes/auth-enhanced-v2.js +++ b/backend/src/routes/auth-enhanced-v2.js @@ -3,6 +3,7 @@ const bcrypt = require('bcrypt'); const jwt = require('jsonwebtoken'); const { body, validationResult } = require('express-validator'); const { db } = require('../database/db'); +const { formatBoolean } = require('../utils/dbCompat'); const { verifyRecaptcha } = require('../services/recaptcha'); const { trackFailedAttempt, @@ -248,7 +249,7 @@ router.post('/gallery/verify', [ return res.status(400).json({ error: 'reCAPTCHA verification failed' }); } - const event = await db('events').where({ slug, is_active: true, is_archived: false }).first(); + const event = await db('events').where({ slug, is_active: formatBoolean(true), is_archived: formatBoolean(false) }).first(); if (!event) { // Don't reveal if gallery exists await trackFailedAttempt(`gallery:${slug}`, ipAddress, userAgent); diff --git a/backend/src/routes/auth-enhanced.js b/backend/src/routes/auth-enhanced.js index e5bf58b..dbc282d 100644 --- a/backend/src/routes/auth-enhanced.js +++ b/backend/src/routes/auth-enhanced.js @@ -3,6 +3,7 @@ const bcrypt = require('bcrypt'); const jwt = require('jsonwebtoken'); const { body, validationResult } = require('express-validator'); const { db } = require('../database/db'); +const { formatBoolean } = require('../utils/dbCompat'); const { verifyRecaptcha } = require('../services/recaptcha'); const { trackFailedAttempt, @@ -167,7 +168,7 @@ router.post('/gallery/verify', [ return res.status(400).json({ error: 'reCAPTCHA verification failed' }); } - const event = await db('events').where({ slug, is_active: true, is_archived: false }).first(); + const event = await db('events').where({ slug, is_active: formatBoolean(true), is_archived: formatBoolean(false) }).first(); if (!event) { // Don't reveal if gallery exists await trackFailedAttempt(`gallery:${slug}`, ipAddress, userAgent); diff --git a/backend/src/routes/auth.js b/backend/src/routes/auth.js index 3b42fdb..76f8321 100644 --- a/backend/src/routes/auth.js +++ b/backend/src/routes/auth.js @@ -3,6 +3,7 @@ const bcrypt = require('bcrypt'); const jwt = require('jsonwebtoken'); const { body, validationResult } = require('express-validator'); const { db } = require('../database/db'); +const { formatBoolean } = require('../utils/dbCompat'); const { verifyRecaptcha } = require('../services/recaptcha'); const router = express.Router(); @@ -76,7 +77,7 @@ router.post('/gallery/verify', [ return res.status(400).json({ error: 'reCAPTCHA verification failed' }); } - const event = await db('events').where({ slug, is_active: true, is_archived: false }).first(); + 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 or expired' }); } diff --git a/backend/src/routes/events.js b/backend/src/routes/events.js index 7901350..e8efd8c 100644 --- a/backend/src/routes/events.js +++ b/backend/src/routes/events.js @@ -3,6 +3,7 @@ const { body, validationResult } = require('express-validator'); const bcrypt = require('bcrypt'); const crypto = require('crypto'); const { db } = require('../database/db'); +const { formatBoolean } = require('../utils/dbCompat'); const { adminAuth } = require('../middleware/auth-enhanced-v2'); const fs = require('fs').promises; const path = require('path'); @@ -113,9 +114,9 @@ router.get('/', adminAuth, async (req, res) => { let query = db('events').select('*'); if (status === 'active') { - query = query.where('is_active', true); + query = query.where('is_active', formatBoolean(true)); } else if (status === 'archived') { - query = query.where('is_archived', true); + query = query.where('is_archived', formatBoolean(true)); } const events = await query.orderBy('created_at', 'desc'); @@ -162,7 +163,7 @@ router.delete('/:id', adminAuth, async (req, res) => { try { const { id } = req.params; - await db('events').where('id', id).update({ is_active: false }); + await db('events').where('id', id).update({ is_active: formatBoolean(false) }); res.json({ success: true }); } catch (error) { @@ -188,7 +189,7 @@ router.post('/:id/extend', adminAuth, [ await db('events').where('id', id).update({ expires_at: newExpiration, - is_active: true // Reactivate if expired + is_active: formatBoolean(true) // Reactivate if expired }); res.json({ expires_at: newExpiration }); diff --git a/backend/src/routes/gallery.js b/backend/src/routes/gallery.js index 45376a4..14a0e2e 100644 --- a/backend/src/routes/gallery.js +++ b/backend/src/routes/gallery.js @@ -1,6 +1,7 @@ const express = require('express'); const jwt = require('jsonwebtoken'); const { db } = require('../database/db'); +const { formatBoolean } = require('../utils/dbCompat'); const archiver = require('archiver'); const path = require('path'); const router = express.Router(); @@ -19,7 +20,7 @@ async function verifyGalleryAccess(req, res, next) { const decoded = jwt.verify(token, process.env.JWT_SECRET); const event = await db('events') - .where({ id: decoded.eventId, is_active: true, is_archived: false }) + .where({ id: decoded.eventId, is_active: formatBoolean(true), is_archived: formatBoolean(false) }) .first(); if (!event) { @@ -40,7 +41,7 @@ router.get('/:slug/verify-token/:token', async (req, res) => { const { slug, token } = req.params; const event = await db('events') - .where({ slug, is_active: true, is_archived: false }) + .where({ slug, is_active: formatBoolean(true), is_archived: formatBoolean(false) }) .select('id', 'share_link') .first(); @@ -121,7 +122,7 @@ router.get('/:slug/photos', verifyGalleryAccess, async (req, res) => { // Get all categories for this event const categories = await db('photo_categories') .where(function() { - this.where('is_global', true) + this.where('is_global', formatBoolean(true)) .orWhere('event_id', req.event.id); }) .orderBy('is_global', 'desc') diff --git a/backend/src/routes/protectedImages.js b/backend/src/routes/protectedImages.js index 146daee..0004d9a 100644 --- a/backend/src/routes/protectedImages.js +++ b/backend/src/routes/protectedImages.js @@ -1,6 +1,7 @@ const express = require('express'); const path = require('path'); const { db } = require('../database/db'); +const { formatBoolean } = require('../utils/dbCompat'); const { verifyGalleryAccess } = require('../middleware/gallery'); const watermarkService = require('../services/watermarkService'); const { getStoragePath } = require('../config/storage'); @@ -141,7 +142,7 @@ router.get('/:slug/photo/:photoId/signed/:token', async (req, res) => { // Get event const event = await db('events') .where({ slug }) - .where('is_active', true) + .where('is_active', formatBoolean(true)) .first(); if (!event) { diff --git a/backend/src/services/expirationChecker.js b/backend/src/services/expirationChecker.js index 27ffbe0..4501f67 100644 --- a/backend/src/services/expirationChecker.js +++ b/backend/src/services/expirationChecker.js @@ -75,7 +75,7 @@ async function queueExpirationWarning(event) { async function handleExpiredEvent(event) { try { // Mark as inactive - await db('events').where('id', event.id).update({ is_active: false }); + await db('events').where('id', event.id).update({ is_active: formatBoolean(false) }); // Queue expiration emails await queueEmail(event.id, event.host_email, 'gallery_expired', { diff --git a/backend/src/services/fileWatcher.js b/backend/src/services/fileWatcher.js index 847f8a4..1094ba2 100644 --- a/backend/src/services/fileWatcher.js +++ b/backend/src/services/fileWatcher.js @@ -2,6 +2,7 @@ const chokidar = require('chokidar'); const path = require('path'); const fs = require('fs').promises; const { db } = require('../database/db'); +const { formatBoolean } = require('../utils/dbCompat'); const { generateThumbnail } = require('./imageProcessor'); const logger = require('../utils/logger'); @@ -51,7 +52,7 @@ async function processNewPhoto(filePath) { if (!['.jpg', '.jpeg', '.png', '.webp'].includes(ext)) return; // Find the event - const event = await db('events').where({ slug: eventSlug, is_active: true }).first(); + const event = await db('events').where({ slug: eventSlug, is_active: formatBoolean(true) }).first(); if (!event) return; // Get file stats diff --git a/backend/src/utils/authSecurity.js b/backend/src/utils/authSecurity.js index 8e61f3f..57a2ab6 100644 --- a/backend/src/utils/authSecurity.js +++ b/backend/src/utils/authSecurity.js @@ -4,6 +4,7 @@ */ const { db } = require('../database/db'); +const { formatBoolean } = require('./dbCompat'); const logger = require('./logger'); // Configuration constants @@ -59,7 +60,7 @@ async function trackSuccessfulLogin(identifier, ipAddress, userAgent) { const cutoffTime = new Date(Date.now() - ATTEMPT_WINDOW); await db('login_attempts') .where('identifier', identifier) - .where('success', false) + .where('success', formatBoolean(false)) .where('attempt_time', '<', cutoffTime.toISOString()) .delete(); } catch (error) { @@ -79,7 +80,7 @@ async function checkAccountLockout(identifier) { // Get recent failed attempts const failedAttempts = await db('login_attempts') .where('identifier', identifier) - .where('success', false) + .where('success', formatBoolean(false)) .where('attempt_time', '>=', recentWindow.toISOString()) .orderBy('attempt_time', 'desc') .limit(MAX_LOGIN_ATTEMPTS);