Newer
Older
EnvoyControlPlane / static / secrets.js
// 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)</span>';
    }
}

/**
 * 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();
}

/**
 * NEW: 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;


// =========================================================================
// 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' button for TLS Certificates regardless of status
            if (isTlsCertificate(secret)) {
                actionButtons += `
                    <button class="action-button view-cert" onclick="window.showCertificateDetailsModal('${secret.name}')">View Details</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}`);
    }
}