Files
proxmox/scripts/nginx-proxy-manager/request-npmplus-7-certs-dns-ui.js

183 lines
7.4 KiB
JavaScript
Raw Permalink Normal View History

#!/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);
});