<% const { apiConfig, generateResponses, config } = it; %> import { message as alert } from "@ctzhian/ui"; import { notFound, redirect } from 'next/navigation'; import { getServerHeader, getServerPathname, getServerSearch, getServerBasePath } from '@/utils/getServerHeader'; export type QueryParamsType = Record; export type ResponseFormat = keyof Omit; export interface FullRequestParams extends Omit { /** set parameter to `true` for call `securityWorker` for this request */ secure?: boolean; /** request path */ path: string; /** content type of request body */ type?: ContentType; /** query params */ query?: QueryParamsType; /** format of response (i.e. response.json() -> format: "json") */ format?: ResponseFormat; /** request body */ body?: unknown; /** base url */ baseUrl?: string; /** request cancellation token */ cancelToken?: CancelToken; } export type RequestParams = Omit & { isAlert?: boolean } export interface DomainResponse { /** @example 200 */ code?: number; data?: any; /** @example "OK" */ message?: string; } type ExtractDataProp = T extends { data?: infer U } ? U : never; export interface ApiConfig { baseUrl?: string; baseApiParams?: Omit; securityWorker?: (securityData: SecurityDataType | null) => Promise | RequestParams | void; customFetch?: typeof fetch; } export interface HttpResponse extends Response { data: D; error: E; } type CancelToken = Symbol | string | number; export enum ContentType { Json = "application/json", FormData = "multipart/form-data", UrlEncoded = "application/x-www-form-urlencoded", Text = "text/plain", } const pathnameWhiteList = ['/auth/login']; const redirectToLogin = () => { const redirectAfterLogin = encodeURIComponent( location.href.replace(location.origin, '') ); const search = `redirect=${redirectAfterLogin}`; const pathname = `${window._BASE_PATH_ || ''}/auth/login`; window.location.href = [pathname, search]?.join('?'); }; export class HttpClient { public baseUrl: string = ''; private securityData: SecurityDataType | null = null; private securityWorker?: ApiConfig["securityWorker"]; private abortControllers = new Map(); private customFetch = (...fetchParams: Parameters) => fetch(...fetchParams); private baseApiParams: RequestParams = { credentials: 'same-origin', headers: {}, redirect: 'follow', referrerPolicy: 'no-referrer', } constructor(apiConfig: ApiConfig = {}) { Object.assign(this, apiConfig); } public setSecurityData = (data: SecurityDataType | null) => { this.securityData = data; } protected encodeQueryParam(key: string, value: any) { const encodedKey = encodeURIComponent(key); return `${encodedKey}=${encodeURIComponent(typeof value === "number" ? value : `${value}`)}`; } protected addQueryParam(query: QueryParamsType, key: string) { return this.encodeQueryParam(key, query[key]); } protected addArrayQueryParam(query: QueryParamsType, key: string) { const value = query[key]; return value.map((v: any) => this.encodeQueryParam(key, v)).join("&"); } protected toQueryString(rawQuery?: QueryParamsType): string { const query = rawQuery || {}; const keys = Object.keys(query).filter((key) => "undefined" !== typeof query[key]); return keys .map((key) => Array.isArray(query[key]) ? this.addArrayQueryParam(query, key) : this.addQueryParam(query, key), ) .join("&"); } protected addQueryParams(rawQuery?: QueryParamsType): string { const queryString = this.toQueryString(rawQuery); return queryString ? `?${queryString}` : ""; } private contentFormatters: Record any> = { [ContentType.Json]: (input:any) => input !== null && (typeof input === "object" || typeof input === "string") ? JSON.stringify(input) : input, [ContentType.Text]: (input:any) => input !== null && typeof input !== "string" ? JSON.stringify(input) : input, [ContentType.FormData]: (input: any) => Object.keys(input || {}).reduce((formData, key) => { const property = input[key]; formData.append( key, property instanceof Blob ? property : typeof property === "object" && property !== null ? JSON.stringify(property) : `${property}` ); return formData; }, new FormData()), [ContentType.UrlEncoded]: (input: any) => this.toQueryString(input), } protected mergeRequestParams(params1: RequestParams, params2?: RequestParams): RequestParams { return { ...this.baseApiParams, ...params1, ...(params2 || {}), headers: { ...(this.baseApiParams.headers || {}), ...(params1.headers || {}), ...((params2 && params2.headers) || {}), }, }; } protected createAbortSignal = (cancelToken: CancelToken): AbortSignal | undefined => { if (this.abortControllers.has(cancelToken)) { const abortController = this.abortControllers.get(cancelToken); if (abortController) { return abortController.signal; } return void 0; } const abortController = new AbortController(); this.abortControllers.set(cancelToken, abortController); return abortController.signal; } public abortRequest = (cancelToken: CancelToken) => { const abortController = this.abortControllers.get(cancelToken) if (abortController) { abortController.abort(); this.abortControllers.delete(cancelToken); } } public request = async ({ isAlert = true, body, secure, path, type, query, format, baseUrl, cancelToken, ...params <% if (config.unwrapResponseData) { %> }: FullRequestParams & { isAlert?: boolean }): Promise> => { <% } else { %> }: FullRequestParams & { isAlert?: boolean }): Promise> => { <% } %> const secureParams = ((typeof secure === 'boolean' ? secure : this.baseApiParams.secure) && this.securityWorker && await this.securityWorker(this.securityData)) || {}; const requestParams = this.mergeRequestParams(params, secureParams); const queryString = query && this.toQueryString(query); const payloadFormatter = this.contentFormatters[type || ContentType.Json]; const responseFormat = format || requestParams.format || "json"; let customHeaders = {}; if (typeof window === 'undefined') { customHeaders = await getServerHeader(); } return this.customFetch( `${baseUrl || this.baseUrl || (typeof window !== 'undefined' ? window._BASE_PATH_ : '')}${path}${queryString ? `?${queryString}` : ""}`, { ...requestParams, headers: { ...customHeaders, ...(requestParams.headers || {}), ...(type && type !== ContentType.FormData ? { "Content-Type": type } : {}), }, signal: (cancelToken ? this.createAbortSignal(cancelToken) : requestParams.signal) || null, body: typeof body === "undefined" || body === null ? null : payloadFormatter(body), } ).then(async (response) => { if (response.status === 401) { if (typeof window === 'undefined') { const pathname = await getServerPathname(); if (!pathnameWhiteList.includes(pathname)) { const search = await getServerSearch(); const basePath = await getServerBasePath(); redirect(`${basePath}/auth/login?redirect=${encodeURIComponent(pathname +search)}`); } return } if (typeof window !== 'undefined') { if (!pathnameWhiteList.includes(window.location.pathname)) { if (response.status === 401) { redirectToLogin(); } } return } } // if (response.status === 403) { // console.log("response 403:", response); // if (typeof window === "undefined") { // const pathname = await getServerPathname(); // if (pathname !== "/block") { // redirect("/block"); // } // } // if (typeof window !== "undefined") { // const pathname = window.location.pathname; // if (pathname !== "/block") { // window.location.href = "/block"; // } // } // return Promise.reject(403); // } // if (response.status === 404) { // if (typeof window === "undefined") { // notFound(); // } // } let data: any = {}; try { data = await response[responseFormat](); } catch (error) {} if (cancelToken) { this.abortControllers.delete(cancelToken); } <% if (!config.disableThrowOnError) { %> if (!response.ok || (data.code !== undefined && data.code !== 0) || (data.success !== undefined && !data.success)) { if (typeof window !== 'undefined') { const urlObj = new URL(response.url); if (urlObj.pathname !== '/api/v1/user/profile') { if (isAlert) { alert.error( (data as DomainResponse).message! || response.statusText ); } } } const errorMessage = { data, url: response.url, response }; console.log("response error:", errorMessage); return Promise.reject({...data, code: response.status === 200 ? data.code : response.status}); } <% } %> <% if (config.unwrapResponseData) { %> return data.data; <% } else { %> return data; <% } %> }); }; } export default new HttpClient({ baseUrl: process.env.TARGET }).request;