diff --git a/.env.development b/.env.development index a4cf532c..8c6427d0 100644 --- a/.env.development +++ b/.env.development @@ -1,4 +1,7 @@ NEXT_PUBLIC_API_URL=https://api.hikka.io API_URL=https://api.hikka.io SITE_URL=http://localhost:3000 -NEXT_PUBLIC_SITE_URL=http://localhost:3000 \ No newline at end of file +NEXT_PUBLIC_SITE_URL=http://localhost:3000 +COOKIE_HTTP_ONLY=false +COOKIE_DOMAIN=localhost +NEXT_PUBLIC_DEV=true \ No newline at end of file diff --git a/.env.production b/.env.production index 9fb3fedc..30393738 100644 --- a/.env.production +++ b/.env.production @@ -1,4 +1,7 @@ NEXT_PUBLIC_API_URL=https://api.hikka.io API_URL=http://backend:8000 SITE_URL=https://hikka.io -NEXT_PUBLIC_SITE_URL=https://hikka.io \ No newline at end of file +NEXT_PUBLIC_SITE_URL=https://hikka.io +COOKIE_HTTP_ONLY=true +COOKIE_DOMAIN=.hikka.io +NEXT_PUBLIC_DEV=false \ No newline at end of file diff --git a/next.config.mjs b/next.config.mjs index 16209021..20b01793 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -31,14 +31,6 @@ const nextConfig = { return config; }, - async rewrites() { - return [ - { - source: '/api/:path*', - destination: `${process.env.API_URL}/:path*`, - }, - ]; - }, async redirects() { return [ { diff --git a/src/services/api/config.ts b/src/services/api/config.ts index fd04c508..4de924c9 100644 --- a/src/services/api/config.ts +++ b/src/services/api/config.ts @@ -1,5 +1,5 @@ const config = { - baseAPI: process.env.API_URL || process.env.NEXT_PUBLIC_SITE_URL + '/api', + baseAPI: process.env.API_URL || process.env.NEXT_PUBLIC_API_URL, config: { headers: { 'Content-type': 'application/json', diff --git a/src/services/api/fetchRequest.ts b/src/services/api/fetchRequest.ts index e1ba2ad9..f53b359b 100644 --- a/src/services/api/fetchRequest.ts +++ b/src/services/api/fetchRequest.ts @@ -2,10 +2,11 @@ import { getCookie } from '@/utils/cookies'; import config from './config'; +// Types +type HttpMethod = 'get' | 'post' | 'put' | 'delete' | 'patch'; + export interface BaseFetchRequestProps< - TParams extends Record | FormData = - | Record - | FormData, + TParams = Record | FormData, > { params?: TParams; page?: number; @@ -16,15 +17,84 @@ export interface BaseFetchRequestProps< auth?: string; } -export interface FetchRequestProps< - TParams extends Record | FormData = - | Record - | FormData, -> extends BaseFetchRequestProps { +export interface FetchRequestProps | FormData> + extends BaseFetchRequestProps { path: string; - method: string; + method: HttpMethod; + signal?: AbortSignal; +} + +// Utility functions +function buildPaginationParams({ + page, + size, +}: Pick): URLSearchParams { + const params = new URLSearchParams(); + if (page) params.set('page', String(page)); + if (size) params.set('size', String(size)); + return params; +} + +function buildQueryParams( + method: HttpMethod, + params?: Record, +): string { + if (method.toLowerCase() !== 'get' || !params) return ''; + return '&' + new URLSearchParams(params).toString(); +} + +function buildRequestBody( + method: HttpMethod, + params?: Record | FormData, + isFormData = false, +): BodyInit | undefined { + if (method.toLowerCase() === 'get' || !params) return undefined; + return isFormData ? (params as FormData) : JSON.stringify(params); } +async function buildHeaders( + options: Pick, + isFormData: boolean, +): Promise { + const headers: Record = { + // Only include default content-type header if not FormData + ...(isFormData ? {} : config.config.headers), + captcha: options.captcha || '', + }; + + // Add auth header for server-side requests + if (typeof window === 'undefined') { + headers.auth = (options.auth || (await getCookie('auth')))!; + } else { + if (process.env.NEXT_PUBLIC_DEV === 'true') { + headers.auth = options.auth || (await getCookie('auth'))!; + } + } + + return headers; +} + +async function handleResponse( + response: Response, +): Promise { + if (!response.ok) { + if (response.status >= 400 && response.status <= 499) { + throw await response.json(); + } + console.error(response); + throw new Error( + `Request failed: ${response.status} ${response.statusText}`, + ); + } + + const contentType = response.headers.get('Content-Type') || ''; + if (contentType.includes('application/json')) { + return response.json(); + } + return response.text() as unknown as TResponse; +} + +// Main fetch request function export async function fetchRequest({ path, method, @@ -32,57 +102,33 @@ export async function fetchRequest({ page, size, captcha, - formData, - config: myConfig, + formData = false, + config: customConfig, auth, + signal, }: FetchRequestProps): Promise { - const paginationParams = new URLSearchParams({ - ...(page ? { page: String(page) } : {}), - ...(size ? { size: String(size) } : {}), - }).toString(); - - const queryParams = - (method === 'get' && - params && - '&' + - new URLSearchParams( - params as Record, - ).toString()) || - ''; - - const input = config.baseAPI + path + '?' + paginationParams + queryParams; - - const res = await fetch(input, { + // Build URL + const paginationParams = buildPaginationParams({ page, size }); + const queryParams = buildQueryParams(method, params as Record); + const url = `${config.baseAPI}${path}?${paginationParams}${queryParams}`; + + // Build request options + const headers = await buildHeaders({ captcha, auth }, formData); + const body = buildRequestBody(method, params, formData); + + // Make request + const response = await fetch(url, { + method: method.toUpperCase(), credentials: 'include', - method: method, - body: - method !== 'get' && params - ? formData - ? (params as FormData) - : JSON.stringify(params) - : undefined, - ...(myConfig ? {} : { cache: 'no-store' }), - ...config.config, - ...myConfig, - headers: { - ...(formData ? {} : config.config.headers), - captcha: captcha || '', - ...(typeof window === 'undefined' - ? { auth: auth || (await getCookie('auth')) } - : {}), - }, + body, + cache: customConfig ? undefined : 'no-store', + ...config.config, // Apply base config + ...customConfig, // Apply custom config + headers, // Apply headers last to ensure proper overwrites + signal, }); - await handleError(res); - - return await res.json(); + return handleResponse(response); } -async function handleError(response: Response) { - if (!response.ok) { - if (response.status >= 400 && response.status <= 499) { - throw await response.json(); - } - throw new Error('Failed to fetch data'); - } -} +export type { HttpMethod }; diff --git a/src/utils/cookies.ts b/src/utils/cookies.ts index 11bb7a37..54275a81 100644 --- a/src/utils/cookies.ts +++ b/src/utils/cookies.ts @@ -3,10 +3,13 @@ import { cookies } from 'next/headers'; export async function setCookie(name: string, value: string) { + console.log(process.env.COOKIE_DOMAIN, process.env.COOKIE_HTTP_ONLY); + (await cookies()).set(name, value, { maxAge: 30 * 24 * 60 * 60, - httpOnly: true, + httpOnly: process.env.COOKIE_HTTP_ONLY === 'true', sameSite: 'lax', + domain: process.env.COOKIE_DOMAIN, }); } diff --git a/yarn.lock b/yarn.lock index bd690a87..808e45de 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7400,11 +7400,11 @@ __metadata: remark-mentions: "npm:^1.1.0" remark-parse: "npm:^11.0.0" sharp: "npm:^0.33.5" - slate: "npm:^0.110.2" + slate: "npm:^0.112.0" slate-dom: "npm:^0.111.0" slate-history: "npm:^0.110.3" slate-hyperscript: "npm:^0.100.0" - slate-react: "npm:^0.111.0" + slate-react: "npm:^0.112.0" tailwind-gradient-mask-image: "npm:^1.2.0" tailwind-merge: "npm:^2.5.4" tailwindcss: "npm:3.4.14" @@ -11165,9 +11165,9 @@ __metadata: languageName: node linkType: hard -"slate-react@npm:^0.111.0": - version: 0.111.0 - resolution: "slate-react@npm:0.111.0" +"slate-react@npm:^0.112.0": + version: 0.112.0 + resolution: "slate-react@npm:0.112.0" dependencies: "@juggle/resize-observer": "npm:^3.4.0" direction: "npm:^1.0.4" @@ -11181,18 +11181,18 @@ __metadata: react-dom: ">=18.2.0" slate: ">=0.99.0" slate-dom: ">=0.110.2" - checksum: 10c0/9228ece95538b9286709ed08fe8d6f5b4669daf83655ab3eca93953f4c2eb8109ff7ba1af74561f6bb800dacbc41514cfc034888df020ac906cd285164f6735e + checksum: 10c0/2d9b88e68f9b6dd17a82551f0e68d30a80e42919076e057c7cb5d512a975ad8f641f575ea20cb64cd498016b23ffb467bb447946b88feee24c703777ef0ba79b languageName: node linkType: hard -"slate@npm:^0.110.2": - version: 0.110.2 - resolution: "slate@npm:0.110.2" +"slate@npm:^0.112.0": + version: 0.112.0 + resolution: "slate@npm:0.112.0" dependencies: immer: "npm:^10.0.3" is-plain-object: "npm:^5.0.0" tiny-warning: "npm:^1.0.3" - checksum: 10c0/90ed1b3c92898ccd4b3cb33216797acfb6eacbc2e60c7b8d8af4f02305195c3832a8cddb4651b10c4beb1da122bfaa42bb748e6464d75c8bfd545f017297041c + checksum: 10c0/e31dc1eb13c20505d243398bb91efeb8a8ef602eef6c8d4b55f616a39dd80ba4405916a4e40f30b0aa4ba8fbfd52a83508ecb947ad4b4cbc68d8e1f6b518eb96 languageName: node linkType: hard