From b5399aaa9b8c64c538fc27bb01569d1bcde79147 Mon Sep 17 00:00:00 2001 From: paul Date: Wed, 24 Sep 2025 17:33:30 +0200 Subject: [PATCH] Add installer flag to regenerate admin credentials --- DEPLOYMENT_GUIDE.md | 12 +++- backend/scripts/reset-admin-password.js | 76 ++++++++++++++++++------- docs/ADMIN_SETUP_GUIDE.md | 4 +- scripts/setup.sh | 24 +++++++- 4 files changed, 91 insertions(+), 25 deletions(-) diff --git a/DEPLOYMENT_GUIDE.md b/DEPLOYMENT_GUIDE.md index ceeec3c..6d818f5 100644 --- a/DEPLOYMENT_GUIDE.md +++ b/DEPLOYMENT_GUIDE.md @@ -420,7 +420,17 @@ Upon first login, the system will **automatically redirect** you to change your If you lose your admin credentials after the first login, you'll need to manually reset the password in the database or create a new admin user through the database. -**Note**: The credentials file (`ADMIN_CREDENTIALS.txt`) is only created during initial deployment and contains the first admin password. After changing the password, this file becomes outdated but is kept for reference. +**Note**: The credentials file (`ADMIN_CREDENTIALS.txt`) is only created during initial deployment and contains the first admin password. After changing the password, this file becomes outdated but is kept for reference. If you need to regenerate the password and file during a reinstall, re-run the installer with the `--force-admin-password-reset` flag: + +```bash +# Native reinstall example +sudo ./setup.sh --native --force-admin-password-reset + +# Docker reinstall example +sudo ./setup.sh --docker --force-admin-password-reset +``` + +The flag calls `scripts/reset-admin-password.js` in non-interactive mode, writes a fresh random password into `data/ADMIN_CREDENTIALS.txt`, and prints the new credentials at the end of the installer run. #### Configuring Admin Email diff --git a/backend/scripts/reset-admin-password.js b/backend/scripts/reset-admin-password.js index a6e8a6e..f72a664 100755 --- a/backend/scripts/reset-admin-password.js +++ b/backend/scripts/reset-admin-password.js @@ -7,12 +7,32 @@ const fs = require('fs').promises; const path = require('path'); const readline = require('readline'); -const rl = readline.createInterface({ +const args = process.argv.slice(2); +const hasFlag = (flag) => args.includes(flag); +const getOption = (name) => { + const index = args.indexOf(`--${name}`); + if (index !== -1 && index + 1 < args.length) { + return args[index + 1]; + } + return null; +}; + +const force = hasFlag('--force') || hasFlag('--yes') || hasFlag('--non-interactive'); +const credentialsFileArg = getOption('credentials-file'); +const resolvedCredentialsFile = credentialsFileArg + ? path.resolve(process.cwd(), credentialsFileArg) + : path.join(__dirname, '..', '..', 'ADMIN_PASSWORD_RESET.txt'); + +const rl = force ? null : readline.createInterface({ input: process.stdin, output: process.stdout }); -async function question(prompt) { +async function ask(prompt) { + if (force) { + return 'yes'; + } + return new Promise((resolve) => { rl.question(prompt, resolve); }); @@ -37,13 +57,18 @@ async function resetAdminPassword() { 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); + if (!force) { + console.log('\nThis will reset the password for this admin account.'); + } + + const confirm = await ask('\nDo you want to continue? (yes/no): '); + + if (!force) { + const normalized = confirm.trim().toLowerCase(); + if (normalized !== 'yes' && normalized !== 'y') { + console.log('\n❌ Password reset cancelled.'); + process.exit(0); + } } // Generate new password @@ -60,39 +85,44 @@ async function resetAdminPassword() { }); // Save to file - const resetInfoPath = path.join(__dirname, '..', '..', 'ADMIN_PASSWORD_RESET.txt'); + const credentialsDir = path.dirname(resolvedCredentialsFile); + await fs.mkdir(credentialsDir, { recursive: true }); + + const adminUrl = `${process.env.ADMIN_URL || 'http://localhost:3001'}/admin`; const resetInfo = ` ======================================== -PicPeak Admin Password Reset +PicPeak Admin Credentials ======================================== -Password has been reset for admin account: +Your admin account has been reset with these credentials: -Username: admin -New Password: ${newPassword} +Username: ${admin.username} +Email: ${admin.email} +Password: ${newPassword} -IMPORTANT: -1. You MUST change this password on next login +IMPORTANT SECURITY NOTES: +1. You MUST change this password after first 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 +Login URL: ${adminUrl} Reset performed on: ${new Date().toISOString()} ======================================== `; - await fs.writeFile(resetInfoPath, resetInfo, 'utf8'); + await fs.writeFile(resolvedCredentialsFile, resetInfo, 'utf8'); console.log('\n✅ Password reset successful!\n'); console.log('========================================'); console.log('New Credentials:'); console.log('========================================'); - console.log('Username: admin'); + console.log(`Username: ${admin.username}`); + console.log(`Email: ${admin.email}`); 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(`2. Credentials are also saved in: ${resolvedCredentialsFile}`); console.log('3. Delete the file after noting the password'); console.log('========================================\n'); @@ -100,10 +130,12 @@ Reset performed on: ${new Date().toISOString()} console.error('❌ Error resetting password:', error.message); process.exit(1); } finally { - rl.close(); + if (rl) { + rl.close(); + } process.exit(0); } } // Run the reset -resetAdminPassword(); \ No newline at end of file +resetAdminPassword(); diff --git a/docs/ADMIN_SETUP_GUIDE.md b/docs/ADMIN_SETUP_GUIDE.md index 718765f..4f13c14 100644 --- a/docs/ADMIN_SETUP_GUIDE.md +++ b/docs/ADMIN_SETUP_GUIDE.md @@ -108,6 +108,8 @@ If ADMIN_CREDENTIALS.txt is missing: - Check the console output from when you ran migrations - File is created in the backend directory root - File might have been deleted for security (as recommended) +- Regenerate it by running `node scripts/reset-admin-password.js --force --credentials-file data/ADMIN_CREDENTIALS.txt` +- When using the unified `setup.sh` installer for a reinstall, append `--force-admin-password-reset` to have the script perform the reset automatically ## Best Practices @@ -161,4 +163,4 @@ If upgrading from the old system with hardcoded `admin123`: - [ ] 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 +- [ ] Configured password policies for organization diff --git a/scripts/setup.sh b/scripts/setup.sh index 30cc0cc..743162a 100755 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -55,6 +55,7 @@ CUSTOM_PORT="" UNATTENDED=false UPDATE_MODE=false UNINSTALL_MODE=false +FORCE_ADMIN_PASSWORD_RESET=false ################################################################################ # Helper Functions @@ -487,6 +488,15 @@ EOF # Run database migrations log_step "Running database migrations..." docker compose exec -T backend npm run migrate + + if [[ "$FORCE_ADMIN_PASSWORD_RESET" == "true" ]]; then + log_step "Resetting admin credentials..." + if docker compose exec -T backend node scripts/reset-admin-password.js --force --credentials-file data/ADMIN_CREDENTIALS.txt; then + docker compose cp backend:/app/data/ADMIN_CREDENTIALS.txt "$app_dir/data/ADMIN_CREDENTIALS.txt" 2>/dev/null || true + else + log_warn "Automatic admin password reset failed; run reset-admin-password.js inside the backend container." + fi + fi log_success "Docker installation completed!" } @@ -737,6 +747,13 @@ EOF log_step "Initializing database..." cd "$NATIVE_APP_DIR/app/backend" run_as_user "npm run migrate" + + if [[ "$FORCE_ADMIN_PASSWORD_RESET" == "true" ]]; then + log_step "Resetting admin credentials..." + if ! run_as_user "node scripts/reset-admin-password.js --force --credentials-file data/ADMIN_CREDENTIALS.txt"; then + log_warn "Automatic admin password reset failed; please run reset-admin-password.js manually." + fi + fi # Create systemd services create_systemd_services @@ -989,7 +1006,7 @@ print_success_message() { fi else echo -e "Email: ${CYAN}$ADMIN_EMAIL${NC}" - echo -e "Password: ${YELLOW}(credentials file not found)${NC}" + echo -e "Password: ${YELLOW}(credentials file not found - rerun setup with --force-admin-password-reset or run node scripts/reset-admin-password.js manually)${NC}" fi echo echo -e "${YELLOW}⚠️ IMPORTANT: Change the admin password on first login!${NC}" @@ -1256,6 +1273,10 @@ parse_arguments() { SMTP_PASS="$2" shift 2 ;; + --force-admin-password-reset) + FORCE_ADMIN_PASSWORD_RESET=true + shift + ;; --enable-ssl) ENABLE_SSL=true shift @@ -1301,6 +1322,7 @@ Options: --smtp-port PORT SMTP server port --smtp-user USER SMTP username --smtp-pass PASS SMTP password + --force-admin-password-reset Regenerate admin credentials after setup --enable-ssl Enable HTTPS with Let's Encrypt --port PORT Custom port (native only) --update Update existing installation