diff --git a/crackcode/client/Dockerfile b/crackcode/client/Dockerfile
index b75e1889..b7c99ad1 100644
--- a/crackcode/client/Dockerfile
+++ b/crackcode/client/Dockerfile
@@ -7,11 +7,14 @@ RUN npm install
COPY . .
-# Accept backend URL as build argument, default to localhost:5051
+# Accept backend URL and API URL as build arguments, default to localhost:5051
ARG VITE_BACKEND_URL=http://localhost:5051
+ARG VITE_API_URL=http://localhost:5051
# Create .env.production file with Vite variables
-RUN echo "VITE_BACKEND_URL=${VITE_BACKEND_URL}" > .env.production
+# Populate both VITE_BACKEND_URL and VITE_API_URL so code using either variable works
+RUN echo "VITE_BACKEND_URL=${VITE_BACKEND_URL}" > .env.production && \
+ echo "VITE_API_URL=${VITE_API_URL}" >> .env.production
RUN npm run build
diff --git a/crackcode/client/src/components/common/SettingsDropdown.jsx b/crackcode/client/src/components/common/SettingsDropdown.jsx
index fef64e83..045dc25d 100644
--- a/crackcode/client/src/components/common/SettingsDropdown.jsx
+++ b/crackcode/client/src/components/common/SettingsDropdown.jsx
@@ -5,7 +5,7 @@ import { Settings, Sun, Moon, Cloud, Palette, User, LogOut, Lock } from 'lucide-
import { THEMES } from '../../context/theme/ThemeContext';
const LOCKED_THEMES = ['country', 'midnight'];
-const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:5051';
+const API_BASE_URL = import.meta.env.VITE_BACKEND_URL || import.meta.env.VITE_API_URL || 'http://localhost:5051';
export default function SettingsDropdown() {
const [open, setOpen] = useState(false);
diff --git a/crackcode/client/src/components/leaderboard/leaderboard.jsx b/crackcode/client/src/components/leaderboard/leaderboard.jsx
index b8ccfdde..55541d22 100644
--- a/crackcode/client/src/components/leaderboard/leaderboard.jsx
+++ b/crackcode/client/src/components/leaderboard/leaderboard.jsx
@@ -101,11 +101,7 @@ export default function Leaderboard() {
if (data.success) setPlayers(data.leaderboard ?? []);
else throw new Error(data.message || "Failed to load leaderboard");
} catch (e) {
- setError(
- e.name === "AbortError"
- ? "Request timed out — is the backend running on port 5050?"
- : e.message
- );
+ setError(e.name === "AbortError" ? "Request timed out — is the backend reachable?" : e.message);
} finally {
setLoading(false);
}
@@ -121,11 +117,7 @@ export default function Leaderboard() {
setPagination(data.pagination);
} else throw new Error(data.message || "Failed to load leaderboard");
} catch (e) {
- setError(
- e.name === "AbortError"
- ? "Request timed out — is the backend running on port 5050?"
- : e.message
- );
+ setError(e.name === "AbortError" ? "Request timed out — is the backend reachable?" : e.message);
} finally {
setLoading(false);
}
diff --git a/crackcode/client/src/components/store/StoreItemCard.jsx b/crackcode/client/src/components/store/StoreItemCard.jsx
index 07db3c23..957b54e1 100644
--- a/crackcode/client/src/components/store/StoreItemCard.jsx
+++ b/crackcode/client/src/components/store/StoreItemCard.jsx
@@ -150,7 +150,7 @@ export default function StoreItemCard({
}) {
const { theme } = useTheme();
- const API_BASE_URL = import.meta.env.VITE_API_URL || "http://localhost:5051";
+ const API_BASE_URL = import.meta.env.VITE_BACKEND_URL || import.meta.env.VITE_API_URL || "http://localhost:5051";
const rawImagePath = item.imageUrl || item.image || "";
diff --git a/crackcode/client/src/context/userauth/authenticationContext.jsx b/crackcode/client/src/context/userauth/authenticationContext.jsx
index eebcdca5..c8e9f3a6 100644
--- a/crackcode/client/src/context/userauth/authenticationContext.jsx
+++ b/crackcode/client/src/context/userauth/authenticationContext.jsx
@@ -1,13 +1,18 @@
import { createContext, useEffect, useState } from "react";
-import axios from 'axios';
+import api from "../../api/axios";
export const AppContent = createContext()
export const AppContextProvider = (props) => {
- axios.defaults.withCredentials = true; // IMPORTANT: Allows cookies to be sent
+ api.defaults.withCredentials = true; // IMPORTANT: Allows cookies to be sent
- const backendUrl = import.meta.env.VITE_BACKEND_URL
+ // Prefer explicit Vite backend URL, fall back to legacy VITE_API_URL or the axios instance baseURL
+ const envBackend = import.meta.env.VITE_BACKEND_URL || import.meta.env.VITE_API_URL;
+ // Derive fallback from central axios instance if available (removes trailing /api)
+ const axiosBase = api?.defaults?.baseURL || '';
+ const inferredBackend = axiosBase ? axiosBase.replace(/\/api$/, '') : '';
+ const backendUrl = envBackend || inferredBackend || '';
const [isLoggedIn, setIsLoggedIn] = useState(false)
const [userData, setUserData] = useState(false) // Fixed typo from userDate
@@ -15,18 +20,17 @@ export const AppContextProvider = (props) => {
const setAuthHeader = () => {
const storedToken = typeof window !== 'undefined' && localStorage.getItem('accessToken');
if (storedToken) {
- axios.defaults.headers.common['Authorization'] = `Bearer ${storedToken}`;
+ api.defaults.headers.common['Authorization'] = `Bearer ${storedToken}`;
return true;
}
- delete axios.defaults.headers.common['Authorization'];
+ delete api.defaults.headers.common['Authorization'];
return false;
};
// Function to check auth status and get user data
const getAuthState = async () => {
try {
- const { data } = await axios.get(`${backendUrl}/api/auth/is-auth`,{
- withCredentials: true,
+ const { data } = await api.get('/auth/is-auth',{
timeout: 5000 // 5 second timeout
});
@@ -44,8 +48,7 @@ export const AppContextProvider = (props) => {
const getUserData = async () => {
try {
- const { data } = await axios.get(`${backendUrl}/api/user/data`,{
- withCredentials: true,
+ const { data } = await api.get('/user/data',{
timeout: 5000 // 5 second timeout
});
diff --git a/crackcode/client/src/pages/shop/AvatarShop.jsx b/crackcode/client/src/pages/shop/AvatarShop.jsx
index 0469189a..3072a374 100644
--- a/crackcode/client/src/pages/shop/AvatarShop.jsx
+++ b/crackcode/client/src/pages/shop/AvatarShop.jsx
@@ -1,8 +1,8 @@
import { useEffect, useState } from "react";
-import axios from "axios";
+import api from "../../api/axios";
-const API_BASE_URL = "http://localhost:5051/api";
+const API_BASE = import.meta.env.VITE_BACKEND_URL || import.meta.env.VITE_API_URL || "http://localhost:5051";
const AvatarShop = () => {
const [avatars, setAvatars] = useState([]);
@@ -27,21 +27,14 @@ const AvatarShop = () => {
setLoading(true);
setError("");
- const avatarsRes = await axios.get(
- `${API_BASE_URL}/shop/items?category=avatar`
- );
+ const avatarsRes = await api.get(`/shop/items?category=avatar`);
setAvatars(avatarsRes.data.items || []);
const token = getToken();
if (token) {
try {
- const inventoryRes = await axios.get(
- `${API_BASE_URL}/shop/inventory?category=avatar`,
- {
- headers: getAuthHeaders(),
- }
- );
+ const inventoryRes = await api.get(`/shop/inventory?category=avatar`);
setInventory(inventoryRes.data.items || []);
} catch (inventoryErr) {
@@ -76,21 +69,13 @@ const AvatarShop = () => {
setError("");
if (avatar.pricing.type === "paid") {
- const res = await axios.post(
- `${API_BASE_URL}/shop/checkout`,
- { itemId: avatar._id },
- { headers: getAuthHeaders() }
- );
+ const res = await api.post(`/shop/checkout`, { itemId: avatar._id });
window.location.href = res.data.checkoutUrl;
return;
}
- await axios.post(
- `${API_BASE_URL}/shop/purchase`,
- { itemId: avatar._id },
- { headers: getAuthHeaders() }
- );
+ await api.post(`/shop/purchase`, { itemId: avatar._id });
await loadAvatarShop();
alert("Avatar purchased successfully!");
@@ -129,7 +114,7 @@ const AvatarShop = () => {
className="border p-4 rounded-xl text-center shadow bg-white"
>
diff --git a/crackcode/client/src/pages/shop/DetectiveStore.jsx b/crackcode/client/src/pages/shop/DetectiveStore.jsx
index 90cfa9d8..b088292b 100644
--- a/crackcode/client/src/pages/shop/DetectiveStore.jsx
+++ b/crackcode/client/src/pages/shop/DetectiveStore.jsx
@@ -718,7 +718,7 @@ export default function DetectiveStore() {
const navigate = useNavigate();
const processingPaymentRef = useRef(false);
- const API_BASE_URL = import.meta.env.VITE_API_URL || "http://localhost:5051";
+ const API_BASE_URL = import.meta.env.VITE_BACKEND_URL || import.meta.env.VITE_API_URL || "http://localhost:5051";
const isLightFamily = ["light", "cream", "country"].includes(theme);
diff --git a/crackcode/client/src/pages/userauth/Login.jsx b/crackcode/client/src/pages/userauth/Login.jsx
index e1d9991b..e69bf75b 100644
--- a/crackcode/client/src/pages/userauth/Login.jsx
+++ b/crackcode/client/src/pages/userauth/Login.jsx
@@ -2,6 +2,7 @@ import React, { useContext, useEffect, useState } from "react";
import { useNavigate, Link } from "react-router-dom";
import { AppContent } from "../../context/userauth/authenticationContext";
import axios from "axios";
+import api from "../../api/axios";
import { toast } from "react-toastify";
import { Mail, LockKeyhole, UserRound } from "lucide-react";
import Logo from "../../assets/logo/crackcode_logo.svg";
@@ -40,7 +41,7 @@ const Login = () => {
return toast.error("You must accept the Terms and Conditions.");
}
- const { data } = await axios.post(`${backendUrl}/api/auth/register`, {
+ const { data } = await api.post(`/auth/register`, {
name,
email,
password,
@@ -55,7 +56,7 @@ const Login = () => {
toast.error(data?.message || "Registration failed.");
}
} else {
- const { data } = await axios.post(`${backendUrl}/api/auth/login`, {
+ const { data } = await api.post(`/auth/login`, {
email,
password,
});
@@ -65,7 +66,7 @@ const Login = () => {
if (data.accessToken) {
try {
localStorage.setItem('accessToken', data.accessToken);
- axios.defaults.headers.common['Authorization'] = `Bearer ${data.accessToken}`;
+ api.defaults.headers.common['Authorization'] = `Bearer ${data.accessToken}`;
} catch (e) {}
}
@@ -76,7 +77,7 @@ const Login = () => {
// If the returned user isn't verified, prompt verification flow
if (data.user && !data.user.isAccountVerified) {
try {
- await axios.post(`${backendUrl}/api/auth/send-verify-otp`);
+ await api.post(`/auth/send-verify-otp`);
toast.info("Please verify your email. OTP sent to your email.");
} catch (err) {
console.log("OTP send error on login:", err);
diff --git a/crackcode/client/src/pages/userprofile/userprofile.jsx b/crackcode/client/src/pages/userprofile/userprofile.jsx
index d958b76b..86fdc96c 100644
--- a/crackcode/client/src/pages/userprofile/userprofile.jsx
+++ b/crackcode/client/src/pages/userprofile/userprofile.jsx
@@ -1,5 +1,5 @@
import React, { useState, useEffect, useContext } from 'react'
-import axios from 'axios'
+import api from '../../api/axios'
import Button from '../../components/ui/Button';
import Header from '../../components/common/Header';
import { AppContent } from '../../context/userauth/authenticationContext';
@@ -64,10 +64,7 @@ const UserProfile = () => {
// Function to fetch user stats from API
const fetchUserStats = async () => {
try {
- const response = await axios.get(`${backendUrl}/api/user/data`, {
- withCredentials: true,
- timeout: 5000
- });
+ const response = await api.get('/user/data', { timeout: 5000 });
if (response.data.success) {
const data = response.data.data;
@@ -94,7 +91,17 @@ const UserProfile = () => {
// Fetch aggregated progress summary (difficulty counts + language counts)
const fetchProgressSummary = async () => {
try {
- const resp = await axios.get(`${backendUrl}/api/user/progress-summary`, { withCredentials: true, timeout: 5000 });
+ const resp = await api.get('/user/progress-summary', { timeout: 5000 });
+ // Debug: log full progress-summary response
+ console.debug('DEBUG: /api/user/progress-summary response', resp?.data);
+
+ // Debug: fetch raw progress documents to help diagnose aggregation issues
+ try {
+ const raw = await axios.get(`${backendUrl}/api/user/progress-raw`, { withCredentials: true, timeout: 5000 });
+ console.debug('DEBUG: /api/user/progress-raw (sample rows)', raw?.data?.data?.slice?.(0,10) || raw?.data);
+ } catch (rawErr) {
+ console.warn('Could not fetch /api/user/progress-raw:', rawErr?.message || rawErr);
+ }
if (!resp?.data?.success) return;
const data = resp.data.data || {};
@@ -120,10 +127,7 @@ const UserProfile = () => {
const fetchProfileSettings = async () => {
try {
setSettingsLoading(true);
- const response = await axios.get(`${backendUrl}/api/profile/settings`, {
- withCredentials: true,
- timeout: 5000
- });
+ const response = await api.get('/profile/settings', { timeout: 5000 });
if (response.data.success) {
setProfileSettings(response.data.data);
@@ -139,11 +143,7 @@ const UserProfile = () => {
const handleDeleteAccount = async () => {
try {
setIsDeleting(true);
- const response = await axios.post(
- `${backendUrl}/api/user/delete-account`,
- {},
- { withCredentials: true, timeout: 5000 }
- );
+ const response = await api.post('/user/delete-account', {}, { timeout: 5000 });
if (response.data.success) {
console.log('✅ Account deleted successfully');
diff --git a/crackcode/client/src/services/api/badgeService.js b/crackcode/client/src/services/api/badgeService.js
index 56131e70..c2dc6a74 100644
--- a/crackcode/client/src/services/api/badgeService.js
+++ b/crackcode/client/src/services/api/badgeService.js
@@ -1,66 +1,22 @@
-// Badge API Service
-const BASE_URL = import.meta.env.VITE_BACKEND_URL || "http://localhost:5051";
+import api from '../../api/axios';
-/*
- Fetch user's badge progress
- */
+// Use central axios instance which already enables credentials
export const fetchBadgeProgress = async () => {
- const res = await fetch(`${BASE_URL}/api/badges/my-progress`, {
- credentials: "include",
- });
-
- if (!res.ok) throw new Error(`Server error: ${res.status}`);
-
- const data = await res.json();
-
- if (data.success && Array.isArray(data.data)) {
- return data.data;
- }
-
- throw new Error(data.message || "Failed to load badge progress");
+ const { data } = await api.get('/badges/my-progress');
+ if (data.success && Array.isArray(data.data)) return data.data;
+ throw new Error(data.message || 'Failed to load badge progress');
};
-/*
- Fetch user's badge statistics
- */
export const fetchBadgeStats = async () => {
- const res = await fetch(`${BASE_URL}/api/badges/stats`, {
- credentials: "include",
- });
-
- if (!res.ok) throw new Error(`Server error: ${res.status}`);
-
- const data = await res.json();
-
- if (data.success) {
- return data.data;
- }
-
- throw new Error(data.message || "Failed to load badge stats");
+ const { data } = await api.get('/badges/stats');
+ if (data.success) return data.data;
+ throw new Error(data.message || 'Failed to load badge stats');
};
-/*
- Trigger manual badge check (refresh)
- */
export const triggerBadgeCheck = async () => {
- const res = await fetch(`${BASE_URL}/api/badges/check-all`, {
- method: "POST",
- credentials: "include",
- });
-
- if (!res.ok) throw new Error(`Server error: ${res.status}`);
-
- const data = await res.json();
-
- if (data.success) {
- return data.data;
- }
-
- throw new Error(data.message || "Failed to check badges");
+ const { data } = await api.post('/badges/check-all');
+ if (data.success) return data.data;
+ throw new Error(data.message || 'Failed to check badges');
};
-export default {
- fetchBadgeProgress,
- fetchBadgeStats,
- triggerBadgeCheck
-};
+export default { fetchBadgeProgress, fetchBadgeStats, triggerBadgeCheck };
diff --git a/crackcode/client/src/services/api/careermapService.js b/crackcode/client/src/services/api/careermapService.js
index 68d41698..ce8329a2 100644
--- a/crackcode/client/src/services/api/careermapService.js
+++ b/crackcode/client/src/services/api/careermapService.js
@@ -1,4 +1,4 @@
-const BASE_URL = `${import.meta.env.VITE_API_URL}/api`;
+const BASE_URL = `${import.meta.env.VITE_BACKEND_URL || import.meta.env.VITE_API_URL}/api`;
// Attach JWT token to requests if available
const authHeader = () => {
diff --git a/crackcode/client/src/services/api/leaderboardService.js b/crackcode/client/src/services/api/leaderboardService.js
index 0ea544be..f03f79ac 100644
--- a/crackcode/client/src/services/api/leaderboardService.js
+++ b/crackcode/client/src/services/api/leaderboardService.js
@@ -1,93 +1,26 @@
-// leaderboardService.js
-// Place this in: src/services/leaderboardService.js (or src/api/leaderboard.js)
-
-// =============================================================================
-// API Configuration
-// =============================================================================
-
-// Retrieve the API base URL from environment variables (Vite-style).
-// Falls back to localhost:5051 for local development if not set.
-const BASE_URL = import.meta.env.VITE_BACKEND_URL || "http://localhost:5051";
-
-// =============================================================================
-// Leaderboard API Functions
-// =============================================================================
+// leaderboardService.js - use central axios instance for consistent baseURL and credentials
+import api from '../../api/axios';
/**
- * Fetch top 10 players (public - no auth needed)
- *
- * Retrieves the global leaderboard showing the top-ranked players.
- * This endpoint is publicly accessible and doesn't require authentication.
- *
- * @returns {Promise<{ success: boolean, leaderboard: Array, source: string }>}
- * - success: indicates if the request was successful
- * - leaderboard: array of top player objects
- * - source: identifies where the data came from (e.g., cache vs database)
- * @throws {Error} If the fetch request fails (non-2xx response)
+ * Fetch top players (public)
*/
export const getGlobalLeaderboard = async () => {
- // Make GET request to the global leaderboard endpoint
- const res = await fetch(`${BASE_URL}/api/leaderboard/global`, {
- credentials: "include", // Include cookies for session handling (if any)
- });
-
- // Throw an error if the response status indicates failure
- if (!res.ok) throw new Error(`Failed to fetch leaderboard: ${res.status}`);
-
- // Parse and return the JSON response body
- return res.json();
+ const res = await api.get('/leaderboard/global');
+ return res.data;
};
/**
- * Fetch paginated leaderboard (public - no auth needed)
- *
- * Retrieves a paginated view of the leaderboard, useful for displaying
- * large lists of players with pagination controls.
- *
- * @param {number} page - The page number to fetch (default: 1, first page)
- * @param {number} limit - Number of results per page (default: 20)
- * @returns {Promise