hmmmammamam

This commit is contained in:
NotII
2025-03-24 00:08:32 +00:00
parent ce9f4e4d49
commit c65511aa5d
11 changed files with 229 additions and 123 deletions

View File

@@ -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"]

View File

@@ -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("; ")

View File

@@ -51,7 +51,7 @@ export const ProductModal: React.FC<ProductModalProps> = ({
// 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);

76
lib/api-utils.ts Normal file
View File

@@ -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<string, string> = {}): Headers {
const headers = new Headers({
'Content-Type': 'application/json',
...customHeaders
});
const authToken = token || getAuthToken();
if (authToken) {
headers.set('Authorization', `Bearer ${authToken}`);
}
return headers;
}

View File

@@ -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<T>(
endpoint: string,
options: FetchOptions = {}
): Promise<T> {
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<string, string> = {
'Content-Type': 'application/json',
...(headers as Record<string, string>),
};
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<string, string>);
// 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<T>(
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,

View File

@@ -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<any> {
export async function clientFetch<T = any>(url: string, options: RequestInit = {}): Promise<T> {
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<string, string>);
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);
// 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,
credentials: 'include',
});
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;
}
}

View File

@@ -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<any> {
export async function fetchData<T = any>(url: string, options: RequestInit = {}): Promise<T> {
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;
}
}

View File

@@ -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",

View File

@@ -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<T = unknown>(
endpoint: string,
options: RequestInit = {}
): Promise<T> {
// 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';
// Ensure there's only one slash between the base URL and endpoint
const url = apiUrl.endsWith('/')
? `${apiUrl}${cleanEndpoint}`
: `${apiUrl}/${cleanEndpoint}`;
// Get the complete backend URL using the utility function
const url = getServerApiUrl(endpoint);
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<T = unknown>(
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}`);
return res.json();
// 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);
}
// 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;

View File

@@ -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<ShippingMethod, "_id">
@@ -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<ShippingMethod>
) => {
try {
const res = await fetchData(

View File

@@ -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 <T = any>(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 <T = any>(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 <T = any>(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 <T = any>(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");
}
};