Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions frontend/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.tsx';
import './index.css';
import './utils/api'; // registers global axios 401 interceptor
import { BrowserRouter } from 'react-router-dom';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { WalletProvider } from './providers/WalletProvider.tsx';
Expand Down
37 changes: 36 additions & 1 deletion frontend/src/utils/api.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import axios from 'axios';
import axios, { AxiosError } from 'axios';
import { toast } from 'sonner';

const api = axios.create({
baseURL: import.meta.env.VITE_API_URL || 'http://localhost:4000/api',
Expand All @@ -21,6 +22,40 @@ api.interceptors.request.use(
}
);

const AUTH_TOKEN_KEY = 'payd_auth_token';
const LOGIN_PATH = '/login';

function handleUnauthorized(error: AxiosError) {
if (error.response?.status !== 401) {
return Promise.reject(error);
}

// Skip auth endpoints (login itself may legitimately return 401 for bad credentials).
const requestUrl = error.config?.url ?? '';
if (requestUrl.includes('/auth/')) {
return Promise.reject(error);
}

const hadToken = Boolean(localStorage.getItem(AUTH_TOKEN_KEY));
localStorage.removeItem(AUTH_TOKEN_KEY);

const currentPath = window.location.pathname;

if (hadToken && currentPath !== LOGIN_PATH) {
toast.error('Session expired, please log in again');
window.location.assign(LOGIN_PATH);
}

return Promise.reject(error);
}

// Register the 401 handler on the shared axios instance.
api.interceptors.response.use((response) => response, handleUnauthorized);

// Also handle 401 globally for every bare `axios.*` call across the app so an
// expired session never surfaces as a silent spinner/empty table.
axios.interceptors.response.use((response) => response, handleUnauthorized);

export default api;

export interface ApiError extends Error {
Expand Down