fix(auth): refresh expired access tokens automatically
This commit is contained in:
@@ -1,49 +1,142 @@
|
|||||||
import { API_BASE_URL } from "../config/constants"
|
import { API_BASE_URL } from "../config/constants"
|
||||||
|
|
||||||
export const authFetch = async (endpoint: string, options: RequestInit = {}) => {
|
let refreshRequest: Promise<string | null> | null = null
|
||||||
const token = localStorage.getItem("accessToken")
|
|
||||||
const isFormData = options.body instanceof FormData
|
const cleanBaseUrl = API_BASE_URL.replace(/\/+$/, "")
|
||||||
|
|
||||||
const headers: HeadersInit = {
|
const buildUrl = (endpoint: string) => {
|
||||||
...(!isFormData && { "Content-Type": "application/json" }),
|
const cleanEndpoint = endpoint.startsWith("/") ? endpoint : `/${endpoint}`
|
||||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
return `${cleanBaseUrl}${cleanEndpoint}`
|
||||||
...options.headers,
|
}
|
||||||
}
|
|
||||||
|
const normalizeJsonResponse = (response: Response) => {
|
||||||
const cleanBaseUrl = API_BASE_URL.replace(/\/+$/, "")
|
const originalJson = response.json.bind(response)
|
||||||
const cleanEndpoint = endpoint.startsWith("/") ? endpoint : `/${endpoint}`
|
|
||||||
const url = `${cleanBaseUrl}${cleanEndpoint}`
|
response.json = async () => {
|
||||||
|
if (response.status === 204 || response.status === 205) {
|
||||||
const response = await fetch(url, {
|
return {}
|
||||||
...options,
|
}
|
||||||
headers,
|
|
||||||
})
|
const data = await originalJson()
|
||||||
|
|
||||||
if (response.status === 401) {
|
if (data && typeof data === "object" && "items" in data && "pages_count" in data) {
|
||||||
localStorage.removeItem("accessToken")
|
return {
|
||||||
localStorage.removeItem("refreshToken")
|
count: data.total_items || 0,
|
||||||
window.location.href = "/auth"
|
next: null,
|
||||||
return response
|
previous: null,
|
||||||
}
|
results: data.items || [],
|
||||||
|
_meta: {
|
||||||
const originalJson = response.json.bind(response)
|
pages_count: data.pages_count,
|
||||||
response.json = async () => {
|
items_per_page: data.items_per_page,
|
||||||
const data = await originalJson()
|
current_page: data.current_page,
|
||||||
|
},
|
||||||
if (data && typeof data === "object" && "items" in data && "pages_count" in data) {
|
}
|
||||||
return {
|
}
|
||||||
count: data.total_items || 0,
|
|
||||||
results: data.items || [],
|
return data
|
||||||
_meta: {
|
}
|
||||||
pages_count: data.pages_count,
|
|
||||||
items_per_page: data.items_per_page,
|
return response
|
||||||
current_page: data.current_page
|
}
|
||||||
}
|
|
||||||
}
|
const clearSessionAndRedirect = () => {
|
||||||
}
|
localStorage.removeItem("accessToken")
|
||||||
|
localStorage.removeItem("refreshToken")
|
||||||
return data
|
if (window.location.pathname !== "/auth") {
|
||||||
}
|
window.location.href = "/auth"
|
||||||
|
}
|
||||||
return response
|
}
|
||||||
}
|
|
||||||
|
const shouldAttemptRefresh = (endpoint: string) => {
|
||||||
|
const normalizedEndpoint = endpoint.startsWith("/") ? endpoint : `/${endpoint}`
|
||||||
|
return ![
|
||||||
|
"/api/users/login/",
|
||||||
|
"/api/users/otp/send/",
|
||||||
|
"/api/users/otp/login/",
|
||||||
|
"/api/users/token/refresh/",
|
||||||
|
].includes(normalizedEndpoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
const refreshAccessToken = async () => {
|
||||||
|
const refreshToken = localStorage.getItem("refreshToken")
|
||||||
|
if (!refreshToken) return null
|
||||||
|
|
||||||
|
if (!refreshRequest) {
|
||||||
|
refreshRequest = (async () => {
|
||||||
|
const response = await fetch(buildUrl("/api/users/token/refresh/"), {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ refresh: refreshToken }),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
const nextAccessToken = typeof data?.access === "string" ? data.access : null
|
||||||
|
const nextRefreshToken = typeof data?.refresh === "string" ? data.refresh : null
|
||||||
|
|
||||||
|
if (!nextAccessToken) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
localStorage.setItem("accessToken", nextAccessToken)
|
||||||
|
if (nextRefreshToken) {
|
||||||
|
localStorage.setItem("refreshToken", nextRefreshToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nextAccessToken
|
||||||
|
})().finally(() => {
|
||||||
|
refreshRequest = null
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return refreshRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
export const authFetch = async (endpoint: string, options: RequestInit = {}, allowRetry = true): Promise<Response> => {
|
||||||
|
const token = localStorage.getItem("accessToken")
|
||||||
|
const isFormData = options.body instanceof FormData
|
||||||
|
|
||||||
|
const headers: HeadersInit = {
|
||||||
|
...(!isFormData && { "Content-Type": "application/json" }),
|
||||||
|
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||||
|
...options.headers,
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(buildUrl(endpoint), {
|
||||||
|
...options,
|
||||||
|
headers,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.status === 401 && allowRetry && shouldAttemptRefresh(endpoint)) {
|
||||||
|
const nextAccessToken = await refreshAccessToken()
|
||||||
|
|
||||||
|
if (nextAccessToken) {
|
||||||
|
return authFetch(
|
||||||
|
endpoint,
|
||||||
|
{
|
||||||
|
...options,
|
||||||
|
headers: {
|
||||||
|
...options.headers,
|
||||||
|
Authorization: `Bearer ${nextAccessToken}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
clearSessionAndRedirect()
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.status === 401 && shouldAttemptRefresh(endpoint)) {
|
||||||
|
clearSessionAndRedirect()
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalizeJsonResponse(response)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user