Public Access
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.
This commit is contained in:
1 parent
085fcf15a4
commit
5ff8d48c53
18 files changed
+127
-70
No files matched your search
@@ -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});
|
||||
}
|
||||
@@ -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'}});
|
||||
});
|
||||
});
|
||||
@@ -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),
|
||||
});
|
||||
|
||||
+8
-2
@@ -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;
|
||||
|
||||
@@ -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}),
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user