// secrets.js import { API_BASE_URL, configStore, cleanupConfigStore } from './global.js'; // ========================================================================= // SECRET UTILITIES // ========================================================================= /** * Extracts a concise description of the secret type (e.g., TlsCertificate). * @param {object} secret - The secret configuration object. * @returns {string} A string describing the secret type. */ function getSecretTypeDetails(secret) { try { const secretType = secret.Type; if (!secretType) return '<span style="color: gray;">(Unknown Type)</span>'; // Find the key that is not 'name' or 'Type' itself const typeKeys = Object.keys(secretType); const typeName = typeKeys.find(key => key !== 'name'); let typeDetails = ''; if (typeName) { // Convert 'TlsCertificate' to 'TLS Certificate' typeDetails = typeName.replace(/([A-Z])/g, ' $1').trim(); } else { typeDetails = '<span style="color: gray;">(Generic)</span>'; } // ADD: Append validity status for TLS Certificates if (typeName === 'TlsCertificate' && secret.validityStatus) { const statusText = secret.validityStatus; // 'Valid' or 'Invalid' const statusClass = statusText.toLowerCase(); // 'valid' or 'invalid' typeDetails += ` <span class="status-label ${statusClass}">${statusText}</span>`; } return typeDetails; } catch { return '<span style="color: gray;">(Config Error)'; } } /** * Checks if a secret is a TLS Certificate. * @param {object} secret - The secret configuration object. * @returns {boolean} True if the secret is a TLS Certificate. */ function isTlsCertificate(secret) { try { const typeKeys = Object.keys(secret.Type); return typeKeys.some(key => key === 'TlsCertificate'); } catch { return false; } } // ========================================================================= // CERTIFICATE LOGIC (API & Modal) // ========================================================================= /** * Calls the backend API to parse and retrieve certificate details. * @param {string} certificatePem - The PEM encoded certificate string. * @returns {Promise<object>} The parsed certificate details as a JSON object. */ async function getCertificateDetails(certificatePem) { const url = `${API_BASE_URL}/parse-certificate`; const payload = { certificate_pem: certificatePem }; const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); if (!response.ok) { const errorBody = await response.text(); throw new Error(`HTTP Error ${response.status}: ${errorBody}`); } return response.json(); } /** * Calls the backend API to check the validity of a certificate. * @param {string} certificatePem - The PEM encoded certificate string. * @returns {Promise<boolean>} True if the certificate is valid, false otherwise. */ async function checkCertificateValidity(certificatePem) { const url = `${API_BASE_URL}/check-certificate-validity`; const payload = { certificate_pem: certificatePem }; try { const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); if (!response.ok) { // Treat an un-ok response as invalid or uncheckable, but don't crash the UI. console.warn(`Validity check failed with status ${response.status} for a certificate.`); return false; } const result = await response.json(); // The API response structure is { "valid": true/false } return result.valid === true; } catch (error) { console.error("Error during certificate validity check:", error); return false; // Assume invalid on API failure } } /** * Shows a modal with the detailed certificate information. * @param {string} secretName - The name of the secret. */ export async function showCertificateDetailsModal(secretName) { const modal = document.getElementById('certificateDetailsModal'); const content = document.getElementById('certificate-details-content'); if (!modal || !content) { console.error("Certificate modal elements not found. Ensure 'certificateDetailsModal' and 'certificate-details-content' exist in your HTML."); alert("Certificate details modal is not properly configured in HTML."); return; } // Get the full secret config, which should contain the 'certificate_pem' const secretConfig = configStore.secrets[secretName]; // Drill down to the PEM string const certPem = secretConfig?.Type?.TlsCertificate?.certificate_chain?.Specifier?.InlineString if (!certPem) { alert(`Could not find certificate PEM in the configuration for secret: ${secretName}`); return; } content.innerHTML = `<p style="text-align: center;">Parsing certificate for <strong>${secretName}</strong>...</p>`; modal.style.display = 'block'; try { const details = await getCertificateDetails(certPem); // Display the pretty-printed JSON response content.innerHTML = ` <h3>TLS Certificate Details: ${secretName}</h3> <pre style="white-space: pre-wrap; word-wrap: break-word; background: var(--input-bg); padding: 10px; border-radius: 5px;">${JSON.stringify(details, null, 2)}</pre> `; } catch (error) { content.innerHTML = ` <h3>Error Parsing Certificate: ${secretName}</h3> <p class="error">🚨 Failed to parse certificate. Error: ${error.message}</p> `; console.error("Certificate Parsing Error:", error); } } // Expose the new function globally for inline HTML onclick handlers window.showCertificateDetailsModal = showCertificateDetailsModal; /** * Core logic to manually renew a TLS certificate. * Prompts the user for the domain name. * @param {string} secretName - The name of the secret/certificate. */ async function manualRenewCertificate(secretName) { // 1. Get user confirmation and input for the domain name const domainName = prompt( `Please enter the domain name associated with secret '${secretName}' to renew its certificate. \n(Example: test.jerxie.com)` ); if (!domainName) { console.log(`Certificate renewal for secret '${secretName}' cancelled or domain not provided.`); return; } if (!confirm(`Confirm renewal request for domain: ${domainName} (Secret: ${secretName})?`)) { return; } // 2. Prepare API payload const renewSecretName = secretName; // Use the secret name itself for the 'secret_name' API param const url = `${API_BASE_URL}/renew-certificate`; const payload = { domain: domainName.trim(), // Use user input secret_name: renewSecretName }; console.log(`Attempting to renew certificate for domain: ${domainName.trim()} with secret: ${renewSecretName}`); // 3. Execute API call with robust error handling try { const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); // Check if the response is NOT OK (e.g., 4xx, 5xx) if (!response.ok) { // Try to read the error from the response body, which could be plain text or JSON const errorText = await response.text(); // Attempt to parse the error body as JSON to get a cleaner message let errorMessage = `HTTP Error ${response.status}: ${response.statusText}`; try { const errorJson = JSON.parse(errorText); // Check for a common 'error' field in the JSON response errorMessage = errorJson.error || errorJson.message || errorText; } catch { // If parsing fails, use the raw text as the message errorMessage = errorText || errorMessage; } // Throw a custom error to be caught below and alerted to the user throw new Error(errorMessage); } // If successful, attempt to parse the JSON body const responseBody = await response.json(); console.log(`Certificate for '${domainName.trim()}' successfully renewed:`, responseBody); alert(`Certificate for ${domainName.trim()} successfully renewed! The list will now refresh.`); // Force a UI refresh to show the potentially updated certificate validity status cleanupConfigStore(); listSecrets(); } catch (error) { // This catches the custom error thrown above OR a network/fetch error console.error(`Failed to renew certificate for '${domainName.trim()}':`, error); // Alert the user with the specific error message alert(`🚨 Renewal failed for ${domainName.trim()}. Error: ${error.message}`); } } /** * UI entrypoint for manual certificate renewal. * @param {string} secretName - The name of the secret/certificate. * @param {Event} event - The click event to stop propagation. */ export function manualRenewCertificateExport(secretName, event) { event.stopPropagation(); manualRenewCertificate(secretName); } // ========================================================================= // SECRET CORE LOGIC (listSecrets) // ========================================================================= export async function listSecrets() { const tableBody = document.getElementById('secret-table-body'); if (!tableBody) { console.error("Could not find element with ID 'secret-table-body'."); return; } tableBody.innerHTML = '<tr><td colspan="4" style="text-align: center; padding: 20px;">Loading...</td></tr>'; try { // The API operations show /list-secrets returns enabled and disabled lists const response = await fetch(`${API_BASE_URL}/list-secrets`); if (!response.ok) throw new Error(response.statusText); const secretResponse = await response.json(); // Combine enabled and disabled secrets for display let allSecrets = [ ...(secretResponse.enabled || []).map(s => ({ ...s, status: 'Enabled', configData: s })), ...(secretResponse.disabled || []).map(s => ({ ...s, status: 'Disabled', configData: s })) ]; if (!allSecrets.length) { tableBody.innerHTML = '<tr><td colspan="4" style="text-align: center; color: var(--secondary-color);">No secrets found.</td></tr>'; configStore.secrets = {}; return; } cleanupConfigStore(); // ---------------------------------------------------------------------- // NEW: Check validity for all TLS certificates concurrently const validityChecks = allSecrets.map(async secret => { if (isTlsCertificate(secret)) { // Drill down to the PEM string const certPem = secret.Type?.TlsCertificate?.certificate_chain?.Specifier?.InlineString; if (certPem) { const isValid = await checkCertificateValidity(certPem); secret.validityStatus = isValid ? 'Valid' : 'Invalid'; } else { secret.validityStatus = 'Invalid'; // Treat missing PEM as invalid } } return secret; }); // Wait for all checks to complete allSecrets = await Promise.all(validityChecks); // ---------------------------------------------------------------------- // Store full configs in memory by name configStore.secrets = allSecrets.reduce((acc, s) => { const existingYaml = acc[s.name]?.yaml; // Also store the validityStatus in the configStore entry for potential future use acc[s.name] = { ...s.configData, yaml: existingYaml, validityStatus: s.validityStatus }; return acc; }, configStore.secrets); tableBody.innerHTML = ''; allSecrets.forEach(secret => { const row = tableBody.insertRow(); if (secret.status === 'Disabled') row.classList.add('disabled-row'); let actionButtons = ''; // NOTE: Assuming the API supports enable/disable for secrets like clusters if (secret.status === 'Enabled') { actionButtons = `<button class="action-button disable" onclick="window.disableSecret('${secret.name}', event)">Disable</button>`; } else { actionButtons = ` <button class="action-button enable" onclick="window.enableSecret('${secret.name}', event)">Enable</button> <button class="action-button remove" onclick="window.removeSecret('${secret.name}', event)">Remove</button> `; } // Add 'View Details' and 'Renew Cert' buttons for TLS Certificates if (isTlsCertificate(secret)) { actionButtons += ` <button class="action-button view-cert" onclick="window.showCertificateDetailsModal('${secret.name}')">View Details</button> <button class="action-button renew-cert" onclick="window.manualRenewCertificateExport('${secret.name}', event)">Renew Cert</button> `; } // Secret Name Hyperlink (uses showSecretConfigModal, which must be imported from global.js or defined globally) const secretNameCell = row.insertCell(); secretNameCell.innerHTML = `<a href="#" onclick="event.preventDefault(); window.showSecretConfigModal('${secret.name}')"><span class="secret-name">${secret.name}</span></a>`; row.insertCell().textContent = secret.status; // The validity label is now included in the result of getSecretTypeDetails row.insertCell().innerHTML = getSecretTypeDetails(secret); row.insertCell().innerHTML = actionButtons; }); } catch (error) { tableBody.innerHTML = `<tr><td colspan="4" class="error" style="text-align: center;">🚨 Secret Error: ${error.message}</td></tr>`; console.error("Secret Fetch/Parse Error:", error); } } // ========================================================================= // SECRET ENABLE/DISABLE/REMOVE LOGIC (toggleSecretStatus) // ========================================================================= async function toggleSecretStatus(secretName, action) { // API endpoints are assumed to be /remove-secret, /enable-secret, /disable-secret let url = (action === 'remove') ? `${API_BASE_URL}/remove-secret` : `${API_BASE_URL}/${action}-secret`; const payload = { name: secretName }; try { const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); if (!response.ok) { const errorBody = await response.text(); throw new Error(`HTTP Error ${response.status}: ${errorBody}`); } console.log(`Secret '${secretName}' successfully ${action}d.`); cleanupConfigStore(); listSecrets(); } catch (error) { console.error(`Failed to ${action} secret '${secretName}':`, error); alert(`Failed to ${action} secret '${secretName}'. Check console for details.`); } } // Expose these functions globally for inline HTML onclick handlers export function disableSecret(secretName, event) { event.stopPropagation(); if (confirm(`Are you sure you want to DISABLE secret: ${secretName}?`)) { toggleSecretStatus(secretName, 'disable'); } } export function enableSecret(secretName, event) { event.stopPropagation(); if (confirm(`Are you sure you want to ENABLE secret: ${secretName}?`)) { toggleSecretStatus(secretName, 'enable'); } } export function removeSecret(secretName, event) { event.stopPropagation(); if (confirm(`⚠️ WARNING: Are you absolutely sure you want to PERMANENTLY REMOVE secret: ${secretName}? This action cannot be undone.`)) { toggleSecretStatus(secretName, 'remove'); } } // ========================================================================= // ADD SECRET LOGIC (showAddSecretModal, hideAddSecretModal, submitNewSecret) // ========================================================================= /** * Shows the modal for adding a new secret. */ export function showAddSecretModal() { document.getElementById('add-secret-yaml-input').value = ''; document.getElementById('addSecretModal').style.display = 'block'; } /** * Hides the modal for adding a new secret. */ export function hideAddSecretModal() { const modal = document.getElementById('addSecretModal'); if (modal) { modal.style.display = 'none'; document.getElementById('add-secret-yaml-input').value = ''; } } /** * Submits the new secret YAML to the /add-secret endpoint. */ export async function submitNewSecret() { const yamlInput = document.getElementById('add-secret-yaml-input'); const secretYaml = yamlInput.value.trim(); if (!secretYaml) { alert('Please paste the secret YAML configuration.'); return; } try { // The /add-secret endpoint expects a JSON body with a 'YAML' key containing the stringified YAML. const payload = { YAML: secretYaml }; const url = `${API_BASE_URL}/add-secret`; const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); if (!response.ok) { const errorBody = await response.text(); throw new Error(`HTTP Error ${response.status}: ${errorBody}`); } console.log(`New secret successfully added.`); alert('Secret successfully added! The dashboard will now refresh.'); yamlInput.value = ''; hideAddSecretModal(); cleanupConfigStore(); listSecrets(); } catch (error) { console.error(`Failed to add new secret:`, error); alert(`Failed to add new secret. Check console for details. Error: ${error.message}`); } }