From a3e430cb677b8b178ffbe12d78eb9cce97a68dd9 Mon Sep 17 00:00:00 2001 From: ectasy663 Date: Wed, 23 Sep 2026 12:11:44 +0530 Subject: [PATCH] feat: Add JWT authentication to backend, configure frontend API client, fix streak layout --- DESIGN_SYSTEM.md | 2 +- backend/main.py | 112 +++++++++++++------- src/app/admin/coupons/page.tsx | 7 +- src/app/admin/settings/page.tsx | 5 +- src/app/admin/students/[studentId]/page.tsx | 7 +- src/app/admin/students/page.tsx | 3 +- src/app/admin/tests/page.tsx | 9 +- src/app/api/og/dashboard/route.tsx | 16 +-- src/app/dashboard/ai-coach/cbt-dpp/page.tsx | 5 +- src/app/dashboard/ai-coach/page.tsx | 3 +- src/app/dashboard/cbt/page.tsx | 5 +- src/app/dashboard/dpp/page.tsx | 5 +- src/app/dashboard/layout.tsx | 2 +- src/app/dashboard/page.tsx | 3 +- src/app/dashboard/settings/page.tsx | 5 +- src/app/dashboard/streaks/page.tsx | 3 +- src/app/login/page.tsx | 11 +- src/app/signup/page.tsx | 7 +- src/components/AI/AITutorChat.tsx | 3 +- src/components/AI/CollegePredictorCard.tsx | 3 +- src/components/AI/ReadinessGauge.tsx | 3 +- src/components/AI/ReferralCard.tsx | 3 +- src/components/AI/StudyPlanCard.tsx | 3 +- src/components/AI/TutorChat.tsx | 3 +- src/hooks/useActiveUser.ts | 72 +++++++------ src/lib/apiClient.ts | 27 +++++ src/store/cbtStore.ts | 3 +- 27 files changed, 210 insertions(+), 120 deletions(-) create mode 100644 src/lib/apiClient.ts diff --git a/DESIGN_SYSTEM.md b/DESIGN_SYSTEM.md index d61e58a..8861686 100644 --- a/DESIGN_SYSTEM.md +++ b/DESIGN_SYSTEM.md @@ -87,7 +87,7 @@ All fundamental colors are defined as CSS variables in `src/app/globals.css` and | **Cyan (`--cyan`)** | `rgb(12, 96, 96)` (Deep AI Teal) | `#00F0FF` (Cyber Cyan) | `var(--cyan)`, `text-[var(--cyan-ai)]`, `bg-[var(--cyan-ai-10)]` | `var(--cyan)`, `text-[#00F0FF]`, `bg-[#00F0FF]/10` | AI Coach, Tutor Chat, PrepPilot AI | | **Light Cyan (`--lightcyan`)** | `#E0FFFF` (Unchanged) | `#E0FFFF` (Unchanged) | W3C Standard `lightcyan` | W3C Standard `lightcyan` | Standard CSS compatibility | | **Dark Cyan (`--darkcyan`)** | `#008B8B` (Unchanged) | `#008B8B` (Unchanged) | W3C Standard `darkcyan` | W3C Standard `darkcyan` | Standard CSS compatibility | -| **Neon Lime** | Subtle green pill badges | `#D1FF4D` | Subtle green pill badges | `bg-primary`, `text-primary`, `shadow-[0_0_15px_-5px_#D1FF4D]` | Dark mode hero highlights & primary actions | +| **Neon Lime** | Subtle green pill badges | `#D1FF4D` | Subtle green pill badges | `bg-primary`, `text-primary`, `shadow-1[0_0_15px_-5px_#D1FF4D]` | Dark mode hero highlights & primary actions | | **Amber Gold** | `#EAB308` | `#EAB308` | `text-yellow-700`, `bg-yellow-50`, `border-yellow-300/60` | `text-yellow-400`, `bg-yellow-500/10`, `shadow-[0_0_12px_-4px_rgba(234,179,8,0.2)]` | Pro Tier, CBT Marked for Review questions | | **Emerald Green** | `#22C55E` | `#22C55E` | `bg-green-500 text-white` | `bg-green-500 text-white shadow-sm` | CBT Answered questions, Success toasts | | **Crimson Red** | `#EF4444` | `#EF4444` | `bg-red-500 text-white`, `text-red-600` | `bg-red-500 text-white`, `text-red-400` | CBT Not Answered, Error banners, Destructive | diff --git a/backend/main.py b/backend/main.py index 1346c7d..93da6d8 100644 --- a/backend/main.py +++ b/backend/main.py @@ -72,6 +72,42 @@ app.add_middleware( def health_check(): return {"status": "operational", "engine": "FastAPI Async", "environment": "local_dev"} +from fastapi.security import OAuth2PasswordBearer +import jwt +from typing import List, Dict, Any, Optional + +SECRET_KEY = os.getenv("JWT_SECRET_KEY", "super-secret-pragativeer-key") +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_DAYS = 30 + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="api/auth/login") + +def create_access_token(data: dict): + to_encode = data.copy() + expire = datetime.utcnow() + timedelta(days=ACCESS_TOKEN_EXPIRE_DAYS) + to_encode.update({"exp": expire}) + encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + return encoded_jwt + +def get_current_user(token: str = Depends(oauth2_scheme), session: Session = Depends(get_session)): + try: + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + user_id_str: str = payload.get("sub") + if user_id_str is None: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token") + except jwt.PyJWTError: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token") + + try: + user_id = uuid.UUID(user_id_str) + except ValueError: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token subject") + + user = session.get(models.User, user_id) + if not user: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="User not found") + return user + # --- ACCESS CONTROL DEPENDENCY --- def get_effective_tier(user: models.User, session: Session) -> models.TierEnum: @@ -204,7 +240,8 @@ def register(payload: RegisterPayload, session: Session = Depends(get_session)): # Send welcome email in background (non-blocking) send_welcome_email(payload.email, payload.name) - return {"success": True, "message": "User registered successfully", "trial_end": trial_end.isoformat() if trial_end else None} + access_token = create_access_token(data={"sub": str(new_user.id)}) + return {"success": True, "message": "User registered successfully", "token": access_token, "user_id": str(new_user.id), "trial_end": trial_end.isoformat() if trial_end else None} @app.post("/api/auth/login", tags=["Authentication"]) def login(payload: LoginPayload, session: Session = Depends(get_session)): @@ -222,7 +259,8 @@ def login(payload: LoginPayload, session: Session = Depends(get_session)): (models.User.email == "demo@pragativeer.com") | (models.User.name == "Neha Verma") )).first() token_id = str(demo_user.id) if demo_user else NEHA_UUID - return {"success": True, "role": "user", "token": token_id} + access_token = create_access_token(data={"sub": token_id}) + return {"success": True, "role": "user", "token": access_token, "user_id": token_id} if payload.email == "admin@pragativeer.com" and payload.password == "Pv9#kL2$mQ8!vX4@": return {"success": True, "role": "admin", "token": "admin"} @@ -236,20 +274,21 @@ def login(payload: LoginPayload, session: Session = Depends(get_session)): user.password_hash = bcrypt.hashpw(payload.password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8') session.add(user) session.commit() - return {"success": True, "role": "admin" if (user.email and "admin" in user.email) else "user", "token": str(user.id)} + access_token = create_access_token(data={"sub": str(user.id)}) + return {"success": True, "role": "admin" if (user.email and "admin" in user.email) else "user", "token": access_token, "user_id": str(user.id)} else: raise HTTPException(status_code=401, detail="Invalid email or password") if not bcrypt.checkpw(payload.password.encode('utf-8'), user.password_hash.encode('utf-8')): raise HTTPException(status_code=401, detail="Invalid email or password") - # Real application would issue a JWT here - return {"success": True, "role": "admin" if (user.email and "admin" in user.email) else "user", "token": str(user.id)} + access_token = create_access_token(data={"sub": str(user.id)}) + return {"success": True, "role": "admin" if (user.email and "admin" in user.email) else "user", "token": access_token, "user_id": str(user.id)} # --- CBT ENGINE --- @app.get("/api/tests", response_model=List[models.Test], tags=["CBT Engine"]) -def list_active_tests(exam_pattern: Optional[str] = None, type: Optional[str] = None, session: Session = Depends(get_session)): +def list_active_tests(exam_pattern: Optional[str] = None, type: Optional[str] = None, session: Session = Depends(get_session), current_user: models.User = Depends(get_current_user)): query = select(models.Test).where(models.Test.is_active == True) if exam_pattern: query = query.where(models.Test.exam_pattern == exam_pattern) @@ -260,14 +299,14 @@ def list_active_tests(exam_pattern: Optional[str] = None, type: Optional[str] = return session.exec(query).all() @app.get("/api/tests/{test_id}", response_model=models.Test, tags=["CBT Engine"]) -def get_test_by_id(test_id: UUID, user_id: Optional[UUID] = None, session: Session = Depends(get_session)): +def get_test_by_id(test_id: UUID, user_id: Optional[UUID] = None, session: Session = Depends(get_session), current_user: models.User = Depends(get_current_user)): test = session.get(models.Test, test_id) if not test: raise HTTPException(status_code=404, detail="Requested examination structure not found.") return test @app.get("/api/tests/{test_id}/questions", response_model=List[models.Question], tags=["CBT Engine"]) -def get_questions_by_test_id(test_id: UUID, session: Session = Depends(get_session)): +def get_questions_by_test_id(test_id: UUID, session: Session = Depends(get_session), current_user: models.User = Depends(get_current_user)): test = session.get(models.Test, test_id) if not test: raise HTTPException(status_code=404, detail="Requested examination structure not found.") @@ -301,14 +340,14 @@ def get_questions_by_test_id(test_id: UUID, session: Session = Depends(get_sessi return questions @app.get("/api/questions/{question_id}", response_model=models.Question, tags=["CBT Engine"]) -def get_question_by_id(question_id: UUID, session: Session = Depends(get_session)): +def get_question_by_id(question_id: UUID, session: Session = Depends(get_session), current_user: models.User = Depends(get_current_user)): question = session.get(models.Question, question_id) if not question: raise HTTPException(status_code=404, detail="Requested question not found.") return question @app.post("/api/attempts/submit", response_model=models.Attempt, tags=["CBT Engine"]) -def submit_test_attempt(payload: Dict[str, Any], session: Session = Depends(get_session)): +def submit_test_attempt(payload: Dict[str, Any], session: Session = Depends(get_session), current_user: models.User = Depends(get_current_user)): user_id = payload.get("user_id") source_type_raw = payload.get("source_type", "Test") source_id = payload.get("source_id") @@ -442,7 +481,7 @@ def submit_test_attempt(payload: Dict[str, Any], session: Session = Depends(get_ # --- AI COACH SPACED REPETITION ENDPOINTS --- @app.get("/api/mistakes/today", tags=["AI Coach"]) -def get_todays_mistakes(user_id: UUID, session: Session = Depends(get_session)): +def get_todays_mistakes(user_id: UUID, session: Session = Depends(get_session), current_user: models.User = Depends(get_current_user)): now = datetime.utcnow() statement = select(models.Mistake, models.Question).join( models.Question, models.Mistake.question_id == models.Question.id @@ -458,7 +497,7 @@ def get_todays_mistakes(user_id: UUID, session: Session = Depends(get_session)): return response_data @app.post("/api/mistakes/review", tags=["AI Coach"]) -def submit_mistake_review(payload: Dict[str, Any], session: Session = Depends(get_session)): +def submit_mistake_review(payload: Dict[str, Any], session: Session = Depends(get_session), current_user: models.User = Depends(get_current_user)): mistake_id = payload.get("mistake_id") selected_answer = payload.get("selected_answer") @@ -505,14 +544,14 @@ def submit_mistake_review(payload: Dict[str, Any], session: Session = Depends(ge # --- PARENT SUBSYSTEM --- @app.post("/api/parent/register", response_model=models.Parent, tags=["Parent Subsystem"]) -def register_parent(parent_data: models.Parent, session: Session = Depends(get_session)): +def register_parent(parent_data: models.Parent, session: Session = Depends(get_session), current_user: models.User = Depends(get_current_user)): session.add(parent_data) session.commit() session.refresh(parent_data) return parent_data @app.post("/api/parent/link-student", tags=["Parent Subsystem"]) -def link_student_to_parent(parent_id: UUID, student_phone_or_email: str, session: Session = Depends(get_session)): +def link_student_to_parent(parent_id: UUID, student_phone_or_email: str, session: Session = Depends(get_session), current_user: models.User = Depends(get_current_user)): parent = session.get(models.Parent, parent_id) if not parent: raise HTTPException(status_code=404, detail="Parent account context not found.") @@ -537,7 +576,7 @@ def link_student_to_parent(parent_id: UUID, student_phone_or_email: str, session return {"message": "Student successfully linked to parent context.", "linked_student_id": student.id} @app.get("/api/parent/{parent_id}/dashboard/{student_id}", tags=["Parent Subsystem"]) -def get_parent_dashboard_metrics(parent_id: UUID, student_id: UUID, session: Session = Depends(get_session)): +def get_parent_dashboard_metrics(parent_id: UUID, student_id: UUID, session: Session = Depends(get_session), current_user: models.User = Depends(get_current_user)): parent = session.get(models.Parent, parent_id) if not parent: raise HTTPException(status_code=404, detail="Unauthorized parent profile access attempt.") @@ -628,7 +667,7 @@ def get_exam_readiness(user_id: UUID, session: Session = Depends(get_session), c return readiness_data @app.get("/api/growth/referral/{user_id}", tags=["Viral Growth"]) -def get_referral_dashboard(user_id: UUID, session: Session = Depends(get_session)): +def get_referral_dashboard(user_id: UUID, session: Session = Depends(get_session), current_user: models.User = Depends(get_current_user)): user = session.get(models.User, user_id) if not user: raise HTTPException(status_code=404, detail="User not found.") @@ -752,10 +791,11 @@ def switch_user_endpoint(id: str, session: Session = Depends(get_session)): user = ensure_demo_user(uid, session) or session.get(models.User, uid) if not user: raise HTTPException(status_code=404, detail="User not found") - return {"status": "success", "user_id": str(user.id), "name": user.name} + access_token = create_access_token(data={"sub": str(user.id)}) + return {"status": "success", "user_id": str(user.id), "name": user.name, "token": access_token} @app.get("/api/users/{user_id}", response_model=models.User, tags=["Users"]) -def get_user_profile(user_id: UUID, session: Session = Depends(get_session)): +def get_user_profile(user_id: UUID, session: Session = Depends(get_session), current_user: models.User = Depends(get_current_user)): user = ensure_demo_user(user_id, session) or session.get(models.User, user_id) if not user: raise HTTPException(status_code=404, detail="User not found") @@ -797,7 +837,7 @@ class UserUpdate(BaseModel): state: Optional[str] = None @app.put("/api/users/{user_id}", response_model=models.User, tags=["Users"]) -def update_user_profile(user_id: UUID, payload: UserUpdate, session: Session = Depends(get_session)): +def update_user_profile(user_id: UUID, payload: UserUpdate, session: Session = Depends(get_session), current_user: models.User = Depends(get_current_user)): user = session.get(models.User, user_id) if not user: raise HTTPException(status_code=404, detail="User not found") @@ -817,7 +857,7 @@ class UserSessionUpdate(BaseModel): session_token: str @app.post("/api/users/{user_id}/session", response_model=models.User, tags=["Users"]) -def update_user_session(user_id: UUID, payload: UserSessionUpdate, session: Session = Depends(get_session)): +def update_user_session(user_id: UUID, payload: UserSessionUpdate, session: Session = Depends(get_session), current_user: models.User = Depends(get_current_user)): user = ensure_demo_user(user_id, session) or session.get(models.User, user_id) if not user: raise HTTPException(status_code=404, detail="User not found") @@ -848,7 +888,7 @@ async def upload_user_avatar(user_id: UUID, file: UploadFile = File(...), sessio return {"url": public_url} @app.get("/api/users/{user_id}/attempts", tags=["Users"]) -def get_user_attempts(user_id: UUID, session: Session = Depends(get_session)): +def get_user_attempts(user_id: UUID, session: Session = Depends(get_session), current_user: models.User = Depends(get_current_user)): query = select(models.Attempt).where(models.Attempt.user_id == user_id) return session.exec(query).all() @@ -860,7 +900,7 @@ DEMO_USER_UUIDS = { } @app.get("/api/users/{user_id}/stats", tags=["Users"]) -def get_user_stats(user_id: UUID, session: Session = Depends(get_session)): +def get_user_stats(user_id: UUID, session: Session = Depends(get_session), current_user: models.User = Depends(get_current_user)): """Return real-time performance stats for the user from the database.""" user = ensure_demo_user(user_id, session) or session.get(models.User, user_id) if not user: @@ -928,7 +968,7 @@ def get_user_stats(user_id: UUID, session: Session = Depends(get_session)): def get_leaderboard( tab: str = "all-india", user_id: Optional[UUID] = None, - session: Session = Depends(get_session) + session: Session = Depends(get_session), current_user: models.User = Depends(get_current_user) ): """Return leaderboard rankings calculated from real users and attempts in DB.""" # Top benchmark peer students @@ -1013,7 +1053,7 @@ def get_leaderboard( # --- ADMIN ROUTES --- @app.get("/api/admin/settings", tags=["Admin"]) -def get_system_settings(session: Session = Depends(get_session)): +def get_system_settings(session: Session = Depends(get_session), current_user: models.User = Depends(get_current_user)): settings = session.get(models.SystemSettings, "global") if not settings: settings = models.SystemSettings() @@ -1030,7 +1070,7 @@ class SettingsUpdate(BaseModel): trial_tier: Optional[models.TierEnum] = None @app.put("/api/admin/settings", tags=["Admin"]) -def update_system_settings(payload: SettingsUpdate, session: Session = Depends(get_session)): +def update_system_settings(payload: SettingsUpdate, session: Session = Depends(get_session), current_user: models.User = Depends(get_current_user)): settings = session.get(models.SystemSettings, "global") if not settings: settings = models.SystemSettings() @@ -1048,7 +1088,7 @@ def update_system_settings(payload: SettingsUpdate, session: Session = Depends(g return settings @app.get("/api/admin/tests", tags=["Admin"]) -def get_admin_tests(session: Session = Depends(get_session)): +def get_admin_tests(session: Session = Depends(get_session), current_user: models.User = Depends(get_current_user)): tests = session.exec(select(models.Test)).all() return tests @@ -1133,7 +1173,7 @@ async def upload_admin_test( @app.post("/api/admin/tests/create_manual", tags=["Admin"]) def create_manual_test( data: ManualTestCreate, - session: Session = Depends(get_session) + session: Session = Depends(get_session), current_user: models.User = Depends(get_current_user) ): import uuid # 1. Create the test object @@ -1187,7 +1227,7 @@ def create_manual_test( return {"message": "Test created successfully", "test_id": test_obj.id} @app.put("/api/admin/tests/{test_id}/toggle", tags=["Admin"]) -def toggle_test_status(test_id: UUID, session: Session = Depends(get_session)): +def toggle_test_status(test_id: UUID, session: Session = Depends(get_session), current_user: models.User = Depends(get_current_user)): test_obj = session.get(models.Test, test_id) if not test_obj: raise HTTPException(status_code=404, detail="Test not found") @@ -1199,7 +1239,7 @@ def toggle_test_status(test_id: UUID, session: Session = Depends(get_session)): return {"status": "success", "is_active": test_obj.is_active} @app.delete("/api/admin/tests/{test_id}", tags=["Admin"]) -def delete_test(test_id: UUID, session: Session = Depends(get_session)): +def delete_test(test_id: UUID, session: Session = Depends(get_session), current_user: models.User = Depends(get_current_user)): from sqlmodel import select test_obj = session.get(models.Test, test_id) if not test_obj: @@ -1280,12 +1320,12 @@ class CouponUpdate(BaseModel): expires_at: Optional[datetime] = None @app.get("/api/admin/coupons", tags=["Admin"]) -def get_admin_coupons(session: Session = Depends(get_session)): +def get_admin_coupons(session: Session = Depends(get_session), current_user: models.User = Depends(get_current_user)): coupons = session.exec(select(models.Coupon)).all() return coupons @app.post("/api/admin/coupons", tags=["Admin"]) -def create_coupon(payload: CouponCreate, session: Session = Depends(get_session)): +def create_coupon(payload: CouponCreate, session: Session = Depends(get_session), current_user: models.User = Depends(get_current_user)): coupon = models.Coupon(**payload.dict()) session.add(coupon) session.commit() @@ -1293,7 +1333,7 @@ def create_coupon(payload: CouponCreate, session: Session = Depends(get_session) return coupon @app.put("/api/admin/coupons/{coupon_id}", tags=["Admin"]) -def update_coupon(coupon_id: UUID, payload: CouponUpdate, session: Session = Depends(get_session)): +def update_coupon(coupon_id: UUID, payload: CouponUpdate, session: Session = Depends(get_session), current_user: models.User = Depends(get_current_user)): coupon = session.get(models.Coupon, coupon_id) if not coupon: raise HTTPException(status_code=404, detail="Coupon not found") @@ -1308,7 +1348,7 @@ def update_coupon(coupon_id: UUID, payload: CouponUpdate, session: Session = Dep return coupon @app.delete("/api/admin/coupons/{coupon_id}", tags=["Admin"]) -def delete_coupon(coupon_id: UUID, session: Session = Depends(get_session)): +def delete_coupon(coupon_id: UUID, session: Session = Depends(get_session), current_user: models.User = Depends(get_current_user)): coupon = session.get(models.Coupon, coupon_id) if not coupon: raise HTTPException(status_code=404, detail="Coupon not found") @@ -1355,7 +1395,7 @@ class AdminUserPatch(BaseModel): state: Optional[str] = None @app.get("/api/admin/students", tags=["Admin"]) -def list_all_students(session: Session = Depends(get_session)): +def list_all_students(session: Session = Depends(get_session), current_user: models.User = Depends(get_current_user)): """Return all users with basic stats for the admin student directory.""" users = session.exec(select(models.User)).all() result = [] @@ -1393,7 +1433,7 @@ def list_all_students(session: Session = Depends(get_session)): @app.get("/api/admin/students/{user_id}", tags=["Admin"]) -def get_student_detail(user_id: UUID, session: Session = Depends(get_session)): +def get_student_detail(user_id: UUID, session: Session = Depends(get_session), current_user: models.User = Depends(get_current_user)): """Full student profile for admin detail view.""" user = session.get(models.User, user_id) if not user: @@ -1490,7 +1530,7 @@ def get_student_detail(user_id: UUID, session: Session = Depends(get_session)): @app.patch("/api/admin/students/{user_id}", tags=["Admin"]) -def patch_student(user_id: UUID, payload: AdminUserPatch, session: Session = Depends(get_session)): +def patch_student(user_id: UUID, payload: AdminUserPatch, session: Session = Depends(get_session), current_user: models.User = Depends(get_current_user)): """Admin update: tier, name, streak, exam, location.""" user = session.get(models.User, user_id) if not user: diff --git a/src/app/admin/coupons/page.tsx b/src/app/admin/coupons/page.tsx index cc9626c..e5376bb 100644 --- a/src/app/admin/coupons/page.tsx +++ b/src/app/admin/coupons/page.tsx @@ -1,5 +1,6 @@ "use client" +import { fetchWithAuth } from "@/lib/apiClient"; import { useState, useEffect } from "react" import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card" import { Button } from "@/components/ui/button" @@ -25,7 +26,7 @@ export default function CouponsAdmin() { const fetchCoupons = async () => { try { - const res = await fetch('/api/admin/coupons') + const res = await fetchWithAuth('/api/admin/coupons') const data = await res.json() setCoupons(data) } catch (e) { @@ -39,7 +40,7 @@ export default function CouponsAdmin() { if (!newCoupon.code) return setSubmitting(true) try { - await fetch('/api/admin/coupons', { + await fetchWithAuth('/api/admin/coupons', { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(newCoupon) @@ -56,7 +57,7 @@ export default function CouponsAdmin() { const handleDelete = async (id: string) => { if (!confirm("Delete this coupon?")) return try { - await fetch(`/api/admin/coupons/${id}`, { method: "DELETE" }) + await fetchWithAuth(`/api/admin/coupons/${id}`, { method: "DELETE" }) await fetchCoupons() } catch (e) { console.error(e) diff --git a/src/app/admin/settings/page.tsx b/src/app/admin/settings/page.tsx index c8913eb..0c0fb4a 100644 --- a/src/app/admin/settings/page.tsx +++ b/src/app/admin/settings/page.tsx @@ -1,5 +1,6 @@ "use client"; +import { fetchWithAuth } from "@/lib/apiClient"; import { useState, useEffect } from "react"; import { Button } from "@/components/ui/button" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" @@ -15,7 +16,7 @@ export default function SettingsPage() { const [loading, setLoading] = useState(true); useEffect(() => { - fetch("/api/admin/settings") + fetchWithAuth("/api/admin/settings") .then(res => res.json()) .then(data => { if(data && !data.detail) { @@ -32,7 +33,7 @@ export default function SettingsPage() { const saveSettings = async (section: string, overrides: Partial = {}) => { try { const payload = { ...settings, ...overrides }; - const res = await fetch("/api/admin/settings", { + const res = await fetchWithAuth("/api/admin/settings", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) diff --git a/src/app/admin/students/[studentId]/page.tsx b/src/app/admin/students/[studentId]/page.tsx index d9c2329..b4928dd 100644 --- a/src/app/admin/students/[studentId]/page.tsx +++ b/src/app/admin/students/[studentId]/page.tsx @@ -1,5 +1,6 @@ "use client" +import { fetchWithAuth } from "@/lib/apiClient"; import { useEffect, useState } from "react" import { useParams, useRouter } from "next/navigation" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" @@ -46,7 +47,7 @@ export default function StudentKundliPage() { const [editExam, setEditExam] = useState("") useEffect(() => { - fetch(`/api/admin/students/${studentId}`) + fetchWithAuth(`/api/admin/students/${studentId}`) .then(r => { if (!r.ok) throw new Error("Not found"); return r.json() }) .then(d => { setData(d) @@ -62,7 +63,7 @@ export default function StudentKundliPage() { const handleSave = async () => { setSaving(true) try { - const res = await fetch(`/api/admin/students/${studentId}`, { + const res = await fetchWithAuth(`/api/admin/students/${studentId}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ @@ -74,7 +75,7 @@ export default function StudentKundliPage() { }) if (!res.ok) throw new Error("Failed to save") // Refresh data - const fresh = await fetch(`/api/admin/students/${studentId}`).then(r => r.json()) + const fresh = await fetchWithAuth(`/api/admin/students/${studentId}`).then(r => r.json()) setData(fresh) setEditing(false) } catch (e: any) { diff --git a/src/app/admin/students/page.tsx b/src/app/admin/students/page.tsx index 28c73e9..2b95604 100644 --- a/src/app/admin/students/page.tsx +++ b/src/app/admin/students/page.tsx @@ -1,5 +1,6 @@ "use client" +import { fetchWithAuth } from "@/lib/apiClient"; import { useEffect, useState } from "react" import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" @@ -25,7 +26,7 @@ export default function StudentsPage() { const [search, setSearch] = useState("") useEffect(() => { - fetch("/api/admin/students") + fetchWithAuth("/api/admin/students") .then(r => r.json()) .then(data => setStudents(data)) .catch(console.error) diff --git a/src/app/admin/tests/page.tsx b/src/app/admin/tests/page.tsx index 67e89bd..2b4c22a 100644 --- a/src/app/admin/tests/page.tsx +++ b/src/app/admin/tests/page.tsx @@ -1,5 +1,6 @@ "use client" +import { fetchWithAuth } from "@/lib/apiClient"; import { useState, useEffect, useRef } from "react" import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card" import { Button } from "@/components/ui/button" @@ -55,7 +56,7 @@ export default function AdminTestsPage() { const fetchTests = async () => { try { - const res = await fetch("/api/admin/tests") + const res = await fetchWithAuth("/api/admin/tests") if (res.ok) { const data = await res.json() setTests(data) @@ -74,7 +75,7 @@ export default function AdminTestsPage() { const handleToggle = async (testId: string) => { try { - const res = await fetch(`/api/admin/tests/${testId}/toggle`, { + const res = await fetchWithAuth(`/api/admin/tests/${testId}/toggle`, { method: "PUT" }) if (res.ok) { @@ -88,7 +89,7 @@ export default function AdminTestsPage() { const handleDelete = async (testId: string) => { if (!confirm("Are you sure you want to delete this test and all its questions? This action cannot be undone.")) return try { - const res = await fetch(`/api/admin/tests/${testId}`, { + const res = await fetchWithAuth(`/api/admin/tests/${testId}`, { method: "DELETE" }) if (res.ok) { @@ -191,7 +192,7 @@ export default function AdminTestsPage() { }; try { - const res = await fetch("/api/admin/tests/create_manual", { + const res = await fetchWithAuth("/api/admin/tests/create_manual", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) diff --git a/src/app/api/og/dashboard/route.tsx b/src/app/api/og/dashboard/route.tsx index 05808f5..741924c 100644 --- a/src/app/api/og/dashboard/route.tsx +++ b/src/app/api/og/dashboard/route.tsx @@ -121,20 +121,20 @@ export async function GET(req: NextRequest) {
-
{name}
-
Generated: {exportDate}
+
{name}
+
{`Generated: ${exportDate}`}
-
{`${target} Aspirant`}
-
•
-
{`${plan} Plan`}
+
{`${target} Aspirant`}
+
•
+
{`${plan} Plan`}
-
pragativeer.com
-
contact@pragativeer.com
-
+91 9079337961
+
pragativeer.com
+
contact@pragativeer.com
+
+91 9079337961
diff --git a/src/app/dashboard/ai-coach/cbt-dpp/page.tsx b/src/app/dashboard/ai-coach/cbt-dpp/page.tsx index 0ebf92e..2aff1a0 100644 --- a/src/app/dashboard/ai-coach/cbt-dpp/page.tsx +++ b/src/app/dashboard/ai-coach/cbt-dpp/page.tsx @@ -1,5 +1,6 @@ "use client" +import { fetchWithAuth } from "@/lib/apiClient"; import { Button } from "@/components/ui/button" import { Card, CardContent } from "@/components/ui/card" import { BrainCircuit, Play, CheckCircle2, Download, Filter, Lock, FileText, Eye, ShieldAlert, X, Sparkles, Share2, Maximize2, Minimize2 } from "lucide-react" @@ -25,14 +26,14 @@ export default function AIDailyPracticePage() { try { const pattern = user.target_exam || "JEE_Main"; // Also fetch from the correct Next.js proxied route instead of localhost:8000 - const res = await fetch(`/api/tests?type=DPS&exam_pattern=${pattern}`); + const res = await fetchWithAuth(`/api/tests?type=DPS&exam_pattern=${pattern}`); if (res.ok) { const data = await res.json() setDpps(data) } if (userId) { - const attemptsRes = await fetch(`/api/users/${userId}/attempts`); + const attemptsRes = await fetchWithAuth(`/api/users/${userId}/attempts`); if (attemptsRes.ok) { const attemptsData = await attemptsRes.json(); setAttempts(attemptsData); diff --git a/src/app/dashboard/ai-coach/page.tsx b/src/app/dashboard/ai-coach/page.tsx index 0c2200d..4520fa9 100644 --- a/src/app/dashboard/ai-coach/page.tsx +++ b/src/app/dashboard/ai-coach/page.tsx @@ -1,5 +1,6 @@ "use client"; +import { fetchWithAuth } from "@/lib/apiClient"; import { useEffect, useState } from "react" import { MistakeHeatmap } from "@/components/AI/MistakeHeatmap" import { StudyPlanCard } from "@/components/AI/StudyPlanCard" @@ -39,7 +40,7 @@ export default function AICoachDashboard() { useEffect(() => { if (!userLoading && userId) { - fetch(`/api/users/${userId}/stats`) + fetchWithAuth(`/api/users/${userId}/stats`) .then(res => res.ok ? res.json() : null) .then(data => setStats(data)) .catch(() => setStats(null)) diff --git a/src/app/dashboard/cbt/page.tsx b/src/app/dashboard/cbt/page.tsx index 87c702e..78d9aa1 100644 --- a/src/app/dashboard/cbt/page.tsx +++ b/src/app/dashboard/cbt/page.tsx @@ -1,5 +1,6 @@ "use client"; +import { fetchWithAuth } from "@/lib/apiClient"; import { useEffect, useState } from "react"; import Link from "next/link"; import { Button } from "@/components/ui/button"; @@ -37,7 +38,7 @@ export default function CBTPage() { async function loadUser() { try { - const userRes = await fetch(`/api/users/${userId}`); + const userRes = await fetchWithAuth(`/api/users/${userId}`); if (!userRes.ok) throw new Error("Failed to load user target exam"); const userData = await userRes.json(); const pattern = userData.target_exam; @@ -75,7 +76,7 @@ export default function CBTPage() { setExams(testsData); } - const attemptsRes = await fetch(`/api/users/${userId}/attempts`); + const attemptsRes = await fetchWithAuth(`/api/users/${userId}/attempts`); if (attemptsRes.ok) { const attemptsData = await attemptsRes.json(); setAttempts(attemptsData); diff --git a/src/app/dashboard/dpp/page.tsx b/src/app/dashboard/dpp/page.tsx index 0883a31..aa6999a 100644 --- a/src/app/dashboard/dpp/page.tsx +++ b/src/app/dashboard/dpp/page.tsx @@ -1,5 +1,6 @@ "use client" +import { fetchWithAuth } from "@/lib/apiClient"; import { Button } from "@/components/ui/button" import { Card, CardContent } from "@/components/ui/card" import { BrainCircuit, Play, CheckCircle2, Download, Filter, Lock, FileText, Eye, ShieldAlert, X, Sparkles, Share2, Maximize2, Minimize2 } from "lucide-react" @@ -25,14 +26,14 @@ export default function DPPPage() { try { const pattern = user.target_exam || "JEE_Main"; // Also fetch from the correct Next.js proxied route instead of localhost:8000 - const res = await fetch(`/api/tests?type=DPS&exam_pattern=${pattern}`); + const res = await fetchWithAuth(`/api/tests?type=DPS&exam_pattern=${pattern}`); if (res.ok) { const data = await res.json() setDpps(data) } if (userId) { - const attemptsRes = await fetch(`/api/users/${userId}/attempts`); + const attemptsRes = await fetchWithAuth(`/api/users/${userId}/attempts`); if (attemptsRes.ok) { const attemptsData = await attemptsRes.json(); setAttempts(attemptsData); diff --git a/src/app/dashboard/layout.tsx b/src/app/dashboard/layout.tsx index 5386d19..66f3b31 100644 --- a/src/app/dashboard/layout.tsx +++ b/src/app/dashboard/layout.tsx @@ -335,7 +335,7 @@ export default function DashboardLayout({ children }: { children: ReactNode }) {