fix login bug
This commit is contained in:
1 parent
4d350c0278
commit
880ac58042
9 files changed
+709
-208
No files matched your search
+86
-27
@@ -1,64 +1,123 @@
|
||||
'use client'
|
||||
|
||||
import axios, { AxiosError, InternalAxiosRequestConfig } from 'axios'
|
||||
import { expireSession, getAccessToken, getRefreshToken, saveTokens } from '@/lib/auth'
|
||||
import { expireSession, getAccessToken, getRefreshToken, getSessionIdentity, saveTokens, validTokens } from '@/lib/auth'
|
||||
|
||||
type RetryRequest = InternalAxiosRequestConfig & { _retry?: boolean }
|
||||
type RetryRequest = InternalAxiosRequestConfig & { _retry?: boolean; _sessionIdentity?: string }
|
||||
class InvalidSession extends Error {}
|
||||
let pendingRefresh: Promise<string> | null = null
|
||||
let pendingRefresh: { identity: string; promise: Promise<string> } | null = null
|
||||
const leaseKey = 'customerRefreshLease'
|
||||
type RefreshLease = { owner: string; identity: string; expires: number }
|
||||
|
||||
function readLease(): RefreshLease | null {
|
||||
try {
|
||||
const value = JSON.parse(window.localStorage.getItem(leaseKey) || 'null')
|
||||
return value && typeof value.owner === 'string' && typeof value.identity === 'string' &&
|
||||
typeof value.expires === 'number' && Number.isFinite(value.expires) ? value : null
|
||||
} catch { return null }
|
||||
}
|
||||
async function withRefreshLease(identity: string, action: () => Promise<string>): Promise<string> {
|
||||
const owner = `${Date.now()}-${Math.random()}`
|
||||
const deadline = Date.now() + 20000
|
||||
while (Date.now() < deadline) {
|
||||
if (identity !== getSessionIdentity()) throw new Error('نشست تغییر کرده است.')
|
||||
const lease = readLease()
|
||||
if (!lease || lease.identity !== identity || lease.expires <= Date.now()) {
|
||||
try { window.localStorage.setItem(leaseKey, JSON.stringify({ owner, identity, expires: Date.now() + 15000 })) }
|
||||
catch { return action() }
|
||||
// Yield before checking ownership so simultaneous contenders see
|
||||
// the same winner rather than both sending a rotating JWT.
|
||||
await new Promise(resolve => setTimeout(resolve, 50))
|
||||
if (readLease()?.owner === owner) {
|
||||
try { return await action() }
|
||||
finally {
|
||||
try { if (readLease()?.owner === owner) window.localStorage.removeItem(leaseKey) }
|
||||
catch { /* An abandoned lease expires, including after a closed tab. */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, 50))
|
||||
}
|
||||
throw new Error('تمدید نشست موقتاً انجام نشد. دوباره تلاش کنید.')
|
||||
}
|
||||
|
||||
const axiosInstance = axios.create({
|
||||
baseURL: process.env.NEXT_PUBLIC_API_URL + '/',
|
||||
timeout: 10000,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
|
||||
axiosInstance.interceptors.request.use(request => {
|
||||
axiosInstance.interceptors.request.use((request: RetryRequest) => {
|
||||
const access = getAccessToken()
|
||||
if (access) request.headers.Authorization = 'Bearer ' + access
|
||||
else delete request.headers.Authorization
|
||||
request._sessionIdentity ??= getSessionIdentity()
|
||||
return request
|
||||
})
|
||||
|
||||
async function refreshAccess(): Promise<string> {
|
||||
const refresh = getRefreshToken()
|
||||
if (!refresh) throw new InvalidSession()
|
||||
try {
|
||||
const response = await axios.post(process.env.NEXT_PUBLIC_API_URL + '/auth/token/refresh/', { refresh }, { timeout: 10000 })
|
||||
if (getRefreshToken() !== refresh) throw new Error('نشست تغییر کرده است.')
|
||||
if (!response.data?.access || !response.data?.refresh) throw new InvalidSession()
|
||||
saveTokens(response.data)
|
||||
return response.data.access
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error) && [400, 401, 403].includes(error.response?.status || 0)) throw new InvalidSession()
|
||||
throw error
|
||||
async function refreshAccess(identity: string): Promise<string> {
|
||||
const originalRefresh = getRefreshToken()
|
||||
async function rotate(): Promise<string> {
|
||||
if (identity !== getSessionIdentity()) throw new Error('نشست تغییر کرده است.')
|
||||
const refresh = getRefreshToken()
|
||||
if (!refresh) throw new InvalidSession()
|
||||
// A different tab may have rotated while this tab waited for the lock.
|
||||
if (refresh !== originalRefresh && getAccessToken()) return getAccessToken()!
|
||||
try {
|
||||
const response = await axios.post(process.env.NEXT_PUBLIC_API_URL + '/auth/token/refresh/', { refresh }, { timeout: 10000 })
|
||||
if (identity !== getSessionIdentity()) throw new Error('نشست تغییر کرده است.')
|
||||
if (getRefreshToken() !== refresh) {
|
||||
if (getAccessToken()) return getAccessToken()!
|
||||
throw new Error('نشست تغییر کرده است.')
|
||||
}
|
||||
if (!validTokens(response.data)) throw new InvalidSession()
|
||||
saveTokens(response.data, refresh)
|
||||
return response.data.access
|
||||
} catch (error) {
|
||||
if (identity !== getSessionIdentity()) throw new Error('نشست تغییر کرده است.')
|
||||
if (getRefreshToken() !== refresh && getAccessToken()) return getAccessToken()!
|
||||
if (axios.isAxiosError(error) && [400, 401, 403].includes(error.response?.status || 0)) throw new InvalidSession()
|
||||
throw error
|
||||
}
|
||||
}
|
||||
// Web Locks serialize refresh rotation across tabs in supporting browsers.
|
||||
if (typeof navigator !== 'undefined' && navigator.locks) {
|
||||
return navigator.locks.request('crm-customer-jwt-refresh', rotate)
|
||||
}
|
||||
return withRefreshLease(identity, rotate)
|
||||
}
|
||||
|
||||
axiosInstance.interceptors.response.use(response => response, async (error: AxiosError) => {
|
||||
axiosInstance.interceptors.response.use(response => {
|
||||
const identity = (response.config as RetryRequest)._sessionIdentity
|
||||
if (identity && identity !== getSessionIdentity()) return Promise.reject(new Error('نشست تغییر کرده است.'))
|
||||
return response
|
||||
}, async (error: AxiosError) => {
|
||||
const request = error.config as RetryRequest | undefined
|
||||
if (error.response?.status !== 401 || !request || /\/auth\//.test(request.url || '')) return Promise.reject(error)
|
||||
const identity = request._sessionIdentity!
|
||||
// Never replay a previous person's request under a newly logged-in account.
|
||||
if (identity !== getSessionIdentity()) return Promise.reject(error)
|
||||
if (request._retry) {
|
||||
const latest = getAccessToken()
|
||||
if (!latest || request.headers.Authorization === 'Bearer ' + latest) expireSession()
|
||||
return Promise.reject(error)
|
||||
}
|
||||
request._retry = true
|
||||
// Another request may already have refreshed the token that this request used.
|
||||
const currentAccess = getAccessToken()
|
||||
if (currentAccess && request.headers.Authorization !== 'Bearer ' + currentAccess) {
|
||||
request.headers.Authorization = 'Bearer ' + currentAccess
|
||||
return axiosInstance(request)
|
||||
}
|
||||
if (currentAccess && request.headers.Authorization !== 'Bearer ' + currentAccess) return axiosInstance(request)
|
||||
const refresh = getRefreshToken()
|
||||
try {
|
||||
if (!pendingRefresh) pendingRefresh = refreshAccess().finally(() => { pendingRefresh = null })
|
||||
request.headers.Authorization = 'Bearer ' + await pendingRefresh
|
||||
if (!pendingRefresh || pendingRefresh.identity !== identity) {
|
||||
const pending = { identity, promise: refreshAccess(identity) }
|
||||
pendingRefresh = pending
|
||||
void pending.promise.finally(() => { if (pendingRefresh === pending) pendingRefresh = null }).catch(() => {})
|
||||
}
|
||||
const access = await pendingRefresh.promise
|
||||
if (identity !== getSessionIdentity()) return Promise.reject(error)
|
||||
request.headers.Authorization = 'Bearer ' + access
|
||||
return axiosInstance(request)
|
||||
} catch (refreshError) {
|
||||
if (refreshError instanceof InvalidSession && getRefreshToken() === refresh) expireSession()
|
||||
if (refreshError instanceof InvalidSession && identity === getSessionIdentity() && getRefreshToken() === refresh) expireSession()
|
||||
return Promise.reject(refreshError)
|
||||
}
|
||||
})
|
||||
|
||||
export default axiosInstance
|
||||
Reference in new issue
Block a user