Some checks failed
Deploy to Phoenix / deploy (push) Has been cancelled
- ADD_CHAIN138_TO_LEDGER_LIVE: Ledger form done; public code review repo bis-innovations/LedgerLive; init/push commands - CONTRACT_DEPLOYMENT_RUNBOOK: Chain 138 gas price 1 gwei, 36-addr check, TransactionMirror workaround - CONTRACT_*: AddressMapper, MirrorManager deployed 2026-02-12; 36-address on-chain check - NEXT_STEPS_FOR_YOU: Ledger done; steps completable now (no LAN); run-completable-tasks-from-anywhere - MASTER_INDEX, OPERATOR_OPTIONAL, SMART_CONTRACTS_INVENTORY_SIMPLE: updates - LEDGER_BLOCKCHAIN_INTEGRATION_COMPLETE: bis-innovations/LedgerLive reference Co-authored-by: Cursor <cursoragent@cursor.com>
416 lines
16 KiB
JavaScript
Executable File
416 lines
16 KiB
JavaScript
Executable File
#!/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);
|