diff --git a/backend/ACTIVATE_AUTH_V2.md b/backend/ACTIVATE_AUTH_V2.md deleted file mode 100644 index a45a686..0000000 --- a/backend/ACTIVATE_AUTH_V2.md +++ /dev/null @@ -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 \ No newline at end of file diff --git a/backend/AUTH_FLAWS_ANALYSIS.md b/backend/AUTH_FLAWS_ANALYSIS.md deleted file mode 100644 index d444709..0000000 --- a/backend/AUTH_FLAWS_ANALYSIS.md +++ /dev/null @@ -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) \ No newline at end of file diff --git a/backend/AUTH_SECURITY_INTEGRATION.md b/backend/AUTH_SECURITY_INTEGRATION.md deleted file mode 100644 index 91345f1..0000000 --- a/backend/AUTH_SECURITY_INTEGRATION.md +++ /dev/null @@ -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 \ No newline at end of file diff --git a/backend/AUTH_SECURITY_MIGRATION.md b/backend/AUTH_SECURITY_MIGRATION.md deleted file mode 100644 index c814ce1..0000000 --- a/backend/AUTH_SECURITY_MIGRATION.md +++ /dev/null @@ -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)" -``` \ No newline at end of file diff --git a/backend/AUTH_SECURITY_ROLLBACK.md b/backend/AUTH_SECURITY_ROLLBACK.md deleted file mode 100644 index 3fbc713..0000000 --- a/backend/AUTH_SECURITY_ROLLBACK.md +++ /dev/null @@ -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 \ No newline at end of file diff --git a/backend/AUTH_SECURITY_SUMMARY.md b/backend/AUTH_SECURITY_SUMMARY.md deleted file mode 100644 index b0e3647..0000000 --- a/backend/AUTH_SECURITY_SUMMARY.md +++ /dev/null @@ -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" -``` \ No newline at end of file diff --git a/backend/AUTH_V2_DEPLOYMENT_PLAN.md b/backend/AUTH_V2_DEPLOYMENT_PLAN.md deleted file mode 100644 index c18dca5..0000000 --- a/backend/AUTH_V2_DEPLOYMENT_PLAN.md +++ /dev/null @@ -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 \ No newline at end of file diff --git a/backend/AUTH_V2_FIXES_SUMMARY.md b/backend/AUTH_V2_FIXES_SUMMARY.md deleted file mode 100644 index 6e0efeb..0000000 --- a/backend/AUTH_V2_FIXES_SUMMARY.md +++ /dev/null @@ -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! \ No newline at end of file diff --git a/backend/SAFE_AUTH_ACTIVATION_PLAN.md b/backend/SAFE_AUTH_ACTIVATION_PLAN.md deleted file mode 100644 index 72439a4..0000000 --- a/backend/SAFE_AUTH_ACTIVATION_PLAN.md +++ /dev/null @@ -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. \ No newline at end of file diff --git a/backend/SECURITY_FIXES_COMPLETE.md b/backend/SECURITY_FIXES_COMPLETE.md deleted file mode 100644 index 17df4bb..0000000 --- a/backend/SECURITY_FIXES_COMPLETE.md +++ /dev/null @@ -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. \ No newline at end of file diff --git a/backend/SQL_INJECTION_FIX_MIGRATION.md b/backend/SQL_INJECTION_FIX_MIGRATION.md deleted file mode 100644 index fa54951..0000000 --- a/backend/SQL_INJECTION_FIX_MIGRATION.md +++ /dev/null @@ -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 \ No newline at end of file diff --git a/backend/SQL_INJECTION_FIX_ROLLBACK.md b/backend/SQL_INJECTION_FIX_ROLLBACK.md deleted file mode 100644 index c2b6248..0000000 --- a/backend/SQL_INJECTION_FIX_ROLLBACK.md +++ /dev/null @@ -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 \ No newline at end of file diff --git a/backend/SQL_INJECTION_FIX_SUMMARY.md b/backend/SQL_INJECTION_FIX_SUMMARY.md deleted file mode 100644 index c92e144..0000000 --- a/backend/SQL_INJECTION_FIX_SUMMARY.md +++ /dev/null @@ -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 \ No newline at end of file diff --git a/backend/data/photo-sharing.db b/backend/data/photo-sharing.db deleted file mode 100644 index e69de29..0000000 diff --git a/backend/data/photo_sharing.db b/backend/data/photo_sharing.db index 35d6872..b5ef213 100644 Binary files a/backend/data/photo_sharing.db and b/backend/data/photo_sharing.db differ diff --git a/backend/data/photos.db b/backend/data/photos.db deleted file mode 100644 index e69de29..0000000 diff --git a/backend/data/wedding-photos.db b/backend/data/wedding-photos.db deleted file mode 100644 index e69de29..0000000 diff --git a/backend/database.db b/backend/database.db deleted file mode 100644 index e69de29..0000000 diff --git a/backend/database.db.backup.1752359355 b/backend/database.db.backup.1752359355 deleted file mode 100644 index e69de29..0000000 diff --git a/backend/migrations/add_must_change_password.js.old b/backend/migrations/add_must_change_password.js.old deleted file mode 100644 index 99e519e..0000000 --- a/backend/migrations/add_must_change_password.js.old +++ /dev/null @@ -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(); \ No newline at end of file diff --git a/backend/scripts/activate-enhanced-auth.js b/backend/scripts/activate-enhanced-auth.js deleted file mode 100644 index f149ccc..0000000 --- a/backend/scripts/activate-enhanced-auth.js +++ /dev/null @@ -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(); \ No newline at end of file diff --git a/backend/scripts/add-auth-tables.js b/backend/scripts/add-auth-tables.js deleted file mode 100644 index a957403..0000000 --- a/backend/scripts/add-auth-tables.js +++ /dev/null @@ -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(); \ No newline at end of file diff --git a/backend/scripts/add-token-revocation-tables.js b/backend/scripts/add-token-revocation-tables.js deleted file mode 100644 index 69bbe33..0000000 --- a/backend/scripts/add-token-revocation-tables.js +++ /dev/null @@ -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(); \ No newline at end of file diff --git a/backend/scripts/check-docker-status.js b/backend/scripts/check-docker-status.js deleted file mode 100644 index afcdbaf..0000000 --- a/backend/scripts/check-docker-status.js +++ /dev/null @@ -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'); - } -}); \ No newline at end of file diff --git a/backend/scripts/debug-event-photos.js b/backend/scripts/debug-event-photos.js deleted file mode 100644 index 243dc78..0000000 --- a/backend/scripts/debug-event-photos.js +++ /dev/null @@ -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(); \ No newline at end of file diff --git a/backend/scripts/debug-photos-enhanced.js b/backend/scripts/debug-photos-enhanced.js deleted file mode 100755 index 89cfa73..0000000 --- a/backend/scripts/debug-photos-enhanced.js +++ /dev/null @@ -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.'); - } - }); -}); \ No newline at end of file diff --git a/backend/scripts/debug-photos.js b/backend/scripts/debug-photos.js deleted file mode 100755 index dbd5ff0..0000000 --- a/backend/scripts/debug-photos.js +++ /dev/null @@ -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.'); - } - }); -}); \ No newline at end of file diff --git a/backend/scripts/deploy-auth-security.sh b/backend/scripts/deploy-auth-security.sh deleted file mode 100755 index d47ef37..0000000 --- a/backend/scripts/deploy-auth-security.sh +++ /dev/null @@ -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" \ No newline at end of file diff --git a/backend/scripts/fix-default-themes.js b/backend/scripts/fix-default-themes.js deleted file mode 100644 index 8afd8eb..0000000 --- a/backend/scripts/fix-default-themes.js +++ /dev/null @@ -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(); \ No newline at end of file diff --git a/backend/scripts/monitor-auth-health.js b/backend/scripts/monitor-auth-health.js deleted file mode 100755 index c2e5168..0000000 --- a/backend/scripts/monitor-auth-health.js +++ /dev/null @@ -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); -}); \ No newline at end of file diff --git a/backend/scripts/safe-auth-deployment.js b/backend/scripts/safe-auth-deployment.js deleted file mode 100755 index c7e153a..0000000 --- a/backend/scripts/safe-auth-deployment.js +++ /dev/null @@ -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(); \ No newline at end of file diff --git a/backend/scripts/seed-categories.js b/backend/scripts/seed-categories.js deleted file mode 100644 index 92f1b91..0000000 --- a/backend/scripts/seed-categories.js +++ /dev/null @@ -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(); \ No newline at end of file diff --git a/backend/scripts/test-admin-photo.js b/backend/scripts/test-admin-photo.js deleted file mode 100644 index 7196912..0000000 --- a/backend/scripts/test-admin-photo.js +++ /dev/null @@ -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(); \ No newline at end of file diff --git a/backend/scripts/test-auth-deployment.sh b/backend/scripts/test-auth-deployment.sh deleted file mode 100755 index dc8d2a1..0000000 --- a/backend/scripts/test-auth-deployment.sh +++ /dev/null @@ -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!" \ No newline at end of file diff --git a/backend/scripts/test-auth-security.js b/backend/scripts/test-auth-security.js deleted file mode 100644 index 4a2258b..0000000 --- a/backend/scripts/test-auth-security.js +++ /dev/null @@ -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); -} \ No newline at end of file diff --git a/backend/scripts/test-auth-v2-fixes.js b/backend/scripts/test-auth-v2-fixes.js deleted file mode 100644 index 3ba59d8..0000000 --- a/backend/scripts/test-auth-v2-fixes.js +++ /dev/null @@ -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); -}); \ No newline at end of file diff --git a/backend/scripts/test-enhanced-auth-docker.js b/backend/scripts/test-enhanced-auth-docker.js deleted file mode 100644 index 074d575..0000000 --- a/backend/scripts/test-enhanced-auth-docker.js +++ /dev/null @@ -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(); \ No newline at end of file diff --git a/backend/scripts/test-jwt-validation.js b/backend/scripts/test-jwt-validation.js deleted file mode 100755 index 2385bf4..0000000 --- a/backend/scripts/test-jwt-validation.js +++ /dev/null @@ -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); \ No newline at end of file diff --git a/backend/scripts/test-routes-after-security-fix.js b/backend/scripts/test-routes-after-security-fix.js deleted file mode 100644 index 8beb795..0000000 --- a/backend/scripts/test-routes-after-security-fix.js +++ /dev/null @@ -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); -}); \ No newline at end of file diff --git a/backend/scripts/test-sql-security.js b/backend/scripts/test-sql-security.js deleted file mode 100644 index 53a5641..0000000 --- a/backend/scripts/test-sql-security.js +++ /dev/null @@ -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); -} \ No newline at end of file diff --git a/backend/scripts/test-upload.js b/backend/scripts/test-upload.js deleted file mode 100644 index 2e5011a..0000000 --- a/backend/scripts/test-upload.js +++ /dev/null @@ -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(); \ No newline at end of file diff --git a/backend/scripts/verify-auth-activation.js b/backend/scripts/verify-auth-activation.js deleted file mode 100644 index c7e9eea..0000000 --- a/backend/scripts/verify-auth-activation.js +++ /dev/null @@ -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(); \ No newline at end of file diff --git a/backend/scripts/verify-sql-fixes.js b/backend/scripts/verify-sql-fixes.js deleted file mode 100644 index 415dd25..0000000 --- a/backend/scripts/verify-sql-fixes.js +++ /dev/null @@ -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'); \ No newline at end of file diff --git a/backend/server-enhanced.js b/backend/server-enhanced.js deleted file mode 100644 index 91a0c0e..0000000 --- a/backend/server-enhanced.js +++ /dev/null @@ -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') - }); - } -}); \ No newline at end of file diff --git a/backend/server.js.backup.1752359680463 b/backend/server.js.backup.1752359680463 deleted file mode 100644 index 4850b28..0000000 --- a/backend/server.js.backup.1752359680463 +++ /dev/null @@ -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 diff --git a/backend/update_email_template.js b/backend/update_email_template.js deleted file mode 100644 index 6441681..0000000 --- a/backend/update_email_template.js +++ /dev/null @@ -1,48 +0,0 @@ -const { db } = require('./src/database/db'); - -async function updateTemplate() { - try { - const englishBody = `
Dear {{host_name}},
-Your photo gallery "{{event_name}}" has been successfully created\!
-{{welcome_message_section}} -Gallery Details:
-Share this link and password with your guests so they can view and download photos.
-`; - - const germanBody = `Liebe(r) {{host_name}},
-Ihre Fotogalerie "{{event_name}}" wurde erfolgreich erstellt\!
-{{welcome_message_section}} -Galerie-Details:
-Teilen Sie diesen Link und das Passwort mit Ihren Gästen, damit sie Fotos ansehen und herunterladen können.
-`; - - 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();