zitinexus-router-script/UI/public/assets/js/app.js

606 lines
21 KiB
JavaScript

/**
* ZitiNexus Router Enrollment UI JavaScript
*/
class EnrollmentUI {
constructor() {
this.enrollmentInProgress = false;
this.progressSteps = [
'INIT', 'REQUIREMENTS', 'INSTALL', 'DIRECTORIES',
'REGISTER', 'CONFIG', 'ENROLL', 'SERVICE', 'START', 'REPORT', 'COMPLETE'
];
this.currentStep = 0;
this.init();
}
init() {
this.bindEvents();
this.loadSystemStatus();
// Auto-refresh system status every 30 seconds
setInterval(() => {
if (!this.enrollmentInProgress) {
this.loadSystemStatus();
}
}, 30000);
}
bindEvents() {
// Enrollment form submission
const enrollForm = document.getElementById('enrollmentForm');
if (enrollForm) {
enrollForm.addEventListener('submit', (e) => {
e.preventDefault();
this.startEnrollment();
});
}
// Hash key validation
const hashKeyInput = document.getElementById('hashKey');
if (hashKeyInput) {
hashKeyInput.addEventListener('input', (e) => {
this.validateHashKey(e.target);
});
}
// API endpoint validation
const apiEndpointInput = document.getElementById('apiEndpoint');
if (apiEndpointInput) {
apiEndpointInput.addEventListener('input', (e) => {
this.validateApiEndpoint(e.target);
});
}
// Refresh system status button
const refreshBtn = document.getElementById('refreshStatus');
if (refreshBtn) {
refreshBtn.addEventListener('click', () => {
this.loadSystemStatus();
});
}
// Clear logs button
const clearLogsBtn = document.getElementById('clearLogs');
if (clearLogsBtn) {
clearLogsBtn.addEventListener('click', () => {
this.clearLogs();
});
}
// Cleanup button
const cleanupBtn = document.getElementById('cleanupBtn');
if (cleanupBtn) {
cleanupBtn.addEventListener('click', () => {
this.showCleanupConfirmation();
});
}
}
validateHashKey(input) {
const value = input.value.trim();
const isValid = /^[a-fA-F0-9]{32}$/.test(value);
if (value.length === 0) {
this.setInputState(input, 'neutral');
} else if (isValid) {
this.setInputState(input, 'valid');
} else {
this.setInputState(input, 'invalid', 'Hash key must be 32 hexadecimal characters');
}
return isValid;
}
validateApiEndpoint(input) {
const value = input.value.trim();
const isValid = /^https?:\/\/.+/.test(value);
if (value.length === 0) {
this.setInputState(input, 'neutral');
} else if (isValid) {
this.setInputState(input, 'valid');
} else {
this.setInputState(input, 'invalid', 'Must be a valid HTTP/HTTPS URL');
}
return isValid;
}
setInputState(input, state, message = '') {
const formGroup = input.closest('.form-group');
const feedback = formGroup.querySelector('.form-feedback') || this.createFeedbackElement(formGroup);
// Remove existing state classes
input.classList.remove('is-valid', 'is-invalid');
feedback.classList.remove('valid-feedback', 'invalid-feedback');
switch (state) {
case 'valid':
input.classList.add('is-valid');
feedback.classList.add('valid-feedback');
feedback.textContent = 'Looks good!';
feedback.style.display = 'block';
break;
case 'invalid':
input.classList.add('is-invalid');
feedback.classList.add('invalid-feedback');
feedback.textContent = message;
feedback.style.display = 'block';
break;
case 'neutral':
default:
feedback.style.display = 'none';
break;
}
}
createFeedbackElement(formGroup) {
const feedback = document.createElement('div');
feedback.className = 'form-feedback';
feedback.style.fontSize = '0.875rem';
feedback.style.marginTop = '0.25rem';
formGroup.appendChild(feedback);
return feedback;
}
async loadSystemStatus() {
try {
const response = await fetch('dashboard.php?action=get_status', {
method: 'GET',
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
this.updateSystemStatus(data);
} catch (error) {
console.error('Failed to load system status:', error);
this.showAlert('Failed to load system status', 'error');
}
}
updateSystemStatus(status) {
// Update hostname
const hostnameElement = document.getElementById('hostname');
if (hostnameElement) {
hostnameElement.textContent = status.hostname || 'Unknown';
}
// Update Ziti status
const zitiStatusElement = document.getElementById('zitiStatus');
const zitiStatusIcon = document.getElementById('zitiStatusIcon');
if (zitiStatusElement && zitiStatusIcon) {
if (status.ziti_status === 'installed') {
zitiStatusElement.textContent = `Installed (${status.ziti_version})`;
zitiStatusIcon.className = 'status-icon success';
zitiStatusIcon.innerHTML = '✓';
} else {
zitiStatusElement.textContent = 'Not Installed';
zitiStatusIcon.className = 'status-icon error';
zitiStatusIcon.innerHTML = '✗';
}
}
// Update service status
const serviceStatusElement = document.getElementById('serviceStatus');
const serviceStatusIcon = document.getElementById('serviceStatusIcon');
if (serviceStatusElement && serviceStatusIcon) {
if (status.service_active) {
serviceStatusElement.textContent = 'Running';
serviceStatusIcon.className = 'status-icon success';
serviceStatusIcon.innerHTML = '▶';
} else {
serviceStatusElement.textContent = 'Stopped';
serviceStatusIcon.className = 'status-icon error';
serviceStatusIcon.innerHTML = '⏹';
}
}
// Update configuration status
const configStatusElement = document.getElementById('configStatus');
const configStatusIcon = document.getElementById('configStatusIcon');
if (configStatusElement && configStatusIcon) {
if (status.config_exists && status.certificates_exist) {
configStatusElement.textContent = 'Configured';
configStatusIcon.className = 'status-icon success';
configStatusIcon.innerHTML = '⚙';
} else if (status.config_exists) {
configStatusElement.textContent = 'Partial';
configStatusIcon.className = 'status-icon warning';
configStatusIcon.innerHTML = '⚠';
} else {
configStatusElement.textContent = 'Not Configured';
configStatusIcon.className = 'status-icon error';
configStatusIcon.innerHTML = '✗';
}
}
}
simulateProgress() {
const steps = [
{ step: 0, message: 'Initializing enrollment process...', delay: 800 },
{ step: 1, message: 'Verifying OpenZiti installation and requirements...', delay: 2500 },
{ step: 3, message: 'Creating necessary directories...', delay: 1800 },
{ step: 4, message: 'Registering router with ZitiNexus Portal...', delay: 3500 },
{ step: 5, message: 'Saving configuration files and certificates...', delay: 2200 },
{ step: 6, message: 'Enrolling router with OpenZiti controller...', delay: 4000 },
{ step: 7, message: 'Creating and configuring systemd service...', delay: 2000 },
{ step: 8, message: 'Starting router service...', delay: 2800 },
{ step: 9, message: 'Reporting enrollment status...', delay: 1500 }
];
let currentIndex = 0;
this.progressSimulationActive = true;
const advanceStep = () => {
if (currentIndex < steps.length && this.enrollmentInProgress && this.progressSimulationActive) {
const { step, message, delay } = steps[currentIndex];
this.currentStep = step;
// Calculate percentage based on step progress
const percentage = Math.round(((step + 1) / 11) * 90); // Cap at 90% until real completion
this.updateProgress(percentage, message);
currentIndex++;
setTimeout(advanceStep, delay);
}
};
// Start simulation after a brief delay
setTimeout(advanceStep, 300);
}
stopProgressSimulation() {
this.progressSimulationActive = false;
}
async startEnrollment() {
if (this.enrollmentInProgress) {
return;
}
const hashKeyInput = document.getElementById('hashKey');
const apiEndpointInput = document.getElementById('apiEndpoint');
const enrollBtn = document.getElementById('enrollBtn');
// Validate inputs
const hashKeyValid = this.validateHashKey(hashKeyInput);
const apiEndpointValid = this.validateApiEndpoint(apiEndpointInput);
if (!hashKeyValid || !apiEndpointValid) {
this.showAlert('Please fix the validation errors before proceeding', 'error');
return;
}
// Start enrollment process
this.enrollmentInProgress = true;
this.currentStep = 0;
// Update UI
enrollBtn.disabled = true;
enrollBtn.innerHTML = '<span class="spinner"></span>Starting Enrollment...';
this.showProgressContainer();
this.clearLogs();
// Start progress simulation immediately
this.simulateProgress();
try {
const formData = new FormData();
formData.append('action', 'enroll');
formData.append('hashKey', hashKeyInput.value.trim());
formData.append('apiEndpoint', apiEndpointInput.value.trim());
formData.append('csrf_token', document.querySelector('input[name="csrf_token"]').value);
const response = await fetch('dashboard.php', {
method: 'POST',
body: formData,
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const result = await response.json();
// Stop progress simulation
this.stopProgressSimulation();
if (result.success) {
// Complete the progress
this.currentStep = 10; // Final step
this.updateProgress(100, 'Enrollment completed successfully!');
this.addLogEntry('success', `Router '${result.routerName}' enrolled successfully`);
this.showAlert(`Router enrollment completed successfully! Router: ${result.routerName}`, 'success');
// Refresh system status after successful enrollment
setTimeout(() => {
this.loadSystemStatus();
}, 2000);
} else {
throw new Error(result.error || 'Enrollment failed');
}
} catch (error) {
console.error('Enrollment failed:', error);
// Stop progress simulation and show error
this.stopProgressSimulation();
this.updateProgress(null, 'Enrollment failed');
this.addLogEntry('error', `Enrollment failed: ${error.message}`);
this.showAlert(`Enrollment failed: ${error.message}`, 'error');
} finally {
this.enrollmentInProgress = false;
enrollBtn.disabled = false;
enrollBtn.innerHTML = 'Start Enrollment';
}
}
showProgressContainer() {
const progressContainer = document.getElementById('progressContainer');
if (progressContainer) {
progressContainer.classList.add('active');
}
}
updateProgress(percentage, message) {
// Update progress bar
const progressFill = document.getElementById('progressFill');
if (progressFill && percentage !== null) {
progressFill.style.width = `${percentage}%`;
}
// Update current step
if (message) {
this.addLogEntry('info', message);
}
// Update progress steps
this.updateProgressSteps();
}
updateProgressSteps() {
const progressSteps = document.querySelectorAll('.progress-step');
progressSteps.forEach((step, index) => {
step.classList.remove('active', 'completed', 'error');
if (index < this.currentStep) {
step.classList.add('completed');
} else if (index === this.currentStep) {
step.classList.add('active');
}
});
}
addLogEntry(type, message) {
const logContainer = document.getElementById('logContainer');
if (!logContainer) return;
const timestamp = new Date().toLocaleTimeString();
const logEntry = document.createElement('div');
logEntry.className = `log-entry ${type}`;
logEntry.textContent = `[${timestamp}] ${message}`;
logContainer.appendChild(logEntry);
logContainer.scrollTop = logContainer.scrollHeight;
}
clearLogs() {
const logContainer = document.getElementById('logContainer');
if (logContainer) {
logContainer.innerHTML = '';
}
}
showAlert(message, type = 'info') {
// Remove existing alerts
const existingAlerts = document.querySelectorAll('.alert');
existingAlerts.forEach(alert => alert.remove());
// Create new alert
const alert = document.createElement('div');
alert.className = `alert alert-${type}`;
alert.textContent = message;
// Insert at the top of main content
const mainContent = document.querySelector('.main-content');
if (mainContent) {
mainContent.insertBefore(alert, mainContent.firstChild);
}
// Auto-remove after 5 seconds for non-error alerts
if (type !== 'error') {
setTimeout(() => {
if (alert.parentNode) {
alert.remove();
}
}, 5000);
}
}
showCleanupConfirmation() {
const confirmed = confirm(
'⚠️ WARNING: Router Cleanup\n\n' +
'This action will:\n' +
'• Stop and disable the ziti-router service\n' +
'• Remove all router configuration files\n' +
'• Delete all certificates\n' +
'• Remove the systemd service file\n\n' +
'This action cannot be undone!\n\n' +
'Are you sure you want to proceed?'
);
if (confirmed) {
this.startCleanup();
}
}
simulateCleanupProgress() {
const cleanupSteps = [
{ step: 0, message: 'Starting router cleanup process...', delay: 500 },
{ step: 2, message: 'Stopping ziti-router service...', delay: 1500 },
{ step: 4, message: 'Resetting service failed state...', delay: 1000 },
{ step: 6, message: 'Cleaning service state...', delay: 1200 },
{ step: 8, message: 'Removing router configuration directory...', delay: 2000 },
{ step: 9, message: 'Removing systemd service file...', delay: 1000 }
];
let currentIndex = 0;
this.cleanupSimulationActive = true;
const advanceStep = () => {
if (currentIndex < cleanupSteps.length && this.cleanupSimulationActive) {
const { step, message, delay } = cleanupSteps[currentIndex];
this.currentStep = step;
// Calculate percentage based on cleanup progress
const percentage = Math.round(((step + 1) / 11) * 85); // Cap at 85% until real completion
this.updateProgress(percentage, message);
currentIndex++;
setTimeout(advanceStep, delay);
}
};
// Start cleanup simulation after a brief delay
setTimeout(advanceStep, 200);
}
stopCleanupSimulation() {
this.cleanupSimulationActive = false;
}
async startCleanup() {
if (this.enrollmentInProgress) {
this.showAlert('Cannot perform cleanup while enrollment is in progress', 'error');
return;
}
const cleanupBtn = document.getElementById('cleanupBtn');
// Update UI
cleanupBtn.disabled = true;
cleanupBtn.innerHTML = '<span class="spinner"></span>Cleaning Up...';
this.showProgressContainer();
this.clearLogs();
this.currentStep = 0;
// Start cleanup progress simulation
this.simulateCleanupProgress();
try {
const formData = new FormData();
formData.append('action', 'cleanup');
formData.append('csrf_token', document.querySelector('input[name="csrf_token"]').value);
const response = await fetch('dashboard.php', {
method: 'POST',
body: formData,
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const result = await response.json();
// Stop cleanup simulation
this.stopCleanupSimulation();
if (result.success) {
// Complete the cleanup progress
this.currentStep = 10; // Final step
this.updateProgress(100, 'Cleanup completed successfully!');
this.addLogEntry('success', result.message);
this.showAlert('Router cleanup completed successfully! The page will reload in 3 seconds.', 'success');
// Clear the hash key input field
const hashKeyInput = document.getElementById('hashKey');
if (hashKeyInput) {
hashKeyInput.value = '';
this.setInputState(hashKeyInput, 'neutral');
}
// Refresh system status immediately
this.loadSystemStatus();
// Auto-reload page after 3 seconds
setTimeout(() => {
window.location.reload();
}, 3000);
} else {
throw new Error(result.error || 'Cleanup failed');
}
} catch (error) {
console.error('Cleanup failed:', error);
// Stop cleanup simulation and show error
this.stopCleanupSimulation();
this.updateProgress(null, 'Cleanup failed');
this.addLogEntry('error', `Cleanup failed: ${error.message}`);
this.showAlert(`Cleanup failed: ${error.message}`, 'error');
} finally {
cleanupBtn.disabled = false;
cleanupBtn.innerHTML = '🗑️ Clean Up Router';
}
}
// Utility method to format file sizes
formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
// Utility method to format timestamps
formatTimestamp(timestamp) {
return new Date(timestamp * 1000).toLocaleString();
}
}
// Initialize the application when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
window.enrollmentUI = new EnrollmentUI();
});
// Add CSS classes for input validation
const style = document.createElement('style');
style.textContent = `
.form-input.is-valid {
border-color: var(--success-color);
box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.1);
}
.form-input.is-invalid {
border-color: var(--error-color);
box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.1);
}
.valid-feedback {
color: var(--success-color);
}
.invalid-feedback {
color: var(--error-color);
}
`;
document.head.appendChild(style);