docs: move documentation to docs.picpeak.app, drop in-repo copies
The full documentation now lives at https://docs.picpeak.app — built from the picpeak-docs Nextra repo. The README, SIMPLE_SETUP, and the v1 OpenAPI generation flow all point there now. Removed (now living at docs.picpeak.app): - DEPLOYMENT_GUIDE.md (root-level — content covered by docs.picpeak.app/deployment) - docs/ADMIN_SETUP_GUIDE.md - docs/JWT_SECRET_MIGRATION.md - docs/SECURITY_BEST_PRACTICES.md - docs/admin-api-quickstart.md → docs.picpeak.app/api - docs/nginx-fix.md → docs.picpeak.app/deployment/reverse-proxy - docs/openapi.json, docs/openapi.yaml → still generated locally as a build artifact (now gitignored), synced into picpeak-docs by scripts/sync-api-docs.sh - docs/picpeak-admin-api.openapi.yaml → ditto Kept: - docs/*.png (logo + screenshots — README still img-tags these) Updated: - README.md — replaced six in-repo doc links with docs.picpeak.app pointers, restructured the Documentation section as a curated link list to the new site - SIMPLE_SETUP.md — single deployment-guide link redirected - .gitignore — docs/openapi.{json,yaml} are now build artifacts, not tracked - backend/src/routes/v1/events.js — comment clarifies the OpenAPI flow
This commit is contained in:
@@ -1,166 +0,0 @@
|
||||
# Admin Setup Guide - Secure Password System
|
||||
|
||||
## Overview
|
||||
|
||||
PicPeak now uses a secure admin setup process that eliminates the default password vulnerability. When you first set up the application, a secure password is automatically generated for the admin account.
|
||||
|
||||
## Initial Setup Process
|
||||
|
||||
### 1. First Installation
|
||||
|
||||
When you run the database migrations for the first time:
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
npm run migrate
|
||||
```
|
||||
|
||||
The system will:
|
||||
- Create an admin user with username `admin`
|
||||
- Generate a secure, random password (e.g., `SwiftEagle3847!`)
|
||||
- Display the credentials in the console
|
||||
- Save the credentials to `ADMIN_CREDENTIALS.txt`
|
||||
|
||||
### 2. Retrieving Your Credentials
|
||||
|
||||
After setup, you can find your admin credentials in:
|
||||
- **Console output** - Displayed immediately after setup
|
||||
- **ADMIN_CREDENTIALS.txt** - File in the project root
|
||||
|
||||
**Example output:**
|
||||
```
|
||||
========================================
|
||||
✅ Admin user created successfully!
|
||||
========================================
|
||||
Username: admin
|
||||
Password: SwiftEagle3847!
|
||||
|
||||
⚠️ IMPORTANT:
|
||||
1. Save these credentials securely
|
||||
2. You will be required to change the password on first login
|
||||
3. Credentials are also saved in: ADMIN_CREDENTIALS.txt
|
||||
========================================
|
||||
```
|
||||
|
||||
### 3. First Login
|
||||
|
||||
1. Navigate to the admin panel: `http://localhost:3001/admin`
|
||||
2. Login with:
|
||||
- Username: `admin`
|
||||
- Password: (from ADMIN_CREDENTIALS.txt)
|
||||
3. You will be prompted to change your password immediately
|
||||
|
||||
### 4. Password Requirements
|
||||
|
||||
When changing your password, it must meet these requirements:
|
||||
- Minimum 12 characters long
|
||||
- Contains uppercase letters (A-Z)
|
||||
- Contains lowercase letters (a-z)
|
||||
- Contains numbers (0-9)
|
||||
- Contains special characters (!@#$%^&*()_+-=[]{}|;:,.<>?)
|
||||
- Not a common password
|
||||
|
||||
## Security Features
|
||||
|
||||
### Generated Passwords
|
||||
- Uses cryptographically secure random generation
|
||||
- Human-readable format: `AdjectiveNoun####!`
|
||||
- Example: `BrightMountain7823$`
|
||||
|
||||
### Password Storage
|
||||
- Passwords are hashed using bcrypt with 12 rounds
|
||||
- Original password is never stored in the database
|
||||
- Credentials file should be deleted after noting the password
|
||||
|
||||
### Forced Password Change
|
||||
- Admin must change password on first login
|
||||
- System tracks `must_change_password` flag
|
||||
- Cannot access admin features until password is changed
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Lost Admin Password
|
||||
|
||||
If you lose the admin password before first login:
|
||||
|
||||
1. Delete the admin user from the database:
|
||||
```sql
|
||||
DELETE FROM admin_users WHERE username = 'admin';
|
||||
```
|
||||
|
||||
2. Run migrations again:
|
||||
```bash
|
||||
npm run migrate
|
||||
```
|
||||
|
||||
3. New credentials will be generated
|
||||
|
||||
### Password Change Issues
|
||||
|
||||
If you can't change your password:
|
||||
- Ensure new password meets all requirements
|
||||
- Check for detailed error messages
|
||||
- Password strength validator provides specific feedback
|
||||
|
||||
### Can't Find Credentials File
|
||||
|
||||
If ADMIN_CREDENTIALS.txt is missing:
|
||||
- Check the console output from when you ran migrations
|
||||
- File is created in the backend directory root
|
||||
- File might have been deleted for security (as recommended)
|
||||
- Regenerate it by running `node scripts/reset-admin-password.js --force --credentials-file data/ADMIN_CREDENTIALS.txt`
|
||||
- When using the unified `picpeak-setup.sh` installer for a reinstall, append `--force-admin-password-reset` to have the script perform the reset automatically
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Immediate Action**
|
||||
- Change the generated password on first login
|
||||
- Use a password manager to store credentials
|
||||
- Delete ADMIN_CREDENTIALS.txt after noting the password
|
||||
|
||||
2. **Password Security**
|
||||
- Use unique passwords for each environment
|
||||
- Rotate passwords regularly (every 90 days)
|
||||
- Never share admin credentials
|
||||
|
||||
3. **Multiple Admins**
|
||||
- Create separate admin accounts for each person
|
||||
- Avoid sharing the main admin account
|
||||
- Use role-based access control when available
|
||||
|
||||
## Migration from Old System
|
||||
|
||||
If upgrading from the old system with hardcoded `admin123`:
|
||||
|
||||
1. The system will detect existing admin user
|
||||
2. You must manually reset the password:
|
||||
```bash
|
||||
# Run the password reset script
|
||||
node scripts/reset-admin-password.js
|
||||
```
|
||||
|
||||
3. Follow the new secure password process
|
||||
|
||||
## Environment-Specific Setup
|
||||
|
||||
### Development
|
||||
- Generated passwords are suitable for development
|
||||
- Consider using simpler passwords for convenience
|
||||
- Always use strong passwords in staging/production
|
||||
|
||||
### Production
|
||||
- Generate new admin account for production
|
||||
- Use extremely strong passwords (20+ characters)
|
||||
- Enable two-factor authentication when available
|
||||
- Regularly audit admin access logs
|
||||
|
||||
## Security Checklist
|
||||
|
||||
- [ ] Retrieved generated password from ADMIN_CREDENTIALS.txt
|
||||
- [ ] Logged in successfully with generated password
|
||||
- [ ] Changed password to a strong, unique password
|
||||
- [ ] Deleted ADMIN_CREDENTIALS.txt file
|
||||
- [ ] Stored new password in password manager
|
||||
- [ ] Tested login with new password
|
||||
- [ ] Set up additional admin accounts if needed
|
||||
- [ ] Configured password policies for organization
|
||||
@@ -1,109 +0,0 @@
|
||||
# JWT_SECRET Security Fix - Migration Guide
|
||||
|
||||
## Overview
|
||||
|
||||
A critical security vulnerability has been fixed where the application would fall back to a hardcoded JWT secret (`'your-secret-key'`) if the `JWT_SECRET` environment variable was not set. This has been addressed by:
|
||||
|
||||
1. Adding startup validation that requires `JWT_SECRET` to be set
|
||||
2. Removing all hardcoded fallback values
|
||||
3. Ensuring the secret meets minimum security requirements
|
||||
|
||||
## Changes Made
|
||||
|
||||
### 1. Added Environment Validation (`backend/src/config/validateEnv.js`)
|
||||
- The server now validates critical environment variables at startup
|
||||
- If `JWT_SECRET` is missing or set to the insecure default, the server will refuse to start
|
||||
- Warns if `JWT_SECRET` is less than 32 characters (recommended minimum)
|
||||
|
||||
### 2. Updated Server Startup (`backend/server.js`)
|
||||
- Added validation call immediately after loading environment variables
|
||||
- Ensures all routes and middleware have access to validated configuration
|
||||
|
||||
### 3. Removed Hardcoded Fallbacks (`backend/src/routes/protectedImages.js`)
|
||||
- Removed `|| 'your-secret-key'` fallback from lines 15 and 27
|
||||
- Functions now rely on the validated `JWT_SECRET` from environment
|
||||
|
||||
## Migration Steps for Production
|
||||
|
||||
### Before Deployment
|
||||
|
||||
1. **Verify JWT_SECRET is set in production**:
|
||||
```bash
|
||||
# Check if JWT_SECRET is set
|
||||
echo $JWT_SECRET
|
||||
```
|
||||
|
||||
2. **Ensure JWT_SECRET is secure**:
|
||||
- Must NOT be `'your-secret-key'`
|
||||
- Should be at least 32 characters long
|
||||
- Should be randomly generated
|
||||
|
||||
3. **Generate a secure JWT_SECRET if needed**:
|
||||
```bash
|
||||
# Generate a secure 64-character secret
|
||||
openssl rand -hex 32
|
||||
```
|
||||
|
||||
### Deployment Process
|
||||
|
||||
1. **Update environment variables** (if needed):
|
||||
```bash
|
||||
# Example for .env file
|
||||
JWT_SECRET=your-secure-64-character-random-string-here
|
||||
```
|
||||
|
||||
2. **Deploy the updated code**
|
||||
|
||||
3. **Monitor startup logs** to ensure no validation errors:
|
||||
```
|
||||
✓ Environment validation passed
|
||||
✓ Server running on port 3000
|
||||
```
|
||||
|
||||
### Rollback Plan
|
||||
|
||||
If the deployment fails due to missing `JWT_SECRET`:
|
||||
|
||||
1. **Quick Fix** (temporary):
|
||||
- Set `JWT_SECRET` environment variable to a secure value
|
||||
- Restart the application
|
||||
|
||||
2. **Full Rollback** (if needed):
|
||||
- Revert to previous version
|
||||
- Set `JWT_SECRET` properly before attempting deployment again
|
||||
|
||||
## Verification
|
||||
|
||||
After deployment, verify the fix is working:
|
||||
|
||||
1. **Check server logs** for successful startup
|
||||
2. **Test authentication** to ensure JWT tokens are working
|
||||
3. **Verify image protection** routes are functioning
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- **Never** commit JWT_SECRET to version control
|
||||
- **Rotate** JWT_SECRET periodically
|
||||
- **Use different** secrets for different environments (dev, staging, production)
|
||||
- **Monitor** for authentication failures that might indicate token issues
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Server won't start
|
||||
- **Error**: "Missing required environment variable: JWT_SECRET"
|
||||
- **Solution**: Set the JWT_SECRET environment variable
|
||||
|
||||
### JWT_SECRET rejection
|
||||
- **Error**: "JWT_SECRET is set to the insecure default value"
|
||||
- **Solution**: Change JWT_SECRET from 'your-secret-key' to a secure value
|
||||
|
||||
### Authentication failures after deployment
|
||||
- **Cause**: Existing tokens were signed with old secret
|
||||
- **Solution**: Users will need to re-authenticate to get new tokens
|
||||
|
||||
## Support
|
||||
|
||||
If you encounter issues during migration:
|
||||
1. Check the server logs for specific error messages
|
||||
2. Verify environment variables are properly set
|
||||
3. Ensure the JWT_SECRET value doesn't contain special characters that might need escaping
|
||||
@@ -1,179 +0,0 @@
|
||||
# Security Best Practices for PicPeak
|
||||
|
||||
## JWT Secret Management
|
||||
|
||||
### Generating Secure Secrets
|
||||
|
||||
Always generate cryptographically secure random secrets for JWT signing:
|
||||
|
||||
```bash
|
||||
# Generate a 64-character hex string (256 bits)
|
||||
openssl rand -hex 32
|
||||
|
||||
# Alternative: Generate a base64 string
|
||||
openssl rand -base64 32
|
||||
|
||||
# Alternative: Using Node.js
|
||||
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
|
||||
```
|
||||
|
||||
### Environment-Specific Secrets
|
||||
|
||||
**NEVER use the same JWT secret across different environments!**
|
||||
|
||||
- **Development**: Use the secure secret in `docker-compose.yml`
|
||||
- **Staging**: Generate a unique secret for staging
|
||||
- **Production**: Generate a unique secret for production
|
||||
|
||||
### Secret Requirements
|
||||
|
||||
1. **Minimum Length**: 32 characters (enforced by application)
|
||||
2. **Recommended Length**: 64 characters (256 bits)
|
||||
3. **Character Set**: Use hex or base64 encoding
|
||||
4. **Uniqueness**: Each environment must have a unique secret
|
||||
|
||||
### What NOT to Do
|
||||
|
||||
❌ **Never commit real secrets to version control**
|
||||
```bash
|
||||
# Bad - real secret in code
|
||||
JWT_SECRET=my-actual-production-secret
|
||||
```
|
||||
|
||||
❌ **Never use predictable or weak secrets**
|
||||
```bash
|
||||
# Bad examples
|
||||
JWT_SECRET=secret123
|
||||
JWT_SECRET=mycompanyname
|
||||
JWT_SECRET=password
|
||||
JWT_SECRET=your-secret-key
|
||||
```
|
||||
|
||||
❌ **Never share secrets between environments**
|
||||
```bash
|
||||
# Bad - same secret everywhere
|
||||
DEV_JWT_SECRET=same-secret
|
||||
PROD_JWT_SECRET=same-secret
|
||||
```
|
||||
|
||||
### Secure Secret Storage
|
||||
|
||||
#### For Local Development
|
||||
- Docker Compose files can contain development secrets
|
||||
- These should still be secure random values
|
||||
|
||||
#### For Production
|
||||
1. **Environment Variables**
|
||||
```bash
|
||||
# Set via secure environment
|
||||
export JWT_SECRET=$(openssl rand -hex 32)
|
||||
```
|
||||
|
||||
2. **Secret Management Services**
|
||||
- AWS Secrets Manager
|
||||
- HashiCorp Vault
|
||||
- Azure Key Vault
|
||||
- Kubernetes Secrets
|
||||
|
||||
3. **CI/CD Integration**
|
||||
- Store secrets in CI/CD platform's secret storage
|
||||
- Never log or echo secrets in build scripts
|
||||
|
||||
### Secret Rotation
|
||||
|
||||
Implement a secret rotation strategy:
|
||||
|
||||
1. **Regular Rotation**: Rotate secrets every 90 days
|
||||
2. **Incident Response**: Rotate immediately if compromised
|
||||
3. **Graceful Rotation**: Support multiple valid secrets during transition
|
||||
|
||||
### Monitoring and Alerts
|
||||
|
||||
1. **Startup Validation**: Application refuses to start without proper JWT_SECRET
|
||||
2. **Length Warnings**: Warnings for secrets shorter than 32 characters
|
||||
3. **Default Detection**: Critical error if default secret is detected
|
||||
|
||||
## Additional Security Measures
|
||||
|
||||
### Password Requirements
|
||||
- Minimum 12 characters
|
||||
- Mix of uppercase, lowercase, numbers, and special characters
|
||||
- Check against common password lists
|
||||
- Implement password strength meter
|
||||
|
||||
### Session Security
|
||||
- Implement token expiration (24 hours for admin, configurable for galleries)
|
||||
- Add refresh token mechanism
|
||||
- Implement token revocation
|
||||
- Use secure session storage (Redis in production)
|
||||
|
||||
### API Security
|
||||
- Rate limiting on all endpoints
|
||||
- Extra strict limits on authentication endpoints
|
||||
- CSRF protection for state-changing operations
|
||||
- Input validation on all user inputs
|
||||
|
||||
### File Upload Security
|
||||
- Validate file types by content, not just extension
|
||||
- Implement virus scanning
|
||||
- Limit file sizes
|
||||
- Sanitize filenames
|
||||
- Store files outside web root
|
||||
|
||||
### Database Security
|
||||
- Use parameterized queries (Knex.js handles this)
|
||||
- Validate and sanitize all inputs
|
||||
- Implement query timeouts
|
||||
- Use least-privilege database users
|
||||
|
||||
### HTTPS and Headers
|
||||
- Always use HTTPS in production
|
||||
- Implement security headers:
|
||||
- Strict-Transport-Security
|
||||
- X-Frame-Options
|
||||
- X-Content-Type-Options
|
||||
- Content-Security-Policy
|
||||
- X-XSS-Protection
|
||||
|
||||
### Logging and Monitoring
|
||||
- Log authentication attempts
|
||||
- Monitor for suspicious patterns
|
||||
- Never log sensitive data (passwords, tokens)
|
||||
- Implement audit trails for admin actions
|
||||
|
||||
## Security Checklist for Deployment
|
||||
|
||||
- [ ] Generate unique JWT_SECRET for environment
|
||||
- [ ] Verify JWT_SECRET meets minimum requirements
|
||||
- [ ] Store secrets securely (not in code)
|
||||
- [ ] Enable HTTPS
|
||||
- [ ] Configure security headers
|
||||
- [ ] Set up rate limiting
|
||||
- [ ] Enable audit logging
|
||||
- [ ] Test authentication flows
|
||||
- [ ] Verify file upload restrictions
|
||||
- [ ] Check database query security
|
||||
|
||||
## Incident Response
|
||||
|
||||
If a security incident occurs:
|
||||
|
||||
1. **Immediate Actions**
|
||||
- Rotate all secrets
|
||||
- Review access logs
|
||||
- Disable compromised accounts
|
||||
|
||||
2. **Investigation**
|
||||
- Analyze logs for unauthorized access
|
||||
- Check for data exfiltration
|
||||
- Review code changes
|
||||
|
||||
3. **Recovery**
|
||||
- Deploy security patches
|
||||
- Force password resets if needed
|
||||
- Notify affected users
|
||||
|
||||
4. **Prevention**
|
||||
- Update security practices
|
||||
- Implement additional monitoring
|
||||
- Conduct security audit
|
||||
@@ -1,147 +0,0 @@
|
||||
# PicPeak Admin API Quickstart
|
||||
|
||||
This guide explains how to authenticate against the PicPeak Admin API, use the OpenAPI documentation, and exercise the three automation endpoints (`create event`, `photo upload`, `resend email`) that now ship with machine-readable docs.
|
||||
|
||||
> **Prerequisites**
|
||||
>
|
||||
> - PicPeak backend running (Docker or local `node backend/server.js`)
|
||||
> - An admin account (see `data/ADMIN_CREDENTIALS.txt` for the seeded defaults)
|
||||
> - API base URL (defaults to `http://localhost:3001/api`)
|
||||
|
||||
---
|
||||
|
||||
## 1. Obtain an Admin API Token
|
||||
|
||||
1. Determine whether reCAPTCHA is enabled in **Admin → Settings → Security**. If disabled (the default), you can skip the `recaptchaToken` field shown below.
|
||||
2. Authenticate with your admin username/email and password:
|
||||
|
||||
```bash
|
||||
curl --fail --silent --show-error \
|
||||
-X POST "http://localhost:3001/api/auth/admin/login" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"username": "admin",
|
||||
"password": "BoldTiger5872%",
|
||||
"recaptchaToken": ""
|
||||
}' | jq
|
||||
```
|
||||
|
||||
Successful responses look like:
|
||||
|
||||
```json
|
||||
{
|
||||
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||
"user": {
|
||||
"id": 1,
|
||||
"username": "admin",
|
||||
"email": "[email protected]",
|
||||
"mustChangePassword": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- PicPeak also sets the `admin_token` cookie; however, when scripting you typically pass the token in an `Authorization: Bearer <token>` header.
|
||||
- Tokens expire after 24 hours. Log in again to refresh them.
|
||||
|
||||
---
|
||||
|
||||
## 2. Use the OpenAPI Documentation
|
||||
|
||||
The machine-readable spec lives at `docs/picpeak-admin-api.openapi.yaml`. You can:
|
||||
|
||||
- Preview it interactively with Redocly:
|
||||
|
||||
```bash
|
||||
npx --yes @redocly/cli preview-docs docs/picpeak-admin-api.openapi.yaml
|
||||
```
|
||||
|
||||
- Import it into Postman, Insomnia, or VS Code REST client.
|
||||
- Validate changes as part of CI with:
|
||||
|
||||
```bash
|
||||
npx --yes @apidevtools/[email protected] validate docs/picpeak-admin-api.openapi.yaml
|
||||
```
|
||||
|
||||
Keep this file in sync whenever the backend endpoints evolve.
|
||||
|
||||
---
|
||||
|
||||
## 3. Call the Key Admin Endpoints
|
||||
|
||||
Below are minimal `curl` examples that rely on the bearer token captured earlier.
|
||||
|
||||
### 3.1 Create an Event
|
||||
|
||||
```bash
|
||||
API_URL="http://localhost:3001/api"
|
||||
TOKEN="REPLACE_WITH_JWT"
|
||||
|
||||
curl --fail --silent --show-error \
|
||||
-X POST "$API_URL/admin/events" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"event_type": "wedding",
|
||||
"event_name": "Emily & Jordan Celebration",
|
||||
"event_date": "2025-06-07",
|
||||
"customer_name": "Emily Carter",
|
||||
"customer_email": "[email protected]",
|
||||
"admin_email": "[email protected]",
|
||||
"require_password": true,
|
||||
"password": "Shutter123",
|
||||
"expiration_days": 45
|
||||
}' | jq
|
||||
```
|
||||
|
||||
### 3.2 Upload Photos to the Event
|
||||
|
||||
```bash
|
||||
EVENT_ID=512
|
||||
|
||||
curl --fail --silent --show-error \
|
||||
-X POST "$API_URL/admin/events/$EVENT_ID/upload" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-F "photos=@/path/to/DSC_2031.jpg" \
|
||||
-F "photos=@/path/to/DSC_2032.jpg" \
|
||||
-F "category_id=individual" | jq
|
||||
```
|
||||
|
||||
- Files must be JPEG/PNG/WebP, each ≤ 50 MB.
|
||||
- The per-request file count respects the `general_max_files_per_upload` admin setting (default 500).
|
||||
|
||||
### 3.3 Resend the Gallery Email
|
||||
|
||||
```bash
|
||||
curl --fail --silent --show-error \
|
||||
-X POST "$API_URL/admin/events/$EVENT_ID/resend-email" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"password": "Shutter123"}' | jq
|
||||
```
|
||||
|
||||
Omit `"password"` to send the standard security message instead.
|
||||
|
||||
---
|
||||
|
||||
## 4. Quick Testing Checklist
|
||||
|
||||
- ✅ Login succeeds and returns a token (HTTP 200).
|
||||
- ✅ Creating an event returns `id`, `slug`, and `share_link`.
|
||||
- ✅ Uploading more files than allowed returns HTTP 400 with a helpful message.
|
||||
- ✅ Resending email for a missing event returns HTTP 404.
|
||||
- ✅ `swagger-cli validate` passes after any spec edits.
|
||||
|
||||
Automate these checks using your preferred test harness or CI pipeline to catch regressions early.
|
||||
|
||||
---
|
||||
|
||||
## 5. Migrating From `host_*`
|
||||
|
||||
- Run backend migrations to add the new `customer_name` / `customer_email` columns: `npm --prefix backend run migrate` (or your existing deployment flow). The migration copies legacy data automatically, so upgrades remain seamless.
|
||||
- All admin APIs now require the `customer_*` fields. Older `host_*` payloads are rejected, which makes downstream client issues obvious during testing instead of silently dropping data.
|
||||
- API responses still mirror `customer_*` even if migrations have not run yet (the server falls back to legacy columns until the upgrade is complete), so existing frontends can move over incrementally.
|
||||
- Once every consumer writes and reads the new fields, you can safely plan the removal of the legacy `host_*` columns in a future release.
|
||||
|
||||
---
|
||||
|
||||
Need deeper integration examples or language-specific SDKs? Import the OpenAPI spec into code generators such as `openapi-generator` or `orval` to scaffold API clients quickly.
|
||||
@@ -1,59 +0,0 @@
|
||||
# Nginx Configuration Fix for Photo Authentication
|
||||
|
||||
If photos and thumbnails are not loading in gallery view but work in admin, it's likely that the Authorization header is being stripped by nginx or another reverse proxy.
|
||||
|
||||
## Common Issue
|
||||
|
||||
The `Authorization` header is often not passed through by default in nginx proxy configurations.
|
||||
|
||||
## Fix
|
||||
|
||||
Add these lines to your nginx configuration for the PicPeak location block:
|
||||
|
||||
```nginx
|
||||
location / {
|
||||
proxy_pass http://localhost:3001;
|
||||
|
||||
# Important: Pass the Authorization header
|
||||
proxy_pass_header Authorization;
|
||||
proxy_set_header Authorization $http_authorization;
|
||||
|
||||
# Other standard proxy headers
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
```
|
||||
|
||||
## Alternative Fix Using Traefik
|
||||
|
||||
If using Traefik, ensure headers are passed:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
picpeak:
|
||||
labels:
|
||||
- "traefik.http.middlewares.picpeak-headers.headers.customrequestheaders.Authorization="
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
1. Check if Authorization header is reaching the backend:
|
||||
```bash
|
||||
curl -H "Authorization: Bearer YOUR_TOKEN" https://picpeak.yourdomain.com/thumbnails/test.jpg -v
|
||||
```
|
||||
|
||||
2. Check nginx logs to see if the header is present:
|
||||
```bash
|
||||
tail -f /var/log/nginx/access.log
|
||||
```
|
||||
|
||||
## Docker Compose Fix
|
||||
|
||||
If using docker-compose with nginx proxy, add:
|
||||
|
||||
```yaml
|
||||
environment:
|
||||
- NGINX_PROXY_PASS_HEADER=Authorization
|
||||
```
|
||||
@@ -1,416 +0,0 @@
|
||||
{
|
||||
"openapi": "3.0.3",
|
||||
"info": {
|
||||
"title": "PicPeak API",
|
||||
"version": "v1",
|
||||
"description": "Public REST API for PicPeak — create gallery events, upload photos, fetch share links. Authenticate with a Bearer token issued via the admin **Settings → API Tokens** tab."
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
"url": "/api/v1",
|
||||
"description": "Same-origin (production)"
|
||||
}
|
||||
],
|
||||
"components": {
|
||||
"securitySchemes": {
|
||||
"bearerAuth": {
|
||||
"type": "http",
|
||||
"scheme": "bearer",
|
||||
"bearerFormat": "pp_live_*",
|
||||
"description": "Long-lived API token. Issue via Settings → API Tokens. Token format: `pp_live_<random>`. Scopes: `read`, `write`, `admin`."
|
||||
}
|
||||
},
|
||||
"schemas": {
|
||||
"EventSummary": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"slug": {
|
||||
"type": "string"
|
||||
},
|
||||
"event_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"event_type": {
|
||||
"type": "string"
|
||||
},
|
||||
"event_date": {
|
||||
"type": "string",
|
||||
"format": "date",
|
||||
"nullable": true
|
||||
},
|
||||
"expires_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"nullable": true
|
||||
},
|
||||
"is_active": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"is_archived": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"is_draft": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"/events": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Events"
|
||||
],
|
||||
"summary": "Create a gallery event",
|
||||
"description": "Returns the new event's id, slug, and absolute share URL.",
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"event_name",
|
||||
"event_type"
|
||||
],
|
||||
"properties": {
|
||||
"event_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"event_type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"wedding",
|
||||
"birthday",
|
||||
"corporate",
|
||||
"other",
|
||||
"family"
|
||||
]
|
||||
},
|
||||
"event_date": {
|
||||
"type": "string",
|
||||
"format": "date",
|
||||
"nullable": true
|
||||
},
|
||||
"customer_name": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"customer_email": {
|
||||
"type": "string",
|
||||
"format": "email",
|
||||
"nullable": true
|
||||
},
|
||||
"customer_phone": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "Only persisted when the global phone-field setting is enabled."
|
||||
},
|
||||
"admin_email": {
|
||||
"type": "string",
|
||||
"format": "email",
|
||||
"nullable": true
|
||||
},
|
||||
"require_password": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"password": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "Required when require_password is true."
|
||||
},
|
||||
"expires_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"nullable": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Event created",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"slug": {
|
||||
"type": "string"
|
||||
},
|
||||
"share_url": {
|
||||
"type": "string",
|
||||
"format": "uri"
|
||||
},
|
||||
"share_token": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Validation error"
|
||||
},
|
||||
"401": {
|
||||
"description": "Missing/invalid token"
|
||||
},
|
||||
"403": {
|
||||
"description": "Token lacks admin scope"
|
||||
}
|
||||
}
|
||||
},
|
||||
"get": {
|
||||
"tags": [
|
||||
"Events"
|
||||
],
|
||||
"summary": "List gallery events (paginated)",
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"in": "query",
|
||||
"name": "page",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"default": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "limit",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 100,
|
||||
"default": 25
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Paginated list",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"events": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/EventSummary"
|
||||
}
|
||||
},
|
||||
"pagination": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"page": {
|
||||
"type": "integer"
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer"
|
||||
},
|
||||
"total": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/events/{id}": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Events"
|
||||
],
|
||||
"summary": "Get a single event",
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "id",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Event details"
|
||||
},
|
||||
"404": {
|
||||
"description": "Not found"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/events/{id}/photos": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Photos"
|
||||
],
|
||||
"summary": "Upload a single photo to an event",
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "id",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"multipart/form-data": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"photo"
|
||||
],
|
||||
"properties": {
|
||||
"photo": {
|
||||
"type": "string",
|
||||
"format": "binary"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Photo uploaded",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"filename": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"thumbnail_path": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"size_bytes": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "No file or invalid type"
|
||||
},
|
||||
"404": {
|
||||
"description": "Event not found"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/events/{id}/share-link": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Events"
|
||||
],
|
||||
"summary": "Get the absolute share URL for an event",
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "id",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Share URL",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"slug": {
|
||||
"type": "string"
|
||||
},
|
||||
"share_token": {
|
||||
"type": "string"
|
||||
},
|
||||
"share_url": {
|
||||
"type": "string",
|
||||
"format": "uri"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not found"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": []
|
||||
}
|
||||
@@ -1,270 +0,0 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: PicPeak API
|
||||
version: v1
|
||||
description: >-
|
||||
Public REST API for PicPeak — create gallery events, upload photos, fetch share links.
|
||||
Authenticate with a Bearer token issued via the admin **Settings → API Tokens** tab.
|
||||
servers:
|
||||
- url: /api/v1
|
||||
description: Same-origin (production)
|
||||
components:
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: pp_live_*
|
||||
description: >-
|
||||
Long-lived API token. Issue via Settings → API Tokens. Token format: `pp_live_<random>`.
|
||||
Scopes: `read`, `write`, `admin`.
|
||||
schemas:
|
||||
EventSummary:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
slug:
|
||||
type: string
|
||||
event_name:
|
||||
type: string
|
||||
event_type:
|
||||
type: string
|
||||
event_date:
|
||||
type: string
|
||||
format: date
|
||||
nullable: true
|
||||
expires_at:
|
||||
type: string
|
||||
format: date-time
|
||||
nullable: true
|
||||
is_active:
|
||||
type: boolean
|
||||
is_archived:
|
||||
type: boolean
|
||||
is_draft:
|
||||
type: boolean
|
||||
created_at:
|
||||
type: string
|
||||
format: date-time
|
||||
security:
|
||||
- bearerAuth: []
|
||||
paths:
|
||||
/events:
|
||||
post:
|
||||
tags:
|
||||
- Events
|
||||
summary: Create a gallery event
|
||||
description: Returns the new event's id, slug, and absolute share URL.
|
||||
security:
|
||||
- bearerAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- event_name
|
||||
- event_type
|
||||
properties:
|
||||
event_name:
|
||||
type: string
|
||||
event_type:
|
||||
type: string
|
||||
enum:
|
||||
- wedding
|
||||
- birthday
|
||||
- corporate
|
||||
- other
|
||||
- family
|
||||
event_date:
|
||||
type: string
|
||||
format: date
|
||||
nullable: true
|
||||
customer_name:
|
||||
type: string
|
||||
nullable: true
|
||||
customer_email:
|
||||
type: string
|
||||
format: email
|
||||
nullable: true
|
||||
customer_phone:
|
||||
type: string
|
||||
nullable: true
|
||||
description: Only persisted when the global phone-field setting is enabled.
|
||||
admin_email:
|
||||
type: string
|
||||
format: email
|
||||
nullable: true
|
||||
require_password:
|
||||
type: boolean
|
||||
default: true
|
||||
password:
|
||||
type: string
|
||||
nullable: true
|
||||
description: Required when require_password is true.
|
||||
expires_at:
|
||||
type: string
|
||||
format: date-time
|
||||
nullable: true
|
||||
responses:
|
||||
'201':
|
||||
description: Event created
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
slug:
|
||||
type: string
|
||||
share_url:
|
||||
type: string
|
||||
format: uri
|
||||
share_token:
|
||||
type: string
|
||||
'400':
|
||||
description: Validation error
|
||||
'401':
|
||||
description: Missing/invalid token
|
||||
'403':
|
||||
description: Token lacks admin scope
|
||||
get:
|
||||
tags:
|
||||
- Events
|
||||
summary: List gallery events (paginated)
|
||||
security:
|
||||
- bearerAuth: []
|
||||
parameters:
|
||||
- in: query
|
||||
name: page
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 1
|
||||
default: 1
|
||||
- in: query
|
||||
name: limit
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 1
|
||||
maximum: 100
|
||||
default: 25
|
||||
responses:
|
||||
'200':
|
||||
description: Paginated list
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
events:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/EventSummary'
|
||||
pagination:
|
||||
type: object
|
||||
properties:
|
||||
page:
|
||||
type: integer
|
||||
limit:
|
||||
type: integer
|
||||
total:
|
||||
type: integer
|
||||
/events/{id}:
|
||||
get:
|
||||
tags:
|
||||
- Events
|
||||
summary: Get a single event
|
||||
security:
|
||||
- bearerAuth: []
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
responses:
|
||||
'200':
|
||||
description: Event details
|
||||
'404':
|
||||
description: Not found
|
||||
/events/{id}/photos:
|
||||
post:
|
||||
tags:
|
||||
- Photos
|
||||
summary: Upload a single photo to an event
|
||||
security:
|
||||
- bearerAuth: []
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
multipart/form-data:
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- photo
|
||||
properties:
|
||||
photo:
|
||||
type: string
|
||||
format: binary
|
||||
responses:
|
||||
'201':
|
||||
description: Photo uploaded
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
filename:
|
||||
type: string
|
||||
path:
|
||||
type: string
|
||||
thumbnail_path:
|
||||
type: string
|
||||
nullable: true
|
||||
size_bytes:
|
||||
type: integer
|
||||
'400':
|
||||
description: No file or invalid type
|
||||
'404':
|
||||
description: Event not found
|
||||
/events/{id}/share-link:
|
||||
get:
|
||||
tags:
|
||||
- Events
|
||||
summary: Get the absolute share URL for an event
|
||||
security:
|
||||
- bearerAuth: []
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
responses:
|
||||
'200':
|
||||
description: Share URL
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
slug:
|
||||
type: string
|
||||
share_token:
|
||||
type: string
|
||||
share_url:
|
||||
type: string
|
||||
format: uri
|
||||
'404':
|
||||
description: Not found
|
||||
tags: []
|
||||
@@ -1,584 +0,0 @@
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: PicPeak Admin API
|
||||
version: 1.1.11
|
||||
summary: High-level administrative endpoints for creating events, uploading photos, and resending gallery access emails.
|
||||
description: |
|
||||
This document describes the core administrative endpoints that power PicPeak automations.
|
||||
It focuses on the three workflows requested by integrators:
|
||||
|
||||
1. Creating events with customer access credentials.
|
||||
2. Uploading photos in bulk to an event gallery.
|
||||
3. Resending the customer-facing gallery email.
|
||||
|
||||
The specification follows the latest [OpenAPI 3.1](https://spec.openapis.org/oas/v3.1.0) best practices
|
||||
and is intended to be kept in sync with backend changes.
|
||||
contact:
|
||||
name: PicPeak Maintainers
|
||||
url: https://github.com/the-luap/picpeak
|
||||
servers:
|
||||
- url: https://api.picpeak.example.com/api
|
||||
description: Example production deployment
|
||||
- url: http://localhost:3001/api
|
||||
description: Local development
|
||||
tags:
|
||||
- name: Admin Events
|
||||
description: Administrative endpoints for managing event galleries.
|
||||
components:
|
||||
securitySchemes:
|
||||
CookieAuth:
|
||||
type: apiKey
|
||||
in: cookie
|
||||
name: admin_token
|
||||
description: >
|
||||
Session cookie issued by the admin authentication flow. When present, the backend mirrors
|
||||
it into the `Authorization` header automatically.
|
||||
BearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
description: >
|
||||
JSON Web Token created by the admin login endpoint. You can also pass the token explicitly
|
||||
as `Authorization: Bearer <token>` instead of using the admin cookie.
|
||||
parameters:
|
||||
EventId:
|
||||
name: eventId
|
||||
in: path
|
||||
description: Numeric identifier of the event.
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 1
|
||||
example: 341
|
||||
schemas:
|
||||
ErrorResponse:
|
||||
type: object
|
||||
properties:
|
||||
error:
|
||||
type: string
|
||||
description: Human readable error message.
|
||||
details:
|
||||
type: string
|
||||
nullable: true
|
||||
description: Additional context (when available).
|
||||
required:
|
||||
- error
|
||||
example:
|
||||
error: Invalid token
|
||||
ValidationErrorItem:
|
||||
type: object
|
||||
properties:
|
||||
type:
|
||||
type: string
|
||||
nullable: true
|
||||
description: Validation error type reported by express-validator.
|
||||
msg:
|
||||
type: string
|
||||
path:
|
||||
type: string
|
||||
description: Dot-delimited path to the invalid field.
|
||||
value:
|
||||
description: Value that failed validation.
|
||||
location:
|
||||
type: string
|
||||
description: Location of the invalid value (always `body` for these endpoints).
|
||||
required:
|
||||
- msg
|
||||
- path
|
||||
- location
|
||||
example:
|
||||
type: field
|
||||
msg: Event date must be a valid ISO 8601 date
|
||||
path: event_date
|
||||
value: 2025/05/01
|
||||
location: body
|
||||
ValidationErrorResponse:
|
||||
type: object
|
||||
properties:
|
||||
errors:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/ValidationErrorItem'
|
||||
required:
|
||||
- errors
|
||||
example:
|
||||
errors:
|
||||
- type: field
|
||||
msg: Customer email must be a valid address
|
||||
path: customer_email
|
||||
value: example@invalid
|
||||
location: body
|
||||
CreateEventRequest:
|
||||
type: object
|
||||
required:
|
||||
- event_type
|
||||
- event_name
|
||||
- event_date
|
||||
- customer_name
|
||||
- customer_email
|
||||
- admin_email
|
||||
properties:
|
||||
event_type:
|
||||
type: string
|
||||
description: Type of event. Controls default theme and copy in the UI.
|
||||
enum: [wedding, birthday, corporate, other]
|
||||
event_name:
|
||||
type: string
|
||||
minLength: 1
|
||||
description: Display name for the gallery shown to end customers.
|
||||
event_date:
|
||||
type: string
|
||||
format: date
|
||||
description: Event date (YYYY-MM-DD). Used to calculate the default expiration.
|
||||
customer_name:
|
||||
type: string
|
||||
minLength: 1
|
||||
description: Name of the customer receiving gallery access.
|
||||
customer_email:
|
||||
type: string
|
||||
format: email
|
||||
description: Email address of the customer who will receive the gallery link.
|
||||
admin_email:
|
||||
type: string
|
||||
format: email
|
||||
description: Admin contact email included in notification messages.
|
||||
require_password:
|
||||
type: boolean
|
||||
default: true
|
||||
description: When true, the gallery requires `password`; when false a random placeholder is stored.
|
||||
password:
|
||||
type: string
|
||||
minLength: 6
|
||||
description: >
|
||||
Gallery password issued to the customer. Required when `require_password` is `true`.
|
||||
Left unset to auto-generate a placeholder when password protection is disabled.
|
||||
expiration_days:
|
||||
type: integer
|
||||
minimum: 1
|
||||
maximum: 365
|
||||
default: 30
|
||||
description: Number of days after the event date before the gallery expires.
|
||||
welcome_message:
|
||||
type: string
|
||||
description: Optional welcome message displayed in the gallery.
|
||||
color_theme:
|
||||
type: string
|
||||
nullable: true
|
||||
description: Optional theme identifier or CSS color settings.
|
||||
allow_user_uploads:
|
||||
type: boolean
|
||||
default: false
|
||||
description: Allow gallery guests to upload their own photos.
|
||||
upload_category_id:
|
||||
type: integer
|
||||
nullable: true
|
||||
description: ID of the default category for user uploads.
|
||||
allow_downloads:
|
||||
type: boolean
|
||||
default: true
|
||||
description: Allow guests to download photos.
|
||||
disable_right_click:
|
||||
type: boolean
|
||||
default: false
|
||||
description: Disable right-click in the gallery view.
|
||||
watermark_downloads:
|
||||
type: boolean
|
||||
default: false
|
||||
description: Enable watermarking on downloaded images.
|
||||
watermark_text:
|
||||
type: string
|
||||
nullable: true
|
||||
description: Custom watermark text when `watermark_downloads` is true.
|
||||
feedback_enabled:
|
||||
type: boolean
|
||||
default: false
|
||||
description: Enable the feedback module for this gallery.
|
||||
allow_ratings:
|
||||
type: boolean
|
||||
default: true
|
||||
allow_likes:
|
||||
type: boolean
|
||||
default: true
|
||||
allow_comments:
|
||||
type: boolean
|
||||
default: true
|
||||
allow_favorites:
|
||||
type: boolean
|
||||
default: true
|
||||
require_name_email:
|
||||
type: boolean
|
||||
default: false
|
||||
description: Require guests to provide name and email when leaving feedback.
|
||||
moderate_comments:
|
||||
type: boolean
|
||||
default: true
|
||||
description: Hold guest comments for moderation.
|
||||
show_feedback_to_guests:
|
||||
type: boolean
|
||||
default: true
|
||||
description: Display aggregated feedback metrics back to guests.
|
||||
example:
|
||||
event_type: wedding
|
||||
event_name: Emily & Jordan Celebration
|
||||
event_date: 2025-06-07
|
||||
customer_name: Emily Carter
|
||||
customer_email: [email protected]
|
||||
admin_email: [email protected]
|
||||
require_password: true
|
||||
password: Shutter123
|
||||
expiration_days: 45
|
||||
welcome_message: >
|
||||
We loved capturing your day! Use the password below to view and download your photos.
|
||||
allow_user_uploads: false
|
||||
allow_downloads: true
|
||||
feedback_enabled: true
|
||||
allow_comments: true
|
||||
show_feedback_to_guests: true
|
||||
EventSummary:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
description: Database identifier of the newly created event.
|
||||
slug:
|
||||
type: string
|
||||
description: Unique slug used to build the gallery URL.
|
||||
event_name:
|
||||
type: string
|
||||
event_type:
|
||||
type: string
|
||||
enum: [wedding, birthday, corporate, other]
|
||||
customer_name:
|
||||
type: string
|
||||
nullable: true
|
||||
description: Name of the customer associated with the event.
|
||||
customer_email:
|
||||
type: string
|
||||
format: email
|
||||
nullable: true
|
||||
description: Email address of the customer associated with the event.
|
||||
require_password:
|
||||
type: boolean
|
||||
share_link:
|
||||
type: string
|
||||
description: Absolute or relative URL guests can use to reach the gallery.
|
||||
expires_at:
|
||||
type: string
|
||||
format: date-time
|
||||
description: ISO 8601 timestamp when the gallery expires.
|
||||
created_at:
|
||||
type: string
|
||||
format: date-time
|
||||
description: ISO 8601 timestamp when the event was created.
|
||||
required:
|
||||
- id
|
||||
- slug
|
||||
- event_name
|
||||
- event_type
|
||||
- require_password
|
||||
- share_link
|
||||
- expires_at
|
||||
- created_at
|
||||
example:
|
||||
id: 512
|
||||
slug: wedding-emily-jordan-2025-06-07
|
||||
event_name: Emily & Jordan Celebration
|
||||
event_type: wedding
|
||||
customer_name: Emily Carter
|
||||
customer_email: [email protected]
|
||||
require_password: true
|
||||
share_link: https://app.picpeak.io/gallery/wedding-emily-jordan-2025-06-07/2f3c8a4d90bb11ef9b2e0242ac120002
|
||||
expires_at: 2025-07-22T00:00:00.000Z
|
||||
created_at: 2025-05-01T14:32:45.000Z
|
||||
UploadPhotosResponse:
|
||||
type: object
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
photos:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/UploadedPhotoSummary'
|
||||
description: Metadata for each photo that was persisted successfully.
|
||||
totalFiles:
|
||||
type: integer
|
||||
minimum: 0
|
||||
description: Total number of files included in the request (valid + invalid).
|
||||
successCount:
|
||||
type: integer
|
||||
minimum: 0
|
||||
failureCount:
|
||||
type: integer
|
||||
minimum: 0
|
||||
errors:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/UploadFailure'
|
||||
description: Present when some files failed validation or processing.
|
||||
required:
|
||||
- message
|
||||
- photos
|
||||
- totalFiles
|
||||
- successCount
|
||||
- failureCount
|
||||
example:
|
||||
message: Uploaded 18 of 20 photos. 2 failed.
|
||||
photos:
|
||||
- id: 9821
|
||||
filename: DSC_2031.jpg
|
||||
size: 4812096
|
||||
category_id: 2
|
||||
- id: 9822
|
||||
filename: DSC_2032.jpg
|
||||
size: 5216743
|
||||
category_id: 2
|
||||
totalFiles: 20
|
||||
successCount: 18
|
||||
failureCount: 2
|
||||
errors:
|
||||
- filename: DSC_2020.raw
|
||||
error: Only JPEG, PNG and WebP images are allowed
|
||||
- filename: portrait.png
|
||||
error: File is empty
|
||||
UploadedPhotoSummary:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
filename:
|
||||
type: string
|
||||
size:
|
||||
type: integer
|
||||
description: File size in bytes.
|
||||
category_id:
|
||||
type: integer
|
||||
nullable: true
|
||||
required:
|
||||
- id
|
||||
- filename
|
||||
- size
|
||||
example:
|
||||
id: 9821
|
||||
filename: DSC_2031.jpg
|
||||
size: 4812096
|
||||
category_id: 2
|
||||
UploadFailure:
|
||||
type: object
|
||||
properties:
|
||||
filename:
|
||||
type: string
|
||||
error:
|
||||
type: string
|
||||
required:
|
||||
- filename
|
||||
- error
|
||||
example:
|
||||
filename: DSC_2031.gif
|
||||
error: Only JPEG, PNG and WebP images are allowed
|
||||
ResendEmailRequest:
|
||||
type: object
|
||||
properties:
|
||||
password:
|
||||
type: string
|
||||
minLength: 1
|
||||
description: >
|
||||
Optional plain-text password to include in the email. When omitted a security notice
|
||||
placeholder is inserted because the stored hash cannot be reversed.
|
||||
example:
|
||||
password: Shutter123
|
||||
ResendEmailResponse:
|
||||
type: object
|
||||
properties:
|
||||
success:
|
||||
type: boolean
|
||||
message:
|
||||
type: string
|
||||
required:
|
||||
- success
|
||||
- message
|
||||
example:
|
||||
success: true
|
||||
message: Creation email has been queued for sending
|
||||
paths:
|
||||
/admin/events:
|
||||
post:
|
||||
tags: [Admin Events]
|
||||
operationId: createAdminEvent
|
||||
summary: Create a new event
|
||||
description: >
|
||||
Creates a new event, provisions storage folders, stores the gallery password, and queues
|
||||
the initial gallery email for the customer. Requires admin authentication.
|
||||
security:
|
||||
- CookieAuth: []
|
||||
- BearerAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/CreateEventRequest'
|
||||
examples:
|
||||
weddingExample:
|
||||
summary: Wedding with password protection
|
||||
value:
|
||||
event_type: wedding
|
||||
event_name: Emily & Jordan Celebration
|
||||
event_date: 2025-06-07
|
||||
customer_name: Emily Carter
|
||||
customer_email: [email protected]
|
||||
admin_email: [email protected]
|
||||
require_password: true
|
||||
password: Shutter123
|
||||
expiration_days: 45
|
||||
welcome_message: >
|
||||
We loved capturing your day! Use the password below to view and download your photos.
|
||||
allow_user_uploads: false
|
||||
allow_downloads: true
|
||||
feedback_enabled: true
|
||||
allow_comments: true
|
||||
show_feedback_to_guests: true
|
||||
responses:
|
||||
'200':
|
||||
description: Event created successfully.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/EventSummary'
|
||||
'400':
|
||||
description: Validation failed. At least one field is invalid or missing.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ValidationErrorResponse'
|
||||
'401':
|
||||
description: Authentication required or token invalid.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
'500':
|
||||
description: Unexpected server error while creating the event.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
/admin/events/{eventId}/upload:
|
||||
post:
|
||||
tags: [Admin Events]
|
||||
operationId: uploadEventPhotos
|
||||
summary: Upload photos to an event gallery
|
||||
description: |
|
||||
Uploads one or more photos to the specified event. Files are validated, moved into the
|
||||
event storage directory, and thumbnails are generated asynchronously.
|
||||
|
||||
The maximum number of files per upload is controlled via the `general_max_files_per_upload`
|
||||
setting (default 500, capped at 2000). Files exceeding 50 MB are rejected.
|
||||
security:
|
||||
- CookieAuth: []
|
||||
- BearerAuth: []
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/EventId'
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
multipart/form-data:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
photos:
|
||||
type: array
|
||||
description: >
|
||||
One or more image files (JPEG, PNG, WebP). Each file must be <= 50 MB.
|
||||
items:
|
||||
type: string
|
||||
format: binary
|
||||
category_id:
|
||||
oneOf:
|
||||
- type: integer
|
||||
- type: string
|
||||
description: >
|
||||
Optional category assignment. Accepts numeric IDs or the string values `collage`
|
||||
and `individual` for backward compatibility.
|
||||
required:
|
||||
- photos
|
||||
encoding:
|
||||
photos:
|
||||
style: form
|
||||
explode: false
|
||||
responses:
|
||||
'200':
|
||||
description: Upload completed. Failed files (if any) are listed in the response.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/UploadPhotosResponse'
|
||||
'400':
|
||||
description: Request failed validation (invalid files, too many files, etc.).
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
'401':
|
||||
description: Authentication required or token invalid.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
'404':
|
||||
description: The referenced event does not exist.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
'500':
|
||||
description: Unexpected server error while processing uploads.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
/admin/events/{eventId}/resend-email:
|
||||
post:
|
||||
tags: [Admin Events]
|
||||
operationId: resendEventEmail
|
||||
summary: Resend the gallery access email to the customer
|
||||
description: >
|
||||
Queues the standard `gallery_created` email for the event's customer. Useful when resending
|
||||
credentials to the customer or communicating an updated password. Requires admin authentication.
|
||||
security:
|
||||
- CookieAuth: []
|
||||
- BearerAuth: []
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/EventId'
|
||||
requestBody:
|
||||
required: false
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ResendEmailRequest'
|
||||
example:
|
||||
password: NewSecurePassword!
|
||||
responses:
|
||||
'200':
|
||||
description: Email successfully queued for delivery.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ResendEmailResponse'
|
||||
'401':
|
||||
description: Authentication required or token invalid.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
'404':
|
||||
description: Event not found.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
'500':
|
||||
description: Unexpected server error while queuing the email.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
Reference in New Issue
Block a user