aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/components/About.svelte59
-rw-r--r--src/components/Nostr.svelte2
-rw-r--r--src/hooks.server.ts14
-rw-r--r--src/lib/nostr.ts10
-rw-r--r--src/lib/parse.ts9
-rw-r--r--src/lib/server/ip.ts2
-rw-r--r--src/lib/utils.ts38
-rw-r--r--src/locales/en.json2
-rw-r--r--src/routes/(app)/+layout.server.ts19
-rw-r--r--src/routes/(app)/admin/+layout.server.ts10
-rw-r--r--src/routes/(app)/pos/+page.server.ts15
-rw-r--r--src/routes/(app)/pos/+page.svelte223
-rw-r--r--src/routes/(app)/receive/[...token]/+page.server.ts10
-rw-r--r--src/routes/(app)/reset/[code]/+page.server.ts3
-rw-r--r--src/routes/(app)/reset/[code]/+page.svelte60
-rw-r--r--src/routes/(app)/send/[...text]/+page.server.ts6
-rw-r--r--src/routes/(app)/send/lightning/[payreq]/[...memo]/+page.svelte4
-rw-r--r--src/routes/(app)/settings/+layout.svelte7
-rw-r--r--src/routes/(static)/+layout.server.ts9
-rw-r--r--src/routes/(static)/docs/+page.server.ts5
-rw-r--r--src/routes/(static)/docs/+page.svelte35
-rw-r--r--src/routes/(static)/forgot/+page.svelte53
-rw-r--r--src/routes/(static)/login/+page.server.ts17
-rw-r--r--src/routes/(static)/login/+page.svelte79
-rw-r--r--src/routes/(static)/register/+page.server.ts4
-rw-r--r--src/routes/(static)/register/+page.svelte54
-rw-r--r--src/routes/(static)/support/+page.svelte31
-rw-r--r--src/routes/+layout.server.ts21
28 files changed, 578 insertions, 223 deletions
diff --git a/src/components/About.svelte b/src/components/About.svelte
index a1c03204..331a5188 100644
--- a/src/components/About.svelte
+++ b/src/components/About.svelte
@@ -11,7 +11,7 @@
{
alt: "Nostr",
src: "/images/nostr.svg",
- url: "https://coinos.io/566c166f3adab0c8fba5da015b0b3bcc8eb3696b455f2a1d43bfbd97059646a8",
+ url: "https://coinos.io/98ae4da926c471c23fd12d1ebdd5839ba82917baa618e184e0c9916d93dcf4f7",
},
{
alt: "Github",
@@ -22,15 +22,6 @@
},
{ name: "cole", socials: [] },
];
-
- let locations = $state();
- onMount(async () => {
- try {
- ({ locations } = await fetch("/locations").then((r) => r.json()));
- } catch (e) {
- console.log(e);
- }
- });
</script>
<div class="flex justify-center">
@@ -59,9 +50,7 @@
<div class="mt-2 flex justify-center items-center space-x-5">
{#each member.socials as { alt, src, url }}
- <a
- href={url}
- class={`rounded-full p-2 hover:opacity-80`}
+ <a href={url} class={`rounded-full p-2 hover:opacity-80`}
><img {src} {alt} class="w-8" /></a
>
{/each}
@@ -77,29 +66,27 @@
</div>
</div>
-{#if locations?.length}
- <div>
- <h3 class="text-5xl font-medium mb-5 text-center">
- {$t("about.locations.header")}
- </h3>
- <p
- class="text-secondary text-xl w-full md:w-10/12 lg:w-2/3 2xl:w-[800px] mx-auto mb-10"
- >
- {@html $t("about.locations.description")}
- </p>
+<div>
+ <h3 class="text-5xl font-medium mb-5 text-center">
+ {$t("about.locations.header")}
+ </h3>
+ <p
+ class="text-secondary text-xl w-full md:w-10/12 lg:w-2/3 2xl:w-[800px] mx-auto mb-10"
+ >
+ {@html $t("about.locations.description")}
+ </p>
- <div class="relative">
- <a href="/map" class="contents">
- <img src="/map.png" class="mx-auto" alt="Map" />
- </a>
+ <div class="relative">
+ <a href="/map" class="contents">
+ <img src="/map.png" class="mx-auto" alt="Map" />
+ </a>
- <a href="/map" class="contents">
- <button
- class="bg-black text-white border border-4 rounded-full px-6 py-2 font-bold hover:bg-secondary absolute left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2 text-2xl"
- >
- {$t("about.locations.viewMap")}
- </button>
- </a>
- </div>
+ <a href="/map" class="contents">
+ <button
+ class="bg-black text-white border border-4 rounded-full px-6 py-2 font-bold hover:bg-secondary absolute left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2 text-2xl"
+ >
+ {$t("about.locations.viewMap")}
+ </button>
+ </a>
</div>
-{/if}
+</div>
diff --git a/src/components/Nostr.svelte b/src/components/Nostr.svelte
index a616194b..f6c870f9 100644
--- a/src/components/Nostr.svelte
+++ b/src/components/Nostr.svelte
@@ -25,8 +25,8 @@
let cancel = () => {
eventToSign.set();
showNsec = false;
- pubkey = undefined;
connectUrl = undefined;
+ $signer = "cancel";
};
let signerAvailable = $derived(
diff --git a/src/hooks.server.ts b/src/hooks.server.ts
new file mode 100644
index 00000000..bd9bbc01
--- /dev/null
+++ b/src/hooks.server.ts
@@ -0,0 +1,14 @@
+import { ipStore } from "$lib/server/ip";
+import { setIpGetter } from "$lib/utils";
+import type { Handle } from "@sveltejs/kit";
+
+setIpGetter(() => ipStore.getStore());
+
+export const handle: Handle = async ({ event, resolve }) => {
+ const ip =
+ event.request.headers.get("cf-connecting-ip") ||
+ event.request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ||
+ event.getClientAddress();
+
+ return ipStore.run(ip, () => resolve(event));
+};
diff --git a/src/lib/nostr.ts b/src/lib/nostr.ts
index 6a8b179a..beebba02 100644
--- a/src/lib/nostr.ts
+++ b/src/lib/nostr.ts
@@ -117,8 +117,14 @@ export const sign = async (event, user) => {
eventToSign.set(event);
let unsubscribe;
- const { method, params } = await new Promise((r) => {
- unsubscribe = $signer.subscribe((v) => v?.ready && r(v));
+ const { method, params } = await new Promise((r, j) => {
+ unsubscribe = $signer.subscribe((v) => {
+ if (v === "cancel") {
+ j("cancelled");
+ $signer.set(undefined);
+ }
+ v?.ready && r(v);
+ });
});
await unsubscribe();
const signedEvent = await signingMethods[method](event, params);
diff --git a/src/lib/parse.ts b/src/lib/parse.ts
index 9c18504a..6ff4556c 100644
--- a/src/lib/parse.ts
+++ b/src/lib/parse.ts
@@ -1,10 +1,10 @@
import { PUBLIC_DOMAIN } from "$env/static/public";
import { decode } from "$lib/bip21";
-import { get, isLiquid, post, sats } from "$lib/utils";
+import { auth, get, isLiquid, post, sats } from "$lib/utils";
import { redirect } from "@sveltejs/kit";
import { validate } from "bitcoin-address-validation";
-export default async (s, host) => {
+export default async (s, host, cookies) => {
if (!s) return;
let t = s.trim();
let amount;
@@ -58,7 +58,10 @@ export default async (s, host) => {
if (t.toLowerCase().startsWith("lnurl")) redirect(307, `/ln/${t}`);
if (t.includes(":")) t = t.split(":")[1];
- if (t.toLowerCase().startsWith("ln")) {
+ if (t.toLowerCase().startsWith("lnr")) {
+ const { iid } = await post("/sendinvoice", { invreq: t }, auth(cookies));
+ invoice = { id: iid };
+ } else if (t.toLowerCase().startsWith("ln")) {
try {
invoice = await get(`/invoice/${t}`);
if (invoice.user.username === "mint") throw new Error("mint payment");
diff --git a/src/lib/server/ip.ts b/src/lib/server/ip.ts
new file mode 100644
index 00000000..cfd933ac
--- /dev/null
+++ b/src/lib/server/ip.ts
@@ -0,0 +1,2 @@
+import { AsyncLocalStorage } from "node:async_hooks";
+export const ipStore = new AsyncLocalStorage<string>();
diff --git a/src/lib/utils.ts b/src/lib/utils.ts
index 28a219e1..b981a4d4 100644
--- a/src/lib/utils.ts
+++ b/src/lib/utils.ts
@@ -21,11 +21,22 @@ export function scroll(section: any) {
const base = browser ? "" : PUBLIC_COINOS_URL;
+let getIp: (() => string | undefined) | undefined;
+
+export const setIpGetter = (fn: () => string | undefined) => {
+ getIp = fn;
+};
+
+const ipHeaders = (): Record<string, string> => {
+ const ip = getIp?.();
+ return ip ? { "cf-connecting-ip": ip } : {};
+};
+
export const punk = (k: string) =>
`${Math.floor((parseInt(k.slice(-2), 16) / 256) * 64) + 1}.webp`;
export const g = (url: string, fetch: any, headers: object) =>
- fetch(base + url, { headers })
+ fetch(base + url, { headers: { ...ipHeaders(), ...headers } })
.then((r: Response) => r.text())
.then((body: string) => {
try {
@@ -36,7 +47,7 @@ export const g = (url: string, fetch: any, headers: object) =>
});
export const get = (url: string, headers = {}) => {
- return fetch(base + url, { headers })
+ return fetch(base + url, { headers: { ...ipHeaders(), ...headers } })
.then(async (r) => {
if (r.ok) return r.text();
throw new Error(await r.text());
@@ -58,6 +69,7 @@ export const post = async (
headers = {
"content-type": "application/json",
accept: "application/json",
+ ...ipHeaders(),
...headers,
};
@@ -148,7 +160,13 @@ export const info = (m: string) => {
};
export const login = async (
- user: { username: string; password: string },
+ user: {
+ username: string;
+ password: string;
+ token?: string;
+ challenge?: string;
+ recaptcha?: string;
+ },
cookies,
ip: string,
) => {
@@ -193,6 +211,20 @@ export const auth = (cookies) => ({
authorization: `Bearer ${cookies.get("token")}`,
});
+export const isInvalidTokenError = (e: unknown) => {
+ const message = (e as Error)?.message || "";
+ if (!message) return false;
+ const normalized = message.toLowerCase();
+ return (
+ normalized.includes("jwt") ||
+ normalized.includes("invalid token") ||
+ normalized.includes("token expired") ||
+ normalized.includes("expired token") ||
+ normalized.includes("unauthorized") ||
+ normalized.includes("not authorized")
+ );
+};
+
export const btc = (fiat: number, rate: number): number =>
Math.round((fiat * sats) / rate);
diff --git a/src/locales/en.json b/src/locales/en.json
index ae25ccc8..229c107c 100644
--- a/src/locales/en.json
+++ b/src/locales/en.json
@@ -212,7 +212,7 @@
"resetSent": "A password reset link will be sent to your email if we have it on file.",
"continue": "Continue",
"reset": "Password reset",
- "resetDesc": "We can send a reset link if you added and verified your email otherwise please reach out to",
+ "resetDesc": "Please send an email with your username and any other details you can recall to",
"optional": "Optional",
"submit": "Submit",
"token": "2FA Token",
diff --git a/src/routes/(app)/+layout.server.ts b/src/routes/(app)/+layout.server.ts
index e28d8b56..8b694241 100644
--- a/src/routes/(app)/+layout.server.ts
+++ b/src/routes/(app)/+layout.server.ts
@@ -1,4 +1,4 @@
-import { auth, get, sleep } from "$lib/utils";
+import { auth, get, isInvalidTokenError, sleep } from "$lib/utils";
import { error, redirect } from "@sveltejs/kit";
export async function load({ cookies, request, url, params }) {
@@ -12,8 +12,21 @@ export async function load({ cookies, request, url, params }) {
user = await get("/me", auth(cookies));
} catch (e) {
const { message } = e as Error;
- if (message.startsWith("Rate")) await sleep(3000);
- user = await get("/me", auth(cookies));
+ if (message.startsWith("Rate")) {
+ await sleep(3000);
+ try {
+ user = await get("/me", auth(cookies));
+ } catch (retryError) {
+ if (isInvalidTokenError(retryError) && pathname !== "/logout") {
+ redirect(307, "/logout");
+ }
+ throw retryError;
+ }
+ } else if (isInvalidTokenError(e) && pathname !== "/logout") {
+ redirect(307, "/logout");
+ } else {
+ throw e;
+ }
}
}
diff --git a/src/routes/(app)/admin/+layout.server.ts b/src/routes/(app)/admin/+layout.server.ts
index d4aa181d..a2da1a90 100644
--- a/src/routes/(app)/admin/+layout.server.ts
+++ b/src/routes/(app)/admin/+layout.server.ts
@@ -1,10 +1,16 @@
-import { auth, get } from "$lib/utils";
+import { auth, get, isInvalidTokenError } from "$lib/utils";
import { redirect } from "@sveltejs/kit";
export async function load({ cookies }) {
const token = cookies.get("token");
if (!token) redirect(307, "/login");
- const user = await get("/me", auth(cookies));
+ let user;
+ try {
+ user = await get("/me", auth(cookies));
+ } catch (e) {
+ if (isInvalidTokenError(e)) redirect(307, "/logout");
+ throw e;
+ }
if (!user) redirect(307, "/login");
return { user };
diff --git a/src/routes/(app)/pos/+page.server.ts b/src/routes/(app)/pos/+page.server.ts
index f7a3fda3..1df1bb33 100644
--- a/src/routes/(app)/pos/+page.server.ts
+++ b/src/routes/(app)/pos/+page.server.ts
@@ -1,11 +1,20 @@
import { PUBLIC_COINOS_URL } from "$env/static/public";
-import { fd } from "$lib/utils";
+import { auth, fd, get } from "$lib/utils";
import { hex } from "@scure/base";
+export async function load({ cookies }) {
+ let token;
+
+ try {
+ token = await get("/ro", auth(cookies));
+ } catch (e) {}
+
+ return { token };
+}
+
export const actions = {
default: async ({ fetch, request }) => {
const body = await fd(request);
- console.log("POSTING", body);
const res = await fetch(`${PUBLIC_COINOS_URL}/flash`, {
method: "POST",
body: JSON.stringify(body),
@@ -16,6 +25,6 @@ export const actions = {
});
const buf = new Uint8Array(await res.arrayBuffer());
- return { data: hex.encode(buf) };
+ return { bytes: hex.encode(buf) };
},
};
diff --git a/src/routes/(app)/pos/+page.svelte b/src/routes/(app)/pos/+page.svelte
index 760a89b4..6f3c5d24 100644
--- a/src/routes/(app)/pos/+page.svelte
+++ b/src/routes/(app)/pos/+page.svelte
@@ -3,103 +3,220 @@
import { ESPLoader, Transport } from "esptool-js";
import { hex } from "@scure/base";
+ // Convert Uint8Array → JS "binary string" in safe chunks
function bs(uint8Array) {
let binaryString = "";
- const chunkSize = 0x8000; // 32,768 bytes (safe limit)
-
+ const chunkSize = 0x8000;
for (let i = 0; i < uint8Array.length; i += chunkSize) {
const chunk = uint8Array.subarray(i, i + chunkSize);
binaryString += String.fromCharCode(...chunk);
}
-
return binaryString;
}
- let { form } = $props();
+ let { data, form } = $props();
+ let { token } = $state(data);
- const address = 0x3d0000;
- const baudrate = 921600;
+ // --- Defaults ---
+ const LITTLEFS_ADDRESS_DEFAULT = 0x3d0000; // your existing LittleFS offset
+ const FW_ADDRESS_DEFAULT = 0x10000; // typical app offset on ESP32-C3
+ const CONNECT_BAUD = 115200; // initial
+ const WORK_BAUD = 921600; // fast flashing
- let file = $state();
- let ssid = $state();
- let key = $state();
- let token = $state();
+ // --- Shared state ---
let esploader = $state();
- let data = $derived(form ? hex.decode(form.data) : undefined);
-
let chip, transport;
- // let data = $state();
+ let connected = $state(false);
+ let portInfo = $state("");
+
+ // --- Existing config path (generated server-side to hex in form.bytes) ---
+ let bytes = $derived(form ? hex.decode(form.bytes) : undefined);
+ let littlefsAddress = $state(LITTLEFS_ADDRESS_DEFAULT);
+ let configProgress = $state(0);
+ let configDone = $state(false);
+
+ // --- New firmware flashing path ---
+ let fwFile = $state(null);
+ let fwBytes = $state(); // Uint8Array
+ let fwAddress = $state(FW_ADDRESS_DEFAULT);
+ let fwEraseAll = $state(false);
+ let fwProgress = $state(0);
+ let fwDone = $state(false);
+ let fwError = $state("");
+
+ // Simple tab toggle
+ let tab = $state("config"); // "config" | "firmware"
+
let connect = async () => {
try {
- let device = await navigator.serial.requestPort({});
+ const device = await navigator.serial.requestPort({});
transport = new Transport(device, true);
-
esploader = new ESPLoader({
transport,
- baudrate,
+ baudrate: CONNECT_BAUD,
});
chip = await esploader.main();
+
+ connected = true;
+ portInfo = (await device.getInfo?.()) ? JSON.stringify(await device.getInfo()) : "Connected";
} catch (e) {
console.log(e);
+ connected = false;
}
};
- let progress = $state();
- let flash = async () => {
- let fileArray = [{ data: bs(data), address }];
+ let flashConfig = async () => {
+ if (!esploader || !bytes) return;
+ configDone = false;
+ configProgress = 0;
- const flashOptions = {
+ const fileArray = [{ data: bs(bytes), address: Number(littlefsAddress) }];
+
+ await esploader.writeFlash({
fileArray,
flashSize: "keep",
eraseAll: false,
compress: true,
- reportProgress: (fileIndex, written, total) => {
- progress = parseInt((written / total) * 100);
+ reportProgress: (_fileIndex, written, total) => {
+ configProgress = Math.floor((written / total) * 100);
},
+ });
+
+ configDone = true;
+ };
+
+ function onPickFw(e) {
+ fwError = "";
+ fwDone = false;
+ const f = e.target.files?.[0];
+ fwFile = f || null;
+ if (!fwFile) {
+ fwBytes = undefined;
+ return;
+ }
+ const reader = new FileReader();
+ reader.onload = () => {
+ const buf = new Uint8Array(reader.result);
+ fwBytes = buf;
+ };
+ reader.onerror = () => {
+ fwError = "Failed to read file.";
+ fwBytes = undefined;
};
+ reader.readAsArrayBuffer(fwFile);
+ }
+
+ let flashFirmware = async () => {
+ if (!esploader) {
+ fwError = "Not connected.";
+ return;
+ }
+ if (!fwBytes?.length) {
+ fwError = "No firmware selected.";
+ return;
+ }
+ fwError = "";
+ fwDone = false;
+ fwProgress = 0;
- await esploader.writeFlash(flashOptions);
+ try {
+ const fileArray = [{ data: bs(fwBytes), address: Number(fwAddress) }];
+
+ await esploader.writeFlash({
+ fileArray,
+ flashSize: "keep",
+ eraseAll: Boolean(fwEraseAll),
+ compress: true,
+ reportProgress: (_fileIndex, written, total) => {
+ fwProgress = Math.floor((written / total) * 100);
+ },
+ });
+
+ fwDone = true;
+ } catch (e) {
+ console.error(e);
+ fwError = e?.message || "Flash failed.";
+ }
};
</script>
<div class="container px-4 max-w-lg mx-auto space-y-5 mt-20">
<h1 class="px-3 md:px-0 text-center text-3xl md:text-4xl font-semibold">
- Flash config
+ ESP32-C3 Flasher
</h1>
- {#if esploader}
- {#if data}
- {#if progress}
- <div class="text-center text-2xl">Done!</div>
+ {#if !connected}
+ <div class="text-center">
+ <button class="btn" onclick={connect}>Connect</button>
+ <div class="text-sm mt-2 opacity-70">{portInfo}</div>
+ </div>
+{/if}
+
+ {#if connected}
+ <!-- Tabs -->
+ <div class="flex gap-2 justify-center">
+ <button class="btn" class:btn-neutral={tab === "config"} onclick={() => (tab = "config")}>Flash config</button>
+ <button class="btn" class:btn-neutral={tab === "firmware"} onclick={() => (tab = "firmware")}>Flash firmware</button>
+ </div>
+
+ {#if tab === "config"}
+ <!-- Existing CONFIG FLOW -->
+ {#if bytes}
+ {#if configDone}
+ <div class="text-center text-2xl">Config written ✔</div>
+ {:else}
+ <div>Generated config file (<b>{bytes.length} bytes</b>)</div>
+ <label class="label mt-2">LittleFS address (hex)</label>
+ <input class="input" bind:value={littlefsAddress} onchange={(e) => (littlefsAddress = Number(e.target.value))} />
+ <div class="mt-3">
+ <button class="btn" onclick={flashConfig}>Flash config</button>
+ </div>
+ {#if configProgress > 0}<div class="mt-2">Progress: {configProgress}%</div>{/if}
+ {/if}
{:else}
- <div>Generated config file (<b>{data.length * 2} bytes</b>)</div>
- <button class="btn" onclick={flash}>Flash</button>
+ <form method="POST" use:enhance class="space-y-2">
+ <input name="ssid" placeholder="Wifi SSID" class="input" />
+ <input name="key" placeholder="Wifi password" class="input" />
+ <input name="token" bind:value={token} placeholder="Coinos API token" class="input" />
+ <button type="submit" class="btn">Generate Config</button>
+ </form>
{/if}
{:else}
- <form method="POST" use:enhance class="space-y-2">
- <input
- name="ssid"
- bind:value={ssid}
- placeholder="Wifi SSID"
- class="input"
- />
- <input
- name="key"
- bind:value={key}
- placeholder="Wifi password"
- class="input"
- />
- <input
- name="token"
- bind:value={token}
- placeholder="Coinos API token"
- class="input"
- />
- <button type="submit" class="btn">Generate Config</button>
- </form>
+ <!-- NEW FIRMWARE FLOW -->
+ <div class="space-y-3">
+ <label class="label">Select .ino.bin (firmware)</label>
+ <input type="file" accept=".bin,application/octet-stream" class="input" onchange={onPickFw} />
+
+ <label class="label">Firmware address (hex, default 0x10000)</label>
+ <input class="input" bind:value={fwAddress} onchange={(e) => (fwAddress = Number(e.target.value))} />
+
+ <label class="flex items-center gap-2 mt-2">
+ <input type="checkbox" bind:checked={fwEraseAll} />
+ <span>Erase all flash (usually NOT needed)</span>
+ </label>
+
+ <div class="mt-3">
+ <button class="btn" disabled={!fwBytes} onclick={flashFirmware}>Flash firmware</button>
+ </div>
+
+ {#if fwProgress > 0 && !fwDone}
+ <div>Progress: {fwProgress}%</div>
+ {/if}
+ {#if fwDone}
+ <div class="text-center text-2xl">Firmware written ✔</div>
+ {/if}
+ {#if fwError}
+ <div class="text-red-600">{fwError}</div>
+ {/if}
+ </div>
{/if}
- {:else}
- <button class="btn" onclick={connect}>Connect</button>
{/if}
</div>
+
+<style>
+ .btn { @apply px-4 py-2 rounded-2xl shadow; }
+ .btn-neutral { @apply bg-gray-200; }
+ .input { @apply w-full px-3 py-2 border rounded-xl; }
+ .label { @apply text-sm opacity-70; }
+</style>
diff --git a/src/routes/(app)/receive/[...token]/+page.server.ts b/src/routes/(app)/receive/[...token]/+page.server.ts
index 05b2dad8..052d507f 100644
--- a/src/routes/(app)/receive/[...token]/+page.server.ts
+++ b/src/routes/(app)/receive/[...token]/+page.server.ts
@@ -1,17 +1,17 @@
+import parse from "$lib/parse";
import { fd } from "$lib/utils";
import { fail, redirect } from "@sveltejs/kit";
-import parse from "$lib/parse";
-export async function load({ params, parent, url }) {
+export async function load({ cookies, params, parent, url }) {
const { user } = await parent();
if (!user) redirect(307, `/register?redirect=${url.pathname}`);
- await parse(params.token, url.host);
+ await parse(params.token, url.host, cookies);
}
export const actions = {
- default: async ({ request, url }) => {
+ default: async ({ cookies, request, url }) => {
const { text } = await fd(request);
- await parse(text, url.host);
+ await parse(text, url.host, cookies);
return fail(400, { error: "default" });
},
};
diff --git a/src/routes/(app)/reset/[code]/+page.server.ts b/src/routes/(app)/reset/[code]/+page.server.ts
index 533e21f5..35d49be9 100644
--- a/src/routes/(app)/reset/[code]/+page.server.ts
+++ b/src/routes/(app)/reset/[code]/+page.server.ts
@@ -7,11 +7,12 @@ export const actions = {
let error;
const { code } = params;
- const { password } = await fd(request);
+ const { password, recaptcha } = await fd(request);
try {
const user = await post("/reset", { code, password });
user.password = password;
+ user.recaptcha = recaptcha;
try {
await login(user, cookies, ip);
diff --git a/src/routes/(app)/reset/[code]/+page.svelte b/src/routes/(app)/reset/[code]/+page.svelte
index df9d1144..ce43a79d 100644
--- a/src/routes/(app)/reset/[code]/+page.svelte
+++ b/src/routes/(app)/reset/[code]/+page.svelte
@@ -1,14 +1,71 @@
<script>
+ import { PUBLIC_RECAPTCHA_SITE_KEY } from "$env/static/public";
+ import { browser } from "$app/environment";
+ import { onDestroy, onMount, tick } from "svelte";
import { t } from "$lib/translations";
import PasswordInput from "$comp/PasswordInput.svelte";
import Icon from "$comp/Icon.svelte";
import Spinner from "$comp/Spinner.svelte";
import { fly } from "svelte/transition";
import { password } from "$lib/store";
+ import { fail } from "$lib/utils";
let { form } = $props();
let email;
let loading;
+ let recaptchaToken = "";
+ let recaptchaSiteKey = PUBLIC_RECAPTCHA_SITE_KEY;
+ let isTor = browser && location.hostname.endsWith(".onion");
+
+ onMount(() => {
+ if (!isTor && recaptchaSiteKey) {
+ let s = document.createElement("script");
+ s.src = "https://www.google.com/recaptcha/api.js?render=" + recaptchaSiteKey;
+ document.head.appendChild(s);
+ }
+ });
+
+ const getRecaptchaToken = () =>
+ new Promise((resolve, reject) => {
+ if (isTor) return resolve("");
+ if (!browser || !grecaptcha) return reject(new Error("captcha unavailable"));
+ grecaptcha.ready(() => {
+ grecaptcha
+ .execute(recaptchaSiteKey, { action: "reset" })
+ .then(resolve)
+ .catch(reject);
+ });
+ });
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+ loading = true;
+ try {
+ recaptchaToken = await getRecaptchaToken();
+ await tick();
+ e.currentTarget.submit();
+ } catch (err) {
+ fail(err.message || "captcha failed");
+ loading = false;
+ }
+ };
+
+ onDestroy(() => {
+ if (!browser) return;
+ const nodeBadge = document.querySelector(".grecaptcha-badge");
+ if (nodeBadge) {
+ document.body.removeChild(nodeBadge.parentNode);
+ }
+
+ const scriptSelector =
+ "script[src='https://www.google.com/recaptcha/api.js?render=" +
+ recaptchaSiteKey +
+ "']";
+ const script = document.querySelector(scriptSelector);
+ if (script) {
+ script.remove();
+ }
+ });
</script>
<div class="pt-10">
@@ -30,7 +87,8 @@
</div>
{/if}
- <form class="space-y-5" method="POST">
+ <form class="space-y-5" method="POST" onsubmit={handleSubmit}>
+ <input type="hidden" name="recaptcha" value={recaptchaToken} />
<PasswordInput
bind:value={$password}
placeholder={$t("login.password")}
diff --git a/src/routes/(app)/send/[...text]/+page.server.ts b/src/routes/(app)/send/[...text]/+page.server.ts
index 9f0891d9..59d53dbe 100644
--- a/src/routes/(app)/send/[...text]/+page.server.ts
+++ b/src/routes/(app)/send/[...text]/+page.server.ts
@@ -9,16 +9,16 @@ export async function load({ cookies, depends, params, parent, url }) {
if (!user) redirect(307, "/login");
const { text } = params;
- await parse(text, url.host);
+ await parse(text, url.host, cookies);
const contacts = await get(`/contacts/${limit}`, auth(cookies));
return { contacts };
}
export const actions = {
- default: async ({ request, url }) => {
+ default: async ({ cookies, request, url }) => {
const { text } = await fd(request);
- await parse(text, url.host);
+ await parse(text, url.host, cookies);
return fail(400, { error: "default" });
},
};
diff --git a/src/routes/(app)/send/lightning/[payreq]/[...memo]/+page.svelte b/src/routes/(app)/send/lightning/[payreq]/[...memo]/+page.svelte
index a5cc168c..2b7c008a 100644
--- a/src/routes/(app)/send/lightning/[payreq]/[...memo]/+page.svelte
+++ b/src/routes/(app)/send/lightning/[payreq]/[...memo]/+page.svelte
@@ -28,9 +28,9 @@
let toggle = () => (show = !show);
let amount = $derived(form?.amount || data.amount);
let maxfee = $state(
- Math.max(5, Math.round(untrack(() => amount) * 0.005 || 0)),
+ Math.max(5, Math.round(untrack(() => amount) * 0.02 || 0)),
);
- $effect(() => (maxfee = Math.max(5, Math.round(amount * 0.005) || 0)));
+ $effect(() => (maxfee = Math.max(5, Math.round(amount * 0.02) || 0)));
</script>
<div class="container px-4 max-w-xl mx-auto text-center space-y-2">
diff --git a/src/routes/(app)/settings/+layout.svelte b/src/routes/(app)/settings/+layout.svelte
index 24c54a97..e603cbf4 100644
--- a/src/routes/(app)/settings/+layout.svelte
+++ b/src/routes/(app)/settings/+layout.svelte
@@ -176,8 +176,7 @@
event = await sign(event, user);
send(event);
} catch (e) {
- console.log(e);
- warning("Nostr profile could not be updated");
+ warning("Nostr profile not updated");
}
}
@@ -252,9 +251,7 @@
{$t("user.settings.header")}
</h1>
- <div
- class="flex flex-wrap justify-around items-center border-b pb-3"
- >
+ <div class="flex flex-wrap justify-around items-center border-b pb-3">
{#each tabs as { name, key }}
<a
href={`/settings/${name}`}
diff --git a/src/routes/(static)/+layout.server.ts b/src/routes/(static)/+layout.server.ts
index c780b5e0..5de752fa 100644
--- a/src/routes/(static)/+layout.server.ts
+++ b/src/routes/(static)/+layout.server.ts
@@ -1,4 +1,5 @@
-import { get, auth } from "$lib/utils";
+import { auth, get, isInvalidTokenError } from "$lib/utils";
+import { redirect } from "@sveltejs/kit";
export async function load({ cookies }) {
const token = cookies.get("token");
@@ -7,7 +8,11 @@ export async function load({ cookies }) {
if (token) {
try {
user = await get("/me", auth(cookies));
- } catch (e) {}
+ } catch (e) {
+ if (isInvalidTokenError(e)) {
+ redirect(307, "/logout");
+ }
+ }
}
return { user };
diff --git a/src/routes/(static)/docs/+page.server.ts b/src/routes/(static)/docs/+page.server.ts
index 86f95824..93ba544c 100644
--- a/src/routes/(static)/docs/+page.server.ts
+++ b/src/routes/(static)/docs/+page.server.ts
@@ -1,4 +1,7 @@
+import { auth, get } from "$lib/utils";
export const load = async ({ cookies }) => {
const token = cookies.get("token");
- return { token };
+ let ro;
+ if (token) ro = await get("/ro", auth(cookies));
+ return { ro, token };
};
diff --git a/src/routes/(static)/docs/+page.svelte b/src/routes/(static)/docs/+page.svelte
index f53787e5..c323f9bd 100644
--- a/src/routes/(static)/docs/+page.svelte
+++ b/src/routes/(static)/docs/+page.svelte
@@ -7,7 +7,7 @@
let { data } = $props();
- let { user, token } = data;
+ let { user, ro, token } = data;
let api = PUBLIC_DOMAIN.includes("localhost")
? `${PUBLIC_COINOS_URL}`
@@ -16,6 +16,8 @@
let tokenSample = token
? `export token="${token}"`
: `export token=<your auth token>`;
+
+ let roSample = `export token="${ro}"`;
</script>
{#snippet cp(something)}
@@ -59,7 +61,7 @@
or get one from the /login endpoint.
{/if}
- Save it in a variable called <b>$token</b> to run the examples.
+ Save it in a variable called <b>$token</b> to run the examples. It gives full access to your account so keep it safe.
</p>
<div
@@ -69,6 +71,19 @@
{@render cp(tokenSample)}
</div>
+ {#if ro}
+ <p class="text-secondary">
+ This read-only token has permission to create invoices and fetch
+ payments but can't be used to make withdrawals
+ </p>
+ <div
+ class="bg-primary text-primary-content rounded-lg p-4 flex gap-4 items-center"
+ >
+ <div class="w-full break-all">{roSample}</div>
+ {@render cp(roSample)}
+ </div>
+ {/if}
+
<h2 class="text-2xl">POST /register</h2>
<p class="text-secondary">
Register a new user account with a username and password
@@ -92,9 +107,13 @@
<div class="font-bold">display</div>
<div class="col-span-2">string - spaces allowed</div>
<div class="font-bold">currency</div>
- <div class="col-span-2">string - three-letter ISO currency code e.g. CAD or USD</div>
+ <div class="col-span-2">
+ string - three-letter ISO currency code e.g. CAD or USD
+ </div>
<div class="font-bold">language</div>
- <div class="col-span-2">string - two-letter language code e.g. en, fr, es</div>
+ <div class="col-span-2">
+ string - two-letter language code e.g. en, fr, es
+ </div>
</div>
<Code sample="update" />
@@ -108,7 +127,9 @@
<div class="font-bold">type (string, default: lightning)</div>
<div class="col-span-2">bitcoin or lightning</div>
<div class="font-bold">fiat (boolean, default: false)</div>
- <div class="col-span-2">whether the amount is denominated in satoshis or local currency</div>
+ <div class="col-span-2">
+ whether the amount is denominated in satoshis or local currency
+ </div>
<div class="font-bold">amount (numeric)</div>
<div class="col-span-2">amount to be paid</div>
<div class="font-bold">webhook (string, optional)</div>
@@ -116,7 +137,9 @@
remote endpoint to call when the invoice is paid
</div>
<div class="font-bold">secret (string, optional)</div>
- <div class="col-span-2">a secret value passed to the webhook to authenticate the request</div>
+ <div class="col-span-2">
+ a secret value passed to the webhook to authenticate the request
+ </div>
</div>
</div>
diff --git a/src/routes/(static)/forgot/+page.svelte b/src/routes/(static)/forgot/+page.svelte
index 45180543..49a837ae 100644
--- a/src/routes/(static)/forgot/+page.svelte
+++ b/src/routes/(static)/forgot/+page.svelte
@@ -6,8 +6,6 @@
import Spinner from "$comp/Spinner.svelte";
import { fly } from "svelte/transition";
- let { form } = $props();
- let email = $state();
let loading;
</script>
@@ -16,51 +14,8 @@
>
<h1 class="text-2xl font-bold text-center">{$t("login.reset")}</h1>
- {#if form?.error}
- <div class="text-red-600 text-center" in:fly>
- {form.error}
- </div>
- {/if}
-
- {#if form?.success}
- <div class="text-secondary">
- {$t("login.resetSent")}
- </div>
-
- <div class="flex w-full">
- <a href="/login" class="btn">
- {$t("login.continue")}
- </a>
- </div>
- {:else}
- <form class="space-y-5" method="POST" use:enhance>
- <div>
- <input
- placeholder={$t("login.email")}
- name="email"
- type="text"
- required
- class="bg-primary"
- bind:value={email}
- use:focus
- autocapitalize="none"
- />
- </div>
-
- <div class="text-secondary">
- {$t("login.resetDesc")}
- <a href="mailto:[email protected]" class="font-bold"
- >
- </div>
-
- <button type="submit" class="btn" disabled={loading}>
- {#if loading}
- <Spinner />
- {:else}
- {$t("login.submit")}
- {/if}
- </button>
- </form>
- {/if}
+ <div class="text-secondary">
+ {$t("login.resetDesc")}
+ <a href="mailto:[email protected]" class="font-bold">[email protected]</a>
+ </div>
</div>
diff --git a/src/routes/(static)/login/+page.server.ts b/src/routes/(static)/login/+page.server.ts
index ffe6335d..2469cb83 100644
--- a/src/routes/(static)/login/+page.server.ts
+++ b/src/routes/(static)/login/+page.server.ts
@@ -1,5 +1,5 @@
import { PUBLIC_COINOS_URL } from "$env/static/public";
-import { fd, get, login } from "$lib/utils";
+import { fd, get, login, post } from "$lib/utils";
import { fail, redirect } from "@sveltejs/kit";
export const load = async ({ parent }) => {
@@ -11,10 +11,15 @@ export const load = async ({ parent }) => {
export const actions = {
login: async ({ cookies, request }) => {
- console.log(request.headers.get('referer'));
const form = await fd(request);
- let { username, password, token, loginRedirect } = form;
- const user = { username, password, token };
+ let { username, password, token, loginRedirect, recaptcha } = form;
+
+ const user = {
+ username,
+ password,
+ token,
+ recaptcha,
+ };
if (loginRedirect === "undefined") loginRedirect = undefined;
@@ -30,7 +35,7 @@ export const actions = {
nostr: async ({ cookies, fetch, request }) => {
const form = await fd(request);
- let { challenge, event, loginRedirect } = form;
+ let { challenge, event, loginRedirect, recaptcha } = form;
if (loginRedirect === "null") loginRedirect = undefined;
event = JSON.parse(event);
@@ -38,7 +43,7 @@ export const actions = {
const res = await fetch(`${PUBLIC_COINOS_URL}/nostrAuth`, {
method: "POST",
- body: JSON.stringify({ challenge, event }),
+ body: JSON.stringify({ challenge, event, recaptcha }),
headers: {
"content-type": "application/json",
accept: "application/json",
diff --git a/src/routes/(static)/login/+page.svelte b/src/routes/(static)/login/+page.svelte
index 825f6a88..04044060 100644
--- a/src/routes/(static)/login/+page.svelte
+++ b/src/routes/(static)/login/+page.svelte
@@ -1,8 +1,9 @@
<script>
+ import { PUBLIC_RECAPTCHA_SITE_KEY } from "$env/static/public";
import { browser } from "$app/environment";
import handler from "$lib/handler";
import { applyAction, deserialize } from "$app/forms";
- import { onMount, tick } from "svelte";
+ import { onDestroy, onMount, tick } from "svelte";
import { fly } from "svelte/transition";
import { enhance } from "$app/forms";
import Pinpad from "$comp/Pinpad.svelte";
@@ -37,8 +38,18 @@
});
let { challenge } = $derived(data);
+ let recaptchaSiteKey = PUBLIC_RECAPTCHA_SITE_KEY;
+ let isTor = browser && location.hostname.endsWith(".onion");
let token = $state("");
+ onMount(() => {
+ if (!isTor && recaptchaSiteKey) {
+ let s = document.createElement("script");
+ s.src = "https://www.google.com/recaptcha/api.js?render=" + recaptchaSiteKey;
+ document.head.appendChild(s);
+ }
+ });
+
let cancel = () => (need2fa = false);
let username = $state(),
@@ -53,28 +64,36 @@
let revealPassword = $state(false);
- async function handleSubmit(e) {
- e.preventDefault();
- let data = new FormData(this);
- let user = Object.fromEntries(data);
-
- for (let k in user) {
- data.set(k, user[k]);
- }
-
- const response = await fetch("?/login", {
- method: "POST",
- body: data,
+ const getRecaptchaToken = () =>
+ new Promise((resolve, reject) => {
+ if (isTor) return resolve("");
+ if (!browser || !grecaptcha) return reject(new Error("captcha unavailable"));
+ grecaptcha.ready(() => {
+ grecaptcha
+ .execute(recaptchaSiteKey, { action: "login" })
+ .then(resolve)
+ .catch(reject);
+ });
});
- const result = deserialize(await response.text());
-
- if (result.type === "success") {
- await invalidateAll();
+ const enhanceLogin = async ({ formData, cancel }) => {
+ try {
+ const recaptcha = await getRecaptchaToken();
+ formData.set("recaptcha", recaptcha);
+ } catch (err) {
+ fail(err.message || "captcha failed");
+ cancel();
+ return;
}
- applyAction(result);
- }
+ return async ({ result }) => {
+ if (result.type === "success") {
+ await invalidateAll();
+ }
+
+ applyAction(result);
+ };
+ };
$effect(() => {
token?.length === 6 &&
@@ -104,10 +123,12 @@
const formData = new FormData();
try {
+ const recaptcha = await getRecaptchaToken();
formData.append("loginRedirect", redirect);
formData.append("token", token);
formData.append("event", JSON.stringify(signedEvent));
formData.append("challenge", challenge);
+ formData.append("recaptcha", recaptcha);
let response = await fetch("/login?/nostr", {
method: "POST",
@@ -121,6 +142,23 @@
fail(e.message);
}
};
+
+ onDestroy(() => {
+ if (!browser) return;
+ const nodeBadge = document.querySelector(".grecaptcha-badge");
+ if (nodeBadge) {
+ document.body.removeChild(nodeBadge.parentNode);
+ }
+
+ const scriptSelector =
+ "script[src='https://www.google.com/recaptcha/api.js?render=" +
+ recaptchaSiteKey +
+ "']";
+ const script = document.querySelector(scriptSelector);
+ if (script) {
+ script.remove();
+ }
+ });
</script>
<div
@@ -134,9 +172,8 @@
{/if}
<form
- use:enhance
+ use:enhance={enhanceLogin}
class="space-y-5"
- onsubmit={handleSubmit}
method="POST"
action="?/login"
>
diff --git a/src/routes/(static)/register/+page.server.ts b/src/routes/(static)/register/+page.server.ts
index cddd97ea..d2dbe299 100644
--- a/src/routes/(static)/register/+page.server.ts
+++ b/src/routes/(static)/register/+page.server.ts
@@ -15,11 +15,11 @@ export const actions = {
default: async ({ cookies, request }) => {
const ip = request.headers.get("cf-connecting-ip");
const form = await fd(request);
- const { picture, username, password } = form;
+ const { picture, username, password, challenge, recaptcha } = form;
let { loginRedirect } = form;
if (loginRedirect === "undefined") loginRedirect = undefined;
- const user = { picture, username, password };
+ const user = { picture, username, password, challenge, recaptcha };
return register(user, ip, cookies, loginRedirect);
},
};
diff --git a/src/routes/(static)/register/+page.svelte b/src/routes/(static)/register/+page.svelte
index b0368d47..bb3af128 100644
--- a/src/routes/(static)/register/+page.svelte
+++ b/src/routes/(static)/register/+page.svelte
@@ -1,5 +1,6 @@
<script>
- import { onMount } from "svelte";
+ import { PUBLIC_RECAPTCHA_SITE_KEY } from "$env/static/public";
+ import { onDestroy, onMount } from "svelte";
import { browser } from "$app/environment";
import punks from "$lib/punks";
import { upload } from "$lib/upload";
@@ -28,6 +29,16 @@
let { form, data } = $props();
let { challenge } = $derived(data);
+ let recaptchaSiteKey = PUBLIC_RECAPTCHA_SITE_KEY;
+ let isTor = browser && location.hostname.endsWith(".onion");
+
+ onMount(() => {
+ if (!isTor && recaptchaSiteKey) {
+ let s = document.createElement("script");
+ s.src = "https://www.google.com/recaptcha/api.js?render=" + recaptchaSiteKey;
+ document.head.appendChild(s);
+ }
+ });
onMount(() => {
if (browser) {
@@ -93,6 +104,18 @@
btn = $state();
let loading = $state();
+ const getRecaptchaToken = () =>
+ new Promise((resolve, reject) => {
+ if (isTor) return resolve("");
+ if (!browser || !grecaptcha) return reject(new Error("captcha unavailable"));
+ grecaptcha.ready(() => {
+ grecaptcha
+ .execute(recaptchaSiteKey, { action: "register" })
+ .then(resolve)
+ .catch(reject);
+ });
+ });
+
async function handleSubmit(e) {
e.preventDefault();
@@ -106,6 +129,15 @@
data.set(k, user[k]);
}
+ try {
+ const recaptcha = await getRecaptchaToken();
+ data.set("recaptcha", recaptcha);
+ } catch (err) {
+ fail(err.message || "captcha failed");
+ loading = false;
+ return;
+ }
+
data.set("picture", `${$page.url.origin}/api/public/${punks[index]}.webp`);
if ($avatar) {
try {
@@ -189,10 +221,12 @@
const formData = new FormData();
try {
+ const recaptcha = await getRecaptchaToken();
formData.append("loginRedirect", redirect);
formData.append("token", token);
formData.append("event", JSON.stringify(signedEvent));
formData.append("challenge", challenge);
+ formData.append("recaptcha", recaptcha);
let response = await fetch("/login?/nostr", {
method: "POST",
@@ -210,6 +244,23 @@
fail(e.message);
}
};
+
+ onDestroy(() => {
+ if (!browser) return;
+ const nodeBadge = document.querySelector(".grecaptcha-badge");
+ if (nodeBadge) {
+ document.body.removeChild(nodeBadge.parentNode);
+ }
+
+ const scriptSelector =
+ "script[src='https://www.google.com/recaptcha/api.js?render=" +
+ recaptchaSiteKey +
+ "']";
+ const script = document.querySelector(scriptSelector);
+ if (script) {
+ script.remove();
+ }
+ });
</script>
{#if need2fa}
@@ -277,6 +328,7 @@
value={$loginRedirect || $page.url.searchParams.get("redirect")}
/>
<input type="hidden" name="token" value={token} />
+ <input type="hidden" name="challenge" value={challenge} />
<label
for="username"
diff --git a/src/routes/(static)/support/+page.svelte b/src/routes/(static)/support/+page.svelte
index b9028cbf..4cf9706d 100644
--- a/src/routes/(static)/support/+page.svelte
+++ b/src/routes/(static)/support/+page.svelte
@@ -1,6 +1,7 @@
<script>
+ import { PUBLIC_RECAPTCHA_SITE_KEY } from "$env/static/public";
import { browser } from "$app/environment";
- import { onDestroy } from "svelte";
+ import { onDestroy, onMount } from "svelte";
import { fail, success, post } from "$lib/utils";
import { scale } from "svelte/transition";
import Icon from "$comp/Icon.svelte";
@@ -13,10 +14,30 @@
let email = $state();
let message = $state();
let sent = $state();
- let recaptchaSiteKey = "6LfCd8YkAAAAANmVJgzN3SQY3n3fv1RhiS5PgMYM";
+ let recaptchaSiteKey = PUBLIC_RECAPTCHA_SITE_KEY;
+ let isTor = browser && location.hostname.endsWith(".onion");
+
+ onMount(() => {
+ if (!isTor && recaptchaSiteKey) {
+ let s = document.createElement("script");
+ s.src = "https://www.google.com/recaptcha/api.js?render=" + recaptchaSiteKey;
+ document.head.appendChild(s);
+ }
+ });
let submit = (e) => {
e.preventDefault();
+ if (isTor) {
+ post("/post/email", {
+ username: user?.username || `${username} (unverified)`,
+ email,
+ message,
+ token: "",
+ })
+ .then(() => (sent = true))
+ .catch(() => fail("problem submitting"));
+ return;
+ }
grecaptcha.ready(() => {
grecaptcha.execute(recaptchaSiteKey, { action: "submit" }).then((token) =>
post("/post/email", {
@@ -49,12 +70,6 @@
});
</script>
-<svelte:head
- ><script
- src="https://www.google.com/recaptcha/api.js?render=6LfCd8YkAAAAANmVJgzN3SQY3n3fv1RhiS5PgMYM"
- ></script></svelte:head
->
-
<div class="container max-w-lg mx-auto space-y-8 p-4 pb-20">
{#if sent}
<h1 class="text-center text-3xl lg:text-4xl font-semibold">Thank you!</h1>
diff --git a/src/routes/+layout.server.ts b/src/routes/+layout.server.ts
index 584e836c..c1f77695 100644
--- a/src/routes/+layout.server.ts
+++ b/src/routes/+layout.server.ts
@@ -3,7 +3,8 @@ import {
loadTranslations,
locales,
} from "$lib/translations/index";
-import { auth, get, sleep } from "$lib/utils";
+import { auth, get, isInvalidTokenError, sleep } from "$lib/utils";
+import { redirect } from "@sveltejs/kit";
export const load = async ({ cookies, request, url }) => {
const { host, pathname } = url;
@@ -13,8 +14,22 @@ export const load = async ({ cookies, request, url }) => {
try {
user = await get("/me", auth(cookies));
} catch (e) {
- if (e.message.startsWith("Rate")) await sleep(3000);
- user = await get("/me", auth(cookies));
+ const { message } = e as Error;
+ if (message.startsWith("Rate")) {
+ await sleep(3000);
+ try {
+ user = await get("/me", auth(cookies));
+ } catch (retryError) {
+ if (isInvalidTokenError(retryError) && pathname !== "/logout") {
+ redirect(307, "/logout");
+ }
+ throw retryError;
+ }
+ } else if (isInvalidTokenError(e) && pathname !== "/logout") {
+ redirect(307, "/logout");
+ } else {
+ throw e;
+ }
}
}