quality / changes (push) Successful in 5s
quality / api (push) Skipped
quality / commitlint (push) Skipped
quality / docker-check (push) Skipped
CD / update-wiki (push) Successful in 5s
quality / web (push) Successful in 57s
CD / quality (push) Successful in 1m9s
CD / publish (push) Successful in 1m39s
- Introduced a new Access Denied route to handle unauthorized access. - Updated routing logic to redirect to the Access Denied page when necessary. - Enhanced authentication checks to prevent infinite redirect loops and improve user experience. - Adjusted API client to handle JWT rejection scenarios more gracefully.
81 lines
2.3 KiB
TypeScript
81 lines
2.3 KiB
TypeScript
import {
|
|
clearToken,
|
|
ensureAuthConfig,
|
|
getToken,
|
|
hasPortalHandoffFlag,
|
|
isAuthEnabled,
|
|
isPortalHandoffCoolingDown,
|
|
redirectToPortalLogin,
|
|
} from '@/lib/auth'
|
|
|
|
export class ApiError extends Error {
|
|
constructor(
|
|
public status: number,
|
|
public code: string,
|
|
message: string,
|
|
) {
|
|
super(message)
|
|
this.name = 'ApiError'
|
|
}
|
|
}
|
|
|
|
async function handoffOnUnauthorized(): Promise<void> {
|
|
clearToken()
|
|
const cfg = await ensureAuthConfig()
|
|
if (
|
|
(cfg.required || isAuthEnabled()) &&
|
|
!hasPortalHandoffFlag() &&
|
|
!isPortalHandoffCoolingDown()
|
|
) {
|
|
redirectToPortalLogin(`${window.location.origin}/auth/callback`)
|
|
return
|
|
}
|
|
// Cooldown / recent handoff — stop SSO storm (wrong JWT secret / issuer).
|
|
if (cfg.required || isAuthEnabled()) {
|
|
window.location.assign(
|
|
`${window.location.origin}/auth/callback?error=jwt_rejected`,
|
|
)
|
|
return
|
|
}
|
|
if (!cfg.required && !isAuthEnabled()) {
|
|
window.location.href = '/login'
|
|
}
|
|
}
|
|
|
|
async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
|
const token = getToken()
|
|
const headers = new Headers(init?.headers)
|
|
if (init?.body != null && !headers.has('Content-Type')) {
|
|
headers.set('Content-Type', 'application/json')
|
|
}
|
|
if (token) headers.set('Authorization', `Bearer ${token}`)
|
|
|
|
const res = await fetch(path, { ...init, headers })
|
|
if (res.status === 401 && !path.includes('/auth/login')) {
|
|
await handoffOnUnauthorized()
|
|
throw new ApiError(401, 'UNAUTHORIZED', 'Unauthorized')
|
|
}
|
|
if (!res.ok) {
|
|
const body = await res.json().catch(() => ({}))
|
|
const err = body?.error
|
|
throw new ApiError(
|
|
res.status,
|
|
err?.code ?? 'UNKNOWN',
|
|
err?.message ?? res.statusText,
|
|
)
|
|
}
|
|
if (res.status === 204) return undefined as T
|
|
return res.json() as Promise<T>
|
|
}
|
|
|
|
export const api = {
|
|
get: <T>(path: string) => request<T>(path),
|
|
post: <T>(path: string, body?: unknown) =>
|
|
request<T>(path, { method: 'POST', body: body ? JSON.stringify(body) : undefined }),
|
|
patch: <T>(path: string, body: unknown) =>
|
|
request<T>(path, { method: 'PATCH', body: JSON.stringify(body) }),
|
|
put: <T>(path: string, body: unknown) =>
|
|
request<T>(path, { method: 'PUT', body: JSON.stringify(body) }),
|
|
delete: <T>(path: string) => request<T>(path, { method: 'DELETE' }),
|
|
}
|