Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 086a4ca342 | |||
| 6de64a1df1 | |||
| 3074748bbc | |||
| 934d6ddc58 |
+99
-33
@@ -8,6 +8,7 @@ This guide covers deploying PicPeak using Docker Compose with direct port exposu
|
||||
- [Quick Start](#quick-start)
|
||||
- [Configuration](#configuration)
|
||||
- [Deployment](#deployment)
|
||||
- [First Login](#first-login)
|
||||
- [Reverse Proxy Setup](#reverse-proxy-setup)
|
||||
- [Maintenance](#maintenance)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
@@ -70,17 +71,33 @@ openssl rand -base64 32 | tr -d '$'
|
||||
- Escape `$` as `$$` (e.g., `Pass$$word` instead of `Pass$word`)
|
||||
- Quote the entire value: `DB_PASSWORD='Pass$word'` (less reliable)
|
||||
|
||||
### Backend Configuration (.env)
|
||||
Update `.env` with:
|
||||
- `JWT_SECRET` - Authentication secret (REQUIRED - generate a secure random value)
|
||||
- `DB_PASSWORD` - PostgreSQL password
|
||||
- `REDIS_PASSWORD` - Redis password
|
||||
- `SMTP_*` - Email configuration
|
||||
- **CRITICAL URL Configuration** (must match your deployment):
|
||||
- `FRONTEND_URL` - Frontend URL with port (e.g., `http://yourdomain.com:3000`)
|
||||
- `ADMIN_URL` - Backend URL with port (e.g., `http://yourdomain.com:3001`)
|
||||
- `VITE_API_URL` - Backend API URL (e.g., `http://yourdomain.com:3001/api`)
|
||||
- **URL Configuration** (for backend CORS):
|
||||
- `FRONTEND_URL` - Frontend URL (e.g., `http://localhost:3000` for Docker)
|
||||
- `ADMIN_URL` - Admin URL (e.g., `http://localhost:3000` for Docker)
|
||||
|
||||
⚠️ **IMPORTANT**: These URLs MUST include the correct ports and match exactly how users will access your site. Mismatched URLs will cause CORS errors and login failures!
|
||||
### Frontend Configuration (frontend/.env)
|
||||
Create `frontend/.env` from `frontend/.env.example`:
|
||||
```bash
|
||||
cp frontend/.env.example frontend/.env
|
||||
```
|
||||
|
||||
Update `frontend/.env` with:
|
||||
- `VITE_API_URL` - Backend API URL
|
||||
- For Docker deployment: `http://localhost:3001/api`
|
||||
- For non-Docker local dev: `http://localhost:3001`
|
||||
- For production with reverse proxy: `/api`
|
||||
|
||||
⚠️ **IMPORTANT PORT CONFIGURATION**:
|
||||
- The frontend runs on port **3000** in Docker (exposed via nginx)
|
||||
- The backend API runs on port **3001**
|
||||
- The frontend `.env` file MUST point to the correct backend port (3001)
|
||||
- Default `.env.example` is configured for Docker deployment
|
||||
|
||||
### Email Configuration Examples
|
||||
|
||||
@@ -127,39 +144,36 @@ By default, services are exposed on:
|
||||
|
||||
### Initial Admin Setup
|
||||
|
||||
When deploying for the first time, an admin account is automatically created with a secure random password. You need to retrieve this password to access the admin panel.
|
||||
When deploying for the first time, an admin account is automatically created with a secure, randomly generated password. This password is displayed in the Docker logs during initialization and **must be changed** on first login.
|
||||
|
||||
#### Finding the Admin Password
|
||||
#### Finding the Auto-Generated Admin Password
|
||||
|
||||
**Option 1: Check the backend logs** (recommended)
|
||||
The admin password is automatically generated during the first startup and displayed in the backend container logs. Here's how to find it:
|
||||
|
||||
**Option 1: Search Docker logs for admin password** (recommended)
|
||||
```bash
|
||||
# View the initial setup logs
|
||||
docker compose logs backend | grep -A 10 "Admin user created"
|
||||
# Find the auto-generated admin password in logs
|
||||
docker compose logs backend | grep "Admin password"
|
||||
```
|
||||
|
||||
You should see output like:
|
||||
```
|
||||
========================================
|
||||
✅ Admin user created successfully!
|
||||
========================================
|
||||
Email: admin@example.com
|
||||
Password: BraveTiger6231!
|
||||
|
||||
⚠️ IMPORTANT:
|
||||
1. Save these credentials securely
|
||||
2. Please change the password after first login
|
||||
========================================
|
||||
✅ Admin password generated: BraveTiger6231!
|
||||
```
|
||||
|
||||
**Note**: You login with the **email address**, not a username!
|
||||
|
||||
**Option 2: Check the saved credentials file**
|
||||
**Option 2: View the complete initialization logs**
|
||||
```bash
|
||||
# The password is saved in the backend container
|
||||
# View the complete admin setup logs
|
||||
docker compose logs backend | grep -A 10 "Admin user created"
|
||||
```
|
||||
|
||||
**Option 3: Check the saved credentials file**
|
||||
```bash
|
||||
# The password is also saved in the backend container
|
||||
docker exec picpeak-backend cat data/ADMIN_CREDENTIALS.txt
|
||||
```
|
||||
|
||||
**Option 3: Use the helper script**
|
||||
**Option 4: Use the helper script**
|
||||
```bash
|
||||
# Show current admin username and email (password is hidden)
|
||||
docker exec picpeak-backend node scripts/show-admin-credentials.js
|
||||
@@ -168,13 +182,64 @@ docker exec picpeak-backend node scripts/show-admin-credentials.js
|
||||
docker exec picpeak-backend node scripts/show-admin-credentials.js --reset
|
||||
```
|
||||
|
||||
#### Important Notes
|
||||
#### Important Security Notes
|
||||
|
||||
- **Login requires the email address**, not username
|
||||
- The admin password is only shown once during initial setup
|
||||
- If you lose the password, use the `--reset` option to generate a new one
|
||||
- You must change the password on first login (enforced by the system)
|
||||
- Password requirements: minimum 12 characters, mixed case, numbers, and special characters
|
||||
- The admin password is only displayed once during initial setup
|
||||
- **Password change is MANDATORY** on first login - the system will force you to change it
|
||||
- If you lose the password before first login, use the `--reset` option to generate a new one
|
||||
- New password requirements: minimum 12 characters, mixed case, numbers, and special characters
|
||||
|
||||
## 🔐 First Login
|
||||
|
||||
After deployment, you must complete the first login process which includes mandatory password change for security.
|
||||
|
||||
### Step 1: Locate Your Admin Password
|
||||
|
||||
1. **Find the auto-generated password** from the credentials file:
|
||||
```bash
|
||||
# Docker deployment
|
||||
docker compose exec backend cat /app/data/ADMIN_CREDENTIALS.txt
|
||||
|
||||
# Or directly from the host (if you have access)
|
||||
cat data/ADMIN_CREDENTIALS.txt
|
||||
```
|
||||
|
||||
2. **Note the admin email** (default: `admin@example.com` unless customized)
|
||||
|
||||
### Step 2: Access Admin Panel
|
||||
|
||||
1. Navigate to your admin panel URL (e.g., `http://your-domain.com:3001/admin` or `https://your-domain.com/admin`)
|
||||
2. Login using:
|
||||
- **Email**: `admin@example.com` (or your custom admin email)
|
||||
- **Password**: The auto-generated password from the logs
|
||||
|
||||
### Step 3: Mandatory Password Change
|
||||
|
||||
Upon first login, the system will **automatically redirect** you to change your password:
|
||||
|
||||
1. **You cannot skip this step** - it's enforced for security
|
||||
2. Enter the current auto-generated password
|
||||
3. Create a new secure password meeting these requirements:
|
||||
- Minimum 12 characters
|
||||
- At least one uppercase letter
|
||||
- At least one lowercase letter
|
||||
- At least one number
|
||||
- At least one special character (!@#$%^&*)
|
||||
|
||||
### Security Best Practices for New Password
|
||||
|
||||
- **Use a unique password** not used elsewhere
|
||||
- **Consider a password manager** for generation and storage
|
||||
- **Include mixed characters**: `MySecureP@ssw0rd2024!`
|
||||
- **Avoid personal information** (names, dates, etc.)
|
||||
- **Save securely** - you cannot recover this password easily
|
||||
|
||||
### If You Lose Access
|
||||
|
||||
If you lose your admin credentials after the first login, you'll need to manually reset the password in the database or create a new admin user through the database.
|
||||
|
||||
**Note**: The credentials file (`ADMIN_CREDENTIALS.txt`) is only created during initial deployment and contains the first admin password. After changing the password, this file becomes outdated but is kept for reference.
|
||||
|
||||
#### Configuring Admin Email
|
||||
|
||||
@@ -404,9 +469,10 @@ docker exec picpeak-backend npm run migrate
|
||||
- Should see `node server.js` process
|
||||
|
||||
4. **Login After Fresh Install**:
|
||||
- Check migration logs for generated credentials
|
||||
- Username: `admin` or the email shown in logs
|
||||
- Password: Shown during first migration (e.g., `SharpPhoenix9920$`)
|
||||
- Check backend logs for auto-generated admin password: `docker compose logs backend | grep "Admin password"`
|
||||
- Email: `admin@example.com` (or your custom admin email from .env)
|
||||
- Password: Auto-generated and shown in logs (e.g., `BraveTiger6231!`)
|
||||
- Remember: Password MUST be changed on first login
|
||||
|
||||
5. **Complete Fix Sequence**:
|
||||
```bash
|
||||
|
||||
@@ -40,6 +40,7 @@ Unlike expensive SaaS solutions, PicPeak gives you:
|
||||
- ⬇️ **Bulk Downloads** - Download all photos with one click
|
||||
- 🔍 **Smart Search** - Find photos quickly
|
||||
- 📤 **Guest Uploads** - Optional client photo uploads
|
||||
- 🛡️ **Download Protection** - Advanced image protection with watermarking and right-click prevention
|
||||
|
||||
### Technical Excellence
|
||||
- 🐳 **Docker Ready** - Deploy in minutes
|
||||
@@ -177,6 +178,17 @@ Organize and manage your photo galleries with intuitive event management tools.
|
||||
|
||||
We're constantly improving PicPeak and welcome contributions from our community! If you have ideas for new features or want to help implement existing ones, please open an issue or submit a pull request. Your contributions help make PicPeak better for everyone.
|
||||
|
||||
### 🚧 Beta Features (Use at your own risk)
|
||||
|
||||
These features are currently in beta testing and may have limited functionality or stability:
|
||||
|
||||
| Feature | Description | Status |
|
||||
|---------|-------------|--------|
|
||||
| **Download Protection** | Advanced image protection system with canvas rendering, invisible watermarking, and right-click prevention to protect your photos from unauthorized downloads | 🧪 Beta |
|
||||
| **Simple Deployment Script** | One-click deployment script for quick server setup with automated configuration and dependency installation | 🧪 Beta |
|
||||
|
||||
### 📋 Future Enhancements
|
||||
|
||||
| Feature | Description | Priority | Status |
|
||||
|---------|-------------|----------|---------|
|
||||
| **Backup & Restore** | Comprehensive backup system with S3/MinIO support, automated scheduling, and safe restore functionality | High | ✅ Implemented |
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
// Fix missing columns identified in GitHub issues
|
||||
|
||||
exports.up = async function(knex) {
|
||||
console.log('Adding missing columns to database tables...');
|
||||
|
||||
// Add must_change_password column to admin_users table
|
||||
const hasMustChangePassword = await knex.schema.hasColumn('admin_users', 'must_change_password');
|
||||
if (!hasMustChangePassword) {
|
||||
console.log('Adding must_change_password column to admin_users table...');
|
||||
await knex.schema.table('admin_users', (table) => {
|
||||
table.boolean('must_change_password').defaultTo(false);
|
||||
});
|
||||
}
|
||||
|
||||
// Add password_changed_at column to admin_users table
|
||||
const hasPasswordChangedAt = await knex.schema.hasColumn('admin_users', 'password_changed_at');
|
||||
if (!hasPasswordChangedAt) {
|
||||
console.log('Adding password_changed_at column to admin_users table...');
|
||||
await knex.schema.table('admin_users', (table) => {
|
||||
table.datetime('password_changed_at');
|
||||
});
|
||||
}
|
||||
|
||||
// Add require_moderation column to event_feedback_settings table
|
||||
const hasEventFeedbackSettings = await knex.schema.hasTable('event_feedback_settings');
|
||||
if (hasEventFeedbackSettings) {
|
||||
const hasRequireModeration = await knex.schema.hasColumn('event_feedback_settings', 'require_moderation');
|
||||
if (!hasRequireModeration) {
|
||||
console.log('Adding require_moderation column to event_feedback_settings table...');
|
||||
await knex.schema.table('event_feedback_settings', (table) => {
|
||||
table.boolean('require_moderation').defaultTo(true);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Add host_name column to events table if missing
|
||||
const hasHostName = await knex.schema.hasColumn('events', 'host_name');
|
||||
if (!hasHostName) {
|
||||
console.log('Adding host_name column to events table...');
|
||||
await knex.schema.table('events', (table) => {
|
||||
table.string('host_name');
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Missing columns have been added successfully');
|
||||
};
|
||||
|
||||
exports.down = async function(knex) {
|
||||
console.log('Removing added columns...');
|
||||
|
||||
// Remove must_change_password column from admin_users table
|
||||
const hasMustChangePassword = await knex.schema.hasColumn('admin_users', 'must_change_password');
|
||||
if (hasMustChangePassword) {
|
||||
await knex.schema.table('admin_users', (table) => {
|
||||
table.dropColumn('must_change_password');
|
||||
});
|
||||
}
|
||||
|
||||
// Remove password_changed_at column from admin_users table
|
||||
const hasPasswordChangedAt = await knex.schema.hasColumn('admin_users', 'password_changed_at');
|
||||
if (hasPasswordChangedAt) {
|
||||
await knex.schema.table('admin_users', (table) => {
|
||||
table.dropColumn('password_changed_at');
|
||||
});
|
||||
}
|
||||
|
||||
// Remove require_moderation column from event_feedback_settings table
|
||||
const hasEventFeedbackSettings = await knex.schema.hasTable('event_feedback_settings');
|
||||
if (hasEventFeedbackSettings) {
|
||||
const hasRequireModeration = await knex.schema.hasColumn('event_feedback_settings', 'require_moderation');
|
||||
if (hasRequireModeration) {
|
||||
await knex.schema.table('event_feedback_settings', (table) => {
|
||||
table.dropColumn('require_moderation');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Remove host_name column from events table
|
||||
const hasHostName = await knex.schema.hasColumn('events', 'host_name');
|
||||
if (hasHostName) {
|
||||
await knex.schema.table('events', (table) => {
|
||||
table.dropColumn('host_name');
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Columns removed');
|
||||
};
|
||||
@@ -0,0 +1,69 @@
|
||||
// Add download control features to events table
|
||||
|
||||
exports.up = async function(knex) {
|
||||
console.log('Adding download control columns to events table...');
|
||||
|
||||
// Add download control columns to events table
|
||||
const hasAllowDownloads = await knex.schema.hasColumn('events', 'allow_downloads');
|
||||
if (!hasAllowDownloads) {
|
||||
await knex.schema.table('events', (table) => {
|
||||
table.boolean('allow_downloads').defaultTo(true);
|
||||
table.boolean('disable_right_click').defaultTo(false);
|
||||
table.boolean('watermark_downloads').defaultTo(false);
|
||||
table.text('watermark_text');
|
||||
});
|
||||
}
|
||||
|
||||
// Add download control settings to app_settings
|
||||
const downloadSettingExists = await knex('app_settings')
|
||||
.where('setting_key', 'default_allow_downloads')
|
||||
.first();
|
||||
|
||||
if (!downloadSettingExists) {
|
||||
await knex('app_settings').insert([
|
||||
{
|
||||
setting_key: 'default_allow_downloads',
|
||||
setting_value: JSON.stringify(true),
|
||||
setting_type: 'gallery'
|
||||
},
|
||||
{
|
||||
setting_key: 'default_disable_right_click',
|
||||
setting_value: JSON.stringify(false),
|
||||
setting_type: 'gallery'
|
||||
},
|
||||
{
|
||||
setting_key: 'default_watermark_downloads',
|
||||
setting_value: JSON.stringify(false),
|
||||
setting_type: 'gallery'
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
||||
console.log('Download control features added successfully');
|
||||
};
|
||||
|
||||
exports.down = async function(knex) {
|
||||
console.log('Removing download control columns...');
|
||||
|
||||
// Remove app settings
|
||||
await knex('app_settings')
|
||||
.whereIn('setting_key', [
|
||||
'default_allow_downloads',
|
||||
'default_disable_right_click',
|
||||
'default_watermark_downloads'
|
||||
])
|
||||
.delete();
|
||||
|
||||
// Remove columns from events table
|
||||
const hasAllowDownloads = await knex.schema.hasColumn('events', 'allow_downloads');
|
||||
if (hasAllowDownloads) {
|
||||
await knex.schema.table('events', (table) => {
|
||||
table.dropColumn('allow_downloads');
|
||||
table.dropColumn('disable_right_click');
|
||||
table.dropColumn('watermark_downloads');
|
||||
table.dropColumn('watermark_text');
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Download control columns removed');
|
||||
};
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "picpeak-backend",
|
||||
"version": "1.0.101",
|
||||
"version": "1.0.102",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "picpeak-backend",
|
||||
"version": "1.0.101",
|
||||
"version": "1.0.102",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.850.0",
|
||||
"@aws-sdk/lib-storage": "^3.850.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "picpeak-backend",
|
||||
"version": "1.0.101",
|
||||
"version": "1.0.102",
|
||||
"description": "Backend for PicPeak event photo sharing platform",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -166,7 +166,7 @@ router.get('/health', adminAuth, async (req, res) => {
|
||||
|
||||
const [failedEmails] = await db('email_queue')
|
||||
.where('status', 'failed')
|
||||
.where('created_at', '>=', twentyFourHoursAgo.toISOString())
|
||||
.where('scheduled_at', '>=', twentyFourHoursAgo.toISOString())
|
||||
.count('* as count');
|
||||
|
||||
const emailStatus = failedEmails.count > 10 ? 'warning' : 'healthy';
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const express = require('express');
|
||||
const { body, query, 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();
|
||||
const bcrypt = require('bcrypt');
|
||||
@@ -12,7 +13,6 @@ const { queueEmail } = require('../services/emailProcessor');
|
||||
const { escapeLikePattern } = require('../utils/sqlSecurity');
|
||||
// formatDate import removed - dates are formatted by email processor
|
||||
const { validatePasswordInContext, getBcryptRounds } = require('../utils/passwordValidation');
|
||||
const { formatBoolean } = require('../utils/dbCompat');
|
||||
|
||||
// Create new event
|
||||
router.post('/', adminAuth, [
|
||||
@@ -27,7 +27,11 @@ router.post('/', adminAuth, [
|
||||
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()
|
||||
body('host_name').notEmpty().trim(),
|
||||
body('allow_downloads').optional().isBoolean(),
|
||||
body('disable_right_click').optional().isBoolean(),
|
||||
body('watermark_downloads').optional().isBoolean(),
|
||||
body('watermark_text').optional().trim()
|
||||
], async (req, res) => {
|
||||
try {
|
||||
console.log('Create event request body:', req.body);
|
||||
@@ -49,9 +53,26 @@ router.post('/', adminAuth, [
|
||||
color_theme = null,
|
||||
expiration_days = 30,
|
||||
allow_user_uploads = false,
|
||||
upload_category_id = null
|
||||
upload_category_id = null,
|
||||
allow_downloads = true,
|
||||
disable_right_click = false,
|
||||
watermark_downloads = false,
|
||||
watermark_text = null
|
||||
} = req.body;
|
||||
|
||||
// Debug logging
|
||||
console.log('Download control values:', {
|
||||
allow_downloads,
|
||||
disable_right_click,
|
||||
watermark_downloads,
|
||||
watermark_text,
|
||||
types: {
|
||||
allow_downloads: typeof allow_downloads,
|
||||
disable_right_click: typeof disable_right_click,
|
||||
watermark_downloads: typeof watermark_downloads
|
||||
}
|
||||
});
|
||||
|
||||
// Validate password strength
|
||||
const passwordValidation = await validatePasswordInContext(password, 'gallery', {
|
||||
eventName: event_name
|
||||
@@ -121,7 +142,11 @@ router.post('/', adminAuth, [
|
||||
expires_at: expires_at.toISOString(),
|
||||
created_at: new Date().toISOString(),
|
||||
allow_user_uploads,
|
||||
upload_category_id
|
||||
upload_category_id,
|
||||
allow_downloads: formatBoolean(allow_downloads !== undefined ? allow_downloads : true),
|
||||
disable_right_click: formatBoolean(disable_right_click !== undefined ? disable_right_click : false),
|
||||
watermark_downloads: formatBoolean(watermark_downloads !== undefined ? watermark_downloads : false),
|
||||
watermark_text
|
||||
}).returning('id');
|
||||
|
||||
// Handle both PostgreSQL (returns array of objects) and SQLite (returns array of IDs)
|
||||
@@ -341,7 +366,11 @@ router.put('/:id', adminAuth, [
|
||||
// Check if it's a number or can be converted to a valid integer
|
||||
const num = Number(value);
|
||||
return !isNaN(num) && Number.isInteger(num);
|
||||
}).withMessage('hero_photo_id must be an integer or null')
|
||||
}).withMessage('hero_photo_id must be an integer or null'),
|
||||
body('allow_downloads').optional().isBoolean(),
|
||||
body('disable_right_click').optional().isBoolean(),
|
||||
body('watermark_downloads').optional().isBoolean(),
|
||||
body('watermark_text').optional().trim()
|
||||
], async (req, res) => {
|
||||
try {
|
||||
const errors = validationResult(req);
|
||||
|
||||
@@ -46,7 +46,8 @@ router.get('/:slug/info', async (req, res) => {
|
||||
|
||||
const event = await db('events')
|
||||
.where({ slug })
|
||||
.select('event_name', 'event_type', 'event_date', 'expires_at', 'is_active', 'is_archived', 'share_link')
|
||||
.select('event_name', 'event_type', 'event_date', 'expires_at', 'is_active', 'is_archived', 'share_link',
|
||||
'allow_downloads', 'disable_right_click', 'watermark_downloads', 'watermark_text')
|
||||
.first();
|
||||
|
||||
if (!event) {
|
||||
@@ -78,7 +79,11 @@ router.get('/:slug/info', async (req, res) => {
|
||||
is_active: event.is_active,
|
||||
is_expired: !event.is_active || new Date(event.expires_at) < new Date(),
|
||||
requires_password: true,
|
||||
color_theme: event.color_theme
|
||||
color_theme: event.color_theme,
|
||||
allow_downloads: event.allow_downloads !== false,
|
||||
disable_right_click: event.disable_right_click === true,
|
||||
watermark_downloads: event.watermark_downloads === true,
|
||||
watermark_text: event.watermark_text
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching gallery info:', error);
|
||||
@@ -125,7 +130,11 @@ router.get('/:slug/photos', verifyGalleryAccess, async (req, res) => {
|
||||
welcome_message: req.event.welcome_message,
|
||||
color_theme: req.event.color_theme,
|
||||
expires_at: req.event.expires_at,
|
||||
hero_photo_id: req.event.hero_photo_id
|
||||
hero_photo_id: req.event.hero_photo_id,
|
||||
allow_downloads: req.event.allow_downloads !== false,
|
||||
disable_right_click: req.event.disable_right_click === true,
|
||||
watermark_downloads: req.event.watermark_downloads === true,
|
||||
watermark_text: req.event.watermark_text
|
||||
},
|
||||
categories: categories.map(cat => ({
|
||||
id: cat.id,
|
||||
@@ -157,6 +166,11 @@ router.get('/:slug/download/:photoId', verifyGalleryAccess, async (req, res) =>
|
||||
try {
|
||||
const { photoId } = req.params;
|
||||
|
||||
// Check if downloads are allowed for this event
|
||||
if (req.event.allow_downloads === false) {
|
||||
return res.status(403).json({ error: 'Downloads are disabled for this gallery' });
|
||||
}
|
||||
|
||||
const photo = await db('photos')
|
||||
.where({ id: photoId, event_id: req.event.id })
|
||||
.first();
|
||||
@@ -205,6 +219,11 @@ router.get('/:slug/download/:photoId', verifyGalleryAccess, async (req, res) =>
|
||||
// Download all photos as ZIP
|
||||
router.get('/:slug/download-all', verifyGalleryAccess, async (req, res) => {
|
||||
try {
|
||||
// Check if downloads are allowed for this event
|
||||
if (req.event.allow_downloads === false) {
|
||||
return res.status(403).json({ error: 'Downloads are disabled for this gallery' });
|
||||
}
|
||||
|
||||
// Fetch photos with category information
|
||||
const photos = await db('photos')
|
||||
.leftJoin('photo_categories', 'photos.category_id', 'photo_categories.id')
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
# Backend API URL
|
||||
# For local development:
|
||||
VITE_API_URL=http://localhost:3001
|
||||
# For local development with Docker:
|
||||
VITE_API_URL=http://localhost:3001/api
|
||||
|
||||
# For local development without Docker:
|
||||
# VITE_API_URL=http://localhost:3001
|
||||
|
||||
# For production behind reverse proxy (Traefik, nginx, etc):
|
||||
# VITE_API_URL=/api
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "picpeak-frontend",
|
||||
"version": "1.0.100",
|
||||
"version": "1.0.102",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "picpeak-frontend",
|
||||
"version": "1.0.100",
|
||||
"version": "1.0.102",
|
||||
"dependencies": {
|
||||
"@tanstack/react-query": "^5.0.0",
|
||||
"@tiptap/extension-character-count": "^2.26.1",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "picpeak-frontend",
|
||||
"private": true,
|
||||
"version": "1.0.100",
|
||||
"version": "1.0.102",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -363,7 +363,11 @@ export const EventDetailsPage: React.FC = () => {
|
||||
)}
|
||||
{event.share_link && (
|
||||
<a
|
||||
href={event.share_link}
|
||||
href={
|
||||
event.share_link.startsWith('http')
|
||||
? event.share_link
|
||||
: `/gallery/${event.share_link}`
|
||||
}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-2 px-3 py-1.5 text-sm font-medium text-primary-600 hover:text-primary-700 border border-primary-600 rounded-lg hover:bg-primary-50 transition-colors"
|
||||
|
||||
@@ -479,7 +479,11 @@ export const EventsListPage: React.FC = () => {
|
||||
</button>
|
||||
{event.share_link ? (
|
||||
<a
|
||||
href={event.share_link}
|
||||
href={
|
||||
event.share_link.startsWith('http')
|
||||
? event.share_link
|
||||
: `/gallery/${event.share_link}`
|
||||
}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="w-full text-left px-4 py-2 text-sm text-neutral-700 hover:bg-neutral-100 flex items-center gap-2"
|
||||
|
||||
Reference in New Issue
Block a user