#!/usr/bin/env node /** * Configure SSL certificates for all domains in Nginx Proxy Manager * Uses browser automation to configure proxy hosts and Let's Encrypt certificates */ import { chromium } from 'playwright'; import { readFileSync } from 'fs'; import { fileURLToPath } from 'url'; import { dirname, join } from 'path'; import { config } from 'dotenv'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const PROJECT_ROOT = join(__dirname, '../..'); // Load environment variables config({ path: join(PROJECT_ROOT, '.env') }); // Configuration const NPM_URL = process.env.NPM_URL || 'http://192.168.11.26:81'; const NPM_EMAIL = process.env.NPM_EMAIL || process.env.NGINX_EMAIL || 'nsatoshi2007@hotmail.com'; const NPM_USERNAME = process.env.NPM_USERNAME || 'nsatoshi2007'; const NPM_PASSWORD = process.env.NPM_PASSWORD || process.env.NGINX_PASSWORD || 'L@ker$2010'; const HEADLESS = process.env.HEADLESS !== 'false'; const PAUSE_MODE = process.env.PAUSE_MODE === 'true'; // All domains to configure const DOMAINS = [ // sankofa.nexus zone { domain: 'sankofa.nexus', target: 'http://192.168.11.140:80', description: 'Sankofa Nexus' }, { domain: 'www.sankofa.nexus', target: 'http://192.168.11.140:80', description: 'Sankofa Nexus WWW' }, { domain: 'phoenix.sankofa.nexus', target: 'http://192.168.11.140:80', description: 'Phoenix Sankofa' }, { domain: 'www.phoenix.sankofa.nexus', target: 'http://192.168.11.140:80', description: 'Phoenix Sankofa WWW' }, { domain: 'the-order.sankofa.nexus', target: 'http://192.168.11.140:80', description: 'The Order' }, // d-bis.org zone { domain: 'explorer.d-bis.org', target: 'http://192.168.11.140:4000', description: 'Blockscout Explorer (Direct Route)' }, { domain: 'rpc-http-pub.d-bis.org', target: 'https://192.168.11.252:443', description: 'RPC HTTP Public' }, { domain: 'rpc-ws-pub.d-bis.org', target: 'https://192.168.11.252:443', description: 'RPC WebSocket Public' }, { domain: 'rpc-http-prv.d-bis.org', target: 'https://192.168.11.251:443', description: 'RPC HTTP Private' }, { domain: 'rpc-ws-prv.d-bis.org', target: 'https://192.168.11.251:443', description: 'RPC WebSocket Private' }, { domain: 'dbis-admin.d-bis.org', target: 'http://192.168.11.130:80', description: 'DBIS Admin' }, { domain: 'dbis-api.d-bis.org', target: 'http://192.168.11.155:3000', description: 'DBIS API' }, { domain: 'dbis-api-2.d-bis.org', target: 'http://192.168.11.156:3000', description: 'DBIS API 2' }, { domain: 'secure.d-bis.org', target: 'http://192.168.11.130:80', description: 'DBIS Secure' }, // mim4u.org zone // MIM4U - VMID 7810 (mim-web-1) @ 192.168.11.37 - Web Frontend serves main site and proxies /api/* to 7811 { domain: 'mim4u.org', target: 'http://192.168.11.37:80', description: 'MIM4U' }, { domain: 'www.mim4u.org', target: 'http://192.168.11.37:80', description: 'MIM4U WWW' }, { domain: 'secure.mim4u.org', target: 'http://192.168.11.37:80', description: 'MIM4U Secure' }, { domain: 'training.mim4u.org', target: 'http://192.168.11.37:80', description: 'MIM4U Training' }, // defi-oracle.io zone { domain: 'rpc.public-0138.defi-oracle.io', target: 'https://192.168.11.252:443', description: 'ThirdWeb RPC' }, ]; // Helper functions const log = { info: (msg) => console.log(`[INFO] ${msg}`), success: (msg) => console.log(`[✓] ${msg}`), warn: (msg) => console.log(`[⚠] ${msg}`), error: (msg) => console.log(`[✗] ${msg}`), }; const pause = async (page, message) => { if (PAUSE_MODE) { log.info(`⏸ PAUSE: ${message}`); log.info('Press Enter to continue...'); await new Promise(resolve => { process.stdin.once('data', () => resolve()); }); } }; async function login(page) { log.info('Logging in to Nginx Proxy Manager...'); try { // Try with more lenient wait strategy await page.goto(NPM_URL, { waitUntil: 'domcontentloaded', timeout: 60000 }); await page.waitForTimeout(2000); // Give page time to fully load await pause(page, 'At login page'); // Take screenshot for debugging await page.screenshot({ path: '/tmp/npm-login-page.png' }).catch(() => {}); log.info('Screenshot saved to /tmp/npm-login-page.png'); // Wait for login form - try multiple selectors try { await page.waitForSelector('input[type="email"], input[name="email"], input[type="text"], input[placeholder*="email" i]', { timeout: 15000 }); } catch (e) { log.warn('Login form not found with standard selectors, trying alternative...'); await page.waitForSelector('input', { timeout: 10000 }); } // Find email input - try multiple strategies let emailInput = await page.$('input[type="email"]'); if (!emailInput) emailInput = await page.$('input[name="email"]'); if (!emailInput) emailInput = await page.$('input[placeholder*="email" i]'); if (!emailInput) emailInput = await page.$('input[type="text"]'); if (!emailInput) { // Get all inputs and try the first one const inputs = await page.$$('input'); if (inputs.length > 0) emailInput = inputs[0]; } if (emailInput) { await emailInput.click(); await emailInput.fill(NPM_EMAIL); log.info(`Filled email: ${NPM_EMAIL}`); } else { log.error('Could not find email input field'); return false; } // Find password input const passwordInput = await page.$('input[type="password"]'); if (passwordInput) { await passwordInput.click(); await passwordInput.fill(''); // Clear first await page.waitForTimeout(300); await passwordInput.fill(NPM_PASSWORD); await page.waitForTimeout(300); // Trigger events to ensure validation await passwordInput.evaluate(el => { el.dispatchEvent(new Event('input', { bubbles: true })); el.dispatchEvent(new Event('change', { bubbles: true })); el.dispatchEvent(new Event('blur', { bubbles: true })); }); log.info('Filled password'); } else { log.error('Could not find password input field'); return false; } await pause(page, 'Credentials filled, ready to submit'); // Find and click login button - try multiple strategies let loginButton = await page.$('button[type="submit"]'); if (!loginButton) loginButton = await page.$('button:has-text("Sign In")'); if (!loginButton) loginButton = await page.$('button:has-text("Login")'); if (!loginButton) loginButton = await page.$('button:has-text("Log in")'); if (!loginButton) { // Try to find any button and click it const buttons = await page.$$('button'); if (buttons.length > 0) loginButton = buttons[buttons.length - 1]; // Usually submit is last } if (loginButton) { await loginButton.click(); log.info('Clicked login button'); } else { // Try pressing Enter log.info('Login button not found, pressing Enter'); await page.keyboard.press('Enter'); } // Wait a moment for any error messages to appear await page.waitForTimeout(2000); // Check for error messages before waiting for navigation const errorSelectors = [ '.error', '.alert-danger', '.alert-error', '[role="alert"]', '.text-danger', '.invalid-feedback', '[class*="error"]', '[class*="invalid"]' ]; for (const selector of errorSelectors) { const errorElement = await page.$(selector); if (errorElement) { const errorText = await errorElement.textContent(); if (errorText && errorText.trim().length > 0) { log.error(`Login error detected: ${errorText.trim()}`); await page.screenshot({ path: '/tmp/npm-login-error.png' }).catch(() => {}); return false; } } } // Wait for navigation or dashboard - with better error handling try { await page.waitForURL(/dashboard|hosts|proxy|login/, { timeout: 20000 }); const currentUrl = page.url(); if (currentUrl.includes('login')) { // Check for error message again after navigation attempt let errorFound = false; for (const selector of errorSelectors) { const errorElement = await page.$(selector); if (errorElement) { const errorText = await errorElement.textContent(); if (errorText && errorText.trim().length > 0) { log.error(`Login failed: ${errorText.trim()}`); errorFound = true; break; } } } if (!errorFound) { // Check page content for error messages const pageText = await page.textContent('body'); if (pageText && (pageText.includes('Invalid') || pageText.includes('incorrect') || pageText.includes('error'))) { log.error('Login failed - error message detected in page content'); errorFound = true; } } if (!errorFound) { log.error('Login failed - still on login page (no error message found)'); } await page.screenshot({ path: '/tmp/npm-login-error.png' }).catch(() => {}); return false; } log.success('Logged in successfully'); await pause(page, 'After login'); return true; } catch (e) { log.error(`Login timeout: ${e.message}`); // Check for errors one more time const pageText = await page.textContent('body').catch(() => ''); if (pageText && (pageText.includes('Invalid') || pageText.includes('incorrect'))) { log.error('Login failed - invalid credentials detected'); } await page.screenshot({ path: '/tmp/npm-login-timeout.png' }).catch(() => {}); return false; } } catch (error) { log.error(`Login error: ${error.message}`); await page.screenshot({ path: '/tmp/npm-login-error.png' }).catch(() => {}); return false; } } async function configureProxyHost(page, domainConfig) { const { domain, target, description } = domainConfig; log.info(`Configuring proxy host for ${domain}...`); try { // Navigate to Proxy Hosts await page.goto(`${NPM_URL}/#/proxy-hosts`, { waitUntil: 'networkidle' }); await pause(page, `At proxy hosts page for ${domain}`); // Click Add Proxy Host button const addButton = await page.$('button:has-text("Add Proxy Host")') || await page.$('a:has-text("Add Proxy Host")') || await page.$('button.btn-primary'); if (!addButton) { log.warn(`Could not find Add Proxy Host button for ${domain}`); return false; } await addButton.click(); await page.waitForTimeout(1000); await pause(page, `Add Proxy Host form opened for ${domain}`); // Fill in domain name const domainInput = await page.$('input[name="domain_names"]') || await page.$('input[placeholder*="domain"]') || await page.$('input[type="text"]'); if (domainInput) { await domainInput.fill(domain); } // Configure forwarding const schemeSelect = await page.$('select[name="forward_scheme"]') || await page.$('select'); if (schemeSelect) { const scheme = target.startsWith('https') ? 'https' : 'http'; await schemeSelect.selectOption(scheme); } const hostInput = await page.$('input[name="forward_hostname"]') || await page.$('input[name="forward_host"]'); if (hostInput) { const host = new URL(target).hostname; await hostInput.fill(host); } const portInput = await page.$('input[name="forward_port"]'); if (portInput) { const port = new URL(target).port || (target.startsWith('https') ? '443' : '80'); await portInput.fill(port); } await pause(page, `Form filled for ${domain}`); // Configure SSL const sslTab = await page.$('a:has-text("SSL")') || await page.$('button:has-text("SSL")'); if (sslTab) { await sslTab.click(); await page.waitForTimeout(500); // Request Let's Encrypt certificate const requestCertButton = await page.$('button:has-text("Request a new SSL Certificate")') || await page.$('button:has-text("Request SSL")'); if (requestCertButton) { await requestCertButton.click(); await page.waitForTimeout(500); // Enable Force SSL const forceSSL = await page.$('input[type="checkbox"][name*="force"]') || await page.$('input[type="checkbox"]'); if (forceSSL) { await forceSSL.check(); } // Enable HTTP/2 const http2 = await page.$('input[type="checkbox"][name*="http2"]'); if (http2) { await http2.check(); } // Enable HSTS const hsts = await page.$('input[type="checkbox"][name*="hsts"]'); if (hsts) { await hsts.check(); } await pause(page, `SSL configured for ${domain}`); } } // Save const saveButton = await page.$('button:has-text("Save")') || await page.$('button.btn-primary:has-text("Save")'); if (saveButton) { await saveButton.click(); await page.waitForTimeout(2000); log.success(`Proxy host configured for ${domain}`); return true; } else { log.warn(`Could not find Save button for ${domain}`); return false; } } catch (error) { log.error(`Error configuring ${domain}: ${error.message}`); return false; } } async function main() { console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); console.log('🔒 Nginx Proxy Manager SSL Configuration'); console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); console.log(''); log.info(`NPM URL: ${NPM_URL}`); log.info(`Email: ${NPM_EMAIL}`); log.info(`Domains to configure: ${DOMAINS.length}`); console.log(''); const browser = await chromium.launch({ headless: HEADLESS, ignoreHTTPSErrors: true, }); const context = await browser.newContext({ ignoreHTTPSErrors: true, }); const page = await context.newPage(); try { // Login const loggedIn = await login(page); if (!loggedIn) { log.error('Failed to login. Please check credentials.'); process.exit(1); } // Configure each domain let successCount = 0; let failCount = 0; for (const domainConfig of DOMAINS) { const success = await configureProxyHost(page, domainConfig); if (success) { successCount++; } else { failCount++; } // Small delay between domains await page.waitForTimeout(1000); } console.log(''); console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); log.info(`Configuration complete: ${successCount} succeeded, ${failCount} failed`); console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); } catch (error) { log.error(`Fatal error: ${error.message}`); console.error(error); process.exit(1); } finally { await browser.close(); } } main().catch(console.error);