// secrets.js
import { API_BASE_URL, configStore, cleanupConfigStore } from './global.js';
// =========================================================================
// SECRET UTILITIES (Unchanged)
// =========================================================================
/**
* 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) (Unchanged)
// =========================================================================
/**
* 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. (Unchanged)
* Prompts the user for the domain name.
* @param {string} secretName - The name of the secret/certificate.
*/
async function manualRenewCertificate(secretName) {
// 1. **NEW FEATURE LOGIC:** Generate suggested domain name
const suggestedDomain = secretName.replace(/_/g, '.');
// 2. 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: ${suggestedDomain})`,
suggestedDomain // <-- ADDED: Use the suggested domain as the default input value
);
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;
}
// 3. 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}`);
// 4. 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. (Unchanged)
* @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) (Unchanged)
// =========================================================================
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) (Unchanged)
// =========================================================================
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.
* MODIFIED: Clears upsert checkbox on show.
*/
export function showAddSecretModal() {
document.getElementById('add-secret-yaml-input').value = '';
const upsertCheckbox = document.getElementById('add-secret-upsert-flag');
if (upsertCheckbox) {
upsertCheckbox.checked = false;
}
document.getElementById('addSecretModal').style.display = 'block';
}
/**
* Hides the modal for adding a new secret.
* MODIFIED: Clears upsert checkbox on hide.
*/
export function hideAddSecretModal() {
const modal = document.getElementById('addSecretModal');
if (modal) {
modal.style.display = 'none';
document.getElementById('add-secret-yaml-input').value = '';
const upsertCheckbox = document.getElementById('add-secret-upsert-flag');
if (upsertCheckbox) {
upsertCheckbox.checked = false;
}
}
}
/**
* Submits the new secret YAML to the /add-secret endpoint.
* MODIFIED: Now checks for an 'allow-upsert' checkbox and adds 'upsert: true' to the payload.
*/
export async function submitNewSecret() {
const yamlInput = document.getElementById('add-secret-yaml-input');
// Get the checkbox element and its state
const upsertCheckbox = document.getElementById('add-secret-upsert-flag');
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 };
// Add upsert flag to payload if checkbox is checked
if (upsertCheckbox && upsertCheckbox.checked) {
// Note: Assuming your API expects the 'upsert' flag as a separate top-level key like the YAML key.
// If the API expects { yaml: ..., upsert: true } use: payload.upsert = true;
// Based on your Listener/Cluster pattern, it's safer to use a top-level `upsert` key if the API supports it.
// Assuming payload: { YAML: "...", upsert: true }
payload.upsert = true;
}
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 = '';
// Uncheck the box upon success/closing
if (upsertCheckbox) {
upsertCheckbox.checked = false;
}
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}`);
}
}