<!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)
});
const data = await response.json();
if (!response.ok) {
// Handle API-side error (e.g., HTTP 400 or 500)
const errorDetails = data.error || 'Unknown server error.';
throw new Error(`API Error: ${response.status} - ${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);
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>