diff --git a/Overlord-Server/public/assets/login.js b/Overlord-Server/public/assets/login.js index 74f7a27..8f8a557 100644 --- a/Overlord-Server/public/assets/login.js +++ b/Overlord-Server/public/assets/login.js @@ -3,6 +3,17 @@ const user = document.getElementById("user"); const pass = document.getElementById("pass"); 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) => { diff --git a/Overlord-Server/public/assets/register.js b/Overlord-Server/public/assets/register.js new file mode 100644 index 0000000..9e525b3 --- /dev/null +++ b/Overlord-Server/public/assets/register.js @@ -0,0 +1,123 @@ +const form = document.getElementById("register-form"); +const errorEl = document.getElementById("error"); +const successEl = document.getElementById("success"); +const keyGroup = document.getElementById("key-group"); +const keyInput = document.getElementById("reg-key"); + +async function checkStatus() { + try { + const res = await fetch("/api/registration/status"); + const data = await res.json(); + if (!data.enabled) { + form.style.display = "none"; + errorEl.textContent = "Registration is currently disabled."; + return; + } + if (data.mode === "key") { + keyGroup.style.display = ""; + keyInput.required = true; + } + } catch { + errorEl.textContent = "Unable to check registration status."; + } +} +checkStatus(); + +if (window.anime) { + document.querySelectorAll(".particle").forEach((p, i) => { + anime({ + targets: p, + translateX: () => anime.random(-300, 300), + translateY: () => anime.random(-300, 300), + scale: () => anime.random(5, 15) / 10, + opacity: [{ value: 0.15, duration: 600 }, { value: 0, duration: 600 }], + easing: "easeInOutQuad", + duration: 3000, + delay: i * 400, + loop: true, + direction: "alternate", + }); + }); + + 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, .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"); +} else { + document.querySelector(".login-card").style.opacity = "1"; +} + +form.addEventListener("submit", async (e) => { + e.preventDefault(); + errorEl.textContent = ""; + successEl.style.display = "none"; + + const username = document.getElementById("reg-user").value.trim(); + const password = document.getElementById("reg-pass").value; + const confirmPassword = document.getElementById("reg-pass-confirm").value; + const key = keyInput.value.trim(); + + if (password !== confirmPassword) { + errorEl.textContent = "Passwords do not match."; + return; + } + if (password.length < 6) { + errorEl.textContent = "Password must be at least 6 characters."; + return; + } + + try { + const res = await fetch("/api/register", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ username, password, key: key || undefined }), + }); + const data = await res.json(); + if (!res.ok) { + errorEl.textContent = data.error || "Registration failed."; + return; + } + form.style.display = "none"; + successEl.style.display = ""; + successEl.textContent = data.message || "Account created!"; + if (!data.pending) { + setTimeout(() => { window.location.href = "/"; }, 2000); + } + } catch { + errorEl.textContent = "Network error. Please try again."; + } +}); diff --git a/Overlord-Server/public/assets/settings.js b/Overlord-Server/public/assets/settings.js index e7438b2..bf73127 100644 --- a/Overlord-Server/public/assets/settings.js +++ b/Overlord-Server/public/assets/settings.js @@ -1057,6 +1057,276 @@ async function handleRemoveInactiveSessions() { } } +// ── Registration Settings ────────────────────────────────────────────────── + +async function loadRegistrationSettings() { + try { + const res = await fetch("/api/settings/registration", { credentials: "include" }); + if (!res.ok) return; + const data = await res.json(); + const reg = data.registration || {}; + const modeEl = document.getElementById("reg-mode"); + const roleEl = document.getElementById("reg-default-role"); + const maxEl = document.getElementById("reg-max-users"); + if (modeEl) modeEl.value = reg.mode || "off"; + if (roleEl) roleEl.value = reg.defaultRole || "operator"; + if (maxEl) maxEl.value = reg.maxUsersTotal ?? 0; + updateRegSubsections(reg.mode || "off"); + if (reg.mode === "key") loadRegistrationKeys(); + if (reg.mode === "approval") loadPendingRegistrations(); + } catch (e) { + console.error("Failed to load registration settings", e); + } +} + +function updateRegSubsections(mode) { + const keySection = document.getElementById("reg-key-section"); + const pendingSection = document.getElementById("reg-pending-section"); + if (keySection) keySection.classList.toggle("hidden", mode !== "key"); + if (pendingSection) pendingSection.classList.toggle("hidden", mode !== "approval"); +} + +function showRegMsg(text, type) { + const el = document.getElementById("reg-settings-msg"); + if (!el) return; + el.textContent = text; + el.className = `text-sm rounded-lg px-3 py-2 border ${type === "error" ? "border-rose-800 bg-rose-900/20 text-rose-200" : "border-emerald-800 bg-emerald-900/20 text-emerald-200"}`; + el.classList.remove("hidden"); + setTimeout(() => el.classList.add("hidden"), 5000); +} + +async function saveRegistrationSettings(e) { + e.preventDefault(); + const mode = document.getElementById("reg-mode")?.value; + const defaultRole = document.getElementById("reg-default-role")?.value; + const maxUsersTotal = Number(document.getElementById("reg-max-users")?.value) || 0; + + try { + const res = await fetch("/api/settings/registration", { + method: "PUT", + credentials: "include", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ mode, defaultRole, maxUsersTotal }), + }); + if (!res.ok) { + const data = await res.json().catch(() => ({})); + showRegMsg(data.error || "Failed to save", "error"); + return; + } + showRegMsg("Registration settings saved.", "success"); + updateRegSubsections(mode); + if (mode === "key") loadRegistrationKeys(); + if (mode === "approval") loadPendingRegistrations(); + } catch { + showRegMsg("Network error.", "error"); + } +} + +async function loadRegistrationKeys() { + const tbody = document.getElementById("reg-keys-tbody"); + if (!tbody) return; + try { + const res = await fetch("/api/registration/keys", { credentials: "include" }); + if (!res.ok) return; + const data = await res.json(); + const keys = data.keys || []; + if (keys.length === 0) { + tbody.innerHTML = '