From c65511aa5d2d9cbba428f68e46a3428c5ce144e9 Mon Sep 17 00:00:00 2001 From: NotII <46204250+NotII@users.noreply.github.com> Date: Mon, 24 Mar 2025 00:08:32 +0000 Subject: [PATCH] hmmmammamam --- Dockerfile | 12 +++- components/modals/broadcast-dialog.tsx | 3 - components/modals/product-modal.tsx | 2 +- lib/api-utils.ts | 76 ++++++++++++++++++++++++++ lib/client-service.ts | 56 +++++-------------- lib/client-utils.ts | 55 +++++++++---------- lib/data-service.ts | 26 +++++++-- lib/productData.ts | 33 ++++++++--- lib/server-service.ts | 36 +++++++----- lib/shippingHelper.ts | 30 +++++++--- lib/storeHelper.ts | 23 ++++---- 11 files changed, 229 insertions(+), 123 deletions(-) create mode 100644 lib/api-utils.ts diff --git a/Dockerfile b/Dockerfile index acb41cb..d455564 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,13 +1,18 @@ # Use official Node.js image as base FROM node:18-alpine AS builder WORKDIR /app -COPY package.json package-lock.json ./ +# Install dependencies +COPY package.json package-lock.json ./ RUN npm install --force +# Copy source code COPY . . +# Set environment variables for build +ENV NODE_ENV=production ENV NEXT_PUBLIC_API_URL=/api +ENV SERVER_API_URL=https://internal-api.inboxi.ng/api # Build the Next.js application RUN npm run build @@ -18,19 +23,22 @@ FROM node:18-alpine AS runner # Set working directory inside the container WORKDIR /app +# Create necessary directories RUN mkdir -p /app/public # Copy only necessary files from builder COPY --from=builder /app/package.json /app/package-lock.json ./ COPY --from=builder /app/.next ./.next COPY --from=builder /app/node_modules ./node_modules +COPY --from=builder /app/public ./public +# Expose the app port EXPOSE 3000 +# Set runtime environment variables ENV NODE_ENV=production ENV NEXT_PUBLIC_API_URL=/api ENV SERVER_API_URL=https://internal-api.inboxi.ng/api - # Start Next.js server CMD ["npm", "run", "start"] \ No newline at end of file diff --git a/components/modals/broadcast-dialog.tsx b/components/modals/broadcast-dialog.tsx index 3a7435d..3cc668c 100644 --- a/components/modals/broadcast-dialog.tsx +++ b/components/modals/broadcast-dialog.tsx @@ -90,9 +90,6 @@ export default function BroadcastDialog({ open, setOpen }: BroadcastDialogProps) try { setIsSending(true); - const API_URL = process.env.NEXT_PUBLIC_API_URL; - if (!API_URL) throw new Error("API URL not configured"); - // Get auth token from cookie const authToken = document.cookie .split("; ") diff --git a/components/modals/product-modal.tsx b/components/modals/product-modal.tsx index b506965..2039c18 100644 --- a/components/modals/product-modal.tsx +++ b/components/modals/product-modal.tsx @@ -51,7 +51,7 @@ export const ProductModal: React.FC = ({ // If productData.image is a *URL* (string), show it as a default preview useEffect(() => { if (productData.image && typeof productData.image === "string" && productData._id) { - setImagePreview(`${process.env.NEXT_PUBLIC_API_URL}/products/${productData._id}/image`); + setImagePreview(`/api/products/${productData._id}/image`); } else if (productData.image && typeof productData.image === "string") { // Image exists but no ID, this is probably a new product setImagePreview(null); diff --git a/lib/api-utils.ts b/lib/api-utils.ts new file mode 100644 index 0000000..4ee689d --- /dev/null +++ b/lib/api-utils.ts @@ -0,0 +1,76 @@ +/** + * API utilities for consistent request handling + */ + +/** + * Normalizes a URL to ensure it passes through the Next.js API proxy + * This ensures all client-side requests go through the Next.js rewrites. + * + * @param url The endpoint URL to normalize + * @returns A normalized URL with proper /api prefix + */ +export function normalizeApiUrl(url: string): string { + if (url.startsWith('/api/')) { + return url; // Already has /api/ prefix + } else { + // Add /api prefix, handling any leading slashes + const cleanUrl = url.startsWith('/') ? url : `/${url}`; + return `/api${cleanUrl}`; + } +} + +/** + * Constructs a server-side API URL for backend requests + * Used in Server Components and API routes to directly access the backend API + * + * @param endpoint The API endpoint path + * @returns A complete URL to the backend API + */ +export function getServerApiUrl(endpoint: string): string { + const apiUrl = process.env.SERVER_API_URL || 'https://internal-api.inboxi.ng/api'; + const cleanEndpoint = endpoint.startsWith('/') ? endpoint.substring(1) : endpoint; + + return apiUrl.endsWith('/') + ? `${apiUrl}${cleanEndpoint}` + : `${apiUrl}/${cleanEndpoint}`; +} + +/** + * Get the authentication token from cookies or localStorage + */ +export function getAuthToken(): string | null { + if (typeof document === 'undefined') return null; // Guard for SSR + + return document.cookie + .split('; ') + .find(row => row.startsWith('Authorization=')) + ?.split('=')[1] || localStorage.getItem('Authorization'); +} + +/** + * Check if the user is logged in + */ +export function isAuthenticated(): boolean { + return !!getAuthToken(); +} + +/** + * Creates standard API request headers with authentication + * + * @param token Optional auth token (fetched automatically if not provided) + * @param customHeaders Additional headers to include + * @returns Headers object ready for fetch requests + */ +export function createApiHeaders(token?: string | null, customHeaders: Record = {}): Headers { + const headers = new Headers({ + 'Content-Type': 'application/json', + ...customHeaders + }); + + const authToken = token || getAuthToken(); + if (authToken) { + headers.set('Authorization', `Bearer ${authToken}`); + } + + return headers; +} \ No newline at end of file diff --git a/lib/client-service.ts b/lib/client-service.ts index 3e9bcbf..98e198f 100644 --- a/lib/client-service.ts +++ b/lib/client-service.ts @@ -1,6 +1,7 @@ 'use client'; import { toast } from "@/components/ui/use-toast"; +import { normalizeApiUrl, getAuthToken, createApiHeaders } from "./api-utils"; type FetchMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'; @@ -11,53 +12,26 @@ interface FetchOptions { headers?: HeadersInit; } -// Helper function to get auth token from cookies -function getAuthToken(): string | null { - if (typeof document === 'undefined') return null; // Guard for SSR - - return document.cookie - .split('; ') - .find(row => row.startsWith('Authorization=')) - ?.split('=')[1] || null; -} - +/** + * Client-side fetch utility that ensures all requests go through the Next.js API proxy + */ export async function fetchClient( endpoint: string, options: FetchOptions = {} ): Promise { const { method = 'GET', body, headers = {}, ...rest } = options; - // Always use the Next.js API proxy by creating a path starting with /api/ - // This ensures requests go through Next.js rewrites - const normalizedEndpoint = endpoint.startsWith('/') ? endpoint : `/${endpoint}`; + // Normalize the endpoint to ensure it starts with /api/ + const url = normalizeApiUrl(endpoint); - // Construct the URL to always use the Next.js API routes - let url; - if (normalizedEndpoint.startsWith('/api/')) { - url = normalizedEndpoint; // Already has /api/ prefix - } else { - url = `/api${normalizedEndpoint}`; // Add /api/ prefix - } - - // Get auth token from cookies - const authToken = getAuthToken(); - - // Prepare headers with authentication if token exists - const requestHeaders: Record = { - 'Content-Type': 'application/json', - ...(headers as Record), - }; - - if (authToken) { - // Backend expects "Bearer TOKEN" format - requestHeaders['Authorization'] = `Bearer ${authToken}`; - console.log('Authorization header set to:', `Bearer ${authToken.substring(0, 10)}...`); - } + // Get auth token and prepare headers + const requestHeaders = createApiHeaders(null, headers as Record); + // Log request details (useful for debugging) console.log('API Request:', { url, method, - hasAuthToken: !!authToken + hasAuthToken: requestHeaders.has('Authorization') }); const fetchOptions: RequestInit = { @@ -76,24 +50,22 @@ export async function fetchClient( if (!response.ok) { const errorData = await response.json().catch(() => ({})); - const errorMessage = errorData.message || errorData.error || 'An error occurred'; - + const errorMessage = errorData.message || errorData.error || `Request failed with status ${response.status}`; throw new Error(errorMessage); } + // Handle 204 No Content responses if (response.status === 204) { return {} as T; } - const data = await response.json(); - return data; + return await response.json(); } catch (error) { console.error('API request failed:', error); - // Only show toast if this is a client-side error (not during SSR) + // Only show toast in browser environment if (typeof window !== 'undefined') { const message = error instanceof Error ? error.message : 'Failed to connect to server'; - toast({ title: 'Error', description: message, diff --git a/lib/client-utils.ts b/lib/client-utils.ts index 07cacdf..fccbe20 100644 --- a/lib/client-utils.ts +++ b/lib/client-utils.ts @@ -1,41 +1,38 @@ +import { normalizeApiUrl, getAuthToken, createApiHeaders } from './api-utils'; + /** * Simple client-side fetch function for making API calls with Authorization header. + * Ensures all requests go through the Next.js API proxy. */ -export async function clientFetch(url: string, options: RequestInit = {}): Promise { +export async function clientFetch(url: string, options: RequestInit = {}): Promise { try { - const authToken = document.cookie - .split('; ') - .find(row => row.startsWith('Authorization=')) - ?.split('=')[1] || localStorage.getItem('Authorization'); + // Create headers with authentication + const headers = createApiHeaders(null, options.headers as Record); - const headers = { - 'Content-Type': 'application/json', - ...(authToken ? { Authorization: `Bearer ${authToken}` } : {}), - ...options.headers, - }; + // Normalize URL to ensure it uses the Next.js API proxy + const fullUrl = normalizeApiUrl(url); + + const res = await fetch(fullUrl, { + ...options, + headers, + credentials: 'include', + }); - // Always use the Next.js API proxy for consistent routing - // Format the URL to ensure it has the /api prefix - let fullUrl; - if (url.startsWith('/api/')) { - fullUrl = url; // Already has /api/ prefix - } else { - // Add /api prefix if not already present - const cleanUrl = url.startsWith('/') ? url : `/${url}`; - fullUrl = `/api${cleanUrl}`; - } - - const res = await fetch(fullUrl, { - ...options, - headers, - }); + if (!res.ok) { + const errorData = await res.json().catch(() => ({})); + const errorMessage = errorData.message || errorData.error || `Request failed: ${res.status} ${res.statusText}`; + throw new Error(errorMessage); + } - if (!res.ok) throw new Error(`Request failed: ${res.statusText}`); + // Handle 204 No Content responses + if (res.status === 204) { + return {} as T; + } - return res.json(); + return await res.json(); } catch (error) { - console.error(`Client fetch error at ${url}:`, error); - throw error; + console.error(`Client fetch error at ${url}:`, error); + throw error; } } diff --git a/lib/data-service.ts b/lib/data-service.ts index 04aa20b..c16ca2c 100644 --- a/lib/data-service.ts +++ b/lib/data-service.ts @@ -1,13 +1,27 @@ /** * Client-side fetch function for API requests. + * A simple wrapper over fetch with improved error handling. */ -export async function fetchData(url: string, options: RequestInit = {}): Promise { +export async function fetchData(url: string, options: RequestInit = {}): Promise { try { - const res = await fetch(url, options); - if (!res.ok) throw new Error(`Request failed: ${res.statusText}`); - return res.json(); + const res = await fetch(url, options); + + // Check for no content response + if (res.status === 204) { + return {} as T; + } + + // Handle errors + if (!res.ok) { + const errorData = await res.json().catch(() => ({})); + const errorMessage = errorData.message || errorData.error || `Request failed: ${res.status} ${res.statusText}`; + throw new Error(errorMessage); + } + + // Parse normal response + return await res.json(); } catch (error) { - console.error(`Fetch error at ${url}:`, error); - throw error; + console.error(`Fetch error at ${url}:`, error); + throw error; } } \ No newline at end of file diff --git a/lib/productData.ts b/lib/productData.ts index 3b6f6ea..1b249d6 100644 --- a/lib/productData.ts +++ b/lib/productData.ts @@ -1,8 +1,12 @@ import { fetchData } from '@/lib/data-service'; +import { normalizeApiUrl } from './api-utils'; +/** + * Fetches product data from the API + */ export const fetchProductData = async (url: string, authToken: string) => { try { - return await fetchData(url, { + return await fetchData(normalizeApiUrl(url), { headers: { Authorization: `Bearer ${authToken}` }, credentials: "include", }); @@ -12,6 +16,9 @@ export const fetchProductData = async (url: string, authToken: string) => { } }; +/** + * Saves product data to the API + */ export const saveProductData = async ( url: string, data: any, @@ -19,7 +26,7 @@ export const saveProductData = async ( method: "POST" | "PUT" = "POST" ) => { try { - return await fetchData(url, { + return await fetchData(normalizeApiUrl(url), { method, headers: { Authorization: `Bearer ${authToken}`, @@ -34,12 +41,15 @@ export const saveProductData = async ( } }; -export const saveProductImage = async(url: string, file:File, authToken: string) => { +/** + * Uploads a product image + */ +export const saveProductImage = async(url: string, file: File, authToken: string) => { try{ const formData = new FormData(); formData.append("file", file); - return await fetchData(url, { + return await fetchData(normalizeApiUrl(url), { method: "PUT", headers: { Authorization: `Bearer ${authToken}`, @@ -52,9 +62,12 @@ export const saveProductImage = async(url: string, file:File, authToken: string) } } +/** + * Deletes a product + */ export const deleteProductData = async (url: string, authToken: string) => { try { - return await fetchData(url, { + return await fetchData(normalizeApiUrl(url), { method: "DELETE", headers: { Authorization: `Bearer ${authToken}`, @@ -68,10 +81,12 @@ export const deleteProductData = async (url: string, authToken: string) => { } }; -// Stock management functions +/** + * Fetches product stock information + */ export const fetchStockData = async (url: string, authToken: string) => { try { - return await fetchData(url, { + return await fetchData(normalizeApiUrl(url), { headers: { Authorization: `Bearer ${authToken}` }, credentials: "include", }); @@ -81,6 +96,9 @@ export const fetchStockData = async (url: string, authToken: string) => { } }; +/** + * Updates product stock information + */ export const updateProductStock = async ( productId: string, stockData: { @@ -91,7 +109,6 @@ export const updateProductStock = async ( authToken: string ) => { try { - // Use Next.js API proxy to ensure request goes through rewrites const url = `/api/stock/${productId}`; return await fetchData(url, { method: "PUT", diff --git a/lib/server-service.ts b/lib/server-service.ts index 499da21..0560b86 100644 --- a/lib/server-service.ts +++ b/lib/server-service.ts @@ -1,32 +1,30 @@ import { cookies } from 'next/headers'; import { redirect } from 'next/navigation'; +import { getServerApiUrl } from './api-utils'; /** * Server-side fetch wrapper with authentication. + * Used in Server Components to make authenticated API requests to the backend. + * This uses the SERVER_API_URL environment variable and is different from client-side fetching. */ export async function fetchServer( endpoint: string, options: RequestInit = {} ): Promise { + // Get auth token from cookies const cookieStore = cookies(); const authToken = cookieStore.get('Authorization')?.value; + // Redirect to login if not authenticated if (!authToken) redirect('/login'); - - // Ensure the endpoint doesn't start with a slash if it's going to be appended to a URL that ends with one - const cleanEndpoint = endpoint.startsWith('/') ? endpoint.substring(1) : endpoint; try { - // Make sure we're using a complete URL (protocol + hostname + path) - const apiUrl = process.env.SERVER_API_URL || 'https://internal-api.inboxi.ng/api'; + // Get the complete backend URL using the utility function + const url = getServerApiUrl(endpoint); - // Ensure there's only one slash between the base URL and endpoint - const url = apiUrl.endsWith('/') - ? `${apiUrl}${cleanEndpoint}` - : `${apiUrl}/${cleanEndpoint}`; - console.log(`Making server request to: ${url}`); + // Make the request with proper auth headers const res = await fetch(url, { ...options, headers: { @@ -34,13 +32,25 @@ export async function fetchServer( Authorization: `Bearer ${authToken}`, ...options.headers, }, - cache: 'no-store', + cache: 'no-store', // Always fetch fresh data }); + // Handle auth failures if (res.status === 401) redirect('/login'); - if (!res.ok) throw new Error(`Request failed: ${res.statusText}`); + + // Handle other errors + if (!res.ok) { + const errorData = await res.json().catch(() => ({})); + const errorMessage = errorData.message || errorData.error || `Request failed: ${res.status} ${res.statusText}`; + throw new Error(errorMessage); + } - return res.json(); + // Handle 204 No Content responses + if (res.status === 204) { + return {} as T; + } + + return await res.json(); } catch (error) { console.error(`Server request to ${endpoint} failed:`, error); throw error; diff --git a/lib/shippingHelper.ts b/lib/shippingHelper.ts index fc56074..e432fce 100644 --- a/lib/shippingHelper.ts +++ b/lib/shippingHelper.ts @@ -1,5 +1,18 @@ import { fetchData } from '@/lib/data-service'; +import { normalizeApiUrl } from './api-utils'; +/** + * Interface for shipping method data + */ +interface ShippingMethod { + name: string; + price: number; + _id?: string; +} + +/** + * Fetches all shipping methods for the current store + */ export const fetchShippingMethods = async (authToken: string) => { try { const res = await fetchData( @@ -23,12 +36,9 @@ export const fetchShippingMethods = async (authToken: string) => { } }; -interface ShippingMethod { - name: string; - price: number; - _id?: string; -} - +/** + * Adds a new shipping method + */ export const addShippingMethod = async ( authToken: string, newShipping: Omit @@ -69,6 +79,9 @@ export const addShippingMethod = async ( } }; +/** + * Deletes a shipping method by ID + */ export const deleteShippingMethod = async (authToken: string, id: string) => { try { const res = await fetchData( @@ -88,10 +101,13 @@ export const deleteShippingMethod = async (authToken: string, id: string) => { } }; +/** + * Updates an existing shipping method + */ export const updateShippingMethod = async ( authToken: string, id: string, - updatedShipping: any + updatedShipping: Partial ) => { try { const res = await fetchData( diff --git a/lib/storeHelper.ts b/lib/storeHelper.ts index 1350952..d5e1867 100644 --- a/lib/storeHelper.ts +++ b/lib/storeHelper.ts @@ -1,11 +1,17 @@ import { fetchData } from '@/lib/data-service'; +import { normalizeApiUrl } from './api-utils'; +/** + * Sends authenticated API requests, ensuring they go through the Next.js API proxy + */ export const apiRequest = async (endpoint: string, method: string = "GET", body?: T | null) => { try { + // Enforce client-side execution if (typeof document === "undefined") { throw new Error("API requests must be made from the client side."); } + // Get authentication token const authToken = document.cookie .split("; ") .find((row) => row.startsWith("Authorization=")) @@ -16,6 +22,7 @@ export const apiRequest = async (endpoint: string, method: string = "GE throw new Error("No authentication token found"); } + // Prepare request options const options: RequestInit = { method, headers: { @@ -29,16 +36,8 @@ export const apiRequest = async (endpoint: string, method: string = "GE options.body = JSON.stringify(body); } - // Always use the Next.js API proxy to ensure all requests go through rewrites - // Format the endpoint to ensure it has the /api prefix - let url; - if (endpoint.startsWith('/api/')) { - url = endpoint; // Already has /api/ prefix - } else { - // Add /api prefix and ensure no duplicate slashes - const cleanEndpoint = endpoint.startsWith('/') ? endpoint : `/${endpoint}`; - url = `/api${cleanEndpoint}`; - } + // Normalize URL to ensure it uses the Next.js API proxy + const url = normalizeApiUrl(endpoint); const res = await fetchData(url, options); @@ -51,11 +50,11 @@ export const apiRequest = async (endpoint: string, method: string = "GE return res; } catch (error: unknown) { if (error instanceof Error) { - console.error(`🚨 API Request Error: ${error.message}`); + console.error(`API Request Error: ${error.message}`); throw new Error(error.message); } - console.error("❌ An unknown error occurred", error); + console.error("An unknown error occurred", error); throw new Error("An unknown error occurred"); } }; \ No newline at end of file