From 5ff8d48c539efea5f5d0b1e6f276d54c4b3c0158 Mon Sep 17 00:00:00 2001 From: lobo Date: Fri, 25 Sep 2026 11:29:14 +0200 Subject: [PATCH] refactor(auth): send login token through apiFetch All authenticated calls now use apiFetch, and only utils/auth.js touches the stored token. The cookie login can then change these two files instead of every page. --- src/App.jsx | 5 +++-- src/components/BacklogCard.jsx | 4 ++-- src/hooks/useForceSync.js | 8 +++---- src/hooks/useLibrarySync.js | 10 ++++----- src/hooks/useSessionExpired.js | 3 ++- src/hooks/useSteamProfile.js | 8 +++---- src/pages/Dashboard.jsx | 25 ++++++++++----------- src/pages/GameDetail.jsx | 8 +++---- src/pages/GameLibrary.jsx | 8 +++---- src/pages/LoginPage.jsx | 4 ++-- src/pages/PlayNextPage.jsx | 10 ++++----- src/pages/SettingsPage.jsx | 16 ++++++-------- src/pages/WishlistPage.jsx | 9 ++++---- src/utils/api.js | 10 +++++++++ src/utils/api.test.js | 40 ++++++++++++++++++++++++++++++++++ src/utils/appearance.js | 9 ++++---- src/utils/auth.js | 10 +++++++-- src/utils/backlog.js | 10 +++++---- 18 files changed, 127 insertions(+), 70 deletions(-) create mode 100644 src/utils/api.js create mode 100644 src/utils/api.test.js diff --git a/src/App.jsx b/src/App.jsx index d624ae5..0a1a4b0 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -18,6 +18,7 @@ import {Toaster} from '@/components/ui/sonner'; import {TooltipProvider} from '@/components/ui/tooltip'; import PageBackground from './components/PageBackground.jsx'; import {usePreferences} from './context/PreferencesContext.jsx'; +import {clearToken, getToken} from './utils/auth.js'; function App() { const {t} = useTranslation(); @@ -31,7 +32,7 @@ function App() { useEffect(() => { // No local expiry check here: a 401 from the backend ends the session (useSessionExpired), and a browser // clock that runs ahead would otherwise end it silently. - setIsAuthenticated(!!localStorage.getItem('tbp_jwt')); + setIsAuthenticated(!!getToken()); }, [location]); // On login and on page load; the cached look shows until Auth answers. @@ -40,7 +41,7 @@ function App() { }, [isAuthenticated, syncAppearance]); const handleLogout = () => { - localStorage.removeItem('tbp_jwt'); + clearToken(); resetAppearance(); setIsAuthenticated(false); navigate('/login'); diff --git a/src/components/BacklogCard.jsx b/src/components/BacklogCard.jsx index b0bbead..6424c78 100644 --- a/src/components/BacklogCard.jsx +++ b/src/components/BacklogCard.jsx @@ -13,6 +13,7 @@ import {cn} from '@/lib/utils'; import {STATUS_META} from './GameStatusBadge.jsx'; import {GAME_PRIORITIES, GAME_STATUSES, NO_PRIORITY, saveBacklogEntry, toBacklogRequest} from '../utils/backlog.js'; import useSessionExpired from '../hooks/useSessionExpired.js'; +import {getToken} from '../utils/auth.js'; const NOTES_MAX = 2000; @@ -41,8 +42,7 @@ export default function BacklogCard({game, onSaved}) { const set = (field) => (value) => setForm((f) => ({...f, [field]: value})); const save = async () => { - const token = localStorage.getItem('tbp_jwt'); - if (!token) { + if (!getToken()) { navigate('/login'); return; } diff --git a/src/hooks/useForceSync.js b/src/hooks/useForceSync.js index 41d9997..257484b 100644 --- a/src/hooks/useForceSync.js +++ b/src/hooks/useForceSync.js @@ -3,6 +3,8 @@ import {useNavigate} from 'react-router-dom'; import {useTranslation} from 'react-i18next'; import {toast} from 'sonner'; import useSessionExpired from './useSessionExpired.js'; +import {apiFetch} from '../utils/api.js'; +import {getToken} from '../utils/auth.js'; const RELOAD_DELAY_MS = 4000; @@ -17,16 +19,14 @@ export default function useForceSync(onStarted) { const [busy, setBusy] = useState(false); const trigger = async () => { - const token = localStorage.getItem('tbp_jwt'); - if (!token) { + if (!getToken()) { navigate('/login'); return; } setBusy(true); try { - const response = await fetch(`${import.meta.env.VITE_CORE_API_URL}/api/library/sync`, { + const response = await apiFetch(`${import.meta.env.VITE_CORE_API_URL}/api/library/sync`, { method: 'POST', - headers: {'Authorization': `Bearer ${token}`}, }); if (response.status === 401) { expireSession(); diff --git a/src/hooks/useLibrarySync.js b/src/hooks/useLibrarySync.js index bffa834..11069eb 100644 --- a/src/hooks/useLibrarySync.js +++ b/src/hooks/useLibrarySync.js @@ -1,4 +1,6 @@ import {useEffect, useRef, useState} from 'react'; +import {apiFetch} from '../utils/api.js'; +import {getToken} from '../utils/auth.js'; const POLL_INTERVAL_MS = 3000; const MAX_POLL_DURATION_MS = 120_000; @@ -32,13 +34,11 @@ export default function useLibrarySync({enabled, expectInitialSync = false, onCo const startedAt = Date.now(); const fetchOnce = async () => { - const token = localStorage.getItem('tbp_jwt'); - if (!token) return; + if (!getToken()) return; try { - const response = await fetch( - `${import.meta.env.VITE_CORE_API_URL}/api/library/sync-status`, - {headers: {'Authorization': `Bearer ${token}`}} + const response = await apiFetch( + `${import.meta.env.VITE_CORE_API_URL}/api/library/sync-status` ); if (cancelled) return; diff --git a/src/hooks/useSessionExpired.js b/src/hooks/useSessionExpired.js index 1d55c53..4a55e36 100644 --- a/src/hooks/useSessionExpired.js +++ b/src/hooks/useSessionExpired.js @@ -1,6 +1,7 @@ import {useCallback, useRef} from 'react'; import {useLocation, useNavigate} from 'react-router-dom'; import {useResetAppearance} from '../context/PreferencesContext.jsx'; +import {clearToken} from '../utils/auth.js'; /** * Returns a function for a 401 answer: it removes the token and the user's look and opens the login page, which @@ -16,7 +17,7 @@ export default function useSessionExpired() { return useCallback(() => { const {pathname, search} = locationRef.current; - localStorage.removeItem('tbp_jwt'); + clearToken(); resetAppearance(); navigate('/login', {replace: true, state: {sessionExpired: true, from: pathname + search}}); }, [navigate, resetAppearance]); diff --git a/src/hooks/useSteamProfile.js b/src/hooks/useSteamProfile.js index d4cda18..aad9943 100644 --- a/src/hooks/useSteamProfile.js +++ b/src/hooks/useSteamProfile.js @@ -1,4 +1,6 @@ import {useEffect, useState} from 'react'; +import {apiFetch} from '../utils/api.js'; +import {getToken} from '../utils/auth.js'; /** * The user's Steam name and avatar from Core, or null while loading, without a Steam ID or on any error. Only @@ -7,7 +9,7 @@ import {useEffect, useState} from 'react'; */ export default function useSteamProfile(enabled) { const [profile, setProfile] = useState(null); - const token = localStorage.getItem('tbp_jwt'); + const token = getToken(); useEffect(() => { if (!enabled || !token) { @@ -17,9 +19,7 @@ export default function useSteamProfile(enabled) { let cancelled = false; (async () => { try { - const response = await fetch(`${import.meta.env.VITE_CORE_API_URL}/api/library/steam-profile`, { - headers: {'Authorization': `Bearer ${token}`}, - }); + const response = await apiFetch(`${import.meta.env.VITE_CORE_API_URL}/api/library/steam-profile`); const data = response.ok ? await response.json() : null; if (!cancelled) setProfile(data); } catch { diff --git a/src/pages/Dashboard.jsx b/src/pages/Dashboard.jsx index e9c72d3..736622c 100644 --- a/src/pages/Dashboard.jsx +++ b/src/pages/Dashboard.jsx @@ -25,6 +25,8 @@ import SteamAvatar from '../components/SteamAvatar.jsx'; import useSteamProfile from '../hooks/useSteamProfile.js'; import {cn} from '@/lib/utils'; import {usePreferences} from '../context/PreferencesContext.jsx'; +import {apiFetch} from '../utils/api.js'; +import {getToken} from '../utils/auth.js'; const TOP_GAMES = 5; // The games endpoint accepts only the page sizes 20, 40 and 80. @@ -32,10 +34,7 @@ const TOP_GAMES_URL = '/api/library/games?page=0&size=20&sort=playtimeMinutes%2C const PLAYING_GAMES = 4; const PLAYING_URL = '/api/library/games?page=0&size=20&sort=playtimeMinutes%2Cdesc&status=PLAYING'; -const authHeaders = () => ({ - 'Authorization': `Bearer ${localStorage.getItem('tbp_jwt')}`, - 'Content-Type': 'application/json', -}); +const JSON_HEADERS = {'Content-Type': 'application/json'}; const Dashboard = () => { const {t} = useTranslation(); @@ -55,7 +54,7 @@ const Dashboard = () => { const fetchSteamCount = useCallback(async () => { try { - const r = await fetch(`${import.meta.env.VITE_CORE_API_URL}/api/library/games-count`, {headers: authHeaders()}); + const r = await apiFetch(`${import.meta.env.VITE_CORE_API_URL}/api/library/games-count`, {headers: JSON_HEADERS}); if (r.status === 401) { expireSession(); return; @@ -70,7 +69,7 @@ const Dashboard = () => { const fetchTopGames = useCallback(async () => { try { - const r = await fetch(`${import.meta.env.VITE_CORE_API_URL}${TOP_GAMES_URL}`, {headers: authHeaders()}); + const r = await apiFetch(`${import.meta.env.VITE_CORE_API_URL}${TOP_GAMES_URL}`, {headers: JSON_HEADERS}); if (r.status === 401) { expireSession(); return; @@ -86,7 +85,7 @@ const Dashboard = () => { const fetchPlaying = useCallback(async () => { try { - const r = await fetch(`${import.meta.env.VITE_CORE_API_URL}${PLAYING_URL}`, {headers: authHeaders()}); + const r = await apiFetch(`${import.meta.env.VITE_CORE_API_URL}${PLAYING_URL}`, {headers: JSON_HEADERS}); if (r.status === 401) { expireSession(); return; @@ -107,7 +106,7 @@ const Dashboard = () => { const fetchSummary = useCallback(async (excludeAppid) => { const query = excludeAppid ? `?exclude=${excludeAppid}` : ''; try { - const r = await fetch(`${import.meta.env.VITE_CORE_API_URL}/api/library/summary${query}`, {headers: authHeaders()}); + const r = await apiFetch(`${import.meta.env.VITE_CORE_API_URL}/api/library/summary${query}`, {headers: JSON_HEADERS}); if (r.status === 401) { expireSession(); return true; @@ -126,7 +125,7 @@ const Dashboard = () => { const fetchActivity = useCallback(async () => { try { - const r = await fetch(`${import.meta.env.VITE_CORE_API_URL}/api/library/activity`, {headers: authHeaders()}); + const r = await apiFetch(`${import.meta.env.VITE_CORE_API_URL}/api/library/activity`, {headers: JSON_HEADERS}); if (r.status === 401) { expireSession(); return; @@ -141,7 +140,7 @@ const Dashboard = () => { const fetchAchievements = useCallback(async () => { try { - const r = await fetch(`${import.meta.env.VITE_CORE_API_URL}/api/library/achievements/summary`, {headers: authHeaders()}); + const r = await apiFetch(`${import.meta.env.VITE_CORE_API_URL}/api/library/achievements/summary`, {headers: JSON_HEADERS}); if (r.status === 401) { expireSession(); return; @@ -173,15 +172,15 @@ const Dashboard = () => { useEffect(() => { const fetchUserData = async () => { - if (!localStorage.getItem('tbp_jwt')) { + if (!getToken()) { navigate('/login'); return; } try { - const profileResponse = await fetch(`${import.meta.env.VITE_API_URL}/api/users/me?v=1`, { + const profileResponse = await apiFetch(`${import.meta.env.VITE_API_URL}/api/users/me?v=1`, { method: 'GET', - headers: authHeaders(), + headers: JSON_HEADERS, }); if (profileResponse.ok) { diff --git a/src/pages/GameDetail.jsx b/src/pages/GameDetail.jsx index e064cad..6d0d4c0 100644 --- a/src/pages/GameDetail.jsx +++ b/src/pages/GameDetail.jsx @@ -19,6 +19,8 @@ import useSessionExpired from '../hooks/useSessionExpired.js'; import useSaveAppearance from '../hooks/useSaveAppearance.js'; import {usePreferences} from '../context/PreferencesContext.jsx'; import PageContainer from '../components/PageContainer.jsx'; +import {apiFetch} from '../utils/api.js'; +import {getToken} from '../utils/auth.js'; const formatUnlockedAt = (instant, locale) => { if (!instant) return ''; @@ -43,8 +45,7 @@ export default function GameDetail() { useEffect(() => { let cancelled = false; - const token = localStorage.getItem('tbp_jwt'); - if (!token) { + if (!getToken()) { navigate('/login'); return; } @@ -53,9 +54,8 @@ export default function GameDetail() { (async () => { try { const url = `${import.meta.env.VITE_CORE_API_URL}/api/library/games/${appid}`; - const response = await fetch(url, { + const response = await apiFetch(url, { headers: { - 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', }, }); diff --git a/src/pages/GameLibrary.jsx b/src/pages/GameLibrary.jsx index 5e92037..f72136f 100644 --- a/src/pages/GameLibrary.jsx +++ b/src/pages/GameLibrary.jsx @@ -19,6 +19,8 @@ import GameCover from '../components/GameCover.jsx'; import LibraryTabs from '../components/LibraryTabs.jsx'; import GameStatusBadge from '../components/GameStatusBadge.jsx'; import PageContainer from '../components/PageContainer.jsx'; +import {apiFetch} from '../utils/api.js'; +import {getToken} from '../utils/auth.js'; const PAGE_SIZES = [20, 40, 80]; const DEFAULT_SIZE = 20; @@ -58,8 +60,7 @@ export default function GameLibrary() { const [error, setError] = useState(null); const load = useCallback(async () => { - const token = localStorage.getItem('tbp_jwt'); - if (!token) { + if (!getToken()) { navigate('/login'); return; } @@ -67,9 +68,8 @@ export default function GameLibrary() { setError(null); try { const url = `${import.meta.env.VITE_CORE_API_URL}/api/library/games?${libraryQuery({page, size, sort, status, q, maxMinutes: shortOnly ? SHORT_GAME_MINUTES : null})}`; - const response = await fetch(url, { + const response = await apiFetch(url, { headers: { - 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', }, }); diff --git a/src/pages/LoginPage.jsx b/src/pages/LoginPage.jsx index 69e6512..bcf3c61 100644 --- a/src/pages/LoginPage.jsx +++ b/src/pages/LoginPage.jsx @@ -8,7 +8,7 @@ import {Input} from '@/components/ui/input'; import {Label} from '@/components/ui/label'; import AuthLayout from '../components/AuthLayout.jsx'; import FeedbackAlert from '../components/FeedbackAlert.jsx'; -import {getValidToken, safeReturnPath} from '../utils/auth.js'; +import {getValidToken, safeReturnPath, setToken} from '../utils/auth.js'; const LOGIN_ERROR_KEY = { INVALID_CREDENTIALS: 'LOGIN.ERROR_INVALID_CREDENTIALS', @@ -51,7 +51,7 @@ export default function LoginPage() { const data = await response.json().catch(() => ({})); if (response.ok) { - localStorage.setItem('tbp_jwt', data.token); + setToken(data.token); navigate(target, {replace: true}); } else { const minutes = Math.ceil(Number(response.headers.get('Retry-After') ?? 900) / 60); diff --git a/src/pages/PlayNextPage.jsx b/src/pages/PlayNextPage.jsx index 9c3f6b9..e4e7ef1 100644 --- a/src/pages/PlayNextPage.jsx +++ b/src/pages/PlayNextPage.jsx @@ -13,6 +13,8 @@ import PageContainer from '../components/PageContainer.jsx'; import useSessionExpired from '../hooks/useSessionExpired.js'; import {saveGameStatus} from '../utils/backlog.js'; import {formatPlaytime} from '../utils/games.js'; +import {apiFetch} from '../utils/api.js'; +import {getToken} from '../utils/auth.js'; // maxMinutes is IGDB's normal time to beat; a limit leaves out games without IGDB data. const BUDGETS = [ @@ -45,8 +47,7 @@ export default function PlayNextPage() { const latestRequest = useRef(0); const load = useCallback(async (maxMinutes, exclude) => { - const token = localStorage.getItem('tbp_jwt'); - if (!token) { + if (!getToken()) { navigate('/login'); return; } @@ -54,9 +55,8 @@ export default function PlayNextPage() { setLoading(true); setError(null); try { - const response = await fetch( - `${import.meta.env.VITE_CORE_API_URL}/api/library/picker?${pickerQuery(maxMinutes, exclude)}`, - {headers: {'Authorization': `Bearer ${token}`}}); + const response = await apiFetch( + `${import.meta.env.VITE_CORE_API_URL}/api/library/picker?${pickerQuery(maxMinutes, exclude)}`); if (request !== latestRequest.current) return; if (response.status === 401) { expireSession(); diff --git a/src/pages/SettingsPage.jsx b/src/pages/SettingsPage.jsx index e9167b0..28dcca7 100644 --- a/src/pages/SettingsPage.jsx +++ b/src/pages/SettingsPage.jsx @@ -22,6 +22,8 @@ import useSaveAppearance from '../hooks/useSaveAppearance.js'; import {ACCENTS, CORNERS} from '../utils/appearance.js'; import {coverUrl} from '../utils/games.js'; import {cn} from '@/lib/utils'; +import {apiFetch} from '../utils/api.js'; +import {getToken, setToken} from '../utils/auth.js'; const BACKEND_ERROR_KEY = { STEAM_ID_ALREADY_IN_USE: 'REGISTRATION.ERROR_STEAM_ID_IN_USE', @@ -61,18 +63,16 @@ export default function SettingsPage() { }); useEffect(() => { - const token = localStorage.getItem('tbp_jwt'); - if (!token) { + if (!getToken()) { navigate('/login'); return; } const load = async () => { try { - const response = await fetch(`${import.meta.env.VITE_API_URL}/api/users/me?v=1`, { + const response = await apiFetch(`${import.meta.env.VITE_API_URL}/api/users/me?v=1`, { method: 'GET', headers: { - 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', }, }); @@ -107,17 +107,15 @@ export default function SettingsPage() { }, [navigate, expireSession, reset, t]); const persistSettings = async (payload) => { - const token = localStorage.getItem('tbp_jwt'); - if (!token) { + if (!getToken()) { navigate('/login'); return {ok: false}; } try { - const response = await fetch(`${import.meta.env.VITE_API_URL}/api/users/settings`, { + const response = await apiFetch(`${import.meta.env.VITE_API_URL}/api/users/settings`, { method: 'PUT', headers: { - 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', }, body: JSON.stringify(payload), @@ -136,7 +134,7 @@ export default function SettingsPage() { } const data = await response.json(); - localStorage.setItem('tbp_jwt', data.token); // Core reads the steamId from the token + setToken(data.token); // Core reads the steamId from the token toast.success(t('SETTINGS.SAVED')); return {ok: true}; } catch (err) { diff --git a/src/pages/WishlistPage.jsx b/src/pages/WishlistPage.jsx index 440e884..aade22a 100644 --- a/src/pages/WishlistPage.jsx +++ b/src/pages/WishlistPage.jsx @@ -11,6 +11,8 @@ import GameCover from '../components/GameCover.jsx'; import LibraryTabs from '../components/LibraryTabs.jsx'; import useSessionExpired from '../hooks/useSessionExpired.js'; import PageContainer from '../components/PageContainer.jsx'; +import {apiFetch} from '../utils/api.js'; +import {getToken} from '../utils/auth.js'; const formatAdded = (instant, locale) => instant ? new Date(instant).toLocaleDateString(locale, {year: 'numeric', month: 'short', day: 'numeric'}) : null; @@ -24,17 +26,14 @@ export default function WishlistPage() { const [onlyOnSale, setOnlyOnSale] = useState(false); useEffect(() => { - const token = localStorage.getItem('tbp_jwt'); - if (!token) { + if (!getToken()) { navigate('/login'); return undefined; } let cancelled = false; (async () => { try { - const response = await fetch(`${import.meta.env.VITE_CORE_API_URL}/api/library/wishlist`, { - headers: {'Authorization': `Bearer ${token}`}, - }); + const response = await apiFetch(`${import.meta.env.VITE_CORE_API_URL}/api/library/wishlist`); if (cancelled) return; if (response.status === 401) { expireSession(); diff --git a/src/utils/api.js b/src/utils/api.js new file mode 100644 index 0000000..5364ca3 --- /dev/null +++ b/src/utils/api.js @@ -0,0 +1,10 @@ +import {getToken} from './auth.js'; + +/** + * fetch with the login token. The caller handles the response status, 401 included. + */ +export function apiFetch(url, init = {}) { + const token = getToken(); + const headers = token ? {...init.headers, 'Authorization': `Bearer ${token}`} : init.headers; + return fetch(url, {...init, headers}); +} diff --git a/src/utils/api.test.js b/src/utils/api.test.js new file mode 100644 index 0000000..b7fd5ea --- /dev/null +++ b/src/utils/api.test.js @@ -0,0 +1,40 @@ +// @vitest-environment jsdom +import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'; +import {apiFetch} from './api.js'; + +describe('apiFetch', () => { + beforeEach(() => { + globalThis.fetch = vi.fn(async () => new Response(null, {status: 204})); + }); + + afterEach(() => { + localStorage.clear(); + }); + + it('sends the stored token and keeps the other headers and options', async () => { + localStorage.setItem('tbp_jwt', 'the-token'); + + await apiFetch('/api/x', {method: 'PUT', headers: {'Content-Type': 'application/json'}, body: '{}'}); + + expect(fetch).toHaveBeenCalledWith('/api/x', { + method: 'PUT', + headers: {'Content-Type': 'application/json', 'Authorization': 'Bearer the-token'}, + body: '{}', + }); + }); + + it('sends no Authorization header without a token', async () => { + await apiFetch('/api/x'); + + expect(fetch).toHaveBeenCalledWith('/api/x', {headers: undefined}); + }); + + it('reads the token on every call', async () => { + localStorage.setItem('tbp_jwt', 'old'); + await apiFetch('/api/x'); + localStorage.setItem('tbp_jwt', 'new'); + await apiFetch('/api/x'); + + expect(fetch).toHaveBeenLastCalledWith('/api/x', {headers: {'Authorization': 'Bearer new'}}); + }); +}); diff --git a/src/utils/appearance.js b/src/utils/appearance.js index ea226a4..dfae219 100644 --- a/src/utils/appearance.js +++ b/src/utils/appearance.js @@ -1,3 +1,5 @@ +import {apiFetch} from './api.js'; + // Auth accepts only these names (AppearanceRequest); style.css has a preset for each. export const ACCENTS = ['violet', 'blue', 'green', 'orange', 'pink', 'red']; export const CORNERS = ['sharp', 'default', 'round']; @@ -49,9 +51,9 @@ export const applyAppearance = (appearance, root = document.documentElement) => * PUT /api/users/settings/appearance on Auth. The caller handles the response status. */ export const saveAppearance = (appearance) => - fetch(`${import.meta.env.VITE_API_URL}/api/users/settings/appearance`, { + apiFetch(`${import.meta.env.VITE_API_URL}/api/users/settings/appearance`, { method: 'PUT', - headers: {'Authorization': `Bearer ${localStorage.getItem('tbp_jwt')}`, 'Content-Type': 'application/json'}, + headers: {'Content-Type': 'application/json'}, body: JSON.stringify(appearance), // Saves run one after another; a hung request must not hold back the next ones. signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), @@ -61,8 +63,7 @@ export const saveAppearance = (appearance) => * GET /api/users/me on Auth, only for its appearance. Null when there is no answer to use. */ export const loadAppearance = async () => { - const response = await fetch(`${import.meta.env.VITE_API_URL}/api/users/me?v=1`, { - headers: {'Authorization': `Bearer ${localStorage.getItem('tbp_jwt')}`}, + const response = await apiFetch(`${import.meta.env.VITE_API_URL}/api/users/me?v=1`, { // Saves wait for this load. signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), }); diff --git a/src/utils/auth.js b/src/utils/auth.js index 80b3fc0..33def21 100644 --- a/src/utils/auth.js +++ b/src/utils/auth.js @@ -1,5 +1,11 @@ const TOKEN_KEY = 'tbp_jwt'; +export const getToken = () => localStorage.getItem(TOKEN_KEY); + +export const setToken = (token) => localStorage.setItem(TOKEN_KEY, token); + +export const clearToken = () => localStorage.removeItem(TOKEN_KEY); + /** * Only checks `exp`. The signature is checked by the backends. */ @@ -17,9 +23,9 @@ export function isTokenValid(token, nowMs = Date.now()) { * Returns the stored token, or null. Removes an expired token. */ export function getValidToken() { - const token = localStorage.getItem(TOKEN_KEY); + const token = getToken(); if (token && !isTokenValid(token)) { - localStorage.removeItem(TOKEN_KEY); + clearToken(); return null; } return token; diff --git a/src/utils/backlog.js b/src/utils/backlog.js index 5adb251..5c1a956 100644 --- a/src/utils/backlog.js +++ b/src/utils/backlog.js @@ -1,3 +1,5 @@ +import {apiFetch} from './api.js'; + export const GAME_STATUSES = ['BACKLOG', 'PLAYING', 'FINISHED', 'COMPLETED', 'DROPPED']; export const GAME_PRIORITIES = ['HIGH', 'MEDIUM', 'LOW']; @@ -19,9 +21,9 @@ export const toBacklogRequest = ({status, rating, notes, priority}) => ({ * @param entry a backlog entry in request form, see toBacklogRequest */ export const saveBacklogEntry = (appid, entry) => - fetch(`${import.meta.env.VITE_CORE_API_URL}/api/library/games/${appid}/backlog`, { + apiFetch(`${import.meta.env.VITE_CORE_API_URL}/api/library/games/${appid}/backlog`, { method: 'PUT', - headers: {'Authorization': `Bearer ${localStorage.getItem('tbp_jwt')}`, 'Content-Type': 'application/json'}, + headers: {'Content-Type': 'application/json'}, body: JSON.stringify(entry), }); @@ -29,9 +31,9 @@ export const saveBacklogEntry = (appid, entry) => * PUT /api/library/games/{appid}/status. The caller handles the response status. */ export const saveGameStatus = (appid, status) => - fetch(`${import.meta.env.VITE_CORE_API_URL}/api/library/games/${appid}/status`, { + apiFetch(`${import.meta.env.VITE_CORE_API_URL}/api/library/games/${appid}/status`, { method: 'PUT', - headers: {'Authorization': `Bearer ${localStorage.getItem('tbp_jwt')}`, 'Content-Type': 'application/json'}, + headers: {'Content-Type': 'application/json'}, body: JSON.stringify({status}), });