diff --git a/client/src/api/AuthApi.ts b/client/src/api/AuthApi.ts index 028e9a6..f2ef26b 100644 --- a/client/src/api/AuthApi.ts +++ b/client/src/api/AuthApi.ts @@ -2,7 +2,7 @@ // service to interact with the api/auth endpoints // handles user registration, user logins, tokens, password reset, etc. -import api from "./axios.ts" +import { api, authStorage } from "./axios.ts" import type { User, RegisterDto, LoginDto } from "../models/User.ts"; const API_URL: string = "/auth"; @@ -10,9 +10,8 @@ const API_URL: string = "/auth"; export const register = async (user: RegisterDto) => { try { - console.log(user); - // TODO: if valid + const response = await api.post(`${API_URL}/register`, user); return true; @@ -28,9 +27,7 @@ export const login = async (user: LoginDto ) => { try { const response = await api.post(`${API_URL}/login`, user); - const token = response.data.token; - - localStorage.setItem("token", token); + localStorage.setTokens(response.data); return true; @@ -41,9 +38,9 @@ export const login = async (user: LoginDto ) => { } export const logout = () => { - localStorage.removeItem("token"); + authStorage.clear(); } export const getToken = () => { - return localStorage.getItem("token"); + authStorage.getAccessToken(); } diff --git a/client/src/api/axios.ts b/client/src/api/axios.ts index 496a119..016a895 100644 --- a/client/src/api/axios.ts +++ b/client/src/api/axios.ts @@ -5,13 +5,41 @@ import axios from "axios"; const baseUrl: string = import.meta.env.DEV ? import.meta.env.VITE_DEV_API_URL : "https://app.vxbard.net/api" -const api = axios.create({ +export const api = axios.create({ baseURL: baseUrl }); -api.interceptors.request.use(config => { +type FailedRequest = { resolve: (token: string) => void, reject: (error: unknown) => void} +let isRefreshing: boolean = false; +let failedQueue: FailedRequest[] = []; - const token = localStorage.getItem("token"); +export const authStorage = { + getAccessToken: () => localStorage.getItem("accessToken"), + getRefreshToken: () => localStorage.getItem("refreshToken"), + + setTokens: ({ accessToken, refreshToken } : { accessToken: string, refreshToken: string }) => { + localStorage.setItem("accessToken", accessToken) + localStorage.setItem("refreshToken", refreshToken) + }, + + clear: () => { + localStorage.removeItem("accessToken") + localStorage.removeItem("refreshToken") + } +} + +const processQueue = (error: unknown, token: string | null = null): void => { + failedQueue.forEach(prom => { + if (error) prom.reject(error); + else prom.resolve(token as string); + }) + failedQueue = []; +} + +// intercept on each request +api.interceptors.request.use(config => { // add access token to request headers + + const token = localStorage.getAccessToken(); if (token) { config.headers.Authorization = `Bearer ${token}`; @@ -21,4 +49,42 @@ api.interceptors.request.use(config => { }); +// intercept on each response +api.interceptors.response.use(response => response, async error => { // mainly for authentication refreshTokens + const originalRequest = error.config; + + // if un authorized then refresh the token + if(error.response?.status === 401 && !originalRequest._retry) { + if(isRefreshing) { + return new Promise((resolve, reject) => { + failedQueue.push({ resolve, reject }) + }).then(token => { + originalRequest.headers.Authorization = `Bearer ${token}`; + return api(originalRequest); + }).catch(err => Promise.reject(err)); + } + + originalRequest._retry = true; + isRefreshing = true; + const refreshToken = authStorage.getRefreshToken(); + try { + // request refresh endpoint get back a new accessToken + const res = await axios.post(`${baseUrl}/auth/refresh`, { refreshToken }); + const { accessToken, refreshToken: newRefresh } = res.data; + authStorage.setTokens({ accessToken, refreshToken: newRefresh }); + processQueue(null, accessToken); + originalRequest.headers.Authorization = `Bearer ${accessToken}`; + return api(originalRequest); + } catch (err) { + processQueue(err, null); + authStorage.clear() + window.location.href = "/login"; + return Promise.reject(err); + } finally { + isRefreshing = false; + } + } + return Promise.reject(error); +}) + export default api;