Files
overlord/Overlord-Server/public/assets/login.js
T

384 lines
11 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");
const oidcLogin = document.getElementById("oidc-login");
const oidcLoginLabel = document.getElementById("oidc-login-label");
const loginBrand = document.getElementById("login-brand");
const loginTitle = document.getElementById("login-title");
const loginSubtitle = document.getElementById("login-subtitle");
const loginIcon = document.getElementById("login-icon");
const loginLogo = document.getElementById("login-logo");
const loginHeroImage = document.getElementById("login-hero-image");
const loginBrandFooter = document.getElementById("login-brand-footer");
const loginSupportLink = document.getElementById("login-support-link");
(() => {
const params = new URLSearchParams(window.location.search);
const oidcError = params.get("oidc_error");
if (oidcError) {
errEl.textContent = oidcError;
window.history.replaceState({}, document.title, "/login.html");
}
})();
(async () => {
try {
const res = await fetch("/api/login/branding");
if (!res.ok) return;
const data = await res.json();
if (loginBrand && data.productName) loginBrand.textContent = data.productName;
if (loginTitle && data.title) loginTitle.textContent = data.title;
if (loginSubtitle && data.subtitle) loginSubtitle.textContent = data.subtitle;
document.title = data.tabName || (data.productName ? `${data.productName} Login` : document.title);
if (data.faviconUrl) {
let favicon = document.querySelector('link[rel~="icon"]');
if (!favicon) {
favicon = document.createElement("link");
favicon.rel = "icon";
document.head.appendChild(favicon);
}
favicon.href = data.faviconUrl;
}
if (data.accentColor) document.documentElement.style.setProperty("--brand-accent", data.accentColor);
if (loginIcon && data.iconClass) {
loginIcon.className = `${data.iconClass} login-crown`;
}
if (loginLogo && loginIcon && data.logoUrl) {
loginLogo.alt = data.logoAlt || `${data.productName || "Overlord"} logo`;
loginLogo.onload = () => {
loginLogo.style.display = "";
loginIcon.style.display = "none";
};
loginLogo.onerror = () => {
loginLogo.style.display = "none";
loginIcon.style.display = "";
};
loginLogo.src = data.logoUrl;
}
if (loginHeroImage && data.heroImageUrl) {
loginHeroImage.alt = data.heroImageAlt || "";
loginHeroImage.onload = () => {
loginHeroImage.style.display = "";
document.body.classList.add("login-has-hero-image");
};
loginHeroImage.onerror = () => {
loginHeroImage.style.display = "none";
document.body.classList.remove("login-has-hero-image");
};
loginHeroImage.src = data.heroImageUrl;
}
if (loginBrandFooter && data.footerText) {
loginBrandFooter.textContent = data.footerText;
loginBrandFooter.style.display = "";
}
if (loginSupportLink && data.supportText && data.supportUrl) {
loginSupportLink.textContent = data.supportText;
loginSupportLink.href = data.supportUrl;
if (!data.supportUrl.startsWith("/")) {
loginSupportLink.target = "_blank";
loginSupportLink.rel = "noopener noreferrer";
}
loginSupportLink.style.display = "";
}
} catch {}
})();
(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 {}
})();
(async () => {
try {
const res = await fetch("/api/oidc/status");
if (!res.ok) return;
const data = await res.json();
if (data.enabled && data.loginUrl && oidcLogin) {
oidcLogin.href = data.loginUrl;
if (oidcLoginLabel && data.label) oidcLoginLabel.textContent = data.label;
oidcLogin.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",
});
}
}
});