- Create individual CSS files: monitoring.css, alerts.css, health.css, logs.css, metrics.css, resources.css - Create individual JS files: monitoring.js, alerts.js, health.js, logs.js, metrics.js, resources.js - Update HTML files to reference external CSS/JS files - Add CSS/JS files for other modules (analytics, chat, mail, meet, tasks, etc.) - Remove obsolete implementation plan files
372 lines
12 KiB
JavaScript
372 lines
12 KiB
JavaScript
/* Auth Module JavaScript - Login, Register, Forgot Password, Reset Password */
|
|
|
|
/**
|
|
* Toggle password visibility
|
|
* @param {string} inputId - ID of the password input
|
|
* @param {string} eyeIconId - ID of the eye icon
|
|
* @param {string} eyeOffIconId - ID of the eye-off icon
|
|
*/
|
|
function togglePassword(inputId = 'password', eyeIconId = 'eye-icon', eyeOffIconId = 'eye-off-icon') {
|
|
const passwordInput = document.getElementById(inputId);
|
|
const eyeIcon = document.getElementById(eyeIconId);
|
|
const eyeOffIcon = document.getElementById(eyeOffIconId);
|
|
|
|
if (passwordInput.type === 'password') {
|
|
passwordInput.type = 'text';
|
|
if (eyeIcon) eyeIcon.style.display = 'none';
|
|
if (eyeOffIcon) eyeOffIcon.style.display = 'block';
|
|
} else {
|
|
passwordInput.type = 'password';
|
|
if (eyeIcon) eyeIcon.style.display = 'block';
|
|
if (eyeOffIcon) eyeOffIcon.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initiate OAuth login flow
|
|
* @param {string} provider - OAuth provider name (google, microsoft, github, apple)
|
|
*/
|
|
function oauthLogin(provider) {
|
|
window.location.href = `/api/auth/oauth/${provider}`;
|
|
}
|
|
|
|
/**
|
|
* Show 2FA challenge section
|
|
* @param {string} sessionToken - Session token for 2FA verification
|
|
*/
|
|
function showTwoFAChallenge(sessionToken) {
|
|
document.getElementById('login-section').style.display = 'none';
|
|
document.getElementById('twofa-section').classList.add('visible');
|
|
document.getElementById('session-token').value = sessionToken;
|
|
|
|
// Focus first code input
|
|
const firstInput = document.querySelector('.code-input[data-index="0"]');
|
|
if (firstInput) firstInput.focus();
|
|
}
|
|
|
|
/**
|
|
* Return to login section from 2FA
|
|
*/
|
|
function backToLogin() {
|
|
document.getElementById('login-section').style.display = 'block';
|
|
document.getElementById('twofa-section').classList.remove('visible');
|
|
|
|
// Clear code inputs
|
|
document.querySelectorAll('.code-input').forEach(input => {
|
|
input.value = '';
|
|
input.classList.remove('filled');
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Update the hidden full code field from individual inputs
|
|
*/
|
|
function updateFullCode() {
|
|
const codeInputs = document.querySelectorAll('.code-input');
|
|
const code = Array.from(codeInputs).map(input => input.value).join('');
|
|
const fullCodeInput = document.getElementById('full-code');
|
|
if (fullCodeInput) fullCodeInput.value = code;
|
|
}
|
|
|
|
/**
|
|
* Initialize 2FA code input handling
|
|
*/
|
|
function initCodeInputs() {
|
|
const codeInputs = document.querySelectorAll('.code-input');
|
|
|
|
codeInputs.forEach((input, index) => {
|
|
input.addEventListener('input', (e) => {
|
|
const value = e.target.value;
|
|
|
|
// Only allow numbers
|
|
e.target.value = value.replace(/[^0-9]/g, '');
|
|
|
|
if (e.target.value) {
|
|
e.target.classList.add('filled');
|
|
// Move to next input
|
|
if (index < codeInputs.length - 1) {
|
|
codeInputs[index + 1].focus();
|
|
}
|
|
} else {
|
|
e.target.classList.remove('filled');
|
|
}
|
|
|
|
updateFullCode();
|
|
});
|
|
|
|
input.addEventListener('keydown', (e) => {
|
|
// Handle backspace
|
|
if (e.key === 'Backspace' && !e.target.value && index > 0) {
|
|
codeInputs[index - 1].focus();
|
|
}
|
|
|
|
// Handle paste
|
|
if (e.key === 'v' && (e.ctrlKey || e.metaKey)) {
|
|
e.preventDefault();
|
|
navigator.clipboard.readText().then(text => {
|
|
const code = text.replace(/[^0-9]/g, '').slice(0, 6);
|
|
code.split('').forEach((char, i) => {
|
|
if (codeInputs[i]) {
|
|
codeInputs[i].value = char;
|
|
codeInputs[i].classList.add('filled');
|
|
}
|
|
});
|
|
updateFullCode();
|
|
if (code.length === 6) {
|
|
codeInputs[5].focus();
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
// Handle paste directly on input
|
|
input.addEventListener('paste', (e) => {
|
|
e.preventDefault();
|
|
const text = e.clipboardData.getData('text');
|
|
const code = text.replace(/[^0-9]/g, '').slice(0, 6);
|
|
code.split('').forEach((char, i) => {
|
|
if (codeInputs[i]) {
|
|
codeInputs[i].value = char;
|
|
codeInputs[i].classList.add('filled');
|
|
}
|
|
});
|
|
updateFullCode();
|
|
if (code.length === 6) {
|
|
codeInputs[5].focus();
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Resend 2FA code with cooldown
|
|
*/
|
|
let resendCooldown = 0;
|
|
function resendCode() {
|
|
if (resendCooldown > 0) return;
|
|
|
|
const sessionToken = document.getElementById('session-token').value;
|
|
fetch('/api/auth/2fa/resend', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ session_token: sessionToken })
|
|
});
|
|
|
|
// Start cooldown
|
|
resendCooldown = 60;
|
|
const resendBtn = document.getElementById('resend-btn');
|
|
resendBtn.disabled = true;
|
|
|
|
const interval = setInterval(() => {
|
|
resendCooldown--;
|
|
resendBtn.textContent = `Resend code (${resendCooldown}s)`;
|
|
|
|
if (resendCooldown <= 0) {
|
|
clearInterval(interval);
|
|
resendBtn.textContent = 'Resend code';
|
|
resendBtn.disabled = false;
|
|
}
|
|
}, 1000);
|
|
}
|
|
|
|
/**
|
|
* Show error message
|
|
* @param {string} message - Error message to display
|
|
*/
|
|
function showError(message) {
|
|
const errorBox = document.getElementById('error-message');
|
|
const errorText = document.getElementById('error-text');
|
|
if (errorText) errorText.textContent = message;
|
|
if (errorBox) errorBox.classList.add('visible');
|
|
}
|
|
|
|
/**
|
|
* Hide error message
|
|
*/
|
|
function hideError() {
|
|
const errorBox = document.getElementById('error-message');
|
|
if (errorBox) errorBox.classList.remove('visible');
|
|
}
|
|
|
|
/**
|
|
* Show success message
|
|
* @param {string} message - Success message to display
|
|
*/
|
|
function showSuccess(message) {
|
|
const successBox = document.getElementById('success-message');
|
|
const successText = document.getElementById('success-text');
|
|
if (successText) successText.textContent = message;
|
|
if (successBox) successBox.classList.add('visible');
|
|
}
|
|
|
|
/**
|
|
* Set loading state on a button
|
|
* @param {string} btnId - Button ID
|
|
* @param {boolean} loading - Loading state
|
|
*/
|
|
function setLoading(btnId, loading) {
|
|
const btn = document.getElementById(btnId);
|
|
if (!btn) return;
|
|
|
|
if (loading) {
|
|
btn.classList.add('loading');
|
|
btn.disabled = true;
|
|
} else {
|
|
btn.classList.remove('loading');
|
|
btn.disabled = false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check password strength
|
|
* @param {string} password - Password to check
|
|
* @returns {object} - Strength level and requirements met
|
|
*/
|
|
function checkPasswordStrength(password) {
|
|
const requirements = {
|
|
length: password.length >= 8,
|
|
lowercase: /[a-z]/.test(password),
|
|
uppercase: /[A-Z]/.test(password),
|
|
number: /[0-9]/.test(password),
|
|
special: /[!@#$%^&*(),.?":{}|<>]/.test(password)
|
|
};
|
|
|
|
const metCount = Object.values(requirements).filter(Boolean).length;
|
|
|
|
let strength = 'weak';
|
|
if (metCount >= 5) strength = 'strong';
|
|
else if (metCount >= 4) strength = 'good';
|
|
else if (metCount >= 3) strength = 'fair';
|
|
|
|
return { strength, requirements, metCount };
|
|
}
|
|
|
|
/**
|
|
* Update password strength indicator
|
|
* @param {string} password - Password to check
|
|
*/
|
|
function updatePasswordStrength(password) {
|
|
const { strength, requirements } = checkPasswordStrength(password);
|
|
|
|
const strengthFill = document.querySelector('.strength-fill');
|
|
const strengthText = document.querySelector('.strength-text');
|
|
|
|
if (strengthFill) {
|
|
strengthFill.className = 'strength-fill ' + strength;
|
|
}
|
|
|
|
if (strengthText) {
|
|
const labels = { weak: 'Weak', fair: 'Fair', good: 'Good', strong: 'Strong' };
|
|
strengthText.textContent = labels[strength];
|
|
}
|
|
|
|
// Update requirement indicators
|
|
Object.entries(requirements).forEach(([key, met]) => {
|
|
const reqEl = document.querySelector(`.requirement[data-req="${key}"]`);
|
|
if (reqEl) {
|
|
reqEl.classList.toggle('met', met);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Initialize password strength checker
|
|
*/
|
|
function initPasswordStrength() {
|
|
const passwordInput = document.getElementById('password');
|
|
if (passwordInput) {
|
|
passwordInput.addEventListener('input', (e) => {
|
|
updatePasswordStrength(e.target.value);
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle HTMX events for auth forms
|
|
*/
|
|
function initHtmxHandlers() {
|
|
document.body.addEventListener('htmx:beforeRequest', function(event) {
|
|
hideError();
|
|
if (event.target.id === 'login-form') {
|
|
setLoading('login-btn', true);
|
|
} else if (event.target.id === 'twofa-form') {
|
|
setLoading('verify-btn', true);
|
|
} else if (event.target.id === 'register-form') {
|
|
setLoading('register-btn', true);
|
|
} else if (event.target.id === 'forgot-form') {
|
|
setLoading('forgot-btn', true);
|
|
} else if (event.target.id === 'reset-form') {
|
|
setLoading('reset-btn', true);
|
|
}
|
|
});
|
|
|
|
document.body.addEventListener('htmx:afterRequest', function(event) {
|
|
if (event.target.id === 'login-form') {
|
|
setLoading('login-btn', false);
|
|
} else if (event.target.id === 'twofa-form') {
|
|
setLoading('verify-btn', false);
|
|
} else if (event.target.id === 'register-form') {
|
|
setLoading('register-btn', false);
|
|
} else if (event.target.id === 'forgot-form') {
|
|
setLoading('forgot-btn', false);
|
|
} else if (event.target.id === 'reset-form') {
|
|
setLoading('reset-btn', false);
|
|
}
|
|
|
|
if (event.detail.successful) {
|
|
try {
|
|
const response = JSON.parse(event.detail.xhr.responseText);
|
|
|
|
// Check if 2FA is required
|
|
if (response.requires_2fa) {
|
|
showTwoFAChallenge(response.session_token);
|
|
return;
|
|
}
|
|
|
|
// Successful login/register - redirect
|
|
if (response.redirect || response.success) {
|
|
window.location.href = response.redirect || '/';
|
|
}
|
|
|
|
// Show success message
|
|
if (response.message) {
|
|
showSuccess(response.message);
|
|
}
|
|
} catch (e) {
|
|
// If response is not JSON, check for redirect header
|
|
if (event.detail.xhr.status === 200) {
|
|
window.location.href = '/';
|
|
}
|
|
}
|
|
} else {
|
|
// Show error
|
|
try {
|
|
const response = JSON.parse(event.detail.xhr.responseText);
|
|
showError(response.error || 'An error occurred. Please try again.');
|
|
} catch (e) {
|
|
showError('An error occurred. Please try again.');
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Initialize auth module
|
|
*/
|
|
function initAuth() {
|
|
initCodeInputs();
|
|
initPasswordStrength();
|
|
initHtmxHandlers();
|
|
|
|
// Clear error when user starts typing
|
|
document.querySelectorAll('.form-input').forEach(input => {
|
|
input.addEventListener('input', hideError);
|
|
});
|
|
}
|
|
|
|
// Auto-initialize when DOM is ready
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', initAuth);
|
|
} else {
|
|
initAuth();
|
|
}
|