#!/usr/bin/env node /** * Request SSL certificates for the 7 NPMplus proxy hosts that have no cert. * Uses browser automation: for each host, edit → SSL → Request new certificate * → DNS Challenge → Cloudflare → (first credential) → email + agree → submit. * * Run from repo root: node scripts/nginx-proxy-manager/request-npmplus-7-certs-dns-ui.js * Requires: .env with NPM_URL, NPM_EMAIL, NPM_PASSWORD. HEADLESS=false to watch. */ import { chromium } from 'playwright'; import { fileURLToPath } from 'url'; import { dirname, join } from 'path'; import { config } from 'dotenv'; import https from 'https'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const PROJECT_ROOT = join(__dirname, '../..'); config({ path: join(PROJECT_ROOT, '.env') }); const NPM_URL = process.env.NPM_URL || 'https://192.168.11.167:81'; const NPM_EMAIL = process.env.NPM_EMAIL || 'admin@example.org'; const NPM_PASSWORD = process.env.NPM_PASSWORD; const LETSENCRYPT_EMAIL = process.env.SSL_EMAIL || process.env.NPM_EMAIL || NPM_EMAIL; const HEADLESS = process.env.HEADLESS !== 'false'; const PAUSE_MODE = process.env.PAUSE_MODE === 'true'; // Host IDs for the 7 proxy hosts without a certificate (from list-npmplus-proxy-hosts-cert-status.sh) // Set FIRST_ONLY=1 to process only host 22 (for testing) const ALL_HOST_IDS = [22, 26, 24, 27, 28, 29, 25]; const HOST_IDS_WITHOUT_CERT = process.env.FIRST_ONLY === '1' || process.env.FIRST_ONLY === 'true' ? ALL_HOST_IDS.slice(0, 1) : ALL_HOST_IDS; if (!NPM_PASSWORD) { console.error('❌ NPM_PASSWORD is required. Set it in .env or export NPM_PASSWORD=...'); process.exit(1); } function log(msg, type = 'info') { const icons = { success: '✅', error: '❌', warning: '⚠️', info: '📋' }; console.log(`${icons[type] || '📋'} ${msg}`); } async function pause(page, message) { if (PAUSE_MODE) { log(`Paused: ${message}`, 'info'); await page.pause(); } } async function login(page) { log('Logging in to NPMplus...'); await page.goto(NPM_URL, { waitUntil: 'domcontentloaded', timeout: 30000 }); await pause(page, 'At login page'); await page.waitForSelector('input[type="email"], input[name="email"], input[placeholder*="email" i]', { timeout: 10000 }); const emailInput = await page.$('input[type="email"]') || await page.$('input[name="email"]') || await page.$('input[placeholder*="email" i]'); if (emailInput) await emailInput.fill(NPM_EMAIL); const passwordInput = await page.$('input[type="password"]'); if (passwordInput) await passwordInput.fill(NPM_PASSWORD); const loginButton = await page.$('button[type="submit"]') || await page.$('button:has-text("Sign In")') || await page.$('button:has-text("Login")'); if (loginButton) await loginButton.click(); else await page.keyboard.press('Enter'); await page.waitForTimeout(3000); const url = page.url(); if (url.includes('login') && !url.includes('proxy')) { const body = await page.textContent('body').catch(() => ''); if (!body.includes('Proxy Hosts') && !body.includes('dashboard')) { log('Login may have failed – still on login page', 'warning'); await page.screenshot({ path: join(PROJECT_ROOT, 'npmplus-login-check.png') }).catch(() => {}); } } log('Logged in', 'success'); return true; } async function requestCertForHostId(page, hostId) { log(`Requesting cert for host ID ${hostId}...`); try { // NPM edit URL: open edit form directly by host ID await page.goto(`${NPM_URL}/#/proxy-hosts/edit/${hostId}`, { waitUntil: 'networkidle' }); await page.waitForTimeout(3000); // SSL tab: NPM edit form usually has Details | SSL | Advanced await page.getByText('SSL').first().click(); await page.waitForTimeout(1500); // "Request a new SSL Certificate" / "Get a new certificate" const requestBtn = page.getByRole('button', { name: /request.*(new )?ssl certificate|get.*certificate/i }).or( page.locator('button:has-text("Request"), button:has-text("Get a new"), a:has-text("Request")').first() ); await requestBtn.click(); await page.waitForTimeout(1500); // DNS Challenge: click option/label for "DNS Challenge" or "Use a DNS Challenge" const dnsOption = page.getByText(/use a dns challenge|dns challenge/i).first(); await dnsOption.click(); await page.waitForTimeout(800); // DNS Provider: Cloudflare (dropdown or first Cloudflare option) const cloudflareOption = page.getByText('Cloudflare').first(); await cloudflareOption.click(); await page.waitForTimeout(800); // Credential: usually first in dropdown if only one Cloudflare credential const credSelect = page.locator('select').filter({ has: page.locator('option') }).first(); if (await credSelect.count() > 0) { await credSelect.selectOption({ index: 1 }); } await page.waitForTimeout(500); // Email for Let's Encrypt const emailField = page.locator('input[type="email"], input[name*="email" i]').first(); await emailField.fill(LETSENCRYPT_EMAIL); // Agree to ToS const agree = page.locator('input[type="checkbox"]').filter({ has: page.locator('..') }).first(); if (await agree.count() > 0 && !(await agree.isChecked())) await agree.check(); await pause(page, `Ready to submit cert request for host ${hostId}`); // Submit const submitBtn = page.getByRole('button', { name: /save|submit|request|get certificate/i }).first(); await submitBtn.click(); await page.waitForTimeout(5000); // Check for success or error const body = await page.textContent('body').catch(() => ''); if (body.includes('error') && body.toLowerCase().includes('internal')) { log(`Request for host ${hostId} may have failed (Internal Error). Check NPM UI.`, 'warning'); return false; } log(`Submitted cert request for host ${hostId}`, 'success'); return true; } catch (e) { log(`Error for host ${hostId}: ${e.message}`, 'error'); await page.screenshot({ path: join(PROJECT_ROOT, `npmplus-cert-error-${hostId}.png`) }).catch(() => {}); return false; } } async function main() { console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); console.log('🔒 NPMplus – Request 7 certificates (DNS Cloudflare)'); console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); console.log(''); log(`NPM: ${NPM_URL}`); log(`Host IDs: ${HOST_IDS_WITHOUT_CERT.join(', ')}`); console.log(''); const browser = await chromium.launch({ headless: HEADLESS, ignoreHTTPSErrors: true }); const context = await browser.newContext({ ignoreHTTPSErrors: true }); const page = await context.newPage(); try { const ok = await login(page); if (!ok) { log('Login failed', 'error'); process.exit(1); } let success = 0; for (const hostId of HOST_IDS_WITHOUT_CERT) { const ok = await requestCertForHostId(page, hostId); if (ok) success++; await page.waitForTimeout(2000); } console.log(''); log(`Done. Submitted requests for ${success}/${HOST_IDS_WITHOUT_CERT.length} hosts. Check NPM SSL Certificates and Hosts to confirm.`, 'success'); log('Run: ./scripts/list-npmplus-proxy-hosts-cert-status.sh', 'info'); } finally { await browser.close(); } } main().catch((e) => { console.error(e); process.exit(1); });