mirror of
https://git.meshkee.com/Meshkee/dashboards.git
synced 2026-08-12 06:40:57 +04:30
Includes business, customer, and super-admin apps with shared packages and production deploy scripts. Co-authored-by: Cursor <cursoragent@cursor.com>
123 lines
2.9 KiB
TypeScript
123 lines
2.9 KiB
TypeScript
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL ?? 'http://localhost:3000/api/v1'
|
|
|
|
const ACCESS_TOKEN_KEY = 'meshkee_access_token'
|
|
const REFRESH_TOKEN_KEY = 'meshkee_refresh_token'
|
|
|
|
export class ApiError extends Error {
|
|
status: number
|
|
|
|
constructor(message: string, status: number) {
|
|
super(message)
|
|
this.name = 'ApiError'
|
|
this.status = status
|
|
}
|
|
}
|
|
|
|
export function isAbortError(err: unknown): boolean {
|
|
return err instanceof DOMException && err.name === 'AbortError'
|
|
}
|
|
|
|
export function getAccessToken() {
|
|
return localStorage.getItem(ACCESS_TOKEN_KEY)
|
|
}
|
|
|
|
export function getRefreshToken() {
|
|
return localStorage.getItem(REFRESH_TOKEN_KEY)
|
|
}
|
|
|
|
export function setTokens(accessToken: string, refreshToken: string) {
|
|
localStorage.setItem(ACCESS_TOKEN_KEY, accessToken)
|
|
localStorage.setItem(REFRESH_TOKEN_KEY, refreshToken)
|
|
}
|
|
|
|
export function clearTokens() {
|
|
localStorage.removeItem(ACCESS_TOKEN_KEY)
|
|
localStorage.removeItem(REFRESH_TOKEN_KEY)
|
|
}
|
|
|
|
function parseErrorMessage(payload: unknown, fallback: string) {
|
|
if (!payload || typeof payload !== 'object') {
|
|
return fallback
|
|
}
|
|
|
|
const message = (payload as { message?: string | string[] }).message
|
|
|
|
if (Array.isArray(message)) {
|
|
return message.join(', ')
|
|
}
|
|
|
|
if (typeof message === 'string') {
|
|
return message
|
|
}
|
|
|
|
return fallback
|
|
}
|
|
|
|
async function refreshAccessToken() {
|
|
const refreshToken = getRefreshToken()
|
|
if (!refreshToken) {
|
|
return false
|
|
}
|
|
|
|
const response = await fetch(`${API_BASE_URL}/auth/refresh`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ refreshToken }),
|
|
})
|
|
|
|
if (!response.ok) {
|
|
clearTokens()
|
|
return false
|
|
}
|
|
|
|
const data = await response.json()
|
|
setTokens(data.accessToken, data.refreshToken)
|
|
return true
|
|
}
|
|
|
|
interface RequestOptions extends Omit<RequestInit, 'body'> {
|
|
body?: unknown
|
|
auth?: boolean
|
|
}
|
|
|
|
export async function apiRequest<T>(path: string, options: RequestOptions = {}): Promise<T> {
|
|
const { body, auth = false, headers, ...rest } = options
|
|
|
|
const requestHeaders = new Headers(headers)
|
|
|
|
if (body !== undefined) {
|
|
requestHeaders.set('Content-Type', 'application/json')
|
|
}
|
|
|
|
if (auth) {
|
|
const accessToken = getAccessToken()
|
|
if (accessToken) {
|
|
requestHeaders.set('Authorization', `Bearer ${accessToken}`)
|
|
}
|
|
}
|
|
|
|
const response = await fetch(`${API_BASE_URL}${path}`, {
|
|
...rest,
|
|
headers: requestHeaders,
|
|
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
})
|
|
|
|
if (response.status === 401 && auth) {
|
|
const refreshed = await refreshAccessToken()
|
|
if (refreshed) {
|
|
return apiRequest<T>(path, options)
|
|
}
|
|
}
|
|
|
|
const payload = await response.json().catch(() => null)
|
|
|
|
if (!response.ok) {
|
|
throw new ApiError(
|
|
parseErrorMessage(payload, `Request failed with status ${response.status}`),
|
|
response.status,
|
|
)
|
|
}
|
|
|
|
return payload as T
|
|
}
|