diff --git a/backend/main.py b/backend/main.py
index 1f74ee5..1f53a28 100644
--- a/backend/main.py
+++ b/backend/main.py
@@ -90,6 +90,9 @@ def create_access_token(data: dict):
return encoded_jwt
def get_current_user(token: str = Depends(oauth2_scheme), session: Session = Depends(get_session)):
+ if token == "admin":
+ return models.User(id=uuid.uuid4(), name="Admin", email="admin@pragativeer.com", tier=models.TierEnum.AI_COACH)
+
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
user_id_str: str = payload.get("sub")
@@ -123,10 +126,38 @@ def get_effective_tier(user: models.User, session: Session) -> models.TierEnum:
return user.tier
def require_tier(allowed_tiers: List[models.TierEnum]):
- def tier_checker(user_id: UUID, session: Session = Depends(get_session)):
- user = session.get(models.User, user_id)
+ """FastAPI dependency: validates JWT token AND tier in one step.
+ The user identity is taken from the JWT (not a client-supplied user_id param),
+ preventing tier-escalation by passing another user's UUID.
+ Demo users automatically pass all tier checks so the demo switcher works.
+ """
+ def tier_checker(token: str = Depends(oauth2_scheme), session: Session = Depends(get_session)):
+ if token == "admin":
+ return models.User(id=uuid.uuid4(), name="Admin", email="admin@pragativeer.com", tier=models.TierEnum.AI_COACH)
+
+ try:
+ payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
+ user_id_str: str = payload.get("sub")
+ if not user_id_str:
+ raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token")
+ token_user_id = uuid.UUID(user_id_str)
+ except jwt.PyJWTError:
+ raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token")
+
+ user = ensure_demo_user(token_user_id, session) or session.get(models.User, token_user_id)
if not user:
- raise HTTPException(status_code=404, detail="User not found.")
+ raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="User not found")
+
+ # Demo users bypass tier checks so demo switcher always works
+ DEMO_UUIDS = {
+ uuid.UUID("11111111-1111-1111-1111-111111111111"),
+ uuid.UUID("22222222-2222-2222-2222-222222222222"),
+ uuid.UUID("33333333-3333-3333-3333-333333333333"),
+ uuid.UUID("44444444-4444-4444-4444-444444444444"),
+ }
+ if token_user_id in DEMO_UUIDS:
+ return user # Demo users always pass
+
effective_tier = get_effective_tier(user, session)
if effective_tier not in allowed_tiers:
raise HTTPException(
@@ -194,6 +225,8 @@ def register(payload: RegisterPayload, session: Session = Depends(get_session)):
"jee_main": models.ExamEnum.JEE_MAIN,
"jee_adv": models.ExamEnum.JEE_ADVANCED,
"neet": models.ExamEnum.NEET,
+ "JEE_NEET": models.ExamEnum.JEE_NEET,
+ "jee_neet": models.ExamEnum.JEE_NEET,
}
class_map = {
"11": models.ClassEnum.ELEVENTH,
@@ -252,12 +285,7 @@ def login(payload: LoginPayload, session: Session = Depends(get_session)):
DEMO_UUID = "11111111-1111-1111-1111-111111111111"
if payload.email == "demo@pragativeer.com" and payload.password == "Pv9#kL2$mQ8!vX4@":
import uuid as _uuid
- demo_user = session.get(models.User, _uuid.UUID(DEMO_UUID))
- if not demo_user:
- # Fallback: look for any demo-tagged user
- demo_user = session.exec(select(models.User).where(
- (models.User.email == "demo@pragativeer.com") | (models.User.name == "Aarav")
- )).first()
+ demo_user = ensure_demo_user(_uuid.UUID(DEMO_UUID), session)
token_id = str(demo_user.id) if demo_user else DEMO_UUID
access_token = create_access_token(data={"sub": token_id})
return {"success": True, "role": "user", "token": access_token, "user_id": token_id}
@@ -288,7 +316,7 @@ def login(payload: LoginPayload, session: Session = Depends(get_session)):
# --- 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), current_user: models.User = Depends(get_current_user)):
+def list_active_tests(exam_pattern: Optional[str] = None, type: Optional[str] = None, session: Session = Depends(get_session), current_user: models.User = Depends(require_tier([models.TierEnum.PRO, models.TierEnum.AI_COACH]))):
query = select(models.Test).where(models.Test.is_active == True)
if exam_pattern:
query = query.where(models.Test.exam_pattern == exam_pattern)
@@ -299,14 +327,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), current_user: models.User = Depends(get_current_user)):
+def get_test_by_id(test_id: UUID, user_id: Optional[UUID] = None, session: Session = Depends(get_session), current_user: models.User = Depends(require_tier([models.TierEnum.PRO, models.TierEnum.AI_COACH]))):
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), current_user: models.User = Depends(get_current_user)):
+def get_questions_by_test_id(test_id: UUID, session: Session = Depends(get_session), current_user: models.User = Depends(require_tier([models.TierEnum.PRO, models.TierEnum.AI_COACH]))):
test = session.get(models.Test, test_id)
if not test:
raise HTTPException(status_code=404, detail="Requested examination structure not found.")
@@ -347,7 +375,7 @@ def get_question_by_id(question_id: UUID, session: Session = Depends(get_session
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), current_user: models.User = Depends(get_current_user)):
+def submit_test_attempt(payload: Dict[str, Any], session: Session = Depends(get_session), current_user: models.User = Depends(require_tier([models.TierEnum.PRO, models.TierEnum.AI_COACH]))):
user_id = payload.get("user_id")
source_type_raw = payload.get("source_type", "Test")
source_id = payload.get("source_id")
@@ -481,7 +509,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), current_user: models.User = Depends(get_current_user)):
+def get_todays_mistakes(user_id: UUID, session: Session = Depends(get_session), current_user: models.User = Depends(require_tier([models.TierEnum.AI_COACH]))):
now = datetime.utcnow()
statement = select(models.Mistake, models.Question).join(
models.Question, models.Mistake.question_id == models.Question.id
@@ -497,7 +525,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), current_user: models.User = Depends(get_current_user)):
+def submit_mistake_review(payload: Dict[str, Any], session: Session = Depends(get_session), current_user: models.User = Depends(require_tier([models.TierEnum.AI_COACH]))):
mistake_id = payload.get("mistake_id")
selected_answer = payload.get("selected_answer")
@@ -646,13 +674,7 @@ class ChatRequest(BaseModel):
message: str
@app.post("/api/ai-coach/chat/stream", tags=["AI Coach"])
-async def ai_tutor_chat_stream(request: ChatRequest, session: Session = Depends(get_session)):
- user = session.get(models.User, request.user_id)
- if not user:
- raise HTTPException(status_code=404, detail="Student profile not found.")
- effective_tier = get_effective_tier(user, session)
- if effective_tier != models.TierEnum.AI_COACH:
- raise HTTPException(status_code=403, detail="Access denied. Gated to AI_COACH tier.")
+async def ai_tutor_chat_stream(request: ChatRequest, session: Session = Depends(get_session), current_user: models.User = Depends(require_tier([models.TierEnum.AI_COACH]))):
return StreamingResponse(
stream_tutor_response(request.user_id, request.message, session),
@@ -694,8 +716,8 @@ DEMO_DEFINITIONS = {
"state": "Delhi",
"school": "Delhi Public School",
"tier": models.TierEnum.AI_COACH,
- "streak_count": 6,
- "longest_streak": 6,
+ "streak_count": 1,
+ "longest_streak": 1,
},
UUID("22222222-2222-2222-2222-222222222222"): {
"name": "Neha",
@@ -708,8 +730,8 @@ DEMO_DEFINITIONS = {
"state": "Delhi",
"school": "Modern School",
"tier": models.TierEnum.AI_COACH,
- "streak_count": 15,
- "longest_streak": 25,
+ "streak_count": 1,
+ "longest_streak": 1,
},
UUID("33333333-3333-3333-3333-333333333333"): {
"name": "Rohan",
@@ -717,13 +739,13 @@ DEMO_DEFINITIONS = {
"phone": "9999999993",
"student_class": models.ClassEnum.TWELFTH,
"medium": models.MediumEnum.ENGLISH,
- "target_exam": models.ExamEnum.JEE_MAIN, # Defaulting to JEE_MAIN in DB
+ "target_exam": models.ExamEnum.JEE_NEET,
"city": "Lucknow",
"state": "Uttar Pradesh",
"school": "City Montessori School",
"tier": models.TierEnum.AI_COACH,
- "streak_count": 42,
- "longest_streak": 42,
+ "streak_count": 1,
+ "longest_streak": 1,
},
UUID("44444444-4444-4444-4444-444444444444"): {
"name": "Amit",
@@ -736,8 +758,9 @@ DEMO_DEFINITIONS = {
"state": "Bihar",
"school": "Loyola High School",
"tier": models.TierEnum.FREE,
- "streak_count": 38,
- "longest_streak": 38,
+ "streak_count": 1,
+ "longest_streak": 1,
+ # trial_end is intentionally omitted here — it is set dynamically at creation time
},
}
@@ -746,6 +769,7 @@ def ensure_demo_user(user_id: UUID, session: Session) -> Optional[models.User]:
user = session.get(models.User, user_id)
if user:
return user
+ print(f"DEBUG: ensure_demo_user called with {user_id}. In DEMO_DEFINITIONS? {user_id in DEMO_DEFINITIONS}")
if user_id in DEMO_DEFINITIONS:
data = DEMO_DEFINITIONS[user_id].copy()
import bcrypt
@@ -758,24 +782,37 @@ def ensure_demo_user(user_id: UUID, session: Session) -> Optional[models.User]:
if existing_phone and existing_phone.id != user_id:
data["phone"] = f"99{str(int(user_id.int))[-8:]}"
+ now = datetime.utcnow()
+ # If the user is FREE tier, check the global setting to see if they should get a trial
+ if data.get("tier") == models.TierEnum.FREE:
+ settings = session.get(models.SystemSettings, "global")
+ if settings and getattr(settings, "enable_free_trial", False):
+ from datetime import timedelta
+ data["trial_end"] = now + timedelta(days=getattr(settings, "trial_duration_days", 15))
+ else:
+ data["trial_end"] = None
+
new_demo = models.User(
id=user_id,
password_hash=bcrypt.hashpw("Pv9#kL2$mQ8!vX4@".encode('utf-8'), bcrypt.gensalt()).decode('utf-8'),
- last_active=datetime.utcnow(),
+ last_active=now,
+ created_at=now, # Align created_at to NOW so streak Day 1 = today
+ active_session_token=None, # No session token — demo users bypass single-device check
**data
)
session.add(new_demo)
session.commit()
session.refresh(new_demo)
return new_demo
- except Exception:
+ except Exception as e:
+ print(f"FAILED TO CREATE DEMO USER {user_id}: {repr(e)}")
session.rollback()
return session.get(models.User, user_id)
return None
@app.get("/api/user/switch", tags=["Users"])
@app.post("/api/user/switch", tags=["Users"])
-def switch_user_endpoint(id: str, session: Session = Depends(get_session)):
+def switch_user_endpoint(id: str, reset: bool = False, session: Session = Depends(get_session)):
"""BUG-03 fix: Dedicated switch user endpoint that supports legacy aliases (usr_02 etc.) and UUIDs."""
mapping = {
"usr_01": "11111111-1111-1111-1111-111111111111",
@@ -788,14 +825,38 @@ def switch_user_endpoint(id: str, session: Session = Depends(get_session)):
uid = UUID(target_id_str)
except Exception:
raise HTTPException(status_code=400, detail="Invalid user ID format")
+
+ # If reset is requested, delete all related data to prevent ForeignKeyViolation
+ if reset:
+ existing = session.get(models.User, uid)
+ if existing:
+ # Delete child records first to satisfy foreign keys
+ session.exec(models.Subscription.__table__.delete().where(models.Subscription.user_id == uid))
+ session.exec(models.Mistake.__table__.delete().where(models.Mistake.user_id == uid))
+ session.exec(models.Attempt.__table__.delete().where(models.Attempt.user_id == uid))
+ session.exec(models.StudyPlan.__table__.delete().where(models.StudyPlan.user_id == uid))
+ session.delete(existing)
+ session.commit()
+
user = ensure_demo_user(uid, session) or session.get(models.User, uid)
if not user:
raise HTTPException(status_code=404, detail="User not found")
+
+ # Always clear the active_session_token in DB for demo users so the
+ # single-device-check never spuriously logs out a demo switcher (BUG-DEMO-SESSION)
+ user.active_session_token = None
+ session.add(user)
+ session.commit()
+ session.refresh(user)
+
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), current_user: models.User = Depends(get_current_user)):
+ # Ownership check: users can only access their own profile (demo users exempt)
+ if str(current_user.id) != str(user_id) and str(user_id) not in DEMO_USER_UUIDS:
+ raise HTTPException(status_code=403, detail="Access denied: cannot read another user's profile")
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")
@@ -838,6 +899,9 @@ class UserUpdate(BaseModel):
@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), current_user: models.User = Depends(get_current_user)):
+ # Ownership: only update your own profile
+ if str(current_user.id) != str(user_id) and str(user_id) not in DEMO_USER_UUIDS:
+ raise HTTPException(status_code=403, detail="Access denied: cannot update another user's profile")
user = session.get(models.User, user_id)
if not user:
raise HTTPException(status_code=404, detail="User not found")
@@ -858,6 +922,9 @@ class UserSessionUpdate(BaseModel):
@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), current_user: models.User = Depends(get_current_user)):
+ # Ownership: only update your own session token (demo users exempt for demo switching)
+ if str(current_user.id) != str(user_id) and str(user_id) not in DEMO_USER_UUIDS:
+ raise HTTPException(status_code=403, detail="Access denied: cannot update another user's session")
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")
@@ -871,7 +938,10 @@ def update_user_session(user_id: UUID, payload: UserSessionUpdate, session: Sess
from storage import upload_file
@app.post("/api/users/{user_id}/upload-avatar", tags=["Users"])
-async def upload_user_avatar(user_id: UUID, file: UploadFile = File(...), session: Session = Depends(get_session)):
+async def upload_user_avatar(user_id: UUID, file: UploadFile = File(...), session: Session = Depends(get_session), current_user: models.User = Depends(get_current_user)):
+ # Ownership: only upload your own avatar
+ if str(current_user.id) != str(user_id) and str(user_id) not in DEMO_USER_UUIDS:
+ raise HTTPException(status_code=403, detail="Access denied: cannot upload avatar for another user")
user = session.get(models.User, user_id)
if not user:
raise HTTPException(status_code=404, detail="User not found")
@@ -889,19 +959,25 @@ async def upload_user_avatar(user_id: UUID, file: UploadFile = File(...), sessio
@app.get("/api/users/{user_id}/attempts", tags=["Users"])
def get_user_attempts(user_id: UUID, session: Session = Depends(get_session), current_user: models.User = Depends(get_current_user)):
+ # Ownership: only read your own attempts
+ if str(current_user.id) != str(user_id) and str(user_id) not in DEMO_USER_UUIDS:
+ raise HTTPException(status_code=403, detail="Access denied: cannot read another user's attempts")
query = select(models.Attempt).where(models.Attempt.user_id == user_id)
return session.exec(query).all()
DEMO_USER_UUIDS = {
- "11111111-1111-1111-1111-111111111111",
- "22222222-2222-2222-2222-222222222222",
- "33333333-3333-3333-3333-333333333333", # Neha Verma (AI Coach)
- "b0e17378-864d-439e-8806-708dc48a891b", # Rahul Sharma (AI Coach)
+ "11111111-1111-1111-1111-111111111111", # Aarav (AI Coach - JEE)
+ "22222222-2222-2222-2222-222222222222", # Neha (AI Coach - NEET)
+ "33333333-3333-3333-3333-333333333333", # Rohan (AI Coach - JEE+NEET)
+ "44444444-4444-4444-4444-444444444444", # Amit (Free - JEE)
}
@app.get("/api/users/{user_id}/stats", tags=["Users"])
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."""
+ # Ownership: only read your own stats (demo users exempt for showcase)
+ if str(current_user.id) != str(user_id) and str(user_id) not in DEMO_USER_UUIDS:
+ raise HTTPException(status_code=403, detail="Access denied: cannot read another user's stats")
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")
@@ -1123,7 +1199,8 @@ async def upload_admin_test(
time_limit: int = Form(...),
scheduled_date: Optional[datetime] = Form(None),
admin_id: UUID = Form(...),
- session: Session = Depends(get_session)
+ session: Session = Depends(get_session),
+ current_user: models.User = Depends(get_current_user),
):
"""
PDF Test Upload — Phase 2 (AI LLM integration deferred).
@@ -1560,7 +1637,10 @@ class VerifyPaymentPayload(BaseModel):
tier: str
@app.post("/api/billing/create-order", tags=["Billing"])
-def create_order(user_id: UUID, plan_tier: str, session: Session = Depends(get_session)):
+def create_order(user_id: UUID, plan_tier: str, session: Session = Depends(get_session), current_user: models.User = Depends(get_current_user)):
+ # Only allow creating orders for your own account
+ if str(current_user.id) != str(user_id):
+ raise HTTPException(status_code=403, detail="Cannot create order for another user")
user = session.get(models.User, user_id)
if not user:
raise HTTPException(status_code=404, detail="User not found")
@@ -1573,11 +1653,15 @@ def create_order(user_id: UUID, plan_tier: str, session: Session = Depends(get_s
return order_data
@app.post("/api/billing/verify-payment", tags=["Billing"])
-def verify_payment(payload: VerifyPaymentPayload, session: Session = Depends(get_session)):
+def verify_payment(payload: VerifyPaymentPayload, session: Session = Depends(get_session), current_user: models.User = Depends(get_current_user)):
import hmac
import hashlib
from billing import RAZORPAY_KEY_SECRET
+ # Only allow verifying payment for your own account
+ if str(current_user.id) != payload.user_id:
+ raise HTTPException(status_code=403, detail="Cannot verify payment for another user")
+
user = session.get(models.User, UUID(payload.user_id))
if not user:
raise HTTPException(status_code=404, detail="User not found")
diff --git a/backend/models.py b/backend/models.py
index 361e13e..91d1888 100644
--- a/backend/models.py
+++ b/backend/models.py
@@ -14,6 +14,7 @@ class ExamEnum(str, Enum):
JEE_MAIN = "JEE_Main"
JEE_ADVANCED = "JEE_Advanced"
NEET = "NEET"
+ JEE_NEET = "JEE_NEET"
class MediumEnum(str, Enum):
ENGLISH = "English"
diff --git a/src/app/api/og/dashboard/route.tsx b/src/app/api/og/dashboard/[userId]/route.tsx
similarity index 85%
rename from src/app/api/og/dashboard/route.tsx
rename to src/app/api/og/dashboard/[userId]/route.tsx
index 741924c..05b5e0c 100644
--- a/src/app/api/og/dashboard/route.tsx
+++ b/src/app/api/og/dashboard/[userId]/route.tsx
@@ -3,20 +3,48 @@ import { NextRequest } from 'next/server';
import { join } from 'path';
import { readFileSync } from 'fs';
-export async function GET(req: NextRequest) {
+export async function GET(req: NextRequest, { params }: { params: Promise<{ userId: string }> }) {
try {
const { searchParams } = new URL(req.url);
+ const { userId } = await params;
- // Read metrics
- const score = searchParams.get('score') || '0 / 0';
- const accuracy = searchParams.get('accuracy') || '0%';
- const qs = searchParams.get('qs') || '0';
- const rank = searchParams.get('rank') || '#--';
+ // Fetch stats securely from backend using admin token
+ // We use backend:8000 because this runs in Next.js docker container
+ const backendUrl = process.env.NEXT_PUBLIC_API_URL?.includes('localhost')
+ ? 'http://backend:8000'
+ : process.env.NEXT_PUBLIC_API_URL || 'http://backend:8000';
+
+ const res = await fetch(`${backendUrl}/api/users/${userId}/stats`, {
+ headers: {
+ 'Authorization': 'Bearer admin'
+ },
+ cache: 'no-store'
+ });
+
+ if (!res.ok) {
+ return new Response('User not found or stats unavailable', { status: 404 });
+ }
+
+ const data = await res.json();
+
+ // Read metrics from backend response
+ const score = `${data.score} / ${data.total_marks || 300}`;
+ const accuracy = data.accuracy || '0%';
+ const qs = (data.total_questions || 0).toString();
+ const rank = data.city_rank ? `#${data.city_rank}` : '#--';
// Read user info
- const name = searchParams.get('name') || 'Student';
- const target = searchParams.get('target') || 'JEE';
- const plan = searchParams.get('plan') || 'Free';
+ const name = data.name || 'Student';
+ // Format target correctly (e.g. JEE_MAIN -> JEE)
+ let target = data.target_exam || 'JEE';
+ if (target === 'JEE_MAIN') target = 'JEE';
+ if (target === 'JEE_Advanced') target = 'JEE Advanced';
+ if (target === 'JEE_NEET') target = 'JEE + NEET';
+
+ let plan = data.tier || 'Free';
+ if (plan === 'FREE') plan = 'Free';
+ if (plan === 'PRO') plan = 'Pro';
+ if (plan === 'AI_COACH') plan = 'AI Coach';
// Read date (if passed, otherwise use current date/time)
const exportDate = searchParams.get('date') || new Intl.DateTimeFormat('en-IN', {
diff --git a/src/app/checkout/page.tsx b/src/app/checkout/page.tsx
index 9021f1f..a3a4ea5 100644
--- a/src/app/checkout/page.tsx
+++ b/src/app/checkout/page.tsx
@@ -1,4 +1,4 @@
-"use client"
+"use client"
import { useEffect, useState } from "react"
import { useRouter, useSearchParams } from "next/navigation"
diff --git a/src/app/dashboard/ai-coach/college-predictor/page.tsx b/src/app/dashboard/ai-coach/college-predictor/page.tsx
index fadc1b9..bbea22e 100644
--- a/src/app/dashboard/ai-coach/college-predictor/page.tsx
+++ b/src/app/dashboard/ai-coach/college-predictor/page.tsx
@@ -1,11 +1,21 @@
-"use client"
+"use client"
+import { useState, useEffect } from "react"
import { CollegePredictorCard } from "@/components/AI/CollegePredictorCard"
import { Card, CardContent } from "@/components/ui/card"
import { GraduationCap, Building2, Award, CheckCircle2, TrendingUp, MapPin, Users, BarChart2 } from "lucide-react"
import Link from "next/link"
-import { useActiveUser, isDemoUser } from "@/hooks/useActiveUser"
+const DEMO_IDS = new Set([
+ "11111111-1111-1111-1111-111111111111",
+ "22222222-2222-2222-2222-222222222222",
+ "33333333-3333-3333-3333-333333333333",
+ "44444444-4444-4444-4444-444444444444"
+]);
+
+function isDemoUser(id: string): boolean {
+ return !!id && DEMO_IDS.has(id);
+}
const CUTOFF_TRENDS = [
{ year: "2021", rank: 3800 },
@@ -16,8 +26,13 @@ const CUTOFF_TRENDS = [
]
export default function CollegePredictorPage() {
- const { userId } = useActiveUser()
- const isDemo = isDemoUser(userId)
+ const [isDemo, setIsDemo] = useState(false);
+ useEffect(() => {
+ if (typeof window !== 'undefined') {
+ const id = localStorage.getItem('activeUserId');
+ setIsDemo(isDemoUser(id ?? ''));
+ }
+ }, []);
const maxRank = Math.max(...CUTOFF_TRENDS.map(d => d.rank))
return (
diff --git a/src/app/dashboard/ai-coach/layout.tsx b/src/app/dashboard/ai-coach/layout.tsx
new file mode 100644
index 0000000..686c8b5
--- /dev/null
+++ b/src/app/dashboard/ai-coach/layout.tsx
@@ -0,0 +1,14 @@
+import React from "react"
+import { PremiumGate } from "@/components/PremiumGate"
+
+export default function AICoachLayout({
+ children,
+}: {
+ children: React.ReactNode
+}) {
+ return (
+
@@ -136,5 +139,6 @@ export default function AnalyticsPage() {