From 37be8ca2943afc23e41b3f96f15bd7c4eaa883cc Mon Sep 17 00:00:00 2001 From: Om Thanage Date: Wed, 9 Jul 2025 00:35:13 +0530 Subject: [PATCH 1/7] feat: add leaderboard --- notes-aid/package-lock.json | 26 + notes-aid/package.json | 1 + notes-aid/src/app/leaderboard/page.tsx | 10 + notes-aid/src/app/page.tsx | 15 + notes-aid/src/components/Leaderboard.tsx | 727 +++++++++++++++++++++++ notes-aid/src/components/lib/utils.ts | 6 + notes-aid/src/components/ui/Card.tsx | 44 ++ notes-aid/src/components/ui/Input.tsx | 23 + notes-aid/src/context/AuthContext.tsx | 493 +++++++++++++++ notes-aid/src/hook/useDebounce.tsx | 17 + 10 files changed, 1362 insertions(+) create mode 100644 notes-aid/src/app/leaderboard/page.tsx create mode 100644 notes-aid/src/components/Leaderboard.tsx create mode 100644 notes-aid/src/components/lib/utils.ts create mode 100644 notes-aid/src/components/ui/Card.tsx create mode 100644 notes-aid/src/components/ui/Input.tsx create mode 100644 notes-aid/src/context/AuthContext.tsx create mode 100644 notes-aid/src/hook/useDebounce.tsx diff --git a/notes-aid/package-lock.json b/notes-aid/package-lock.json index cb82b6e..fb6ad1e 100644 --- a/notes-aid/package-lock.json +++ b/notes-aid/package-lock.json @@ -20,6 +20,7 @@ "next-auth": "^4.24.11", "next-pwa": "^5.6.0", "next-themes": "^0.4.4", + "nookies": "^2.5.2", "react": "^19.0.0", "react-dom": "^19.0.0", "tailwind-merge": "^3.0.1" @@ -7453,6 +7454,25 @@ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==" }, + "node_modules/nookies": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/nookies/-/nookies-2.5.2.tgz", + "integrity": "sha512-x0TRSaosAEonNKyCrShoUaJ5rrT5KHRNZ5DwPCuizjgrnkpE5DRf3VL7AyyQin4htict92X1EQ7ejDbaHDVdYA==", + "license": "MIT", + "dependencies": { + "cookie": "^0.4.1", + "set-cookie-parser": "^2.4.6" + } + }, + "node_modules/nookies/node_modules/cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/normalize-range": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", @@ -8466,6 +8486,12 @@ "randombytes": "^2.1.0" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "license": "MIT" + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", diff --git a/notes-aid/package.json b/notes-aid/package.json index 121c606..93fc6f3 100644 --- a/notes-aid/package.json +++ b/notes-aid/package.json @@ -21,6 +21,7 @@ "next-auth": "^4.24.11", "next-pwa": "^5.6.0", "next-themes": "^0.4.4", + "nookies": "^2.5.2", "react": "^19.0.0", "react-dom": "^19.0.0", "tailwind-merge": "^3.0.1" diff --git a/notes-aid/src/app/leaderboard/page.tsx b/notes-aid/src/app/leaderboard/page.tsx new file mode 100644 index 0000000..c9c2ac1 --- /dev/null +++ b/notes-aid/src/app/leaderboard/page.tsx @@ -0,0 +1,10 @@ +import Leaderboard from "@/components/Leaderboard"; +import { AuthProvider } from "@/context/AuthContext"; + +export default function LeaderboardPage() { + return ( + + + + ) +} diff --git a/notes-aid/src/app/page.tsx b/notes-aid/src/app/page.tsx index 1b407f7..85af8da 100644 --- a/notes-aid/src/app/page.tsx +++ b/notes-aid/src/app/page.tsx @@ -9,6 +9,7 @@ import { Calendar, NotebookText, RotateCcw, + Trophy, } from "lucide-react"; const branches = [ @@ -86,6 +87,10 @@ export default function MainPage() { } }; + const handleLeaderboard = () => { + router.push("/leaderboard"); + }; + const handleReturnToPrevious = () => { const previousBranch = localStorage.getItem("selectedBranch"); const previousYear = localStorage.getItem("selectedYear"); @@ -208,6 +213,16 @@ export default function MainPage() { Continue to Modules + + + +
{hasPreviousSelection && ( +
+ + {/* Modal Content */} +
+ {/* Overall Stats */} +
+ + +
+
+ {selectedStudent.cgpa ? selectedStudent.cgpa.toFixed(2) : 'N/A'} +
+
Overall CGPA
+
+
+
+ + +
+
#{selectedStudent.rank}
+
Current Rank
+
+
+
+
+ + {/* Semester-wise SGPA */} +
+

+ + Semester-wise SGPA +

+ + {selectedStudent.sgpa_list && selectedStudent.sgpa_list.length > 0 ? ( +
{selectedStudent.sgpa_list + .sort((a, b) => a.semester - b.semester) + .map((item) => ( +
+
+ Semester {item.semester} +
+
+
+
{item.sgpa.toFixed(2)}
+
SGPA
+
+
+
+ ))} +
+ ) : ( +
+
No semester data available
+
+ )} +
+ + {/* Summary Stats */} + {selectedStudent.sgpa_list && selectedStudent.sgpa_list.length > 0 && ( +
+

Performance Summary

+
+
+
+ {Math.max(...selectedStudent.sgpa_list.map(s => s.sgpa)).toFixed(2)} +
+
Highest SGPA
+
+
+
+ {(selectedStudent.sgpa_list.reduce((sum, s) => sum + s.sgpa, 0) / selectedStudent.sgpa_list.length).toFixed(2)} +
+
Average SGPA
+
+
+
+ {selectedStudent.totalSemesters} +
+
Total Semesters
+
+
+
+ )} +
+ + + ); }; + + // Show minimal static content during hydration + if (!isHydrated) { + return ( +
+
+ {/* Header */} +
+

Leaderboard

+

Track academic excellence across batches and semesters

+
+
+
+
+

Loading leaderboard...

+
+
+
+
+ ); + } + + return ( +
+
+ {/* Header */} +
+

Leaderboard

+

Track academic excellence across batches and semesters

+
{/* Search Bar */} +
+ + setSearchTerm(e.target.value)} + className="pl-12 h-12 w-full text-base-content dark:text-neutral-content" + /> + {searchTerm && ( +
+ +
+ )} + + {/* Search Results Counter */} + {searchTerm && ( +
+

+ Found {filteredStudents.length} student{filteredStudents.length !== 1 ? 's' : ''} + {searchTerm && ` matching "${searchTerm}"`} +

+
+ )} +
+ + {/* Filters */} +
+ {/* Year Selection */} +
+ + + +
+ + {/* Semester Selection */} +
+ + +
+ {/* Refresh Button */} +
+ +
+
+ + {/* Loading State */} + {loading && ( +
+
+
+

Loading leaderboard...

+
+
+ )} + {/* Error State */} + {error && ( +
+
+

{error}

+ +
+
+ )} + {/* Students List */} + {!loading && !error && filteredStudents.length > 0 && ( + <> + {/* Desktop Table View */} +
+ {/* Table Header */} +
+
+
Rank
+
Student
+
Roll Number
+
Year
+
Semesters
+
{getScoreLabel()}
+
+
+ + {/* Table Body */} +
+ {filteredStudents.map((student) => ( +
showStudentDetails(student)} + > +
+ {/* Rank */} +
+
+ {student.rank <= 3 ? ( +
+ {getRankIcon(student.rank)} +
+ ) : ( +
+ #{student.rank} +
+ )} +
+
+ + {/* Student Name */} +
+
+
+

+ {student.name} +

+ {student.rank <= 3 && ( +
+ + Top Performer +
+ )} +
+
+
+ + {/* Roll Number */} +
+ {student.seat_number} +
+ + {/* Year */} +
+ {student.admission_year} +
+ + {/* Semesters */} +
+ {student.totalSemesters} +
+ + {/* Score */} +
+
+ + {getScoreValue(student)} + + +
+
+
+ + {/* Hover indicator */} +
+

Click to view detailed SGPA breakdown

+
+
+ ))} +
+
+ + {/* Mobile/Tablet Card View */} +
+ {filteredStudents.map((student) => ( +
showStudentDetails(student)} + > +
+
+
+ {student.rank <= 3 ? ( +
+ {getRankIcon(student.rank)} +
+ ) : ( +
+ #{student.rank} +
+ )} +
+
+

{student.name}

+ {student.rank <= 3 && ( +
+ + Top Performer +
+ )} +
+
+ +
+ +
+
+ Roll Number +

{student.seat_number}

+
+
+ Year +

{student.admission_year}

+
+
+ Semesters +

{student.totalSemesters}

+
+
+ {getScoreLabel()} +

{getScoreValue(student)}

+
+
+ +
+

Tap to view detailed SGPA breakdown

+
+
+ ))} +
+ + {/* Pagination */} + {totalPages > 1 && ( +
+
+ Page {currentPage} of {totalPages} ({totalStudents} total students) +
+
+ + +
+
+ )} + + )} {/* Empty State */} + {!loading && !error && filteredStudents.length === 0 && ( + + +
+ +

+ {searchTerm ? 'No search results found' : 'No students found'} +

+

+ {searchTerm + ? `No students match "${searchTerm}". Try a different search term or check your spelling.` + : 'No data available for the selected criteria. Try selecting a different year or semester.' + } +

+ {searchTerm && ( + + )} +
+
+
)} +
+ + {/* SGPA Detail Modal */} + +
+ ); +} diff --git a/notes-aid/src/components/lib/utils.ts b/notes-aid/src/components/lib/utils.ts new file mode 100644 index 0000000..e6a8be0 --- /dev/null +++ b/notes-aid/src/components/lib/utils.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from "clsx"; +import { twMerge } from "tailwind-merge"; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} diff --git a/notes-aid/src/components/ui/Card.tsx b/notes-aid/src/components/ui/Card.tsx new file mode 100644 index 0000000..3ccd4c6 --- /dev/null +++ b/notes-aid/src/components/ui/Card.tsx @@ -0,0 +1,44 @@ +import { cn } from "../lib/utils"; + +export function Card({ className, ...props }: React.HTMLAttributes) { + return ( +
+ ); +} + +export function CardHeader({ className, ...props }: React.HTMLAttributes) { + return ( +
+ ); +} + +export function CardTitle({ className, ...props }: React.HTMLAttributes) { + return ( +

+ ); +} + +export function CardDescription({ className, ...props }: React.HTMLAttributes) { + return ( +

+ ); +} + +export function CardContent({ className, ...props }: React.HTMLAttributes) { + return

; +} diff --git a/notes-aid/src/components/ui/Input.tsx b/notes-aid/src/components/ui/Input.tsx new file mode 100644 index 0000000..b25562c --- /dev/null +++ b/notes-aid/src/components/ui/Input.tsx @@ -0,0 +1,23 @@ +import { forwardRef } from "react"; +import { cn } from "../lib/utils"; + +export type InputProps = React.InputHTMLAttributes; + +const Input = forwardRef( + ({ className, type, ...props }, ref) => { + return ( + + ); + } +); +Input.displayName = "Input"; + +export { Input }; diff --git a/notes-aid/src/context/AuthContext.tsx b/notes-aid/src/context/AuthContext.tsx new file mode 100644 index 0000000..3a622ba --- /dev/null +++ b/notes-aid/src/context/AuthContext.tsx @@ -0,0 +1,493 @@ +"use client"; + +import { createContext, useContext, useState, useEffect, useCallback, ReactNode } from 'react'; +import { useRouter } from 'next/navigation'; +import { parseCookies, setCookie, destroyCookie } from 'nookies'; + +// Define types +interface User { + id: string; + name: string; + email: string; + rollNo?: string; + year?: number | string; + branch?: string; + phone?: string; + isProfileComplete?: boolean; +} + +// Define a type for SGPA entries +interface SGPAEntry { + _id: string; + semester: number; + sgpa: number; +} + +// Add dashboard-related interfaces +interface UserStats { + cgpa?: number; + totalSemesters?: number; + sgpaList?: SGPAEntry[]; // Added sgpaList to UserStats + rank: number; + totalStudents: number; + admissionYear?: number; + currentYear?: number; + isProfileComplete?: boolean; + documentsUploaded: number; +} + +interface DocumentItem { + _id: string; + subject: string; + module: string; + faculty?: string; + year?: number; + uploadedBy: { + name: string; + }; + url: string; + cloudinaryId?: string; + fileName?: string; + fileSize?: number; + description?: string; + createdAt: string; +} + +interface PodcastItem { + _id: string; + title: string; + topic: string; + uploadedBy: { + name: string; + }; + createdAt: string; + duration?: number; +} + +interface DashboardData { + stats: { + totalDocuments: number; + totalPodcasts: number; + documentsUploaded: number; + rank: number; + totalUsers: number; + }; + userStats: UserStats; + recentDocuments: DocumentItem[]; + recentPodcasts: PodcastItem[]; + isLoading: boolean; + error: string | null; +} + +interface AuthContextType { + user: User | null; + token: string | null; + loading: boolean; + error: string | null; + login: (userData: User, token: string) => void; + logout: () => void; + updateUser: (userData: Partial) => void; + updateProfile: (userData: Partial) => void; + dashboardData: DashboardData | null; + refreshDashboardData: () => Promise; +} + +// Create context +const AuthContext = createContext(undefined); + +// Utility function to construct API URLs properly +const getApiUrl = (endpoint: string) => { + const baseUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:5000'; + const cleanBase = baseUrl.replace(/\/+$/, ''); // Remove trailing slashes + const cleanEndpoint = endpoint.replace(/^\/+/, ''); // Remove leading slashes + return `${cleanBase}/${cleanEndpoint}`; +}; + +// Cookie options with httpOnly and secure flags for production +const COOKIE_OPTIONS = { + maxAge: 30 * 24 * 60 * 60, // 30 days + path: '/', + secure: process.env.NODE_ENV === 'production', + sameSite: 'lax' as const, +}; + + +export function AuthProvider({ children }: { children: ReactNode }) { + const [user, setUser] = useState(null); + const [token, setToken] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [mounted, setMounted] = useState(false); + const [justLoggedIn, setJustLoggedIn] = useState(false); // Track fresh login + const router = useRouter(); + + // Dashboard state + const [dashboardData, setDashboardData] = useState(null); + + // Handle mounting to prevent hydration issues + useEffect(() => { + setMounted(true); + }, []); + + useEffect(() => { + if (!mounted) return; // Don't run auth check until mounted + + const checkAuth = async () => { + try { + const cookies = parseCookies(); + // Try new cookie names first, then fall back to legacy names + const authToken = cookies.quadratoken || cookies.authToken; + const userCookie = cookies.user || cookies.userData; + if (authToken && userCookie) { + const userData = JSON.parse(userCookie); + setUser(userData); + setToken(authToken); + // Skip token validation if we just logged in (to avoid CORS issues during callback) + if (justLoggedIn) { + setJustLoggedIn(false); + setLoading(false); + return; + } + + try { const response = await fetch(getApiUrl('api/v1/auth/status'), { + headers: { Authorization: `Bearer ${authToken}` }, + credentials: 'include' // Include cookies in requests + }); + + if (!response.ok) { + throw new Error('Invalid token'); + } const data = await response.json(); + if (data.user) { + // After token validation, fetch complete user profile + try { + const profileResponse = await fetch(getApiUrl(`api/v1/user/${data.user.id}`), { + headers: { Authorization: `Bearer ${authToken}` }, + credentials: 'include' + }); + + if (profileResponse.ok) { + const profileData = await profileResponse.json(); + if (profileData.success && profileData.user) { + // Use complete profile data instead of incomplete auth status data + setUser(profileData.user); + setCookie(null, 'user', JSON.stringify(profileData.user), COOKIE_OPTIONS); + } else { + // Fallback to merging with existing data if profile fetch fails + setUser(prevUser => { + const mergedUser = { ...prevUser, ...data.user }; + setCookie(null, 'user', JSON.stringify(mergedUser), COOKIE_OPTIONS); + return mergedUser; + }); + } + } else { + // Fallback to merging with existing data if profile fetch fails + setUser(prevUser => { + const mergedUser = { ...prevUser, ...data.user }; + setCookie(null, 'user', JSON.stringify(mergedUser), COOKIE_OPTIONS); + return mergedUser; + }); + } + } catch (profileErr) { + console.error('Failed to fetch complete profile:', profileErr); + // Fallback to merging with existing data + setUser(prevUser => { + const mergedUser = { ...prevUser, ...data.user }; + setCookie(null, 'user', JSON.stringify(mergedUser), COOKIE_OPTIONS); + return mergedUser; + }); + } + + // Clean up legacy cookie names + if (cookies.userData) { + destroyCookie(null, 'userData', { path: '/' }); + } + if (cookies.authToken) { + destroyCookie(null, 'authToken', { path: '/' }); + setCookie(null, 'quadratoken', authToken, COOKIE_OPTIONS); + } + + // Fetch dashboard data after successful authentication + if (data.user.isProfileComplete !== false) { + fetchDashboardData(data.user.id, authToken); + } + } + } catch (err) { + console.error('Token validation failed:', err); + logout(); + } + } + } catch (err) { + console.error('Auth restore error:', err); + logout(); + setError('Authentication session expired. Please login again.'); + } finally { + setLoading(false); + } + }; checkAuth(); + }, [mounted, justLoggedIn]); + + const fetchDashboardData = async (userId: string, authToken: string) => { + try { + setDashboardData(prev => prev ? { ...prev, isLoading: true, error: null } : { + stats: { + totalDocuments: 0, + totalPodcasts: 0, + documentsUploaded: 0, + rank: 0, + totalUsers: 0 + }, + userStats: { + rank: 0, + totalStudents: 0, + documentsUploaded: 0 + }, + recentDocuments: [], + recentPodcasts: [], + isLoading: true, + error: null + }); // Fetch real data from backend endpoints + const [userStatsRes, documentsRes, podcastsRes] = await Promise.allSettled([ + fetch(getApiUrl(`api/v1/user/${userId}/stats`), { + headers: { Authorization: `Bearer ${authToken}` } + }), + fetch(getApiUrl('api/v1/documents'), { + headers: { Authorization: `Bearer ${authToken}` } + }), + fetch(getApiUrl('api/v1/podcasts'), { + headers: { Authorization: `Bearer ${authToken}` } + }) + ]); + + let userStats: UserStats = { + rank: 0, + totalStudents: 0, + documentsUploaded: 0 + }; + + if (userStatsRes.status === 'fulfilled' && userStatsRes.value.ok) { + const userStatsData = await userStatsRes.value.json(); + + if (userStatsData.success) { + const stats = userStatsData.stats; + userStats = { + cgpa: stats.cgpa, + totalSemesters: stats.totalSemesters, + sgpaList: stats.sgpaList, // Include sgpaList in userStats + rank: stats.rank, + totalStudents: stats.totalStudentsInYear, + admissionYear: stats.admissionYear, + currentYear: stats.year, + isProfileComplete: stats.isProfileComplete, + documentsUploaded: stats.documentsUploaded || 0, + }; + } + } // Now fetch leaderboard with the correct admission year + let leaderboardRes = null; + if (userStats.admissionYear) { + try { + leaderboardRes = await fetch( + getApiUrl(`api/v1/leaderboard?admission_year=${userStats.admissionYear}&limit=100`), + { + headers: { Authorization: `Bearer ${authToken}` } + } + ); + } catch (error) { + console.error('Error fetching leaderboard:', error); + } + } + + // Process documents + let documentsData: DocumentItem[] = []; + let totalDocuments = 0; + if (documentsRes.status === 'fulfilled' && documentsRes.value.ok) { + const docsResponse = await documentsRes.value.json(); + if (docsResponse.success && docsResponse.data) { + documentsData = docsResponse.data.slice(0, 5); // Recent documents + totalDocuments = docsResponse.totalDocuments; + } + } + + // Process podcasts + let podcastsData: PodcastItem[] = []; + let totalPodcasts = 0; + if (podcastsRes.status === 'fulfilled' && podcastsRes.value.ok) { + const podcastsResponse = await podcastsRes.value.json(); + if (podcastsResponse.success && podcastsResponse.data) { + podcastsData = podcastsResponse.data.slice(0, 3); // Recent podcasts + totalPodcasts = podcastsResponse.totalPodcasts; + } + } // Process leaderboard for user rank + let userRank = userStats.rank || 0; + let totalUsers = userStats.totalStudents || 0; + + if (leaderboardRes && leaderboardRes.ok) { + try { + const leaderboardResponse = await leaderboardRes.json(); + + if (leaderboardResponse.success && leaderboardResponse.data && Array.isArray(leaderboardResponse.data)) { + const leaderboard = leaderboardResponse.data; + totalUsers = leaderboard.length; + + // Find user by rollNo/seat_number instead of user._id + const userPosition = leaderboard.findIndex((entry: any) => + entry.seat_number === user?.rollNo + ); + + if (userPosition >= 0) { + userRank = userPosition + 1; + } else { + // If not found in leaderboard, use the rank from userStats + userRank = userStats.rank || 0; + } + } + } catch (error) { + console.error('Error processing leaderboard response:', error); + // Fallback to userStats rank if leaderboard processing fails + userRank = userStats.rank || 0; + } + } setDashboardData({ + stats: { + totalDocuments, + totalPodcasts, + documentsUploaded: userStats.documentsUploaded || 0, + rank: userRank, + totalUsers + }, + userStats, + recentDocuments: documentsData, + recentPodcasts: podcastsData, + isLoading: false, + error: null + }); + + } catch (error) { + console.error('Error fetching dashboard data:', error); + setDashboardData(prev => prev ? { + ...prev, + isLoading: false, + error: 'Failed to load dashboard data' + } : { + stats: { + totalDocuments: 0, + totalPodcasts: 0, + documentsUploaded: 0, + rank: 0, + totalUsers: 0 + }, + userStats: { + rank: 0, + totalStudents: 0, + documentsUploaded: 0 + }, + recentDocuments: [], + recentPodcasts: [], + isLoading: false, + error: 'Failed to load dashboard data' + }); + } + }; + // Function to refresh dashboard data + const refreshDashboardData = useCallback(async () => { + if (user && token) { + await fetchDashboardData(user.id, token); + } + }, [user, token]); const login = (userData: User, authToken: string) => { + setUser(userData); + setToken(authToken); + setLoading(false); // Set loading to false immediately + setJustLoggedIn(true); // Mark as fresh login to skip validation + + // Use new cookie names for better security + setCookie(null, 'quadratoken', authToken, COOKIE_OPTIONS); + setCookie(null, 'user', JSON.stringify(userData), COOKIE_OPTIONS); + + // Clean up any legacy cookies + destroyCookie(null, 'authToken', { path: '/' }); + destroyCookie(null, 'userData', { path: '/' }); + + // Navigate to appropriate page after login + setTimeout(() => { + if (userData.isProfileComplete === false) { + router.push('/onboarding'); + } else { + router.push('/dashboard'); + } + }, 100); + + // Fetch dashboard data upon login if profile is complete + if (userData.isProfileComplete !== false) { + fetchDashboardData(userData.id, authToken); + } + }; + + const logout = async () => { + try { if (token) { + await fetch(getApiUrl('api/v1/auth/logout'), { + method: 'POST', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + }, + credentials: 'include' // Include cookies in logout request + }); + } + } catch (error) { + console.error('Logout error:', error); + } finally { + // Clear all possible cookie names (new and legacy) + destroyCookie(null, 'quadratoken', { path: '/' }); + destroyCookie(null, 'user', { path: '/' }); + destroyCookie(null, 'authToken', { path: '/' }); + destroyCookie(null, 'userData', { path: '/' }); + setUser(null); + setToken(null); + setDashboardData(null); + router.push('/'); + } + }; + const updateUser = (userData: Partial) => { + setUser(prevUser => { + if (!prevUser) return null; + const updatedUser = {...prevUser, ...userData}; + setCookie(null, 'user', JSON.stringify(updatedUser), COOKIE_OPTIONS); + + // Refresh dashboard data if profile is now complete + if (userData.isProfileComplete === true && token) { + fetchDashboardData(updatedUser.id, token); + } + + return updatedUser; + }); + }; + + // Alias for updateUser to maintain consistent API + const updateProfile = (userData: Partial) => { + updateUser(userData); + }; + const value = { + user, + token, + loading, + error, + login, + logout, + updateUser, + updateProfile, + dashboardData, + refreshDashboardData + }; + + return ( + + {children} + + ); +} + +export const useAuth = () => { + const context = useContext(AuthContext); + if (context === undefined) { + throw new Error('useAuth must be used within an AuthProvider'); + } + return context; +}; \ No newline at end of file diff --git a/notes-aid/src/hook/useDebounce.tsx b/notes-aid/src/hook/useDebounce.tsx new file mode 100644 index 0000000..e3706e6 --- /dev/null +++ b/notes-aid/src/hook/useDebounce.tsx @@ -0,0 +1,17 @@ +import { useState, useEffect } from 'react'; + +export function useDebounce(value: T, delay: number): T { + const [debouncedValue, setDebouncedValue] = useState(value); + + useEffect(() => { + const handler = setTimeout(() => { + setDebouncedValue(value); + }, delay); + + return () => { + clearTimeout(handler); + }; + }, [value, delay]); + + return debouncedValue; +} From 9c2866546f540e4904b6995b94f6681f7f365440 Mon Sep 17 00:00:00 2001 From: Om Thanage Date: Sun, 20 Jul 2025 15:17:44 +0530 Subject: [PATCH 2/7] feat: Added leaderboard with backend --- .gitignore | 3 - leaderboard_backend/.gitignore | 2 + leaderboard_backend/config/db.js | 10 + .../controllers/leaderboard.js | 898 ++++++++ leaderboard_backend/middlewares/auth.js | 63 + leaderboard_backend/models/studentMarks.js | 29 + leaderboard_backend/package-lock.json | 1937 +++++++++++++++++ leaderboard_backend/package.json | 26 + leaderboard_backend/routes/leaderboard.js | 29 + leaderboard_backend/server.js | 37 + notes-aid/package-lock.json | 31 + notes-aid/package.json | 3 + notes-aid/src/app/leaderboard/page.tsx | 100 +- notes-aid/src/components/Leaderboard.tsx | 88 +- notes-aid/src/context/AuthContext.tsx | 493 ----- 15 files changed, 3219 insertions(+), 530 deletions(-) delete mode 100644 .gitignore create mode 100644 leaderboard_backend/.gitignore create mode 100644 leaderboard_backend/config/db.js create mode 100644 leaderboard_backend/controllers/leaderboard.js create mode 100644 leaderboard_backend/middlewares/auth.js create mode 100644 leaderboard_backend/models/studentMarks.js create mode 100644 leaderboard_backend/package-lock.json create mode 100644 leaderboard_backend/package.json create mode 100644 leaderboard_backend/routes/leaderboard.js create mode 100644 leaderboard_backend/server.js delete mode 100644 notes-aid/src/context/AuthContext.tsx diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 0654ead..0000000 --- a/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -package-lock.json -node_modules -package.json \ No newline at end of file diff --git a/leaderboard_backend/.gitignore b/leaderboard_backend/.gitignore new file mode 100644 index 0000000..97aca2e --- /dev/null +++ b/leaderboard_backend/.gitignore @@ -0,0 +1,2 @@ +.env +node_modules \ No newline at end of file diff --git a/leaderboard_backend/config/db.js b/leaderboard_backend/config/db.js new file mode 100644 index 0000000..1a9a56d --- /dev/null +++ b/leaderboard_backend/config/db.js @@ -0,0 +1,10 @@ +import mongoose from 'mongoose'; + +function connectToDb() { + mongoose.connect(process.env.MONGODB_URI + ).then(() => { + console.log('MongoDB connected successfully'); + }).catch(err => console.log(err)); +} + +export default connectToDb; \ No newline at end of file diff --git a/leaderboard_backend/controllers/leaderboard.js b/leaderboard_backend/controllers/leaderboard.js new file mode 100644 index 0000000..f72bf31 --- /dev/null +++ b/leaderboard_backend/controllers/leaderboard.js @@ -0,0 +1,898 @@ +import StudentMarks from "../models/studentMarks.js"; + +/** + * Helper function to assign ranks with proper tie handling (Dense Ranking: 1,2,2,3,4,4,5) + * @param {Array} students - Array of students sorted by score + * @param {string} scoreField - Field name to compare for ties (e.g., 'cgpa', 'semesterSGPA') + * @param {number} startRank - Starting rank (for pagination) + * @returns {Array} Students with proper rank assignment + */ +const assignRanksWithTies = (students, scoreField, startRank = 1) => { + if (students.length === 0) return students; + + let currentRank = startRank; + + return students.map((student, index) => { + if (index > 0) { + // Check if current student has same score as previous student + const currentScore = student[scoreField]; + const previousScore = students[index - 1][scoreField]; + + if (currentScore !== previousScore) { + // Different score, increment rank + currentRank++; + } + // If same score, keep the same rank + } + + return { + ...student, + rank: currentRank, + }; + }); +}; + +/** + * @desc Get overall CGPA leaderboard for a specific admission year + * @route GET /api/v1/leaderboard?admission_year=2021&limit=50 + * @access Private + */ +export const getOverallLeaderboard = async (req, res) => { + try { + const { admission_year, limit = 50, page = 1 } = req.query; + + if (!admission_year) { + return res.status(400).json({ + success: false, + message: "Admission year is required", + }); + } + + const skip = (page - 1) * limit; + + // Aggregate pipeline to calculate CGPA and rank students + const pipeline = [ + { + $match: { + admission_year: parseInt(admission_year), + }, + }, + { + $addFields: { + // Calculate CGPA (average of all SGPA values) + cgpa: { + $cond: { + if: { $gt: [{ $size: "$sgpa_list" }, 0] }, + then: { + $round: [{ $avg: "$sgpa_list.sgpa" }, 2], + }, + else: 0, + }, + }, + // Get total semesters completed + totalSemesters: { $size: "$sgpa_list" }, + // Get latest semester SGPA + latestSGPA: { + $cond: { + if: { $gt: [{ $size: "$sgpa_list" }, 0] }, + then: { + $let: { + vars: { + lastSemester: { + $arrayElemAt: [ + { + $sortArray: { + input: "$sgpa_list", + sortBy: { semester: -1 }, + }, + }, + 0, + ], + }, + }, + in: "$$lastSemester.sgpa", + }, + }, + else: 0, + }, + }, + // Get latest semester number + latestSemester: { + $cond: { + if: { $gt: [{ $size: "$sgpa_list" }, 0] }, + then: { $max: "$sgpa_list.semester" }, + else: 0, + }, + }, + }, + }, + { + $match: { + cgpa: { $gt: 0 }, // Only include students with CGPA > 0 + }, + }, + { + $sort: { + cgpa: -1, // Sort by CGPA in descending order + latestSGPA: -1, // Then by latest SGPA + name: 1, // Then alphabetically by name + }, + }, + { + $skip: skip, + }, + { + $limit: parseInt(limit), + }, + { + $project: { + seat_number: 1, + name: 1, + admission_year: 1, + cgpa: 1, + totalSemesters: 1, + latestSGPA: 1, + latestSemester: 1, + sgpa_list: 1, + }, + }, + ]; + const students = await StudentMarks.aggregate(pipeline); + + // For proper tie handling, we need to get all students with their ranks + // and then apply pagination to the ranked results + if (page === 1) { + // For first page, we can directly assign ranks + const studentsWithRank = assignRanksWithTies(students, "cgpa", 1); + + // Get total count for pagination + const totalCountPipeline = [ + { + $match: { + admission_year: parseInt(admission_year), + }, + }, + { + $addFields: { + cgpa: { + $cond: { + if: { $gt: [{ $size: "$sgpa_list" }, 0] }, + then: { $avg: "$sgpa_list.sgpa" }, + else: 0, + }, + }, + }, + }, + { + $match: { + cgpa: { $gt: 0 }, + }, + }, + { + $count: "total", + }, + ]; + + const totalCountResult = await StudentMarks.aggregate(totalCountPipeline); + const total = totalCountResult.length > 0 ? totalCountResult[0].total : 0; + + res.status(200).json({ + success: true, + data: studentsWithRank, + pagination: { + page: parseInt(page), + limit: parseInt(limit), + total, + pages: Math.ceil(total / limit), + }, + meta: { + admission_year: parseInt(admission_year), + type: "overall_cgpa", + }, + }); + } else { + // For subsequent pages, we need to get all students to calculate proper ranks + const allStudentsPipeline = [ + { + $match: { + admission_year: parseInt(admission_year), + }, + }, + { + $addFields: { + cgpa: { + $cond: { + if: { $gt: [{ $size: "$sgpa_list" }, 0] }, + then: { + $round: [{ $avg: "$sgpa_list.sgpa" }, 2], + }, + else: 0, + }, + }, + totalSemesters: { $size: "$sgpa_list" }, + latestSGPA: { + $cond: { + if: { $gt: [{ $size: "$sgpa_list" }, 0] }, + then: { + $let: { + vars: { + lastSemester: { + $arrayElemAt: [ + { + $sortArray: { + input: "$sgpa_list", + sortBy: { semester: -1 }, + }, + }, + 0, + ], + }, + }, + in: "$$lastSemester.sgpa", + }, + }, + else: 0, + }, + }, + latestSemester: { + $cond: { + if: { $gt: [{ $size: "$sgpa_list" }, 0] }, + then: { $max: "$sgpa_list.semester" }, + else: 0, + }, + }, + }, + }, + { + $match: { + cgpa: { $gt: 0 }, + }, + }, + { + $sort: { + cgpa: -1, + latestSGPA: -1, + name: 1, + }, + }, + { + $project: { + seat_number: 1, + name: 1, + admission_year: 1, + cgpa: 1, + totalSemesters: 1, + latestSGPA: 1, + latestSemester: 1, + sgpa_list: 1, + }, + }, + ]; + + const allStudents = await StudentMarks.aggregate(allStudentsPipeline); + const allStudentsWithRank = assignRanksWithTies(allStudents, "cgpa", 1); + + // Apply pagination to ranked results + const startIndex = skip; + const endIndex = startIndex + parseInt(limit); + const paginatedStudents = allStudentsWithRank.slice(startIndex, endIndex); + + res.status(200).json({ + success: true, + data: paginatedStudents, + pagination: { + page: parseInt(page), + limit: parseInt(limit), + total: allStudents.length, + pages: Math.ceil(allStudents.length / limit), + }, + meta: { + admission_year: parseInt(admission_year), + type: "overall_cgpa", + }, + }); + } + } catch (error) { + res.status(500).json({ + success: false, + message: "Error fetching leaderboard", + error: error.message, + }); + } +}; + +/** + * @desc Get semester-wise leaderboard for a specific admission year and semester + * @route GET /api/v1/leaderboard/:semester?admission_year=2021&limit=50 + * @access Private + */ +export const getSemesterLeaderboard = async (req, res) => { + try { + const { semester } = req.params; + const { admission_year, limit = 50, page = 1 } = req.query; + + if (!admission_year) { + return res.status(400).json({ + success: false, + message: "Admission year is required", + }); + } + + if (!semester || isNaN(semester)) { + return res.status(400).json({ + success: false, + message: "Valid semester number is required", + }); + } + + const skip = (page - 1) * limit; + + // Aggregate pipeline to get semester-specific SGPA and rank students + const pipeline = [ + { + $match: { + admission_year: parseInt(admission_year), + "sgpa_list.semester": parseInt(semester), + }, + }, + { + $addFields: { + // Extract SGPA for the specific semester + semesterSGPA: { + $let: { + vars: { + semesterData: { + $arrayElemAt: [ + { + $filter: { + input: "$sgpa_list", + cond: { $eq: ["$$this.semester", parseInt(semester)] }, + }, + }, + 0, + ], + }, + }, + in: "$$semesterData.sgpa", + }, + }, + // Calculate overall CGPA for reference + cgpa: { + $cond: { + if: { $gt: [{ $size: "$sgpa_list" }, 0] }, + then: { + $round: [{ $avg: "$sgpa_list.sgpa" }, 2], + }, + else: 0, + }, + }, + totalSemesters: { $size: "$sgpa_list" }, + }, + }, + { + $match: { + semesterSGPA: { $exists: true, $ne: null }, + }, + }, + { + $sort: { + semesterSGPA: -1, // Sort by semester SGPA in descending order + cgpa: -1, // Then by overall CGPA + name: 1, // Then alphabetically by name + }, + }, + { + $skip: skip, + }, + { + $limit: parseInt(limit), + }, + { + $project: { + seat_number: 1, + name: 1, + admission_year: 1, + semesterSGPA: 1, + cgpa: 1, + totalSemesters: 1, + sgpa_list: 1, + }, + }, + ]; + const students = await StudentMarks.aggregate(pipeline); + + // For proper tie handling, we need to get all students with their ranks + // and then apply pagination to the ranked results + if (page === 1) { + // For first page, we can directly assign ranks + const studentsWithRank = assignRanksWithTies(students, "semesterSGPA", 1); + + // Get total count for pagination + const totalCountPipeline = [ + { + $match: { + admission_year: parseInt(admission_year), + "sgpa_list.semester": parseInt(semester), + }, + }, + { + $count: "total", + }, + ]; + + const totalCountResult = await StudentMarks.aggregate(totalCountPipeline); + const total = totalCountResult.length > 0 ? totalCountResult[0].total : 0; + + res.status(200).json({ + success: true, + data: studentsWithRank, + pagination: { + page: parseInt(page), + limit: parseInt(limit), + total, + pages: Math.ceil(total / limit), + }, + meta: { + admission_year: parseInt(admission_year), + semester: parseInt(semester), + type: "semester_sgpa", + }, + }); + } else { + // For subsequent pages, we need to get all students to calculate proper ranks + const allStudentsPipeline = [ + { + $match: { + admission_year: parseInt(admission_year), + "sgpa_list.semester": parseInt(semester), + }, + }, + { + $addFields: { + semesterSGPA: { + $let: { + vars: { + semesterData: { + $arrayElemAt: [ + { + $filter: { + input: "$sgpa_list", + cond: { + $eq: ["$$this.semester", parseInt(semester)], + }, + }, + }, + 0, + ], + }, + }, + in: "$$semesterData.sgpa", + }, + }, + cgpa: { + $cond: { + if: { $gt: [{ $size: "$sgpa_list" }, 0] }, + then: { + $round: [{ $avg: "$sgpa_list.sgpa" }, 2], + }, + else: 0, + }, + }, + totalSemesters: { $size: "$sgpa_list" }, + }, + }, + { + $match: { + semesterSGPA: { $exists: true, $ne: null }, + }, + }, + { + $sort: { + semesterSGPA: -1, + cgpa: -1, + name: 1, + }, + }, + { + $project: { + seat_number: 1, + name: 1, + admission_year: 1, + semesterSGPA: 1, + cgpa: 1, + totalSemesters: 1, + sgpa_list: 1, + }, + }, + ]; + + const allStudents = await StudentMarks.aggregate(allStudentsPipeline); + const allStudentsWithRank = assignRanksWithTies( + allStudents, + "semesterSGPA", + 1 + ); + + // Apply pagination to ranked results + const startIndex = skip; + const endIndex = startIndex + parseInt(limit); + const paginatedStudents = allStudentsWithRank.slice(startIndex, endIndex); + + res.status(200).json({ + success: true, + data: paginatedStudents, + pagination: { + page: parseInt(page), + limit: parseInt(limit), + total: allStudents.length, + pages: Math.ceil(allStudents.length / limit), + }, + meta: { + admission_year: parseInt(admission_year), + semester: parseInt(semester), + type: "semester_sgpa", + }, + }); + } + } catch (error) { + res.status(500).json({ + success: false, + message: "Error fetching semester leaderboard", + error: error.message, + }); + } +}; + +/** + * @desc Get current semester leaderboard for a specific admission year + * @route GET /api/v1/leaderboard/current?admission_year=2021&limit=50 + * @access Private + */ +export const getCurrentSemesterLeaderboard = async (req, res) => { + try { + const { admission_year, limit = 50, page = 1 } = req.query; + + if (!admission_year) { + return res.status(400).json({ + success: false, + message: "Admission year is required", + }); + } + + // First, find the current/latest semester for the given admission year + const currentSemesterPipeline = [ + { + $match: { + admission_year: parseInt(admission_year), + }, + }, + { + $unwind: "$sgpa_list", + }, + { + $group: { + _id: null, + maxSemester: { $max: "$sgpa_list.semester" }, + }, + }, + ]; + + const currentSemesterResult = await StudentMarks.aggregate( + currentSemesterPipeline + ); + + if (currentSemesterResult.length === 0) { + return res.status(404).json({ + success: false, + message: "No semester data found for the specified admission year", + }); + } + + const currentSemester = currentSemesterResult[0].maxSemester; + const skip = (page - 1) * limit; + + // Get leaderboard for the current semester + const pipeline = [ + { + $match: { + admission_year: parseInt(admission_year), + "sgpa_list.semester": currentSemester, + }, + }, + { + $addFields: { + // Extract SGPA for the current semester + currentSemesterSGPA: { + $let: { + vars: { + semesterData: { + $arrayElemAt: [ + { + $filter: { + input: "$sgpa_list", + cond: { $eq: ["$$this.semester", currentSemester] }, + }, + }, + 0, + ], + }, + }, + in: "$$semesterData.sgpa", + }, + }, + // Calculate overall CGPA for reference + cgpa: { + $cond: { + if: { $gt: [{ $size: "$sgpa_list" }, 0] }, + then: { + $round: [{ $avg: "$sgpa_list.sgpa" }, 2], + }, + else: 0, + }, + }, + totalSemesters: { $size: "$sgpa_list" }, + }, + }, + { + $match: { + currentSemesterSGPA: { $exists: true, $ne: null }, + }, + }, + { + $sort: { + currentSemesterSGPA: -1, // Sort by current semester SGPA in descending order + cgpa: -1, // Then by overall CGPA + name: 1, // Then alphabetically by name + }, + }, + { + $skip: skip, + }, + { + $limit: parseInt(limit), + }, + { + $project: { + seat_number: 1, + name: 1, + admission_year: 1, + currentSemesterSGPA: 1, + cgpa: 1, + totalSemesters: 1, + sgpa_list: 1, + }, + }, + ]; + const students = await StudentMarks.aggregate(pipeline); + + // For proper tie handling, we need to get all students with their ranks + // and then apply pagination to the ranked results + if (page === 1) { + // For first page, we can directly assign ranks + const studentsWithRank = assignRanksWithTies( + students, + "currentSemesterSGPA", + 1 + ); + + // Get total count for pagination + const totalCountPipeline = [ + { + $match: { + admission_year: parseInt(admission_year), + "sgpa_list.semester": currentSemester, + }, + }, + { + $count: "total", + }, + ]; + + const totalCountResult = await StudentMarks.aggregate(totalCountPipeline); + const total = totalCountResult.length > 0 ? totalCountResult[0].total : 0; + + res.status(200).json({ + success: true, + data: studentsWithRank, + pagination: { + page: parseInt(page), + limit: parseInt(limit), + total, + pages: Math.ceil(total / limit), + }, + meta: { + admission_year: parseInt(admission_year), + currentSemester: currentSemester, + type: "current_semester_sgpa", + }, + }); + } else { + // For subsequent pages, we need to get all students to calculate proper ranks + const allStudentsPipeline = [ + { + $match: { + admission_year: parseInt(admission_year), + "sgpa_list.semester": currentSemester, + }, + }, + { + $addFields: { + currentSemesterSGPA: { + $let: { + vars: { + semesterData: { + $arrayElemAt: [ + { + $filter: { + input: "$sgpa_list", + cond: { $eq: ["$$this.semester", currentSemester] }, + }, + }, + 0, + ], + }, + }, + in: "$$semesterData.sgpa", + }, + }, + cgpa: { + $cond: { + if: { $gt: [{ $size: "$sgpa_list" }, 0] }, + then: { + $round: [{ $avg: "$sgpa_list.sgpa" }, 2], + }, + else: 0, + }, + }, + totalSemesters: { $size: "$sgpa_list" }, + }, + }, + { + $match: { + currentSemesterSGPA: { $exists: true, $ne: null }, + }, + }, + { + $sort: { + currentSemesterSGPA: -1, + cgpa: -1, + name: 1, + }, + }, + { + $project: { + seat_number: 1, + name: 1, + admission_year: 1, + currentSemesterSGPA: 1, + cgpa: 1, + totalSemesters: 1, + sgpa_list: 1, + }, + }, + ]; + + const allStudents = await StudentMarks.aggregate(allStudentsPipeline); + const allStudentsWithRank = assignRanksWithTies( + allStudents, + "currentSemesterSGPA", + 1 + ); + + // Apply pagination to ranked results + const startIndex = skip; + const endIndex = startIndex + parseInt(limit); + const paginatedStudents = allStudentsWithRank.slice(startIndex, endIndex); + + res.status(200).json({ + success: true, + data: paginatedStudents, + pagination: { + page: parseInt(page), + limit: parseInt(limit), + total: allStudents.length, + pages: Math.ceil(allStudents.length / limit), + }, + meta: { + admission_year: parseInt(admission_year), + currentSemester: currentSemester, + type: "current_semester_sgpa", + }, + }); + } + } catch (error) { + res.status(500).json({ + success: false, + message: "Error fetching current semester leaderboard", + error: error.message, + }); + } +}; + +/** + * @desc Get available admission years for leaderboard + * @route GET /api/v1/leaderboard/meta/years + * @access Private + */ +export const getAvailableYears = async (req, res) => { + try { + const pipeline = [ + { + $group: { + _id: "$admission_year", + }, + }, + { + $sort: { _id: -1 }, // Most recent first + }, + ]; + + const yearResult = await StudentMarks.aggregate(pipeline); + const years = yearResult.map((item) => item._id); + + res.status(200).json({ + success: true, + data: years, + count: years.length, + }); + } catch (error) { + res.status(500).json({ + success: false, + message: "Error fetching available years", + error: error.message, + }); + } +}; + +/** + * @desc Get available semesters for a specific admission year + * @route GET /api/v1/leaderboard/meta/semesters?admission_year=2021 + * @access Private + */ +export const getAvailableSemesters = async (req, res) => { + try { + const { admission_year } = req.query; + + if (!admission_year) { + return res.status(400).json({ + success: false, + message: "Admission year is required", + }); + } + + const pipeline = [ + { + $match: { + admission_year: parseInt(admission_year), + }, + }, + { + $unwind: "$sgpa_list", + }, + { + $group: { + _id: "$sgpa_list.semester", + }, + }, + { + $sort: { _id: 1 }, + }, + ]; + + const semesterResult = await StudentMarks.aggregate(pipeline); + const semesters = semesterResult.map((item) => item._id); + + res.status(200).json({ + success: true, + data: semesters, + count: semesters.length, + admission_year: parseInt(admission_year), + }); + } catch (error) { + res.status(500).json({ + success: false, + message: "Error fetching available semesters", + error: error.message, + }); + } +}; diff --git a/leaderboard_backend/middlewares/auth.js b/leaderboard_backend/middlewares/auth.js new file mode 100644 index 0000000..f9181f5 --- /dev/null +++ b/leaderboard_backend/middlewares/auth.js @@ -0,0 +1,63 @@ +import { OAuth2Client } from 'google-auth-library'; +import jwt from 'jsonwebtoken'; + +const client = new OAuth2Client(process.env.GOOGLE_CLIENT_ID); + +export const verifyGoogleToken = async (req, res, next) => { + try { + const authHeader = req.headers.authorization; + + if (!authHeader || !authHeader.startsWith('Bearer ')) { + return res.status(401).json({ + success: false, + message: 'No token provided or invalid format' + }); + } + + const token = authHeader.substring(7); + + try { + const ticket = await client.verifyIdToken({ + idToken: token, + audience: process.env.GOOGLE_CLIENT_ID + }); + + const payload = ticket.getPayload(); + + if (!payload.email || !payload.email.endsWith('@somaiya.edu')) { + return res.status(403).json({ + success: false, + message: 'Access denied. Please use your Somaiya email.' + }); + } + + req.user = { + email: payload.email, + name: payload.name, + googleId: payload.sub + }; + + next(); + } catch (googleError) { + + try { + const decoded = jwt.verify(token, process.env.JWT_SECRET || 'your-secret-key'); + req.user = decoded; + next(); + } catch (jwtError) { + return res.status(401).json({ + success: false, + message: 'Invalid token' + }); + } + } + } catch (error) { + console.error('Auth middleware error:', error); + return res.status(500).json({ + success: false, + message: 'Authentication error' + }); + } +}; + +export const verifyToken = verifyGoogleToken; \ No newline at end of file diff --git a/leaderboard_backend/models/studentMarks.js b/leaderboard_backend/models/studentMarks.js new file mode 100644 index 0000000..2ead13e --- /dev/null +++ b/leaderboard_backend/models/studentMarks.js @@ -0,0 +1,29 @@ +import mongoose from "mongoose"; + +const studentMarksSchema = new mongoose.Schema( + { + seat_number: { type: String, required: true, index: true }, + name: { type: String, required: true }, + admission_year: { type: Number, required: true, index: true }, + sgpa_list: [ + { + semester: { type: Number, required: true }, + sgpa: { type: Number, required: true }, + }, + ], + }, + { timestamps: true } +); + +// Adding text index for full-text search +studentMarksSchema.index({ + seat_number: "text", + name: "text", +}); + +const StudentMarks = mongoose.model( + "StudentMarks", + studentMarksSchema, + "students" +); +export default StudentMarks; diff --git a/leaderboard_backend/package-lock.json b/leaderboard_backend/package-lock.json new file mode 100644 index 0000000..15c94c5 --- /dev/null +++ b/leaderboard_backend/package-lock.json @@ -0,0 +1,1937 @@ +{ + "name": "leaderboard_backend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "leaderboard_backend", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "cookie-parser": "^1.4.7", + "cookies": "^0.9.1", + "cors": "^2.8.5", + "dotenv": "^17.2.0", + "express": "^5.1.0", + "google-auth-library": "^10.1.0", + "jsonwebtoken": "^9.0.2", + "mongoose": "^8.16.4", + "nodemon": "^3.1.10" + } + }, + "node_modules/@mongodb-js/saslprep": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.3.0.tgz", + "integrity": "sha512-zlayKCsIjYb7/IdfqxorK5+xUMyi4vOKcFy10wKJYc63NSdKI8mNME+uJqfatkPmOSMMUiojrL58IePKBm3gvQ==", + "license": "MIT", + "dependencies": { + "sparse-bitfield": "^3.0.3" + } + }, + "node_modules/@types/webidl-conversions": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==", + "license": "MIT" + }, + "node_modules/@types/whatwg-url": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", + "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", + "license": "MIT", + "dependencies": { + "@types/webidl-conversions": "*" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/bson": { + "version": "6.10.4", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.4.tgz", + "integrity": "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==", + "license": "Apache-2.0", + "engines": { + "node": ">=16.20.1" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/cookies": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.9.1.tgz", + "integrity": "sha512-TG2hpqe4ELx54QER/S3HQ9SRVnQnGBtKUz5bLQWtYAQ+o6GpgMs6sYUvaiJjVxb+UXwhRhAEP3m7LbsIZ77Hmw==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "keygrip": "~1.1.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dotenv": { + "version": "17.2.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.0.tgz", + "integrity": "sha512-Q4sgBT60gzd0BB0lSyYD3xM4YxrXA9y4uBDof1JNYGzOXrQdQ6yX+7XIAqoFOGQFOTK1D3Hts5OllpxMDZFONQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gaxios": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.1.tgz", + "integrity": "sha512-Odju3uBUJyVCkW64nLD4wKLhbh93bh6vIg/ZIXkWiLPBrdgtc65+tls/qml+un3pr6JqYVFDZbbmLDQT68rTOQ==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/gcp-metadata": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-7.0.1.tgz", + "integrity": "sha512-UcO3kefx6dCcZkgcTGgVOTFb7b1LlQ02hY1omMjjrrBzkajRMCFgYOjs7J71WqnuG1k2b+9ppGL7FsOfhZMQKQ==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/google-auth-library": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.1.0.tgz", + "integrity": "sha512-GspVjZj1RbyRWpQ9FbAXMKjFGzZwDKnUHi66JJ+tcjcu5/xYAP1pdlWotCuIkMwjfVsxxDvsGZXGLzRt72D0sQ==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^7.0.0", + "gcp-metadata": "^7.0.0", + "google-logging-utils": "^1.0.0", + "gtoken": "^8.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-auth-library/node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/google-auth-library/node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/google-logging-utils": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.1.tgz", + "integrity": "sha512-rcX58I7nqpu4mbKztFeOAObbomBbHU2oIb/d3tJfF3dizGSApqtSwYJigGCooHdnMyQBIw8BrWyK96w3YXgr6A==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gtoken": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-8.0.0.tgz", + "integrity": "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==", + "license": "MIT", + "dependencies": { + "gaxios": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/gtoken/node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/gtoken/node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "license": "ISC" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/kareem": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz", + "integrity": "sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/keygrip": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz", + "integrity": "sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==", + "license": "MIT", + "dependencies": { + "tsscmp": "1.0.6" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", + "license": "MIT" + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mongodb-connection-string-url": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz", + "integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==", + "license": "Apache-2.0", + "dependencies": { + "@types/whatwg-url": "^11.0.2", + "whatwg-url": "^14.1.0 || ^13.0.0" + } + }, + "node_modules/mongoose": { + "version": "8.16.4", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.16.4.tgz", + "integrity": "sha512-jslgdQ8pY2vcNSKPv3Dbi5ogo/NT8zcvf6kPDyD8Sdsjsa1at3AFAF0F5PT+jySPGSPbvlNaQ49nT9h+Kx2UDA==", + "license": "MIT", + "dependencies": { + "bson": "^6.10.4", + "kareem": "2.6.3", + "mongodb": "~6.17.0", + "mpath": "0.9.0", + "mquery": "5.0.0", + "ms": "2.1.3", + "sift": "17.1.3" + }, + "engines": { + "node": ">=16.20.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mongoose" + } + }, + "node_modules/mongoose/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/mongoose/node_modules/gaxios": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.1.3.tgz", + "integrity": "sha512-95hVgBRgEIRQQQHIbnxBXeHbW4TqFk4ZDJW7wmVtvYar72FdhRIo1UGOLS2eRAKCPEdPBWu+M7+A33D9CdX9rA==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/mongoose/node_modules/gcp-metadata": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.3.0.tgz", + "integrity": "sha512-FNTkdNEnBdlqF2oatizolQqNANMrcqJt6AAYt99B3y1aLLC8Hc5IOBb+ZnnzllodEEf6xMBp6wRcBbc16fa65w==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "gaxios": "^5.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/mongoose/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/mongoose/node_modules/mongodb": { + "version": "6.17.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.17.0.tgz", + "integrity": "sha512-neerUzg/8U26cgruLysKEjJvoNSXhyID3RvzvdcpsIi2COYM3FS3o9nlH7fxFtefTb942dX3W9i37oPfCVj4wA==", + "license": "Apache-2.0", + "dependencies": { + "@mongodb-js/saslprep": "^1.1.9", + "bson": "^6.10.4", + "mongodb-connection-string-url": "^3.0.0" + }, + "engines": { + "node": ">=16.20.1" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.188.0", + "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", + "gcp-metadata": "^5.2.0", + "kerberos": "^2.0.1", + "mongodb-client-encryption": ">=6.0.0 <7", + "snappy": "^7.2.2", + "socks": "^2.7.1" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } + }, + "node_modules/mongoose/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/mongoose/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/mongoose/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause", + "optional": true, + "peer": true + }, + "node_modules/mongoose/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/mpath": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", + "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mquery": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz", + "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==", + "license": "MIT", + "dependencies": { + "debug": "4.x" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/nodemon": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", + "integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==", + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", + "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", + "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.6.3", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sift": { + "version": "17.1.3", + "resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz", + "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==", + "license": "MIT" + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "license": "MIT", + "dependencies": { + "memory-pager": "^1.0.2" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tsscmp": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", + "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==", + "license": "MIT", + "engines": { + "node": ">=0.6.x" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + } + } +} diff --git a/leaderboard_backend/package.json b/leaderboard_backend/package.json new file mode 100644 index 0000000..6b0acac --- /dev/null +++ b/leaderboard_backend/package.json @@ -0,0 +1,26 @@ +{ + "name": "leaderboard_backend", + "version": "1.0.0", + "description": "", + "main": "server.js", + "scripts": { + "start": "nodemon server.js", + "dev": "nodemon server.js", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "type": "module", + "dependencies": { + "cookie-parser": "^1.4.7", + "cookies": "^0.9.1", + "cors": "^2.8.5", + "dotenv": "^17.2.0", + "express": "^5.1.0", + "google-auth-library": "^10.1.0", + "jsonwebtoken": "^9.0.2", + "mongoose": "^8.16.4", + "nodemon": "^3.1.10" + } +} diff --git a/leaderboard_backend/routes/leaderboard.js b/leaderboard_backend/routes/leaderboard.js new file mode 100644 index 0000000..2bb908a --- /dev/null +++ b/leaderboard_backend/routes/leaderboard.js @@ -0,0 +1,29 @@ +import express from "express"; +import { + getOverallLeaderboard, + getSemesterLeaderboard, + getCurrentSemesterLeaderboard, + getAvailableYears, + getAvailableSemesters, +} from "../controllers/leaderboard.js"; +import { verifyGoogleToken } from "../middlewares/auth.js"; + +const router = express.Router(); + +// GET /api/v1/leaderboard - Overall CGPA leaderboard for specific admission year +router.get("/", verifyGoogleToken, getOverallLeaderboard); + +// GET /api/v1/leaderboard/current - Current semester leaderboard for specific admission year +router.get("/current", verifyGoogleToken, getCurrentSemesterLeaderboard); + +// GET /api/v1/leaderboard/:semester - Semester-specific leaderboard for specific admission year +router.get("/:semester", verifyGoogleToken, getSemesterLeaderboard); + +// Helper routes for available data +// GET /api/v1/leaderboard/meta/years - Get available admission years +router.get("/meta/years", verifyGoogleToken, getAvailableYears); + +// GET /api/v1/leaderboard/meta/semesters - Get available semesters for specific admission year +router.get("/meta/semesters", verifyGoogleToken, getAvailableSemesters); + +export default router; diff --git a/leaderboard_backend/server.js b/leaderboard_backend/server.js new file mode 100644 index 0000000..237bd55 --- /dev/null +++ b/leaderboard_backend/server.js @@ -0,0 +1,37 @@ +import express from 'express'; +import cors from 'cors'; +import cookieParser from 'cookie-parser'; +import dotenv from 'dotenv'; +import connectToDb from './config/db.js'; +import leaderboardRoutes from './routes/leaderboard.js'; + +const app = express(); +dotenv.config(); +const port = process.env.PORT || 5000; + +// Database connection +connectToDb(); + +// Middlewares +app.use(cors({ + origin: ['http://localhost:3000', 'http://localhost:3001'], // Add your frontend URLs + credentials: true +})); +app.use(express.json()); +app.use(express.urlencoded({ extended: true })); +app.use(cookieParser()); + +// Routes +app.use('/api/v1/leaderboard', leaderboardRoutes); + +app.get('/', (req, res) => { + res.json({ + success: true, + message: 'Leaderboard API is running!', + timestamp: new Date().toISOString() + }); +}); + +app.listen(port, () => { + console.log(`Server is running at http://localhost:${port}`); +}); \ No newline at end of file diff --git a/notes-aid/package-lock.json b/notes-aid/package-lock.json index fb6ad1e..aa72221 100644 --- a/notes-aid/package-lock.json +++ b/notes-aid/package-lock.json @@ -11,10 +11,13 @@ "@octokit/rest": "^21.1.1", "@radix-ui/react-dropdown-menu": "^2.1.6", "@radix-ui/react-icons": "^1.3.2", + "@react-oauth/google": "^0.12.2", "autoprefixer": "^10.4.21", "axios": "^1.8.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "js-cookie": "^3.0.5", + "jwt-decode": "^4.0.0", "lucide-react": "^0.475.0", "next": "^15.2.4", "next-auth": "^4.24.11", @@ -2959,6 +2962,16 @@ "integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==", "license": "MIT" }, + "node_modules/@react-oauth/google": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@react-oauth/google/-/google-0.12.2.tgz", + "integrity": "sha512-d1GVm2uD4E44EJft2RbKtp8Z1fp/gK8Lb6KHgs3pHlM0PxCXGLaq8LLYQYENnN4xPWO1gkL4apBtlPKzpLvZwg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, "node_modules/@rollup/plugin-babel": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", @@ -6704,6 +6717,15 @@ "url": "https://github.com/sponsors/panva" } }, + "node_modules/js-cookie": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", + "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -6807,6 +6829,15 @@ "node": ">=4.0" } }, + "node_modules/jwt-decode": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", + "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", diff --git a/notes-aid/package.json b/notes-aid/package.json index 93fc6f3..869d4d3 100644 --- a/notes-aid/package.json +++ b/notes-aid/package.json @@ -12,10 +12,13 @@ "@octokit/rest": "^21.1.1", "@radix-ui/react-dropdown-menu": "^2.1.6", "@radix-ui/react-icons": "^1.3.2", + "@react-oauth/google": "^0.12.2", "autoprefixer": "^10.4.21", "axios": "^1.8.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "js-cookie": "^3.0.5", + "jwt-decode": "^4.0.0", "lucide-react": "^0.475.0", "next": "^15.2.4", "next-auth": "^4.24.11", diff --git a/notes-aid/src/app/leaderboard/page.tsx b/notes-aid/src/app/leaderboard/page.tsx index c9c2ac1..d0a30c5 100644 --- a/notes-aid/src/app/leaderboard/page.tsx +++ b/notes-aid/src/app/leaderboard/page.tsx @@ -1,10 +1,100 @@ +"use client"; +import { useState, useEffect } from "react"; import Leaderboard from "@/components/Leaderboard"; -import { AuthProvider } from "@/context/AuthContext"; +import { GoogleLogin, GoogleOAuthProvider, CredentialResponse, googleLogout } from '@react-oauth/google'; +import { parseCookies, setCookie, destroyCookie } from "nookies"; +import { jwtDecode } from "jwt-decode"; + +interface DecodedToken { + email?: string; + [key: string]: any; +} export default function LeaderboardPage() { + const [isLoggedIn, setIsLoggedIn] = useState(false); + const [error, setError] = useState(""); + const handleLogout = () => { + googleLogout(); + destroyCookie(null, "googleToken"); + setIsLoggedIn(false); + setError(""); + }; + + useEffect(() => { + const cookies = parseCookies(); + const token = cookies.googleToken; + + if (token) { + try { + const decoded = jwtDecode(token); + if (decoded.email?.endsWith("@somaiya.edu")) { + setIsLoggedIn(true); + } else { + destroyCookie(null, "googleToken"); + } + } catch { + destroyCookie(null, "googleToken"); + } + } + }, []); + + const handleLoginSuccess = (credentialResponse: CredentialResponse) => { + const token = credentialResponse.credential; + if (!token) { + setError("No token received from Google."); + return; + } + + try { + const decoded = jwtDecode(token); + const email = decoded.email || ""; + + if (email.endsWith("@somaiya.edu")) { + setCookie(null, "googleToken", token, { + maxAge: 7 * 24 * 60 * 60, + path: "/", + }); + setIsLoggedIn(true); + setError(""); + } else { + setError("Please sign in with your Somaiya email."); + } + } catch { + setError("Invalid token. Try again."); + } + }; + return ( - - - - ) + + {!isLoggedIn ? ( +
+
+

