Files
overlord/Overlord-Server/public/assets/login.js
T
2026-06-01 16:37:23 -05:00

284 lines
7.1 KiB
JavaScript

const form = document.getElementById("login-form");
const user = document.getElementById("user");
const pass = document.getElementById("pass");
const mfaGroup = document.getElementById("mfa-group");
const mfaCode = document.getElementById("mfa-code");
const errEl = document.getElementById("error");
(async () => {
try {
const res = await fetch("/api/registration/status");
const data = await res.json();
if (data.enabled) {
const el = document.getElementById("register-link");
if (el) el.style.display = "";
}
} catch {}
})();
if (typeof anime !== "undefined") {
const particles = document.querySelectorAll(".particle");
particles.forEach((particle, i) => {
anime({
targets: particle,
translateX: () => anime.random(-100, 100),
translateY: () => anime.random(-100, 100),
scale: () => anime.random(0.5, 1.5),
opacity: [0, 0.6, 0],
duration: () => anime.random(3000, 6000),
delay: i * 200,
easing: "easeInOutQuad",
loop: true,
});
});
anime
.timeline()
.add({
targets: ".login-card",
opacity: [0, 1],
translateY: [30, 0],
duration: 800,
easing: "easeOutExpo",
})
.add(
{
targets: ".login-crown-wrapper",
scale: [0, 1],
rotate: [180, 0],
duration: 600,
easing: "easeOutBack",
},
"-=400",
)
.add(
{
targets: ".login-brand",
opacity: [0, 1],
translateY: [-10, 0],
duration: 500,
easing: "easeOutQuad",
},
"-=300",
)
.add(
{
targets: ".login-title, .login-subtitle",
opacity: [0, 1],
translateY: [10, 0],
duration: 500,
delay: anime.stagger(100),
easing: "easeOutQuad",
},
"-=300",
)
.add(
{
targets: ".form-group",
opacity: [0, 1],
translateY: [15, 0],
duration: 500,
delay: anime.stagger(100),
easing: "easeOutQuad",
},
"-=300",
)
.add(
{
targets: ".login-btn",
opacity: [0, 1],
scale: [0.95, 1],
duration: 400,
easing: "easeOutQuad",
},
"-=200",
);
anime({
targets: ".login-crown",
scale: [1, 1.1, 1],
duration: 2000,
easing: "easeInOutQuad",
loop: true,
});
const loginBtn = document.querySelector(".login-btn");
if (loginBtn) {
const prefersReducedMotion = window.matchMedia(
"(prefers-reduced-motion: reduce)",
).matches;
let particleInterval = prefersReducedMotion ? 0 : 220;
let currentInterval;
function createParticle() {
const btnRect = loginBtn.getBoundingClientRect();
const particle = document.createElement("div");
particle.className = "login-btn-particle";
const edge = Math.random();
let startX, startY, moveX, moveY;
if (edge < 0.7) {
startX = btnRect.left + Math.random() * btnRect.width;
startY = btnRect.bottom;
moveX = anime.random(-20, 20);
moveY = anime.random(40, 80);
} else if (edge < 0.8) {
startX = btnRect.left;
startY = btnRect.top + Math.random() * btnRect.height;
moveX = anime.random(-50, -25);
moveY = anime.random(-15, 25);
} else if (edge < 0.9) {
startX = btnRect.right;
startY = btnRect.top + Math.random() * btnRect.height;
moveX = anime.random(25, 50);
moveY = anime.random(-15, 25);
} else {
startX = btnRect.left + Math.random() * btnRect.width;
startY = btnRect.top;
moveX = anime.random(-25, 25);
moveY = anime.random(-50, -25);
}
const size = anime.random(3, 7);
particle.style.width = size + "px";
particle.style.height = size + "px";
particle.style.left = startX + "px";
particle.style.top = startY + "px";
document.body.appendChild(particle);
anime({
targets: particle,
translateX: [0, moveX],
translateY: [0, moveY],
opacity: [0, 0.9, 0.7, 0],
scale: [0.5, 1, 0.8, 0.3],
rotate: anime.random(-180, 180),
duration: anime.random(1200, 2000),
easing: "easeOutQuad",
complete: () => particle.remove(),
});
}
function startParticles(interval) {
if (currentInterval) clearInterval(currentInterval);
if (interval > 0) {
currentInterval = setInterval(createParticle, interval);
}
}
startParticles(particleInterval);
loginBtn.addEventListener("mouseenter", () => {
if (!prefersReducedMotion) {
startParticles(120);
}
});
loginBtn.addEventListener("mouseleave", () => {
startParticles(particleInterval);
});
}
document.querySelectorAll(".input-animated input").forEach((input) => {
input.addEventListener("focus", (e) => {
anime({
targets: e.target.parentElement,
scale: [1, 1.02],
duration: 200,
easing: "easeOutQuad",
});
});
input.addEventListener("blur", (e) => {
anime({
targets: e.target.parentElement,
scale: [1.02, 1],
duration: 200,
easing: "easeOutQuad",
});
});
});
}
form?.addEventListener("submit", async (e) => {
e.preventDefault();
errEl.textContent = "";
const submitBtn = e.target.querySelector('button[type="submit"]');
if (typeof anime !== "undefined" && submitBtn) {
anime({
targets: submitBtn,
scale: [1, 0.95, 1],
duration: 300,
easing: "easeInOutQuad",
});
}
try {
const res = await fetch("/api/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
user: user.value,
pass: pass.value,
mfaCode: mfaCode?.value || "",
}),
});
if (res.status === 202) {
const data = await res.json().catch(() => ({}));
if (data.mfaRequired) {
if (mfaGroup) mfaGroup.style.display = "";
if (mfaCode) {
mfaCode.required = true;
mfaCode.focus();
}
errEl.textContent = "Enter your authenticator code to continue.";
return;
}
}
if (!res.ok) {
const data = await res.json();
throw new Error(data.error || "Invalid credentials");
}
const data = await res.json();
if (data.user && data.user.mustChangePassword) {
sessionStorage.setItem("temp_token", data.token);
sessionStorage.setItem("temp_user", JSON.stringify(data.user));
window.location.href = "/change-password.html";
} else {
window.location.href = "/";
}
} catch (err) {
errEl.textContent = err.message || "Login failed";
if (typeof anime !== "undefined") {
anime({
targets: ".login-card",
translateX: [
{ value: -10, duration: 100 },
{ value: 10, duration: 100 },
{ value: -10, duration: 100 },
{ value: 10, duration: 100 },
{ value: 0, duration: 100 },
],
easing: "easeInOutQuad",
});
anime({
targets: "#error",
opacity: [0, 1],
translateY: [-5, 0],
duration: 300,
easing: "easeOutQuad",
});
}
}
});