SPB Git forge
28commits 1branches 0releases
7.7 MBsize
maindefault branch
10 days agolast push
Python 66.3% TypeScript 22.7% JavaScript 8.6% HTML 1.4% CSS 0.7%
3.0 KB · 96 lines typescript
Raw Blame History
1'use client';2/**3 * Owner token for watchlists and alerts (no account, spec §55): a random string generated once in the browser4 * (`crypto.randomUUID()` twice → 72 chars) and kept in localStorage. Sent as `X-CA-Owner-Token`; the API stores a hash.5 * Losing the browser storage loses the watchlist — the /watchlist page says so and offers export/import of the token.6 */7import { useEffect, useState } from 'react';89export const OWNER_KEY = 'ca-owner-token';10const EVENT = 'ca-owner-change';1112export function readOwnerToken(): string | null {13  if (typeof window === 'undefined') return null;14  try {15    const v = window.localStorage.getItem(OWNER_KEY);16    return v && v.length >= 24 ? v : null;17  } catch {18    return null;19  }20}2122export function ensureOwnerToken(): string {23  const cur = readOwnerToken();24  if (cur) return cur;25  const t = `${crypto.randomUUID()}${crypto.randomUUID()}`.replace(/-/g, '');26  try {27    window.localStorage.setItem(OWNER_KEY, t);28    window.dispatchEvent(new CustomEvent(EVENT));29  } catch {30    /* storage disabled: token lives for this page only */31  }32  return t;33}3435export function setOwnerToken(t: string): boolean {36  if (t.trim().length < 24) return false;37  try {38    window.localStorage.setItem(OWNER_KEY, t.trim());39    window.dispatchEvent(new CustomEvent(EVENT));40    return true;41  } catch {42    return false;43  }44}4546/** Token after mount (null during SSR / first paint). `create` generates one when absent. */47export function useOwnerToken(create = false): string | null {48  const [token, setToken] = useState<string | null>(null);49  useEffect(() => {50    const read = () => setToken(create ? ensureOwnerToken() : readOwnerToken());51    read();52    window.addEventListener(EVENT, read);53    window.addEventListener('storage', read);54    return () => {55      window.removeEventListener(EVENT, read);56      window.removeEventListener('storage', read);57    };58  }, [create]);59  return token;60}6162/** Local mirror of watched slugs so watch buttons render instantly and work when the API is briefly unavailable. */63export const WATCHED_KEY = 'ca-watched';64const WATCHED_EVENT = 'ca-watched-change';65export function readWatched(): string[] {66  if (typeof window === 'undefined') return [];67  try {68    const arr = JSON.parse(window.localStorage.getItem(WATCHED_KEY) ?? '[]') as unknown;69    return Array.isArray(arr) ? arr.filter((x): x is string => typeof x === 'string') : [];70  } catch {71    return [];72  }73}74export function writeWatched(slugs: string[]) {75  try {76    window.localStorage.setItem(WATCHED_KEY, JSON.stringify([...new Set(slugs)]));77  } catch {78    /* ignore */79  }80  window.dispatchEvent(new CustomEvent(WATCHED_EVENT));81}82export function useWatched(): string[] {83  const [w, setW] = useState<string[]>([]);84  useEffect(() => {85    const read = () => setW(readWatched());86    read();87    window.addEventListener(WATCHED_EVENT, read);88    window.addEventListener('storage', read);89    return () => {90      window.removeEventListener(WATCHED_EVENT, read);91      window.removeEventListener('storage', read);92    };93  }, []);94  return w;95}96