Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 086a4ca342 | |||
| 6de64a1df1 | |||
| 3074748bbc | |||
| 934d6ddc58 | |||
| a699a0477b | |||
| ed0243ec39 | |||
| ac31798bf5 | |||
| 65d796b9f0 | |||
| 6389b9df3f | |||
| 87d1761091 | |||
| fda132eed4 | |||
| 1cadce196b | |||
| 840b8870ec | |||
| ad495a92c4 |
@@ -10,10 +10,13 @@ JWT_SECRET=your_very_long_random_jwt_secret_here
|
||||
# Database Configuration (PostgreSQL)
|
||||
DATABASE_CLIENT=pg
|
||||
DB_USER=picpeak
|
||||
# IMPORTANT: Avoid $ character in passwords - Docker Compose interprets it as variable substitution
|
||||
# If you must use $, escape it as $$ (e.g., Pass$$word instead of Pass$word)
|
||||
DB_PASSWORD=your_secure_postgres_password_here
|
||||
DB_NAME=picpeak_prod
|
||||
|
||||
# Redis Configuration
|
||||
# IMPORTANT: Same warning applies - avoid $ or escape as $$
|
||||
REDIS_PASSWORD=your_secure_redis_password_here
|
||||
|
||||
# Admin Account (initial setup)
|
||||
|
||||
+192
-14
@@ -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)
|
||||
@@ -23,8 +24,8 @@ This guide covers deploying PicPeak using Docker Compose with direct port exposu
|
||||
|
||||
1. **Clone the repository**
|
||||
```bash
|
||||
git clone https://github.com/yourusername/wedding-photo-sharing.git
|
||||
cd wedding-photo-sharing
|
||||
git clone https://github.com/the-luap/picpeak.git
|
||||
cd picpeak
|
||||
```
|
||||
|
||||
2. **Set up environment**
|
||||
@@ -58,21 +59,45 @@ Generate secure values:
|
||||
# JWT Secret
|
||||
openssl rand -base64 64
|
||||
|
||||
# Database Password
|
||||
openssl rand -base64 32
|
||||
# Database Password (avoid $ character - see warning below)
|
||||
openssl rand -base64 32 | tr -d '$'
|
||||
|
||||
# Redis Password
|
||||
openssl rand -base64 32
|
||||
# Redis Password (avoid $ character - see warning below)
|
||||
openssl rand -base64 32 | tr -d '$'
|
||||
```
|
||||
|
||||
⚠️ **PASSWORD WARNING**: Docker Compose interprets `$` as variable substitution. Either:
|
||||
- Avoid `$` in passwords (recommended - use the commands above)
|
||||
- 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
|
||||
- `JWT_SECRET` - Authentication secret (REQUIRED - generate a secure random value)
|
||||
- `DB_PASSWORD` - PostgreSQL password
|
||||
- `REDIS_PASSWORD` - Redis password
|
||||
- `SMTP_*` - Email configuration
|
||||
- `FRONTEND_URL` - Your domain URL
|
||||
- `ADMIN_URL` - Backend admin URL
|
||||
- `VITE_API_URL` - API URL for frontend
|
||||
- **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)
|
||||
|
||||
### 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
|
||||
|
||||
@@ -119,20 +144,114 @@ By default, services are exposed on:
|
||||
|
||||
### Initial Admin Setup
|
||||
|
||||
The admin credentials are generated during first startup. Check the logs:
|
||||
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 Auto-Generated Admin Password
|
||||
|
||||
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
|
||||
docker compose logs backend | grep -A 5 "Admin user created"
|
||||
# Find the auto-generated admin password in logs
|
||||
docker compose logs backend | grep "Admin password"
|
||||
```
|
||||
|
||||
Or use the helper script:
|
||||
You should see output like:
|
||||
```
|
||||
✅ Admin password generated: BraveTiger6231!
|
||||
```
|
||||
|
||||
**Option 2: View the complete initialization logs**
|
||||
```bash
|
||||
# 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 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
|
||||
|
||||
# To reset password
|
||||
# Reset the admin password to a new random password
|
||||
docker exec picpeak-backend node scripts/show-admin-credentials.js --reset
|
||||
```
|
||||
|
||||
#### Important Security Notes
|
||||
|
||||
- **Login requires the email address**, not username
|
||||
- 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
|
||||
|
||||
By default, the admin email is `admin@example.com`. To use a different email address, set it in your `.env` file before first deployment:
|
||||
|
||||
```env
|
||||
# .env
|
||||
ADMIN_EMAIL=your-email@yourdomain.com
|
||||
```
|
||||
|
||||
**Note**: This only works on first deployment. To change the admin email after deployment, you'll need to update it in the database or create a new admin user through the admin panel.
|
||||
|
||||
## 🔒 Reverse Proxy Setup
|
||||
|
||||
For production deployments, you should use a reverse proxy for SSL/HTTPS. The application exposes ports directly, allowing you to use any reverse proxy solution.
|
||||
@@ -325,6 +444,54 @@ docker exec picpeak-backend npm run migrate
|
||||
|
||||
### Common Issues
|
||||
|
||||
#### 502 Bad Gateway / Login Failures
|
||||
**This is the most common deployment issue!** Usually caused by misconfigured URLs or network problems:
|
||||
|
||||
1. **CORS Configuration Errors**:
|
||||
```bash
|
||||
# WRONG - Missing port will cause CORS errors
|
||||
FRONTEND_URL=http://10.0.252.12
|
||||
|
||||
# CORRECT - Include the port you're accessing from
|
||||
FRONTEND_URL=http://10.0.252.12:3000
|
||||
```
|
||||
|
||||
The backend validates Origin headers against `FRONTEND_URL` for CORS. If they don't match exactly, you'll get 500 errors on login.
|
||||
|
||||
2. **After Container Restarts**:
|
||||
- Nginx may have cached old container IPs
|
||||
- Solution: `docker restart picpeak-frontend`
|
||||
- Always wait 30-60 seconds for health checks
|
||||
|
||||
3. **Backend Not Starting After Migrations**:
|
||||
- The logs may only show migrations completed
|
||||
- Check if server is actually running: `docker exec picpeak-backend ps aux | grep node`
|
||||
- Should see `node server.js` process
|
||||
|
||||
4. **Login After Fresh Install**:
|
||||
- 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
|
||||
# 1. Fix your .env file URLs
|
||||
# 2. Full restart
|
||||
docker-compose down
|
||||
docker-compose up -d
|
||||
|
||||
# 3. Wait for healthy status
|
||||
sleep 60
|
||||
docker ps # All should show (healthy)
|
||||
|
||||
# 4. Test backend directly
|
||||
curl http://localhost:3001/health
|
||||
|
||||
# 5. Test through frontend
|
||||
curl http://localhost:3000/api/public/settings
|
||||
```
|
||||
|
||||
#### Port Already in Use
|
||||
```bash
|
||||
# Check what's using the port
|
||||
@@ -336,6 +503,17 @@ FRONTEND_PORT=3002
|
||||
BACKEND_PORT=3003
|
||||
```
|
||||
|
||||
#### Docker Compose Variable Substitution Errors
|
||||
If you see warnings like:
|
||||
```
|
||||
WARN[0000] The "fgbf" variable is not set. Defaulting to a blank string.
|
||||
```
|
||||
|
||||
This means your password contains `$` which Docker Compose interprets as a variable. Solutions:
|
||||
1. **Best**: Generate passwords without `$`: `openssl rand -base64 32 | tr -d '$'`
|
||||
2. **Alternative**: Escape `$` as `$$` in your .env file
|
||||
3. **Example**: `DB_PASSWORD=Pass@#$$fgbf` instead of `DB_PASSWORD=Pass@#$fgbf`
|
||||
|
||||
#### Permission Errors
|
||||
```bash
|
||||
# Fix ownership
|
||||
|
||||
@@ -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
|
||||
@@ -71,7 +72,7 @@ docker-compose up -d
|
||||
|
||||
## 📖 Documentation
|
||||
|
||||
- 📘 [**Deployment Guide**](DEPLOYMENT.md) - Detailed installation instructions
|
||||
- 📘 [**Deployment Guide**](DEPLOYMENT_GUIDE.md) - Detailed installation instructions
|
||||
- 🤝 [**Contributing**](CONTRIBUTING.md) - How to contribute
|
||||
- 📜 [**License**](LICENSE) - MIT License
|
||||
- 🔒 [**Security**](SECURITY.md) - Security policies
|
||||
@@ -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 |
|
||||
@@ -209,7 +221,7 @@ PicPeak is released under the [MIT License](LICENSE). Use it freely for personal
|
||||
## 🚀 Ready to Get Started?
|
||||
|
||||
1. ⭐ **Star this repository** to show your support
|
||||
2. 📖 Read the [Deployment Guide](DEPLOYMENT.md)
|
||||
2. 📖 Read the [Deployment Guide](DEPLOYMENT_GUIDE.md)
|
||||
3. 🐛 Report issues or request features
|
||||
4. 🤝 Join our community and contribute!
|
||||
|
||||
@@ -219,6 +231,6 @@ PicPeak is released under the [MIT License](LICENSE). Use it freely for personal
|
||||
Made with ❤️ by photographers, for photographers
|
||||
<br>
|
||||
<a href="https://github.com/the-luap/picpeak">GitHub</a> •
|
||||
<a href="DEPLOYMENT.md">Documentation</a> •
|
||||
<a href="DEPLOYMENT_GUIDE.md">Documentation</a> •
|
||||
<a href="https://github.com/the-luap/picpeak/issues">Support</a>
|
||||
</p>
|
||||
@@ -18,9 +18,13 @@ exports.up = async function(knex) {
|
||||
const generatedPassword = generateReadablePassword();
|
||||
const passwordHash = await bcrypt.hash(generatedPassword, 12); // Increased rounds for better security
|
||||
|
||||
// Get admin credentials from environment or use defaults
|
||||
const adminUsername = process.env.ADMIN_USERNAME || 'admin';
|
||||
const adminEmail = process.env.ADMIN_EMAIL || 'admin@example.com';
|
||||
|
||||
await knex('admin_users').insert({
|
||||
username: 'admin',
|
||||
email: 'admin@example.com',
|
||||
username: adminUsername,
|
||||
email: adminEmail,
|
||||
password_hash: passwordHash,
|
||||
created_at: new Date()
|
||||
});
|
||||
@@ -36,7 +40,7 @@ PicPeak Admin Credentials
|
||||
|
||||
Your admin account has been created with these credentials:
|
||||
|
||||
Username: admin
|
||||
Email: ${adminEmail}
|
||||
Password: ${generatedPassword}
|
||||
|
||||
IMPORTANT SECURITY NOTES:
|
||||
@@ -47,6 +51,8 @@ IMPORTANT SECURITY NOTES:
|
||||
|
||||
Login URL: ${process.env.ADMIN_URL || 'http://localhost:3001'}/admin
|
||||
|
||||
Login with the email address shown above
|
||||
|
||||
Generated on: ${new Date().toISOString()}
|
||||
========================================
|
||||
`;
|
||||
@@ -65,7 +71,7 @@ Generated on: ${new Date().toISOString()}
|
||||
console.log('\n========================================');
|
||||
console.log('✅ Admin user created successfully!');
|
||||
console.log('========================================');
|
||||
console.log('Username: admin');
|
||||
console.log(`Email: ${adminEmail}`);
|
||||
console.log(`Password: ${generatedPassword}`);
|
||||
console.log('\n⚠️ IMPORTANT:');
|
||||
console.log('1. Save these credentials securely');
|
||||
|
||||
@@ -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.98",
|
||||
"version": "1.0.102",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "picpeak-backend",
|
||||
"version": "1.0.98",
|
||||
"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.98",
|
||||
"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);
|
||||
@@ -526,16 +555,15 @@ router.post('/:id/reset-password', adminAuth, async (req, res) => {
|
||||
}
|
||||
|
||||
// Generate new password
|
||||
const { generatePassword } = require('../utils/passwordGenerator');
|
||||
const newPassword = generatePassword();
|
||||
const { generateReadablePassword } = require('../utils/passwordGenerator');
|
||||
const newPassword = generateReadablePassword();
|
||||
const passwordHash = await bcrypt.hash(newPassword, 10);
|
||||
|
||||
// Update event with new password
|
||||
await db('events')
|
||||
.where('id', id)
|
||||
.update({
|
||||
password_hash: passwordHash,
|
||||
updated_at: new Date()
|
||||
password_hash: passwordHash
|
||||
});
|
||||
|
||||
// Log activity
|
||||
|
||||
@@ -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,5 +1,3 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
backend:
|
||||
build:
|
||||
|
||||
@@ -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
|
||||
|
||||
+4
-4
@@ -39,7 +39,7 @@ server {
|
||||
|
||||
# API proxy
|
||||
location /api {
|
||||
proxy_pass http://backend:3000;
|
||||
proxy_pass http://backend:3001;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
@@ -53,7 +53,7 @@ server {
|
||||
|
||||
# Photo serving proxy
|
||||
location /photos {
|
||||
proxy_pass http://backend:3000;
|
||||
proxy_pass http://backend:3001;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
@@ -67,7 +67,7 @@ server {
|
||||
|
||||
# Thumbnail serving proxy
|
||||
location /thumbnails {
|
||||
proxy_pass http://backend:3000;
|
||||
proxy_pass http://backend:3001;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
@@ -81,7 +81,7 @@ server {
|
||||
|
||||
# Uploads serving proxy (logos, favicons, watermarks)
|
||||
location /uploads {
|
||||
proxy_pass http://backend:3000;
|
||||
proxy_pass http://backend:3001;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "picpeak-frontend",
|
||||
"version": "1.0.98",
|
||||
"version": "1.0.102",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "picpeak-frontend",
|
||||
"version": "1.0.98",
|
||||
"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.98",
|
||||
"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