Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions crackcode/client/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
12 changes: 2 additions & 10 deletions crackcode/client/src/components/leaderboard/leaderboard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -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);
}
Expand Down
2 changes: 1 addition & 1 deletion crackcode/client/src/components/store/StoreItemCard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 || "";

Expand Down
21 changes: 12 additions & 9 deletions crackcode/client/src/context/userauth/authenticationContext.jsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,36 @@
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

// Helper function to set authorization header from stored token
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
});

Expand All @@ -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
});

Expand Down
29 changes: 7 additions & 22 deletions crackcode/client/src/pages/shop/AvatarShop.jsx
Original file line number Diff line number Diff line change
@@ -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([]);
Expand All @@ -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) {
Expand Down Expand Up @@ -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!");
Expand Down Expand Up @@ -129,7 +114,7 @@ const AvatarShop = () => {
className="border p-4 rounded-xl text-center shadow bg-white"
>
<img
src={`http://localhost:5051${avatar.imageUrl}`}
src={`${API_BASE}${avatar.imageUrl}`}
alt={avatar.name}
className="w-24 h-24 mx-auto mb-3 rounded-full object-cover"
/>
Expand Down
2 changes: 1 addition & 1 deletion crackcode/client/src/pages/shop/DetectiveStore.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
9 changes: 5 additions & 4 deletions crackcode/client/src/pages/userauth/Login.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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,
Expand All @@ -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,
});
Expand All @@ -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) {}
}

Expand All @@ -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);
Expand Down
30 changes: 15 additions & 15 deletions crackcode/client/src/pages/userprofile/userprofile.jsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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;
Expand All @@ -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 || {};
Expand All @@ -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);
Expand All @@ -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');
Expand Down
68 changes: 12 additions & 56 deletions crackcode/client/src/services/api/badgeService.js
Original file line number Diff line number Diff line change
@@ -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 };
2 changes: 1 addition & 1 deletion crackcode/client/src/services/api/careermapService.js
Original file line number Diff line number Diff line change
@@ -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 = () => {
Expand Down
Loading
Loading