add registration
This commit is contained in:
@@ -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) => {
|
||||
|
||||
@@ -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.";
|
||||
}
|
||||
});
|
||||
@@ -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 = '<tr><td colspan="6" class="px-3 py-4 text-center text-slate-500">No keys generated yet.</td></tr>';
|
||||
return;
|
||||
}
|
||||
tbody.innerHTML = keys.map(k => {
|
||||
const status = k.used_by ? '<span class="text-amber-400">Used</span>'
|
||||
: (k.expires_at && k.expires_at < Date.now()) ? '<span class="text-slate-500">Expired</span>'
|
||||
: '<span class="text-emerald-400">Available</span>';
|
||||
const created = new Date(k.created_at).toLocaleDateString();
|
||||
const expires = k.expires_at ? new Date(k.expires_at).toLocaleDateString() : "Never";
|
||||
return `<tr class="border-b border-slate-800">
|
||||
<td class="px-3 py-2 font-mono text-xs">${escapeHtml(k.key.substring(0, 12))}...</td>
|
||||
<td class="px-3 py-2">${escapeHtml(k.label || "—")}</td>
|
||||
<td class="px-3 py-2">${status}</td>
|
||||
<td class="px-3 py-2">${created}</td>
|
||||
<td class="px-3 py-2">${expires}</td>
|
||||
<td class="px-3 py-2">${!k.used_by ? `<button data-delete-key="${k.id}" class="text-rose-400 hover:text-rose-300 text-xs"><i class="fa-solid fa-trash"></i></button>` : ""}</td>
|
||||
</tr>`;
|
||||
}).join("");
|
||||
} catch (e) {
|
||||
console.error("Failed to load keys", e);
|
||||
}
|
||||
}
|
||||
|
||||
async function generateRegistrationKeys() {
|
||||
const count = Number(document.getElementById("reg-key-count")?.value) || 1;
|
||||
const label = document.getElementById("reg-key-label")?.value || undefined;
|
||||
const expiresInHours = Number(document.getElementById("reg-key-expires")?.value) || undefined;
|
||||
|
||||
try {
|
||||
const res = await fetch("/api/registration/keys", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ count, label, expiresInHours }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
showRegMsg("Failed to generate keys", "error");
|
||||
return;
|
||||
}
|
||||
showRegMsg(`Generated ${count} key(s).`, "success");
|
||||
loadRegistrationKeys();
|
||||
} catch {
|
||||
showRegMsg("Network error.", "error");
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteRegistrationKey(keyId) {
|
||||
try {
|
||||
const res = await fetch(`/api/registration/keys/${keyId}`, {
|
||||
method: "DELETE",
|
||||
credentials: "include",
|
||||
});
|
||||
if (res.ok) loadRegistrationKeys();
|
||||
} catch {}
|
||||
}
|
||||
|
||||
async function loadPendingRegistrations() {
|
||||
const tbody = document.getElementById("reg-pending-tbody");
|
||||
const emptyEl = document.getElementById("reg-pending-empty");
|
||||
if (!tbody) return;
|
||||
try {
|
||||
const res = await fetch("/api/registration/pending", { credentials: "include" });
|
||||
if (!res.ok) return;
|
||||
const data = await res.json();
|
||||
const pending = (data.pending || []).filter(p => p.status === "pending");
|
||||
if (pending.length === 0) {
|
||||
tbody.innerHTML = "";
|
||||
if (emptyEl) emptyEl.classList.remove("hidden");
|
||||
return;
|
||||
}
|
||||
if (emptyEl) emptyEl.classList.add("hidden");
|
||||
tbody.innerHTML = pending.map(p => {
|
||||
const requested = new Date(p.requested_at).toLocaleString();
|
||||
return `<tr class="border-b border-slate-800">
|
||||
<td class="px-3 py-2">${escapeHtml(p.username)}</td>
|
||||
<td class="px-3 py-2">${requested}</td>
|
||||
<td class="px-3 py-2"><span class="text-amber-400">Pending</span></td>
|
||||
<td class="px-3 py-2 space-x-2">
|
||||
<button data-approve-pending="${p.id}" class="text-emerald-400 hover:text-emerald-300 text-xs"><i class="fa-solid fa-check"></i> Approve</button>
|
||||
<button data-deny-pending="${p.id}" class="text-rose-400 hover:text-rose-300 text-xs"><i class="fa-solid fa-xmark"></i> Deny</button>
|
||||
</td>
|
||||
</tr>`;
|
||||
}).join("");
|
||||
} catch (e) {
|
||||
console.error("Failed to load pending registrations", e);
|
||||
}
|
||||
}
|
||||
|
||||
async function handlePendingAction(id, action) {
|
||||
try {
|
||||
const res = await fetch(`/api/registration/pending/${id}/${action}`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
});
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => ({}));
|
||||
showRegMsg(data.error || `Failed to ${action}`, "error");
|
||||
return;
|
||||
}
|
||||
showRegMsg(`Registration ${action}d.`, "success");
|
||||
loadPendingRegistrations();
|
||||
} catch {
|
||||
showRegMsg("Network error.", "error");
|
||||
}
|
||||
}
|
||||
|
||||
function initRegistrationHandlers() {
|
||||
const regForm = document.getElementById("registration-form");
|
||||
if (regForm) regForm.addEventListener("submit", saveRegistrationSettings);
|
||||
|
||||
const modeEl = document.getElementById("reg-mode");
|
||||
if (modeEl) modeEl.addEventListener("change", () => {
|
||||
updateRegSubsections(modeEl.value);
|
||||
if (modeEl.value === "key") loadRegistrationKeys();
|
||||
if (modeEl.value === "approval") loadPendingRegistrations();
|
||||
});
|
||||
|
||||
const genBtn = document.getElementById("reg-key-generate-btn");
|
||||
if (genBtn) genBtn.addEventListener("click", generateRegistrationKeys);
|
||||
|
||||
// Delegate key delete and pending approve/deny
|
||||
document.addEventListener("click", (e) => {
|
||||
const deleteBtn = e.target.closest("[data-delete-key]");
|
||||
if (deleteBtn) {
|
||||
deleteRegistrationKey(Number(deleteBtn.dataset.deleteKey));
|
||||
return;
|
||||
}
|
||||
const approveBtn = e.target.closest("[data-approve-pending]");
|
||||
if (approveBtn) {
|
||||
handlePendingAction(Number(approveBtn.dataset.approvePending), "approve");
|
||||
return;
|
||||
}
|
||||
const denyBtn = e.target.closest("[data-deny-pending]");
|
||||
if (denyBtn) {
|
||||
handlePendingAction(Number(denyBtn.dataset.denyPending), "deny");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ── Build Rate Limit Settings ──────────────────────────────────────────────
|
||||
|
||||
async function loadBuildRateLimitSettings() {
|
||||
try {
|
||||
const res = await fetch("/api/settings/build-rate-limit", { credentials: "include" });
|
||||
if (!res.ok) return;
|
||||
const data = await res.json();
|
||||
const brl = data.buildRateLimit || {};
|
||||
const perHourEl = document.getElementById("brl-max-per-hour");
|
||||
const concUserEl = document.getElementById("brl-max-concurrent-user");
|
||||
const globalConcEl = document.getElementById("brl-global-concurrent");
|
||||
if (perHourEl) perHourEl.value = brl.maxBuildsPerHour ?? 5;
|
||||
if (concUserEl) concUserEl.value = brl.maxConcurrentPerUser ?? 1;
|
||||
if (globalConcEl) globalConcEl.value = brl.globalMaxConcurrent ?? 3;
|
||||
} catch (e) {
|
||||
console.error("Failed to load build rate limit settings", e);
|
||||
}
|
||||
}
|
||||
|
||||
function showBrlMsg(text, type) {
|
||||
const el = document.getElementById("brl-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 saveBuildRateLimitSettings(e) {
|
||||
e.preventDefault();
|
||||
const maxBuildsPerHour = Number(document.getElementById("brl-max-per-hour")?.value) || 5;
|
||||
const maxConcurrentPerUser = Number(document.getElementById("brl-max-concurrent-user")?.value) || 1;
|
||||
const globalMaxConcurrent = Number(document.getElementById("brl-global-concurrent")?.value) || 3;
|
||||
|
||||
try {
|
||||
const res = await fetch("/api/settings/build-rate-limit", {
|
||||
method: "PUT",
|
||||
credentials: "include",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ maxBuildsPerHour, maxConcurrentPerUser, globalMaxConcurrent }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => ({}));
|
||||
showBrlMsg(data.error || "Failed to save", "error");
|
||||
return;
|
||||
}
|
||||
showBrlMsg("Build rate limit settings saved.", "success");
|
||||
} catch {
|
||||
showBrlMsg("Network error.", "error");
|
||||
}
|
||||
}
|
||||
|
||||
function initBuildRateLimitHandlers() {
|
||||
const brlForm = document.getElementById("build-rate-limit-form");
|
||||
if (brlForm) brlForm.addEventListener("submit", saveBuildRateLimitSettings);
|
||||
}
|
||||
|
||||
async function init() {
|
||||
try {
|
||||
await loadCurrentUser();
|
||||
@@ -1076,6 +1346,18 @@ async function init() {
|
||||
await loadChatSettings();
|
||||
await loadBannedIps();
|
||||
|
||||
// Registration + Build Rate Limit (admin only)
|
||||
if (isAdmin(currentUser?.role)) {
|
||||
const regSection = document.getElementById("registration-section");
|
||||
const brlSection = document.getElementById("build-rate-limit-section");
|
||||
if (regSection) regSection.classList.remove("hidden");
|
||||
if (brlSection) brlSection.classList.remove("hidden");
|
||||
await loadRegistrationSettings();
|
||||
await loadBuildRateLimitSettings();
|
||||
initRegistrationHandlers();
|
||||
initBuildRateLimitHandlers();
|
||||
}
|
||||
|
||||
passwordForm.addEventListener("submit", updatePassword);
|
||||
prefsForm.addEventListener("submit", savePrefs);
|
||||
|
||||
|
||||
@@ -65,7 +65,12 @@
|
||||
</button>
|
||||
<div id="error" class="error" aria-live="polite"></div>
|
||||
</form>
|
||||
<div id="register-link" style="text-align: center; margin-top: 1rem; display: none">
|
||||
<a href="/register.html" class="muted" style="font-size: 0.85rem; text-decoration: underline"
|
||||
>Don't have an account? Register</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<script type="module" src="/assets/login.js"></script>
|
||||
<script src="/assets/login.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Overlord - Register</title>
|
||||
<link rel="icon" type="image/x-icon" href="/assets/favicon.ico" />
|
||||
<link rel="stylesheet" href="/vendor/inter/400.css" />
|
||||
<link rel="stylesheet" href="/vendor/inter/600.css" />
|
||||
<link rel="stylesheet" href="/vendor/inter/700.css" />
|
||||
<link rel="stylesheet" href="/vendor/fontawesome/css/all.min.css" />
|
||||
<link rel="stylesheet" href="/assets/main.css" />
|
||||
<link rel="stylesheet" href="/assets/custom.css" />
|
||||
<script src="/vendor/animejs/anime.min.js"></script>
|
||||
</head>
|
||||
<body class="login-body">
|
||||
<div class="login-particles">
|
||||
<div class="particle"></div>
|
||||
<div class="particle"></div>
|
||||
<div class="particle"></div>
|
||||
<div class="particle"></div>
|
||||
<div class="particle"></div>
|
||||
</div>
|
||||
<div class="login-card">
|
||||
<div class="login-crown-wrapper">
|
||||
<i class="fa-solid fa-crown login-crown"></i>
|
||||
</div>
|
||||
<div class="brand login-brand">Overlord</div>
|
||||
<h2 class="login-title">Create Account</h2>
|
||||
<p class="muted login-subtitle">Register for access</p>
|
||||
<form id="register-form">
|
||||
<div class="form-group">
|
||||
<label class="label">Username</label>
|
||||
<div class="input-row input-animated">
|
||||
<i class="fa-solid fa-user"></i
|
||||
><input
|
||||
id="reg-user"
|
||||
name="user"
|
||||
type="text"
|
||||
autocomplete="username"
|
||||
required
|
||||
minlength="3"
|
||||
maxlength="32"
|
||||
pattern="[a-zA-Z0-9_\-]+"
|
||||
placeholder="Choose a username"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="label">Password</label>
|
||||
<div class="input-row input-animated">
|
||||
<i class="fa-solid fa-lock"></i
|
||||
><input
|
||||
id="reg-pass"
|
||||
name="pass"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
required
|
||||
minlength="6"
|
||||
placeholder="Choose a password"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="label">Confirm Password</label>
|
||||
<div class="input-row input-animated">
|
||||
<i class="fa-solid fa-lock"></i
|
||||
><input
|
||||
id="reg-pass-confirm"
|
||||
name="pass-confirm"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
required
|
||||
placeholder="Confirm password"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" id="key-group" style="display: none">
|
||||
<label class="label">Registration Key</label>
|
||||
<div class="input-row input-animated">
|
||||
<i class="fa-solid fa-key"></i
|
||||
><input
|
||||
id="reg-key"
|
||||
name="key"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
placeholder="Enter your registration key"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="button primary full login-btn">
|
||||
<span class="btn-text">Create Account</span>
|
||||
<i class="fa-solid fa-user-plus btn-icon"></i>
|
||||
</button>
|
||||
<div id="error" class="error" aria-live="polite"></div>
|
||||
<div id="success" class="success" aria-live="polite" style="display: none"></div>
|
||||
</form>
|
||||
<div style="text-align: center; margin-top: 1rem">
|
||||
<a href="/" class="muted" style="font-size: 0.85rem; text-decoration: underline"
|
||||
>Already have an account? Sign in</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/assets/register.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -664,6 +664,141 @@
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<!-- Registration Settings (admin only) -->
|
||||
<section id="registration-section" class="hidden bg-slate-900/60 border border-slate-800 rounded-xl p-5 space-y-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex items-center justify-center w-9 h-9 rounded-lg bg-emerald-500/10 border border-emerald-500/30">
|
||||
<i class="fa-solid fa-user-plus text-emerald-400"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold">Registration</h2>
|
||||
<p class="text-sm text-slate-400">Admin-only. Control how new users can register.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form id="registration-form" class="space-y-4">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||
<label class="block">
|
||||
<span class="text-sm text-slate-300">Registration Mode</span>
|
||||
<select id="reg-mode" class="mt-1 w-full px-3 py-2 rounded-lg bg-slate-900 border border-slate-700 focus:outline-none focus:ring-2 focus:ring-sky-500">
|
||||
<option value="off">Off</option>
|
||||
<option value="open">Open</option>
|
||||
<option value="key">Key Required</option>
|
||||
<option value="approval">Approval Required</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="block">
|
||||
<span class="text-sm text-slate-300">Default Role</span>
|
||||
<select id="reg-default-role" class="mt-1 w-full px-3 py-2 rounded-lg bg-slate-900 border border-slate-700 focus:outline-none focus:ring-2 focus:ring-sky-500">
|
||||
<option value="operator">Operator</option>
|
||||
<option value="viewer">Viewer</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="block">
|
||||
<span class="text-sm text-slate-300">Max Users (0 = unlimited)</span>
|
||||
<input id="reg-max-users" type="number" min="0" max="10000" class="mt-1 w-full px-3 py-2 rounded-lg bg-slate-900 border border-slate-700 focus:outline-none focus:ring-2 focus:ring-sky-500" />
|
||||
</label>
|
||||
</div>
|
||||
<button type="submit" class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-emerald-700 hover:bg-emerald-600 transition-colors text-white text-sm font-medium">
|
||||
<i class="fa-solid fa-floppy-disk"></i>Save Registration Settings
|
||||
</button>
|
||||
</form>
|
||||
<p id="reg-settings-msg" class="hidden text-sm rounded-lg px-3 py-2 border"></p>
|
||||
|
||||
<!-- Key Management (visible when mode=key) -->
|
||||
<div id="reg-key-section" class="hidden space-y-3 border-t border-slate-700 pt-4">
|
||||
<h3 class="text-md font-semibold text-slate-200">Registration Keys</h3>
|
||||
<div class="flex flex-wrap items-end gap-3">
|
||||
<label class="block">
|
||||
<span class="text-sm text-slate-300">Count</span>
|
||||
<input id="reg-key-count" type="number" value="1" min="1" max="100" class="mt-1 w-24 px-3 py-2 rounded-lg bg-slate-900 border border-slate-700 focus:outline-none focus:ring-2 focus:ring-sky-500" />
|
||||
</label>
|
||||
<label class="block">
|
||||
<span class="text-sm text-slate-300">Label (optional)</span>
|
||||
<input id="reg-key-label" type="text" maxlength="64" placeholder="e.g. Batch 1" class="mt-1 w-40 px-3 py-2 rounded-lg bg-slate-900 border border-slate-700 focus:outline-none focus:ring-2 focus:ring-sky-500" />
|
||||
</label>
|
||||
<label class="block">
|
||||
<span class="text-sm text-slate-300">Expires (hours, 0=never)</span>
|
||||
<input id="reg-key-expires" type="number" value="0" min="0" max="8760" class="mt-1 w-28 px-3 py-2 rounded-lg bg-slate-900 border border-slate-700 focus:outline-none focus:ring-2 focus:ring-sky-500" />
|
||||
</label>
|
||||
<button id="reg-key-generate-btn" type="button" class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-emerald-700 hover:bg-emerald-600 transition-colors text-white text-sm font-medium">
|
||||
<i class="fa-solid fa-plus"></i>Generate
|
||||
</button>
|
||||
<a id="reg-key-export-btn" href="/api/registration/keys/export" class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-slate-700 hover:bg-slate-600 transition-colors text-white text-sm font-medium">
|
||||
<i class="fa-solid fa-file-export"></i>Export
|
||||
</a>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm text-left text-slate-300">
|
||||
<thead class="text-xs uppercase text-slate-500 border-b border-slate-700">
|
||||
<tr>
|
||||
<th class="px-3 py-2">Key</th>
|
||||
<th class="px-3 py-2">Label</th>
|
||||
<th class="px-3 py-2">Status</th>
|
||||
<th class="px-3 py-2">Created</th>
|
||||
<th class="px-3 py-2">Expires</th>
|
||||
<th class="px-3 py-2"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="reg-keys-tbody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pending Registrations (visible when mode=approval) -->
|
||||
<div id="reg-pending-section" class="hidden space-y-3 border-t border-slate-700 pt-4">
|
||||
<h3 class="text-md font-semibold text-slate-200">Pending Registrations</h3>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm text-left text-slate-300">
|
||||
<thead class="text-xs uppercase text-slate-500 border-b border-slate-700">
|
||||
<tr>
|
||||
<th class="px-3 py-2">Username</th>
|
||||
<th class="px-3 py-2">Requested</th>
|
||||
<th class="px-3 py-2">Status</th>
|
||||
<th class="px-3 py-2">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="reg-pending-tbody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p id="reg-pending-empty" class="text-sm text-slate-500">No pending registrations.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Build Rate Limit (admin only) -->
|
||||
<section id="build-rate-limit-section" class="hidden bg-slate-900/60 border border-slate-800 rounded-xl p-5 space-y-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex items-center justify-center w-9 h-9 rounded-lg bg-orange-500/10 border border-orange-500/30">
|
||||
<i class="fa-solid fa-gauge-high text-orange-400"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold">Build Rate Limit</h2>
|
||||
<p class="text-sm text-slate-400">Admin-only. Prevent abuse by limiting build frequency.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form id="build-rate-limit-form" class="space-y-4">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
||||
<label class="block">
|
||||
<span class="text-sm text-slate-300">Max Builds per Hour (per user)</span>
|
||||
<input id="brl-max-per-hour" type="number" min="1" max="100" class="mt-1 w-full px-3 py-2 rounded-lg bg-slate-900 border border-slate-700 focus:outline-none focus:ring-2 focus:ring-sky-500" />
|
||||
</label>
|
||||
<label class="block">
|
||||
<span class="text-sm text-slate-300">Max Concurrent (per user)</span>
|
||||
<input id="brl-max-concurrent-user" type="number" min="1" max="10" class="mt-1 w-full px-3 py-2 rounded-lg bg-slate-900 border border-slate-700 focus:outline-none focus:ring-2 focus:ring-sky-500" />
|
||||
</label>
|
||||
<label class="block">
|
||||
<span class="text-sm text-slate-300">Global Max Concurrent</span>
|
||||
<input id="brl-global-concurrent" type="number" min="1" max="50" class="mt-1 w-full px-3 py-2 rounded-lg bg-slate-900 border border-slate-700 focus:outline-none focus:ring-2 focus:ring-sky-500" />
|
||||
</label>
|
||||
</div>
|
||||
<button type="submit" class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-orange-700 hover:bg-orange-600 transition-colors text-white text-sm font-medium">
|
||||
<i class="fa-solid fa-floppy-disk"></i>Save Rate Limit Settings
|
||||
</button>
|
||||
</form>
|
||||
<p id="brl-settings-msg" class="hidden text-sm rounded-lg px-3 py-2 border"></p>
|
||||
</section>
|
||||
|
||||
<footer class="text-center text-sm text-slate-500 py-4 border-t border-slate-800 mt-4">
|
||||
<p>Created by KDot — <a href="https://t.me/kunta" target="_blank" rel="noopener noreferrer" class="text-sky-400 hover:text-sky-300 transition-colors"><i class="fa-brands fa-telegram"></i> t.me/kunta</a></p>
|
||||
</footer>
|
||||
|
||||
Reference in New Issue
Block a user