diff options
Diffstat (limited to 'src/lib/utils.ts')
| -rw-r--r-- | src/lib/utils.ts | 123 |
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}`); +}; |
