various improvements and fixes to PoW challenge UI and token refresh

This commit is contained in:
nai-degen
2024-09-22 11:11:30 -05:00
parent ff0d3dfdcd
commit ee26e7be65
8 changed files with 257 additions and 134 deletions
+1 -2
View File
@@ -30,7 +30,6 @@ self.onmessage = async (event) => {
nonce = data.nonce; nonce = data.nonce;
const c = data.challenge; const c = data.challenge;
// decode salt to Uint8Array
const salt = new Uint8Array(c.s.length / 2); const salt = new Uint8Array(c.s.length / 2);
for (let i = 0; i < c.s.length; i += 2) { for (let i = 0; i < c.s.length; i += 2) {
salt[i / 2] = parseInt(c.s.slice(i, i + 2), 16); salt[i / 2] = parseInt(c.s.slice(i, i + 2), 16);
@@ -99,7 +98,7 @@ const solve = async () => {
self.postMessage({ type: "solved", nonce: solution.nonce }); self.postMessage({ type: "solved", nonce: solution.nonce });
active = false; active = false;
} else { } else {
if (Date.now() - lastNotify > 1000) { if (Date.now() - lastNotify >= 500) {
console.log("Last nonce", nonce, "Hashes", hashesSinceLastNotify); console.log("Last nonce", nonce, "Hashes", hashesSinceLastNotify);
self.postMessage({ type: "progress", hashes: hashesSinceLastNotify }); self.postMessage({ type: "progress", hashes: hashesSinceLastNotify });
lastNotify = Date.now(); lastNotify = Date.now();
+4 -3
View File
@@ -344,10 +344,11 @@ router.post("/maintenance", (req, res) => {
case "setDifficulty": { case "setDifficulty": {
const selected = req.body["pow-difficulty"]; const selected = req.body["pow-difficulty"];
const valid = ["low", "medium", "high", "extreme"]; const valid = ["low", "medium", "high", "extreme"];
if (!selected || !valid.includes(selected)) { const isNumber = Number.isInteger(Number(selected));
throw new HttpError(400, "Invalid difficulty" + selected); if (!selected || !valid.includes(selected) && !isNumber) {
throw new HttpError(400, "Invalid difficulty " + selected);
} }
config.powDifficultyLevel = selected; config.powDifficultyLevel = isNumber ? Number(selected) : selected;
invalidatePowChallenges(); invalidatePowChallenges();
break; break;
} }
+29 -9
View File
@@ -38,15 +38,20 @@
<h3>Difficulty Level</h3> <h3>Difficulty Level</h3>
<div> <div>
<label for="difficulty">Difficulty Level:</label> <label for="difficulty">Difficulty Level:</label>
<span id="currentDifficulty">Current: <%= difficulty %></span> <select name="difficulty" id="difficulty" onchange="difficultyChanged(event)">
<select name="difficulty" id="difficulty">
<option value="low">Low</option> <option value="low">Low</option>
<option value="medium">Medium</option> <option value="medium">Medium</option>
<option value="high">High</option> <option value="high">High</option>
<option value="extreme">Extreme</option> <option value="extreme">Extreme</option>
<option value="custom">Custom</option>
</select> </select>
<div id="custom-difficulty-container" style="display: none">
<label for="customDifficulty">Hashes required (average):</label>
<input type="number" id="customDifficulty" value="0" min="1" max="1000000000" />
</div>
<button onclick='doAction("setDifficulty")'>Update Difficulty</button> <button onclick='doAction("setDifficulty")'>Update Difficulty</button>
</div> </div>
<div><span id="currentDifficulty">Current Difficulty: <%= difficulty %></span></div>
<% } %> <% } %>
<form id="maintenanceForm" action="/admin/manage/maintenance" method="post"> <form id="maintenanceForm" action="/admin/manage/maintenance" method="post">
<input id="_csrf" type="hidden" name="_csrf" value="<%= csrfToken %>" /> <input id="_csrf" type="hidden" name="_csrf" value="<%= csrfToken %>" />
@@ -63,15 +68,15 @@
<div> <div>
<h2>IP Whitelists and Blacklists</h2> <h2>IP Whitelists and Blacklists</h2>
<p> <p>
You can specify IP ranges to whitelist or blacklist from accessing the proxy. Note that changes here are not You can specify IP ranges to whitelist or blacklist from accessing the proxy. Entries can be specified as single
persisted across server restarts. If you want to make changes permanent, you can copy the values to your deployment addresses or
configuration.
</p>
<p>
Entries can be specified as single addresses or
<a href="https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing#CIDR_notation">CIDR notation</a>. IPv6 is <a href="https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing#CIDR_notation">CIDR notation</a>. IPv6 is
supported but not recommended for use with the current version of the proxy. supported but not recommended for use with the current version of the proxy.
</p> </p>
<p>
<strong>Note:</strong> Changes here are not persisted across server restarts. If you want to make changes permanent,
you can copy the values to your deployment configuration.
</p>
<% for (let i = 0; i < whitelists.length; i++) { %> <% for (let i = 0; i < whitelists.length; i++) { %>
<%- include("partials/admin-cidr-widget", { list: whitelists[i] }) %> <%- include("partials/admin-cidr-widget", { list: whitelists[i] }) %>
<% } %> <% } %>
@@ -99,10 +104,25 @@
</div> </div>
<script> <script>
function difficultyChanged(event) {
const value = event.target.value;
if (value === "custom") {
document.getElementById("custom-difficulty-container").style.display = "block";
} else {
document.getElementById("custom-difficulty-container").style.display = "none";
}
}
function doAction(action) { function doAction(action) {
document.getElementById("hiddenAction").value = action; document.getElementById("hiddenAction").value = action;
if (action === "setDifficulty") { if (action === "setDifficulty") {
document.getElementById("hiddenDifficulty").value = document.getElementById("difficulty").value; const selected = document.getElementById("difficulty").value;
const hiddenDifficulty = document.getElementById("hiddenDifficulty");
if (selected === "custom") {
hiddenDifficulty.value = document.getElementById("customDifficulty").value;
} else {
hiddenDifficulty.value = selected;
}
} }
document.getElementById("maintenanceForm").submit(); document.getElementById("maintenanceForm").submit();
} }
+1 -1
View File
@@ -171,7 +171,7 @@ function getSelfServiceLinks() {
} }
return `<div class="self-service-links">${links return `<div class="self-service-links">${links
.map(([text, link]) => `<a target="_blank" href="${link}">${text}</a>`) .map(([text, link]) => `<a href="${link}">${text}</a>`)
.join(" | ")}</div>`; .join(" | ")}</div>`;
} }
@@ -1,7 +1,14 @@
<p> <p>
Next refresh: <time><%- nextQuotaRefresh %></time> Next refresh: <time><%- nextQuotaRefresh %></time>
</p> </p>
<table class="striped"> <%
const quotaTableId = Math.random().toString(36).slice(2);
%>
<div>
<label for="quota-family-filter-<%= quotaTableId %>">Filter:</label>
<input type="text" id="quota-family-filter-<%= quotaTableId %>" oninput="filterQuotaTable(this, '<%= quotaTableId %>')" />
</div>
<table class="striped" id="quota-table-<%= quotaTableId %>">
<thead> <thead>
<tr> <tr>
<th scope="col">Model Family</th> <th scope="col">Model Family</th>
@@ -50,3 +57,18 @@
<% }) %> <% }) %>
</tbody> </tbody>
</table> </table>
<script>
function filterQuotaTable(input, tableId) {
const filter = input.value.toLowerCase();
const table = document.getElementById("quota-table-" + tableId);
const rows = table.querySelectorAll("tbody tr");
for (const row of rows) {
const modelFamily = row.querySelector("th").textContent;
if (modelFamily.toLowerCase().includes(filter)) {
row.style.display = "";
} else {
row.style.display = "none";
}
}
}
</script>
+33 -25
View File
@@ -187,7 +187,7 @@ function verifyTokenRefreshable(token: string, req: express.Request) {
} }
} }
req.log.info({ token }, "Allowing token refresh"); req.log.info({ token: `...${token.slice(-5)}` }, "Allowing token refresh");
return true; return true;
} }
@@ -227,51 +227,57 @@ router.post("/verify", async (req, res) => {
const ip = req.ip; const ip = req.ip;
req.log.info("Got verification request"); req.log.info("Got verification request");
if (recentAttempts.has(ip)) { if (recentAttempts.has(ip)) {
res const error = "Rate limited; wait a minute before trying again";
.status(429) req.log.info({ error }, "Verification rejected");
.json({ error: "Rate limited; wait a minute before trying again" }); res.status(429).json({ error });
return; return;
} }
const result = verifySchema.safeParse(req.body); const result = verifySchema.safeParse(req.body);
if (!result.success) { if (!result.success) {
res const error = "Invalid verify request";
.status(400) req.log.info({ error, result }, "Verification rejected");
.json({ error: "Invalid verify request", details: result.error }); res.status(400).json({ error, details: result.error });
return; return;
} }
const { challenge, signature, solution } = result.data; const { challenge, signature, solution } = result.data;
if (signMessage(challenge, powKeySalt) !== signature) { if (signMessage(challenge, powKeySalt) !== signature) {
res.status(400).json({ const error =
error: "Invalid signature; server may have restarted since challenge was issued. Please request a new challenge.";
"Invalid signature; server may have restarted since challenge was issued. Please request a new challenge.", req.log.info({ error }, "Verification rejected");
}); res.status(400).json({ error });
return; return;
} }
if (config.proxyKey && result.data.proxyKey !== config.proxyKey) { if (config.proxyKey && result.data.proxyKey !== config.proxyKey) {
res.status(401).json({ error: "Invalid proxy password" }); const error = "Invalid proxy password";
req.log.info({ error }, "Verification rejected");
res.status(401).json({ error, password: result.data.proxyKey });
return; return;
} }
if (challenge.ip && challenge.ip !== ip) { if (challenge.ip && challenge.ip !== ip) {
req.log.warn("Attempt to verify from different IP address"); const error = "Solution must be verified from original IP address";
res.status(400).json({ req.log.info(
error: "Solution must be verified from original IP address", { error, challengeIp: challenge.ip, clientIp: ip },
}); "Verification rejected"
);
res.status(400).json({ error });
return; return;
} }
if (solves.has(signature)) { if (solves.has(signature)) {
req.log.warn("Attempt to reuse signature"); const error = "Reused signature";
res.status(400).json({ error: "Reused signature" }); req.log.info({ error }, "Verification rejected");
res.status(400).json({ error });
return; return;
} }
if (Date.now() > challenge.e) { if (Date.now() > challenge.e) {
req.log.warn("Verification took too long"); const error = "Verification took too long";
res.status(400).json({ error: "Verification took too long" }); req.log.info({ error }, "Verification rejected");
res.status(400).json({ error });
return; return;
} }
@@ -285,7 +291,7 @@ router.post("/verify", async (req, res) => {
const success = await verifySolution(challenge, solution, req.log); const success = await verifySolution(challenge, solution, req.log);
if (!success) { if (!success) {
recentAttempts.set(ip, Date.now() + 1000 * 60 * 60 * 6); recentAttempts.set(ip, Date.now() + 1000 * 60 * 60 * 6);
req.log.warn("Solution failed verification"); req.log.warn("Bogus solution, client blocked");
res.status(400).json({ error: "Solution failed verification" }); res.status(400).json({ error: "Solution failed verification" });
return; return;
} }
@@ -299,10 +305,12 @@ router.post("/verify", async (req, res) => {
if (challenge.token) { if (challenge.token) {
const user = getUser(challenge.token); const user = getUser(challenge.token);
if (user) { if (user) {
user.expiresAt = Date.now() + config.powTokenHours * 60 * 60 * 1000; upsertUser({
user.disabledAt = undefined; token: challenge.token,
user.disabledReason = undefined; expiresAt: Date.now() + config.powTokenHours * 60 * 60 * 1000,
upsertUser(user); disabledAt: null,
disabledReason: null,
});
req.log.info( req.log.info(
{ token: `...${challenge.token.slice(-5)}` }, { token: `...${challenge.token.slice(-5)}` },
"Token refreshed" "Token refreshed"
@@ -5,8 +5,8 @@
</noscript> </noscript>
<style> <style>
#captcha-container { #captcha-container {
max-width: 500px; max-width: 550px;
margin: 50px auto; margin: 20px auto;
} }
@media (max-width: 1000px) { @media (max-width: 1000px) {
#captcha-container { #captcha-container {
@@ -42,7 +42,7 @@
#captcha-progress-text { #captcha-progress-text {
width: 100%; width: 100%;
height: 18rem; height: 20rem;
resize: vertical; resize: vertical;
font-family: monospace; font-family: monospace;
} }
@@ -70,13 +70,22 @@
height: 100%; height: 100%;
background-color: #76c7c0; background-color: #76c7c0;
} }
#copy-token {
border: none;
background: none;
filter: saturate(0);
padding: 0;
}
#copy-token:hover {
filter: saturate(1);
}
</style> </style>
<div style="display: none" id="captcha-container"> <div style="display: none" id="captcha-container">
<p> <p>
Your device needs to perform a verification task before you can receive a token. This might take anywhere from a few Your device needs to be verified before you can receive a token. This might take anywhere from a few seconds to a
seconds to a few minutes, depending on your device and the proxy's security settings. few minutes, depending on your device and the proxy's security settings.
</p> </p>
<p>Click the button below to start.</p>
<details> <details>
<summary>What is this?</summary> <summary>What is this?</summary>
<p> <p>
@@ -107,18 +116,6 @@
faster than the first one. faster than the first one.
</p> </p>
</details> </details>
<details>
<summary>What is the "Workers" setting?</summary>
<p>
This controls how many CPU cores will be used to solve the verification task. If your device gets too hot or slows
down too much during verification, reduce the number of workers.
</p>
<p>
For fastest verification, set this to the number of physical CPU cores in your device. Setting more workers than
you have actual cores will generally only slow down verification.
</p>
<p>If you don't understand what this means, leave it at the default setting.</p>
</details>
<details> <details>
<summary>Other important information</summary> <summary>Other important information</summary>
<ul> <ul>
@@ -134,15 +131,27 @@
</li> </li>
</ul> </ul>
</details> </details>
<details>
<summary>Settings</summary>
<div>
<label for="workers">Workers:</label>
<input type="number" id="workers" value="1" min="1" max="32" onchange="spawnWorkers()" />
</div>
<p>
This controls how many CPU cores will be used to solve the verification task. If your device gets too hot or slows
down too much during verification, reduce the number of workers.
</p>
<p>
For fastest verification, set this to the number of physical CPU cores in your device. Setting more workers than
you have actual cores will generally only slow down verification.
</p>
<p>If you don't understand what this means, leave it at the default setting.</p>
</details>
<form id="captcha-form" style="display: none"> <form id="captcha-form" style="display: none">
<input type="hidden" name="_csrf" value="<%= csrfToken %>" /> <input type="hidden" name="_csrf" value="<%= csrfToken %>" />
<input type="hidden" name="tokenLifetime" value="<%= tokenLifetime %>" /> <input type="hidden" name="tokenLifetime" value="<%= tokenLifetime %>" />
</form> </form>
<div id="captcha-control"> <div id="captcha-control">
<div>
<label for="workers">Workers:</label>
<input type="number" id="workers" value="1" min="1" max="32" onchange="spawnWorkers()" />
</div>
<button id="worker-control" onclick="toggleWorker()">Start verification</button> <button id="worker-control" onclick="toggleWorker()">Start verification</button>
</div> </div>
<div id="captcha-progress-container" style="display: none"> <div id="captcha-progress-container" style="display: none">
@@ -185,6 +194,9 @@
function handleWorkerMessage(e) { function handleWorkerMessage(e) {
switch (e.data.type) { switch (e.data.type) {
case "progress": case "progress":
if (solution) {
return;
}
totalHashes += e.data.hashes; totalHashes += e.data.hashes;
reports++; reports++;
break; break;
@@ -206,13 +218,13 @@
} }
workers.forEach((w, i) => { workers.forEach((w, i) => {
w.postMessage({ type: "stop" }); w.postMessage({ type: "stop" });
setTimeout(() => w.terminate(), 1000 + i * 100) setTimeout(() => w.terminate(), 1000 + i * 100);
}); });
workers = []; workers = [];
active = false; active = false;
solution = e.data.nonce; solution = e.data.nonce;
document.getElementById("captcha-result").textContent = document.getElementById("captcha-result").textContent =
"Verification completed. Submitting solution for verification..."; "Solution found. Verifying with server...";
document.getElementById("captcha-control").style.display = "none"; document.getElementById("captcha-control").style.display = "none";
submitVerification(); submitVerification();
break; break;
@@ -233,6 +245,21 @@
estimateProgress(); estimateProgress();
} }
function copyToClipboard(text) {
if (!navigator.clipboard) {
const textArea = document.createElement("textarea");
textArea.value = text;
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
document.execCommand("copy");
textArea.remove();
} else {
navigator.clipboard.writeText(text);
}
alert("Copied to clipboard.");
}
function loadNewChallenge(c, s) { function loadNewChallenge(c, s) {
const btn = document.getElementById("worker-control"); const btn = document.getElementById("worker-control");
btn.textContent = "Start verification"; btn.textContent = "Start verification";
@@ -248,6 +275,7 @@
startTime = 0; startTime = 0;
lastUpdateTime = 0; lastUpdateTime = 0;
elapsedTime = 0; elapsedTime = 0;
totalHashes = 0;
const targetValue = challenge.d.slice(0, -1); const targetValue = challenge.d.slice(0, -1);
const hashLength = challenge.hl; const hashLength = challenge.hl;
workFactor = Number(BigInt(2) ** BigInt(8 * hashLength) / BigInt(targetValue)); workFactor = Number(BigInt(2) ** BigInt(8 * hashLength) / BigInt(targetValue));
@@ -329,52 +357,53 @@
document.getElementById("captcha-progress").style.display = "none"; document.getElementById("captcha-progress").style.display = "none";
document.getElementById("captcha-result").innerHTML = ` document.getElementById("captcha-result").innerHTML = `
<p style="color: green">Verification complete</p> <p style="color: green">Verification complete</p>
<p>Your user token is: <code>${data.token}</code></p> <p>Your user token is: <code>${data.token}</code> <button id="copy-token" onclick="copyToClipboard('${data.token}')">📋</button></p>
<p>Valid until: ${new Date(Date.now() + lifetime * 3600 * 1000).toLocaleString()}</p> <p>Valid until: ${new Date(Date.now() + lifetime * 3600 * 1000).toLocaleString()}</p>
`; `;
} }
}); });
} }
function formatTime(time) {
if (time < 60) {
return time.toFixed(1) + "s";
} else if (time < 3600) {
const minutes = Math.floor(time / 60);
const seconds = Math.floor(time % 60);
return minutes + "m " + seconds + "s";
} else {
const hours = Math.floor(time / 3600);
const minutes = Math.floor((time % 3600) / 60);
return hours + "h " + minutes + "m";
}
}
function estimateProgress() { function estimateProgress() {
if (reports % workers.length !== 0) { // if (reports % workers.length !== 0) {
// return;
// }
if (Date.now() - lastUpdateTime < 500) {
return; return;
} }
elapsedTime += (Date.now() - lastUpdateTime) / 1000; elapsedTime += (Date.now() - lastUpdateTime) / 1000;
lastUpdateTime = Date.now(); lastUpdateTime = Date.now();
const hashRate = totalHashes / elapsedTime; const hashRate = totalHashes / elapsedTime;
const timeRemaining = (workFactor - totalHashes) / hashRate; const timeRemaining = (workFactor - totalHashes) / hashRate;
const progress = 100 * (1 - Math.exp(-totalHashes / workFactor)); // const progress = 100 * (1 - Math.exp(-totalHashes / workFactor));
const formatTime = (time) => {
if (time < 60) {
return time.toFixed(1) + "s";
} else if (time < 3600) {
const minutes = Math.floor(time / 60);
const seconds = Math.floor(time % 60);
return minutes + "m " + seconds + "s";
} else {
const hours = Math.floor(time / 3600);
const minutes = Math.floor((time % 3600) / 60);
return hours + "h " + minutes + "m";
}
};
const p = 1 / workFactor; const p = 1 / workFactor;
const odds = ((1 - p) ** totalHashes * 100).toFixed(2); const odds = ((1 - p) ** totalHashes * 100).toFixed(3);
const progress = 100 - odds;
let note = ""; // let note = " (" + odds + "% odds of no solution yet)";
if (odds < 33) {
note = " (" + odds + "% odds of no solution yet)";
}
document.getElementById("captcha-progress").style.width = Math.min(progress, 100) + "%"; document.querySelector("#captcha-progress>.progress").style.width = Math.min(progress, 100) + "%";
document.getElementById("captcha-progress-text").value = ` document.getElementById("captcha-progress-text").value = `
Solution probability: 1 in ${workFactor.toLocaleString()} hashes Solution probability: 1 in ${workFactor.toLocaleString()} hashes
Hashes computed: ${totalHashes.toLocaleString()}${note} Hashes computed: ${totalHashes.toLocaleString()}
Luckiness: ${odds}%
Elapsed time: ${formatTime(elapsedTime)} Elapsed time: ${formatTime(elapsedTime)}
Hash rate: ${hashRate.toFixed(2)} H/s Hash rate: ${hashRate.toFixed(2)} H/s
Workers: ${workers.length}${isMobileWebkit ? " (iOS/iPadOS detected)" : ""} Workers: ${workers.length}${isMobileWebkit ? " (iOS/iPadOS detected)" : ""}
${active ? `Average time remaining: ${formatTime(timeRemaining)}` : "Verification task stopped"}`.trim(); ${active ? `Average time remaining: ${formatTime(timeRemaining)}` : "Verification stopped"}`.trim();
} }
</script> </script>
+87 -43
View File
@@ -1,20 +1,27 @@
<%- include("partials/shared_header", { title: "Request User Token" }) %> <%- include("partials/shared_header", { title: "Request User Token" }) %>
<style> <style>
#request-buttons { #request-container {
display: flex; display: flex;
justify-content: space-between; flex-direction: column;
margin-top: 20px; align-items: center;
width: 400px; margin: 20px 0;
width: 100%;
gap: 10px;
} }
#request-buttons button { #request-container button {
margin: 0 10px;
flex: 1; flex: 1;
width: 100%;
max-width: 300px;
padding: 10px 20px; padding: 10px 20px;
font-size: 16px; font-size: 16px;
cursor: pointer; cursor: pointer;
} }
#refresh-token-input {
width: 100%;
}
</style> </style>
<h1>Request User Token</h1> <h1>Request User Token</h1>
@@ -28,37 +35,70 @@
</div> </div>
</div> </div>
<% } %> <% } %>
<div id="existing-token" style="display: none"> <div id="request-container">
<p>It looks like you might have an older temporary user token. If it has expired, you can try to refresh it.</p> <button id="request-token" onclick="requestChallenge('new')">Get a new token</button>
<strong id="existing-token-value">Existing token:</strong> <button id="refresh-token-toggle" onclick="switchSection('refresh')">Refresh an old token</button>
<h6 id="existing-token-value" style="display: none">Existing token:</h6>
</div> </div>
<div id="request-buttons"> <div id="back-to-menu" style="display: none">
<button disabled id="refresh-token" onclick="requestChallenge('refresh')">Refresh old token</button> <a href="#" onclick="switchSection('root')">« Back</a>
<button id="request_token" onclick="requestChallenge('new')">Request new token</button> </div>
<div id="refresh-container" style="display: none">
<div id="existing-token">
<p>
If you have an existing or expired token, enter it here to try to refresh it by completing a shorter verification.
</p>
<div>
<label for="refresh-token-input">Existing token:</label>
<input type="text" id="refresh-token-input" />
<button id="refresh-token" onclick="requestChallenge('refresh')">Refresh</button>
</div>
</div>
</div> </div>
<%- include("partials/user_challenge_widget") %> <%- include("partials/user_challenge_widget") %>
<script> <script>
function switchSection(sectionId) {
const backToMenu = document.getElementById("back-to-menu");
const captchaSection = document.getElementById("captcha-container");
const requestSection = document.getElementById("request-container");
const refreshSection = document.getElementById("refresh-container");
[backToMenu, captchaSection, requestSection, refreshSection].forEach((element) => (element.style.display = "none"));
switch (sectionId) {
case "root":
requestSection.style.display = "flex";
maybeLoadExistingToken();
break;
case "captcha":
captchaSection.style.display = "block";
backToMenu.style.display = "block";
break;
case "refresh":
refreshSection.style.display = "block";
backToMenu.style.display = "block";
document.getElementById("refresh-token-input").focus();
break;
}
}
function requestChallenge(action) { function requestChallenge(action) {
const token = localStorage.getItem("captcha-temp-token"); const savedToken = localStorage.getItem("captcha-temp-token");
if (token && action === "new") { const refreshInput = document.getElementById("refresh-token-input").value;
const data = JSON.parse(token); if (savedToken && action === "new") {
const { expires } = data; const confirmation = confirm(
const expiresDate = new Date(expires); "It looks like you might already have an existing token. Are you sure you want to request a new one?"
const now = new Date(); );
if (expiresDate > now) { if (!confirmation) {
if (!confirm("You already have an existing token. Are you sure you want to request a new one?")) { return;
return;
}
localStorage.removeItem("captcha-temp-token");
document.getElementById("existing-token").style.display = "none";
document.getElementById("refresh-token").disabled = true;
} }
} else if (!token && action === "refresh") { localStorage.removeItem("captcha-temp-token");
alert("You don't have an existing token to refresh"); document.getElementById("existing-token").style.display = "none";
document.getElementById("refresh-token").disabled = true;
} else if (!refreshInput?.length && action === "refresh") {
alert("You need to provide a token to refresh.");
return; return;
} }
const refreshToken = token && action === "refresh" ? JSON.parse(token).token : undefined; const refreshToken = action === "refresh" ? refreshInput : undefined;
const keyInput = document.getElementById("proxy-key"); const keyInput = document.getElementById("proxy-key");
const proxyKey = (keyInput && keyInput.value) || undefined; const proxyKey = (keyInput && keyInput.value) || undefined;
if (!proxyKey?.length) { if (!proxyKey?.length) {
@@ -79,7 +119,7 @@
} }
const { challenge, signature } = data; const { challenge, signature } = data;
loadNewChallenge(challenge, signature); loadNewChallenge(challenge, signature);
document.getElementById("request-buttons").style.display = "none"; switchSection("captcha");
}) })
.catch(function (error) { .catch(function (error) {
console.error(error); console.error(error);
@@ -87,22 +127,26 @@
}); });
} }
const existingToken = localStorage.getItem("captcha-temp-token"); function maybeLoadExistingToken() {
if (existingToken) { const existingToken = localStorage.getItem("captcha-temp-token");
const data = JSON.parse(existingToken); if (existingToken) {
const { token, expires } = data; const data = JSON.parse(existingToken);
const expiresDate = new Date(expires); const { token, expires } = data;
document.getElementById( const expiresDate = new Date(expires);
"existing-token-value" document.getElementById(
).textContent = `Your token: ${token} (valid until ${expiresDate.toLocaleString()})`; "existing-token-value"
document.getElementById("existing-token").style.display = "block"; ).textContent = `User token: ${token} (valid until ${expiresDate.toLocaleString()})`;
document.getElementById("refresh-token").disabled = false; document.getElementById("existing-token-value").style.display = "block";
document.getElementById("refresh-token-input").value = token;
}
const proxyKey = localStorage.getItem("captcha-proxy-key");
if (proxyKey && document.getElementById("proxy-key")) {
document.getElementById("proxy-key").value = proxyKey;
}
} }
const proxyKey = localStorage.getItem("captcha-proxy-key"); switchSection("root");
if (proxyKey && document.getElementById("proxy-key")) {
document.getElementById("proxy-key").value = proxyKey;
}
</script> </script>
<%- include("partials/user_footer") %> <%- include("partials/user_footer") %>