mirror of
https://git.meshkee.com/BaloutPastry/dashboards.git
synced 2026-08-12 06:40:59 +04:30
Initial commit of Balout Pastry admin dashboards.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+119
@@ -0,0 +1,119 @@
|
||||
import { getAccessToken, getRefreshToken, setSession, clearSession } from './auth'
|
||||
|
||||
const API_BASE_URL =
|
||||
import.meta.env.VITE_API_BASE_URL ?? 'http://localhost:3100/api/v1'
|
||||
|
||||
export class ApiError extends Error {
|
||||
status: number
|
||||
|
||||
constructor(status: number, message: string) {
|
||||
super(message)
|
||||
this.name = 'ApiError'
|
||||
this.status = status
|
||||
}
|
||||
}
|
||||
|
||||
type RequestOptions = {
|
||||
method?: string
|
||||
body?: unknown
|
||||
auth?: boolean
|
||||
/** Skip refresh retry (used by refresh itself). */
|
||||
skipRefresh?: boolean
|
||||
}
|
||||
|
||||
let refreshInFlight: Promise<boolean> | null = null
|
||||
|
||||
function parseErrorMessage(payload: unknown, fallback: string) {
|
||||
if (!payload || typeof payload !== 'object') return fallback
|
||||
const message = (payload as { message?: unknown }).message
|
||||
if (typeof message === 'string' && message.trim()) return message
|
||||
if (Array.isArray(message)) {
|
||||
const parts = message.filter((item) => typeof item === 'string')
|
||||
if (parts.length > 0) return parts.join('، ')
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
async function tryRefreshSession(): Promise<boolean> {
|
||||
if (refreshInFlight) return refreshInFlight
|
||||
|
||||
refreshInFlight = (async () => {
|
||||
const refreshToken = getRefreshToken()
|
||||
if (!refreshToken) {
|
||||
clearSession()
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/auth/refresh`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ refreshToken }),
|
||||
})
|
||||
|
||||
const payload = await response.json().catch(() => null)
|
||||
if (!response.ok) {
|
||||
clearSession()
|
||||
return false
|
||||
}
|
||||
|
||||
setSession({
|
||||
accessToken: payload.accessToken,
|
||||
refreshToken: payload.refreshToken,
|
||||
user: payload.user,
|
||||
})
|
||||
return true
|
||||
} catch {
|
||||
clearSession()
|
||||
return false
|
||||
} finally {
|
||||
refreshInFlight = null
|
||||
}
|
||||
})()
|
||||
|
||||
return refreshInFlight
|
||||
}
|
||||
|
||||
export async function apiRequest<T>(
|
||||
path: string,
|
||||
options: RequestOptions = {},
|
||||
): Promise<T> {
|
||||
const { method = 'GET', body, auth = true, skipRefresh = false } = options
|
||||
|
||||
const headers: Record<string, string> = {}
|
||||
if (body !== undefined) {
|
||||
headers['Content-Type'] = 'application/json'
|
||||
}
|
||||
if (auth) {
|
||||
const token = getAccessToken()
|
||||
if (token) headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}${path}`, {
|
||||
method,
|
||||
headers,
|
||||
body: body !== undefined ? JSON.stringify(body) : undefined,
|
||||
})
|
||||
|
||||
if (response.status === 401 && auth && !skipRefresh) {
|
||||
const refreshed = await tryRefreshSession()
|
||||
if (refreshed) {
|
||||
return apiRequest<T>(path, { ...options, skipRefresh: true })
|
||||
}
|
||||
}
|
||||
|
||||
if (response.status === 204) {
|
||||
return undefined as T
|
||||
}
|
||||
|
||||
const payload = await response.json().catch(() => null)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new ApiError(
|
||||
response.status,
|
||||
parseErrorMessage(payload, 'خطایی رخ داد'),
|
||||
)
|
||||
}
|
||||
|
||||
return payload as T
|
||||
}
|
||||
Reference in New Issue
Block a user