Chose Option A from the follow-up brief: useImpersonationStore
already holds an `ImpersonationState` ref hydrated from
sessionStorage at store-init and exposes the active impersonation
target user as a public `targetUserId` computed. The store is the
canonical source; sessionStorage is just its persistence sidecar.
Adds a fifth callback `getImpersonationTargetUserId: () => string
| null` to AxiosBindingsDeps and replaces the
sessionStorage.getItem('crewli_impersonation') + JSON.parse block
in the request interceptor with a single `deps.getImpersonationTargetUserId()`
call. The bindings plugin wires it to
`useImpersonationStore().targetUserId`.
After this commit lib/axios.ts has zero references to
sessionStorage and zero magic strings about impersonation
persistence — the only persistence-mechanism knowledge left is in
useImpersonationStore (where it belongs) and in
plugins/3.axios-bindings.ts (allowed to know about stores). The
HTTP module is now unambiguously pure infrastructure.
Behavior preserved 1:1: the store hydrates from sessionStorage
synchronously inside the defineStore factory, so the very first
HTTP request after page load sees the same target user id as
before.
Co-Authored-By: Claude <noreply@anthropic.com>
98 lines
3.0 KiB
TypeScript
98 lines
3.0 KiB
TypeScript
import axios from 'axios'
|
|
import type { AxiosInstance, InternalAxiosRequestConfig } from 'axios'
|
|
|
|
/**
|
|
* Seam contract between the HTTP client and the rest of the app.
|
|
* `lib/axios.ts` knows nothing about Pinia, stores, or routing — it
|
|
* only invokes these callbacks. The bindings plugin
|
|
* (`plugins/3.axios-bindings.ts`) supplies the runtime closures.
|
|
*/
|
|
export interface AxiosBindingsDeps {
|
|
getActiveOrgId: () => string | null
|
|
getImpersonationTargetUserId: () => string | null
|
|
notify: (message: string, level: 'error' | 'warning') => void
|
|
onAuthFail: () => void
|
|
onImpersonationRevoked: () => void
|
|
}
|
|
|
|
const apiClient: AxiosInstance = axios.create({
|
|
baseURL: import.meta.env.VITE_API_URL,
|
|
withCredentials: true,
|
|
headers: {
|
|
'Accept': 'application/json',
|
|
'Content-Type': 'application/json',
|
|
},
|
|
timeout: 30000,
|
|
})
|
|
|
|
export function registerInterceptors(client: AxiosInstance, deps: AxiosBindingsDeps): void {
|
|
client.interceptors.request.use(
|
|
(config: InternalAxiosRequestConfig) => {
|
|
const orgId = deps.getActiveOrgId()
|
|
if (orgId)
|
|
config.headers['X-Organisation-Id'] = orgId
|
|
|
|
const impersonationTargetUserId = deps.getImpersonationTargetUserId()
|
|
if (impersonationTargetUserId)
|
|
config.headers['X-Impersonate-User'] = impersonationTargetUserId
|
|
|
|
if (import.meta.env.DEV)
|
|
console.log(`🚀 ${config.method?.toUpperCase()} ${config.url}`, config.data)
|
|
|
|
return config
|
|
},
|
|
async error => { throw error },
|
|
)
|
|
|
|
client.interceptors.response.use(
|
|
response => {
|
|
if (import.meta.env.DEV)
|
|
console.log(`✅ ${response.status} ${response.config.url}`, response.data)
|
|
|
|
return response
|
|
},
|
|
async error => {
|
|
if (import.meta.env.DEV)
|
|
console.error(`❌ ${error.response?.status} ${error.config?.url}`, error.response?.data)
|
|
|
|
const status = error.response?.status
|
|
|
|
// Backend revoked the impersonation session mid-flight; the callback
|
|
// owns the local cleanup and the post-revoke redirect.
|
|
if (status === 403 && error.response?.data?.impersonation_ended) {
|
|
deps.onImpersonationRevoked()
|
|
|
|
throw error
|
|
}
|
|
|
|
if (status === 401) {
|
|
deps.onAuthFail()
|
|
}
|
|
else if (status === 403) {
|
|
deps.notify('You don\'t have permission for this action.', 'error')
|
|
}
|
|
else if (status === 404) {
|
|
deps.notify('The requested item was not found.', 'warning')
|
|
}
|
|
else if (status === 422) {
|
|
const message = error.response?.data?.message
|
|
if (message && typeof message === 'string')
|
|
deps.notify(message, 'error')
|
|
}
|
|
else if (status === 503) {
|
|
deps.notify('Service temporarily unavailable. Please try again later.', 'error')
|
|
}
|
|
else if (status && status >= 500) {
|
|
deps.notify('An unexpected error occurred. Please try again later.', 'error')
|
|
}
|
|
else if (!error.response) {
|
|
deps.notify('Unable to connect to the server. Check your internet connection.', 'error')
|
|
}
|
|
|
|
throw error
|
|
},
|
|
)
|
|
}
|
|
|
|
export { apiClient }
|