diff --git a/.gitignore b/.gitignore index 0fb9656..f3fa5ce 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,12 @@ yarn-error.log* .env.test.local .env.production.local +# Security - Never commit credentials +ADMIN_CREDENTIALS.txt +ADMIN_PASSWORD_RESET.txt +*_CREDENTIALS.txt +*_PASSWORD_RESET.txt + # Storage and data storage/events/active/* storage/events/archived/* diff --git a/README-LOCAL.md b/README-LOCAL.md index ff842cc..ae9c9b7 100644 --- a/README-LOCAL.md +++ b/README-LOCAL.md @@ -31,10 +31,10 @@ That's it! šŸŽ‰ ## Default Credentials -- **Admin Login**: admin / admin123 +- **Admin Login**: Check `ADMIN_CREDENTIALS.txt` after first setup - **Test Gallery**: - Create via Admin Panel - - Password: test123 + - Set your own secure password ## Common Tasks diff --git a/README.md b/README.md index e9042aa..1cba90e 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ A secure, self-hosted photo sharing platform designed for weddings and events. F 4. Setup SSL: `./scripts/setup-ssl.sh` 5. Start: `docker-compose -f docker-compose.prod.yml up -d` -Default credentials: admin / admin123 (change immediately!) +Default credentials: Check ADMIN_CREDENTIALS.txt after first setup ## Documentation diff --git a/backend/data/photo_sharing.db b/backend/data/photo_sharing.db index c551221..35d6872 100644 Binary files a/backend/data/photo_sharing.db and b/backend/data/photo_sharing.db differ diff --git a/backend/migrations/add_must_change_password.js b/backend/migrations/add_must_change_password.js new file mode 100644 index 0000000..99e519e --- /dev/null +++ b/backend/migrations/add_must_change_password.js @@ -0,0 +1,25 @@ +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/migrations/init.js b/backend/migrations/init.js index 7cdb0db..162bc28 100644 --- a/backend/migrations/init.js +++ b/backend/migrations/init.js @@ -1,5 +1,8 @@ const bcrypt = require('bcrypt'); const { db, initializeDatabase } = require('../src/database/db'); +const { generateReadablePassword } = require('../src/utils/passwordGenerator'); +const fs = require('fs').promises; +const path = require('path'); async function runMigrations() { console.log('Running database migrations...'); @@ -11,19 +14,54 @@ async function runMigrations() { // Create default admin user if none exists const adminExists = await db('admin_users').first(); if (!adminExists) { - const defaultPassword = 'admin123'; // Change this! - const passwordHash = await bcrypt.hash(defaultPassword, 10); + // Generate a secure random password + const generatedPassword = generateReadablePassword(); + const passwordHash = await bcrypt.hash(generatedPassword, 12); // Increased rounds for better security await db('admin_users').insert({ username: 'admin', email: 'admin@example.com', - password_hash: passwordHash + password_hash: passwordHash, + must_change_password: true, // Flag for forcing password change + created_at: new Date() }); - console.log('Default admin user created:'); + // Save the generated password to a file for the user to retrieve + const setupInfoPath = path.join(__dirname, '..', '..', 'ADMIN_CREDENTIALS.txt'); + const setupInfo = ` +======================================== +PicPeak Admin Credentials +======================================== + +Your admin account has been created with these credentials: + +Username: admin +Password: ${generatedPassword} + +IMPORTANT SECURITY NOTES: +1. You MUST change this password on first login +2. This file will be created only once +3. Store these credentials securely +4. Delete this file after noting the password + +Login URL: ${process.env.ADMIN_URL || 'http://localhost:3001'}/admin + +Generated on: ${new Date().toISOString()} +======================================== +`; + + await fs.writeFile(setupInfoPath, setupInfo, 'utf8'); + + console.log('\n========================================'); + console.log('āœ… Admin user created successfully!'); + console.log('========================================'); console.log('Username: admin'); - console.log('Password: admin123'); - console.log('āš ļø Please change this password immediately!'); + console.log(`Password: ${generatedPassword}`); + console.log('\nāš ļø IMPORTANT:'); + console.log('1. Save these credentials securely'); + console.log('2. You will be required to change the password on first login'); + console.log('3. Credentials are also saved in: ADMIN_CREDENTIALS.txt'); + console.log('========================================\n'); } // Create default email templates if none exist diff --git a/backend/scripts/reset-admin-password.js b/backend/scripts/reset-admin-password.js new file mode 100755 index 0000000..a6e8a6e --- /dev/null +++ b/backend/scripts/reset-admin-password.js @@ -0,0 +1,109 @@ +#!/usr/bin/env node + +const bcrypt = require('bcrypt'); +const { db } = require('../src/database/db'); +const { generateReadablePassword } = require('../src/utils/passwordGenerator'); +const fs = require('fs').promises; +const path = require('path'); +const readline = require('readline'); + +const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout +}); + +async function question(prompt) { + return new Promise((resolve) => { + rl.question(prompt, resolve); + }); +} + +async function resetAdminPassword() { + console.log('\n========================================'); + console.log('PicPeak Admin Password Reset Tool'); + console.log('========================================\n'); + + try { + // Check if admin user exists + const admin = await db('admin_users') + .where({ username: 'admin' }) + .first(); + + if (!admin) { + console.error('āŒ No admin user found in the database.'); + console.log('Run migrations first: npm run migrate'); + process.exit(1); + } + + console.log('Found admin user:', admin.username); + console.log('Email:', admin.email); + console.log('\nThis will reset the password for this admin account.'); + + const confirm = await question('\nDo you want to continue? (yes/no): '); + + if (confirm.toLowerCase() !== 'yes' && confirm.toLowerCase() !== 'y') { + console.log('\nāŒ Password reset cancelled.'); + process.exit(0); + } + + // Generate new password + const newPassword = generateReadablePassword(); + const passwordHash = await bcrypt.hash(newPassword, 12); + + // Update the admin user + await db('admin_users') + .where({ username: 'admin' }) + .update({ + password_hash: passwordHash, + must_change_password: true, + updated_at: new Date() + }); + + // Save to file + const resetInfoPath = path.join(__dirname, '..', '..', 'ADMIN_PASSWORD_RESET.txt'); + const resetInfo = ` +======================================== +PicPeak Admin Password Reset +======================================== + +Password has been reset for admin account: + +Username: admin +New Password: ${newPassword} + +IMPORTANT: +1. You MUST change this password on next login +2. This file contains sensitive information +3. Delete this file after noting the password + +Login URL: ${process.env.ADMIN_URL || 'http://localhost:3001'}/admin + +Reset performed on: ${new Date().toISOString()} +======================================== +`; + + await fs.writeFile(resetInfoPath, resetInfo, 'utf8'); + + console.log('\nāœ… Password reset successful!\n'); + console.log('========================================'); + console.log('New Credentials:'); + console.log('========================================'); + console.log('Username: admin'); + console.log(`Password: ${newPassword}`); + console.log('\nāš ļø IMPORTANT:'); + console.log('1. You will be required to change this password on next login'); + console.log('2. Credentials are also saved in: ADMIN_PASSWORD_RESET.txt'); + console.log('3. Delete the file after noting the password'); + console.log('========================================\n'); + + } catch (error) { + console.error('āŒ Error resetting password:', error.message); + process.exit(1); + } finally { + rl.close(); + process.exit(0); + } +} + +// Run the reset +resetAdminPassword(); \ No newline at end of file diff --git a/backend/src/routes/adminAuth.js b/backend/src/routes/adminAuth.js index 3c65556..0764088 100644 --- a/backend/src/routes/adminAuth.js +++ b/backend/src/routes/adminAuth.js @@ -4,13 +4,14 @@ const { body, validationResult } = require('express-validator'); const { db, logActivity } = require('../database/db'); const { adminAuth } = require('../middleware/auth'); const { endSession } = require('../middleware/sessionTimeout'); +const { validatePasswordStrength } = require('../utils/passwordGenerator'); const router = express.Router(); // Change password router.post('/change-password', [ adminAuth, body('currentPassword').notEmpty().withMessage('Current password is required'), - body('newPassword').isLength({ min: 6 }).withMessage('New password must be at least 6 characters') + body('newPassword').isLength({ min: 12 }).withMessage('New password must be at least 12 characters') ], async (req, res) => { try { const errors = validationResult(req); @@ -21,6 +22,15 @@ router.post('/change-password', [ const { currentPassword, newPassword } = req.body; const userId = req.admin.id; // Changed from req.user.id to req.admin.id + // Validate new password strength + const passwordValidation = validatePasswordStrength(newPassword); + if (!passwordValidation.isValid) { + return res.status(400).json({ + error: 'Password does not meet security requirements', + details: passwordValidation.messages + }); + } + // Get user from database const user = await db('admin_users') .where('id', userId) @@ -36,14 +46,15 @@ router.post('/change-password', [ return res.status(400).json({ error: 'Current password is incorrect' }); } - // Hash new password - const newPasswordHash = await bcrypt.hash(newPassword, 10); + // Hash new password with more rounds + const newPasswordHash = await bcrypt.hash(newPassword, 12); - // Update password + // Update password and clear must_change_password flag await db('admin_users') .where('id', userId) .update({ password_hash: newPasswordHash, + must_change_password: false, updated_at: new Date() }); diff --git a/backend/src/routes/auth.js b/backend/src/routes/auth.js index cbfb0ab..3b42fdb 100644 --- a/backend/src/routes/auth.js +++ b/backend/src/routes/auth.js @@ -41,14 +41,15 @@ router.post('/admin/login', [ // Update last login await db('admin_users').where('id', admin.id).update({ last_login: new Date() }); - const token = jwt.sign({ id: admin.id }, process.env.JWT_SECRET, { expiresIn: '24h' }); + const token = jwt.sign({ id: admin.id, type: 'admin' }, process.env.JWT_SECRET, { expiresIn: '24h' }); res.json({ token, user: { id: admin.id, username: admin.username, - email: admin.email + email: admin.email, + mustChangePassword: admin.must_change_password || false } }); } catch (error) { diff --git a/backend/src/utils/passwordGenerator.js b/backend/src/utils/passwordGenerator.js index 68d2214..0200766 100644 --- a/backend/src/utils/passwordGenerator.js +++ b/backend/src/utils/passwordGenerator.js @@ -1,39 +1,122 @@ +const crypto = require('crypto'); + /** * Generate a secure random password - * @param {number} length - Password length (default 12) + * @param {number} length - Password length (default: 16) * @returns {string} Generated password */ -function generatePassword(length = 12) { +function generateSecurePassword(length = 16) { + const charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+-=[]{}|;:,.<>?'; + let password = ''; + + // Ensure at least one of each required character type const lowercase = 'abcdefghijklmnopqrstuvwxyz'; const uppercase = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; const numbers = '0123456789'; - const symbols = '!@#$%&*'; + const special = '!@#$%^&*()_+-=[]{}|;:,.<>?'; - // Ensure at least one character from each set - const requiredChars = [ - lowercase[Math.floor(Math.random() * lowercase.length)], - uppercase[Math.floor(Math.random() * uppercase.length)], - numbers[Math.floor(Math.random() * numbers.length)], - symbols[Math.floor(Math.random() * symbols.length)] - ]; + // Add one of each required type + password += lowercase[crypto.randomInt(lowercase.length)]; + password += uppercase[crypto.randomInt(uppercase.length)]; + password += numbers[crypto.randomInt(numbers.length)]; + password += special[crypto.randomInt(special.length)]; - // Fill the rest with random characters from all sets - const allChars = lowercase + uppercase + numbers + symbols; - const remainingLength = length - requiredChars.length; - - let password = ''; - for (let i = 0; i < remainingLength; i++) { - password += allChars[Math.floor(Math.random() * allChars.length)]; + // Fill the rest randomly + for (let i = password.length; i < length; i++) { + password += charset[crypto.randomInt(charset.length)]; } - // Combine and shuffle - const passwordArray = [...requiredChars, ...password]; - for (let i = passwordArray.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)); - [passwordArray[i], passwordArray[j]] = [passwordArray[j], passwordArray[i]]; - } - - return passwordArray.join(''); + // Shuffle the password + return password.split('').sort(() => crypto.randomInt(3) - 1).join(''); } -module.exports = { generatePassword }; \ No newline at end of file +/** + * Generate a human-readable password using words and numbers + * @returns {string} Generated password + */ +function generateReadablePassword() { + const adjectives = [ + 'Swift', 'Bright', 'Strong', 'Happy', 'Clever', + 'Brave', 'Noble', 'Quick', 'Sharp', 'Bold' + ]; + + const nouns = [ + 'Eagle', 'Mountain', 'River', 'Thunder', 'Forest', + 'Ocean', 'Falcon', 'Dragon', 'Phoenix', 'Tiger' + ]; + + const adjective = adjectives[crypto.randomInt(adjectives.length)]; + const noun = nouns[crypto.randomInt(nouns.length)]; + const number = crypto.randomInt(1000, 9999); + const special = '!@#$%'[crypto.randomInt(5)]; + + return `${adjective}${noun}${number}${special}`; +} + +/** + * Validate password strength + * @param {string} password - Password to validate + * @returns {object} Validation result with score and messages + */ +function validatePasswordStrength(password) { + const result = { + score: 0, + messages: [], + isValid: false + }; + + // Length check + if (password.length < 8) { + result.messages.push('Password must be at least 8 characters long'); + } else if (password.length < 12) { + result.score += 1; + } else { + result.score += 2; + } + + // Character type checks + if (!/[a-z]/.test(password)) { + result.messages.push('Password must contain lowercase letters'); + } else { + result.score += 1; + } + + if (!/[A-Z]/.test(password)) { + result.messages.push('Password must contain uppercase letters'); + } else { + result.score += 1; + } + + if (!/[0-9]/.test(password)) { + result.messages.push('Password must contain numbers'); + } else { + result.score += 1; + } + + if (!/[!@#$%^&*()_+\-=\[\]{}|;:,.<>?]/.test(password)) { + result.messages.push('Password must contain special characters'); + } else { + result.score += 1; + } + + // Common password check + const commonPasswords = [ + 'password', 'admin123', '12345678', 'qwerty', 'abc123', + 'password123', 'admin', 'letmein', 'welcome', 'monkey' + ]; + + if (commonPasswords.includes(password.toLowerCase())) { + result.score = 0; + result.messages.push('Password is too common'); + } + + result.isValid = result.score >= 4 && result.messages.length === 0; + + return result; +} + +module.exports = { + generateSecurePassword, + generateReadablePassword, + validatePasswordStrength +}; \ No newline at end of file diff --git a/docs/ADMIN_SETUP_GUIDE.md b/docs/ADMIN_SETUP_GUIDE.md new file mode 100644 index 0000000..718765f --- /dev/null +++ b/docs/ADMIN_SETUP_GUIDE.md @@ -0,0 +1,164 @@ +# Admin Setup Guide - Secure Password System + +## Overview + +PicPeak now uses a secure admin setup process that eliminates the default password vulnerability. When you first set up the application, a secure password is automatically generated for the admin account. + +## Initial Setup Process + +### 1. First Installation + +When you run the database migrations for the first time: + +```bash +cd backend +npm run migrate +``` + +The system will: +- Create an admin user with username `admin` +- Generate a secure, random password (e.g., `SwiftEagle3847!`) +- Display the credentials in the console +- Save the credentials to `ADMIN_CREDENTIALS.txt` + +### 2. Retrieving Your Credentials + +After setup, you can find your admin credentials in: +- **Console output** - Displayed immediately after setup +- **ADMIN_CREDENTIALS.txt** - File in the project root + +**Example output:** +``` +======================================== +āœ… Admin user created successfully! +======================================== +Username: admin +Password: SwiftEagle3847! + +āš ļø IMPORTANT: +1. Save these credentials securely +2. You will be required to change the password on first login +3. Credentials are also saved in: ADMIN_CREDENTIALS.txt +======================================== +``` + +### 3. First Login + +1. Navigate to the admin panel: `http://localhost:3001/admin` +2. Login with: + - Username: `admin` + - Password: (from ADMIN_CREDENTIALS.txt) +3. You will be prompted to change your password immediately + +### 4. Password Requirements + +When changing your password, it must meet these requirements: +- Minimum 12 characters long +- Contains uppercase letters (A-Z) +- Contains lowercase letters (a-z) +- Contains numbers (0-9) +- Contains special characters (!@#$%^&*()_+-=[]{}|;:,.<>?) +- Not a common password + +## Security Features + +### Generated Passwords +- Uses cryptographically secure random generation +- Human-readable format: `AdjectiveNoun####!` +- Example: `BrightMountain7823$` + +### Password Storage +- Passwords are hashed using bcrypt with 12 rounds +- Original password is never stored in the database +- Credentials file should be deleted after noting the password + +### Forced Password Change +- Admin must change password on first login +- System tracks `must_change_password` flag +- Cannot access admin features until password is changed + +## Troubleshooting + +### Lost Admin Password + +If you lose the admin password before first login: + +1. Delete the admin user from the database: + ```sql + DELETE FROM admin_users WHERE username = 'admin'; + ``` + +2. Run migrations again: + ```bash + npm run migrate + ``` + +3. New credentials will be generated + +### Password Change Issues + +If you can't change your password: +- Ensure new password meets all requirements +- Check for detailed error messages +- Password strength validator provides specific feedback + +### Can't Find Credentials File + +If ADMIN_CREDENTIALS.txt is missing: +- Check the console output from when you ran migrations +- File is created in the backend directory root +- File might have been deleted for security (as recommended) + +## Best Practices + +1. **Immediate Action** + - Change the generated password on first login + - Use a password manager to store credentials + - Delete ADMIN_CREDENTIALS.txt after noting the password + +2. **Password Security** + - Use unique passwords for each environment + - Rotate passwords regularly (every 90 days) + - Never share admin credentials + +3. **Multiple Admins** + - Create separate admin accounts for each person + - Avoid sharing the main admin account + - Use role-based access control when available + +## Migration from Old System + +If upgrading from the old system with hardcoded `admin123`: + +1. The system will detect existing admin user +2. You must manually reset the password: + ```bash + # Run the password reset script + node scripts/reset-admin-password.js + ``` + +3. Follow the new secure password process + +## Environment-Specific Setup + +### Development +- Generated passwords are suitable for development +- Consider using simpler passwords for convenience +- Always use strong passwords in staging/production + +### Production +- Generate new admin account for production +- Use extremely strong passwords (20+ characters) +- Enable two-factor authentication when available +- Regularly audit admin access logs + +## Security Checklist + +- [ ] Retrieved generated password from ADMIN_CREDENTIALS.txt +- [ ] Logged in successfully with generated password +- [ ] Changed password to a strong, unique password +- [ ] Deleted ADMIN_CREDENTIALS.txt file +- [ ] Stored new password in password manager +- [ ] Tested login with new password +- [ ] Set up additional admin accounts if needed +- [ ] Configured password policies for organization \ No newline at end of file