824 lines
29 KiB
JavaScript
824 lines
29 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();
|
|
});
|
|
}
|
|
|
|
// Settings button
|
|
const settingsBtn = document.getElementById('settingsBtn');
|
|
if (settingsBtn) {
|
|
settingsBtn.addEventListener('click', () => {
|
|
this.showSettingsModal();
|
|
});
|
|
}
|
|
|
|
// Settings modal events
|
|
this.bindSettingsModalEvents();
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
// Settings Modal Methods
|
|
bindSettingsModalEvents() {
|
|
const modal = document.getElementById('settingsModal');
|
|
const closeBtn = document.getElementById('closeSettingsModal');
|
|
const cancelBtn = document.getElementById('cancelSettings');
|
|
const settingsForm = document.getElementById('settingsForm');
|
|
|
|
// Close modal events
|
|
if (closeBtn) {
|
|
closeBtn.addEventListener('click', () => {
|
|
this.hideSettingsModal();
|
|
});
|
|
}
|
|
|
|
if (cancelBtn) {
|
|
cancelBtn.addEventListener('click', () => {
|
|
this.hideSettingsModal();
|
|
});
|
|
}
|
|
|
|
// Close modal when clicking outside
|
|
if (modal) {
|
|
modal.addEventListener('click', (e) => {
|
|
if (e.target === modal) {
|
|
this.hideSettingsModal();
|
|
}
|
|
});
|
|
}
|
|
|
|
// Settings form submission
|
|
if (settingsForm) {
|
|
settingsForm.addEventListener('submit', (e) => {
|
|
e.preventDefault();
|
|
this.handleSettingsSubmit();
|
|
});
|
|
}
|
|
|
|
// Password strength and match validation
|
|
const newPasswordInput = document.getElementById('settings_new_password');
|
|
const confirmPasswordInput = document.getElementById('settings_confirm_password');
|
|
|
|
if (newPasswordInput) {
|
|
newPasswordInput.addEventListener('input', () => {
|
|
this.updatePasswordStrength('settings');
|
|
this.checkPasswordMatch('settings');
|
|
});
|
|
}
|
|
|
|
if (confirmPasswordInput) {
|
|
confirmPasswordInput.addEventListener('input', () => {
|
|
this.checkPasswordMatch('settings');
|
|
});
|
|
}
|
|
}
|
|
|
|
showSettingsModal() {
|
|
const modal = document.getElementById('settingsModal');
|
|
if (modal) {
|
|
modal.classList.add('active');
|
|
// Focus on first input
|
|
const firstInput = modal.querySelector('input[type="password"]');
|
|
if (firstInput) {
|
|
setTimeout(() => firstInput.focus(), 100);
|
|
}
|
|
}
|
|
}
|
|
|
|
hideSettingsModal() {
|
|
const modal = document.getElementById('settingsModal');
|
|
if (modal) {
|
|
modal.classList.remove('active');
|
|
// Reset form
|
|
const form = document.getElementById('settingsForm');
|
|
if (form) {
|
|
form.reset();
|
|
// Reset password indicators
|
|
document.getElementById('settingsPasswordStrength').innerHTML = '';
|
|
document.getElementById('settingsPasswordMatch').innerHTML = '';
|
|
}
|
|
}
|
|
}
|
|
|
|
updatePasswordStrength(prefix) {
|
|
const passwordInput = document.getElementById(`${prefix}_new_password`);
|
|
const strengthDiv = document.getElementById(`${prefix}PasswordStrength`);
|
|
|
|
if (!passwordInput || !strengthDiv) return;
|
|
|
|
const password = passwordInput.value;
|
|
let strength = 0;
|
|
let feedback = '';
|
|
|
|
if (password.length >= 6) strength++;
|
|
if (password.length >= 8) strength++;
|
|
if (/[A-Z]/.test(password)) strength++;
|
|
if (/[0-9]/.test(password)) strength++;
|
|
if (/[^A-Za-z0-9]/.test(password)) strength++;
|
|
|
|
switch (strength) {
|
|
case 0:
|
|
case 1:
|
|
feedback = '<span style="color: #ef4444;">Weak</span>';
|
|
break;
|
|
case 2:
|
|
case 3:
|
|
feedback = '<span style="color: #f59e0b;">Medium</span>';
|
|
break;
|
|
case 4:
|
|
case 5:
|
|
feedback = '<span style="color: #10b981;">Strong</span>';
|
|
break;
|
|
}
|
|
|
|
strengthDiv.innerHTML = password.length > 0 ? 'Strength: ' + feedback : '';
|
|
}
|
|
|
|
checkPasswordMatch(prefix) {
|
|
const passwordInput = document.getElementById(`${prefix}_new_password`);
|
|
const confirmInput = document.getElementById(`${prefix}_confirm_password`);
|
|
const matchDiv = document.getElementById(`${prefix}PasswordMatch`);
|
|
|
|
if (!passwordInput || !confirmInput || !matchDiv) return;
|
|
|
|
const password = passwordInput.value;
|
|
const confirm = confirmInput.value;
|
|
|
|
if (confirm.length === 0) {
|
|
matchDiv.innerHTML = '';
|
|
return;
|
|
}
|
|
|
|
if (password === confirm) {
|
|
matchDiv.innerHTML = '<span style="color: #10b981;">✓ Passwords match</span>';
|
|
} else {
|
|
matchDiv.innerHTML = '<span style="color: #ef4444;">✗ Passwords do not match</span>';
|
|
}
|
|
}
|
|
|
|
async handleSettingsSubmit() {
|
|
const form = document.getElementById('settingsForm');
|
|
const submitBtn = form.querySelector('button[type="submit"]');
|
|
|
|
// Get form data
|
|
const currentPassword = document.getElementById('settings_current_password').value;
|
|
const newUsername = document.getElementById('settings_new_username').value;
|
|
const newPassword = document.getElementById('settings_new_password').value;
|
|
const confirmPassword = document.getElementById('settings_confirm_password').value;
|
|
|
|
// Validate passwords match
|
|
if (newPassword !== confirmPassword) {
|
|
this.showAlert('New passwords do not match', 'error');
|
|
return;
|
|
}
|
|
|
|
// Validate password length
|
|
if (newPassword.length < 6) {
|
|
this.showAlert('Password must be at least 6 characters long', 'error');
|
|
return;
|
|
}
|
|
|
|
// Update UI
|
|
submitBtn.disabled = true;
|
|
submitBtn.innerHTML = '<span class="spinner"></span>Saving...';
|
|
|
|
try {
|
|
const formData = new FormData();
|
|
formData.append('action', 'change_credentials');
|
|
formData.append('current_password', currentPassword);
|
|
formData.append('new_username', newUsername);
|
|
formData.append('new_password', newPassword);
|
|
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();
|
|
|
|
if (result.success) {
|
|
this.showAlert('Credentials updated successfully! You will be logged out in 3 seconds.', 'success');
|
|
this.hideSettingsModal();
|
|
|
|
// Redirect to login page after 3 seconds
|
|
setTimeout(() => {
|
|
window.location.href = 'index.php?message=credentials_changed';
|
|
}, 3000);
|
|
} else {
|
|
throw new Error(result.error || 'Failed to update credentials');
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('Settings update failed:', error);
|
|
this.showAlert(`Failed to update credentials: ${error.message}`, 'error');
|
|
} finally {
|
|
submitBtn.disabled = false;
|
|
submitBtn.innerHTML = '💾 Save Changes';
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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);
|