botui/ui/suite/auth/auth.js
Rodrigo Rodriguez (Pragmatismo) d8e52bf330 feat(auth): Add user profile loading and auth state management
- Add JavaScript to load user profile from /api/auth/me endpoint
- Save access_token to localStorage/sessionStorage on login
- Update user menu to show actual user name and email
- Toggle Sign in/Sign out based on authentication state
- Add IDs to user menu elements for dynamic updates
2026-01-06 22:57:00 -03:00

393 lines
11 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;
}
// Save token on successful login
if (response.access_token) {
const rememberMe = document.getElementById("remember");
if (rememberMe && rememberMe.checked) {
localStorage.setItem("access_token", response.access_token);
} else {
sessionStorage.setItem("access_token", response.access_token);
}
}
// 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();
}