From b780366bc95da14399a64c0ed0642f6e31a124b9 Mon Sep 17 00:00:00 2001 From: Igor S Date: Sun, 16 Nov 2025 19:11:52 +0100 Subject: [PATCH 1/2] Added web browser storage functions and fetching hook & fetchClient which appends token to url searchparams - as in aliceo2 framework --- .../webapp/app/routes/tokens/overview.tsx | 2 +- Tokenization/webapp/app/utils/fetcher.tsx | 97 +++++++++++++++++++ .../webapp/app/{hooks => utils}/session.tsx | 0 Tokenization/webapp/app/utils/storage.tsx | 49 ++++++++++ 4 files changed, 147 insertions(+), 1 deletion(-) create mode 100644 Tokenization/webapp/app/utils/fetcher.tsx rename Tokenization/webapp/app/{hooks => utils}/session.tsx (100%) create mode 100644 Tokenization/webapp/app/utils/storage.tsx diff --git a/Tokenization/webapp/app/routes/tokens/overview.tsx b/Tokenization/webapp/app/routes/tokens/overview.tsx index f216f888f..643200ee1 100644 --- a/Tokenization/webapp/app/routes/tokens/overview.tsx +++ b/Tokenization/webapp/app/routes/tokens/overview.tsx @@ -21,7 +21,7 @@ import { Tab, Dialog, DialogTitle, DialogContent, DialogContentText, DialogActio import ActionBlock from '~/components/tokens/action-block'; import { useSetHeader } from '~/ui/header/headerContext'; import { TabsNavbar } from '~/ui/navbar'; -import { useAuth } from '~/hooks/session'; +import { useAuth } from '~/utils/session'; /** * Client loader that fetches all tokens from the API. diff --git a/Tokenization/webapp/app/utils/fetcher.tsx b/Tokenization/webapp/app/utils/fetcher.tsx new file mode 100644 index 000000000..c774e63a2 --- /dev/null +++ b/Tokenization/webapp/app/utils/fetcher.tsx @@ -0,0 +1,97 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +import React, { useEffect } from 'react'; +import { useSession } from './session'; + +/** + * Low-level fetch wrapper that appends the session token as a query parameter. + * + * Notes / constraints: + * - Only accepts URLs starting with '/api' and will throw otherwise. + * - This helper obtains the token via useSession(), so it must be invoked from React hook/component + * call context (consider refactoring to accept a token param if you want to call it from plain code). + * - The function does not parse the response; it returns the raw Response object. + * + * @param {string} url - Relative URL; must start with '/api'. + * @param {RequestInit} [options] - Optional fetch options forwarded to window.fetch. + * @returns {Promise} The fetch Response promise. + * @throws {Error} If the url does not start with '/api'. + */ +export function fetchClient(url: string, options?: RequestInit): Promise { + if (!url.startsWith('/api')) { + throw new Error('Only /api requests are allowed'); + } + const { token } = useSession(); + const _url = new URL(url, window.location.origin); + _url.searchParams.append('token', token || ''); + + return fetch(_url.toString(), options); +}; + +/** + * React hook for fetching JSON from REST API endpoints under '/api'. + * + * Behavior and expectations: + * - This hook calls fetchClient which appends the session token as a 'token' query param. + * - The hook performs a single fetch on mount (empty dependency array). + * - The hook expects the endpoint to return JSON and calls res.json(). If the response is not valid JSON, + * res.json() will throw and the error will be surfaced via the `error` return value. + * - Designed for fetching REST JSON endpoints under '/api' only. + * + * Caveats: + * - If you need different behavior (retries, polling, conditional fetches, non-JSON responses), + * consider extending or replacing this hook. + * - Because fetchClient currently uses useSession(), ensure calls follow React Hooks rules. + * + * @param {string} url - Relative URL to fetch; must start with '/api'. + * @param {RequestInit} [options] - Optional fetch options forwarded to fetchClient/window.fetch. + * @returns {{ data: object|null, loading: boolean, error: Error|null }} Hook state: + * - data: parsed JSON result or null + * - loading: whether request is in progress + * - error: error object if fetch or JSON parsing failed + */ +export function useFetching(url: string, options?: RequestInit) { + // Adds token to url + const fetcher = fetchClient(url, options); + // Logic for managing fetch state + const [data, setData] = React.useState(null); + const [loading, setLoading] = React.useState(false); + const [error, setError] = React.useState(null); + + useEffect(() => { + let unmounted = false; + setLoading(true); + fetcher + .then(res => res.json()) + .then(res => { + if (!unmounted) { + setData(res); + setLoading(false); + } + }).catch(err => { + if (!unmounted) { + setError(err); + setLoading(false); + } + }); + + return () => { + // Happens after component unmounts + unmounted = true; + }; + }, []); + + return { data, loading, error }; +} diff --git a/Tokenization/webapp/app/hooks/session.tsx b/Tokenization/webapp/app/utils/session.tsx similarity index 100% rename from Tokenization/webapp/app/hooks/session.tsx rename to Tokenization/webapp/app/utils/session.tsx diff --git a/Tokenization/webapp/app/utils/storage.tsx b/Tokenization/webapp/app/utils/storage.tsx new file mode 100644 index 000000000..d5ff5eacb --- /dev/null +++ b/Tokenization/webapp/app/utils/storage.tsx @@ -0,0 +1,49 @@ + +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +/** + * Retrieve and parse a value from localStorage or sessionStorage. + * + * @param {string} key - The storage key to read. + * @param {'local'|'session'} [storage='local'] - 'local' uses localStorage, 'session' uses sessionStorage. + * @returns {any|null} The parsed JSON value, or null if the key is not present or the stored value is empty. + * + */ +export function getStorageItem(key: string, storage: 'local' | 'session' = 'local') { + const store = storage === 'local' ? localStorage : sessionStorage; + const item = store.getItem(key); + if (item === undefined) { // Parsing undefined throws an error + return null; + } + return item ? JSON.parse(item) : null; +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type Storable = number | string | object | any[]; + +/** + * Serialize and save a value to localStorage or sessionStorage. + * + * @param {string} key - The storage key under which to save the value. + * @param {number|string|object|any[]} value - The value to be serialized with JSON.stringify. + * @param {'local'|'session'} [storage='local'] - 'local' uses localStorage, 'session' uses sessionStorage. + * @returns {number|string|object|any[]} The same value that was passed in. + * + */ +export function setStorageItem(key: string, value: Storable, storage: 'local' | 'session' = 'local') { + const store = storage === 'local' ? localStorage : sessionStorage; + store.setItem(key, JSON.stringify(value)); + return value; +} From 312166365445c26b696d208934bcec5f94980cd2 Mon Sep 17 00:00:00 2001 From: Igor S Date: Wed, 26 Nov 2025 16:32:09 +0100 Subject: [PATCH 2/2] Added suggestions to code changes inside fetcher.tsx --- Tokenization/webapp/app/utils/fetcher.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Tokenization/webapp/app/utils/fetcher.tsx b/Tokenization/webapp/app/utils/fetcher.tsx index c774e63a2..c7641e2fd 100644 --- a/Tokenization/webapp/app/utils/fetcher.tsx +++ b/Tokenization/webapp/app/utils/fetcher.tsx @@ -29,13 +29,13 @@ import { useSession } from './session'; * @returns {Promise} The fetch Response promise. * @throws {Error} If the url does not start with '/api'. */ -export function fetchClient(url: string, options?: RequestInit): Promise { +export function useFetchClient(url: string, options?: RequestInit): Promise { if (!url.startsWith('/api')) { throw new Error('Only /api requests are allowed'); } const { token } = useSession(); const _url = new URL(url, window.location.origin); - _url.searchParams.append('token', token || ''); + _url.searchParams.append('token', token ?? ''); return fetch(_url.toString(), options); }; @@ -64,7 +64,7 @@ export function fetchClient(url: string, options?: RequestInit): Promise(null); const [loading, setLoading] = React.useState(false); @@ -91,7 +91,7 @@ export function useFetching(url: string, options?: RequestInit) { // Happens after component unmounts unmounted = true; }; - }, []); + }, [fetcher]); return { data, loading, error }; }