feat: Add JWT authentication to backend, configure frontend API client, fix streak layout
This commit is contained in:
@@ -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 |
|
||||
|
||||
112
backend/main.py
112
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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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<typeof settings> = {}) => {
|
||||
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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -121,20 +121,20 @@ export async function GET(req: NextRequest) {
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<div style={{ fontSize: 44, fontWeight: 900, color: textColor, letterSpacing: '-0.02em' }}>{name}</div>
|
||||
<div style={{ fontSize: 20, color: textSec, marginTop: 4 }}>Generated: {exportDate}</div>
|
||||
<div style={{ display: 'flex', fontSize: 44, fontWeight: 900, color: textColor, letterSpacing: '-0.02em' }}>{name}</div>
|
||||
<div style={{ display: 'flex', fontSize: 20, color: textSec, marginTop: 4 }}>{`Generated: ${exportDate}`}</div>
|
||||
<div style={{ display: 'flex', marginTop: 8 }}>
|
||||
<div style={{ fontSize: 22, color: '#06b6d4', fontWeight: 700, marginRight: 16 }}>{`${target} Aspirant`}</div>
|
||||
<div style={{ fontSize: 22, color: textSec, fontWeight: 600 }}>•</div>
|
||||
<div style={{ fontSize: 22, color: textTert, fontWeight: 600, marginLeft: 16 }}>{`${plan} Plan`}</div>
|
||||
<div style={{ display: 'flex', fontSize: 22, color: '#06b6d4', fontWeight: 700, marginRight: 16 }}>{`${target} Aspirant`}</div>
|
||||
<div style={{ display: 'flex', fontSize: 22, color: textSec, fontWeight: 600 }}>•</div>
|
||||
<div style={{ display: 'flex', fontSize: 22, color: textTert, fontWeight: 600, marginLeft: 16 }}>{`${plan} Plan`}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: '4px' }}>
|
||||
<div style={{ fontSize: 22, color: textTert, fontWeight: 600 }}>pragativeer.com</div>
|
||||
<div style={{ fontSize: 22, color: textSec, fontWeight: 600 }}>contact@pragativeer.com</div>
|
||||
<div style={{ fontSize: 22, color: textSec, fontWeight: 600 }}>+91 9079337961</div>
|
||||
<div style={{ display: 'flex', fontSize: 22, color: textTert, fontWeight: 600 }}>pragativeer.com</div>
|
||||
<div style={{ display: 'flex', fontSize: 22, color: textSec, fontWeight: 600 }}>contact@pragativeer.com</div>
|
||||
<div style={{ display: 'flex', fontSize: 22, color: textSec, fontWeight: 600 }}>+91 9079337961</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -335,7 +335,7 @@ export default function DashboardLayout({ children }: { children: ReactNode }) {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => window.dispatchEvent(new CustomEvent("pragativeer-show-streak-animation"))}
|
||||
className="hover:opacity-85 transition-opacity cursor-pointer border-none bg-transparent p-0"
|
||||
className="flex items-center justify-center hover:opacity-85 transition-opacity cursor-pointer border-none bg-transparent p-0"
|
||||
title="View Streak Celebration"
|
||||
>
|
||||
<StreakCounter streak={user.streak_count} />
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import { fetchWithAuth } from "@/lib/apiClient";
|
||||
import { useRef, useState, useEffect } from "react"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
@@ -26,7 +27,7 @@ export default function DashboardPage() {
|
||||
}
|
||||
async function fetchStats() {
|
||||
try {
|
||||
const res = await fetch(`/api/users/${currentUserId}/stats`)
|
||||
const res = await fetchWithAuth(`/api/users/${currentUserId}/stats`)
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
setUserStats(data)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { fetchWithAuth } from "@/lib/apiClient";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -50,7 +51,7 @@ export default function SettingsPage() {
|
||||
formData.append("file", file);
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/users/${userId}/avatar`, {
|
||||
const res = await fetchWithAuth(`/api/users/${userId}/avatar`, {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
@@ -72,7 +73,7 @@ export default function SettingsPage() {
|
||||
if (!userId) return;
|
||||
setSaving(true);
|
||||
try {
|
||||
const res = await fetch(`/api/users/${userId}`, {
|
||||
const res = await fetchWithAuth(`/api/users/${userId}`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import { fetchWithAuth } from "@/lib/apiClient";
|
||||
import { useState, useEffect } from "react"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { Flame, Gift, Star, Trophy, Target, ArrowRight, CheckCircle2 } from "lucide-react"
|
||||
@@ -20,7 +21,7 @@ export default function StreaksPage() {
|
||||
}
|
||||
async function loadStats() {
|
||||
try {
|
||||
const res = await fetch(`/api/users/${user?.id}/stats`)
|
||||
const res = await fetchWithAuth(`/api/users/${user?.id}/stats`)
|
||||
if (res.ok) {
|
||||
const stats = await res.json()
|
||||
if (!stats.is_new_user) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client"
|
||||
"use client"
|
||||
|
||||
import { fetchWithAuth } from "@/lib/apiClient";
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { ArrowLeft, Eye, EyeOff } from "lucide-react"
|
||||
@@ -34,14 +35,14 @@ export default function LoginPage() {
|
||||
const cookieVal = data.role === "admin" ? "admin" : (data.token || "authenticated");
|
||||
document.cookie = `auth_token=${cookieVal}; path=/; max-age=2592000; SameSite=Lax`;
|
||||
|
||||
localStorage.setItem("activeUserId", data.token);
|
||||
// Create new session token for single-device login
|
||||
const sessionToken = crypto.randomUUID();
|
||||
localStorage.setItem("activeUserId", data.user_id || data.token);
|
||||
// Store the JWT as the active session token
|
||||
const sessionToken = data.token;
|
||||
localStorage.setItem("active_session_token", sessionToken);
|
||||
|
||||
if (data.token && data.token !== "admin") {
|
||||
try {
|
||||
await fetch(`/api/users/${data.token}/session`, {
|
||||
await fetchWithAuth(`/api/users/${data.user_id}/session`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ session_token: sessionToken })
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import { fetchWithAuth } from "@/lib/apiClient";
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { ArrowLeft, Eye, EyeOff, CheckCircle2 } from "lucide-react"
|
||||
@@ -61,10 +62,10 @@ export default function SignupPage() {
|
||||
|
||||
if (loginData.success && typeof window !== "undefined") {
|
||||
document.cookie = `auth_token=${loginData.token}; path=/; max-age=2592000; SameSite=Lax`;
|
||||
localStorage.setItem("activeUserId", loginData.token);
|
||||
const sessionToken = crypto.randomUUID();
|
||||
localStorage.setItem("activeUserId", loginData.user_id || loginData.token);
|
||||
const sessionToken = loginData.token;
|
||||
localStorage.setItem("active_session_token", sessionToken);
|
||||
await fetch(`/api/users/${loginData.token}/session`, {
|
||||
await fetchWithAuth(`/api/users/${loginData.user_id}/session`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ session_token: sessionToken })
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import { fetchWithAuth } from "@/lib/apiClient";
|
||||
import { useState, useRef, useEffect } from "react"
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardFooter } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
@@ -66,7 +67,7 @@ export function AITutorChat() {
|
||||
try {
|
||||
const activeId = user?.id || (typeof window !== "undefined" ? localStorage.getItem("activeUserId") : null) || DEFAULT_USER_ID;
|
||||
|
||||
const res = await fetch('/api/ai-coach/chat/stream', {
|
||||
const res = await fetchWithAuth('/api/ai-coach/chat/stream', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ user_id: activeId, message: userText })
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import { fetchWithAuth } from "@/lib/apiClient";
|
||||
import { useEffect, useState } from "react"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { RefreshCw, TrendingUp, MapPin, Star } from "lucide-react"
|
||||
@@ -61,7 +62,7 @@ export function CollegePredictorCard() {
|
||||
if (userLoading || !userId) return
|
||||
async function fetchData() {
|
||||
try {
|
||||
const res = await fetch(`/api/ai-coach/readiness/${userId}`)
|
||||
const res = await fetchWithAuth(`/api/ai-coach/readiness/${userId}`)
|
||||
if (!res.ok) throw new Error("fallback")
|
||||
} catch {
|
||||
// Use mock data — no-op
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import { fetchWithAuth } from "@/lib/apiClient";
|
||||
import { useEffect, useState } from "react"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { TrendingUp, Flame, RefreshCw } from "lucide-react"
|
||||
@@ -18,7 +19,7 @@ export function ReadinessGauge() {
|
||||
|
||||
async function fetchReadiness() {
|
||||
try {
|
||||
const res = await fetch(`/api/ai-coach/readiness/${userId}`)
|
||||
const res = await fetchWithAuth(`/api/ai-coach/readiness/${userId}`)
|
||||
if (!res.ok) throw new Error("Failed to fetch")
|
||||
const json = await res.json()
|
||||
setData(json)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import { fetchWithAuth } from "@/lib/apiClient";
|
||||
import { useEffect, useState } from "react"
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
@@ -16,7 +17,7 @@ export function ReferralCard() {
|
||||
if (userLoading || !userId) return;
|
||||
async function fetchReferral() {
|
||||
try {
|
||||
const res = await fetch(`/api/growth/referral/${userId}`)
|
||||
const res = await fetchWithAuth(`/api/growth/referral/${userId}`)
|
||||
if (res.ok) {
|
||||
const json = await res.json()
|
||||
setData(json)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import { fetchWithAuth } from "@/lib/apiClient";
|
||||
import { useEffect, useState } from "react"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { Check, RefreshCw } from "lucide-react"
|
||||
@@ -17,7 +18,7 @@ export function StudyPlanCard() {
|
||||
|
||||
async function fetchPlan() {
|
||||
try {
|
||||
const res = await fetch(`/api/ai-coach/study-plan/today?user_id=${userId}`)
|
||||
const res = await fetchWithAuth(`/api/ai-coach/study-plan/today?user_id=${userId}`)
|
||||
if (!res.ok) {
|
||||
// If no plan is generated yet, fallback silently
|
||||
throw new Error("silent_fallback")
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import { fetchWithAuth } from "@/lib/apiClient";
|
||||
import { useState, useRef, useEffect } from "react"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { Send, Loader2, BrainCircuit, Sparkles } from "lucide-react"
|
||||
@@ -49,7 +50,7 @@ export function TutorChat() {
|
||||
setStreaming(true)
|
||||
|
||||
try {
|
||||
const res = await fetch("/api/ai-coach/chat/stream", {
|
||||
const res = await fetchWithAuth("/api/ai-coach/chat/stream", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ user_id: userId, message: text.trim() })
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState, useCallback, useRef } from "react";
|
||||
import { fetchWithAuth } from "@/lib/apiClient";
|
||||
|
||||
export interface UserProfile {
|
||||
id: string;
|
||||
@@ -110,7 +111,7 @@ async function fetchSettings(): Promise<any> {
|
||||
if (cachedSettings && now < settingsCacheExpiry) return cachedSettings;
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/admin/settings`, { cache: "no-store" });
|
||||
const res = await fetchWithAuth(`/api/admin/settings`, { cache: "no-store" });
|
||||
if (res.ok) {
|
||||
cachedSettings = await res.json();
|
||||
settingsCacheExpiry = now + 60_000; // cache for 60 seconds
|
||||
@@ -147,7 +148,7 @@ export function useActiveUser() {
|
||||
try {
|
||||
// ── PERF FIX: run both fetches in PARALLEL instead of sequentially ──
|
||||
const [res, settingsData] = await Promise.all([
|
||||
fetch(`/api/users/${id}`),
|
||||
fetchWithAuth(`/api/users/${id}`),
|
||||
fetchSettings(),
|
||||
]);
|
||||
|
||||
@@ -226,48 +227,49 @@ export function useActiveUser() {
|
||||
if (typeof window === "undefined" || newId === activeUserIdRef.current) return;
|
||||
|
||||
setSwitching(true);
|
||||
|
||||
// ── Update cookie + localStorage immediately (optimistic) ───────────────
|
||||
document.cookie = `auth_token=${newId}; path=/; max-age=2592000; SameSite=Lax`;
|
||||
const sessionToken = crypto.randomUUID();
|
||||
localStorage.setItem("active_session_token", sessionToken);
|
||||
localStorage.setItem("activeUserId", newId);
|
||||
|
||||
// Update refs and state before awaiting network
|
||||
activeUserIdRef.current = newId;
|
||||
setUserIdState(newId);
|
||||
setUser(null); // Clear old user immediately so stale data isn't shown
|
||||
|
||||
// Notify listeners immediately of account switch
|
||||
if (typeof window !== "undefined") {
|
||||
window.dispatchEvent(new CustomEvent("pragativeer-user-switched", { detail: { userId: newId } }));
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
// ── Run session update + new user profile fetch IN PARALLEL ─────────
|
||||
const [_, newUserRes] = await Promise.all([
|
||||
// Fire-and-forget session token update
|
||||
fetch(`/api/users/${newId}/session`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ session_token: sessionToken }),
|
||||
}).catch(() => {}), // ignore failure — non-critical
|
||||
// Fetch the new user's profile immediately
|
||||
fetch(`/api/users/${newId}`),
|
||||
]);
|
||||
// First, get the JWT token for this demo user by calling the switch API
|
||||
const switchRes = await fetch(`/api/user/switch?id=${newId}`);
|
||||
const switchData = await switchRes.json();
|
||||
|
||||
if (!switchRes.ok || !switchData.token) {
|
||||
throw new Error("Switch failed");
|
||||
}
|
||||
|
||||
const sessionToken = switchData.token; // This is the JWT
|
||||
const actualUserId = switchData.user_id;
|
||||
|
||||
// ── Update cookie + localStorage immediately (optimistic) ───────────────
|
||||
document.cookie = `auth_token=${sessionToken}; path=/; max-age=2592000; SameSite=Lax`;
|
||||
localStorage.setItem("active_session_token", sessionToken);
|
||||
localStorage.setItem("activeUserId", actualUserId);
|
||||
|
||||
// Update refs and state before awaiting network
|
||||
activeUserIdRef.current = actualUserId;
|
||||
setUserIdState(actualUserId);
|
||||
setUser(null); // Clear old user immediately so stale data isn't shown
|
||||
|
||||
// Notify listeners immediately of account switch
|
||||
if (typeof window !== "undefined") {
|
||||
window.dispatchEvent(new CustomEvent("pragativeer-user-switched", { detail: { userId: actualUserId } }));
|
||||
}
|
||||
|
||||
// Fetch the new user's profile immediately using the new token
|
||||
const newUserRes = await fetchWithAuth(`/api/users/${actualUserId}`);
|
||||
|
||||
if (newUserRes && newUserRes.ok) {
|
||||
const data = await newUserRes.json();
|
||||
setUser(data);
|
||||
if (typeof window !== "undefined") {
|
||||
window.dispatchEvent(new CustomEvent("pragativeer-user-switched", { detail: { userId: newId, user: data } }));
|
||||
window.dispatchEvent(new CustomEvent("pragativeer-user-switched", { detail: { userId: actualUserId, user: data } }));
|
||||
}
|
||||
} else {
|
||||
// Fallback: use the regular fetch path
|
||||
await fetchUserProfile(newId);
|
||||
await fetchUserProfile(actualUserId);
|
||||
}
|
||||
} catch {
|
||||
// Network error — fall back to full fetch
|
||||
} catch (err) {
|
||||
// Network error
|
||||
console.error("Failed to switch user", err);
|
||||
await fetchUserProfile(newId);
|
||||
} finally {
|
||||
setSwitching(false);
|
||||
|
||||
27
src/lib/apiClient.ts
Normal file
27
src/lib/apiClient.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
export async function fetchWithAuth(url: string, options: RequestInit = {}): Promise<Response> {
|
||||
const isServer = typeof window === 'undefined';
|
||||
|
||||
if (isServer) {
|
||||
// If we're on the server-side, we should pass along headers from next/headers
|
||||
// or just let it fall back to standard fetch if no auth is needed for server-side fetches.
|
||||
// For this app, most data fetching seems to be client-side.
|
||||
return fetch(url, options);
|
||||
}
|
||||
|
||||
// Get token from localStorage
|
||||
const token = localStorage.getItem('active_session_token');
|
||||
|
||||
// Prepare headers
|
||||
const headers = new Headers(options.headers || {});
|
||||
|
||||
// Apply token if it exists
|
||||
if (token) {
|
||||
headers.set('Authorization', `Bearer ${token}`);
|
||||
}
|
||||
|
||||
// Perform fetch with updated headers
|
||||
return fetch(url, {
|
||||
...options,
|
||||
headers,
|
||||
});
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { fetchWithAuth } from "@/lib/apiClient";
|
||||
import { create } from 'zustand'
|
||||
import { persist, StateStorage, createJSONStorage } from 'zustand/middleware'
|
||||
import { get, set, del } from 'idb-keyval'
|
||||
@@ -252,7 +253,7 @@ export const useCBTStore = create<CBTState>()(
|
||||
if (!payload) return null;
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/attempts/submit`, {
|
||||
const res = await fetchWithAuth(`/api/attempts/submit`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
|
||||
Reference in New Issue
Block a user