Add Vite API proxy and harden admin HTTP client.

Support relative API base URL in development, clearer network errors, and ASCII phone digits on login.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-08 11:50:19 +02:00
co-authored by Cursor
parent 43ebebd8c4
commit 47c7d1dc27
5 changed files with 405 additions and 65 deletions
+45 -13
View File
@@ -34,7 +34,14 @@ type RequestOptions = {
function buildUrl(path: string, query?: RequestOptions['query']): string {
const base = getApiBaseUrl()
const url = new URL(path.startsWith('http') ? path : `${base}${path.startsWith('/') ? '' : '/'}${path}`)
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
@@ -44,6 +51,23 @@ function buildUrl(path: string, query?: RequestOptions['query']): string {
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',
@@ -56,11 +80,25 @@ export async function apiRequest<T>(path: string, options: RequestOptions = {}):
if (token) headers.Authorization = `Bearer ${token}`
}
const 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,
})
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
@@ -73,13 +111,7 @@ export async function apiRequest<T>(path: string, options: RequestOptions = {}):
}
if (!res.ok) {
const message =
typeof data === 'object' && data && 'message' in data
? Array.isArray((data as { message: unknown }).message)
? ((data as { message: string[] }).message).join('، ')
: String((data as { message: unknown }).message)
: `خطای سرور (${res.status})`
throw new ApiError(res.status, message)
throw new ApiError(res.status, errorMessageFromBody(data, res.status))
}
return data as T
+8 -2
View File
@@ -10,20 +10,26 @@ const route = useRoute()
const auth = useAuthStore()
const toast = useToast()
const phone = ref('۰۹۱۲۱۱۱۲۲۲۲')
const phone = ref('09121112222')
const password = ref('')
const remember = ref(true)
const showPassword = ref(false)
const isFormValid = computed(() => phone.value.trim().length > 0 && password.value.length > 0)
function toAsciiDigits(value: string) {
return value.replace(/[۰-۹]/g, (d) => String('۰۱۲۳۴۵۶۷۸۹'.indexOf(d))).replace(/[٠-٩]/g, (d) =>
String('٠١٢٣٤٥٦٧٨٩'.indexOf(d)),
)
}
async function submit() {
if (!isFormValid.value) {
auth.error = 'لطفاً شماره موبایل و رمز عبور را وارد کنید.'
return
}
const ok = await auth.login(phone.value.trim(), password.value, remember.value)
const ok = await auth.login(toAsciiDigits(phone.value.trim()), password.value, remember.value)
if (!ok) return
toast.success(`خوش آمدید، ${auth.fullName}`)