Files
nademitex_admin/src/services/http.ts
T
mohmdsdgandCopilot 9854842ada fix(auth): secure sessions and app routing
Validate sessions against the API, honor configurable base paths, and route unknown pages to 404. Keep development ports stable so user and admin apps cannot be confused.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-09-24 20:14:11 +03:30

136 lines
4.3 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://nademitex.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>
timeoutMs?: number
}
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(options.timeoutMs ?? 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) {
if (res.status === 401 && options.auth !== false) {
setAccessToken(null)
localStorage.removeItem('fabric-admin-session')
sessionStorage.removeItem('fabric-admin-session')
const loginPath = `${import.meta.env.BASE_URL}login`
if (typeof window !== 'undefined' && window.location.pathname !== loginPath) {
const basePath = import.meta.env.BASE_URL.replace(/\/$/, '')
const routePath = window.location.pathname.startsWith(basePath)
? window.location.pathname.slice(basePath.length) || '/'
: window.location.pathname
const redirect = encodeURIComponent(
`${routePath}${window.location.search}`,
)
window.location.assign(`${loginPath}?redirect=${redirect}`)
}
}
throw new ApiError(res.status, errorMessageFromBody(data, res.status))
}
return data as T
}