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" > {avatar.name} 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} Paginated leaderboard data with player entries - * @throws {Error} If the fetch request fails (non-2xx response) + * Fetch paginated leaderboard (public) */ export const getPaginatedLeaderboard = async (page = 1, limit = 20) => { - // Build URL with query parameters for pagination - const res = await fetch( - `${BASE_URL}/api/leaderboard/paginated?page=${page}&limit=${limit}`, - { credentials: "include" } // Include cookies for session handling - ); - - // 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/paginated?page=${page}&limit=${limit}`); + return res.data; }; /** - * Fetch current user's rank (requires login) - * - * Retrieves the authenticated user's position on the leaderboard. - * This endpoint requires the user to be logged in (session cookie). - * - * @returns {Promise<{ success: boolean, position: number, username: string, totalXP: number, rank: string }>} - * - success: indicates if the request was successful - * - position: user's numerical rank on the leaderboard - * - username: the user's display name - * - totalXP: user's accumulated experience points - * - rank: user's tier/rank title (e.g., "Gold", "Platinum") - * @throws {Error} If the fetch request fails or user is not authenticated + * Fetch authenticated user's rank (requires session cookie) */ export const getMyRank = async () => { - // Make GET request to the user-specific rank endpoint - // credentials: "include" is required here to send the auth session cookie - const res = await fetch(`${BASE_URL}/api/leaderboard/me`, { - credentials: "include", - }); - - // Throw an error if the response status indicates failure - // Common failures: 401 (not logged in), 404 (user not found) - if (!res.ok) throw new Error(`Failed to fetch rank: ${res.status}`); - - // Parse and return the JSON response body - return res.json(); + const res = await api.get('/leaderboard/me'); + return res.data; }; \ No newline at end of file diff --git a/crackcode/client/src/services/api/shopService.js b/crackcode/client/src/services/api/shopService.js index bec60c98..bda8c7e7 100644 --- a/crackcode/client/src/services/api/shopService.js +++ b/crackcode/client/src/services/api/shopService.js @@ -22,8 +22,10 @@ import axios from "axios"; +const BASE = import.meta.env.VITE_BACKEND_URL || import.meta.env.VITE_API_URL || "http://localhost:5051"; + const API = axios.create({ - baseURL: "http://localhost:5050/api", + baseURL: `${BASE}/api`, withCredentials: true, }); diff --git a/crackcode/docker-compose.yml b/crackcode/docker-compose.yml index 83712ce3..e4e93bb7 100644 --- a/crackcode/docker-compose.yml +++ b/crackcode/docker-compose.yml @@ -13,7 +13,11 @@ services: restart: unless-stopped client: - build: ./client + build: + context: ./client + args: + VITE_BACKEND_URL: http://64.227.140.199:5051 + VITE_API_URL: http://64.227.140.199:5051 image: crackcode-client ports: - "4173:80" diff --git a/crackcode/server/scripts/fixProgressMetadata.mjs b/crackcode/server/scripts/fixProgressMetadata.mjs new file mode 100644 index 00000000..565de9b6 --- /dev/null +++ b/crackcode/server/scripts/fixProgressMetadata.mjs @@ -0,0 +1,76 @@ +import dotenv from 'dotenv'; +import mongoose from 'mongoose'; + +dotenv.config(); + +import UserQuestionProgress from '../src/modules/progress/UserQuestionProgress.model.js'; +import Question from '../src/modules/learn/Question.model.js'; + +const MONGO_URI = process.env.MONGO_URI || process.env.DB_URI || 'mongodb://localhost:27017/crackcode'; + +async function main() { + const userId = process.argv[2]; + if (!userId) { + console.error('Usage: node fixProgressMetadata.mjs '); + process.exit(1); + } + + await mongoose.connect(MONGO_URI); + + const userObjId = mongoose.Types.ObjectId(userId); + + console.log('Connected to MongoDB:', MONGO_URI); + console.log('Scanning UserQuestionProgress for user:', userId); + + const docs = await UserQuestionProgress.find({ + userId: userObjId, + $or: [ + { difficulty: { $in: [null, ''] } }, + { language: { $in: [null, ''] } }, + ], + }); + + console.log(`Found ${docs.length} progress docs missing difficulty or language`); + + let updated = 0; + for (const doc of docs) { + let question = null; + try { + const qId = doc.questionId; + if (qId && typeof qId === 'string' && /^[0-9a-fA-F]{24}$/.test(qId)) { + question = await Question.findById(qId).lean(); + } else { + question = await Question.findOne({ problemId: qId }).lean(); + } + } catch (err) { + // ignore + } + + if (!question) { + console.log(' • Question not found for progress:', doc._id.toString(), 'questionId:', doc.questionId); + continue; + } + + const set = {}; + if (!doc.difficulty && question.difficulty) set.difficulty = question.difficulty; + if (!doc.language && (question.language || question.languageName)) set.language = question.language || question.languageName; + + if (Object.keys(set).length === 0) { + console.log(' • No update available for progress:', doc._id.toString()); + continue; + } + + await UserQuestionProgress.updateOne({ _id: doc._id }, { $set: set }); + console.log(' • Updated progress', doc._id.toString(), 'set:', set); + updated += 1; + } + + console.log(`Completed. Documents updated: ${updated}`); + await mongoose.disconnect(); + process.exit(0); +} + +main().catch(err => { + console.error('Script error:', err); + process.exit(1); +}); diff --git a/crackcode/server/src/modules/codeEditor/codeEditor.controller.js b/crackcode/server/src/modules/codeEditor/codeEditor.controller.js index 38e4e525..e897cb47 100644 --- a/crackcode/server/src/modules/codeEditor/codeEditor.controller.js +++ b/crackcode/server/src/modules/codeEditor/codeEditor.controller.js @@ -114,7 +114,9 @@ export const executeTestCases = async (req, res) => { 'coding', sourceArea, passedCount, - testCases.length + testCases.length, + difficulty || null, + language || null ); console.log('✅ Question marked as solved'); diff --git a/crackcode/server/src/modules/session/session.controller.js b/crackcode/server/src/modules/session/session.controller.js index 466af20e..5fe03f25 100644 --- a/crackcode/server/src/modules/session/session.controller.js +++ b/crackcode/server/src/modules/session/session.controller.js @@ -18,14 +18,14 @@ const isProduction = () => process.env.NODE_ENV === "production"; export const accessCookieOptions = () => ({ httpOnly: true, secure: isProduction(), - sameSite: isProduction() ? "strict" : "lax", + sameSite: "lax", maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days }); export const refreshCookieOptions = () => ({ httpOnly: true, secure: isProduction(), - sameSite: isProduction() ? "strict" : "lax", + sameSite: "lax", maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days path: "/api/session/refresh", // only sent to the refresh endpoint }); diff --git a/crackcode/server/src/modules/user/controller.js b/crackcode/server/src/modules/user/controller.js index fb5f757c..173c612d 100644 --- a/crackcode/server/src/modules/user/controller.js +++ b/crackcode/server/src/modules/user/controller.js @@ -134,8 +134,10 @@ export const getProgressSummary = async (req, res) => { } }); - // If there are no progress documents, fall back to user's completedQuestions - if (totalSolved === 0 && Array.isArray(fullUser.completedQuestions) && fullUser.completedQuestions.length > 0) { + // If aggregation produced no or fewer progress documents than user's recorded completions, + // fall back to computing distribution from the embedded `completedQuestions` array. + const completedCount = Array.isArray(fullUser.completedQuestions) ? fullUser.completedQuestions.length : 0; + if ((totalSolved === 0 || totalSolved < completedCount) && completedCount > 0) { // helper maps const idMap = { 50: 'c', diff --git a/crackcode/server/src/modules/user/routes.js b/crackcode/server/src/modules/user/routes.js index b9c2af33..56a489a9 100644 --- a/crackcode/server/src/modules/user/routes.js +++ b/crackcode/server/src/modules/user/routes.js @@ -1,13 +1,13 @@ import express from "express"; -import userAuth from "../auth/middleware.js"; +import { sessionAuth } from "../session/session.middleware.js"; import { getUserData, getProgressSummary, getUserActivity, getUserProgressRaw, deleteAccount } from "./controller.js"; const router = express.Router(); -router.get("/data", userAuth, getUserData); -router.get("/progress-summary", userAuth, getProgressSummary); -router.get("/activity", userAuth, getUserActivity); -router.get("/progress-raw", userAuth, getUserProgressRaw); -router.post("/delete-account", userAuth, deleteAccount); +router.get("/data", sessionAuth, getUserData); +router.get("/progress-summary", sessionAuth, getProgressSummary); +router.get("/activity", sessionAuth, getUserActivity); +router.get("/progress-raw", sessionAuth, getUserProgressRaw); +router.post("/delete-account", sessionAuth, deleteAccount); export default router;