All files / frontend/src/auth auth-interceptor.ts

27.9% Statements 12/43
53.33% Branches 8/15
44.44% Functions 4/9
28.57% Lines 12/42

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90                  1x 1x                         1x 3x 3x 1x   3x       1x 2x   1x     1x         1x                                                                                            
/**
 * Axios Auth Interceptor
 *
 * Attaches JWT token to requests and handles 401 refresh flow.
 */
 
import axios from 'axios';
import { getAccessToken, getRefreshToken, setTokens, clearTokens } from './token-storage';
 
let isRefreshing = false;
let refreshSubscribers: ((token: string) => void)[] = [];
 
function onRefreshed(token: string) {
  refreshSubscribers.forEach(cb => cb(token));
  refreshSubscribers = [];
}
 
function addRefreshSubscriber(cb: (token: string) => void) {
  refreshSubscribers.push(cb);
}
 
export function setupAuthInterceptors(): void {
  // Request interceptor: attach token
  axios.interceptors.request.use((config) => {
    const token = getAccessToken();
    if (token && config.headers) {
      config.headers.Authorization = `Bearer ${token}`;
    }
    return config;
  });
 
  // Response interceptor: handle 401 with token refresh
  axios.interceptors.response.use(
    (response) => response,
    async (error) => {
      const originalRequest = error.config;
 
      // Don't retry auth endpoints or already-retried requests
      Eif (
        !originalRequest ||
        originalRequest._retry ||
        originalRequest.url?.includes('/api/auth/')
      ) {
        return Promise.reject(error);
      }
 
      if (error.response?.status === 401) {
        if (isRefreshing) {
          // Queue this request until refresh completes
          return new Promise((resolve) => {
            addRefreshSubscriber((token: string) => {
              originalRequest.headers.Authorization = `Bearer ${token}`;
              resolve(axios(originalRequest));
            });
          });
        }
 
        originalRequest._retry = true;
        isRefreshing = true;
 
        const refreshToken = getRefreshToken();
        if (!refreshToken) {
          clearTokens();
          window.location.reload();
          return Promise.reject(error);
        }
 
        try {
          const response = await axios.post('/api/auth/refresh', { refreshToken });
          const { token, refreshToken: newRefreshToken } = response.data;
          setTokens(token, newRefreshToken);
          isRefreshing = false;
          onRefreshed(token);
 
          originalRequest.headers.Authorization = `Bearer ${token}`;
          return axios(originalRequest);
        } catch {
          isRefreshing = false;
          refreshSubscribers = [];
          clearTokens();
          window.location.reload();
          return Promise.reject(error);
        }
      }
 
      return Promise.reject(error);
    }
  );
}