aboutsummaryrefslogtreecommitdiffstats
path: root/src/lib/utils.ts
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib/utils.ts')
-rw-r--r--src/lib/utils.ts123
1 files changed, 111 insertions, 12 deletions
diff --git a/src/lib/utils.ts b/src/lib/utils.ts
index 2e1bb840..b6bbdb53 100644
--- a/src/lib/utils.ts
+++ b/src/lib/utils.ts
@@ -2,7 +2,8 @@ import { browser } from "$app/environment";
import { goto } from "$app/navigation";
import { page } from "$app/stores";
import { PUBLIC_COINOS_NETWORK, PUBLIC_COINOS_URL } from "$env/static/public";
-import { bytesToHex } from "@noble/hashes/utils";
+import { bytesToHex } from "@noble/hashes/utils.js";
+import { fail as svelteFail, redirect } from "@sveltejs/kit";
import { toast } from "@zerodevx/svelte-toast";
import {
differenceInDays,
@@ -20,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 {
@@ -35,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());
@@ -57,6 +69,7 @@ export const post = async (
headers = {
"content-type": "application/json",
accept: "application/json",
+ ...ipHeaders(),
...headers,
};
@@ -147,29 +160,54 @@ 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,
+ host?: string,
+ extraHeaders?: Record<string, string>,
) => {
- const maxAge = 380 * 24 * 60 * 60;
+ const headers: Record<string, string> = {
+ "content-type": "application/json",
+ accept: "application/json",
+ "cf-connecting-ip": ip,
+ ...extraHeaders,
+ };
+ if (host) headers["x-forwarded-host"] = host;
const res = await fetch(`${base}/login`, {
method: "POST",
body: JSON.stringify(user),
- headers: {
- "content-type": "application/json",
- accept: "application/json",
- "cf-connecting-ip": ip,
- },
+ headers,
});
+
+ const text = await res.text();
+
if (res.status === 401) {
- const text = await res.text();
if (text.startsWith("2fa")) throw new Error("2fa");
+ throw new Error(text);
}
- const { user: u, token } = await res.json();
+ const { user: u, token } = JSON.parse(text);
if (!token) throw new Error("Login failed");
+ setSession(cookies, user, u, token);
+};
+
+// Session cookies for a freshly issued JWT — shared by login and register,
+// which both get a token back from the server.
+const setSession = (
+ cookies,
+ user: { username: string; password: string },
+ u: { language?: string; nsec?: string },
+ token: string,
+) => {
+ const maxAge = 380 * 24 * 60 * 60;
const expires = new Date();
expires.setSeconds(expires.getSeconds() + maxAge);
@@ -192,6 +230,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);
@@ -456,3 +508,50 @@ export const ago = (t) => {
const days = differenceInDays(now, date);
return `${days}d`;
};
+
+export const register = async (user, ip, cookies, loginRedirect, host?, extraHeaders?: Record<string, string>) => {
+ let error;
+
+ const headers: Record<string, string> = {
+ ...extraHeaders,
+ };
+ if (ip) headers["cf-connecting-ip"] = ip;
+
+ let sk, token, u;
+ try {
+ ({ sk, token, ...u } = await post("/register", { user }, headers));
+ } catch (e) {
+ ({ message: error } = e as Error);
+ }
+
+ // /register verifies the recaptcha token itself now, and Google tokens
+ // are single-use: a follow-up /login carrying the same token comes back
+ // "failed captcha" for an account that was just created. The register
+ // response already includes the JWT, so start the session from it and
+ // only fall back to /login when registration failed (e.g. the username
+ // already exists and this is really a login).
+ if (token) {
+ setSession(cookies, user, u, token);
+ error = null;
+ } else {
+ try {
+ await login(user, cookies, ip, host, extraHeaders);
+ error = null;
+ } catch (e) {
+ const { message } = e as Error;
+ error ||= message;
+ }
+ }
+
+ const expires = new Date();
+ expires.setSeconds(expires.getSeconds() + 21000000);
+ cookies.set("sk", sk, {
+ path: "/",
+ expires,
+ httpOnly: false,
+ sameSite: "lax",
+ });
+
+ if (error) return svelteFail(400, { error });
+ redirect(303, loginRedirect || `/${user.username}`);
+};