-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
-
Configuration
+
+
+
+
Select or create a key to see details
+
-
-
- Danger zone
-
-
+
+
-
-
diff --git a/standalone/public/js/api.js b/standalone/public/js/api.js
deleted file mode 100644
index aa14f3d..0000000
--- a/standalone/public/js/api.js
+++ /dev/null
@@ -1,42 +0,0 @@
-export default async (method, path, body) => {
- try {
- const { token, hash } = JSON.parse(localStorage.getItem("cap_auth"));
-
- const requestInit = {
- method,
- headers: {
- Authorization: `Bearer ${btoa(
- JSON.stringify({
- token,
- hash,
- })
- )}`,
- },
- };
-
- if (body) {
- requestInit.headers["Content-Type"] = "application/json";
- requestInit.body = JSON.stringify(body);
- }
-
- const json = await (await fetch(`/server${path}`, requestInit)).json();
-
- if (json?.error === "Unauthorized") {
- localStorage.removeItem("cap_auth");
- if (window?.CookieStore && CookieStore.prototype.delete) {
- window.cookieStore.delete("cap_authed");
- } else {
- // biome-ignore lint/suspicious/noDocumentCookie: fallback implemented
- document.cookie =
- "cap_authed=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
- }
- location.reload();
- }
-
- return json;
- } catch (e) {
- return {
- error: e.message,
- };
- }
-};
diff --git a/standalone/public/js/createApiKey.js b/standalone/public/js/createApiKey.js
deleted file mode 100644
index 9c19f30..0000000
--- a/standalone/public/js/createApiKey.js
+++ /dev/null
@@ -1,82 +0,0 @@
-import sendApiRequest from "./api.js";
-
-import { openSettings } from "./settings.js";
-import { createModal } from "./utils.js";
-
-async function openCreateApiKey() {
- const modal = createModal(
- "Add a key",
- `
-
-
-
-
-
`
- );
- modal.querySelector(".key-name-input").focus();
-
- modal.querySelector(".key-name-input").addEventListener("input", (event) => {
- modal.querySelector(".create-key-button").disabled =
- !event.target.value.trim();
- });
-
- modal
- .querySelector(".key-name-input")
- .addEventListener("keydown", (event) => {
- if (event.target.value.trim() && event.key === "Enter") {
- modal.querySelector(".create-key-button").click();
- }
- });
-
- modal.querySelector(".close-button").addEventListener("click", () => {
- document.body.removeChild(modal);
- });
-
- modal
- .querySelector(".create-key-button")
- .addEventListener("click", async () => {
- const name = modal.querySelector(".key-name-input").value.trim();
-
- modal.querySelector(".create-key-button").disabled = true;
-
- try {
- const { apiKey } = await sendApiRequest("POST", "/settings/apikeys", {
- name,
- });
-
- document.body.removeChild(modal);
-
- const successModal = createModal(
- "Key created",
- `
-
-
-
-
Make sure to copy your API key as it will not be shown again later.
-
-
-
`
- );
-
- successModal
- .querySelector(".close-small-button")
- .addEventListener("click", () => {
- document.body.removeChild(successModal);
- });
-
- openSettings();
- } catch (error) {
- createModal(
- "Error",
- `
Failed to create API key: ${error.message}
`
- );
- modal.querySelector(".create-key-button").disabled = false;
- }
- });
-}
-
-document
- .querySelector(".create-api-key")
- .addEventListener("click", openCreateApiKey);
-
-export default openCreateApiKey;
diff --git a/standalone/public/js/createKey.js b/standalone/public/js/createKey.js
deleted file mode 100644
index a9140c9..0000000
--- a/standalone/public/js/createKey.js
+++ /dev/null
@@ -1,99 +0,0 @@
-import sendApiRequest from "./api.js";
-import initHomepage from "./homepage.js";
-import loadKeyPage from "./keyPage.js";
-import setState from "./state.js";
-import { createModal } from "./utils.js";
-
-async function openCreateKey(namePrefill = "") {
- const modal = createModal(
- "Add a key",
- `
-
-
-
-
-
`
- );
- modal.querySelector(".key-name-input").focus();
-
- if (!namePrefill)
- document.querySelector(".create-key-button").disabled = true;
-
- modal.querySelector(".key-name-input").addEventListener("input", (event) => {
- modal.querySelector(".create-key-button").disabled =
- !event.target.value.trim();
- });
-
- modal
- .querySelector(".key-name-input")
- .addEventListener("keydown", (event) => {
- if (event.target.value.trim() && event.key === "Enter") {
- modal.querySelector(".create-key-button").click();
- }
- });
-
- modal.querySelector(".close-button").addEventListener("click", () => {
- document.body.removeChild(modal);
- });
-
- modal
- .querySelector(".create-key-button")
- .addEventListener("click", async () => {
- const name = modal.querySelector(".key-name-input").value.trim();
-
- modal.querySelector(".create-key-button").disabled = true;
-
- try {
- const { siteKey, secretKey } = await sendApiRequest("POST", "/keys", {
- name,
- });
-
- document.body.removeChild(modal);
-
- const successModal = createModal(
- "Key created",
- `
-
-
-
-
-
-
-
Make sure to copy your secret key as it will not be shown again later.
-
-
-
-
`
- );
-
- successModal
- .querySelector(".open-key-button")
- .addEventListener("click", () => {
- document.body.removeChild(successModal);
- loadKeyPage(siteKey, secretKey);
- });
- successModal
- .querySelector(".close-small-button")
- .addEventListener("click", () => {
- document.body.removeChild(successModal);
- });
-
- await initHomepage();
- setState("homepage");
- } catch (error) {
- setState("homepage");
-
- createModal(
- "Error",
- `
Failed to create key: ${error.message}
`
- );
- modal.querySelector(".create-key-button").disabled = false;
- }
- });
-}
-
-document.querySelector(".create-key").addEventListener("click", () => {
- openCreateKey(document.querySelector(".search-input").value?.trim());
-});
-
-export default openCreateKey;
diff --git a/standalone/public/js/dashboard.js b/standalone/public/js/dashboard.js
new file mode 100644
index 0000000..a2fa6db
--- /dev/null
+++ b/standalone/public/js/dashboard.js
@@ -0,0 +1,937 @@
+let keys = [];
+let selectedKey;
+let chart;
+
+const keysList = document.getElementById("keysList");
+const searchInput = document.getElementById("searchInput");
+const welcomeScreen = document.getElementById("welcomeScreen");
+const keyDetail = document.getElementById("keyDetail");
+
+const escapeHtml = (s) => {
+ return s.replaceAll("<", "<").replaceAll(">", ">");
+};
+
+const api = async (method, path, body) => {
+ try {
+ const auth = JSON.parse(localStorage.getItem("cap_auth"));
+ if (!auth) throw new Error("Not authenticated");
+
+ const opts = {
+ method,
+ headers: {
+ Authorization: `Bearer ${btoa(JSON.stringify({ token: auth.token, hash: auth.hash }))}`,
+ },
+ };
+
+ if (body) {
+ opts.headers["Content-Type"] = "application/json";
+ opts.body = JSON.stringify(body);
+ }
+
+ return await await fetch(`/server${path}`, opts).json();
+ } catch (e) {
+ return { error: e.message };
+ }
+};
+
+const formatRelative = (date) => {
+ const diff = new Date(date) - Date.now();
+ const past = diff < 0;
+ const d = Math.abs(diff);
+
+ const ms = {
+ year: 365 * 24 * 60 * 60 * 1000,
+ month: 30 * 24 * 60 * 60 * 1000,
+ week: 7 * 24 * 60 * 60 * 1000,
+ day: 24 * 60 * 60 * 1000,
+ hour: 60 * 60 * 1000,
+ minute: 60 * 1000,
+ };
+
+ for (const [u, v] of Object.entries(ms)) {
+ if (d >= v) {
+ const val = Math.floor(d / v);
+ return past
+ ? `${val} ${u}${val > 1 ? "s" : ""} ago`
+ : `in ${val} ${u}${val > 1 ? "s" : ""}`;
+ }
+ }
+ return past ? "just now" : "in a moment";
+};
+
+const randKey = () => {
+ const left =
+ "admiring adoring affectionate agitated amazing angry awesome beautiful blissful bold boring brave busy charming clever compassionate competent condescending confident cool cranky crazy dazzling determined distracted dreamy eager ecstatic elastic elated elegant eloquent epic exciting fervent festive flamboyant focused friendly frosty funny gallant gifted goofy gracious great happy hardcore heuristic hopeful hungry infallible inspiring intelligent interesting jolly jovial keen kind laughing loving lucid magical modest musing mystifying naughty nervous nice nifty nostalgic objective optimistic peaceful pedantic pensive practical priceless quirky quizzical recursing relaxed reverent romantic sad serene sharp silly sleepy stoic strange stupefied suspicious sweet tender thirsty trusting unruffled upbeat vibrant vigilant vigorous wizardly wonderful xenodochial youthful zealous zen".split(
+ " ",
+ );
+ const right =
+ "agnesi albattani allen almeida antonelli archimedes ardinghelli aryabhata austin babbage banach banzai bardeen bartik bassi beaver bell benz bhabha bhaskara black blackburn blackwell bohr booth borg bose bouman boyd brahmagupta brattain brown buck burnell cannon carson cartwright carver cerf chandrasekhar chaplygin chatelet chatterjee chaum chebyshev clarke cohen colden cori cray curie curran darwin davinci dewdney dhawan diffie dijkstra dirac driscoll dubinsky easley edison einstein elbakyan elgamal elion ellis engelbart euclid euler faraday feistel fermat fermi feynman franklin gagarin galileo galois ganguly gates gauss germain goldberg goldstine goldwasser golick goodall gould greider grothendieck haibt hamilton haslett hawking heisenberg hellman hermann herschel hertz heyrovsky hodgkin hofstadter hoover hopper hugle hypatia ishizaka jackson jang jemison jennings jepsen johnson joliot jones kalam kapitsa kare keldysh keller kepler khayyam khorana kilby kirch knuth kowalevski lalande lamarr lamport leakey leavitt lederberg lehmann lewin lichterman liskov lovelace lumiere mahavira margulis matsumoto maxwell mayer mccarthy mcclintock mclaren mclean mcnulty meitner mendel mendeleev meninsky merkle mestorf mirzakhani montalcini moore morse moser murdock napier nash neumann newton nightingale nobel noether northcutt noyce panini pare pascal pasteur payne perlman pike poincare poitras proskuriakova ptolemy raman ramanujan rhodes ride ritchie robinson roentgen rosalind rubin saha sammet sanderson satoshi shamir shannon shaw shirley shockley shtern sinoussi snyder solomon spence stonebraker sutherland swanson swartz swirles taussig tesla tharp thompson torvalds tu turing varahamihira vaughan villani visvesvaraya volhard wescoff wilbur wiles williams williamson wilson wing wozniak wright wu yalow yonath zhukovsky".split(
+ " ",
+ );
+
+ return `${left[Math.floor(Math.random() * left.length)]}-${right[Math.floor(Math.random() * right.length)]}`;
+};
+
+async function init() {
+ if (!localStorage.getItem("cap_auth")) {
+ document.cookie =
+ "cap_authed=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
+ return;
+ }
+
+ await loadKeys();
+}
+
+async function loadKeys() {
+ keys = await api("GET", "/keys");
+ if (keys.error?.includes?.("Unauthorized")) {
+ localStorage.removeItem("cap_auth");
+ document.cookie =
+ "cap_authed=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
+ location.reload();
+ return;
+ }
+ if (keys.error) {
+ keysList.innerHTML = `
`;
+ return;
+ }
+ renderKeysList();
+}
+
+function renderKeysList(filter = "") {
+ const filtered = keys.filter((k) =>
+ k.name.toLowerCase().includes(filter.toLowerCase()),
+ );
+
+ if (filtered.length === 0) {
+ keysList.innerHTML = `
+
+
${filter ? "No matching keys" : "No keys yet"}
+ ${
+ !filter
+ ? `
`
+ : ""
+ }
+
+ `;
+ return;
+ }
+
+ keysList.innerHTML = filtered
+ .map(
+ (key) => `
+
+
${escapeHtml(key.name)}
+
+
${key.solvesLast24h} solves/24h
+ ${
+ key.difference.direction
+ ? `
+
+ ${
+ key.difference.direction === "up"
+ ? ''
+ : ''
+ }
+ ${key.difference.value}%
+
+ `
+ : ""
+ }
+
+
+ `,
+ )
+ .join("");
+
+ keysList.querySelectorAll(".key-item").forEach((el) => {
+ el.addEventListener("click", () => {
+ keysList.querySelectorAll(".key-item").forEach((e) => {
+ e.classList.remove("active");
+ });
+ el.classList.add("active");
+ selectKey(el.dataset.key);
+ });
+ });
+}
+
+async function selectKey(siteKey) {
+ document.querySelector("#detailPanel").style.opacity = ".3";
+ const data = await api("GET", `/keys/${siteKey}`);
+ document.querySelector("#detailPanel").style.opacity = "1";
+ if (data.error) {
+ showModal(
+ "Error",
+ `
Failed to load key: ${data.error}
`,
+ );
+ return;
+ }
+
+ selectedKey = data.key;
+ selectedKey.stats = data.stats;
+ selectedKey.chartData = data.chartData;
+
+ renderKeyDetail();
+ keysList.querySelectorAll(".key-item").forEach((el) => {
+ el.classList.remove("active");
+ if (el.dataset.key === siteKey) {
+ el.classList.add("active");
+ }
+ });
+}
+
+function renderKeyDetail() {
+ welcomeScreen.style.display = "none";
+ keyDetail.style.display = "block";
+
+ const key = selectedKey;
+ const keyFromList = keys.find((k) => k.siteKey === key.siteKey);
+ const change = keyFromList?.difference || { value: 0, direction: "" };
+
+ keyDetail.innerHTML = `
+
+
+
+
+
Solves (24h)
+
${key.stats.solvesLast24h.toLocaleString()}
+ ${
+ change.direction
+ ? `
+
+ ${
+ change.direction === "up"
+ ? '
'
+ : '
'
+ }
+ ${change.value}% vs yesterday
+
+ `
+ : ""
+ }
+
+
+
Difficulty
+
${key.config.difficulty}
+
+
+
Challenges
+
${key.config.challengeCount}
+
+
+
+
+
+
+
Configuration
+
+
+
+
+
+
+
+
Danger zone
+
These actions are irreversible. Please be certain.
+
+
+
+
+
+ `;
+
+ document.getElementById("copyKeyBtn").addEventListener("click", async () => {
+ try {
+ await navigator.clipboard.writeText(key.siteKey);
+ const btn = document.getElementById("copyKeyBtn");
+ btn.innerHTML = `
+
+ Copied!
+ `;
+ setTimeout(() => {
+ btn.innerHTML = `
+
+ Copy site key
+ `;
+ }, 2000);
+ } catch {
+ showModal(
+ "Site Key",
+ `
+
+ `,
+ );
+ }
+ });
+
+ document.getElementById("timeSelect").addEventListener("change", (e) => {
+ loadChartData(e.target.value);
+ });
+
+ document
+ .getElementById("saveConfigBtn")
+ .addEventListener("click", saveConfig);
+ document
+ .getElementById("rotateSecretBtn")
+ .addEventListener("click", rotateSecret);
+ document.getElementById("deleteKeyBtn").addEventListener("click", deleteKey);
+
+ renderChart(key.chartData);
+}
+
+async function loadChartData(duration) {
+ document.getElementById("chartLoading").classList.add("visible");
+
+ const data = await api(
+ "GET",
+ `/keys/${selectedKey.siteKey}?chartDuration=${duration}`,
+ );
+
+ document.getElementById("chartLoading").classList.remove("visible");
+
+ if (data.chartData) {
+ renderChart(data.chartData);
+ }
+}
+
+function renderChart(chartData) {
+ const canvas = document.getElementById("chart");
+ const ctx = canvas.getContext("2d");
+
+ if (chart) chart.destroy();
+
+ const { data, duration } = chartData;
+ const now = Math.floor(Date.now() / 1000);
+
+ const labels = [];
+ const values = [];
+
+ const dataMap = new Map(data.map((d) => [d.bucket, d.count]));
+
+ if (duration === "today") {
+ const startTime = Math.floor(Date.now() / 1000 / 86400) * 86400;
+ const buckets = Math.ceil((now - startTime) / 3600) + 1;
+ for (let i = 0; i < buckets; i++) {
+ const t = startTime + i * 3600;
+ labels.push(
+ new Date(t * 1000).toLocaleTimeString("en-US", {
+ hour: "numeric",
+ hour12: true,
+ }),
+ );
+ values.push(dataMap.get(t) || 0);
+ }
+ } else if (duration === "yesterday") {
+ const startTime = Math.floor(Date.now() / 1000 / 86400) * 86400 - 86400;
+ for (let i = 0; i < 24; i++) {
+ const t = startTime + i * 3600;
+ labels.push(
+ new Date(t * 1000).toLocaleTimeString("en-US", {
+ hour: "numeric",
+ hour12: true,
+ }),
+ );
+ values.push(dataMap.get(t) || 0);
+ }
+ } else {
+ data.forEach((d) => {
+ labels.push(
+ new Date(d.bucket * 1000).toLocaleDateString("en-US", {
+ month: "short",
+ day: "numeric",
+ }),
+ );
+ values.push(d.count || 0);
+ });
+ }
+
+ chart = new Chart(ctx, {
+ type: "line",
+ data: {
+ labels,
+ datasets: [
+ {
+ data: values,
+ fill: true,
+ backgroundColor: "rgba(137, 180, 250, 0.15)",
+ borderColor: "#89b4fa",
+ borderWidth: 2,
+ pointRadius: 0,
+ pointHoverRadius: 4,
+ pointHoverBackgroundColor: "#89b4fa",
+ tension: 0.3,
+ },
+ ],
+ },
+ options: {
+ responsive: true,
+ maintainAspectRatio: false,
+ animation: { duration: 300 },
+ interaction: { mode: "index", intersect: false },
+ scales: {
+ x: {
+ grid: { display: false },
+ ticks: { color: "#6c7086", maxTicksLimit: 8 },
+ },
+ y: {
+ beginAtZero: true,
+ grid: { color: "rgba(108, 112, 134, 0.1)" },
+ ticks: { color: "#6c7086" },
+ },
+ },
+ plugins: {
+ legend: { display: false },
+ tooltip: {
+ backgroundColor: "#1e1e2e",
+ titleColor: "#cdd6f4",
+ bodyColor: "#cdd6f4",
+ borderColor: "#313244",
+ borderWidth: 1,
+ padding: 10,
+ displayColors: false,
+ callbacks: {
+ label: (ctx) => `${ctx.raw} solutions`,
+ },
+ },
+ },
+ },
+ });
+}
+
+async function saveConfig() {
+ const btn = document.getElementById("saveConfigBtn");
+ btn.disabled = true;
+
+ const name = document.getElementById("cfgName").value.trim();
+ const difficulty = parseInt(
+ document.getElementById("cfgDifficulty").value,
+ 10,
+ );
+ const challengeCount = parseInt(
+ document.getElementById("cfgChallengeCount").value,
+ 10,
+ );
+ const saltSize = parseInt(document.getElementById("cfgSaltSize").value, 10);
+
+ if (!name || difficulty < 2 || challengeCount < 1 || saltSize < 7) {
+ showModal(
+ "Validation Error",
+ `
Please check your input values. Difficulty must be ≥2, challenge count ≥1, and salt size ≥7.
`,
+ );
+ btn.disabled = false;
+ return;
+ }
+
+ const res = await api("PUT", `/keys/${selectedKey.siteKey}/config`, {
+ name,
+ difficulty,
+ challengeCount,
+ saltSize,
+ });
+
+ btn.disabled = false;
+
+ if (res.success) {
+ await loadKeys();
+ selectedKey.name = name;
+ selectedKey.config = { difficulty, challengeCount, saltSize };
+ document.querySelector(".key-header h1").textContent = name;
+ renderKeysList(searchInput.value);
+ } else {
+ showModal(
+ "Error",
+ `
Failed to save configuration.
`,
+ );
+ }
+}
+
+function rotateSecret() {
+ showConfirmModal(
+ "Rotate Secret?",
+ "This will generate a new secret key. Your existing integrations will stop working until updated.",
+ "Rotate",
+ async () => {
+ const res = await api(
+ "POST",
+ `/keys/${selectedKey.siteKey}/rotate-secret`,
+ );
+ if (res.secretKey) {
+ showModal(
+ "Rotated secret key",
+ `
+
+
+
+
+
Make sure to copy this - it won't be shown again.
+
+
+ `,
+ );
+ } else {
+ showModal(
+ "Error",
+ `
Failed to rotate secret key.
`,
+ );
+ }
+ },
+ );
+}
+
+function deleteKey() {
+ showConfirmModal(
+ "Delete Key?",
+ "This will permanently delete this key and all associated data. This cannot be undone.",
+ "Delete",
+ async () => {
+ const res = await api("DELETE", `/keys/${selectedKey.siteKey}`);
+ if (res.success) {
+ selectedKey = null;
+ welcomeScreen.style.display = "flex";
+ keyDetail.style.display = "none";
+ await loadKeys();
+ } else {
+ showModal(
+ "Error",
+ `
`,
+ );
+ }
+ },
+ true,
+ );
+}
+
+function createModal(title, content, isSettings = false) {
+ closeModal();
+ const overlay = document.createElement("div");
+ overlay.className = "modal-overlay";
+ overlay.innerHTML = `
+
+
+ ${content}
+
+ `;
+ document.body.appendChild(overlay);
+
+ overlay.querySelector(".modal-close").addEventListener("click", closeModal);
+ overlay.addEventListener("mousedown", (e) => {
+ if (e.target === overlay) closeModal();
+ });
+ document.addEventListener("keydown", escapeHandler);
+
+ return overlay;
+}
+
+function escapeHandler(e) {
+ if (e.key === "Escape") closeModal();
+}
+
+function closeModal() {
+ const overlay = document.querySelector(".modal-overlay");
+ if (overlay) {
+ overlay.style.opacity = "0";
+ overlay.querySelector(".modal").style.transform =
+ "scale(0.95) translateY(10px)";
+ overlay.querySelector(".modal").style.filter = "blur(2px)";
+ document.removeEventListener("keydown", escapeHandler);
+
+ setTimeout(() => {
+ overlay.remove();
+ }, 150);
+ }
+}
+
+function showModal(title, content) {
+ createModal(title, content);
+}
+
+function showConfirmModal(
+ title,
+ message,
+ confirmText,
+ onConfirm,
+ isDanger = false,
+) {
+ const modal = createModal(
+ title,
+ `
+
+
+ `,
+ );
+
+ modal.querySelector("#cancelBtn").addEventListener("click", () => {
+ closeModal();
+ });
+ modal.querySelector("#confirmBtn").addEventListener("click", async () => {
+ modal.querySelector("#confirmBtn").disabled = true;
+ await onConfirm();
+ closeModal();
+ });
+}
+
+function openCreateKeyModal(prefill = "") {
+ const placeholder = randKey();
+
+ const modal = createModal(
+ "Create key",
+ `
+
+
+ `,
+ );
+
+ const input = modal.querySelector("#newKeyName");
+ const submitBtn = modal.querySelector("#createKeySubmit");
+
+ input.select();
+
+ input.focus();
+ input.addEventListener("input", () => {
+ submitBtn.disabled = !input.value.trim();
+ });
+
+ input.addEventListener("keydown", (e) => {
+ if (e.key === "Enter" && input.value.trim()) {
+ submitBtn.click();
+ }
+ });
+
+ submitBtn.addEventListener("click", async () => {
+ submitBtn.disabled = true;
+ const res = await api("POST", "/keys", { name: input.value.trim() });
+
+ if (res.siteKey && res.secretKey) {
+ closeModal();
+ showModal(
+ "Key created",
+ `
+
+
+
+
+
+
+
+
+
Make sure to copy your secret key - it won't be shown again.
+
+
+
+ `,
+ );
+ await loadKeys();
+ selectKey(res.siteKey);
+ } else {
+ showModal(
+ "Error",
+ `
`,
+ );
+ }
+ });
+}
+
+async function openSettings() {
+ const modal = createModal(
+ "Settings",
+ `
+
+
+
+
+
+
+ `,
+ true,
+ );
+
+ modal.querySelectorAll(".settings-tab").forEach((tab) => {
+ tab.addEventListener("click", () => {
+ modal.querySelectorAll(".settings-tab").forEach((t) => {
+ t.classList.remove("active");
+ });
+ modal.querySelectorAll(".settings-section").forEach((s) => {
+ s.classList.remove("active");
+ });
+ tab.classList.add("active");
+ modal.querySelector(`#${tab.dataset.tab}Section`).classList.add("active");
+ });
+ });
+
+ const sessions = await api("GET", "/settings/sessions");
+ const currentHash = JSON.parse(localStorage.getItem("cap_auth"))?.hash || "";
+
+ if (Array.isArray(sessions)) {
+ document.getElementById("sessionsList").innerHTML = sessions
+ .map(
+ (s) => `
+
+
+
${s.token}
+
+ ${currentHash.endsWith(s.token) ? 'Current • ' : ""}
+ expires ${formatRelative(s.expires)}
+
+
+
+
+ `,
+ )
+ .join("");
+
+ document.querySelectorAll(".session-action").forEach((btn) => {
+ btn.addEventListener("click", async () => {
+ const token = btn.dataset.token;
+
+ if (!currentHash.endsWith(token)) {
+ btn.parentElement.remove();
+ }
+
+ await api("POST", "/logout", { session: token });
+ if (currentHash.endsWith(token)) {
+ localStorage.removeItem("cap_auth");
+ document.cookie =
+ "cap_authed=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
+ location.reload();
+ }
+ });
+ });
+ }
+
+ const apikeys = await api("GET", "/settings/apikeys");
+ if (Array.isArray(apikeys)) {
+ if (apikeys.length === 0) {
+ document.getElementById("apikeysList").innerHTML =
+ '
No API keys yet
';
+ } else {
+ document.getElementById("apikeysList").innerHTML = apikeys
+ .map(
+ (k) => `
+
+
+
${escapeHtml(k.name)}
+
${k.id.slice(0, 12)}... • created ${formatRelative(k.created)}
+
+
+
+ `,
+ )
+ .join("");
+
+ document.querySelectorAll(".apikey-action").forEach((btn) => {
+ btn.addEventListener("click", () => {
+ showConfirmModal(
+ "Delete API key?",
+ "This will permanently delete this API key.",
+ "Delete",
+ async () => {
+ await api("DELETE", `/settings/apikeys/${btn.dataset.id}`);
+ },
+ true,
+ );
+ });
+ });
+ }
+ }
+
+ document.getElementById("createApiKeyBtn").addEventListener("click", () => {
+ closeModal();
+ openCreateApiKeyModal();
+ });
+
+ const about = await api("GET", "/about");
+ if (about.ver) {
+ document.getElementById("aboutInfo").innerHTML = `
+ Standalone
v${about.ver}Bun
v${about.bun}
+ `;
+ }
+}
+
+function openCreateApiKeyModal() {
+ const placeholder = randKey();
+
+ const modal = createModal(
+ "Create API Key",
+ `
+
+
+ `,
+ );
+
+ const input = modal.querySelector("#apiKeyName");
+ const submitBtn = modal.querySelector("#createApiKeySubmit");
+
+ input.select();
+
+ input.focus();
+ input.addEventListener("input", () => {
+ submitBtn.disabled = !input.value.trim();
+ });
+
+ input.addEventListener("keydown", (e) => {
+ if (e.key === "Enter" && input.value.trim()) {
+ submitBtn.click();
+ }
+ });
+
+ submitBtn.addEventListener("click", async () => {
+ submitBtn.disabled = true;
+ const res = await api("POST", "/settings/apikeys", {
+ name: input.value.trim(),
+ });
+
+ if (res.apiKey) {
+ closeModal();
+ showModal(
+ "API key created",
+ `
+
+
+
+
+
Make sure to copy your API key - it won't be shown again.
+
+
+
+ `,
+ );
+ } else {
+ showModal(
+ "Error",
+ `
Failed to create API key.
`,
+ );
+ }
+ });
+}
+
+document.getElementById("createKeyBtn").addEventListener("click", () => {
+ openCreateKeyModal(searchInput.value.trim());
+});
+
+document.getElementById("searchInput").addEventListener("input", (e) => {
+ renderKeysList(e.target.value);
+});
+
+document.getElementById("settingsBtn").addEventListener("click", openSettings);
+init();
diff --git a/standalone/public/js/homepage.js b/standalone/public/js/homepage.js
deleted file mode 100644
index 94f18ef..0000000
--- a/standalone/public/js/homepage.js
+++ /dev/null
@@ -1,82 +0,0 @@
-import sendApiRequest from "./api.js";
-import loadKeyPage from "./keyPage.js";
-import setState from "./state.js";
-
-const initHomepage = async () => {
- setState("loading");
-
- let keys;
-
- try {
- keys = await sendApiRequest("GET", "/keys");
- } catch {
- setState("homepage");
-
- document.querySelector(".keys-list").classList.add("no-keys");
- document.querySelector(
- ".keys-list"
- ).innerHTML = `
An error occurred while fetching your keys. Please try again later.
`;
- return;
- }
-
- setState("homepage");
-
- document.querySelector(".keys-list").classList.remove("no-keys");
- document.querySelector(".keys-list").innerHTML = "";
-
- if (keys.length === 0) {
- document.querySelector(".keys-list").classList.add("no-keys");
- document.querySelector(
- ".keys-list"
- ).innerHTML = `
You don't have any keys
`;
- return;
- }
-
- keys.forEach((key) => {
- const keyEl = document.createElement("div");
- keyEl.classList.add("key");
-
- let differenceItem = "〰";
- if (key.difference.direction === "up") {
- differenceItem = `
`;
- } else if (key.difference.direction === "down") {
- differenceItem = `
`;
- }
-
- keyEl.innerHTML = `
-
-
${key.name}
-
${key.solvesLast24h} solves in last 24h • ${differenceItem} ${key.difference.value}%
-
-
- `;
- document.querySelector(".keys-list").appendChild(keyEl);
-
- keyEl.addEventListener("click", () => {
- keyEl.querySelector(".open-key").click();
- });
-
- keyEl.querySelector(".open-key").addEventListener("click", () => {
- loadKeyPage(key.siteKey);
- });
- });
-};
-
-document.querySelector(".keys-top input").addEventListener("input", (event) => {
- const searchTerm = event.target.value.toLowerCase();
- const keys = document.querySelectorAll(".keys-list .key");
-
- keys.forEach((key) => {
- const keyName = key.querySelector("h2").innerText.toLowerCase();
- if (keyName.includes(searchTerm)) {
- key.style.display = "flex";
- } else {
- key.style.display = "none";
- }
- });
-});
-
-export default initHomepage;
diff --git a/standalone/public/js/index.js b/standalone/public/js/index.js
deleted file mode 100644
index 5f77e27..0000000
--- a/standalone/public/js/index.js
+++ /dev/null
@@ -1,51 +0,0 @@
-import sendApiRequest from "./api.js";
-import initHomepage from "./homepage.js";
-import { openSettings } from "./settings.js";
-import setState from "./state.js";
-import { createModal } from "./utils.js";
-
-import "./createKey.js";
-import "./createApiKey.js";
-
-initHomepage();
-
-document.querySelectorAll(".logo, .keypage-back").forEach((el) =>
- el.addEventListener("click", () => {
- setState("homepage");
- initHomepage();
- })
-);
-
-document.querySelector(".settings").addEventListener("click", openSettings);
-
-document.querySelector(".logout").addEventListener("click", async () => {
- const modal = createModal(
- "Logout?",
- `
-
`
- );
-
- modal
- .querySelector(".logout-confirm-button")
- .addEventListener("click", async () => {
- modal.querySelector(".logout-confirm-button").disabled = true;
- await sendApiRequest("POST", "/logout");
-
- localStorage.removeItem("cap_auth");
-
- // biome-ignore lint/suspicious/noDocumentCookie: n/a
- document.cookie =
- "cap_authed=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
- location.reload();
- });
-
- modal.querySelector(".logout-cancel-button").addEventListener("click", () => {
- document.body.removeChild(modal);
- });
-});
-
-if (!localStorage.getItem("cap_auth")) {
- // biome-ignore lint/suspicious/noDocumentCookie: n/a
- document.cookie =
- "cap_authed=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
-}
diff --git a/standalone/public/js/keyPage.js b/standalone/public/js/keyPage.js
deleted file mode 100644
index bcac484..0000000
--- a/standalone/public/js/keyPage.js
+++ /dev/null
@@ -1,431 +0,0 @@
-import makeApiRequest from "./api.js";
-import initHomepage from "./homepage.js";
-import setState from "./state.js";
-import { createModal } from "./utils.js";
-
-let currentChart = null;
-let currentSiteKey = null;
-
-export default async function loadKeyPage(siteKey) {
- setState("loading");
- currentSiteKey = siteKey;
-
- try {
- const response = await makeApiRequest("GET", `/keys/${siteKey}`);
-
- if (!response.success && response.error) {
- throw new Error(response.error);
- }
-
- displayKeyData(response);
- await loadChartData();
- setupEventListeners();
- setState("keyPage");
- } catch (error) {
- createModal(
- "Error",
- `
Failed to load key data: ${error.message}
`
- );
- setState("homepage");
- }
-}
-
-function displayKeyData(data) {
- const { key } = data;
-
- const titleElement = document.querySelector(".state-key-page .topbar h1");
- titleElement.innerText = key.name;
-
- const configElement = document.querySelector(".state-key-page .config");
- configElement.querySelector("#keyName").value = key.name;
- configElement.querySelector("#difficulty").value = key.config.difficulty;
- configElement.querySelector("#challengeCount").value =
- key.config.challengeCount;
- configElement.querySelector("#saltSize").value = key.config.saltSize;
-
- configElement.querySelector("button").onclick = async () => {
- const name = configElement.querySelector("#keyName").value.trim();
- const difficulty = parseInt(
- configElement.querySelector("#difficulty").value,
- 10
- );
- const challengeCount = parseInt(
- configElement.querySelector("#challengeCount").value,
- 10
- );
- const saltSize = parseInt(
- configElement.querySelector("#saltSize").value,
- 10
- );
-
- if (
- !name ||
- Number.isNaN(difficulty) ||
- Number.isNaN(challengeCount) ||
- Number.isNaN(saltSize)
- ) {
- createModal(
- "Error",
- `
Please fill in all fields correctly.
`
- );
-
- return;
- }
-
- if (difficulty <= 1 || challengeCount < 1 || saltSize <= 6) {
- createModal(
- "Validation failed",
- `
Difficulty must be greater than 1, challenge count must be greater than zero and salt size must be greater than 6.
`
- );
-
- return;
- }
-
- document.querySelector(".state-key-page h1").textContent =
- configElement.querySelector("#keyName").value;
- configElement.querySelector("button").disabled = true;
-
- const { success } = await makeApiRequest(
- "PUT",
- `/keys/${key.siteKey}/config`,
- {
- name,
- difficulty,
- challengeCount,
- saltSize,
- }
- );
-
- if (!success) {
- createModal(
- "Error",
- `
`
- );
- }
-
- configElement.querySelector("button").disabled = false;
- };
-
- document
- .querySelector(".danger-zone-buttons .delete-key")
- .addEventListener("click", async () => {
- const modal = createModal(
- "Delete key?",
- `
Are you sure you want to delete this key? This action cannot be undone.
-
-
`
- );
-
- document.body.appendChild(modal);
-
- modal
- .querySelector(".delete-confirm-button")
- .addEventListener("click", async () => {
- modal.querySelector(".delete-confirm-button").disabled = true;
- const { success } = await makeApiRequest(
- "DELETE",
- `/keys/${key.siteKey}`
- );
-
- document.body.removeChild(modal);
-
- if (success) {
- initHomepage();
- } else {
- createModal(
- "Error",
- `
`
- );
- }
- });
-
- modal
- .querySelector(".delete-cancel-button")
- .addEventListener("click", () => {
- document.body.removeChild(modal);
- });
- });
-
- document
- .querySelector(".danger-zone-buttons .rotate-secret")
- .addEventListener("click", async () => {
- const modal = createModal(
- "Rotate secret key?",
- `
Are you sure you want to rotate this key's secret? This won't delete the key itself.
-
-
`
- );
-
- modal
- .querySelector(".rotate-confirm-button")
- .addEventListener("click", async () => {
- modal.querySelector(".rotate-confirm-button").disabled = true;
- const { secretKey } = await makeApiRequest(
- "POST",
- `/keys/${key.siteKey}/rotate-secret`
- );
-
- document.body.removeChild(modal);
-
- if (!secretKey) {
- createModal(
- "Error",
- `
Failed to rotate key secret.
`
- );
- return;
- }
- const successModal = createModal(
- "Secret key rotated",
- `
-
-
-
-
Make sure to copy your secret key as it will not be shown again later.
-
-
-
`
- );
-
- successModal
- .querySelector(".close-small-button")
- .addEventListener("click", () => {
- document.body.removeChild(successModal);
- });
- });
-
- modal
- .querySelector(".rotate-cancel-button")
- .addEventListener("click", () => {
- document.body.removeChild(modal);
- });
- });
-
- const copyButton = document.querySelector(".state-key-page .topbar button");
- copyButton.innerHTML = `
Copy site key`;
-
- copyButton.onclick = async () => {
- try {
- await navigator.clipboard.writeText(key.siteKey);
- const originalText = copyButton.innerHTML;
- copyButton.innerHTML = `
Copied!`;
- setTimeout(() => {
- copyButton.innerHTML = originalText;
- }, 2000);
- } catch {
- createModal(
- "Site key",
- `
`
- );
- }
- };
-}
-
-async function loadChartData(duration = "today") {
- document.querySelector(".chart-container").style.opacity = "0.3";
- document.querySelector(".chart-container").style.pointerEvents = "none";
-
- const response = await makeApiRequest(
- "GET",
- `/keys/${currentSiteKey}?chartDuration=${duration}`
- );
-
- document.querySelector(".chart-container").style.opacity = "1";
- document.querySelector(".chart-container").style.pointerEvents = "all";
-
- renderChart(response.chartData, duration);
-}
-
-function formatDateLabel(timestamp, duration) {
- const date = new Date(timestamp * 1000);
-
- switch (duration) {
- case "today":
- case "yesterday":
- return date.toLocaleTimeString("en-US", {
- hour: "numeric",
- hour12: true,
- });
- case "last7days":
- case "last28days":
- case "last91days":
- case "alltime":
- return date.toLocaleDateString("en-US", {
- month: "short",
- day: "numeric",
- });
- default:
- return date.toLocaleDateString("en-US", {
- month: "short",
- day: "numeric",
- });
- }
-}
-
-function renderChart(chartData, duration) {
- const canvas = document.getElementById("chart");
- const ctx = canvas.getContext("2d");
-
- if (currentChart) {
- currentChart.destroy();
- }
-
- const labels = [];
- const dataPoints = [];
-
- const { bucketSize, data } = chartData;
- const now = Math.floor(Date.now() / 1000);
-
- let startTime, endTime, buckets;
-
- switch (duration) {
- case "today":
- startTime = Math.floor(Date.now() / 1000 / 86400) * 86400;
- endTime = startTime + 86400;
- buckets = Math.floor((endTime - startTime) / bucketSize);
- break;
- case "yesterday":
- startTime = Math.floor(Date.now() / 1000 / 86400) * 86400 - 86400;
- endTime = startTime + 86400;
- buckets = Math.floor((endTime - startTime) / bucketSize);
- break;
- case "last7days":
- startTime = now - 7 * 86400;
- endTime = now;
- buckets = 7;
- break;
- case "last28days":
- startTime = now - 28 * 86400;
- endTime = now;
- buckets = 28;
- break;
- case "last91days":
- startTime = now - 91 * 86400;
- endTime = now;
- buckets = 91;
- break;
- case "alltime":
- if (data.length > 0) {
- startTime = data[0].bucket;
- endTime = now;
- buckets = Math.ceil((endTime - startTime) / bucketSize);
- } else {
- startTime = now - 86400;
- endTime = now;
- buckets = 1;
- }
- break;
- default:
- startTime = now - 86400;
- endTime = now;
- buckets = 24;
- }
-
- const dataMap = new Map();
- data.forEach((item) => {
- dataMap.set(item.bucket, item.count);
- });
-
- for (let i = 0; i < buckets; i++) {
- const bucketTime = startTime + i * bucketSize;
- labels.push(formatDateLabel(bucketTime, duration));
-
- const alignedBucket = Math.floor(bucketTime / bucketSize) * bucketSize;
- dataPoints.push(dataMap.get(alignedBucket) || 0);
- }
- currentChart = new Chart(ctx, {
- type: "line",
- data: {
- labels: labels,
- datasets: [
- {
- label: "Solutions",
- data: dataPoints,
- fill: true,
- backgroundColor: "rgba(59, 130, 246, 0.5)",
- borderColor: "#3b82f6",
- borderWidth: 1,
- pointRadius: 0,
- pointBackgroundColor: "#3b82f6",
- pointBorderColor: "#3b82f6",
- pointHoverBorderWidth: 0,
- pointHoverRadius: 3,
- pointHoverBackgroundColor: "#3b82f6",
- tension: 0.4,
- animation: {
- duration: 0
- }
- },
- ],
- },
- options: {
- responsive: true,
- maintainAspectRatio: false,
- interaction: {
- mode: "index",
- intersect: false,
- },
- scales: {
- x: {
- grid: {
- drawOnChartArea: false,
- },
- ticks: {
- color: "#9ca3af",
- },
- },
- y: {
- beginAtZero: true,
- grid: {
- color: "rgba(255, 255, 255, 0.05)",
- drawBorder: false,
- },
- ticks: {
- color: "#9ca3af",
- },
- },
- },
- plugins: {
- legend: {
- display: false,
- },
- tooltip: {
- enabled: true,
- displayColors: false,
- backgroundColor: "#161718",
- titleColor: "#e5e7eb",
- bodyColor: "#e5e7eb",
- padding: 5,
- borderColor: "#1F2021",
- borderWidth: 1,
- callbacks: {
- label: (context) => {
- return `${context.raw} solutions`;
- },
- },
- },
- },
- animation: {
- duration: 0,
- },
- },
- });
-
- const resizeObserver = new ResizeObserver(() => {
- if (currentChart) {
- currentChart.update("resize");
- }
- });
-
- resizeObserver.observe(canvas);
-}
-
-function setupEventListeners() {
- const timeDurationSelect = document.querySelector(
- ".state-key-page .time-duration"
- );
-
- timeDurationSelect.onchange = (e) => {
- const duration = e.target.value;
- loadChartData(duration);
- };
-}
diff --git a/standalone/public/js/settings.js b/standalone/public/js/settings.js
deleted file mode 100644
index 7cf61da..0000000
--- a/standalone/public/js/settings.js
+++ /dev/null
@@ -1,183 +0,0 @@
-import makeApiRequest from "./api.js";
-import setState from "./state.js";
-import { createModal } from "./utils.js";
-
-export const openSettings = () => {
- setState("settings");
-
- const formatDate = (dateStr) => {
- const date = new Date(dateStr);
- return date.toLocaleString(undefined, {
- year: "numeric",
- month: "2-digit",
- day: "2-digit",
- hour: "2-digit",
- minute: "2-digit",
- });
- };
-
- (async () => {
- const sessions = await makeApiRequest("GET", "/settings/sessions");
-
- const sessionsList = document.querySelector(
- ".state-settings .sessions-list"
- );
- sessionsList.innerHTML = "";
- function formatRelative(date) {
- const diff = new Date(date) - Date.now();
- const past = diff < 0;
- const d = Math.abs(diff);
-
- const ms = {
- year: 365 * 24 * 60 * 60 * 1000,
- month: 30 * 24 * 60 * 60 * 1000,
- week: 7 * 24 * 60 * 60 * 1000,
- day: 24 * 60 * 60 * 1000,
- hour: 60 * 60 * 1000,
- minute: 60 * 1000,
- };
-
- if (d >= ms.year) {
- const y = Math.floor(d / ms.year);
- const m = Math.floor((d - y * ms.year) / ms.month);
- return past
- ? `${y} year${y > 1 ? "s" : ""}${
- m ? " " + m + " month" + (m > 1 ? "s" : "") : ""
- } ago`
- : `in ${y} year${y > 1 ? "s" : ""}${
- m ? " " + m + " month" + (m > 1 ? "s" : "") : ""
- }`;
- }
-
- for (const [u, v] of Object.entries(ms)) {
- if (d >= v) {
- const val = Math.floor(d / v);
- return past
- ? `${val} ${u}${val > 1 ? "s" : ""} ago`
- : `in ${val} ${u}${val > 1 ? "s" : ""}`;
- }
- }
-
- return past ? "just now" : "in a few seconds";
- }
-
- sessions.forEach((session) => {
- const sessionEl = document.createElement("div");
- sessionEl.classList.add("session");
-
- sessionEl.innerHTML = `
-
- ${session.token}...
- ${
- JSON.parse(localStorage.getItem("cap_auth")).hash.endsWith(
- session.token
- )
- ? "current • "
- : ""
- }expires ${formatRelative(
- session.expires
- )} • created ${formatRelative(session.created)}
-
-
- `;
-
- sessionsList.appendChild(sessionEl);
-
- sessionEl
- .querySelector(".session-delete-button")
- .addEventListener("click", async () => {
- const modal = createModal(
- "Logout session?",
- `
-
`
- );
-
- modal
- .querySelector(".logout-confirm-button")
- .addEventListener("click", async () => {
- modal.querySelector(".logout-confirm-button").disabled = true;
- await makeApiRequest("POST", `/logout`, {
- session: session.token,
- });
- document.body.removeChild(modal);
- if (sessions.length === 1) {
- localStorage.removeItem("cap_auth");
- location.reload();
- }
- openSettings();
- });
-
- modal
- .querySelector(".logout-cancel-button")
- .addEventListener("click", () => {
- document.body.removeChild(modal);
- });
- });
- });
- })();
-
- (async () => {
- const apikeys = await makeApiRequest("GET", "/settings/apikeys");
-
- const apikeysList = document.querySelector(".state-settings .apikeys-list");
- apikeysList.innerHTML = "";
-
- if (apikeys.length === 0) {
- apikeysList.innerHTML = `
You don't have any API keys
`;
- return;
- }
-
- apikeys.forEach((key) => {
- const keyEl = document.createElement("div");
- keyEl.classList.add("apikey");
-
- keyEl.innerHTML = `
-
- ${key.name
- .replaceAll("<", "<")
- .replaceAll(">", ">")}
- ${key.id.slice(
- 0,
- 12
- )}... • created ${formatDate(key.created)}
-
-
- `;
-
- apikeysList.appendChild(keyEl);
-
- keyEl
- .querySelector(".apikey-delete-button")
- .addEventListener("click", async () => {
- const modal = createModal(
- "Delete API key?",
- `
-
`
- );
-
- modal
- .querySelector(".delete-confirm-button")
- .addEventListener("click", async () => {
- modal.querySelector(".delete-confirm-button").disabled = true;
- await makeApiRequest("DELETE", `/settings/apikeys/${key.id}`);
- document.body.removeChild(modal);
- openSettings();
- });
-
- modal
- .querySelector(".delete-cancel-button")
- .addEventListener("click", () => {
- document.body.removeChild(modal);
- });
- });
- });
- })();
-
- (async () => {
- const info = await makeApiRequest("GET", "/about");
-
- document.querySelector(
- ".about-info"
- ).innerHTML = `You're using Cap Standalone
${info.ver} on Bun
v${info.bun}`;
- })();
-};
diff --git a/standalone/public/js/state.js b/standalone/public/js/state.js
deleted file mode 100644
index 3a5eebc..0000000
--- a/standalone/public/js/state.js
+++ /dev/null
@@ -1,16 +0,0 @@
-const states = {
- loading: document.querySelector(".state-loading"),
- homepage: document.querySelector(".state-homepage"),
- keyPage: document.querySelector(".state-key-page"),
- settings: document.querySelector(".state-settings"),
-};
-
-export default function setState(stateName) {
- for (const [name, element] of Object.entries(states)) {
- if (name === stateName) {
- element.style.display = "flex";
- } else {
- element.style.display = "none";
- }
- }
-}
diff --git a/standalone/public/js/utils.js b/standalone/public/js/utils.js
deleted file mode 100644
index dbeea0a..0000000
--- a/standalone/public/js/utils.js
+++ /dev/null
@@ -1,36 +0,0 @@
-export const createModal = (title, content) => {
- if (document.querySelector(".modal"))
- document.querySelector(".modal").remove();
-
- const modal = document.createElement("div");
- modal.classList.add("modal");
- modal.innerHTML = `
-
- `;
-
- document.body.appendChild(modal);
- modal.querySelector(".close-button").addEventListener("click", () => {
- document.body.removeChild(modal);
- });
-
- document.body.addEventListener("click", (event) => {
- if (event.target === modal) {
- document.body.removeChild(modal);
- }
- });
-
- document.body.addEventListener("keydown", (event) => {
- if (event.key === "Escape") {
- document.body.removeChild(modal);
- }
- });
-
- return modal;
-};
diff --git a/standalone/public/login.html b/standalone/public/login.html
index 67bdc2d..e78953a 100644
--- a/standalone/public/login.html
+++ b/standalone/public/login.html
@@ -34,6 +34,21 @@
margin-bottom: 28px;
margin-top: 0;
user-select: none;
+
+
+ animation: title-in 0.6s both;
+ animation-delay: 0.1s;
+ }
+ @keyframes title-in {
+ 0% {
+ opacity: 0;
+ translate: 0 50px;
+ filter: blur(10px);
+ }
+
+ to {
+ opacity: 1;
+ }
}
img {
width: 60px;
@@ -46,6 +61,9 @@
margin-bottom: 8px;
color: #eee;
user-select: none;
+ will-change: transform, filter, opacity;
+ animation: title-in 0.5s both;
+ animation-delay: 0.2s;
}
.password-wrapper {
@@ -55,6 +73,9 @@
display: flex;
border-radius: calc(0.8rem - 2px);
margin-bottom: 16px;
+ will-change: transform, filter, opacity;
+ animation: title-in 0.5s both;
+ animation-delay: 0.3s;
}
.showhide-password {
@@ -122,6 +143,11 @@
font-size: 14px;
z-index: 12;
transition: box-shadow 0.13s, opacity 0.2s;
+
+
+ will-change: transform, filter, opacity;
+ animation: title-in 0.5s both;
+ animation-delay: 0.4s;
}
.submit:focus {
@@ -141,6 +167,11 @@
align-items: center;
gap: 4px;
margin-top: 24px;
+
+
+ will-change: transform, filter, opacity;
+ animation: title-in 0.5s both;
+ animation-delay: 0.5s;
}
p.bottom .poweredby {
margin-left: auto;
diff --git a/standalone/src/auth.js b/standalone/src/auth.js
index a68e76f..9742131 100644
--- a/standalone/src/auth.js
+++ b/standalone/src/auth.js
@@ -7,9 +7,9 @@ import { ratelimitGenerator } from "./ratelimit.js";
const { ADMIN_KEY } = process.env;
if (!ADMIN_KEY) throw new Error("auth: Admin key missing. Please add one");
-if (ADMIN_KEY.length < 30)
+if (ADMIN_KEY.length < 12)
throw new Error(
- "auth: Admin key too short. Please use one that's at least 30 characters"
+ "auth: Admin key too short. Please use one that's at least 12 characters"
);
export const auth = new Elysia({