Compare commits

...

6 Commits

Author SHA1 Message Date
Gitea Actions Bot ae1b508726 chore: bump version to 1.0.7
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2025-07-13 20:19:10 +00:00
paul 26c05912fc fix: resolve critical production deployment issues
Test and Lint / backend-test (push) Successful in 1m11s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m21s
Version and Release / version-bump (push) Successful in 33s
Version and Release / trigger-drone (push) Successful in 2s
- Fix database connection error "getaddrinfo ENOTFOUND postgres"
- Add wait-for-db.sh script to ensure PostgreSQL is ready before starting
- Fix email processor initialization timing issue
- Add missing storage path environment variables
- Add database dependency to backend service
- Enhance health check endpoint with database connectivity check
- Update production database defaults to match docker-compose
- Install postgresql-client in Docker image for health checks
- Document all required environment variables in .env.example

Fixes immediate production deployment failures and ensures proper service startup order.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-13 22:14:44 +02:00
Gitea Actions Bot d1033cb83a chore: bump version to 1.0.6
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2025-07-13 20:04:46 +00:00
paul b7458b5a37 fix: add missing pg dependency for PostgreSQL support
Test and Lint / backend-test (push) Successful in 1m10s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m7s
Version and Release / version-bump (push) Successful in 36s
Version and Release / trigger-drone (push) Successful in 3s
- Add pg package required for production PostgreSQL connections
- Fixes "Cannot find module 'pg'" error in production deployment

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-13 22:00:41 +02:00
Gitea Actions Bot face8f1496 chore: bump version to 1.0.5
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is passing
2025-07-13 19:54:25 +00:00
paul ba6ee55bf7 chore: clean up obsolete files and documentation
Test and Lint / backend-test (push) Successful in 1m5s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m12s
Version and Release / version-bump (push) Successful in 39s
Version and Release / trigger-drone (push) Successful in 3s
- Remove completed auth migration documentation (13 files)
- Delete unused test and one-time scripts (23 files)
- Remove backup files and old logs
- Clean up duplicate/empty database files
- Remove old migration backup file
- Delete root level setup scripts

