<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>SSL Certificate Issuance Tool 🛡️</title> <style> :root { --primary: #007bff; --success: #28a745; --danger: #dc3545; --bg-light: #f8f9fa; --border-color: #ced4da; } body { font-family: Arial, sans-serif; margin: 20px; background-color: var(--bg-light); color: #333; } .container { max-width: 800px; margin: auto; background: white; padding: 30px; border-radius: 8px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); } h1 { text-align: center; color: var(--primary); margin-bottom: 30px; } .form-group { margin-bottom: 20px; } label { display: block; margin-bottom: 5px; font-weight: bold; } input[type="text"], input[type="email"], select { width: 100%; padding: 10px; border: 1px solid var(--border-color); border-radius: 4px; box-sizing: border-box; } button { background-color: var(--primary); color: white; padding: 12px 20px; border: none; border-radius: 4px; cursor: pointer; font-size: 16px; transition: background-color 0.3s; } button:hover:not(:disabled) { background-color: #0056b3; } button:disabled { background-color: #6c757d; cursor: not-allowed; } /* Status and Results */ #status-message { padding: 15px; border-radius: 4px; margin-top: 20px; font-weight: bold; display: none; } .status-success { background-color: #d4edda; color: var(--success); border: 1px solid var(--success); } .status-error { background-color: #f8d7da; color: var(--danger); border: 1px solid var(--danger); } #results-area { margin-top: 30px; padding-top: 20px; border-top: 1px solid var(--border-color); } .cert-item { background-color: var(--bg-light); padding: 15px; margin-bottom: 15px; border-radius: 4px; } .cert-item h3 { margin-top: 0; color: var(--primary); display: flex; justify-content: space-between; align-items: center; } textarea { width: 100%; height: 150px; margin-top: 10px; padding: 10px; border: 1px solid var(--border-color); border-radius: 4px; font-family: monospace; resize: vertical; white-space: pre; word-wrap: normal; overflow-x: scroll; } .action-buttons { display: flex; gap: 10px; margin-top: 10px; } .action-buttons button { padding: 8px 15px; font-size: 14px; } .copy-btn { background-color: #6c757d; } .copy-btn:hover { background-color: #5a6268; } </style> </head> <body> <div class="container"> <h1>SSL Certificate Issuance Tool 🛡️</h1> <form id="certificate-form"> <div class="form-group"> <label for="domain">Domain Name (e.g., abc.jerxie.com)</label> <input type="text" id="domain" name="domain" required placeholder="Enter the domain to secure"> </div> <div class="form-group"> <label for="email">Contact Email (for renewal notices)</label> <input type="email" id="email" name="email" required placeholder="Enter your contact email"> </div> <div class="form-group"> <label for="issuer">Certificate Issuer</label> <select id="issuer" name="issuer" required> <option value="letsencrypt">Let's Encrypt (Recommended)</option> </select> </div> <button type="submit" id="submit-btn">Issue Certificate</button> </form> <div id="status-message"></div> <div id="results-area" style="display: none;"> <h2>Certificate & Key Files</h2> <p>Download these files and install them on your web server.</p> </div> </div> <script> const API_ENDPOINT = window.location.origin + '/issue-certificate'; const form = document.getElementById('certificate-form'); const submitBtn = document.getElementById('submit-btn'); const statusMessage = document.getElementById('status-message'); const resultsArea = document.getElementById('results-area'); const domainInput = document.getElementById('domain'); // Helper function to create a text area and action buttons for a single component function createCertComponent(title, key, content, domain) { const extMap = { 'CertPEM': '.crt', 'KeyPEM': '.key', 'FullChain': '.fullchain.pem', 'AccountKey': '.account.key' }; const defaultFilename = `${domain}${extMap[key]}`; const itemDiv = document.createElement('div'); itemDiv.className = 'cert-item'; itemDiv.id = `cert-item-${key}`; const header = document.createElement('h3'); header.textContent = title; itemDiv.appendChild(header); const textarea = document.createElement('textarea'); textarea.readOnly = true; textarea.dataset.key = key; let displayContent; let downloadContent; // --- FIX APPLIED HERE --- // Key components (Cert, Key, FullChain) are expected to be PEM strings after decoding. // The AccountKey might be a raw binary key, which should be downloaded as-is // or displayed in its Base64 form if it causes encoding issues. if (key === 'AccountKey' || key === 'KeyPEM') { // For keys, we should ideally download the raw decoded content. // But for display, we'll try to decode and fall back to Base64 if it fails // to look like a proper PEM. try { // Attempt to Base64 decode and use the decoded data for both display and download downloadContent = atob(content); displayContent = downloadContent; // Heuristic check: If it doesn't look like a standard PEM (starts with "-----"), // display the Base64 version to prevent "garbage" text. if (!displayContent.startsWith('-----')) { displayContent = content; // Display Base64 } } catch (e) { // If decoding fails, just use the Base64 string for display downloadContent = content; displayContent = content; } } else { // CertPEM and FullChain are almost certainly PEM format after decoding. downloadContent = atob(content); displayContent = downloadContent; } textarea.value = displayContent; // --- END FIX --- itemDiv.appendChild(textarea); const actionsDiv = document.createElement('div'); actionsDiv.className = 'action-buttons'; // 1. Download Button const downloadBtn = document.createElement('button'); downloadBtn.textContent = `Download (${extMap[key] || '.txt'})`; // Use the potentially raw/decoded content for download downloadBtn.onclick = () => downloadFile(downloadContent, defaultFilename, 'text/plain'); actionsDiv.appendChild(downloadBtn); // 2. Copy Button const copyBtn = document.createElement('button'); copyBtn.textContent = 'Copy to Clipboard'; copyBtn.className = 'copy-btn'; copyBtn.onclick = () => copyToClipboard(textarea); actionsDiv.appendChild(copyBtn); itemDiv.appendChild(actionsDiv); resultsArea.appendChild(itemDiv); } // --- Core Functions --- async function copyToClipboard(textarea) { try { await navigator.clipboard.writeText(textarea.value); alert('Content copied to clipboard!'); } catch (err) { console.error('Could not copy text: ', err); textarea.select(); document.execCommand('copy'); alert('Content copied to clipboard! (Fallback)'); } } function downloadFile(content, filename, mimeType) { const blob = new Blob([content], { type: mimeType }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } function showStatus(message, isError = false) { statusMessage.textContent = message; statusMessage.className = isError ? 'status-error' : 'status-success'; statusMessage.style.display = 'block'; } function clearResults() { // Remove all children except the h2 and p tags Array.from(resultsArea.children).forEach(child => { if (!['H2', 'P'].includes(child.tagName)) { child.remove(); } }); resultsArea.style.display = 'none'; statusMessage.style.display = 'none'; } async function handleSubmit(event) { event.preventDefault(); clearResults(); submitBtn.disabled = true; submitBtn.textContent = 'Issuing... Please Wait ⏳'; const domain = domainInput.value.trim(); const email = document.getElementById('email').value.trim(); const issuer = document.getElementById('issuer').value; showStatus(`Requesting certificate for ${domain}...`, false); const requestBody = { domain, email, issuer }; try { const response = await fetch(API_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(requestBody) }); // Try to read the JSON data first let data = {}; try { data = await response.json(); } catch (e) { // If JSON parsing fails (e.g., non-JSON error response), use an empty object console.error("Failed to parse response body as JSON:", e); } if (!response.ok) { // Handle API-side error (e.g., HTTP 400 or 500) // Prioritize specific error messages from the server response (e.g., 'error' or 'message') const errorDetails = data.error || data.message || `HTTP ${response.status} Error`; // Throw the detailed error. The catch block will display it. throw new Error(errorDetails); } // Successful Response showStatus(`✅ Certificate issued successfully for ${domain}!`, false); resultsArea.style.display = 'block'; // Map of API Key to User-Friendly Title const certComponents = { 'CertPEM': 'Certificate (Public Key)', 'KeyPEM': 'Private Key (Keep Secure!)', 'FullChain': 'Full Certificate Chain', 'AccountKey': 'ACME Account Key' }; // Dynamically create the output components for (const key in certComponents) { if (data[key]) { createCertComponent(certComponents[key], key, data[key], domain); } } } catch (error) { console.error('Certificate Issuance Failed:', error); // Display the specific error message from the thrown error showStatus(`❌ Issuance failed. Details: ${error.message || 'Check console for network issues.'}`, true); } finally { submitBtn.disabled = false; submitBtn.textContent = 'Issue Certificate'; } } form.addEventListener('submit', handleSubmit); </script> </body> </html>