Admin client and Vite proxy now use the montest host; rebuild dist for deploy. Co-authored-by: Cursor <cursoragent@cursor.com>
119 lines
3.5 KiB
TypeScript
119 lines
3.5 KiB
TypeScript
const TOKEN_KEY = 'fabric-admin-token'
|
|
|
|
export function getApiBaseUrl(): string {
|
|
return (import.meta.env.VITE_API_BASE_URL as string | undefined)?.replace(/\/$/, '')
|
|
|| 'https://parche.montest.ir/server/api'
|
|
}
|
|
|
|
export function getAccessToken(): string | null {
|
|
return localStorage.getItem(TOKEN_KEY) ?? sessionStorage.getItem(TOKEN_KEY)
|
|
}
|
|
|
|
export function setAccessToken(token: string | null, remember = true): void {
|
|
localStorage.removeItem(TOKEN_KEY)
|
|
sessionStorage.removeItem(TOKEN_KEY)
|
|
if (!token) return
|
|
if (remember) localStorage.setItem(TOKEN_KEY, token)
|
|
else sessionStorage.setItem(TOKEN_KEY, token)
|
|
}
|
|
|
|
export class ApiError extends Error {
|
|
status: number
|
|
constructor(status: number, message: string) {
|
|
super(message)
|
|
this.status = status
|
|
}
|
|
}
|
|
|
|
type RequestOptions = {
|
|
method?: string
|
|
body?: unknown
|
|
auth?: boolean
|
|
query?: Record<string, string | number | boolean | undefined | null>
|
|
}
|
|
|
|
function buildUrl(path: string, query?: RequestOptions['query']): string {
|
|
const base = getApiBaseUrl()
|
|
const joined = path.startsWith('http')
|
|
? path
|
|
: `${base.replace(/\/$/, '')}/${path.replace(/^\//, '')}`
|
|
|
|
const url = joined.startsWith('http')
|
|
? new URL(joined)
|
|
: new URL(joined, typeof window !== 'undefined' ? window.location.origin : 'http://127.0.0.1')
|
|
|
|
if (query) {
|
|
for (const [key, value] of Object.entries(query)) {
|
|
if (value === undefined || value === null || value === '') continue
|
|
url.searchParams.set(key, String(value))
|
|
}
|
|
}
|
|
return url.toString()
|
|
}
|
|
|
|
function errorMessageFromBody(data: unknown, status: number): string {
|
|
if (typeof data === 'object' && data && 'message' in data) {
|
|
const message = (data as { message: unknown }).message
|
|
if (Array.isArray(message)) return message.join('، ')
|
|
return String(message)
|
|
}
|
|
if (typeof data === 'string') {
|
|
if (/upstream error/i.test(data)) {
|
|
return 'سرور API در دسترس نیست. کمی بعد دوباره تلاش کنید.'
|
|
}
|
|
if (data.trim() && !data.trimStart().startsWith('<')) {
|
|
return data.trim().slice(0, 200)
|
|
}
|
|
}
|
|
return `خطای سرور (${status})`
|
|
}
|
|
|
|
export async function apiRequest<T>(path: string, options: RequestOptions = {}): Promise<T> {
|
|
const headers: Record<string, string> = {
|
|
Accept: 'application/json',
|
|
}
|
|
if (options.body !== undefined) {
|
|
headers['Content-Type'] = 'application/json'
|
|
}
|
|
if (options.auth !== false) {
|
|
const token = getAccessToken()
|
|
if (token) headers.Authorization = `Bearer ${token}`
|
|
}
|
|
|
|
let res: Response
|
|
try {
|
|
res = await fetch(buildUrl(path, options.query), {
|
|
method: options.method ?? (options.body !== undefined ? 'POST' : 'GET'),
|
|
headers,
|
|
body: options.body !== undefined ? JSON.stringify(options.body) : undefined,
|
|
signal: AbortSignal.timeout(20_000),
|
|
})
|
|
} catch (err) {
|
|
const timedOut =
|
|
(err instanceof DOMException && err.name === 'TimeoutError') ||
|
|
(err instanceof Error && err.name === 'TimeoutError')
|
|
throw new ApiError(
|
|
0,
|
|
timedOut
|
|
? 'پاسخ سرور طول کشید. دوباره تلاش کنید.'
|
|
: 'اتصال به سرور برقرار نشد. اینترنت یا آدرس API را بررسی کنید.',
|
|
)
|
|
}
|
|
|
|
const text = await res.text()
|
|
let data: unknown = null
|
|
if (text) {
|
|
try {
|
|
data = JSON.parse(text)
|
|
} catch {
|
|
data = text
|
|
}
|
|
}
|
|
|
|
if (!res.ok) {
|
|
throw new ApiError(res.status, errorMessageFromBody(data, res.status))
|
|
}
|
|
|
|
return data as T
|
|
}
|