Total: ~185KB of obsolete files removed
All active functionality preserved

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-13 21:50:18 +02:00
58 changed files with 332 additions and 4626 deletions
+84
View File
@@ -0,0 +1,84 @@
# Production Deployment Fixes
This document describes the fixes applied to resolve production deployment issues in Docker.
## Issues Fixed
### 1. Database Connection Error: "getaddrinfo ENOTFOUND postgres"
**Problem**: The backend was trying to connect to hostname "postgres" but the database service is named "db" in docker-compose.
**Solution**:
- Updated `knexfile.js` to use correct default host "db" instead of "postgres"
- Added `depends_on: db` to backend service in docker-compose.prod.yml
### 2. Backend Starting Before Database Ready
**Problem**: Backend service started before PostgreSQL was ready, causing connection failures.
**Solution**:
- Created `wait-for-db.sh` script that waits for PostgreSQL to be ready
- Updated Dockerfile to install postgresql-client and use the wait script
- Script also runs migrations automatically on startup
### 3. Email Processor Initialization Failure
**Problem**: Email processor tried to initialize on module load before database was available.
**Solution**:
- Modified `emailProcessor.js` to export initialization functions
- Updated `server.js` to call initialization after database is ready
- Added proper error handling for email service initialization
### 4. Missing Environment Variables
**Problem**: Critical storage path environment variables were missing.
**Solution**:
- Added STORAGE_PATH, EVENTS_PATH, and ARCHIVE_PATH to docker-compose.prod.yml
- Created `.env.example` documenting all required environment variables
### 5. Enhanced Health Check
**Problem**: Basic health check didn't verify database connectivity.
**Solution**:
- Updated `/api/health` endpoint to check database connection
- Returns proper HTTP 503 status when unhealthy
## Files Modified
1. **backend/knexfile.js** - Fixed production database defaults
2. **backend/wait-for-db.sh** - Created database wait script
3. **backend/Dockerfile** - Added postgresql-client and wait script
4. **docker-compose.prod.yml** - Added dependencies and environment variables
5. **backend/src/services/emailProcessor.js** - Disabled auto-initialization
6. **backend/server.js** - Added email initialization and improved health check
7. **backend/.env.example** - Created environment variable documentation
## Deployment Steps
1. Ensure all environment variables are set according to `.env.example`
2. Build and deploy with docker-compose:
```bash
docker-compose -f docker-compose.prod.yml build
docker-compose -f docker-compose.prod.yml up -d
```
3. The backend will now:
- Wait for PostgreSQL to be ready
- Run migrations automatically
- Initialize all services in proper order
- Provide health status at `/api/health`
## Verification
Check deployment health:
```bash
curl http://localhost/api/health
```
Expected response:
```json
{
"status": "ok",
"database": "connected",
"timestamp": "2025-07-13T20:30:00.000Z"
}
```
## Email Configuration
Email service requires configuration in the database. If email is not configured:
- The service will log a warning but continue running
- Emails will be queued but not sent
- Configure email settings in the admin panel after deployment
+33 -34
View File
@@ -1,42 +1,41 @@
NODE_ENV=development
# Backend Environment Variables Example
# Copy this file to .env and update with your values
# Application
NODE_ENV=production
PORT=3000
# URLs
ADMIN_URL=http://localhost:3000
FRONTEND_URL=http://localhost:3001
# Security
JWT_SECRET=dev-secret-key
JWT_SECRET=your-very-secure-jwt-secret-at-least-32-characters-long
# Email Configuration
SMTP_HOST=mailhog
SMTP_PORT=1025
SMTP_SECURE=false
SMTP_USER=
SMTP_PASS=
EMAIL_FROM=noreply@localhost
# Storage Paths (relative to project root)
STORAGE_PATH=./storage
EVENTS_PATH=./storage/events
ARCHIVE_PATH=./storage/events/archived
# Logging
LOG_LEVEL=info
# URLs
ADMIN_URL=https://yourdomain.com
FRONTEND_URL=https://yourdomain.com
# Database Configuration
# Development: Use SQLite
DATABASE_CLIENT=sqlite3
DATABASE_PATH=./data/photo_sharing.db
DATABASE_CLIENT=pg
DB_HOST=db
DB_PORT=5432
DB_USER=picpeak
DB_PASSWORD=your-secure-database-password
DB_NAME=picpeak
# Production: Use PostgreSQL
# DATABASE_CLIENT=pg
# DB_HOST=localhost
# DB_PORT=5432
# DB_USER=picpeak
# DB_PASSWORD=your-secure-password
# DB_NAME=picpeak
# Email Configuration
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_SECURE=false
SMTP_USER=your-smtp-username
SMTP_PASS=your-smtp-password
EMAIL_FROM=noreply@yourdomain.com
# Umami Analytics (optional)
UMAMI_URL=
UMAMI_WEBSITE_ID=
# Storage Paths (Docker)
STORAGE_PATH=/app/storage
EVENTS_PATH=/app/storage/events
ARCHIVE_PATH=/app/storage/events/archived
# Analytics (Optional)
UMAMI_URL=https://analytics.yourdomain.com
UMAMI_WEBSITE_ID=your-website-id
# Logging
LOG_LEVEL=info
-137
View File
@@ -1,137 +0,0 @@
# Quick Guide: Activate Authentication V2 Fixes
## Step 1: Install Dependency
```bash
cd backend
npm install zxcvbn@4.4.2
```
## Step 2: Add to Docker & Run Migration
```bash
# Rebuild Docker with new dependency
docker-compose down
docker-compose up -d --build
# Run migration for token revocation
docker exec wedding-photo-sharing-backend-1 node /app/scripts/add-token-revocation-tables.js
```
## Step 3: Update server.js
### 3.1 Fix Rate Limiting (Line ~10)
```javascript
// Add after other requires
const { createSecureSkipFunction, logRateLimitHit } = require('./src/utils/rateLimitSecurity');
```
### 3.2 Update Rate Limiter (Line ~59)
```javascript
const limiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: process.env.NODE_ENV === 'development' ? 1000 : 100,
skip: createSecureSkipFunction(), // CHANGE THIS LINE
handler: (req, res) => {
logRateLimitHit(req, res); // ADD THIS
res.status(429).json({
error: 'Too many requests from this IP, please try again later.'
});
}
});
```
### 3.3 Update Auth Limiter (Line ~81)
```javascript
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5,
skipSuccessfulRequests: true, // ADD THIS
handler: (req, res) => {
logRateLimitHit(req, res); // ADD THIS
res.status(429).json({
error: 'Too many login attempts, please try again later.',
retryAfter: res.getHeader('Retry-After')
});
}
});
```
### 3.4 Change Auth Routes (Line ~22)
```javascript
// Change from:
const authRoutes = require('./src/routes/auth-enhanced');
// To:
const authRoutes = require('./src/routes/auth-enhanced-v2');
```
### 3.5 Add Token Revocation (After line ~147)
```javascript
// After initializeCleanupJob();
const { initializeRevocationCleanup } = require('./src/utils/tokenRevocation');
initializeRevocationCleanup();
```
## Step 4: Update Middleware Imports
In files that import adminAuth:
```javascript
// Change from:
const { adminAuth } = require('../middleware/auth-enhanced');
// To:
const { adminAuth } = require('../middleware/auth-enhanced-v2');
```
## Step 5: Update adminEvents.js
Add password validation to event creation:
```javascript
// At top of file
const { validatePasswordInContext, getBcryptRounds } = require('../utils/passwordValidation');
// In POST route, after extracting password, add:
const passwordValidation = validatePasswordInContext(password, 'gallery', {
eventName: event_name
});
if (!passwordValidation.valid) {
return res.status(400).json({
error: 'Password does not meet security requirements',
details: passwordValidation.errors,
score: passwordValidation.score,
feedback: passwordValidation.feedback
});
}
// Change password hashing to:
const password_hash = await bcrypt.hash(password, getBcryptRounds());
```
## Step 6: Add Environment Variable
```bash
# In .env file
BCRYPT_ROUNDS=12
```
## Step 7: Restart & Test
```bash
docker-compose restart backend
# Test rate limiting
curl -H "Authorization: Bearer invalid" http://localhost:3001/api/admin/events
# Test password validation
node scripts/test-auth-v2-fixes.js
```
## Verification Checklist
- [ ] zxcvbn installed
- [ ] Token revocation tables created
- [ ] Rate limiting can't be bypassed
- [ ] Weak passwords rejected
- [ ] Password change works
- [ ] No errors in logs
## Rollback
If issues occur:
1. Revert server.js changes
2. Restart backend
3. All new features are additive, so existing functionality remains
-64
View File
@@ -1,64 +0,0 @@
# Authentication & Authorization Flaws Analysis
## Already Fixed ✅
1. **Missing Token Type Validation**
- Fixed in `auth-enhanced.js` line 31
- Checks `decoded.type !== 'admin'`
- Prevents gallery tokens from accessing admin endpoints
2. **No Audit Logging**
- Added `login_attempts` table
- Tracks all login attempts with IP, user agent, timestamp
- Automatic cleanup of old records
3. **Account Lockout Protection**
- Lockout after 5 failed attempts
- 30-minute lockout duration
- Prevents brute force attacks
4. **Basic Session Management**
- Added session timeout middleware
- Tracks active sessions
- Can invalidate sessions
## Still Needs Fixing ❌
### 1. Weak Password Requirements 🔴
- **Current**: No minimum length validation
- **Required**: Minimum 12 characters + complexity
- **Risk**: Vulnerable to brute force
### 2. Rate Limiting Bypass 🔴
- **Current**: Invalid JWT bypasses rate limiting
- **Location**: `server.js:64-71`
- **Risk**: Attackers can spam with invalid tokens
### 3. No Password Complexity 🟡
- **Current**: Any 6+ character password accepted
- **Required**: Upper, lower, number, special char
- **Risk**: Weak passwords
### 4. No Token Revocation 🟡
- **Current**: Tokens valid until expiration
- **Required**: Blacklist/revocation mechanism
- **Risk**: Can't invalidate compromised tokens
### 5. Fixed Bcrypt Rounds 🟡
- **Current**: Hardcoded to 10 rounds
- **Required**: Configurable (12-14 recommended)
- **Risk**: May become insufficient over time
### 6. In-Memory Session Storage 🟡
- **Current**: Sessions stored in memory
- **Required**: Redis or database storage
- **Risk**: Lost on restart, not scalable
## Priority Fixes
1. **Rate Limiting Bypass** (Critical)
2. **Password Requirements** (High)
3. **Password Complexity** (High)
4. **Token Revocation** (Medium)
5. **Bcrypt Rounds** (Medium)
6. **Session Storage** (Low - for scalability)
-216
View File
@@ -1,216 +0,0 @@
# Authentication Security Integration Guide
## How The Enhanced Security Works
### 1. Login Flow with Protection
```
User Login Attempt
Rate Limiter (5 attempts/15 min)
Account Lockout Check
reCAPTCHA Verification
Credentials Validation
Track Login Attempt
Generate Enhanced JWT
```
### 2. Token Structure
**Before** (Basic JWT):
```json
{
"id": 1,
"type": "admin",
"exp": 1234567890
}
```
**After** (Enhanced JWT):
```json
{
"id": 1,
"username": "admin",
"type": "admin",
"ip": "192.168.1.100",
"loginTime": 1234567890,
"exp": 1234567890,
"iss": "picpeak-auth"
}
```
### 3. Security Layers
1. **Network Level**:
- Rate limiting (express-rate-limit)
- CORS restrictions
- Helmet security headers
2. **Application Level**:
- Account lockout (5 attempts)
- reCAPTCHA validation
- Login attempt tracking
3. **Session Level**:
- JWT with expiration
- Session timeout tracking
- IP validation
- Password change detection
4. **Database Level**:
- Bcrypt password hashing
- Audit trail (login_attempts)
- Secure token storage
## Integration Points
### Server.js Changes
```javascript
// Add after database initialization
const { initializeCleanupJob } = require('./src/utils/authSecurity');
initializeCleanupJob();
// Update route import (when ready)
const authRoutes = require('./src/routes/auth-enhanced');
```
### Middleware Updates
For routes requiring enhanced security:
```javascript
// Change from:
router.get('/sensitive', adminAuth, handler);
// To:
const { adminAuth } = require('../middleware/auth-enhanced');
router.get('/sensitive', adminAuth, handler);
```
### Frontend Integration
1. **Handle New Error Codes**:
```javascript
// Lockout error
if (error.response?.status === 423) {
const retryAfter = error.response.data.retryAfter;
showError(`Account locked. Try again in ${retryAfter} seconds`);
}
// Session expired
if (error.response?.data?.code === 'SESSION_TIMEOUT') {
redirectToLogin();
}
```
2. **Implement Logout**:
```javascript
async function logout() {
await api.post('/auth/logout');
clearToken();
redirectToLogin();
}
```
3. **Check Session Status**:
```javascript
async function checkSession() {
const response = await api.get('/auth/session');
if (!response.data.valid) {
redirectToLogin();
}
}
```
## Configuration
### Environment Variables
No new environment variables required. Uses existing:
- `JWT_SECRET` - For token signing
- `NODE_ENV` - For environment detection
### Security Settings
In `authSecurity.js`:
```javascript
const MAX_LOGIN_ATTEMPTS = 5; // Attempts before lockout
const LOCKOUT_DURATION = 30 * 60 * 1000; // 30 minutes
const ATTEMPT_WINDOW = 15 * 60 * 1000; // 15 minute window
```
## Monitoring & Maintenance
### Daily Monitoring
```sql
-- Check for brute force attempts
SELECT identifier, COUNT(*) as attempts,
MAX(attempt_time) as last_attempt
FROM login_attempts
WHERE success = 0
AND attempt_time > datetime('now', '-24 hours')
GROUP BY identifier
HAVING COUNT(*) > 10
ORDER BY attempts DESC;
```
### Weekly Review
```sql
-- Suspicious activity patterns
SELECT DATE(attempt_time) as date,
COUNT(DISTINCT identifier) as unique_users,
COUNT(DISTINCT ip_address) as unique_ips,
COUNT(*) as total_attempts,
SUM(CASE WHEN success = 0 THEN 1 ELSE 0 END) as failed_attempts
FROM login_attempts
WHERE attempt_time > datetime('now', '-7 days')
GROUP BY DATE(attempt_time)
ORDER BY date DESC;
```
### Automated Cleanup
The system automatically cleans up login attempts older than 7 days to prevent database bloat.
## Troubleshooting
### User Locked Out
```sql
-- Check lockout status
SELECT * FROM login_attempts
WHERE identifier = 'user@example.com'
AND attempt_time > datetime('now', '-30 minutes')
ORDER BY attempt_time DESC;
-- Clear lockout
DELETE FROM login_attempts
WHERE identifier = 'user@example.com'
AND success = 0;
```
### Token Issues
```javascript
// Debug token in browser console
const token = localStorage.getItem('token');
const decoded = JSON.parse(atob(token.split('.')[1]));
console.log('Token expires:', new Date(decoded.exp * 1000));
console.log('Token IP:', decoded.ip);
```
## Security Best Practices
1. **Monitor Failed Attempts**: Set up alerts for excessive failures
2. **Review IP Patterns**: Look for geographic anomalies
3. **Rotate JWT Secret**: Periodically update in production
4. **Update Dependencies**: Keep auth libraries current
5. **Test Lockouts**: Regularly verify protection works
## Future Enhancements
1. **Two-Factor Authentication**: Database columns already added
2. **IP Whitelist**: For admin accounts
3. **Device Fingerprinting**: Enhanced session security
4. **OAuth Integration**: Social login options
5. **WebAuthn/Passkeys**: Passwordless authentication
-221
View File
@@ -1,221 +0,0 @@
# Authentication Security Enhancement Migration Guide
## Overview
This guide provides a safe migration path to enhance authentication security without disrupting the production system.
## Security Enhancements Implemented
### 1. Account Lockout Protection
- Locks accounts after 5 failed login attempts within 15 minutes
- 30-minute lockout duration
- Prevents brute force attacks
### 2. Login Attempt Tracking
- Records all login attempts (success/failure)
- Tracks IP addresses and user agents
- Enables security monitoring and alerting
### 3. Enhanced Token Security
- Added issuer validation
- IP address tracking in tokens
- Login time tracking
- Password change detection
### 4. Generic Error Messages
- Prevents user enumeration attacks
- Returns "Invalid credentials" for all auth failures
### 5. Logout Endpoint
- Properly invalidates sessions
- Clears server-side session tracking
## Migration Steps
### Step 1: Database Migrations (Low Risk)
First, run the new migrations to add required tables/columns:
```bash
cd backend
# Run new migrations
npx knex migrate:latest
# Verify migrations
npx knex migrate:status
```
This adds:
- `login_attempts` table
- `password_changed_at` column to `admin_users`
- `last_login_ip` column to `admin_users`
### Step 2: Deploy Enhanced Auth Utilities (Low Risk)
The new files don't affect existing functionality:
- `src/utils/authSecurity.js` - New security utilities
- `src/middleware/auth-enhanced.js` - Enhanced auth middleware
- `src/routes/auth-enhanced.js` - Enhanced auth routes
### Step 3: Gradual Rollout Plan
#### Phase 1: Testing (Day 1)
1. Deploy code but keep using existing auth routes
2. Test enhanced routes in parallel:
```bash
# Test existing endpoint
curl -X POST http://localhost:3001/api/auth/admin/login
# Test enhanced endpoint (if added to routes)
curl -X POST http://localhost:3001/api/auth-enhanced/admin/login
```
#### Phase 2: Monitoring (Days 2-3)
1. Add the auth security initialization to server.js:
```javascript
// In server.js, after database initialization
const { initializeCleanupJob } = require('./src/utils/authSecurity');
initializeCleanupJob();
```
2. Monitor logs for any issues
3. Check login_attempts table is populating
#### Phase 3: Switch Routes (Day 4)
1. Update route imports in server.js:
```javascript
// Change from:
const authRoutes = require('./src/routes/auth');
// To:
const authRoutes = require('./src/routes/auth-enhanced');
```
2. Update middleware imports where needed:
```javascript
// Change from:
const { adminAuth } = require('./src/middleware/auth');
// To:
const { adminAuth } = require('./src/middleware/auth-enhanced');
```
### Step 4: Rollback Plan
If issues occur at any phase:
```bash
# Quick rollback - revert route imports
# In server.js, change back to:
const authRoutes = require('./src/routes/auth');
const { adminAuth } = require('./src/middleware/auth');
# Restart application
docker-compose restart backend
# or
pm2 restart picpeak-backend
```
## Testing Checklist
### Before Production Deployment:
1. **Test Normal Login Flow**:
```bash
# Should work normally
curl -X POST http://localhost:3001/api/auth/admin/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"correct-password"}'
```
2. **Test Account Lockout**:
```bash
# Make 5 failed attempts
for i in {1..5}; do
curl -X POST http://localhost:3001/api/auth/admin/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"wrong-password"}'
done
# 6th attempt should return lockout error
```
3. **Test Logout**:
```bash
curl -X POST http://localhost:3001/api/auth/logout \
-H "Authorization: Bearer YOUR_TOKEN"
```
4. **Test Session Info**:
```bash
curl http://localhost:3001/api/auth/session \
-H "Authorization: Bearer YOUR_TOKEN"
```
## Configuration Options
### Adjusting Security Settings
In `src/utils/authSecurity.js`, you can adjust:
```javascript
const MAX_LOGIN_ATTEMPTS = 5; // Number of attempts before lockout
const LOCKOUT_DURATION = 30 * 60 * 1000; // Lockout time in ms
const ATTEMPT_WINDOW = 15 * 60 * 1000; // Time window for counting attempts
```
## Monitoring
### Check Login Attempts:
```sql
-- Recent failed attempts
SELECT * FROM login_attempts
WHERE success = false
ORDER BY attempt_time DESC
LIMIT 20;
-- Accounts with multiple failures
SELECT identifier, COUNT(*) as failed_attempts
FROM login_attempts
WHERE success = false
AND attempt_time > datetime('now', '-1 hour')
GROUP BY identifier
HAVING COUNT(*) > 3;
```
### Monitor Locked Accounts:
```sql
-- Check currently locked accounts
SELECT identifier, COUNT(*) as attempts,
MAX(attempt_time) as last_attempt
FROM login_attempts
WHERE success = false
AND attempt_time > datetime('now', '-15 minutes')
GROUP BY identifier
HAVING COUNT(*) >= 5;
```
## Security Benefits
1. **Prevents Brute Force**: Account lockout after failed attempts
2. **Audit Trail**: Complete login history for security analysis
3. **Session Security**: Tokens invalidated on password change
4. **IP Monitoring**: Detect suspicious login patterns
5. **User Privacy**: Generic errors prevent user enumeration
## Notes
- Old tokens remain valid until expiration
- No immediate user impact
- Gradual rollout minimizes risk
- Full rollback possible at any stage
## Support
Monitor logs after deployment:
```bash
# Docker
docker-compose logs -f backend | grep -E "(auth|login|security)"
# PM2
pm2 logs picpeak-backend | grep -E "(auth|login|security)"
```
-187
View File
@@ -1,187 +0,0 @@
# Authentication Security Enhancement Rollback Plan
## Quick Rollback Steps
### Immediate Rollback (< 2 minutes)
If auth issues occur after deployment, follow these steps:
```bash
# 1. SSH into production server
ssh your-server
# 2. Navigate to backend directory
cd /path/to/picpeak/backend
# 3. Revert route changes in server.js
# Change from:
# const authRoutes = require('./src/routes/auth-enhanced');
# Back to:
# const authRoutes = require('./src/routes/auth');
# 4. Revert middleware if changed
# Change from:
# const { adminAuth } = require('./src/middleware/auth-enhanced');
# Back to:
# const { adminAuth } = require('./src/middleware/auth');
# 5. Restart application
docker-compose restart backend
# OR
pm2 restart picpeak-backend
```
## Rollback Scenarios
### Scenario 1: Users Can't Login
**Symptoms**:
- All login attempts fail
- Generic "Invalid credentials" error
- Admin panel inaccessible
**Quick Fix**:
```bash
# Revert to original auth routes
cd backend
git checkout HEAD -- server.js
docker-compose restart backend
```
### Scenario 2: Account Lockout Issues
**Symptoms**:
- Legitimate users locked out
- "Account temporarily locked" errors
**Quick Fix**:
```sql
-- Clear all lockouts
DELETE FROM login_attempts WHERE success = false;
-- Or clear specific user
DELETE FROM login_attempts
WHERE identifier = 'username_or_email'
AND success = false;
```
### Scenario 3: Token Validation Errors
**Symptoms**:
- "Invalid token" errors
- Existing sessions broken
- API calls failing
**Quick Fix**:
```javascript
// In auth middleware, temporarily disable strict validation
// Comment out issuer validation:
// issuer: 'picpeak-auth'
// Just use basic verification:
const decoded = jwt.verify(token, process.env.JWT_SECRET);
```
### Scenario 4: Database Migration Issues
**Symptoms**:
- Application won't start
- Database errors in logs
**Rollback Migration**:
```bash
# Rollback last 2 migrations
npx knex migrate:rollback --all
npx knex migrate:up 014_add_default_welcome_message.js
# Or manually fix:
sqlite3 database.db
DROP TABLE IF EXISTS login_attempts;
ALTER TABLE admin_users DROP COLUMN password_changed_at;
ALTER TABLE admin_users DROP COLUMN last_login_ip;
```
## Verification After Rollback
1. **Test Admin Login**:
```bash
curl -X POST http://your-domain/api/auth/admin/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"your-password"}'
```
2. **Test Gallery Access**:
```bash
curl -X POST http://your-domain/api/auth/gallery/verify \
-H "Content-Type: application/json" \
-d '{"slug":"test-gallery","password":"gallery-password"}'
```
3. **Check Logs**:
```bash
# No auth errors should appear
docker-compose logs backend | tail -100 | grep -i error
```
## File Restoration
If files were modified, restore from backup:
```bash
# List of files that can be safely reverted
git checkout HEAD -- src/middleware/auth.js
git checkout HEAD -- src/routes/auth.js
git checkout HEAD -- server.js
# Remove new files (safe to delete)
rm -f src/utils/authSecurity.js
rm -f src/middleware/auth-enhanced.js
rm -f src/routes/auth-enhanced.js
rm -f migrations/015_add_login_attempts_table.js
rm -f migrations/016_add_auth_security_columns.js
```
## Emergency SQL Fixes
```sql
-- Clear all security restrictions
DELETE FROM login_attempts;
-- Reset admin password if locked out
UPDATE admin_users
SET password_hash = '$2b$10$YourKnownGoodHashHere'
WHERE username = 'admin';
-- Remove security columns if causing issues
-- (SQLite doesn't support DROP COLUMN easily, so ignore)
```
## Monitoring After Rollback
```bash
# Watch for stability
watch -n 5 'docker-compose logs backend | tail -20'
# Check active connections
netstat -an | grep :3001 | wc -l
# Monitor CPU/Memory
docker stats wedding-photo-sharing-backend-1
```
## Prevention for Next Attempt
Before re-attempting the security enhancement:
1. **Test in staging environment first**
2. **Implement gradual rollout with feature flags**
3. **Add backwards compatibility for tokens**
4. **Create admin bypass for lockouts**
5. **Set up monitoring alerts**
## Contact
If rollback fails:
1. Check `backend/logs/error.log`
2. Restore from last known good backup
3. Use original auth implementation as reference
-119
View File
@@ -1,119 +0,0 @@
# Authentication Security Enhancement Summary
## Security Issues Fixed
### 1. ✅ Account Lockout Protection
- **Issue**: No protection against brute force attacks
- **Fix**: Lock account after 5 failed attempts in 15 minutes
- **Files**: `authSecurity.js`, `login_attempts` table
### 2. ✅ Login Attempt Tracking
- **Issue**: No audit trail for security monitoring
- **Fix**: Track all login attempts with IP, user agent, timestamp
- **Database**: New `login_attempts` table
### 3. ✅ Generic Error Messages
- **Issue**: Different errors could reveal if username exists
- **Fix**: Always return "Invalid credentials"
- **Impact**: Prevents user enumeration attacks
### 4. ✅ Session Management
- **Issue**: No way to invalidate tokens/logout
- **Fix**: Added `/api/auth/logout` endpoint
- **Fix**: Session tracking with timeout
### 5. ✅ Enhanced Token Security
- **Issue**: Basic JWT with minimal claims
- **Fix**: Added issuer, IP, loginTime claims
- **Fix**: Token invalidation on password change
## Implementation Details
### New Files Created
```
backend/
├── src/
│ ├── utils/
│ │ └── authSecurity.js (122 lines)
│ ├── middleware/
│ │ └── auth-enhanced.js (169 lines)
│ └── routes/
│ └── auth-enhanced.js (244 lines)
├── migrations/
│ ├── 015_add_login_attempts_table.js
│ └── 016_add_auth_security_columns.js
└── scripts/
└── test-auth-security.js
```
### Database Changes
1. **login_attempts** table:
- Tracks all authentication attempts
- Enables lockout and monitoring
2. **admin_users** additions:
- `password_changed_at` - Invalidate old tokens
- `last_login_ip` - Security monitoring
- `two_factor_enabled` - Future 2FA support
## Security Improvements
### Before
- ❌ Unlimited login attempts
- ❌ No audit trail
- ❌ User enumeration possible
- ❌ No session invalidation
- ❌ Basic JWT validation
### After
- ✅ Brute force protection
- ✅ Complete audit trail
- ✅ Generic error messages
- ✅ Logout functionality
- ✅ Enhanced token validation
- ✅ IP tracking
- ✅ Password change detection
## Deployment Safety
### Gradual Rollout
1. **Phase 1**: Deploy code (no impact)
2. **Phase 2**: Run migrations (adds tables only)
3. **Phase 3**: Initialize tracking (monitoring only)
4. **Phase 4**: Switch routes (activates protection)
### Risk Mitigation
- ✅ Backward compatible
- ✅ No breaking changes
- ✅ Existing tokens remain valid
- ✅ Quick rollback possible
- ✅ Comprehensive testing
## Testing Results
```
✅ All 10 security tests passed
✅ Generic errors working
✅ Lockout logic verified
✅ Token enhancements tested
```
## Next Steps
1. **Deploy database migrations** (safe)
2. **Deploy new files** (no impact)
3. **Test in staging** if available
4. **Gradual production rollout**
5. **Monitor login_attempts table**
## Monitoring Commands
```bash
# Check failed login attempts
sqlite3 database.db "SELECT identifier, COUNT(*) as attempts FROM login_attempts WHERE success = 0 AND attempt_time > datetime('now', '-1 hour') GROUP BY identifier"
# View recent login activity
sqlite3 database.db "SELECT * FROM login_attempts ORDER BY attempt_time DESC LIMIT 10"
# Check locked accounts
sqlite3 database.db "SELECT identifier FROM login_attempts WHERE success = 0 GROUP BY identifier HAVING COUNT(*) >= 5"
```
-232
View File
@@ -1,232 +0,0 @@
# Authentication Security V2 Deployment Plan
## Overview
This deployment adds remaining authentication security fixes identified in the security scan.
## New Security Features
### 1. Rate Limiting Bypass Fix ✅
- **File**: `src/utils/rateLimitSecurity.js`
- **Fix**: Properly validates JWT before skipping rate limit
- **Impact**: Prevents attackers from bypassing with invalid tokens
### 2. Password Complexity Requirements ✅
- **File**: `src/utils/passwordValidation.js`
- **Features**:
- Minimum 12 characters (up from 6)
- Must contain: uppercase, lowercase, numbers, special chars
- Password strength scoring (zxcvbn)
- Context-aware validation (admin vs gallery)
- Configurable bcrypt rounds
### 3. Token Revocation System ✅
- **Files**: `src/utils/tokenRevocation.js`, migration
- **Features**:
- Revoke individual tokens
- Revoke all user tokens
- Automatic cleanup of expired revocations
- Check on every auth request
### 4. Enhanced Auth Routes ✅
- **File**: `src/routes/auth-enhanced-v2.js`
- **Features**:
- Password change endpoint with validation
- Real-time password strength checking
- Better error responses with feedback
## Dependencies to Install
```bash
npm install zxcvbn@4.4.2
```
## Database Migrations
```sql
-- Token revocation tables
CREATE TABLE revoked_tokens (
id INTEGER PRIMARY KEY,
token_id TEXT UNIQUE NOT NULL,
user_id INTEGER,
token_type TEXT,
revoked_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMP NOT NULL,
reason TEXT,
metadata TEXT
);
CREATE TABLE user_token_revocations (
user_id INTEGER PRIMARY KEY,
revoked_at TIMESTAMP NOT NULL,
reason TEXT
);
```
## Deployment Steps
### Phase 1: Preparation (Day 1)
1. **Install Dependencies**
```bash
cd backend
npm install zxcvbn@4.4.2
```
2. **Run Migrations**
```bash
docker exec wedding-photo-sharing-backend-1 node scripts/add-token-revocation-tables.js
```
3. **Deploy New Files** (No impact yet)
- `rateLimitSecurity.js`
- `passwordValidation.js`
- `tokenRevocation.js`
- `auth-enhanced-v2.js`
### Phase 2: Testing (Day 2)
1. **Test Rate Limiting Fix**
```bash
# Try with invalid token
curl -H "Authorization: Bearer invalid-token" \
http://localhost:3001/api/admin/events
# Should apply rate limiting
```
2. **Test Password Validation**
```bash
node -e "
const {validatePassword} = require('./src/utils/passwordValidation');
console.log(validatePassword('weak'));
console.log(validatePassword('StrongP@ssw0rd123'));
"
```
### Phase 3: Gradual Activation (Day 3)
#### Step 1: Update Server.js for Rate Limiting
```javascript
// Replace in server.js
const { createSecureSkipFunction, logRateLimitHit } = require('./src/utils/rateLimitSecurity');
const limiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: process.env.NODE_ENV === 'development' ? 1000 : 100,
skip: createSecureSkipFunction(), // NEW: Secure skip function
handler: (req, res) => {
logRateLimitHit(req, res); // NEW: Logging
res.status(429).json({
error: 'Too many requests from this IP, please try again later.'
});
}
});
```
#### Step 2: Update Auth Routes
```javascript
// In server.js, change to v2
const authRoutes = require('./src/routes/auth-enhanced-v2');
```
#### Step 3: Update Middleware
```javascript
// Update imports to use v2
const { adminAuth } = require('./src/middleware/auth-enhanced-v2');
```
#### Step 4: Update Event Creation
```javascript
// In adminEvents.js, add password validation
const { validatePasswordInContext, getBcryptRounds } = require('../utils/passwordValidation');
// In the POST route, add validation before hashing
```
#### Step 5: Initialize Token Revocation
```javascript
// In server.js, after initializeCleanupJob()
const { initializeRevocationCleanup } = require('./src/utils/tokenRevocation');
initializeRevocationCleanup();
```
## Environment Variables
Add to `.env`:
```bash
# Bcrypt rounds (12-14 recommended)
BCRYPT_ROUNDS=12
```
## Testing Checklist
- [ ] Invalid tokens can't bypass rate limiting
- [ ] Weak passwords are rejected
- [ ] Password change requires strong password
- [ ] Tokens can be revoked
- [ ] Revoked tokens are rejected
- [ ] Admin passwords require higher strength
- [ ] Gallery passwords check for event name
## Rollback Plan
### Quick Rollback
```bash
# Revert server.js changes
git checkout HEAD -- server.js
# Restart
docker-compose restart backend
```
### Rollback Specific Features
1. **Rate Limiting**: Revert to old skip function
2. **Password Validation**: Remove validation calls
3. **Token Revocation**: Skip revocation checks
## Monitoring
### Check Password Validation Failures
```bash
docker-compose logs backend | grep "Password validation failed"
```
### Check Rate Limiting
```bash
docker-compose logs backend | grep "Rate limit"
```
### Check Token Revocations
```bash
docker exec wedding-photo-sharing-backend-1 node -e "
const {db} = require('./src/database/db');
db('revoked_tokens').count().first()
.then(r => console.log('Revoked tokens:', r['count(*)'] || 0))
.then(() => db.destroy());
"
```
## Security Improvements
| Feature | Before | After |
|---------|---------|--------|
| Rate Limiting | Can bypass with invalid token | Properly validated |
| Password Length | 6 chars | 12 chars minimum |
| Password Complexity | None | Upper+lower+number+special |
| Password Strength | Not checked | zxcvbn scoring |
| Token Revocation | Not possible | Full revocation system |
| Bcrypt Rounds | Fixed (10) | Configurable (12) |
## Performance Considerations
1. **Password Validation**: ~50ms per check (zxcvbn)
2. **Token Revocation**: Adds 1 DB query per request
3. **Bcrypt Rounds**: 12 rounds = ~250ms (vs 100ms for 10)
## Success Criteria
- ✅ No invalid tokens bypass rate limiting
- ✅ All new passwords meet complexity requirements
- ✅ Password change works with validation
- ✅ Tokens can be revoked on logout
- ✅ No performance degradation > 100ms
-114
View File
@@ -1,114 +0,0 @@
# Authentication V2 Security Fixes Summary
## What We Fixed
### 1. ✅ Rate Limiting Bypass (CRITICAL)
**Issue**: Invalid JWT tokens could bypass rate limiting
**Fix**: Created `rateLimitSecurity.js` that properly validates tokens
**Impact**: Attackers can no longer spam requests with invalid tokens
### 2. ✅ Weak Password Requirements (HIGH)
**Issue**: Only 6 character minimum, no complexity
**Fix**: Created `passwordValidation.js` with:
- 12 character minimum
- Must have: uppercase, lowercase, numbers, special chars
- Password strength scoring (zxcvbn)
- Context-aware validation (prevents username/event name in password)
- Configurable bcrypt rounds (default 12)
**Impact**: Much stronger passwords, resistant to brute force
### 3. ✅ Token Revocation (MEDIUM)
**Issue**: No way to invalidate tokens before expiration
**Fix**: Created `tokenRevocation.js` with full revocation system
- Individual token revocation
- User-level revocation (all tokens)
- Automatic cleanup
- Database tables for tracking
**Impact**: Can now invalidate compromised tokens
### 4. ✅ Enhanced Authentication Routes
**Fix**: Created `auth-enhanced-v2.js` with:
- Password change endpoint with validation
- Real-time password strength API
- Better error messages with feedback
**Impact**: Users get helpful password feedback
## Files Created
```
backend/
├── src/
│ ├── utils/
│ │ ├── rateLimitSecurity.js (118 lines)
│ │ ├── passwordValidation.js (267 lines)
│ │ └── tokenRevocation.js (127 lines)
│ ├── routes/
│ │ ├── auth-enhanced-v2.js (332 lines)
│ │ └── adminEvents-enhanced.js (partial)
│ └── middleware/
│ └── auth-enhanced-v2.js (updated)
├── migrations/
│ └── 017_add_token_revocation_tables.js
├── scripts/
│ ├── add-token-revocation-tables.js
│ └── test-auth-v2-fixes.js
└── server-enhanced.js (partial)
```
## Deployment Status
### Ready to Deploy ✅
- All code written and tested
- Migration scripts ready
- Test scripts available
- Rollback plan documented
### Required Actions
1. Install `zxcvbn` dependency
2. Run token revocation migration
3. Update server.js with new imports
4. Update auth routes to v2
5. Test thoroughly before production
## Security Improvements Summary
| Vulnerability | Severity | Status | Fix |
|--------------|----------|---------|-----|
| Rate Limiting Bypass | 🔴 Critical | ✅ Fixed | Proper token validation |
| Weak Passwords | 🔴 High | ✅ Fixed | 12 chars + complexity |
| No Token Revocation | 🟡 Medium | ✅ Fixed | Full revocation system |
| Fixed Bcrypt Rounds | 🟡 Medium | ✅ Fixed | Configurable (env var) |
| No Password Feedback | 🟡 Low | ✅ Fixed | Strength API endpoint |
## What's Still Pending
From the original auth flaws, these remain lower priority:
1. **In-memory session storage** - Works fine for single instance
2. **No refresh tokens** - 24h tokens are reasonable for this use case
3. **Fixed token expiration** - Could make configurable later
## Testing Commands
```bash
# Test rate limiting fix
node scripts/test-auth-v2-fixes.js
# Test password validation
node -e "
const {validatePassword} = require('./src/utils/passwordValidation');
console.log(validatePassword('Test123!Pass'));
"
# Check if tables exist
docker exec wedding-photo-sharing-backend-1 node scripts/add-token-revocation-tables.js
```
## Next Steps
1. Review `AUTH_V2_DEPLOYMENT_PLAN.md`
2. Install zxcvbn: `npm install zxcvbn@4.4.2`
3. Run migrations
4. Deploy incrementally
5. Monitor for issues
All critical authentication vulnerabilities have been addressed with production-ready fixes!
+6 -3
View File
@@ -16,8 +16,8 @@ FROM node:18-alpine
WORKDIR /app
# Install dumb-init for proper signal handling
RUN apk add --no-cache dumb-init
# Install dumb-init for proper signal handling and postgresql-client for database checks
RUN apk add --no-cache dumb-init postgresql-client
# Create non-root user
RUN addgroup -g 1001 -S nodejs && adduser -S nodejs -u 1001
@@ -26,6 +26,9 @@ RUN addgroup -g 1001 -S nodejs && adduser -S nodejs -u 1001
COPY --from=builder --chown=nodejs:nodejs /app/node_modules ./node_modules
COPY --chown=nodejs:nodejs . .
# Make wait script executable
RUN chmod +x wait-for-db.sh
# Create necessary directories
RUN mkdir -p storage/events/active storage/events/archived storage/thumbnails data logs && \
chown -R nodejs:nodejs storage data logs
@@ -35,4 +38,4 @@ USER nodejs
EXPOSE 3000
ENTRYPOINT ["dumb-init", "--"]
CMD ["node", "server.js"]
CMD ["./wait-for-db.sh", "node", "server.js"]
-215
View File
@@ -1,215 +0,0 @@
# Safe Authentication Security Activation Plan
## Current Situation Analysis
### ✅ What's Already Protected:
- **SQL Injection**: Fully protected with parameterized queries
- **Rate Limiting**: Basic rate limiting active (5 attempts/15 min on /auth)
- **Password Hashing**: Bcrypt in use
- **CORS**: Properly configured
### ❌ What's NOT Protected:
- **No Account Lockout**: After rate limit, users can keep trying
- **No Audit Trail**: Can't track attack patterns
- **No Session Invalidation**: Can't force logout
- **Limited Token Security**: Basic JWT validation only
## Potential Problems & Solutions
### Problem 1: Existing User Sessions
**Risk**: Users might get logged out unexpectedly
**Solution**:
- Enhanced auth accepts old tokens (backward compatible)
- Tokens remain valid until natural expiration
- Only new features (IP check, password change detection) are additions
### Problem 2: Accidental Lockouts
**Risk**: Legitimate users locked out due to typos
**Solution**:
- 5 attempts is reasonable (not too strict)
- 30-minute lockout (not permanent)
- Clear lockout message with retry time
- Admin bypass SQL query ready
### Problem 3: Database Migration Failure
**Risk**: Schema changes could fail
**Solution**:
- Migrations only ADD tables/columns (no modifications)
- Automatic backup before migration
- Rollback plan ready
- SQLite is forgiving with schema changes
### Problem 4: Performance Impact
**Risk**: Login tracking could slow down auth
**Solution**:
- Indexed columns for performance
- Automatic cleanup of old records
- Async logging (non-blocking)
## Step-by-Step Activation Plan
### Phase 1: Pre-Flight Checks (NOW)
```bash
# Run safety check script
cd backend
node scripts/safe-auth-deployment.js
```
This will:
- ✓ Check database health
- ✓ Count active sessions
- ✓ Create backup
- ✓ Test enhanced auth modules
### Phase 2: Database Preparation (SAFE)
```bash
# Run in Docker
docker exec wedding-photo-sharing-backend-1 npx knex migrate:latest
```
Creates:
- `login_attempts` table (new)
- Security columns in `admin_users` (nullable)
### Phase 3: Test Without Activation
```bash
# Test enhanced auth endpoints
chmod +x scripts/test-auth-deployment.sh
./scripts/test-auth-deployment.sh
```
Verifies enhanced auth works before switching
### Phase 4: Gradual Activation
#### Option A: Canary Deployment (SAFEST)
Add temporary route to test:
```javascript
// In server.js, add both temporarily
app.use('/api/auth', authRoutes); // Original
app.use('/api/auth-new', authEnhancedRoutes); // Test enhanced
```
Test with `/api/auth-new/admin/login` first
#### Option B: Feature Flag (RECOMMENDED)
```javascript
// In server.js
const useEnhancedAuth = process.env.USE_ENHANCED_AUTH === 'true';
const authRoutes = useEnhancedAuth
? require('./src/routes/auth-enhanced')
: require('./src/routes/auth');
```
Then activate with environment variable
#### Option C: Direct Switch (FASTER)
```javascript
// Change in server.js
const authRoutes = require('./src/routes/auth-enhanced');
// Add after DB init
const { initializeCleanupJob } = require('./src/utils/authSecurity');
initializeCleanupJob();
```
### Phase 5: Monitor After Activation
```bash
# Run monitoring script
node scripts/monitor-auth-health.js
```
Watch for:
- Sudden spike in failures
- Multiple lockouts
- Low success rate
## Rollback Procedures
### Quick Rollback (< 30 seconds):
```bash
# In server.js, revert to:
const authRoutes = require('./src/routes/auth');
# Restart
docker-compose restart backend
```
### Clear All Lockouts:
```bash
docker exec wedding-photo-sharing-backend-1 node -e "
const {db} = require('./src/database/db');
db('login_attempts').where('success', false).delete()
.then(() => console.log('Lockouts cleared'))
.then(() => db.destroy());
"
```
### Emergency Admin Access:
```sql
-- If admin is locked out
DELETE FROM login_attempts WHERE identifier = 'admin';
```
## Success Criteria
After activation, you should see:
1. ✅ Failed login attempts recorded in database
2. ✅ Account lockout after 5 failures
3. ✅ Logout endpoint working
4. ✅ No increase in auth errors
5. ✅ Existing users still able to login
## Timeline Recommendation
**Day 1 (Now)**:
- Run migrations ✓
- Deploy code ✓
- Test endpoints
**Day 2**:
- Monitor current auth patterns
- Run test script during low traffic
**Day 3**:
- Activate with feature flag
- Monitor closely for 2 hours
- Full activation if stable
**Day 4+**:
- Review login_attempts data
- Adjust thresholds if needed
- Plan 2FA implementation
## Commands Reference
```bash
# Activate enhanced auth
docker exec -it wedding-photo-sharing-backend-1 /bin/sh
vi server.js # Make changes
exit
docker-compose restart backend
# Monitor
docker-compose logs -f backend | grep -i auth
# Check lockouts
docker exec wedding-photo-sharing-backend-1 node -e "
const {db} = require('./src/database/db');
db('login_attempts')
.select('identifier')
.where('success', false)
.where('attempt_time', '>', new Date(Date.now() - 15*60*1000).toISOString())
.groupBy('identifier')
.havingRaw('COUNT(*) >= 5')
.then(locked => console.log('Locked accounts:', locked))
.then(() => db.destroy());
"
```
## Final Safety Notes
1. **It's been tested**: 10/10 unit tests pass
2. **It's backward compatible**: Old tokens work
3. **It's gradual**: Can activate features separately
4. **It's reversible**: Quick rollback available
5. **It's monitored**: Health checking included
The enhanced auth is designed to be transparent to users while significantly improving security. The only visible change is lockout messages after failed attempts.
-167
View File
@@ -1,167 +0,0 @@
# Security Fixes Deployment Complete ✅
## Current Protection Status
### 🛡️ FULLY PROTECTED Against:
1. **SQL Injection**
- All `whereRaw` queries replaced with parameterized queries
- LIKE patterns properly escaped
- Input validation for all user inputs
- **Status**: ACTIVE & PROTECTING
2. **Brute Force Attacks**
- Account lockout after 5 failed attempts
- 30-minute lockout duration
- IP and user agent tracking
- **Status**: ACTIVE & PROTECTING
3. **User Enumeration**
- Generic error messages for all auth failures
- Returns "Invalid credentials" consistently
- **Status**: ACTIVE & PROTECTING
4. **Session Security**
- Enhanced JWT with issuer validation
- IP tracking in tokens
- Password change detection
- Logout endpoint functional
- **Status**: ACTIVE & PROTECTING
5. **Audit Trail**
- All login attempts tracked in database
- Success/failure logging with timestamps
- IP address and user agent recording
- **Status**: ACTIVE & LOGGING
## What Was Done
### Database Changes
- ✅ Created `login_attempts` table for tracking
- ✅ Added security columns to `admin_users`:
- `password_changed_at`
- `last_login_ip`
- `two_factor_enabled`
- `two_factor_secret`
### Code Changes
- ✅ SQL injection fixes in 3 files
- ✅ Enhanced auth middleware deployed
- ✅ Enhanced auth routes active
- ✅ Security utilities in place
- ✅ Cleanup job running
### Files Modified/Created
```
backend/
├── src/
│ ├── utils/
│ │ ├── sqlSecurity.js ✅
│ │ └── authSecurity.js ✅
│ ├── middleware/
│ │ └── auth-enhanced.js ✅
│ └── routes/
│ ├── auth-enhanced.js ✅
│ ├── adminDashboard.js ✅ (SQL fixes)
│ ├── adminEvents.js ✅ (SQL fixes)
│ └── adminPhotos.js ✅ (SQL fixes)
└── server.js ✅ (using enhanced auth)
```
## Monitoring Commands
### Check Login Attempts
```bash
docker exec wedding-photo-sharing-backend-1 node -e "
const {db} = require('./src/database/db');
db('login_attempts')
.orderBy('attempt_time', 'desc')
.limit(10)
.then(attempts => {
console.log('Recent login attempts:');
attempts.forEach(a => {
console.log(\`\${a.attempt_time} - \${a.identifier} - \${a.success ? 'SUCCESS' : 'FAILED'}\`);
});
})
.then(() => db.destroy());
"
```
### Check Locked Accounts
```bash
docker exec wedding-photo-sharing-backend-1 node -e "
const {db} = require('./src/database/db');
db('login_attempts')
.select('identifier')
.where('success', false)
.where('attempt_time', '>', new Date(Date.now() - 15*60*1000).toISOString())
.groupBy('identifier')
.havingRaw('COUNT(*) >= 5')
.then(locked => console.log('Locked accounts:', locked))
.then(() => db.destroy());
"
```
### Monitor Health
```bash
node scripts/monitor-auth-health.js
```
## Rollback Plan (If Needed)
### Quick Rollback
```bash
# Restore original server.js
cp server.js.backup.1752359680463 server.js
# Restart
docker-compose restart backend
```
### Clear Lockouts
```bash
docker exec wedding-photo-sharing-backend-1 node -e "
const {db} = require('./src/database/db');
db('login_attempts').where('success', false).delete()
.then(() => console.log('All lockouts cleared'))
.then(() => db.destroy());
"
```
## Next Steps
### Immediate
1. Monitor logs for any auth errors
2. Watch for excessive lockouts
3. Review login attempts daily
### Short Term (1-2 weeks)
1. Analyze login patterns
2. Adjust lockout thresholds if needed
3. Set up alerts for suspicious activity
### Long Term
1. Implement 2FA (columns already added)
2. Add IP whitelisting for admins
3. Implement password complexity requirements
4. Add password expiration policies
## Security Improvements Summary
| Vulnerability | Before | After | Impact |
|--------------|---------|--------|---------|
| SQL Injection | ❌ Direct interpolation | ✅ Parameterized queries | Critical fix |
| Brute Force | ❌ Unlimited attempts | ✅ 5 attempt lockout | High impact |
| User Enum | ❌ Different errors | ✅ Generic errors | Medium impact |
| Audit Trail | ❌ No tracking | ✅ Complete logging | High value |
| Session Mgmt | ❌ Basic JWT | ✅ Enhanced validation | Medium impact |
## Final Notes
- All fixes are backward compatible
- Existing sessions remain valid
- No user impact expected
- Quick rollback available
- Monitoring in place
The application is now significantly more secure with protection against common attack vectors. The enhanced authentication system provides defense-in-depth with multiple layers of security.
-158
View File
@@ -1,158 +0,0 @@
# SQL Injection Fix Migration Guide
## Overview
This document describes the SQL injection vulnerability fixes applied to the PicPeak backend and the migration process for deploying these fixes to production.
## Vulnerabilities Fixed
### 1. WhereRaw Date Queries (High Risk)
**Location**: `adminDashboard.js`
- **Issue**: Direct string interpolation in SQL date calculations
- **Example**: `.whereRaw(\`timestamp >= datetime("now", "-${days} days")\`)`
- **Fix**: Replaced with parameterized queries using ISO date strings
### 2. LIKE Pattern Injection (Medium Risk)
**Locations**: `adminEvents.js`, `adminPhotos.js`
- **Issue**: Unescaped user input in LIKE queries
- **Example**: `.where('event_name', 'like', \`%${search}%\`)`
- **Fix**: Added proper escaping for LIKE special characters (%, _, \)
### 3. Dynamic Column/Order Injection (Low Risk)
**Locations**: Various sorting operations
- **Issue**: Unvalidated column names in ORDER BY
- **Fix**: Whitelist validation for sort columns and orders
## Files Changed
1. **Created**: `backend/src/utils/sqlSecurity.js`
- Central security utility functions
- `sanitizeDays()` - Validates numeric input
- `escapeLikePattern()` - Escapes LIKE wildcards
- `validateSortColumn()` - Whitelist validation
- `validateSortOrder()` - Ensures only 'asc' or 'desc'
2. **Modified**: `backend/src/routes/adminDashboard.js`
- Lines 21-24, 39-41, 45-47, 57-61, 65-68: Replaced whereRaw with parameterized queries
- Line 4: Added security utility imports
- Line 198: Added sanitizeDays for analytics
3. **Modified**: `backend/src/routes/adminEvents.js`
- Line 11: Added escapeLikePattern import
- Lines 156-161: Escaped search patterns in LIKE queries
4. **Modified**: `backend/src/routes/adminPhotos.js`
- Line 9: Added escapeLikePattern import
- Lines 477-478: Escaped search patterns in LIKE queries
## Migration Steps
### 1. Pre-Deployment Testing
```bash
# Run security utility tests
cd backend
node scripts/test-sql-security.js
# Run verification script
node scripts/verify-sql-fixes.js
```
### 2. Development Environment Testing
```bash
# Start development server
npm run dev
# Test key endpoints:
curl http://localhost:3001/api/admin/dashboard/stats -H "Authorization: Bearer YOUR_TOKEN"
curl http://localhost:3001/api/admin/events?search=test -H "Authorization: Bearer YOUR_TOKEN"
curl http://localhost:3001/api/admin/dashboard/analytics?days=7 -H "Authorization: Bearer YOUR_TOKEN"
```
### 3. Production Deployment
#### Option A: Docker Deployment
```bash
# Pull latest changes
git pull
# Rebuild and restart
docker-compose down
docker-compose up -d --build
```
#### Option B: PM2 Deployment
```bash
# Pull latest changes
git pull
# Install dependencies (if any)
cd backend
npm install
# Restart with PM2
pm2 restart picpeak-backend
```
### 4. Post-Deployment Verification
1. **Monitor Logs**:
```bash
# Docker
docker-compose logs -f backend
# PM2
pm2 logs picpeak-backend
```
2. **Test Critical Functions**:
- Admin dashboard loads correctly
- Event search works with special characters
- Analytics charts display properly
- Photo search functions normally
3. **Check Error Rates**:
- Monitor for any 500 errors
- Check database query logs for errors
## Testing Special Characters
After deployment, test these scenarios:
1. **Search with wildcards**: Search for "50%" or "user_name"
2. **Search with quotes**: Search for "O'Brien"
3. **Date range**: Change analytics to different day ranges
4. **Malicious input**: Try "'; DROP TABLE --" (should return no results)
## Rollback Instructions
If issues occur, see `SQL_INJECTION_FIX_ROLLBACK.md` for immediate rollback steps.
## Performance Impact
- Minimal performance impact expected
- Date calculations now use ISO strings instead of SQLite functions
- LIKE pattern escaping adds negligible overhead
- All changes maintain existing query optimization
## Security Improvements
1. **Eliminated SQL Injection Vectors**: No more direct string interpolation
2. **Input Validation**: All user inputs are validated/sanitized
3. **Parameterized Queries**: Using Knex's built-in parameterization
4. **Defense in Depth**: Multiple layers of protection
## Future Recommendations
1. Add request validation middleware
2. Implement rate limiting on search endpoints
3. Add SQL query logging for security auditing
4. Consider using prepared statements for complex queries
## Questions/Support
If you encounter any issues during migration:
1. Check the rollback plan first
2. Review error logs for specific issues
3. Test individual endpoints to isolate problems
4. Contact development team if needed
-94
View File
@@ -1,94 +0,0 @@
# SQL Injection Fix Rollback Plan
## Overview
This document provides a rollback plan in case the SQL injection fixes cause issues in production.
## Changes Made
1. **Created**: `backend/src/utils/sqlSecurity.js` - Central security utilities
2. **Modified**: `backend/src/routes/adminDashboard.js` - Replaced whereRaw with parameterized queries
3. **Modified**: `backend/src/routes/adminPhotos.js` - Added LIKE pattern escaping
4. **Modified**: `backend/src/routes/adminEvents.js` - Added LIKE pattern escaping
## Quick Rollback Steps
### Step 1: Revert Code Changes
If issues occur, run these commands to revert:
```bash
# Navigate to backend directory
cd backend
# Revert specific files
git checkout HEAD -- src/routes/adminDashboard.js
git checkout HEAD -- src/routes/adminPhotos.js
git checkout HEAD -- src/routes/adminEvents.js
# Remove the new security utility file
rm src/utils/sqlSecurity.js
```
### Step 2: Restart Services
```bash
# If using Docker
docker-compose restart backend
# If using PM2
pm2 restart picpeak-backend
```
## Verification After Rollback
1. Check admin dashboard loads: `/admin/dashboard`
2. Test event search functionality
3. Test photo search functionality
4. Verify analytics charts display correctly
## Symptoms That May Require Rollback
1. **Dashboard Statistics Not Loading**
- Empty or NaN values in stats
- Analytics charts not rendering
2. **Search Features Broken**
- Event search returns no results
- Photo search returns errors
- Special characters in search causing issues
3. **Date Filtering Issues**
- Activity logs not showing correct date ranges
- Analytics showing incorrect time periods
## Safe Testing Before Production
1. **Test in Development First**:
```bash
cd backend
npm run dev
```
2. **Test Key Features**:
- Admin dashboard stats: `http://localhost:3001/api/admin/dashboard/stats`
- Analytics: `http://localhost:3001/api/admin/dashboard/analytics?days=7`
- Event search: `http://localhost:3001/api/admin/events?search=test`
- Photo search: `http://localhost:3001/api/admin/events/1/photos?search=test`
3. **Monitor Logs**:
```bash
# Docker logs
docker-compose logs -f backend
# PM2 logs
pm2 logs picpeak-backend
```
## Emergency Contacts
- Keep database backups before deploying
- Have monitoring alerts for 500 errors
- Document any custom SQL queries in use
## Post-Rollback Actions
If rollback is needed:
1. Document the specific issue encountered
2. Create test cases for the failure scenario
3. Fix the issue in development
4. Re-test thoroughly before re-deploying
-64
View File
@@ -1,64 +0,0 @@
# SQL Injection Fix Summary
## Quick Overview
Fixed SQL injection vulnerabilities in the admin panel endpoints by:
1. Replacing dangerous `whereRaw` queries with parameterized queries
2. Escaping special characters in LIKE patterns
3. Validating sort columns and orders
## Test Results
✅ All 31 security tests passed
✅ Verification script confirms fixes working
✅ No breaking changes to API functionality
## Changed Files
```
backend/
├── src/
│ ├── utils/
│ │ └── sqlSecurity.js (NEW - 117 lines)
│ └── routes/
│ ├── adminDashboard.js (6 changes)
│ ├── adminEvents.js (2 changes)
│ └── adminPhotos.js (2 changes)
└── scripts/
├── test-sql-security.js (NEW)
└── verify-sql-fixes.js (NEW)
```
## Before & After Examples
### Date Range Queries
```javascript
// ❌ BEFORE (Vulnerable)
.whereRaw(`timestamp >= datetime("now", "-${days} days")`)
// ✅ AFTER (Safe)
const startDate = new Date();
startDate.setDate(startDate.getDate() - sanitizeDays(days));
.where('timestamp', '>=', startDate.toISOString())
```
### LIKE Queries
```javascript
// ❌ BEFORE (Vulnerable)
.where('event_name', 'like', `%${search}%`)
// ✅ AFTER (Safe)
const escapedSearch = escapeLikePattern(search);
.where('event_name', 'like', `%${escapedSearch}%`)
```
## Deployment Checklist
- [ ] Run `node scripts/test-sql-security.js` (should show 31/31 passed)
- [ ] Test in development environment
- [ ] Review rollback plan (`SQL_INJECTION_FIX_ROLLBACK.md`)
- [ ] Deploy to production
- [ ] Monitor logs for errors
- [ ] Test search functionality with special characters
## Risk Assessment
- **Risk Level**: Low (with proper testing)
- **Breaking Changes**: None
- **Performance Impact**: Minimal
- **Rollback Time**: < 2 minutes
View File
Binary file not shown.
View File
View File
View File
+4 -4
View File
@@ -27,11 +27,11 @@ const config = {
production: {
client: process.env.DATABASE_CLIENT || 'pg',
connection: {
host: process.env.DB_HOST || 'postgres',
host: process.env.DB_HOST || 'db',
port: process.env.DB_PORT || 5432,
user: process.env.DB_USER || 'postgres',
password: process.env.DB_PASSWORD || 'postgres',
database: process.env.DB_NAME || 'photo_sharing'
user: process.env.DB_USER || 'picpeak',
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME || 'picpeak'
},
pool: {
min: 2,
@@ -1,25 +0,0 @@
const { db } = require('../src/database/db');
async function addMustChangePasswordColumn() {
try {
// Check if the column already exists
const hasMustChangePassword = await db.schema.hasColumn('admin_users', 'must_change_password');
if (!hasMustChangePassword) {
await db.schema.table('admin_users', (table) => {
table.boolean('must_change_password').defaultTo(false);
});
console.log('✅ Added must_change_password column to admin_users table');
} else {
console.log('️ must_change_password column already exists');
}
process.exit(0);
} catch (error) {
console.error('❌ Migration failed:', error);
process.exit(1);
}
}
addMustChangePasswordColumn();
+140 -2
View File
@@ -1,12 +1,12 @@
{
"name": "picpeak-backend",
"version": "1.0.4",
"version": "1.0.7",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "picpeak-backend",
"version": "1.0.4",
"version": "1.0.7",
"dependencies": {
"adm-zip": "^0.5.16",
"archiver": "^5.3.1",
@@ -29,6 +29,7 @@
"multer": "^2.0.1",
"node-cron": "^3.0.2",
"nodemailer": "^6.9.1",
"pg": "^8.16.3",
"react-i18next": "^15.6.0",
"sharp": "^0.32.0",
"sqlite3": "^5.1.6",
@@ -6471,12 +6472,101 @@
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
"license": "MIT"
},
"node_modules/pg": {
"version": "8.16.3",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz",
"integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==",
"license": "MIT",
"dependencies": {
"pg-connection-string": "^2.9.1",
"pg-pool": "^3.10.1",
"pg-protocol": "^1.10.3",
"pg-types": "2.2.0",
"pgpass": "1.0.5"
},
"engines": {
"node": ">= 16.0.0"
},
"optionalDependencies": {
"pg-cloudflare": "^1.2.7"
},
"peerDependencies": {
"pg-native": ">=3.0.1"
},
"peerDependenciesMeta": {
"pg-native": {
"optional": true
}
}
},
"node_modules/pg-cloudflare": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.7.tgz",
"integrity": "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==",
"license": "MIT",
"optional": true
},
"node_modules/pg-connection-string": {
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.1.tgz",
"integrity": "sha512-w6ZzNu6oMmIzEAYVw+RLK0+nqHPt8K3ZnknKi+g48Ak2pr3dtljJW3o+D/n2zzCG07Zoe9VOX3aiKpj+BN0pjg==",
"license": "MIT"
},
"node_modules/pg-int8": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
"integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
"license": "ISC",
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/pg-pool": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.1.tgz",
"integrity": "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==",
"license": "MIT",
"peerDependencies": {
"pg": ">=8.0"
}
},
"node_modules/pg-protocol": {
"version": "1.10.3",
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.3.tgz",
"integrity": "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==",
"license": "MIT"
},
"node_modules/pg-types": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
"integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
"license": "MIT",
"dependencies": {
"pg-int8": "1.0.1",
"postgres-array": "~2.0.0",
"postgres-bytea": "~1.0.0",
"postgres-date": "~1.0.4",
"postgres-interval": "^1.1.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/pg/node_modules/pg-connection-string": {
"version": "2.9.1",
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz",
"integrity": "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==",
"license": "MIT"
},
"node_modules/pgpass": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
"integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
"license": "MIT",
"dependencies": {
"split2": "^4.1.0"
}
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -6575,6 +6665,45 @@
"node": ">=8"
}
},
"node_modules/postgres-array": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
"integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/postgres-bytea": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz",
"integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/postgres-date": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
"integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/postgres-interval": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
"integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
"license": "MIT",
"dependencies": {
"xtend": "^4.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/prebuild-install": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
@@ -7494,6 +7623,15 @@
"source-map": "^0.6.0"
}
},
"node_modules/split2": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
"integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
"license": "ISC",
"engines": {
"node": ">= 10.x"
}
},
"node_modules/sprintf-js": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
+2 -1
View File
@@ -1,6 +1,6 @@
{
"name": "picpeak-backend",
"version": "1.0.4",
"version": "1.0.7",
"description": "Backend for PicPeak event photo sharing platform",
"main": "server.js",
"scripts": {
@@ -32,6 +32,7 @@
"multer": "^2.0.1",
"node-cron": "^3.0.2",
"nodemailer": "^6.9.1",
"pg": "^8.16.3",
"react-i18next": "^15.6.0",
"sharp": "^0.32.0",
"sqlite3": "^5.1.6",
-80
View File
@@ -1,80 +0,0 @@
#!/usr/bin/env node
/**
* Activate enhanced authentication in server.js
* This script safely updates the server configuration
*/
const fs = require('fs').promises;
const path = require('path');
async function activateEnhancedAuth() {
console.log('=== Activating Enhanced Authentication ===\n');
try {
const serverPath = path.join(__dirname, '../server.js');
// Read current server.js
let serverContent = await fs.readFile(serverPath, 'utf8');
// Backup current server.js
const backupPath = `${serverPath}.backup.${Date.now()}`;
await fs.writeFile(backupPath, serverContent);
console.log(`✓ Created backup: ${path.basename(backupPath)}`);
// Check current state
if (serverContent.includes("require('./src/routes/auth-enhanced')")) {
console.log('! Enhanced auth already active');
return;
}
// Replace auth routes import
const originalLine = "const authRoutes = require('./src/routes/auth');";
const enhancedLine = "const authRoutes = require('./src/routes/auth-enhanced');";
if (!serverContent.includes(originalLine)) {
console.log('✗ Could not find original auth import line');
console.log('Please manually update server.js');
return;
}
serverContent = serverContent.replace(originalLine, enhancedLine);
console.log('✓ Updated auth routes import');
// Add cleanup job initialization after database init
const dbInitLine = 'initializeDatabase()';
const cleanupAddition = `
// Initialize auth security cleanup job
const { initializeCleanupJob } = require('./src/utils/authSecurity');
initializeCleanupJob();
`;
if (!serverContent.includes('initializeCleanupJob')) {
const dbInitIndex = serverContent.indexOf(dbInitLine);
if (dbInitIndex !== -1) {
const insertPoint = serverContent.indexOf('\n', dbInitIndex) + 1;
serverContent = serverContent.slice(0, insertPoint) + cleanupAddition + serverContent.slice(insertPoint);
console.log('✓ Added cleanup job initialization');
}
}
// Write updated server.js
await fs.writeFile(serverPath, serverContent);
console.log('✓ Updated server.js');
console.log('\n✅ Enhanced authentication activated!');
console.log('\nNext steps:');
console.log('1. Restart the backend:');
console.log(' docker-compose restart backend');
console.log('\n2. Monitor auth health:');
console.log(' node scripts/monitor-auth-health.js');
console.log('\n3. To rollback if needed:');
console.log(` cp ${path.basename(backupPath)} server.js`);
console.log(' docker-compose restart backend');
} catch (error) {
console.error('❌ Error activating enhanced auth:', error);
}
}
activateEnhancedAuth();
-94
View File
@@ -1,94 +0,0 @@
#!/usr/bin/env node
/**
* Add authentication security tables to existing database
*/
const { db } = require('../src/database/db');
async function addAuthTables() {
console.log('Adding authentication security tables...\n');
try {
// 1. Create login_attempts table
const hasLoginAttempts = await db.schema.hasTable('login_attempts');
if (!hasLoginAttempts) {
await db.schema.createTable('login_attempts', table => {
table.increments('id').primary();
table.string('identifier').notNullable();
table.string('ip_address', 45).notNullable();
table.text('user_agent');
table.timestamp('attempt_time').defaultTo(db.fn.now());
table.boolean('success').defaultTo(false);
// Indexes for performance
table.index('identifier');
table.index('attempt_time');
table.index(['identifier', 'success', 'attempt_time']);
});
console.log('✓ Created login_attempts table');
} else {
console.log('! login_attempts table already exists');
}
// 2. Add columns to admin_users
const hasPasswordChangedAt = await db.schema.hasColumn('admin_users', 'password_changed_at');
if (!hasPasswordChangedAt) {
await db.schema.table('admin_users', table => {
table.timestamp('password_changed_at').nullable();
});
console.log('✓ Added password_changed_at column');
}
const hasLastLoginIp = await db.schema.hasColumn('admin_users', 'last_login_ip');
if (!hasLastLoginIp) {
await db.schema.table('admin_users', table => {
table.string('last_login_ip', 45).nullable();
});
console.log('✓ Added last_login_ip column');
}
const hasTwoFactorEnabled = await db.schema.hasColumn('admin_users', 'two_factor_enabled');
if (!hasTwoFactorEnabled) {
await db.schema.table('admin_users', table => {
table.boolean('two_factor_enabled').defaultTo(false);
});
console.log('✓ Added two_factor_enabled column');
}
const hasTwoFactorSecret = await db.schema.hasColumn('admin_users', 'two_factor_secret');
if (!hasTwoFactorSecret) {
await db.schema.table('admin_users', table => {
table.string('two_factor_secret').nullable();
});
console.log('✓ Added two_factor_secret column');
}
// 3. Verify everything
console.log('\nVerifying tables...');
const loginAttemptsInfo = await db('login_attempts').columnInfo();
console.log('✓ login_attempts columns:', Object.keys(loginAttemptsInfo).join(', '));
const adminUsersInfo = await db('admin_users').columnInfo();
const securityColumns = ['password_changed_at', 'last_login_ip', 'two_factor_enabled', 'two_factor_secret'];
const hasAllColumns = securityColumns.every(col => adminUsersInfo[col]);
if (hasAllColumns) {
console.log('✓ All security columns present in admin_users');
} else {
console.log('✗ Some security columns missing from admin_users');
}
console.log('\n✅ Authentication security tables ready!');
await db.destroy();
process.exit(0);
} catch (error) {
console.error('\n❌ Error adding auth tables:', error);
await db.destroy();
process.exit(1);
}
}
addAuthTables();
@@ -1,71 +0,0 @@
#!/usr/bin/env node
/**
* Add token revocation tables to existing database
*/
const { db } = require('../src/database/db');
async function addTokenRevocationTables() {
console.log('Adding token revocation tables...\n');
try {
// 1. Create revoked_tokens table
const hasRevokedTokens = await db.schema.hasTable('revoked_tokens');
if (!hasRevokedTokens) {
await db.schema.createTable('revoked_tokens', table => {
table.increments('id').primary();
table.string('token_id').notNullable().unique();
table.integer('user_id').nullable();
table.string('token_type', 20);
table.timestamp('revoked_at').defaultTo(db.fn.now());
table.timestamp('expires_at').notNullable();
table.string('reason', 100);
table.text('metadata');
// Indexes
table.index('token_id');
table.index('user_id');
table.index('expires_at');
});
console.log('✓ Created revoked_tokens table');
} else {
console.log('! revoked_tokens table already exists');
}
// 2. Create user_token_revocations table
const hasUserRevocations = await db.schema.hasTable('user_token_revocations');
if (!hasUserRevocations) {
await db.schema.createTable('user_token_revocations', table => {
table.integer('user_id').primary();
table.timestamp('revoked_at').notNullable();
table.string('reason', 100);
table.index('revoked_at');
});
console.log('✓ Created user_token_revocations table');
} else {
console.log('! user_token_revocations table already exists');
}
// 3. Verify tables
console.log('\nVerifying tables...');
const revokedTokensInfo = await db('revoked_tokens').columnInfo();
console.log('✓ revoked_tokens columns:', Object.keys(revokedTokensInfo).join(', '));
const userRevocationsInfo = await db('user_token_revocations').columnInfo();
console.log('✓ user_token_revocations columns:', Object.keys(userRevocationsInfo).join(', '));
console.log('\n✅ Token revocation tables ready!');
await db.destroy();
process.exit(0);
} catch (error) {
console.error('\n❌ Error adding token revocation tables:', error);
await db.destroy();
process.exit(1);
}
}
addTokenRevocationTables();
-96
View File
@@ -1,96 +0,0 @@
#!/usr/bin/env node
/**
* Check Docker deployment status for security fixes
*/
console.log('=== Docker Deployment Status Check ===\n');
// Color codes
const GREEN = '\x1b[32m';
const RED = '\x1b[31m';
const YELLOW = '\x1b[33m';
const RESET = '\x1b[0m';
let allGood = true;
// Check SQL Security
console.log('1. SQL Injection Fixes:');
try {
const { sanitizeDays, escapeLikePattern } = require('../src/utils/sqlSecurity');
console.log(`${GREEN}${RESET} sqlSecurity.js exists`);
console.log(`${GREEN}${RESET} Security functions available`);
} catch (e) {
console.log(`${RED}${RESET} sqlSecurity.js missing`);
allGood = false;
}
// Check Auth Security
console.log('\n2. Authentication Security:');
try {
const authSec = require('../src/utils/authSecurity');
console.log(`${GREEN}${RESET} authSecurity.js exists`);
const authEnhanced = require('../src/middleware/auth-enhanced');
console.log(`${GREEN}${RESET} auth-enhanced middleware exists`);
const authRoutes = require('../src/routes/auth-enhanced');
console.log(`${GREEN}${RESET} auth-enhanced routes exist`);
} catch (e) {
console.log(`${YELLOW}!${RESET} Auth security files exist but not active`);
}
// Check Database
console.log('\n3. Database Status:');
const { db } = require('../src/database/db');
async function checkDatabase() {
try {
// Check login_attempts table
await db('login_attempts').count();
console.log(`${GREEN}${RESET} login_attempts table exists`);
} catch (e) {
console.log(`${YELLOW}!${RESET} login_attempts table not created (run migrations)`);
}
try {
// Check admin_users columns
await db('admin_users').select('password_changed_at').limit(1);
console.log(`${GREEN}${RESET} Auth security columns exist`);
} catch (e) {
console.log(`${YELLOW}!${RESET} Auth security columns missing (run migrations)`);
}
// Close database connection
await db.destroy();
}
// Check server configuration
console.log('\n4. Server Configuration:');
const fs = require('fs');
const serverContent = fs.readFileSync('./server.js', 'utf8');
if (serverContent.includes("require('./src/routes/auth-enhanced')")) {
console.log(`${GREEN}${RESET} Using enhanced auth routes`);
} else if (serverContent.includes("require('./src/routes/auth')")) {
console.log(`${YELLOW}!${RESET} Using original auth routes (enhanced not active)`);
}
if (serverContent.includes('initializeCleanupJob')) {
console.log(`${GREEN}${RESET} Auth cleanup job initialized`);
} else {
console.log(`${YELLOW}!${RESET} Auth cleanup job not initialized`);
}
// Run async checks
checkDatabase().then(() => {
console.log('\n=== Summary ===');
if (allGood) {
console.log(`${GREEN}All security fixes are deployed!${RESET}`);
} else {
console.log(`${YELLOW}Some security features need activation:${RESET}`);
console.log('1. Run migrations: npx knex migrate:latest');
console.log('2. Update server.js to use auth-enhanced routes');
console.log('3. Restart the container');
}
});
-56
View File
@@ -1,56 +0,0 @@
const knex = require('knex')({
client: 'sqlite3',
connection: { filename: '/app/data/photo_sharing.db' },
useNullAsDefault: true
});
async function debugEventPhotos() {
try {
// Get all photos for event 12
const photos = await knex('photos')
.where('event_id', 12)
.select('id', 'filename', 'path', 'thumbnail_path')
.orderBy('id');
console.log('Total photos for event 12:', photos.length);
console.log('\nSample photos:');
// Show first few and specific IDs that were failing
const sampleIds = [1686, 1687, 1688, 1689, 1715, 1717, 1718, 1719];
const samples = photos.filter(p => sampleIds.includes(p.id));
samples.forEach(p => {
console.log(`\nID ${p.id}: ${p.filename}`);
console.log(` Path: ${p.path}`);
console.log(` Thumbnail: ${p.thumbnail_path}`);
});
// Check for any photos without thumbnails
const noThumbs = photos.filter(p => !p.thumbnail_path);
if (noThumbs.length > 0) {
console.log(`\nPhotos without thumbnails: ${noThumbs.length}`);
noThumbs.forEach(p => console.log(` ID ${p.id}: ${p.filename}`));
}
// Check file existence for failing photos
const fs = require('fs').promises;
console.log('\nChecking file existence for samples:');
for (const photo of samples) {
const thumbPath = `/app/storage/${photo.thumbnail_path}`;
try {
await fs.access(thumbPath);
console.log(`✓ ID ${photo.id}: Thumbnail exists at ${thumbPath}`);
} catch (err) {
console.log(`✗ ID ${photo.id}: Thumbnail NOT FOUND at ${thumbPath}`);
}
}
} catch (error) {
console.error('Error:', error);
} finally {
knex.destroy();
}
}
debugEventPhotos();
-81
View File
@@ -1,81 +0,0 @@
#!/usr/bin/env node
const sqlite3 = require('sqlite3').verbose();
const fs = require('fs');
const path = require('path');
// Connect to the database
const dbPath = '/app/data/photo_sharing.db';
console.log(`Connecting to database at: ${dbPath}`);
const db = new sqlite3.Database(dbPath, sqlite3.OPEN_READONLY, (err) => {
if (err) {
console.error('Error opening database:', err.message);
process.exit(1);
}
console.log('Connected to the SQLite database.\n');
});
// Query for photos with IDs 1688 and 1689 where event_id = 12
const query = `
SELECT p.id, p.filename, p.path, p.thumbnail_path, p.event_id,
e.slug as event_slug, e.is_active, e.is_archived
FROM photos p
JOIN events e ON p.event_id = e.id
WHERE p.id IN (1688, 1689) AND p.event_id = 12
`;
console.log('Executing query to get photo details with event information...\n');
db.all(query, [], (err, rows) => {
if (err) {
console.error('Error executing query:', err.message);
db.close();
process.exit(1);
}
console.log(`Found ${rows.length} photo(s):\n`);
if (rows.length === 0) {
console.log('No photos found matching the criteria.');
} else {
rows.forEach((row) => {
console.log('=== Photo ID:', row.id, '===');
console.log('Filename:', row.filename);
console.log('DB Path:', row.path);
console.log('DB Thumbnail Path:', row.thumbnail_path);
console.log('Event ID:', row.event_id);
console.log('Event Slug:', row.event_slug);
console.log('Event is_active:', row.is_active);
console.log('Event is_archived:', row.is_archived);
// Check file existence
const storageBase = '/app/storage';
const eventStatusDir = row.is_active ? 'active' : 'archived';
// Check full image path
const fullImagePath1 = path.join(storageBase, row.path);
const fullImagePath2 = path.join(storageBase, 'events', eventStatusDir, row.path);
console.log('\nChecking full image paths:');
console.log(` Path 1: ${fullImagePath1} - ${fs.existsSync(fullImagePath1) ? 'EXISTS' : 'NOT FOUND'}`);
console.log(` Path 2: ${fullImagePath2} - ${fs.existsSync(fullImagePath2) ? 'EXISTS' : 'NOT FOUND'}`);
// Check thumbnail path
const thumbnailPath = path.join(storageBase, row.thumbnail_path);
console.log('\nChecking thumbnail path:');
console.log(` ${thumbnailPath} - ${fs.existsSync(thumbnailPath) ? 'EXISTS' : 'NOT FOUND'}`);
console.log('\n---\n');
});
}
// Close the database connection
db.close((err) => {
if (err) {
console.error('Error closing database:', err.message);
} else {
console.log('Database connection closed.');
}
});
});
-56
View File
@@ -1,56 +0,0 @@
#!/usr/bin/env node
const sqlite3 = require('sqlite3').verbose();
const path = require('path');
// Connect to the database
const dbPath = '/app/data/photo_sharing.db';
console.log(`Connecting to database at: ${dbPath}`);
const db = new sqlite3.Database(dbPath, sqlite3.OPEN_READONLY, (err) => {
if (err) {
console.error('Error opening database:', err.message);
process.exit(1);
}
console.log('Connected to the SQLite database.');
});
// Query for photos with IDs 1688 and 1689 where event_id = 12
const query = `
SELECT id, filename, path, thumbnail_path
FROM photos
WHERE id IN (1688, 1689) AND event_id = 12
`;
console.log('\nExecuting query:', query);
db.all(query, [], (err, rows) => {
if (err) {
console.error('Error executing query:', err.message);
db.close();
process.exit(1);
}
console.log(`\nFound ${rows.length} photo(s):\n`);
if (rows.length === 0) {
console.log('No photos found matching the criteria.');
} else {
rows.forEach((row) => {
console.log('Photo ID:', row.id);
console.log('Filename:', row.filename);
console.log('Path:', row.path);
console.log('Thumbnail Path:', row.thumbnail_path);
console.log('---');
});
}
// Close the database connection
db.close((err) => {
if (err) {
console.error('Error closing database:', err.message);
} else {
console.log('\nDatabase connection closed.');
}
});
});
-132
View File
@@ -1,132 +0,0 @@
#!/bin/bash
# Authentication Security Enhancement Deployment Script
# This script helps safely deploy auth security enhancements
set -e
echo "=== PicPeak Authentication Security Deployment ==="
echo ""
# Color codes
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Check if we're in the backend directory
if [ ! -f "package.json" ] || [ ! -d "src" ]; then
echo -e "${RED}Error: Must run from backend directory${NC}"
exit 1
fi
# Function to prompt for confirmation
confirm() {
read -p "$1 (y/n): " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo -e "${YELLOW}Deployment cancelled${NC}"
exit 1
fi
}
echo "This script will help deploy authentication security enhancements"
echo ""
echo "Current deployment phase options:"
echo "1. Run database migrations only (safe)"
echo "2. Test enhanced auth endpoints"
echo "3. Switch to enhanced auth (full deployment)"
echo "4. Rollback to original auth"
echo ""
read -p "Select phase (1-4): " PHASE
case $PHASE in
1)
echo -e "${GREEN}Phase 1: Running database migrations${NC}"
confirm "Run migrations?"
echo "Creating backup..."
cp database.db database.db.backup.$(date +%Y%m%d_%H%M%S) 2>/dev/null || true
echo "Running migrations..."
npx knex migrate:latest
echo -e "${GREEN}✓ Migrations completed${NC}"
echo "New tables added: login_attempts"
echo "New columns added to admin_users: password_changed_at, last_login_ip"
;;
2)
echo -e "${GREEN}Phase 2: Testing enhanced auth${NC}"
# Check if server is running
if ! curl -s http://localhost:3001/health > /dev/null; then
echo -e "${RED}Server not running on port 3001${NC}"
exit 1
fi
echo "Running auth security tests..."
node scripts/test-auth-security.js
echo ""
echo "Test endpoints manually:"
echo "- Login: POST /api/auth/admin/login"
echo "- Logout: POST /api/auth/logout"
echo "- Session: GET /api/auth/session"
;;
3)
echo -e "${YELLOW}Phase 3: Full deployment${NC}"
echo "This will switch to enhanced authentication"
confirm "Deploy enhanced auth?"
# Check if migrations are run
if ! npx knex migrate:status | grep -q "015_add_login_attempts_table"; then
echo -e "${RED}Error: Migrations not run. Run phase 1 first.${NC}"
exit 1
fi
echo "Updating server.js to use enhanced auth..."
# This is where you'd update the imports
# For safety, we'll just show what needs to be done
echo -e "${YELLOW}Manual steps required:${NC}"
echo "1. Edit server.js"
echo "2. Change: const authRoutes = require('./src/routes/auth');"
echo " To: const authRoutes = require('./src/routes/auth-enhanced');"
echo "3. Restart the application"
echo ""
echo "After restart, the enhanced auth will be active with:"
echo "- Account lockout protection"
echo "- Login attempt tracking"
echo "- Enhanced security logging"
;;
4)
echo -e "${RED}Phase 4: Rollback${NC}"
confirm "Rollback auth changes?"
echo "Rolling back to original auth..."
echo ""
echo -e "${YELLOW}Manual steps required:${NC}"
echo "1. Edit server.js"
echo "2. Change: const authRoutes = require('./src/routes/auth-enhanced');"
echo " To: const authRoutes = require('./src/routes/auth');"
echo "3. Restart the application"
echo ""
echo "Optional: Clear lockouts"
echo "sqlite3 database.db \"DELETE FROM login_attempts WHERE success = 0\""
;;
*)
echo -e "${RED}Invalid option${NC}"
exit 1
;;
esac
echo ""
echo -e "${GREEN}Done!${NC}"
echo ""
echo "Monitor logs after any changes:"
echo "docker-compose logs -f backend | grep -i auth"
-37
View File
@@ -1,37 +0,0 @@
const path = require('path');
require('dotenv').config({ path: path.join(__dirname, '../.env') });
const { db } = require('../src/database/db');
async function fixDefaultThemes() {
try {
console.log('Fixing events with "default" theme...');
// Find all events with "default" as color_theme
const eventsToFix = await db('events')
.where('color_theme', 'default')
.select('id', 'event_name');
console.log(`Found ${eventsToFix.length} events to fix`);
if (eventsToFix.length > 0) {
// Update them to null so they use the global theme
await db('events')
.where('color_theme', 'default')
.update({ color_theme: null });
console.log('Updated events to use global theme');
eventsToFix.forEach(event => {
console.log(`- Fixed event: ${event.event_name} (ID: ${event.id})`);
});
}
console.log('Theme fix completed successfully');
process.exit(0);
} catch (error) {
console.error('Error fixing themes:', error);
process.exit(1);
}
}
fixDefaultThemes();
-136
View File
@@ -1,136 +0,0 @@
#!/usr/bin/env node
/**
* Monitor authentication health after deployment
* Run this after activating enhanced auth to watch for issues
*/
const { db } = require('../src/database/db');
console.log('=== Authentication Health Monitor ===\n');
console.log('Monitoring auth system... (Press Ctrl+C to stop)\n');
// Color codes
const GREEN = '\x1b[32m';
const RED = '\x1b[31m';
const YELLOW = '\x1b[33m';
const RESET = '\x1b[0m';
let previousStats = {
totalAttempts: 0,
failedAttempts: 0,
lockedAccounts: 0
};
async function getAuthStats() {
try {
const stats = {};
// Total login attempts in last hour
const totalAttempts = await db('login_attempts')
.where('attempt_time', '>', new Date(Date.now() - 60 * 60 * 1000).toISOString())
.count('id as count')
.first();
stats.totalAttempts = totalAttempts.count || 0;
// Failed attempts in last hour
const failedAttempts = await db('login_attempts')
.where('attempt_time', '>', new Date(Date.now() - 60 * 60 * 1000).toISOString())
.where('success', false)
.count('id as count')
.first();
stats.failedAttempts = failedAttempts.count || 0;
// Currently locked accounts
const recentWindow = new Date(Date.now() - 15 * 60 * 1000);
const lockedAccounts = await db('login_attempts')
.select('identifier')
.where('success', false)
.where('attempt_time', '>=', recentWindow.toISOString())
.groupBy('identifier')
.havingRaw('COUNT(*) >= 5');
stats.lockedAccounts = lockedAccounts.length;
// Success rate
stats.successRate = stats.totalAttempts > 0
? ((stats.totalAttempts - stats.failedAttempts) / stats.totalAttempts * 100).toFixed(1)
: 100;
// Recent failures (last 5 minutes)
const recentFailures = await db('login_attempts')
.where('success', false)
.where('attempt_time', '>', new Date(Date.now() - 5 * 60 * 1000).toISOString())
.orderBy('attempt_time', 'desc')
.limit(5)
.select('identifier', 'ip_address', 'attempt_time');
stats.recentFailures = recentFailures;
return stats;
} catch (error) {
return { error: error.message };
}
}
async function displayStats() {
const stats = await getAuthStats();
if (stats.error) {
console.log(`${RED}Error: ${stats.error}${RESET}`);
console.log('Enhanced auth might not be active yet.\n');
return;
}
// Clear console for clean display
console.clear();
console.log('=== Authentication Health Monitor ===\n');
console.log(new Date().toLocaleString());
console.log('─'.repeat(50));
// Display metrics
console.log(`\n📊 Last Hour Statistics:`);
console.log(` Total Login Attempts: ${stats.totalAttempts}`);
console.log(` Failed Attempts: ${stats.failedAttempts}`);
console.log(` Success Rate: ${stats.successRate}%`);
console.log(` Currently Locked: ${stats.lockedAccounts} accounts`);
// Alerts
if (stats.failedAttempts > previousStats.failedAttempts + 10) {
console.log(`\n${RED}⚠️ ALERT: Spike in failed login attempts!${RESET}`);
}
if (stats.lockedAccounts > 5) {
console.log(`\n${YELLOW}⚠️ WARNING: Multiple accounts locked (${stats.lockedAccounts})${RESET}`);
}
if (stats.successRate < 50) {
console.log(`\n${RED}⚠️ ALERT: Low success rate (${stats.successRate}%)${RESET}`);
}
// Recent failures
if (stats.recentFailures && stats.recentFailures.length > 0) {
console.log(`\n📋 Recent Failed Attempts (last 5 min):`);
stats.recentFailures.forEach(failure => {
const time = new Date(failure.attempt_time).toLocaleTimeString();
console.log(` ${time} - ${failure.identifier} from ${failure.ip_address}`);
});
}
// Health status
console.log(`\n✅ Status: ${stats.failedAttempts === 0 ? 'Healthy' : 'Active'}`);
console.log('\nPress Ctrl+C to stop monitoring\n');
previousStats = stats;
}
// Monitor every 10 seconds
setInterval(displayStats, 10000);
// Initial display
displayStats();
// Graceful shutdown
process.on('SIGINT', async () => {
console.log('\nStopping monitor...');
await db.destroy();
process.exit(0);
});
-269
View File
@@ -1,269 +0,0 @@
#!/usr/bin/env node
/**
* Safe Authentication Deployment Script
* Carefully activates auth security with multiple safety checks
*/
const { db } = require('../src/database/db');
const logger = require('../src/utils/logger');
console.log('=== Safe Authentication Security Deployment ===\n');
// Color codes
const GREEN = '\x1b[32m';
const RED = '\x1b[31m';
const YELLOW = '\x1b[33m';
const BLUE = '\x1b[34m';
const RESET = '\x1b[0m';
async function runSafetyChecks() {
console.log(`${BLUE}Running pre-deployment safety checks...${RESET}\n`);
const checks = {
databaseConnected: false,
adminUsersExist: false,
activeSessionsExist: false,
migrationsReady: true,
diskSpace: true
};
try {
// Check 1: Database connection
await db.raw('SELECT 1');
checks.databaseConnected = true;
console.log(`${GREEN}${RESET} Database connection healthy`);
} catch (e) {
console.log(`${RED}${RESET} Database connection failed`);
return checks;
}
try {
// Check 2: Admin users exist
const adminCount = await db('admin_users').count('id as count').first();
checks.adminUsersExist = adminCount.count > 0;
console.log(`${GREEN}${RESET} Found ${adminCount.count} admin users`);
} catch (e) {
console.log(`${RED}${RESET} Could not check admin users`);
}
try {
// Check 3: Check for active sessions (optional warning)
const recentLogins = await db('access_logs')
.where('action', 'login_success')
.where('timestamp', '>', new Date(Date.now() - 60 * 60 * 1000).toISOString())
.count('id as count')
.first();
if (recentLogins.count > 0) {
checks.activeSessionsExist = true;
console.log(`${YELLOW}!${RESET} Warning: ${recentLogins.count} active sessions in last hour`);
} else {
console.log(`${GREEN}${RESET} No recent active sessions`);
}
} catch (e) {
// Table might not exist yet, that's ok
console.log(`${GREEN}${RESET} No access logs table yet (expected)`);
}
try {
// Check 4: Check if migrations would conflict
const tables = await db.raw(`
SELECT name FROM sqlite_master
WHERE type='table' AND name IN ('login_attempts')
`);
if (tables.length > 0) {
console.log(`${YELLOW}!${RESET} login_attempts table already exists`);
checks.migrationsReady = false;
} else {
console.log(`${GREEN}${RESET} Ready to create login_attempts table`);
}
} catch (e) {
console.log(`${RED}${RESET} Could not check existing tables`);
checks.migrationsReady = false;
}
return checks;
}
async function backupDatabase() {
console.log(`\n${BLUE}Creating database backup...${RESET}`);
try {
const fs = require('fs').promises;
const path = require('path');
const dbPath = process.env.DB_PATH || './data/database.db';
const backupPath = `${dbPath}.backup.${Date.now()}`;
await fs.copyFile(dbPath, backupPath);
console.log(`${GREEN}${RESET} Database backed up to: ${path.basename(backupPath)}`);
return backupPath;
} catch (e) {
console.log(`${YELLOW}!${RESET} Could not create backup: ${e.message}`);
return null;
}
}
async function runMigrations() {
console.log(`\n${BLUE}Running database migrations...${RESET}`);
try {
// Run migrations
const knex = db;
await knex.migrate.latest();
console.log(`${GREEN}${RESET} Migrations completed successfully`);
// Verify tables exist
const loginAttempts = await db('login_attempts').count().first();
console.log(`${GREEN}${RESET} login_attempts table created`);
const adminColumns = await db('admin_users').columnInfo();
if (adminColumns.password_changed_at) {
console.log(`${GREEN}${RESET} Security columns added to admin_users`);
}
return true;
} catch (e) {
console.log(`${RED}${RESET} Migration failed: ${e.message}`);
return false;
}
}
async function testEnhancedAuth() {
console.log(`\n${BLUE}Testing enhanced authentication (without activating)...${RESET}`);
try {
// Test that enhanced modules load correctly
const authSecurity = require('../src/utils/authSecurity');
const authEnhanced = require('../src/middleware/auth-enhanced');
const authRoutes = require('../src/routes/auth-enhanced');
console.log(`${GREEN}${RESET} Enhanced auth modules load correctly`);
// Test lockout logic (without real data)
const lockoutStatus = await authSecurity.checkAccountLockout('test-user-that-doesnt-exist');
console.log(`${GREEN}${RESET} Account lockout check works: ${lockoutStatus.isLocked ? 'locked' : 'not locked'}`);
// Test generic error
const error = authSecurity.getGenericAuthError();
console.log(`${GREEN}${RESET} Generic error message: "${error}"`);
return true;
} catch (e) {
console.log(`${RED}${RESET} Enhanced auth test failed: ${e.message}`);
return false;
}
}
async function createDeploymentInstructions() {
console.log(`\n${BLUE}Deployment Instructions:${RESET}\n`);
const instructions = `
${GREEN}Step 1: Update server.js${RESET}
Change:
${YELLOW}const authRoutes = require('./src/routes/auth');${RESET}
To:
${GREEN}const authRoutes = require('./src/routes/auth-enhanced');${RESET}
Add after database initialization:
${GREEN}const { initializeCleanupJob } = require('./src/utils/authSecurity');
initializeCleanupJob();${RESET}
${GREEN}Step 2: Update middleware imports (if needed)${RESET}
In files using adminAuth, change:
${YELLOW}const { adminAuth } = require('../middleware/auth');${RESET}
To:
${GREEN}const { adminAuth } = require('../middleware/auth-enhanced');${RESET}
${GREEN}Step 3: Restart the application${RESET}
${BLUE}docker-compose restart backend${RESET}
or
${BLUE}pm2 restart picpeak-backend${RESET}
${GREEN}Step 4: Monitor logs${RESET}
${BLUE}docker-compose logs -f backend | grep -i auth${RESET}
${YELLOW}Rollback if needed:${RESET}
Revert server.js changes and restart
`;
console.log(instructions);
}
async function main() {
try {
// Step 1: Run safety checks
const checks = await runSafetyChecks();
if (!checks.databaseConnected) {
console.log(`\n${RED}Cannot proceed: Database not connected${RESET}`);
process.exit(1);
}
if (checks.activeSessionsExist) {
console.log(`\n${YELLOW}Warning: There are active user sessions.`);
console.log(`Consider deploying during low-traffic period.${RESET}`);
}
// Step 2: Backup database
const backupPath = await backupDatabase();
// Step 3: Run migrations
console.log(`\n${YELLOW}Ready to run migrations. This will:`);
console.log(`- Create login_attempts table`);
console.log(`- Add security columns to admin_users`);
console.log(`No existing data will be modified.${RESET}\n`);
const readline = require('readline').createInterface({
input: process.stdin,
output: process.stdout
});
readline.question('Continue with migrations? (y/n): ', async (answer) => {
if (answer.toLowerCase() !== 'y') {
console.log(`${YELLOW}Deployment cancelled${RESET}`);
readline.close();
await db.destroy();
return;
}
const migrationSuccess = await runMigrations();
if (!migrationSuccess) {
console.log(`\n${RED}Migrations failed. Database backup available at: ${backupPath}${RESET}`);
readline.close();
await db.destroy();
return;
}
// Step 4: Test enhanced auth
const authTestSuccess = await testEnhancedAuth();
if (!authTestSuccess) {
console.log(`\n${YELLOW}Enhanced auth tests failed, but migrations succeeded.`);
console.log(`Review the errors before activating enhanced auth.${RESET}`);
}
// Step 5: Show deployment instructions
await createDeploymentInstructions();
console.log(`\n${GREEN}✓ Pre-deployment complete!${RESET}`);
console.log(`${YELLOW}Enhanced auth is ready but NOT YET ACTIVE.${RESET}`);
console.log(`Follow the instructions above to activate when ready.\n`);
readline.close();
await db.destroy();
});
} catch (error) {
console.error(`${RED}Deployment script error:${RESET}`, error);
await db.destroy();
process.exit(1);
}
}
// Run the deployment
main();
-47
View File
@@ -1,47 +0,0 @@
const path = require('path');
require('dotenv').config({ path: path.join(__dirname, '../.env') });
const { db } = require('../src/database/db');
async function seedCategories() {
try {
console.log('Seeding default categories...');
const defaultCategories = [
{ name: 'Portraits', slug: 'portraits' },
{ name: 'Group Photos', slug: 'group-photos' },
{ name: 'Ceremony', slug: 'ceremony' },
{ name: 'Reception', slug: 'reception' },
{ name: 'Dancing', slug: 'dancing' },
{ name: 'Candids', slug: 'candids' },
{ name: 'Details', slug: 'details' },
{ name: 'Getting Ready', slug: 'getting-ready' }
];
for (const category of defaultCategories) {
// Check if category already exists
const existing = await db('photo_categories')
.where({ slug: category.slug, is_global: true })
.first();
if (!existing) {
await db('photo_categories').insert({
name: category.name,
slug: category.slug,
is_global: true,
event_id: null
});
console.log(`Created category: ${category.name}`);
} else {
console.log(`Category already exists: ${category.name}`);
}
}
console.log('Default categories seeded successfully');
process.exit(0);
} catch (error) {
console.error('Error seeding categories:', error);
process.exit(1);
}
}
seedCategories();
-55
View File
@@ -1,55 +0,0 @@
const axios = require('axios');
async function testAdminPhotoEndpoint() {
try {
// First login
console.log('1. Logging in as admin...');
const loginResponse = await axios.post('http://localhost:3000/api/admin/auth/login', {
username: 'admin',
password: 'admin123'
});
const token = loginResponse.data.token;
console.log('✓ Login successful, got token');
// Test thumbnail endpoint
console.log('\n2. Testing thumbnail endpoint for photo 1688...');
try {
const thumbResponse = await axios.get('http://localhost:3000/api/admin/events/12/thumbnail/1688', {
headers: {
Authorization: `Bearer ${token}`
},
responseType: 'arraybuffer'
});
console.log('✓ Thumbnail request successful');
console.log(' Response headers:', thumbResponse.headers);
console.log(' Data size:', thumbResponse.data.length, 'bytes');
} catch (error) {
console.error('✗ Thumbnail request failed:', error.response?.status, error.response?.data?.toString());
}
// Test from frontend proxy port
console.log('\n3. Testing through nginx proxy (port 3001)...');
try {
const proxyResponse = await axios.get('http://localhost:3001/api/admin/events/12/thumbnail/1688', {
headers: {
Authorization: `Bearer ${token}`,
Origin: 'http://localhost:3005'
},
responseType: 'arraybuffer'
});
console.log('✓ Proxy request successful');
console.log(' Response headers:', proxyResponse.headers);
console.log(' Data size:', proxyResponse.data.length, 'bytes');
} catch (error) {
console.error('✗ Proxy request failed:', error.response?.status, error.response?.data?.toString());
}
} catch (error) {
console.error('Error:', error.message);
}
}
testAdminPhotoEndpoint();
-146
View File
@@ -1,146 +0,0 @@
#!/bin/bash
# Test authentication deployment
# This script tests the enhanced auth without affecting production
set -e
echo "=== Testing Authentication Deployment ==="
echo ""
# Color codes
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Configuration
API_URL="http://localhost:3001/api"
TEST_USER="admin"
TEST_PASS="wrong-password"
echo -e "${BLUE}This script will test the authentication system${NC}"
echo "It will make failed login attempts to test lockout"
echo ""
# Check if server is running
echo -e "${BLUE}Checking server status...${NC}"
if curl -s -f "$API_URL/../health" > /dev/null; then
echo -e "${GREEN}✓ Server is running${NC}"
else
echo -e "${RED}✗ Server not accessible at $API_URL${NC}"
exit 1
fi
# Function to make login attempt
make_login_attempt() {
local username=$1
local password=$2
local expected_status=$3
response=$(curl -s -w "\n%{http_code}" -X POST "$API_URL/auth/admin/login" \
-H "Content-Type: application/json" \
-d "{\"username\":\"$username\",\"password\":\"$password\"}")
http_code=$(echo "$response" | tail -n1)
body=$(echo "$response" | head -n-1)
if [ "$http_code" -eq "$expected_status" ]; then
echo -e "${GREEN}${NC} Got expected status $http_code"
return 0
else
echo -e "${RED}${NC} Expected $expected_status, got $http_code"
echo "Response: $body"
return 1
fi
}
# Test 1: Normal failed login
echo -e "\n${BLUE}Test 1: Normal failed login${NC}"
make_login_attempt "$TEST_USER" "$TEST_PASS" 401
# Test 2: Multiple failed attempts (testing lockout)
echo -e "\n${BLUE}Test 2: Testing account lockout (5 attempts)${NC}"
echo "Making 4 more failed attempts..."
for i in {2..5}; do
echo -n "Attempt $i: "
make_login_attempt "$TEST_USER" "$TEST_PASS" 401
sleep 1
done
# Test 3: 6th attempt should be locked
echo -e "\n${BLUE}Test 3: 6th attempt (should be locked if enhanced auth active)${NC}"
echo -n "Attempt 6: "
response=$(curl -s -w "\n%{http_code}" -X POST "$API_URL/auth/admin/login" \
-H "Content-Type: application/json" \
-d "{\"username\":\"$TEST_USER\",\"password\":\"$TEST_PASS\"}")
http_code=$(echo "$response" | tail -n1)
body=$(echo "$response" | head -n-1)
if [ "$http_code" -eq "423" ]; then
echo -e "${GREEN}✓ Account locked as expected!${NC}"
echo -e "${GREEN}Enhanced auth is ACTIVE${NC}"
echo "Lockout message: $(echo $body | jq -r '.error')"
ENHANCED_ACTIVE=true
elif [ "$http_code" -eq "401" ]; then
echo -e "${YELLOW}! Still got 401 - Enhanced auth NOT active${NC}"
echo "Original auth is still in use"
ENHANCED_ACTIVE=false
else
echo -e "${RED}✗ Unexpected status: $http_code${NC}"
echo "Response: $body"
fi
# Test 4: Check if we can query login attempts
echo -e "\n${BLUE}Test 4: Checking login attempts table${NC}"
if [ "$ENHANCED_ACTIVE" = true ]; then
# This would need database access, so we'll check via API behavior
echo -e "${GREEN}✓ Login tracking is active${NC}"
else
echo -e "${YELLOW}! Login tracking not active (migrations might not be run)${NC}"
fi
# Test 5: Test logout endpoint
echo -e "\n${BLUE}Test 5: Testing logout endpoint${NC}"
# First need a valid token (this assumes you have one for testing)
# For now, just check if endpoint exists
logout_response=$(curl -s -w "\n%{http_code}" -X POST "$API_URL/auth/logout" \
-H "Authorization: Bearer invalid-token")
logout_code=$(echo "$logout_response" | tail -n1)
if [ "$logout_code" -eq "200" ] || [ "$logout_code" -eq "401" ]; then
echo -e "${GREEN}✓ Logout endpoint exists${NC}"
else
echo -e "${YELLOW}! Logout endpoint might not be active${NC}"
fi
# Summary
echo -e "\n${BLUE}=== Summary ===${NC}"
if [ "$ENHANCED_ACTIVE" = true ]; then
echo -e "${GREEN}✅ Enhanced authentication is ACTIVE${NC}"
echo "- Account lockout protection: Working"
echo "- Login attempt tracking: Active"
echo "- Enhanced security: Enabled"
echo ""
echo -e "${YELLOW}Note: Test account might be locked for 30 minutes${NC}"
else
echo -e "${YELLOW}⚠️ Enhanced authentication is NOT ACTIVE${NC}"
echo "- Using original auth system"
echo "- No lockout protection"
echo "- No login tracking"
echo ""
echo "To activate:"
echo "1. Run migrations: docker exec wedding-photo-sharing-backend-1 npx knex migrate:latest"
echo "2. Update server.js to use auth-enhanced routes"
echo "3. Restart: docker-compose restart backend"
fi
echo ""
echo "Test complete!"
-112
View File
@@ -1,112 +0,0 @@
#!/usr/bin/env node
/**
* Test script to verify authentication security enhancements
*/
console.log('=== Testing Authentication Security Enhancements ===\n');
const {
checkAccountLockout,
getGenericAuthError,
MAX_LOGIN_ATTEMPTS,
LOCKOUT_DURATION
} = require('../src/utils/authSecurity');
let passed = 0;
let failed = 0;
function test(description, fn) {
try {
const result = fn();
if (result || result === undefined) {
console.log(`${description}`);
passed++;
} else {
console.log(`${description}`);
failed++;
}
} catch (error) {
console.log(`${description} - Error: ${error.message}`);
failed++;
}
}
// Test generic error message
console.log('Testing generic error messages:');
test('Generic error prevents user enumeration', () => {
const error = getGenericAuthError();
return error === 'Invalid credentials';
});
// Test constants
console.log('\nTesting security constants:');
test('Max login attempts is reasonable', () => MAX_LOGIN_ATTEMPTS === 5);
test('Lockout duration is 30 minutes', () => LOCKOUT_DURATION === 30 * 60 * 1000);
// Test JWT structure
console.log('\nTesting JWT token claims:');
const jwt = require('jsonwebtoken');
const testToken = jwt.sign({
id: 1,
username: 'testuser',
type: 'admin',
ip: '127.0.0.1',
loginTime: Date.now()
}, 'test-secret', {
expiresIn: '24h',
issuer: 'picpeak-auth'
});
const decoded = jwt.verify(testToken, 'test-secret', { complete: true });
test('Token has issuer claim', () => decoded.payload.iss === 'picpeak-auth');
test('Token has IP claim', () => decoded.payload.ip === '127.0.0.1');
test('Token has loginTime claim', () => typeof decoded.payload.loginTime === 'number');
test('Token expires in 24 hours', () => {
const exp = decoded.payload.exp;
const iat = decoded.payload.iat;
return (exp - iat) === 24 * 60 * 60;
});
// Test auth middleware logic
console.log('\nTesting auth middleware logic:');
test('Token type validation works', () => {
const adminToken = { type: 'admin' };
const galleryToken = { type: 'gallery' };
const invalidToken = { type: 'invalid' };
return adminToken.type === 'admin' &&
galleryToken.type === 'gallery' &&
invalidToken.type !== 'admin' &&
invalidToken.type !== 'gallery';
});
// Test IP validation logic
console.log('\nTesting IP validation:');
test('IP mismatch is detected', () => {
const tokenIp = '192.168.1.100';
const currentIp = '10.0.0.50';
return tokenIp !== currentIp;
});
// Test password change detection
console.log('\nTesting password change detection:');
test('Token issued before password change is invalid', () => {
const tokenIssuedAt = Math.floor(Date.now() / 1000) - 3600; // 1 hour ago
const passwordChangedAt = Math.floor(Date.now() / 1000) - 1800; // 30 minutes ago
return tokenIssuedAt < passwordChangedAt;
});
// Summary
console.log('\n=== Test Summary ===');
console.log(`Total tests: ${passed + failed}`);
console.log(`Passed: ${passed}`);
console.log(`Failed: ${failed}`);
if (failed === 0) {
console.log('\n✅ All authentication security tests passed!');
process.exit(0);
} else {
console.log('\n❌ Some tests failed. Review the implementation.');
process.exit(1);
}
-146
View File
@@ -1,146 +0,0 @@
#!/usr/bin/env node
/**
* Test Authentication V2 Security Fixes
*/
console.log('=== Testing Authentication V2 Fixes ===\n');
const jwt = require('jsonwebtoken');
let passed = 0;
let failed = 0;
function test(description, fn) {
try {
const result = fn();
if (result || result === undefined) {
console.log(`${description}`);
passed++;
} else {
console.log(`${description}`);
failed++;
}
} catch (error) {
console.log(`${description} - Error: ${error.message}`);
failed++;
}
}
async function runTests() {
// Test 1: Rate Limiting Security
console.log('1. Testing Rate Limiting Security:');
const { hasValidAdminToken } = require('../src/utils/rateLimitSecurity');
// Mock requests
const validAdminReq = {
path: '/api/admin/events',
headers: {
authorization: 'Bearer ' + jwt.sign({ id: 1, type: 'admin' }, process.env.JWT_SECRET || 'test')
},
ip: '127.0.0.1'
};
const invalidTokenReq = {
path: '/api/admin/events',
headers: {
authorization: 'Bearer invalid.token.here'
},
ip: '127.0.0.1'
};
const galleryTokenReq = {
path: '/api/admin/events',
headers: {
authorization: 'Bearer ' + jwt.sign({ id: 1, type: 'gallery' }, process.env.JWT_SECRET || 'test')
},
ip: '127.0.0.1'
};
test('Valid admin token skips rate limit', () => hasValidAdminToken(validAdminReq) === true);
test('Invalid token applies rate limit', () => hasValidAdminToken(invalidTokenReq) === false);
test('Gallery token cannot bypass admin rate limit', () => hasValidAdminToken(galleryTokenReq) === false);
// Test 2: Password Validation
console.log('\n2. Testing Password Validation:');
const { validatePassword, validatePasswordInContext } = require('../src/utils/passwordValidation');
const weakPassword = validatePassword('weak123');
test('Weak password is rejected', () => !weakPassword.valid);
test('Weak password has errors', () => weakPassword.errors.length > 0);
const strongPassword = validatePassword('Str0ng!P@ssw0rd123');
test('Strong password is accepted', () => strongPassword.valid);
test('Strong password has good score', () => strongPassword.score >= 3);
const shortPassword = validatePassword('Short!1');
test('Short password is rejected', () => !shortPassword.valid &&
shortPassword.errors.some(e => e.includes('12 characters')));
const noSpecialChar = validatePassword('NoSpecialChar123');
test('Password without special char is rejected', () => !noSpecialChar.valid &&
noSpecialChar.errors.some(e => e.includes('special character')));
// Context validation
const adminContext = validatePasswordInContext('Admin123!Pass', 'admin', { username: 'admin' });
test('Admin password with username is rejected', () => !adminContext.valid);
const galleryContext = validatePasswordInContext('Event123!Pass', 'gallery', { eventName: 'event' });
test('Gallery password with event name is rejected', () => !galleryContext.valid);
// Test 3: Token Revocation
console.log('\n3. Testing Token Revocation:');
const { isTokenRevoked } = require('../src/utils/tokenRevocation');
const testToken = {
jti: 'test-123',
id: 1,
type: 'admin',
iat: Math.floor(Date.now() / 1000)
};
// This would need database setup to fully test
test('Token revocation check runs', async () => {
try {
await isTokenRevoked(testToken);
return true;
} catch (e) {
// Expected if tables don't exist yet
return true;
}
});
// Test 4: Bcrypt Rounds
console.log('\n4. Testing Configurable Bcrypt:');
const { getBcryptRounds, PASSWORD_CONFIG } = require('../src/utils/passwordValidation');
test('Bcrypt rounds are configurable', () => {
const rounds = getBcryptRounds();
return rounds >= 10 && rounds <= 14;
});
test('Default bcrypt rounds is 12', () => {
return PASSWORD_CONFIG.bcryptRounds === 12 ||
PASSWORD_CONFIG.bcryptRounds === parseInt(process.env.BCRYPT_ROUNDS);
});
// Summary
console.log('\n=== Test Summary ===');
console.log(`Total tests: ${passed + failed}`);
console.log(`Passed: ${passed}`);
console.log(`Failed: ${failed}`);
if (failed === 0) {
console.log('\n✅ All authentication V2 tests passed!');
process.exit(0);
} else {
console.log('\n❌ Some tests failed. Review the implementation.');
process.exit(1);
}
}
// Run tests
runTests().catch(error => {
console.error('Test error:', error);
process.exit(1);
});
@@ -1,75 +0,0 @@
#!/usr/bin/env node
/**
* Test enhanced authentication features in Docker
*/
const { db } = require('../src/database/db');
const {
checkAccountLockout,
trackFailedAttempt,
trackSuccessfulLogin
} = require('../src/utils/authSecurity');
console.log('=== Testing Enhanced Auth Features ===\n');
async function testAuthFeatures() {
try {
// Test 1: Check if tables exist
console.log('1. Checking database tables...');
const loginAttempts = await db('login_attempts').count().first();
console.log('✓ login_attempts table exists');
// Test 2: Test failed attempt tracking
console.log('\n2. Testing failed attempt tracking...');
await trackFailedAttempt('test-user', '127.0.0.1', 'Test User Agent');
const attempts = await db('login_attempts')
.where('identifier', 'test-user')
.count()
.first();
console.log(`✓ Failed attempt tracked (${attempts.count} total)`);
// Test 3: Test lockout check
console.log('\n3. Testing lockout detection...');
// Add 4 more failures to trigger lockout
for (let i = 0; i < 4; i++) {
await trackFailedAttempt('test-user', '127.0.0.1', 'Test User Agent');
}
const lockoutStatus = await checkAccountLockout('test-user');
console.log(`✓ Lockout check works: ${lockoutStatus.isLocked ? 'LOCKED' : 'NOT LOCKED'}`);
if (lockoutStatus.isLocked) {
console.log(` Remaining lockout time: ${lockoutStatus.remainingTime} seconds`);
}
// Test 4: Test successful login tracking
console.log('\n4. Testing successful login tracking...');
await trackSuccessfulLogin('test-user', '127.0.0.1', 'Test User Agent');
console.log('✓ Successful login tracked');
// Test 5: Check if auth routes load
console.log('\n5. Testing enhanced auth routes...');
try {
const authRoutes = require('../src/routes/auth-enhanced');
console.log('✓ Enhanced auth routes load successfully');
} catch (e) {
console.log('✗ Error loading enhanced auth routes:', e.message);
}
// Clean up test data
await db('login_attempts').where('identifier', 'test-user').delete();
console.log('\n✓ Test data cleaned up');
console.log('\n✅ Enhanced auth features are working correctly!');
console.log('\nNext step: Update server.js to use enhanced auth routes');
} catch (error) {
console.error('\n❌ Test failed:', error);
} finally {
await db.destroy();
}
}
testAuthFeatures();
-98
View File
@@ -1,98 +0,0 @@
#!/usr/bin/env node
/**
* Test script to verify JWT_SECRET validation works correctly
* This ensures our security fix doesn't break production
*/
const { spawn } = require('child_process');
const path = require('path');
console.log('=== Testing JWT_SECRET Validation ===\n');
// Test 1: Server should fail to start without JWT_SECRET
console.log('Test 1: Starting server without JWT_SECRET...');
const test1 = spawn('node', [path.join(__dirname, '..', 'server.js')], {
env: { ...process.env, JWT_SECRET: '' },
stdio: 'pipe'
});
let test1Output = '';
test1.stderr.on('data', (data) => {
test1Output += data.toString();
});
test1.on('close', (code) => {
if (code === 1 && test1Output.includes('Missing required environment variable: JWT_SECRET')) {
console.log('✅ Test 1 PASSED: Server correctly refuses to start without JWT_SECRET\n');
runTest2();
} else {
console.log('❌ Test 1 FAILED: Server should have failed to start');
console.log('Exit code:', code);
console.log('Output:', test1Output);
process.exit(1);
}
});
// Test 2: Server should fail with insecure default value
function runTest2() {
console.log('Test 2: Starting server with insecure JWT_SECRET...');
const test2 = spawn('node', [path.join(__dirname, '..', 'server.js')], {
env: { ...process.env, JWT_SECRET: 'your-secret-key' },
stdio: 'pipe'
});
let test2Output = '';
test2.stderr.on('data', (data) => {
test2Output += data.toString();
});
test2.on('close', (code) => {
if (code === 1 && test2Output.includes('JWT_SECRET is set to the insecure default value')) {
console.log('✅ Test 2 PASSED: Server correctly refuses insecure JWT_SECRET\n');
runTest3();
} else {
console.log('❌ Test 2 FAILED: Server should have rejected insecure JWT_SECRET');
console.log('Exit code:', code);
console.log('Output:', test2Output);
process.exit(1);
}
});
}
// Test 3: Verify protectedImages functions work with valid JWT_SECRET
function runTest3() {
console.log('Test 3: Testing protectedImages functions...');
// Set a valid JWT_SECRET for this test
process.env.JWT_SECRET = 'test-secret-key-that-is-long-enough-for-security';
try {
// Load the module to test
const protectedImagesPath = path.join(__dirname, '..', 'src', 'routes', 'protectedImages.js');
delete require.cache[protectedImagesPath]; // Clear cache to ensure fresh load
// This will throw if JWT_SECRET is not available
require(protectedImagesPath);
console.log('✅ Test 3 PASSED: protectedImages module loads successfully with valid JWT_SECRET\n');
console.log('=== All Tests Passed! ===');
console.log('\nThe JWT_SECRET validation is working correctly.');
console.log('Production systems must have JWT_SECRET set to a secure value.');
process.exit(0);
} catch (error) {
console.log('❌ Test 3 FAILED: Error loading protectedImages module');
console.log('Error:', error.message);
process.exit(1);
}
}
// Give the first test some time to complete
setTimeout(() => {
if (test1.exitCode === null) {
console.log('❌ Test 1 TIMEOUT: Server did not exit as expected');
test1.kill();
process.exit(1);
}
}, 5000);
@@ -1,171 +0,0 @@
#!/usr/bin/env node
/**
* Test script to verify routes work correctly after SQL security fixes
* Run this before deploying to production
*/
const request = require('supertest');
const app = require('../src/app');
const { db } = require('../src/database/db');
const jwt = require('jsonwebtoken');
// Generate admin token for testing
const adminToken = jwt.sign(
{ id: 1, username: 'admin', role: 'admin' },
process.env.JWT_SECRET || 'test-secret'
);
console.log('=== Testing Routes After SQL Security Fixes ===\n');
let passed = 0;
let failed = 0;
async function testRoute(description, testFn) {
try {
await testFn();
console.log(`${description}`);
passed++;
} catch (error) {
console.log(`${description}`);
console.error(` Error: ${error.message}`);
failed++;
}
}
async function runTests() {
// Test Dashboard Stats (uses whereRaw fixes)
await testRoute('Dashboard stats endpoint', async () => {
const res = await request(app)
.get('/api/admin/dashboard/stats')
.set('Authorization', `Bearer ${adminToken}`)
.expect(200);
if (!res.body.hasOwnProperty('activeEvents')) {
throw new Error('Missing activeEvents in response');
}
});
// Test Analytics with days parameter (uses sanitizeDays)
await testRoute('Analytics with valid days parameter', async () => {
const res = await request(app)
.get('/api/admin/dashboard/analytics?days=7')
.set('Authorization', `Bearer ${adminToken}`)
.expect(200);
if (!res.body.chartData || res.body.chartData.length !== 7) {
throw new Error('Invalid chart data');
}
});
// Test Analytics with SQL injection attempt in days
await testRoute('Analytics rejects SQL injection in days parameter', async () => {
const res = await request(app)
.get('/api/admin/dashboard/analytics?days=7; DROP TABLE events; --')
.set('Authorization', `Bearer ${adminToken}`)
.expect(200);
// Should default to 7 days
if (res.body.chartData.length !== 7) {
throw new Error('Days parameter not properly sanitized');
}
});
// Test Event search with normal text (uses escapeLikePattern)
await testRoute('Event search with normal text', async () => {
const res = await request(app)
.get('/api/admin/events?search=test')
.set('Authorization', `Bearer ${adminToken}`)
.expect(200);
if (!res.body.hasOwnProperty('events')) {
throw new Error('Missing events in response');
}
});
// Test Event search with special characters
await testRoute('Event search with special characters', async () => {
const res = await request(app)
.get('/api/admin/events?search=50%_test')
.set('Authorization', `Bearer ${adminToken}`)
.expect(200);
// Should handle special chars safely
if (!res.body.hasOwnProperty('events')) {
throw new Error('Failed to handle special characters');
}
});
// Test Event search with SQL injection attempt
await testRoute('Event search prevents SQL injection', async () => {
const res = await request(app)
.get("/api/admin/events?search=' OR 1=1 --")
.set('Authorization', `Bearer ${adminToken}`)
.expect(200);
// Should return empty results, not all events
if (!res.body.hasOwnProperty('events')) {
throw new Error('SQL injection may not be prevented');
}
});
// Test Photo search (if event exists)
await testRoute('Photo search functionality', async () => {
// First check if we have any events
const event = await db('events').first();
if (event) {
const res = await request(app)
.get(`/api/admin/events/${event.id}/photos?search=test`)
.set('Authorization', `Bearer ${adminToken}`)
.expect(200);
if (!res.body.hasOwnProperty('photos')) {
throw new Error('Missing photos in response');
}
}
});
// Test Activity endpoint
await testRoute('Activity log endpoint', async () => {
const res = await request(app)
.get('/api/admin/dashboard/activity?limit=10')
.set('Authorization', `Bearer ${adminToken}`)
.expect(200);
if (!Array.isArray(res.body)) {
throw new Error('Activity should return array');
}
});
// Test Health endpoint
await testRoute('Health check endpoint', async () => {
const res = await request(app)
.get('/api/admin/dashboard/health')
.set('Authorization', `Bearer ${adminToken}`)
.expect(200);
if (!res.body.hasOwnProperty('overall')) {
throw new Error('Missing overall health status');
}
});
// Summary
console.log('\n=== Test Summary ===');
console.log(`Total tests: ${passed + failed}`);
console.log(`Passed: ${passed}`);
console.log(`Failed: ${failed}`);
if (failed === 0) {
console.log('\n✅ All route tests passed! Safe to deploy.');
process.exit(0);
} else {
console.log('\n❌ Some tests failed. Review the fixes before deploying.');
process.exit(1);
}
}
// Run tests
runTests().catch(error => {
console.error('Test runner error:', error);
process.exit(1);
});
-108
View File
@@ -1,108 +0,0 @@
#!/usr/bin/env node
/**
* Test script to verify SQL security fixes work correctly
* Tests both functionality and security of the fixes
*/
const {
sanitizeDays,
escapeLikePattern,
validateSortColumn,
validateSortOrder
} = require('../src/utils/sqlSecurity');
console.log('=== Testing SQL Security Utilities ===\n');
let passed = 0;
let failed = 0;
function test(description, fn) {
try {
const result = fn();
if (result) {
console.log(`${description}`);
passed++;
} else {
console.log(`${description}`);
failed++;
}
} catch (error) {
console.log(`${description} - Error: ${error.message}`);
failed++;
}
}
// Test sanitizeDays
console.log('Testing sanitizeDays function:');
test('Valid number returns same number', () => sanitizeDays(7) === 7);
test('String number is parsed correctly', () => sanitizeDays('30') === 30);
test('Invalid input returns default 7', () => sanitizeDays('abc') === 7);
test('Negative number returns 1', () => sanitizeDays(-5) === 1);
test('Zero returns 1', () => sanitizeDays(0) === 1);
test('Large number is capped at 365', () => sanitizeDays(500) === 365);
test('NaN returns default 7', () => sanitizeDays(NaN) === 7);
test('Null returns default 7', () => sanitizeDays(null) === 7);
test('Undefined returns default 7', () => sanitizeDays(undefined) === 7);
// Test escapeLikePattern
console.log('\nTesting escapeLikePattern function:');
test('Normal text unchanged', () => escapeLikePattern('hello world') === 'hello world');
test('Percent sign escaped', () => escapeLikePattern('50%') === '50\\%');
test('Underscore escaped', () => escapeLikePattern('user_name') === 'user\\_name');
test('Backslash escaped', () => escapeLikePattern('path\\to\\file') === 'path\\\\to\\\\file');
test('Multiple special chars escaped', () => escapeLikePattern('50%_test\\') === '50\\%\\_test\\\\');
test('Empty string returns empty', () => escapeLikePattern('') === '');
test('Null returns empty string', () => escapeLikePattern(null) === '');
test('Undefined returns empty string', () => escapeLikePattern(undefined) === '');
test('Single quotes escaped', () => escapeLikePattern("O'Brien") === "O''Brien");
// Test SQL injection attempts
console.log('\nTesting SQL injection prevention:');
test('SQL injection attempt with quotes', () => {
const malicious = "'; DROP TABLE users; --";
const escaped = escapeLikePattern(malicious);
return escaped === "''; DROP TABLE users; --" && escaped.includes("''");
});
test('SQL injection with LIKE wildcards', () => {
const malicious = "%' OR 1=1 --";
const escaped = escapeLikePattern(malicious);
return escaped === "\\%'' OR 1=1 --";
});
test('Days parameter injection attempt', () => {
const malicious = "7; DROP TABLE events; --";
return sanitizeDays(malicious) === 7;
});
// Test validateSortColumn
console.log('\nTesting validateSortColumn function:');
const allowedColumns = ['name', 'date', 'size'];
test('Valid column accepted', () => validateSortColumn('name', allowedColumns, 'date') === 'name');
test('Invalid column returns default', () => validateSortColumn('price', allowedColumns, 'date') === 'date');
test('Null returns default', () => validateSortColumn(null, allowedColumns, 'date') === 'date');
test('Empty string returns default', () => validateSortColumn('', allowedColumns, 'date') === 'date');
// Test validateSortOrder
console.log('\nTesting validateSortOrder function:');
test('Valid asc accepted', () => validateSortOrder('asc') === 'asc');
test('Valid ASC accepted', () => validateSortOrder('ASC') === 'asc');
test('Valid desc accepted', () => validateSortOrder('desc') === 'desc');
test('Invalid order returns desc', () => validateSortOrder('random') === 'desc');
test('Null returns desc', () => validateSortOrder(null) === 'desc');
test('Empty returns desc', () => validateSortOrder('') === 'desc');
// Summary
console.log('\n=== Test Summary ===');
console.log(`Total tests: ${passed + failed}`);
console.log(`Passed: ${passed}`);
console.log(`Failed: ${failed}`);
if (failed === 0) {
console.log('\n✅ All tests passed! SQL security utilities are working correctly.');
process.exit(0);
} else {
console.log('\n❌ Some tests failed. Please check the implementation.');
process.exit(1);
}
-59
View File
@@ -1,59 +0,0 @@
const axios = require('axios');
const FormData = require('form-data');
const fs = require('fs');
const path = require('path');
async function testUpload() {
try {
// First login
console.log('1. Logging in as admin...');
const loginResponse = await axios.post('http://localhost:3000/api/admin/auth/login', {
username: 'admin',
password: 'admin123'
});
const token = loginResponse.data.token;
console.log('✓ Login successful');
// Create a test image file
const testImagePath = path.join(__dirname, 'test-image.png');
const imageBuffer = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==', 'base64');
fs.writeFileSync(testImagePath, imageBuffer);
// Test upload
console.log('\n2. Testing upload with category_id=7...');
const form = new FormData();
form.append('photos', fs.createReadStream(testImagePath), 'test-image.png');
form.append('category_id', '7');
console.log('Form data headers:', form.getHeaders());
try {
const uploadResponse = await axios.post(
'http://localhost:3000/api/admin/events/12/upload',
form,
{
headers: {
...form.getHeaders(),
'Authorization': `Bearer ${token}`
}
}
);
console.log('✓ Upload successful:', uploadResponse.data);
} catch (error) {
console.error('✗ Upload failed:', error.response?.status, error.response?.data);
if (error.response?.data) {
console.error('Error details:', JSON.stringify(error.response.data, null, 2));
}
}
// Clean up
fs.unlinkSync(testImagePath);
} catch (error) {
console.error('Error:', error.message);
}
}
testUpload();
-106
View File
@@ -1,106 +0,0 @@
#!/usr/bin/env node
/**
* Verify enhanced authentication is active and working
*/
const { db } = require('../src/database/db');
console.log('=== Verifying Enhanced Authentication Activation ===\n');
async function verifyAuth() {
const results = {
databaseTables: false,
serverConfig: false,
lockoutActive: false,
cleanupActive: false
};
try {
// 1. Check database tables
console.log('1. Checking database tables...');
const hasLoginAttempts = await db.schema.hasTable('login_attempts');
const hasSecurityColumns = await db.schema.hasColumn('admin_users', 'password_changed_at');
if (hasLoginAttempts && hasSecurityColumns) {
results.databaseTables = true;
console.log('✓ Auth tables and columns exist');
// Count attempts
const attempts = await db('login_attempts').count().first();
console.log(` Total login attempts tracked: ${attempts['count(*)'] || 0}`);
} else {
console.log('✗ Auth tables missing');
}
// 2. Check server configuration
console.log('\n2. Checking server configuration...');
const fs = require('fs');
const serverContent = fs.readFileSync('./server.js', 'utf8');
if (serverContent.includes("require('./src/routes/auth-enhanced')")) {
results.serverConfig = true;
console.log('✓ Server using enhanced auth routes');
} else {
console.log('✗ Server using original auth routes');
}
if (serverContent.includes('initializeCleanupJob')) {
results.cleanupActive = true;
console.log('✓ Cleanup job initialized');
} else {
console.log('✗ Cleanup job not initialized');
}
// 3. Test lockout functionality
console.log('\n3. Testing lockout functionality...');
const { checkAccountLockout } = require('../src/utils/authSecurity');
// Check a test account
const lockoutTest = await checkAccountLockout('lockout-test-user');
console.log(`✓ Lockout check functional: ${lockoutTest.isLocked ? 'locked' : 'not locked'}`);
results.lockoutActive = true;
// 4. Check recent activity
console.log('\n4. Recent authentication activity...');
const recentAttempts = await db('login_attempts')
.orderBy('attempt_time', 'desc')
.limit(5);
if (recentAttempts.length > 0) {
console.log('Recent login attempts:');
recentAttempts.forEach(attempt => {
const time = new Date(attempt.attempt_time).toLocaleString();
console.log(` ${time} - ${attempt.identifier} - ${attempt.success ? 'SUCCESS' : 'FAILED'}`);
});
} else {
console.log('No login attempts recorded yet');
}
// Summary
console.log('\n=== Summary ===');
const allGood = Object.values(results).every(v => v === true);
if (allGood) {
console.log('✅ Enhanced authentication is FULLY ACTIVE!');
console.log('\nFeatures enabled:');
console.log('- Account lockout protection (5 attempts)');
console.log('- Login attempt tracking');
console.log('- Enhanced token validation');
console.log('- Session management');
console.log('- Automatic cleanup of old records');
} else {
console.log('⚠️ Some features not active:');
Object.entries(results).forEach(([key, value]) => {
console.log(` ${key}: ${value ? '✓' : '✗'}`);
});
}
} catch (error) {
console.error('Verification error:', error);
} finally {
await db.destroy();
}
}
verifyAuth();
-80
View File
@@ -1,80 +0,0 @@
#!/usr/bin/env node
/**
* Verification script to check SQL queries are built correctly
* This simulates the query building without running the full app
*/
const {
sanitizeDays,
escapeLikePattern,
validateSortColumn,
validateSortOrder
} = require('../src/utils/sqlSecurity');
console.log('=== Verifying SQL Security Fixes ===\n');
// Test 1: Verify date range queries
console.log('1. Testing date range query building:');
console.log(' Input days: "7; DROP TABLE events; --"');
const safeDays = sanitizeDays("7; DROP TABLE events; --");
console.log(' Sanitized days:', safeDays);
console.log(' ✅ SQL injection attempt neutralized\n');
// Test 2: Verify LIKE pattern escaping
console.log('2. Testing LIKE pattern escaping:');
const testPatterns = [
"normal search",
"50%_wildcard",
"'; DROP TABLE users; --",
"test\\path",
"O'Brien"
];
testPatterns.forEach(pattern => {
const escaped = escapeLikePattern(pattern);
console.log(` "${pattern}" → "${escaped}"`);
});
console.log(' ✅ All patterns safely escaped\n');
// Test 3: Simulate date query building
console.log('3. Simulating safe date query:');
const days = 7;
const startDate = new Date();
startDate.setDate(startDate.getDate() - days);
console.log(` WHERE timestamp >= '${startDate.toISOString()}'`);
console.log(' ✅ Using parameterized date instead of whereRaw\n');
// Test 4: Verify sort validation
console.log('4. Testing sort column/order validation:');
const allowedColumns = ['created_at', 'event_name', 'expires_at'];
console.log(' Allowed columns:', allowedColumns);
const testSorts = [
{ column: 'created_at', order: 'desc' },
{ column: 'invalid_column', order: 'asc' },
{ column: '; DROP TABLE --', order: 'random' }
];
testSorts.forEach(({ column, order }) => {
const safeColumn = validateSortColumn(column, allowedColumns, 'created_at');
const safeOrder = validateSortOrder(order);
console.log(` "${column}" ${order} → "${safeColumn}" ${safeOrder}`);
});
console.log(' ✅ Invalid columns/orders rejected\n');
// Test 5: Show example of safe query patterns
console.log('5. Safe Query Patterns Used:');
console.log(' ❌ OLD: .whereRaw(`timestamp >= datetime("now", "-${days} days")`)')
console.log(' ✅ NEW: .where("timestamp", ">=", startDate.toISOString())\n');
console.log(' ❌ OLD: .where("event_name", "like", `%${search}%`)')
console.log(' ✅ NEW: .where("event_name", "like", `%${escapeLikePattern(search)}%`)\n');
console.log('=== Verification Complete ===');
console.log('All SQL injection vulnerabilities have been addressed.');
console.log('\nNext steps:');
console.log('1. Test in development environment');
console.log('2. Monitor logs during testing');
console.log('3. Deploy with rollback plan ready');
console.log('4. Monitor production logs after deployment');
-32
View File
@@ -1,32 +0,0 @@
// This is a partial server.js showing the enhanced rate limiting
// Only the relevant parts are shown - merge with existing server.js
const { createSecureSkipFunction, logRateLimitHit } = require('./src/utils/rateLimitSecurity');
// Enhanced rate limiting with secure skip function
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: process.env.NODE_ENV === 'development' ? 1000 : 100,
skip: createSecureSkipFunction(), // Use secure skip function
handler: (req, res) => {
logRateLimitHit(req, res);
res.status(429).json({
error: 'Too many requests from this IP, please try again later.'
});
},
standardHeaders: true, // Return rate limit info in headers
legacyHeaders: false, // Disable X-RateLimit headers
});
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5, // limit auth attempts
skipSuccessfulRequests: true, // Don't count successful logins
handler: (req, res) => {
logRateLimitHit(req, res);
res.status(429).json({
error: 'Too many login attempts, please try again later.',
retryAfter: res.getHeader('Retry-After')
});
}
});
+22 -4
View File
@@ -13,7 +13,7 @@ const path = require('path');
const { initializeDatabase } = require('./src/database/db');
const { startFileWatcher } = require('./src/services/fileWatcher');
const { startExpirationChecker } = require('./src/services/expirationChecker');
const { startEmailQueueProcessor } = require('./src/services/emailProcessor');
const { initializeTransporter, startEmailQueueProcessor } = require('./src/services/emailProcessor');
const { maintenanceMiddleware } = require('./src/middleware/maintenance');
const { sessionTimeoutMiddleware } = require('./src/middleware/sessionTimeout');
const logger = require('./src/utils/logger');
@@ -152,8 +152,25 @@ app.use('/thumbnails', require('./src/middleware/photoAuth'), setCorsHeaders, se
app.use('/uploads', setCorsHeaders, secureStatic(path.join(__dirname, 'storage/uploads')));
// Health check endpoint
app.get('/api/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
app.get('/api/health', async (req, res) => {
try {
// Check database connectivity
await db.raw('SELECT 1');
res.json({
status: 'ok',
database: 'connected',
timestamp: new Date().toISOString()
});
} catch (error) {
logger.error('Health check failed:', error);
res.status(503).json({
status: 'error',
database: 'disconnected',
error: error.message,
timestamp: new Date().toISOString()
});
}
});
// Routes
@@ -189,7 +206,8 @@ async function startServer() {
// Start expiration checker
startExpirationChecker();
// Start email queue processor
// Initialize email transporter and start queue processor
await initializeTransporter();
startEmailQueueProcessor();
app.listen(PORT, () => {
-167
View File
@@ -1,167 +0,0 @@
require('dotenv').config();
// Validate critical environment variables before proceeding
const { validateEnvironment } = require('./src/config/validateEnv');
validateEnvironment();
const express = require('express');
const helmet = require('helmet');
const cors = require('cors');
const rateLimit = require('express-rate-limit');
const jwt = require('jsonwebtoken');
const path = require('path');
const { initializeDatabase } = require('./src/database/db');
const { startFileWatcher } = require('./src/services/fileWatcher');
const { startExpirationChecker } = require('./src/services/expirationChecker');
const { startEmailQueueProcessor } = require('./src/services/emailProcessor');
const { maintenanceMiddleware } = require('./src/middleware/maintenance');
const { sessionTimeoutMiddleware } = require('./src/middleware/sessionTimeout');
const logger = require('./src/utils/logger');
// Import routes
const authRoutes = require('./src/routes/auth');
const eventRoutes = require('./src/routes/events');
const galleryRoutes = require('./src/routes/gallery');
const adminRoutes = require('./src/routes/admin');
const adminAuthRoutes = require('./src/routes/adminAuth');
const app = express();
const PORT = process.env.PORT || 3000;
// Security middleware
app.use(helmet());
// CORS configuration
const corsOptions = {
origin: function (origin, callback) {
const allowedOrigins = [
process.env.FRONTEND_URL || 'http://localhost:3005',
process.env.ADMIN_URL || 'http://localhost:3005',
'http://localhost:5173', // Vite dev server
'http://localhost:3002', // Backend server
'http://localhost:3001', // For API testing
'http://localhost:3000' // Direct backend access
];
// Allow requests with no origin (like mobile apps or curl)
if (!origin || allowedOrigins.indexOf(origin) !== -1) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true
};
app.use(cors(corsOptions));
// Rate limiting with admin bypass
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: process.env.NODE_ENV === 'development' ? 1000 : 100, // More lenient in development
skip: (req) => {
// Skip rate limiting for authenticated admin users
if (req.path.startsWith('/api/admin/') && req.headers.authorization) {
const token = req.headers.authorization.replace('Bearer ', '');
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
return decoded.type === 'admin';
} catch (err) {
return false;
}
}
// Also skip rate limiting for public settings endpoint in development
if (process.env.NODE_ENV === 'development' && req.path === '/api/public/settings') {
return true;
}
return false;
}
});
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5 // limit auth attempts
});
// Apply rate limiting - admin routes check will skip for valid admin tokens
app.use('/api/', limiter);
app.use('/api/auth', authLimiter);
// Body parsing middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Maintenance mode middleware - add after body parsing but before routes
app.use(maintenanceMiddleware);
// Session timeout middleware for admin routes
app.use('/api/admin', sessionTimeoutMiddleware);
// Middleware to set CORS headers for static files
const setCorsHeaders = (req, res, next) => {
res.header('Access-Control-Allow-Origin', req.headers.origin || '*');
res.header('Access-Control-Allow-Credentials', 'true');
res.header('Cross-Origin-Resource-Policy', 'cross-origin');
next();
};
// Static file serving for photos (protected)
app.use('/photos', require('./src/middleware/photoAuth'), setCorsHeaders, express.static(path.join(__dirname, 'storage/events/active')));
// Static file serving for thumbnails (protected)
app.use('/thumbnails', require('./src/middleware/photoAuth'), setCorsHeaders, express.static(path.join(__dirname, 'storage/thumbnails')));
// Static file serving for uploads (public - logos, favicons)
app.use('/uploads', setCorsHeaders, express.static(path.join(__dirname, 'storage/uploads')));
// Health check endpoint
app.get('/api/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
// Routes
app.use('/api/auth', authRoutes);
app.use('/api/events', eventRoutes);
app.use('/api/gallery', galleryRoutes);
app.use('/api/admin', adminRoutes);
app.use('/api/admin/auth', adminAuthRoutes);
app.use('/api/admin/system', require('./src/routes/adminSystem'));
app.use('/api/public/settings', require('./src/routes/publicSettings'));
app.use('/api/public', require('./src/routes/publicCMS'));
app.use('/api/images', require('./src/routes/protectedImages'));
// Error handling middleware
app.use((err, req, res, next) => {
logger.error(err.stack);
res.status(500).json({ error: 'Something went wrong!' });
});
// Initialize services
async function startServer() {
try {
// Initialize database
await initializeDatabase();
// Start file watcher
startFileWatcher();
// Start expiration checker
startExpirationChecker();
// Start email queue processor
startEmailQueueProcessor();
app.listen(PORT, () => {
logger.info(`Server running on port ${PORT}`);
logger.info(`Admin interface: ${process.env.ADMIN_URL || 'http://localhost:3000'}`);
logger.info(`Frontend: ${process.env.FRONTEND_URL || 'http://localhost:3001'}`);
});
} catch (error) {
logger.error('Failed to start server:', error);
process.exit(1);
}
}
startServer();
module.exports = app; // For testing
+7 -4
View File
@@ -395,12 +395,15 @@ function stopEmailQueueProcessor() {
}
}
// Initialize on module load
initializeTransporter().then(() => {
startEmailQueueProcessor();
});
// Initialize on module load - DISABLED for production startup
// This will be called from server.js after database is ready
// initializeTransporter().then(() => {
// startEmailQueueProcessor();
// });
module.exports = {
initializeTransporter,
startEmailQueueProcessor,
sendTemplateEmail,
processEmailQueue,
queueEmail,
-48
View File
@@ -1,48 +0,0 @@
const { db } = require('./src/database/db');
async function updateTemplate() {
try {
const englishBody = `<h2>Gallery Successfully Created</h2>
<p>Dear {{host_name}},</p>
<p>Your photo gallery "{{event_name}}" has been successfully created\!</p>
{{welcome_message_section}}
<p><strong>Gallery Details:</strong></p>
<ul>
<li>Event Date: {{event_date}}</li>
<li>Gallery Link: <a href="{{gallery_link}}" style="color: #5C8762; text-decoration: none;">{{gallery_link}}</a></li>
<li>Password: {{gallery_password}}</li>
<li>Valid Until: {{expiry_date}}</li>
</ul>
<p>Share this link and password with your guests so they can view and download photos.</p>
<p><a href="{{gallery_link}}" style="display: inline-block; padding: 10px 20px; background-color: #5C8762; color: white; text-decoration: none; border-radius: 5px;">View Gallery</a></p>`;
const germanBody = `<h2>Galerie erfolgreich erstellt</h2>
<p>Liebe(r) {{host_name}},</p>
<p>Ihre Fotogalerie "{{event_name}}" wurde erfolgreich erstellt\!</p>
{{welcome_message_section}}
<p><strong>Galerie-Details:</strong></p>
<ul>
<li>Veranstaltungsdatum: {{event_date}}</li>
<li>Galerie-Link: <a href="{{gallery_link}}" style="color: #5C8762; text-decoration: none;">{{gallery_link}}</a></li>
<li>Passwort: {{gallery_password}}</li>
<li>Gültig bis: {{expiry_date}}</li>
</ul>
<p>Teilen Sie diesen Link und das Passwort mit Ihren Gästen, damit sie Fotos ansehen und herunterladen können.</p>
<p><a href="{{gallery_link}}" style="display: inline-block; padding: 10px 20px; background-color: #5C8762; color: white; text-decoration: none; border-radius: 5px;">Galerie anzeigen</a></p>`;
await db('email_templates')
.where('template_key', 'gallery_created')
.update({
body_html_en: englishBody,
body_html_de: germanBody
});
console.log('Email template updated successfully');
} catch (error) {
console.error('Error updating template:', error);
} finally {
process.exit(0);
}
}
updateTemplate();
+25
View File
@@ -0,0 +1,25 @@
#!/bin/sh
# wait-for-db.sh - Wait for PostgreSQL to be ready before starting the application
set -e
host="$DB_HOST"
port="${DB_PORT:-5432}"
user="${DB_USER:-picpeak}"
echo "Waiting for PostgreSQL at $host:$port..."
# Wait for PostgreSQL to be ready
until PGPASSWORD=$DB_PASSWORD psql -h "$host" -p "$port" -U "$user" -d "${DB_NAME:-picpeak}" -c '\q' 2>/dev/null; do
>&2 echo "PostgreSQL is unavailable - sleeping"
sleep 2
done
>&2 echo "PostgreSQL is up - executing command"
# Run migrations
echo "Running database migrations..."
npm run migrate
# Execute the main command
exec "$@"
+6
View File
@@ -8,6 +8,8 @@ services:
context: ./backend
dockerfile: Dockerfile
restart: unless-stopped
depends_on:
- db
environment:
- NODE_ENV=production
- PORT=3000
@@ -31,6 +33,10 @@ services:
# Analytics
- UMAMI_URL=${UMAMI_URL}
- UMAMI_WEBSITE_ID=${UMAMI_WEBSITE_ID}
# Storage paths
- STORAGE_PATH=/app/storage
- EVENTS_PATH=/app/storage/events
- ARCHIVE_PATH=/app/storage/events/archived
volumes:
- ./storage:/app/storage
- ./data:/app/data
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "picpeak-frontend",
"version": "1.0.4",
"version": "1.0.7",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "picpeak-frontend",
"version": "1.0.4",
"version": "1.0.7",
"dependencies": {
"@tanstack/react-query": "^5.0.0",
"@tiptap/extension-link": "^2.25.0",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "picpeak-frontend",
"private": true,
"version": "1.0.4",
"version": "1.0.7",
"type": "module",
"scripts": {
"dev": "vite",