botui/ui/suite/auth/auth.js

373 lines
12 KiB
JavaScript
Raw Normal View History

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