diff --git a/PROJECTS/beginner/canary-token-generator/frontend/src/api/client.ts b/PROJECTS/beginner/canary-token-generator/frontend/src/api/client.ts new file mode 100644 index 00000000..1f00d19f --- /dev/null +++ b/PROJECTS/beginner/canary-token-generator/frontend/src/api/client.ts @@ -0,0 +1,86 @@ +// =================== +// ©AngelaMos | 2026 +// client.ts +// =================== + +import axios, { type AxiosInstance, type InternalAxiosRequestConfig } from 'axios' +import type { ZodType } from 'zod' +import { ApiError, ApiErrorCode, transformAxiosError } from '@/core/api' +import { successEnvelope } from './types/error' + +const REQUEST_TIMEOUT_MS = 15000 +const TURNSTILE_HEADER_NAME = 'CF-Turnstile-Response' + +const resolveBaseURL = (): string => { + const fromEnv = import.meta.env.VITE_API_URL + if (typeof fromEnv === 'string' && fromEnv.length > 0) { + return fromEnv + } + return '/api' +} + +export const apiClient: AxiosInstance = axios.create({ + baseURL: resolveBaseURL(), + timeout: REQUEST_TIMEOUT_MS, + headers: { 'Content-Type': 'application/json' }, +}) + +export type TurnstileTokenProvider = () => string | null | undefined + +let turnstileProvider: TurnstileTokenProvider | null = null + +export function setTurnstileTokenProvider( + provider: TurnstileTokenProvider | null +): void { + turnstileProvider = provider +} + +apiClient.interceptors.request.use( + (config: InternalAxiosRequestConfig): InternalAxiosRequestConfig => { + const token = turnstileProvider?.() + if (typeof token === 'string' && token.length > 0) { + config.headers.set(TURNSTILE_HEADER_NAME, token) + } + return config + } +) + +apiClient.interceptors.response.use( + (response) => response, + (error: unknown) => { + if (axios.isAxiosError(error)) { + return Promise.reject(transformAxiosError(error)) + } + return Promise.reject(error) + } +) + +function unwrapEnvelope(data: unknown, schema: ZodType, status: number): T { + const parsed = successEnvelope(schema).safeParse(data) + if (!parsed.success) { + throw new ApiError( + 'response shape mismatch', + ApiErrorCode.PARSE_ERROR, + status + ) + } + return parsed.data.data +} + +export async function apiGet(path: string, schema: ZodType): Promise { + const response = await apiClient.get(path) + return unwrapEnvelope(response.data, schema, response.status) +} + +export async function apiPost( + path: string, + body: unknown, + schema: ZodType +): Promise { + const response = await apiClient.post(path, body) + return unwrapEnvelope(response.data, schema, response.status) +} + +export async function apiDelete(path: string): Promise { + await apiClient.delete(path) +} diff --git a/PROJECTS/beginner/canary-token-generator/frontend/src/api/index.ts b/PROJECTS/beginner/canary-token-generator/frontend/src/api/index.ts index 5796616a..d29a44f9 100644 --- a/PROJECTS/beginner/canary-token-generator/frontend/src/api/index.ts +++ b/PROJECTS/beginner/canary-token-generator/frontend/src/api/index.ts @@ -3,5 +3,6 @@ // index.ts // =================== +export * from './client' export * from './hooks' export * from './types'