+ Sign in with Somaiya Email to view Leaderboard +

+ setError("Google Login failed")} + shape="rectangular" + text="continue_with" + theme="outline" + logo_alignment="center" + hosted_domain="somaiya.edu" + /> + {error &&

{error}

} +
+
+ ) : ( +
+ + +
+ )} +
+ ); } diff --git a/notes-aid/src/components/Leaderboard.tsx b/notes-aid/src/components/Leaderboard.tsx index 21ae613..68704d5 100644 --- a/notes-aid/src/components/Leaderboard.tsx +++ b/notes-aid/src/components/Leaderboard.tsx @@ -4,14 +4,13 @@ import { useState, useEffect, useMemo } from 'react'; import { Card, CardContent } from './ui/Card'; import { Input } from './ui/Input'; import { useDebounce } from '../hook/useDebounce'; -import { useAuth } from '../context/AuthContext'; import { Search, Trophy, Medal, Award, Users, ChevronLeft, ChevronRight, RefreshCw, Star, X, BarChart3, ChevronDown } from 'lucide-react'; +import { parseCookies } from 'nookies'; -// Utility function to construct API URLs properly const getApiUrl = (endpoint: string) => { const baseUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:5000'; - const cleanBase = baseUrl.replace(/\/+$/, ''); // Remove trailing slashes - const cleanEndpoint = endpoint.replace(/^\/+/, ''); // Remove leading slashes + const cleanBase = baseUrl.replace(/\/+$/, ''); + const cleanEndpoint = endpoint.replace(/^\/+/, ''); return `${cleanBase}/${cleanEndpoint}`; }; @@ -61,31 +60,37 @@ interface SemestersResponse { export default function Leaderboard() { - const { token } = useAuth(); + // Get token from cookies instead of hardcoding + const [token, setToken] = useState(''); const [students, setStudents] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [isHydrated, setIsHydrated] = useState(false); - // Filter states const [searchTerm, setSearchTerm] = useState(''); const [selectedYear, setSelectedYear] = useState(''); const [selectedSemester, setSelectedSemester] = useState('overall'); const [currentPage, setCurrentPage] = useState(1); const [limit] = useState(500); - // Available options const [availableYears, setAvailableYears] = useState([]); const [availableSemesters, setAvailableSemesters] = useState([]); - // Pagination info const [totalPages, setTotalPages] = useState(0); const [totalStudents, setTotalStudents] = useState(0); - // Handle hydration + // Get token from cookies on component mount useEffect(() => { setIsHydrated(true); + const cookies = parseCookies(); + const googleToken = cookies.googleToken; + if (googleToken) { + setToken(googleToken); + } else { + setError('No authentication token found. Please login again.'); + setLoading(false); + } }, []); - // Debounced search term for client-side filtering + const debouncedSearchTerm = useDebounce(searchTerm, 100); // Client-side search filtering const filteredStudents = useMemo(() => { if (!debouncedSearchTerm) return students; @@ -98,12 +103,13 @@ export default function Leaderboard() { return nameMatch || rollMatch || yearMatch; }); - }, [students, debouncedSearchTerm]); // Fetch available years on component mount + }, [students, debouncedSearchTerm]); useEffect(() => { - if (!isHydrated || !token) return; // Wait for hydration and token + if (!isHydrated || !token) return; - const fetchAvailableYears = async () => { try { - setLoading(true); // Set loading when starting fetch + const fetchAvailableYears = async () => { + try { + setLoading(true); const response = await fetch( getApiUrl('api/v1/leaderboard/meta/years'), { @@ -113,15 +119,18 @@ export default function Leaderboard() { } } ); - if (response.ok) { + + if (response.ok) { const data: YearsResponse = await response.json(); setAvailableYears(data.data || []); if (data.data && data.data.length > 0) { setSelectedYear(data.data[0].toString()); } else { - // No years available, stop loading setLoading(false); } + } else if (response.status === 401 || response.status === 403) { + setError('Authentication failed. Please login again.'); + setLoading(false); } else { console.error('Failed to fetch years:', response.status); setError('Failed to fetch available years'); @@ -132,12 +141,16 @@ export default function Leaderboard() { setError('Failed to connect to server'); setLoading(false); } - }; fetchAvailableYears(); + }; + + fetchAvailableYears(); }, [isHydrated, token]); // Wait for hydration and token // Fetch available semesters when year changes - useEffect(() => { const fetchAvailableSemesters = async () => { + useEffect(() => { + const fetchAvailableSemesters = async () => { if (!selectedYear || !token) return; - try { const response = await fetch( + try { + const response = await fetch( getApiUrl(`api/v1/leaderboard/meta/semesters?admission_year=${selectedYear}`), { headers: { @@ -150,19 +163,28 @@ export default function Leaderboard() { if (response.ok) { const data: SemestersResponse = await response.json(); setAvailableSemesters(data.data); + } else if (response.status === 401 || response.status === 403) { + setError('Authentication failed. Please login again.'); } } catch (err) { console.error('Error fetching semesters:', err); } - }; if (selectedYear) { + }; + + if (selectedYear) { fetchAvailableSemesters(); } }, [selectedYear, token]); // Fetch leaderboard data - useEffect(() => { const fetchLeaderboard = async () => { - if (!selectedYear || !token) { return; - } + useEffect(() => { + const fetchLeaderboard = async () => { + if (!selectedYear || !token) { + return; + } + setLoading(true); - setError(null); try { + setError(null); + + try { let endpoint = 'api/v1/leaderboard'; if (selectedSemester === 'overall') { @@ -171,18 +193,23 @@ export default function Leaderboard() { endpoint += `/current?admission_year=${selectedYear}&page=${currentPage}&limit=${limit}`; } else { endpoint += `/${selectedSemester}?admission_year=${selectedYear}&page=${currentPage}&limit=${limit}`; - } + } + const url = getApiUrl(endpoint); const response = await fetch(url, { headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' } - }); - if (response.ok) { const data: LeaderboardResponse = await response.json(); + }); + + if (response.ok) { + const data: LeaderboardResponse = await response.json(); setStudents(data.data || []); setTotalPages(data.pagination?.pages || 1); setTotalStudents(data.pagination?.total || 0); + } else if (response.status === 401 || response.status === 403) { + setError('Authentication failed. Please login again.'); } else { const errorText = await response.text(); console.error('API Error:', response.status, errorText); @@ -190,10 +217,13 @@ export default function Leaderboard() { } } catch (err) { console.error('Network error:', err); - setError('Network error. Please check if the backend server is running.'); } finally { + setError('Network error. Please check if the backend server is running.'); + } finally { setLoading(false); } - }; fetchLeaderboard(); + }; + + fetchLeaderboard(); }, [selectedYear, selectedSemester, currentPage, limit, token]); // Reset page when filters change (but not search) diff --git a/notes-aid/src/context/AuthContext.tsx b/notes-aid/src/context/AuthContext.tsx deleted file mode 100644 index 3a622ba..0000000 --- a/notes-aid/src/context/AuthContext.tsx +++ /dev/null @@ -1,493 +0,0 @@ -"use client"; - -import { createContext, useContext, useState, useEffect, useCallback, ReactNode } from 'react'; -import { useRouter } from 'next/navigation'; -import { parseCookies, setCookie, destroyCookie } from 'nookies'; - -// Define types -interface User { - id: string; - name: string; - email: string; - rollNo?: string; - year?: number | string; - branch?: string; - phone?: string; - isProfileComplete?: boolean; -} - -// Define a type for SGPA entries -interface SGPAEntry { - _id: string; - semester: number; - sgpa: number; -} - -// Add dashboard-related interfaces -interface UserStats { - cgpa?: number; - totalSemesters?: number; - sgpaList?: SGPAEntry[]; // Added sgpaList to UserStats - rank: number; - totalStudents: number; - admissionYear?: number; - currentYear?: number; - isProfileComplete?: boolean; - documentsUploaded: number; -} - -interface DocumentItem { - _id: string; - subject: string; - module: string; - faculty?: string; - year?: number; - uploadedBy: { - name: string; - }; - url: string; - cloudinaryId?: string; - fileName?: string; - fileSize?: number; - description?: string; - createdAt: string; -} - -interface PodcastItem { - _id: string; - title: string; - topic: string; - uploadedBy: { - name: string; - }; - createdAt: string; - duration?: number; -} - -interface DashboardData { - stats: { - totalDocuments: number; - totalPodcasts: number; - documentsUploaded: number; - rank: number; - totalUsers: number; - }; - userStats: UserStats; - recentDocuments: DocumentItem[]; - recentPodcasts: PodcastItem[]; - isLoading: boolean; - error: string | null; -} - -interface AuthContextType { - user: User | null; - token: string | null; - loading: boolean; - error: string | null; - login: (userData: User, token: string) => void; - logout: () => void; - updateUser: (userData: Partial) => void; - updateProfile: (userData: Partial) => void; - dashboardData: DashboardData | null; - refreshDashboardData: () => Promise; -} - -// Create context -const AuthContext = createContext(undefined); - -// Utility function to construct API URLs properly -const getApiUrl = (endpoint: string) => { - const baseUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:5000'; - const cleanBase = baseUrl.replace(/\/+$/, ''); // Remove trailing slashes - const cleanEndpoint = endpoint.replace(/^\/+/, ''); // Remove leading slashes - return `${cleanBase}/${cleanEndpoint}`; -}; - -// Cookie options with httpOnly and secure flags for production -const COOKIE_OPTIONS = { - maxAge: 30 * 24 * 60 * 60, // 30 days - path: '/', - secure: process.env.NODE_ENV === 'production', - sameSite: 'lax' as const, -}; - - -export function AuthProvider({ children }: { children: ReactNode }) { - const [user, setUser] = useState(null); - const [token, setToken] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [mounted, setMounted] = useState(false); - const [justLoggedIn, setJustLoggedIn] = useState(false); // Track fresh login - const router = useRouter(); - - // Dashboard state - const [dashboardData, setDashboardData] = useState(null); - - // Handle mounting to prevent hydration issues - useEffect(() => { - setMounted(true); - }, []); - - useEffect(() => { - if (!mounted) return; // Don't run auth check until mounted - - const checkAuth = async () => { - try { - const cookies = parseCookies(); - // Try new cookie names first, then fall back to legacy names - const authToken = cookies.quadratoken || cookies.authToken; - const userCookie = cookies.user || cookies.userData; - if (authToken && userCookie) { - const userData = JSON.parse(userCookie); - setUser(userData); - setToken(authToken); - // Skip token validation if we just logged in (to avoid CORS issues during callback) - if (justLoggedIn) { - setJustLoggedIn(false); - setLoading(false); - return; - } - - try { const response = await fetch(getApiUrl('api/v1/auth/status'), { - headers: { Authorization: `Bearer ${authToken}` }, - credentials: 'include' // Include cookies in requests - }); - - if (!response.ok) { - throw new Error('Invalid token'); - } const data = await response.json(); - if (data.user) { - // After token validation, fetch complete user profile - try { - const profileResponse = await fetch(getApiUrl(`api/v1/user/${data.user.id}`), { - headers: { Authorization: `Bearer ${authToken}` }, - credentials: 'include' - }); - - if (profileResponse.ok) { - const profileData = await profileResponse.json(); - if (profileData.success && profileData.user) { - // Use complete profile data instead of incomplete auth status data - setUser(profileData.user); - setCookie(null, 'user', JSON.stringify(profileData.user), COOKIE_OPTIONS); - } else { - // Fallback to merging with existing data if profile fetch fails - setUser(prevUser => { - const mergedUser = { ...prevUser, ...data.user }; - setCookie(null, 'user', JSON.stringify(mergedUser), COOKIE_OPTIONS); - return mergedUser; - }); - } - } else { - // Fallback to merging with existing data if profile fetch fails - setUser(prevUser => { - const mergedUser = { ...prevUser, ...data.user }; - setCookie(null, 'user', JSON.stringify(mergedUser), COOKIE_OPTIONS); - return mergedUser; - }); - } - } catch (profileErr) { - console.error('Failed to fetch complete profile:', profileErr); - // Fallback to merging with existing data - setUser(prevUser => { - const mergedUser = { ...prevUser, ...data.user }; - setCookie(null, 'user', JSON.stringify(mergedUser), COOKIE_OPTIONS); - return mergedUser; - }); - } - - // Clean up legacy cookie names - if (cookies.userData) { - destroyCookie(null, 'userData', { path: '/' }); - } - if (cookies.authToken) { - destroyCookie(null, 'authToken', { path: '/' }); - setCookie(null, 'quadratoken', authToken, COOKIE_OPTIONS); - } - - // Fetch dashboard data after successful authentication - if (data.user.isProfileComplete !== false) { - fetchDashboardData(data.user.id, authToken); - } - } - } catch (err) { - console.error('Token validation failed:', err); - logout(); - } - } - } catch (err) { - console.error('Auth restore error:', err); - logout(); - setError('Authentication session expired. Please login again.'); - } finally { - setLoading(false); - } - }; checkAuth(); - }, [mounted, justLoggedIn]); - - const fetchDashboardData = async (userId: string, authToken: string) => { - try { - setDashboardData(prev => prev ? { ...prev, isLoading: true, error: null } : { - stats: { - totalDocuments: 0, - totalPodcasts: 0, - documentsUploaded: 0, - rank: 0, - totalUsers: 0 - }, - userStats: { - rank: 0, - totalStudents: 0, - documentsUploaded: 0 - }, - recentDocuments: [], - recentPodcasts: [], - isLoading: true, - error: null - }); // Fetch real data from backend endpoints - const [userStatsRes, documentsRes, podcastsRes] = await Promise.allSettled([ - fetch(getApiUrl(`api/v1/user/${userId}/stats`), { - headers: { Authorization: `Bearer ${authToken}` } - }), - fetch(getApiUrl('api/v1/documents'), { - headers: { Authorization: `Bearer ${authToken}` } - }), - fetch(getApiUrl('api/v1/podcasts'), { - headers: { Authorization: `Bearer ${authToken}` } - }) - ]); - - let userStats: UserStats = { - rank: 0, - totalStudents: 0, - documentsUploaded: 0 - }; - - if (userStatsRes.status === 'fulfilled' && userStatsRes.value.ok) { - const userStatsData = await userStatsRes.value.json(); - - if (userStatsData.success) { - const stats = userStatsData.stats; - userStats = { - cgpa: stats.cgpa, - totalSemesters: stats.totalSemesters, - sgpaList: stats.sgpaList, // Include sgpaList in userStats - rank: stats.rank, - totalStudents: stats.totalStudentsInYear, - admissionYear: stats.admissionYear, - currentYear: stats.year, - isProfileComplete: stats.isProfileComplete, - documentsUploaded: stats.documentsUploaded || 0, - }; - } - } // Now fetch leaderboard with the correct admission year - let leaderboardRes = null; - if (userStats.admissionYear) { - try { - leaderboardRes = await fetch( - getApiUrl(`api/v1/leaderboard?admission_year=${userStats.admissionYear}&limit=100`), - { - headers: { Authorization: `Bearer ${authToken}` } - } - ); - } catch (error) { - console.error('Error fetching leaderboard:', error); - } - } - - // Process documents - let documentsData: DocumentItem[] = []; - let totalDocuments = 0; - if (documentsRes.status === 'fulfilled' && documentsRes.value.ok) { - const docsResponse = await documentsRes.value.json(); - if (docsResponse.success && docsResponse.data) { - documentsData = docsResponse.data.slice(0, 5); // Recent documents - totalDocuments = docsResponse.totalDocuments; - } - } - - // Process podcasts - let podcastsData: PodcastItem[] = []; - let totalPodcasts = 0; - if (podcastsRes.status === 'fulfilled' && podcastsRes.value.ok) { - const podcastsResponse = await podcastsRes.value.json(); - if (podcastsResponse.success && podcastsResponse.data) { - podcastsData = podcastsResponse.data.slice(0, 3); // Recent podcasts - totalPodcasts = podcastsResponse.totalPodcasts; - } - } // Process leaderboard for user rank - let userRank = userStats.rank || 0; - let totalUsers = userStats.totalStudents || 0; - - if (leaderboardRes && leaderboardRes.ok) { - try { - const leaderboardResponse = await leaderboardRes.json(); - - if (leaderboardResponse.success && leaderboardResponse.data && Array.isArray(leaderboardResponse.data)) { - const leaderboard = leaderboardResponse.data; - totalUsers = leaderboard.length; - - // Find user by rollNo/seat_number instead of user._id - const userPosition = leaderboard.findIndex((entry: any) => - entry.seat_number === user?.rollNo - ); - - if (userPosition >= 0) { - userRank = userPosition + 1; - } else { - // If not found in leaderboard, use the rank from userStats - userRank = userStats.rank || 0; - } - } - } catch (error) { - console.error('Error processing leaderboard response:', error); - // Fallback to userStats rank if leaderboard processing fails - userRank = userStats.rank || 0; - } - } setDashboardData({ - stats: { - totalDocuments, - totalPodcasts, - documentsUploaded: userStats.documentsUploaded || 0, - rank: userRank, - totalUsers - }, - userStats, - recentDocuments: documentsData, - recentPodcasts: podcastsData, - isLoading: false, - error: null - }); - - } catch (error) { - console.error('Error fetching dashboard data:', error); - setDashboardData(prev => prev ? { - ...prev, - isLoading: false, - error: 'Failed to load dashboard data' - } : { - stats: { - totalDocuments: 0, - totalPodcasts: 0, - documentsUploaded: 0, - rank: 0, - totalUsers: 0 - }, - userStats: { - rank: 0, - totalStudents: 0, - documentsUploaded: 0 - }, - recentDocuments: [], - recentPodcasts: [], - isLoading: false, - error: 'Failed to load dashboard data' - }); - } - }; - // Function to refresh dashboard data - const refreshDashboardData = useCallback(async () => { - if (user && token) { - await fetchDashboardData(user.id, token); - } - }, [user, token]); const login = (userData: User, authToken: string) => { - setUser(userData); - setToken(authToken); - setLoading(false); // Set loading to false immediately - setJustLoggedIn(true); // Mark as fresh login to skip validation - - // Use new cookie names for better security - setCookie(null, 'quadratoken', authToken, COOKIE_OPTIONS); - setCookie(null, 'user', JSON.stringify(userData), COOKIE_OPTIONS); - - // Clean up any legacy cookies - destroyCookie(null, 'authToken', { path: '/' }); - destroyCookie(null, 'userData', { path: '/' }); - - // Navigate to appropriate page after login - setTimeout(() => { - if (userData.isProfileComplete === false) { - router.push('/onboarding'); - } else { - router.push('/dashboard'); - } - }, 100); - - // Fetch dashboard data upon login if profile is complete - if (userData.isProfileComplete !== false) { - fetchDashboardData(userData.id, authToken); - } - }; - - const logout = async () => { - try { if (token) { - await fetch(getApiUrl('api/v1/auth/logout'), { - method: 'POST', - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json' - }, - credentials: 'include' // Include cookies in logout request - }); - } - } catch (error) { - console.error('Logout error:', error); - } finally { - // Clear all possible cookie names (new and legacy) - destroyCookie(null, 'quadratoken', { path: '/' }); - destroyCookie(null, 'user', { path: '/' }); - destroyCookie(null, 'authToken', { path: '/' }); - destroyCookie(null, 'userData', { path: '/' }); - setUser(null); - setToken(null); - setDashboardData(null); - router.push('/'); - } - }; - const updateUser = (userData: Partial) => { - setUser(prevUser => { - if (!prevUser) return null; - const updatedUser = {...prevUser, ...userData}; - setCookie(null, 'user', JSON.stringify(updatedUser), COOKIE_OPTIONS); - - // Refresh dashboard data if profile is now complete - if (userData.isProfileComplete === true && token) { - fetchDashboardData(updatedUser.id, token); - } - - return updatedUser; - }); - }; - - // Alias for updateUser to maintain consistent API - const updateProfile = (userData: Partial) => { - updateUser(userData); - }; - const value = { - user, - token, - loading, - error, - login, - logout, - updateUser, - updateProfile, - dashboardData, - refreshDashboardData - }; - - return ( - - {children} - - ); -} - -export const useAuth = () => { - const context = useContext(AuthContext); - if (context === undefined) { - throw new Error('useAuth must be used within an AuthProvider'); - } - return context; -}; \ No newline at end of file From bcca4c57442e6a2be3552fdeff580571f822f43e Mon Sep 17 00:00:00 2001 From: Om Thanage Date: Sun, 20 Jul 2025 15:42:58 +0530 Subject: [PATCH 3/7] fix: minor bugs --- notes-aid/src/app/leaderboard/page.tsx | 2 +- notes-aid/src/components/Leaderboard.tsx | 34 +++++++++++++----------- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/notes-aid/src/app/leaderboard/page.tsx b/notes-aid/src/app/leaderboard/page.tsx index d0a30c5..966791a 100644 --- a/notes-aid/src/app/leaderboard/page.tsx +++ b/notes-aid/src/app/leaderboard/page.tsx @@ -88,7 +88,7 @@ export default function LeaderboardPage() {
diff --git a/notes-aid/src/components/Leaderboard.tsx b/notes-aid/src/components/Leaderboard.tsx index 68704d5..147278a 100644 --- a/notes-aid/src/components/Leaderboard.tsx +++ b/notes-aid/src/components/Leaderboard.tsx @@ -60,13 +60,15 @@ interface SemestersResponse { export default function Leaderboard() { - // Get token from cookies instead of hardcoding const [token, setToken] = useState(''); const [students, setStudents] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [isHydrated, setIsHydrated] = useState(false); - const [searchTerm, setSearchTerm] = useState(''); + + // Separate the display value from the search logic + const [searchTerm, setSearchTerm] = useState(''); // This will update immediately + const debouncedSearchTerm = useDebounce(searchTerm, 300); // This will be debounced for filtering const [selectedYear, setSelectedYear] = useState(''); const [selectedSemester, setSelectedSemester] = useState('overall'); const [currentPage, setCurrentPage] = useState(1); @@ -78,7 +80,6 @@ export default function Leaderboard() { const [totalPages, setTotalPages] = useState(0); const [totalStudents, setTotalStudents] = useState(0); - // Get token from cookies on component mount useEffect(() => { setIsHydrated(true); const cookies = parseCookies(); @@ -91,7 +92,7 @@ export default function Leaderboard() { } }, []); - const debouncedSearchTerm = useDebounce(searchTerm, 100); // Client-side search filtering + // Client-side search filtering using debounced term const filteredStudents = useMemo(() => { if (!debouncedSearchTerm) return students; @@ -103,7 +104,8 @@ export default function Leaderboard() { return nameMatch || rollMatch || yearMatch; }); - }, [students, debouncedSearchTerm]); + }, [students, debouncedSearchTerm]); // Use debouncedSearchTerm for filtering + useEffect(() => { if (!isHydrated || !token) return; @@ -418,9 +420,11 @@ export default function Leaderboard() {

Leaderboard

Track academic excellence across batches and semesters

-
{/* Search Bar */} +
+ + {/* Search Bar */}
- +
)} +
- {/* Search Results Counter */} - {searchTerm && ( + {/* Search Results Counter - use debouncedSearchTerm for display */} + {debouncedSearchTerm && (

Found {filteredStudents.length} student{filteredStudents.length !== 1 ? 's' : ''} - {searchTerm && ` matching "${searchTerm}"`} + {debouncedSearchTerm && ` matching "${debouncedSearchTerm}"`}

)} -

{/* Filters */}
@@ -729,15 +733,15 @@ export default function Leaderboard() {

- {searchTerm ? 'No search results found' : 'No students found'} + {debouncedSearchTerm ? 'No search results found' : 'No students found'}

- {searchTerm - ? `No students match "${searchTerm}". Try a different search term or check your spelling.` + {debouncedSearchTerm + ? `No students match "${debouncedSearchTerm}". Try a different search term or check your spelling.` : 'No data available for the selected criteria. Try selecting a different year or semester.' }

- {searchTerm && ( + {debouncedSearchTerm && (
{/* Semester Selection */} From ffc3ef14981046b719212cffb0e818cbbb582015 Mon Sep 17 00:00:00 2001 From: Namita Mehra Date: Sat, 9 Aug 2025 14:33:41 +0530 Subject: [PATCH 6/7] Created a Bookmark feature along with all stored bookmark's page. (#113) * bookmark page, module rendering done * Bookmark page with states * Bookmark and Bookmark pages done * Bookmark and Bookmark page done * Fixed UI * UI fix --- .../app/[year]/[branch]/[semester]/page.tsx | 33 ++++- notes-aid/src/app/bookmarks/page.tsx | 133 ++++++++++++++++++ notes-aid/src/app/globals.css | 3 +- notes-aid/src/components/BookmarkButton.tsx | 57 ++++++++ notes-aid/src/components/ModuleCard.tsx | 26 +++- notes-aid/src/components/Navbar.tsx | 6 +- notes-aid/src/components/ThemeChanger.tsx | 2 +- notes-aid/src/components/TopicList.tsx | 19 ++- notes-aid/src/components/Video.tsx | 14 ++ notes-aid/tailwindconfig.mjs | 8 ++ 10 files changed, 289 insertions(+), 12 deletions(-) create mode 100644 notes-aid/src/app/bookmarks/page.tsx create mode 100644 notes-aid/src/components/BookmarkButton.tsx create mode 100644 notes-aid/tailwindconfig.mjs diff --git a/notes-aid/src/app/[year]/[branch]/[semester]/page.tsx b/notes-aid/src/app/[year]/[branch]/[semester]/page.tsx index 7a7b236..a186fb2 100644 --- a/notes-aid/src/app/[year]/[branch]/[semester]/page.tsx +++ b/notes-aid/src/app/[year]/[branch]/[semester]/page.tsx @@ -110,16 +110,42 @@ const EngineeringCurriculum: React.FC = () => { const initialSubject = subjects ? Object.keys(subjects)[0] : ""; const [selectedSubject, setSelectedSubject] = useState(initialSubject); const [selectedModule, setSelectedModule] = useState(1); + + + useEffect(() => { + const params = new URLSearchParams(window.location.search); + const fromBookmark = params.get('fromBookmark'); + + if (fromBookmark) { + try { + const bookmarkState = JSON.parse(fromBookmark); + if (bookmarkState.selectedSubject && bookmarkState.selectedModule) { + setSelectedSubject(bookmarkState.selectedSubject); + setSelectedModule(bookmarkState.selectedModule); + } + } catch (e) { + console.error('Error parsing bookmark state', e); + } + } + }, []); useEffect(() => { - if (subjects && selectedSubject) { + if (subjects && selectedSubject) { + const params = new URLSearchParams(window.location.search); + const moduleParam = params.get("module"); + + if (moduleParam) { + setSelectedModule(parseInt(moduleParam)); + } else { const firstModuleKey = Object.keys( subjects[selectedSubject]?.modules || {} )[0]; setSelectedModule(firstModuleKey ? parseInt(firstModuleKey) : 1); } + } }, [selectedSubject, subjects]); + const { progressData, updateVideoProgress, resetProgress } = useProgress(selectedSubject); if (loading) { @@ -270,6 +296,7 @@ const EngineeringCurriculum: React.FC = () => { { progressData.moduleProgress[moduley] || 0 } numberOfVideos={numberVideoInModule(moduley)} - + currentSubject={selectedSubject} + + /> ); } diff --git a/notes-aid/src/app/bookmarks/page.tsx b/notes-aid/src/app/bookmarks/page.tsx new file mode 100644 index 0000000..8ff8dfc --- /dev/null +++ b/notes-aid/src/app/bookmarks/page.tsx @@ -0,0 +1,133 @@ +'use client'; +import { useEffect, useState } from 'react'; +import { ChevronDown } from 'lucide-react'; +import Link from 'next/link'; + +interface BookmarkItem { + id: string; + title: string; + subject: string; + url?: string; + module?: number; + topic?: string; +} + +type BookmarkType = 'modules' | 'topics' | 'videos'; + +export default function BookmarksPage() { + const [bookmarks, setBookmarks] = useState([]); + const [activeTab, setActiveTab] = useState('modules'); + const [activeBookmarkId, setActiveBookmarkId] = useState(null); + + const toggleActive = (id: string) => { + setActiveBookmarkId(prev => (prev === id ? null : id)); + }; + + useEffect(() => { + const stored = JSON.parse(localStorage.getItem('bookmarks') || '[]'); + setBookmarks(stored); + }, []); + + const removeBookmark = (id: string) => { + const updated = bookmarks.filter(b => b.id !== id); + localStorage.setItem('bookmarks', JSON.stringify(updated)); + setBookmarks(updated); + }; + + const filteredBookmarks = bookmarks.filter(bookmark => { + if (activeTab === 'modules') return !bookmark.id.includes('topic') && !bookmark.id.includes('video'); + if (activeTab === 'topics') return bookmark.id.includes('topic'); + if (activeTab === 'videos') return bookmark.id.includes('video'); + return true; + }); + + return ( +
+

Your Bookmarks

+ +
+ + + +
+ + {filteredBookmarks.length === 0 ? ( +

+ No {activeTab} bookmarked yet +

+ ) : ( +
+ {filteredBookmarks.map((item) => ( +
+
+
+ +

+ {item.title} +

+ +

+ {item.subject} {item.module && `• Module ${item.module}`} +

+ {activeTab === 'videos' && ( +

Video

+ )} +
+
+ {activeTab === 'videos' && ( + toggleActive(item.id)} + className={`w-4 h-4 text-gray-500 dark:text-gray-400 transition-transform duration-200 cursor-pointer ${ + activeBookmarkId === item.id ? 'rotate-180' : '' + }`} + /> + )} + +
+
+ + {activeTab === 'videos' && activeBookmarkId === item.id && item.url && ( +
+
+ +
+
+ )} +
+ ))} +
+ )} +
+ ); +} \ No newline at end of file diff --git a/notes-aid/src/app/globals.css b/notes-aid/src/app/globals.css index 25ad212..3e59dea 100644 --- a/notes-aid/src/app/globals.css +++ b/notes-aid/src/app/globals.css @@ -7,8 +7,6 @@ layer(base); @custom-variant dark (&:is(.dark *)); - - .text-gray { color: #C2C0B6; } @@ -28,3 +26,4 @@ code { } /* this is a test */ + diff --git a/notes-aid/src/components/BookmarkButton.tsx b/notes-aid/src/components/BookmarkButton.tsx new file mode 100644 index 0000000..2394dcd --- /dev/null +++ b/notes-aid/src/components/BookmarkButton.tsx @@ -0,0 +1,57 @@ +"use client"; +import { useState, useEffect } from 'react'; + +interface BookmarkItem { + id: string; + title: string; + type: 'module' | 'topic' | 'video'; + subject: string; + module?: number; + topics?: string; + url?: string; + state?: { + selectedsubject: string; + selectedmodule: number; + } +} + +export const BookmarkButton = ({ item }: { item: BookmarkItem }) => { + const [isBookmarked, setIsBookmarked] = useState(false); + + useEffect(() => { + const bookmarks = getBookmarks(); + setIsBookmarked(bookmarks.some(b => b.id === item.id)); + }, [item.id]); + + const toggleBookmark = () => { + const bookmarks = getBookmarks(); + const updatedBookmarks = isBookmarked + ? bookmarks.filter(b => b.id !== item.id) + : [...bookmarks, item]; // item includes url for videos + + localStorage.setItem('bookmarks', JSON.stringify(updatedBookmarks)); + setIsBookmarked(!isBookmarked); + }; + + return ( + + ); +}; + +const getBookmarks = (): BookmarkItem[] => { + try { + return JSON.parse(localStorage.getItem('bookmarks') || '[]'); + } catch { + return []; + } +}; diff --git a/notes-aid/src/components/ModuleCard.tsx b/notes-aid/src/components/ModuleCard.tsx index 1735597..ee669c6 100644 --- a/notes-aid/src/components/ModuleCard.tsx +++ b/notes-aid/src/components/ModuleCard.tsx @@ -2,23 +2,28 @@ import React from "react"; // import { useState } from "react"; import { ChevronDown } from "lucide-react"; import ProgressBar from "./ProgressBar"; +import { BookmarkButton } from "./BookmarkButton"; interface ModuleCardProps { module: number; + subjectName: string; topics: number; isActive: boolean; onClick: () => void; numberOfVideos: number; numberOfVideosCompleted: number; + currentSubject: string; } const ModuleCard: React.FC = ({ module, + subjectName, topics, isActive, onClick, numberOfVideos, - numberOfVideosCompleted + numberOfVideosCompleted, + currentSubject }) => { // const total = 100; // const [done, setdone] = useState(20); @@ -66,7 +71,7 @@ const ModuleCard: React.FC = ({ } `} > -
+

@@ -76,11 +81,28 @@ const ModuleCard: React.FC = ({ {topics} topics

+ + +
+ +
diff --git a/notes-aid/src/components/Navbar.tsx b/notes-aid/src/components/Navbar.tsx index 140ba69..8ca8ec5 100644 --- a/notes-aid/src/components/Navbar.tsx +++ b/notes-aid/src/components/Navbar.tsx @@ -1,7 +1,7 @@ "use client"; import { useState, useEffect, useRef } from "react"; import Link from "next/link"; -import { NotebookPen, Bell, X, Download } from "lucide-react"; +import { NotebookPen, Bell, X, Download, BookmarkIcon } from "lucide-react"; import ThemeSwitcher from "./ThemeChanger"; interface Notification { @@ -413,7 +413,11 @@ const Navbar = () => { )}
+ + + +
diff --git a/notes-aid/src/components/ThemeChanger.tsx b/notes-aid/src/components/ThemeChanger.tsx index 97a5fd6..ed23fa9 100644 --- a/notes-aid/src/components/ThemeChanger.tsx +++ b/notes-aid/src/components/ThemeChanger.tsx @@ -48,7 +48,7 @@ const ThemeSwitcher = () => { } else { document.documentElement.setAttribute("data-theme", currentTheme); } - }, []); + }, [currentTheme]); const changeTheme = (theme: string) => { setCurrentTheme(theme); diff --git a/notes-aid/src/components/TopicList.tsx b/notes-aid/src/components/TopicList.tsx index 1647ab2..795d4c6 100644 --- a/notes-aid/src/components/TopicList.tsx +++ b/notes-aid/src/components/TopicList.tsx @@ -2,6 +2,7 @@ import React, { useEffect, useState } from "react"; import { BookOpen, ChevronDown } from "lucide-react"; import VideoAccordion from "./VideoAccordion"; import ProgressBar from "./ProgressBar"; +import { BookmarkButton } from "./BookmarkButton"; interface Topic { title: string; description: string; @@ -150,11 +151,21 @@ const TopicList: React.FC = ({

{topic.title}

- + + + }`} + /> +

{topic.description} diff --git a/notes-aid/src/components/Video.tsx b/notes-aid/src/components/Video.tsx index dcc00f5..69f6f35 100644 --- a/notes-aid/src/components/Video.tsx +++ b/notes-aid/src/components/Video.tsx @@ -2,6 +2,7 @@ import React from "react"; import { RefObject, Dispatch, SetStateAction } from "react"; import { CheckSquare, Square, ChevronDown } from "lucide-react"; +import { BookmarkButton } from "./BookmarkButton"; interface VideoProps { checked: boolean; @@ -71,11 +72,24 @@ function Video({ {video.title} +

+ +
Date: Tue, 12 Aug 2025 10:24:37 +0530 Subject: [PATCH 7/7] fix: add vercel.json --- leaderboard_backend/routes/leaderboard.js | 6 ++++-- leaderboard_backend/server.js | 12 +++++++++--- leaderboard_backend/vercel.json | 15 +++++++++++++++ 3 files changed, 28 insertions(+), 5 deletions(-) create mode 100644 leaderboard_backend/vercel.json diff --git a/leaderboard_backend/routes/leaderboard.js b/leaderboard_backend/routes/leaderboard.js index 2bb908a..c6d3df3 100644 --- a/leaderboard_backend/routes/leaderboard.js +++ b/leaderboard_backend/routes/leaderboard.js @@ -16,8 +16,6 @@ router.get("/", verifyGoogleToken, getOverallLeaderboard); // GET /api/v1/leaderboard/current - Current semester leaderboard for specific admission year router.get("/current", verifyGoogleToken, getCurrentSemesterLeaderboard); -// GET /api/v1/leaderboard/:semester - Semester-specific leaderboard for specific admission year -router.get("/:semester", verifyGoogleToken, getSemesterLeaderboard); // Helper routes for available data // GET /api/v1/leaderboard/meta/years - Get available admission years @@ -26,4 +24,8 @@ router.get("/meta/years", verifyGoogleToken, getAvailableYears); // GET /api/v1/leaderboard/meta/semesters - Get available semesters for specific admission year router.get("/meta/semesters", verifyGoogleToken, getAvailableSemesters); +// GET /api/v1/leaderboard/:semester - Semester-specific leaderboard for specific admission year +router.get("/:semester", verifyGoogleToken, getSemesterLeaderboard); + + export default router; diff --git a/leaderboard_backend/server.js b/leaderboard_backend/server.js index 29f4913..41a1b82 100644 --- a/leaderboard_backend/server.js +++ b/leaderboard_backend/server.js @@ -14,12 +14,18 @@ connectToDb(); // Middlewares app.use(cors({ - origin: ['http://localhost:3000', 'https://notes-aid.minavkaria.tech', 'https://notes-aid-git-fork-om-thanage-main-minavkarias-projects.vercel.app', 'https://notes-d0qrfpgl6-minavkarias-projects.vercel.app' -], + origin: [ + 'http://localhost:3000', + 'https://notes-aid.minavkaria.tech', + 'https://notes-aid-git-fork-om-thanage-main-minavkarias-projects.vercel.app', + 'https://notes-d0qrfpgl6-minavkarias-projects.vercel.app' + ], credentials: true, - methods: ['GET', 'OPTIONS'], + methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], allowedHeaders: ['Content-Type', 'Authorization'], })); +app.options('*', cors()); + app.use(express.json()); app.use(express.urlencoded({ extended: true })); app.use(cookieParser()); diff --git a/leaderboard_backend/vercel.json b/leaderboard_backend/vercel.json new file mode 100644 index 0000000..b633a24 --- /dev/null +++ b/leaderboard_backend/vercel.json @@ -0,0 +1,15 @@ +{ + "version": 2, + "builds": [ + { + "src": "server.js", + "use": "@vercel/node" + } + ], + "routes": [ + { + "src": "/(.*)", + "dest": "server.js" + } + ] +}