diff --git a/.gitignore b/.gitignore index 5ef6a52..7c19414 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,4 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts +/scratch/ diff --git a/API_DOCUMENTATION.md b/API_DOCUMENTATION.md new file mode 100644 index 0000000..5cd6792 --- /dev/null +++ b/API_DOCUMENTATION.md @@ -0,0 +1,1219 @@ +# PragatiVeer v2 — Comprehensive API & Architecture Developer Documentation + +> **Document Type:** Full Developer Reference & Technical API Specification +> **Target Audience:** Frontend Engineers, Backend Engineers, DevOps, QA Engineers +> **Last Verified:** September 2026 +> **Backend Engine:** FastAPI (Python 3.11) + SQLModel (SQLAlchemy) + Celery + Redis +> **Frontend Engine:** Next.js 16 (React 19, TypeScript, TailwindCSS) +> **Database & Storage:** PostgreSQL 15 (PgBouncer connection pooler) + MinIO (S3-compatible) + +--- + +## 1. System Architecture & Container Topology + +PragatiVeer v2 operates as an 8-container orchestrated cluster via Docker Compose: + +```mermaid +graph TD + Client["Browser / Mobile Client"] -->|Port 3000| Frontend["pragativeer_frontend (Next.js 16)"] + Client -->|Port 8000| Backend["pragativeer_backend (FastAPI)"] + Frontend -->|Internal Rewrites /api/*| Backend + Backend -->|Pooled Queries :5432| PgBouncer["pragativeer_pgbouncer"] + PgBouncer --> DB[("pragativeer_db (PostgreSQL 15)")] + Backend -->|Pub/Sub & Cache :6379| Redis[("pragativeer_cache (Redis 7)")] + Backend -->|Uploads / Files :9000| MinIO[("pragativeer_storage (MinIO S3)")] + Worker["pragativeer_worker (Celery)"] --> Redis + Worker --> DB + Worker --> MinIO + Beat["pragativeer_beat (Celery Beat)"] --> Redis +``` + +### Port Mapping & Infrastructure Reference +| Container Service | Image / Engine | Internal Port | Host Port | Role | +| :--- | :--- | :---: | :---: | :--- | +| `pragativeer_frontend` | Next.js 16 Standalone | 3000 | `3000` | UI, CBT Interface, Next.js Edge OG Image Generation | +| `pragativeer_backend` | Python 3.11 / Uvicorn | 8000 | `8000` | Core REST API, Auth, CBT Engine, AI Coach | +| `pragativeer_db` | PostgreSQL 15 Alpine | 5432 | `5432` | Relational storage (Users, Tests, Attempts, Mistakes) | +| `pragativeer_pgbouncer` | PgBouncer | 5432 | Internal | High-concurrency connection pooler for PostgreSQL | +| `pragativeer_cache` | Redis 7 Alpine | 6379 | `6379` | Response cache & Celery task message broker | +| `pragativeer_worker` | Celery Worker | — | Internal | Background PDF AI question parsing & exam analytics | +| `pragativeer_beat` | Celery Beat | — | Internal | Scheduled 5:00 AM IST study plan generation | +| `pragativeer_storage` | MinIO S3 | 9000 / 9001 | `9000` / `9001` | PDF test uploads, question diagrams, user avatars | + +--- + +## 2. Authentication, Cookies & Request/Response Headers Overview + +### 2.1 Next.js 16 Proxy Middleware Protection (`src/proxy.ts`) +Next.js 16 uses `src/proxy.ts` (modern convention replacing older middleware patterns) to govern edge route protection: +- **Cookie Name:** `auth_token` +- **Dashboard Routes (`/dashboard/:path*`):** Requires cookie `auth_token=`. If absent, redirects to `/login` (HTTP 307 Temporary Redirect). +- **Admin Routes (`/admin/:path*`):** Requires cookie `auth_token=admin`. If mismatched or missing, redirects to `/login` (HTTP 307 Temporary Redirect). +- **Public Routes:** `/`, `/login`, `/signup`, `/about`, `/contact`, `/pricing`, `/cbt-exam/:path*`. + +### 2.2 Global Header Standards Across APIs +All PragatiVeer v2 endpoints conform to strict HTTP/1.1 and HTTP/2 header conventions: + +#### Common Request Headers +| Header | Description | Required On | +| :--- | :--- | :--- | +| `Accept` | `application/json`, `text/event-stream`, or `image/png` | All API requests | +| `Content-Type` | `application/json` (or `multipart/form-data` for file uploads) | All `POST`, `PUT`, `PATCH` requests | +| `Cookie` | `auth_token=` | Protected routes (`/dashboard/*`, `/admin/*`) | +| `X-Razorpay-Signature` | HMAC-SHA256 hex digest computed with Razorpay Webhook Secret | `POST /api/billing/webhook/razorpay` | +| `Cache-Control` | `no-cache` | Streaming SSE endpoints | + +#### Common Response Headers +| Header | Value / Usage | Endpoints | +| :--- | :--- | :--- | +| `Content-Type` | `application/json` | REST endpoints | +| `Content-Type` | `text/event-stream; charset=utf-8` | AI Tutor Streaming (`/api/ai-coach/chat/stream`) | +| `Content-Type` | `image/png` | Dynamic OG Image Generator (`/api/og/progress`) | +| `Cache-Control` | `no-cache, no-transform` | Real-time AI Coach streaming | +| `Connection` | `keep-alive` | Long-lived streaming connections | +| `X-Accel-Buffering` | `no` | Prevents Nginx/Reverse Proxy chunk buffering | + +### 2.3 Tier-Based Gating (`require_tier`) +FastAPI dependency `require_tier(allowed_tiers: List[models.TierEnum])` inspects `user.tier`: +- **`Free`:** Unlocks basic mock tests, question practice, dashboard overview. +- **`Pro`:** Unlocks advanced mock tests, detailed performance metrics, daily practice sheets (DPS). +- **`Al_Coach`:** Unlocks real-time AI Tutor streaming (`/api/ai-coach/chat/stream`), Exam Readiness & College Prediction (`/api/ai-coach/readiness/{user_id}`), and Daily AI Study Plans (`/api/ai-coach/study-plan/today`). + +--- + +## 3. Exhaustive REST API Specification (With Headers, Payloads & Responses) + +--- + +### 3.1 Authentication Subsystem + +#### `POST /api/auth/register` +- **Purpose:** Create a new student account with bcrypt password hashing and default profile fields. +- **Access:** Public +- **Request Headers:** + - `Accept: application/json` + - `Content-Type: application/json` +- **Request Body Schema:** + ```json + { + "name": "Aryan Sharma", + "email": "aryan@example.com", + "password": "Password123!" + } + ``` +- **Response Headers:** + - `Content-Type: application/json` +- **Status Codes & Response Payloads:** + - **`200 OK`**: + ```json + { + "success": true, + "message": "User registered successfully" + } + ``` + - **`400 Bad Request`**: + ```json + { + "detail": "Email already registered" + } + ``` + - **`422 Unprocessable Entity`**: If required fields (`name`, `email`, `password`) are missing or malformed. + +--- + +#### `POST /api/auth/login` +- **Purpose:** Authenticate credentials using bcrypt verification, support seeded credentials & demo bypass, and issue active user token. +- **Access:** Public +- **Request Headers:** + - `Accept: application/json` + - `Content-Type: application/json` +- **Request Body Schema:** + ```json + { + "email": "demo@pragativeer.com", + "password": "Pv9#kL2$mQ8!vX4@" + } + ``` +- **Response Headers:** + - `Content-Type: application/json` +- **Status Codes & Response Payloads:** + - **`200 OK (Student)`**: + ```json + { + "success": true, + "role": "user", + "token": "b0e17378-864d-439e-8806-708dc48a891b" + } + ``` + - **`200 OK (Admin)`**: + ```json + { + "success": true, + "role": "admin", + "token": "admin" + } + ``` + - **`401 Unauthorized`**: + ```json + { + "detail": "Invalid email or password" + } + ``` + - **`422 Unprocessable Entity`**: If email or password is omitted. + +--- + +#### `POST /api/auth/otp-request` +- **Purpose:** Request one-time verification code for phone/email authentication. +- **Access:** Public +- **Request Headers:** + - `Accept: application/json` +- **Request Query Parameters:** + - `phone_or_email` (string, required): e.g. `9876543210` or `student@pragativeer.com` +- **Response Headers:** + - `Content-Type: application/json` +- **Status Codes & Response Payloads:** + - **`200 OK`**: + ```json + { + "message": "Verification code dispatched successfully", + "debug_otp": "123456" + } + ``` + - **`422 Unprocessable Entity`**: If `phone_or_email` is missing. + +--- + +#### `POST /api/auth/otp-verify` +- **Purpose:** Verify OTP code and return session token. +- **Access:** Public +- **Request Headers:** + - `Accept: application/json` +- **Request Query Parameters:** + - `phone_or_email` (string, required): Phone number or email + - `code` (string, required): 6-digit verification code +- **Response Headers:** + - `Content-Type: application/json` +- **Status Codes & Response Payloads:** + - **`200 OK`**: + ```json + { + "status": "authenticated", + "token": "mock_jwt_session_token_string" + } + ``` + - **`400 Bad Request`**: `{"detail": "Invalid or expired OTP code"}` + +--- + +### 3.2 Users, Session Storage & Avatar Uploads + +#### Single-Device Session Storage Architecture +PragatiVeer v2 enforces a strict single-device login policy: +1. Upon successful login (`/login`), the browser generates a random UUID: `const sessionToken = crypto.randomUUID()`. +2. The browser writes this token to `localStorage.setItem("active_session_token", sessionToken)`. +3. The browser makes an immediate `POST /api/users/{user_id}/session` request, storing `active_session_token` in the database. +4. On subsequent page loads, `useActiveUser` calls `GET /api/users/{user_id}`. If the database's `active_session_token` does not match the browser's `localStorage` session token, the student has logged in on another device, triggering an automatic logout and redirect. + +--- + +#### `GET /api/users/{user_id}` +- **Purpose:** Retrieve student profile and automatically compute consecutive-day streaks based on `last_active`. +- **Access:** Authenticated User +- **Request Headers:** + - `Accept: application/json` + - `Cookie: auth_token=` +- **Request Path Parameters:** + - `user_id` (UUID, required): Student user UUID +- **Response Headers:** + - `Content-Type: application/json` +- **Status Codes & Response Payloads:** + - **`200 OK`**: + ```json + { + "id": "b0e17378-864d-439e-8806-708dc48a891b", + "phone": "9999999999", + "email": "student@pragativeer.com", + "name": "Rahul Sharma", + "student_class": "12", + "medium": "English", + "target_exam": "JEE_Main", + "school": "Delhi Public School", + "city": "New Delhi", + "state": "Delhi", + "tier": "Al_Coach", + "streak_count": 6, + "longest_streak": 6, + "profile_image_url": "http://localhost:9000/avatars/user_avatar.jpg", + "active_session_token": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "last_active": "2026-09-04T06:00:00.000000", + "created_at": "2026-08-01T10:00:00.000000" + } + ``` + - **`404 Not Found`**: `{"detail": "User not found"}` + - **`422 Unprocessable Entity`**: If `user_id` is not a valid UUID format. + +--- + +#### `PUT /api/users/{user_id}` +- **Purpose:** Update profile attributes (target exam, school, grade class, state). +- **Access:** Authenticated User +- **Request Headers:** + - `Accept: application/json` + - `Content-Type: application/json` + - `Cookie: auth_token=` +- **Request Path Parameters:** + - `user_id` (UUID, required): Student UUID +- **Request Body Schema:** + ```json + { + "name": "Rahul Sharma", + "target_exam": "JEE_Advanced", + "student_class": "12", + "school": "DPS RK Puram", + "state": "Delhi" + } + ``` +- **Response Headers:** + - `Content-Type: application/json` +- **Status Codes & Response Payloads:** + - **`200 OK`**: Returns updated `models.User` object. + - **`404 Not Found`**: `{"detail": "User not found"}` + - **`422 Unprocessable Entity`**: Malformed JSON or invalid UUID. + +--- + +#### `POST /api/users/{user_id}/session` +- **Purpose:** Update single active session token in database to prevent multi-device concurrent logins. +- **Access:** Authenticated User +- **Request Headers:** + - `Accept: application/json` + - `Content-Type: application/json` + - `Cookie: auth_token=` +- **Request Path Parameters:** + - `user_id` (UUID, required): Student UUID +- **Request Body Schema:** + ```json + { + "session_token": "4f3b2a1c-9e8d-7c6b-5a4f-3e2d1c0b9a8f" + } + ``` +- **Response Headers:** + - `Content-Type: application/json` +- **Status Codes & Response Payloads:** + - **`200 OK`**: Returns `models.User` with updated `active_session_token`. + - **`404 Not Found`**: `{"detail": "User not found"}` + - **`422 Unprocessable Entity`**: Invalid UUID or missing `session_token`. + +--- + +#### `POST /api/users/{user_id}/avatar` +- **Purpose:** Multipart form image upload for student avatar. Uploads image to MinIO S3 bucket `avatars` and updates `user.profile_image_url`. +- **Access:** Authenticated User +- **Request Headers:** + - `Accept: application/json` + - `Content-Type: multipart/form-data; boundary=----WebKitFormBoundary...` + - `Cookie: auth_token=` +- **Request Path Parameters:** + - `user_id` (UUID, required): Student UUID +- **Request Form Data:** + - `file`: Image binary (`image/jpeg`, `image/png`, `image/webp`) +- **Response Headers:** + - `Content-Type: application/json` +- **Status Codes & Response Payloads:** + - **`200 OK`**: + ```json + { + "success": true, + "profile_image_url": "http://localhost:9000/avatars/b0e17378_avatar.jpg" + } + ``` + - **`400 Bad Request`**: If uploaded file is not a supported image format. + - **`404 Not Found`**: `{"detail": "User not found"}` + +--- + +#### `GET /api/users/{user_id}/attempts` +- **Purpose:** List historical exam test attempts for a student, with scores, accuracy, and timestamps. +- **Access:** Authenticated User +- **Request Headers:** + - `Accept: application/json` + - `Cookie: auth_token=` +- **Request Path Parameters:** + - `user_id` (UUID, required): Student UUID +- **Response Headers:** + - `Content-Type: application/json` +- **Status Codes & Response Payloads:** + - **`200 OK`**: Array of `models.Attempt` records: + ```json + [ + { + "id": "e4f5a6b7-c8d9-0e1f-2a3b-4c5d6e7f8a9b", + "user_id": "b0e17378-864d-439e-8806-708dc48a891b", + "source_type": "Test", + "source_id": "b6faa295-3bf5-4de2-a587-d72b889629c4", + "score": 184, + "max_score": 300, + "accuracy": 74.2, + "time_taken": 9840, + "retake_count": 0, + "created_at": "2026-09-02T14:30:00.000000" + } + ] + ``` + - **`404 Not Found`**: If student does not exist. + +--- + +### 3.3 CBT Testing Engine & Question Bank + +#### `GET /api/tests` +- **Purpose:** List active examination tests (Mock Tests, Weekly Tests, PYQs, DPS). +- **Access:** Authenticated User +- **Request Headers:** + - `Accept: application/json` + - `Cookie: auth_token=` +- **Request Query Parameters:** + - `exam_pattern` (string, optional): `JEE_Main` | `JEE_Advanced` | `NEET` + - `type` (string, optional): `Mock` | `Weekly` | `PYQ` | `DPS`. If omitted, automatically excludes `DPS` to keep Mock Tests separate. +- **Response Headers:** + - `Content-Type: application/json` +- **Status Codes & Response Payloads:** + - **`200 OK`**: Array of `models.Test` objects: + ```json + [ + { + "id": "b6faa295-3bf5-4de2-a587-d72b889629c4", + "title": "JEE Main Full Mock Test #1 (2025)", + "type": "Mock", + "exam_pattern": "JEE_Main", + "sections": { + "Physics Section A": { "questions": ["uuid-1", "uuid-2"], "time_limit": 3600 }, + "Chemistry Section A": { "questions": ["uuid-3", "uuid-4"], "time_limit": 3600 } + }, + "total_time": 10800, + "difficulty": "Mixed", + "is_active": true + } + ] + ``` + +--- + +#### `GET /api/tests/{test_id}` +- **Purpose:** Retrieve specific test configuration and section mapping. +- **Access:** Authenticated User +- **Request Headers:** + - `Accept: application/json` + - `Cookie: auth_token=` +- **Request Path Parameters:** + - `test_id` (UUID, required): Test ID +- **Request Query Parameters:** + - `user_id` (UUID, optional): Optional student UUID to check eligibility +- **Response Headers:** + - `Content-Type: application/json` +- **Status Codes & Response Payloads:** + - **`200 OK`**: Returns single `models.Test` record. + - **`404 Not Found`**: `{"detail": "Requested examination structure not found."}` + - **`422 Unprocessable Entity`**: Invalid UUID format. + +--- + +#### `GET /api/tests/{test_id}/questions` +- **Purpose:** Fetch all full Question records belonging to a test's sections. +- **Access:** Authenticated User +- **Request Headers:** + - `Accept: application/json` + - `Cookie: auth_token=` +- **Request Path Parameters:** + - `test_id` (UUID, required): Test ID +- **Response Headers:** + - `Content-Type: application/json` +- **Status Codes & Response Payloads:** + - **`200 OK`**: Array of `models.Question` entities: + ```json + [ + { + "id": "00000000-0000-0000-0000-000000000001", + "subject": "Physics", + "topic": "Kinematics", + "subtopic": "Relative Motion", + "difficulty": "Medium", + "type": "MCQ_Single", + "question_text": "A particle moves along a straight line with constant acceleration...", + "question_text_hi": "एक कण स्थिर त्वरण के साथ एक सीधी रेखा में गति करता है...", + "options": { + "A": "10 m/s", + "B": "20 m/s", + "C": "30 m/s", + "D": "40 m/s" + }, + "correct_answer": { "correct": "B" }, + "solution_text": "Using kinematic formula v = u + at...", + "language": "Bilingual", + "exam": "JEE_Main", + "is_active": true + } + ] + ``` + - **`404 Not Found`**: If `test_id` is invalid or contains no questions. + +--- + +#### `GET /api/questions/{question_id}` +- **Purpose:** Fetch single question data with complete step-by-step solution text. +- **Access:** Authenticated User +- **Request Headers:** + - `Accept: application/json` + - `Cookie: auth_token=` +- **Request Path Parameters:** + - `question_id` (UUID, required) +- **Response Headers:** + - `Content-Type: application/json` +- **Status Codes & Response Payloads:** + - **`200 OK`**: Single `models.Question` record. + - **`404 Not Found`**: `{"detail": "Requested question not found."}` + +--- + +#### `POST /api/attempts/submit` +- **Purpose:** Core grading engine. Auto-grades student answers, calculates accuracy %, tracks retakes, and automatically records mistakes into `models.Mistake` with SuperMemo-2 spaced repetition intervals. +- **Access:** Authenticated User +- **Request Headers:** + - `Accept: application/json` + - `Content-Type: application/json` + - `Cookie: auth_token=` +- **Request Body Schema:** + ```json + { + "user_id": "b0e17378-864d-439e-8806-708dc48a891b", + "source_type": "Test", + "source_id": "b6faa295-3bf5-4de2-a587-d72b889629c4", + "time_taken": 3600, + "answers": [ + { + "question_id": "00000000-0000-0000-0000-000000000001", + "selected_answer": "B", + "is_subjective": false, + "time_spent": 45 + } + ] + } + ``` +- **Response Headers:** + - `Content-Type: application/json` +- **Status Codes & Response Payloads:** + - **`200 OK`**: Returns graded `models.Attempt` record with total score, accuracy %, and mistake IDs recorded. + - **`404 Not Found`**: If `user_id` or test source does not exist. + - **`422 Unprocessable Entity`**: Malformed payload. + +--- + +### 3.4 AI Coach, Tutor Stream & Spaced Repetition + +#### `POST /api/ai-coach/chat/stream` +- **Purpose:** Real-time conversational AI tutor for instant doubt resolution. Streams response tokens using Server-Sent Events (SSE). +- **Access:** Gated to `Al_Coach` tier (`HTTP 403` if Free or Pro) +- **Request Headers:** + - `Accept: text/event-stream` + - `Content-Type: application/json` + - `Cache-Control: no-cache` + - `Cookie: auth_token=` +- **Request Body Schema:** + ```json + { + "user_id": "b0e17378-864d-439e-8806-708dc48a891b", + "message": "Why does friction act forward on the driven wheel of an accelerating car?" + } + ``` +- **Response Headers:** + - `Content-Type: text/event-stream; charset=utf-8` + - `Cache-Control: no-cache, no-transform` + - `Connection: keep-alive` + - `X-Accel-Buffering: no` +- **Response Stream Chunks:** + ``` + data: When + data: a + data: car + data: accelerates, + data: the + data: engine + data: applies + data: torque... + ``` +- **Status Codes:** + - **`200 OK`**: Server-Sent Events stream initiated. + - **`403 Forbidden`**: `{"detail": "Access denied. Gated to AI_COACH tier."}` + - **`404 Not Found`**: `{"detail": "Student profile not found."}` + - **`422 Unprocessable Entity`**: If `user_id` or `message` is missing. + +--- + +#### `GET /api/ai-coach/study-plan/today` +- **Purpose:** Retrieve today's personalized AI study tasks generated nightly by Celery Beat scheduler based on weak areas and pending mistakes. +- **Access:** Gated to `Al_Coach` tier (`HTTP 403` if Free or Pro) +- **Request Headers:** + - `Accept: application/json` + - `Cookie: auth_token=` +- **Request Query Parameters:** + - `user_id` (UUID, required): Student UUID +- **Response Headers:** + - `Content-Type: application/json` +- **Status Codes & Response Payloads:** + - **`200 OK`**: + ```json + { + "id": "7f8e9d0a-1b2c-3d4e-5f6a-7b8c9d0e1f2a", + "user_id": "b0e17378-864d-439e-8806-708dc48a891b", + "date": "2026-09-04", + "tasks": [ + { + "type": "Revision", + "description": "Revise Rotational Mechanics", + "duration_minutes": 15, + "status": "Pending" + }, + { + "type": "Mistake_Review", + "description": "Daily Mistake Review Session", + "duration_minutes": 10, + "status": "Pending" + }, + { + "type": "DPS", + "description": "Attempt Rotational Mechanics Practice Sheet", + "duration_minutes": 20, + "status": "Pending" + } + ], + "is_completed": false + } + ``` + - **`403 Forbidden`**: If user is not on `Al_Coach` tier. + - **`404 Not Found`**: `{"detail": "No study plan generated for today yet."}` + - **`422 Unprocessable Entity`**: If `user_id` query param is omitted. + +--- + +#### `GET /api/ai-coach/readiness/{user_id}` +- **Purpose:** Computes weighted exam readiness score, predicts All India Rank (AIR), and estimates probability of admission to top IITs/AIIMS colleges. +- **Access:** Gated to `Al_Coach` tier +- **Request Headers:** + - `Accept: application/json` + - `Cookie: auth_token=` +- **Request Path Parameters:** + - `user_id` (UUID, required): Student UUID +- **Response Headers:** + - `Content-Type: application/json` +- **Status Codes & Response Payloads:** + - **`200 OK`**: + ```json + { + "readiness_score": 78.4, + "predicted_rank": 2840, + "target_exam": "JEE_Main", + "predicted_college": "IIT Delhi (Mechanical / Chemical)", + "probability": 84.5, + "weak_topics": ["Thermodynamics", "Wave Optics"], + "strong_topics": ["Kinematics", "Electrostatics"] + } + ``` + - **`403 Forbidden`**: If user is not subscribed to `Al_Coach`. + - **`404 Not Found`**: If user does not exist. + +--- + +#### `GET /api/mistakes/today` +- **Purpose:** Retrieve mistake notebook items scheduled for review today based on SuperMemo-2 (SM-2) spaced repetition interval. +- **Access:** Authenticated User +- **Request Headers:** + - `Accept: application/json` + - `Cookie: auth_token=` +- **Request Query Parameters:** + - `user_id` (UUID, required): Student UUID +- **Response Headers:** + - `Content-Type: application/json` +- **Status Codes & Response Payloads:** + - **`200 OK`**: Array of `models.Mistake` items with associated question context: + ```json + [ + { + "id": "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d", + "user_id": "b0e17378-864d-439e-8806-708dc48a891b", + "question_id": "00000000-0000-0000-0000-000000000001", + "mistake_type": "Calculation_Error", + "review_count": 1, + "interval_days": 3, + "ease_factor": 2.5, + "next_review": "2026-09-04", + "resolved": false + } + ] + ``` + - **`422 Unprocessable Entity`**: If `user_id` parameter is missing. + +--- + +#### `POST /api/mistakes/review` +- **Purpose:** Update spaced repetition schedule after reviewing a mistake item (SM-2 algorithm calculates next review interval based on student recall quality 0-5). +- **Access:** Authenticated User +- **Request Headers:** + - `Accept: application/json` + - `Content-Type: application/json` + - `Cookie: auth_token=` +- **Request Body Schema:** + ```json + { + "mistake_id": "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d", + "quality": 4 + } + ``` +- **Response Headers:** + - `Content-Type: application/json` +- **Status Codes & Response Payloads:** + - **`200 OK`**: + ```json + { + "success": true, + "next_review": "2026-09-10", + "interval_days": 6, + "ease_factor": 2.6 + } + ``` + - **`404 Not Found`**: `{"detail": "Mistake record not found"}` + +--- + +### 3.5 Subscription, Billing & Gateways + +#### `POST /api/billing/create-order` +- **Purpose:** Initiates Razorpay payment order for tier upgrade (`Pro` or `Al_Coach`). +- **Access:** Authenticated User +- **Request Headers:** + - `Accept: application/json` + - `Content-Type: application/json` + - `Cookie: auth_token=` +- **Request Body Schema:** + ```json + { + "user_id": "b0e17378-864d-439e-8806-708dc48a891b", + "tier": "Al_Coach", + "amount": 299900, + "coupon_code": "DISCOUNT20" + } + ``` +- **Response Headers:** + - `Content-Type: application/json` +- **Status Codes & Response Payloads:** + - **`200 OK`**: + ```json + { + "order_id": "order_EKf34dhu72ndD2", + "amount": 239920, + "currency": "INR", + "key_id": "rzp_test_YOUR_KEY" + } + ``` + - **`400 Bad Request`**: `{"detail": "Invalid tier or invalid coupon code."}` + +--- + +#### `POST /api/billing/verify-payment` +- **Purpose:** Verifies Razorpay payment signature HMAC-SHA256, records transaction, and upgrades user's tier. +- **Access:** Authenticated User +- **Request Headers:** + - `Accept: application/json` + - `Content-Type: application/json` + - `Cookie: auth_token=` +- **Request Body Schema:** + ```json + { + "user_id": "b0e17378-864d-439e-8806-708dc48a891b", + "razorpay_order_id": "order_EKf34dhu72ndD2", + "razorpay_payment_id": "pay_29QQoUBi66xm2f", + "razorpay_signature": "9ef4705b83f6c19ff76543210abcd1234567890abcdef1234567890abcdef", + "tier": "Al_Coach" + } + ``` +- **Response Headers:** + - `Content-Type: application/json` +- **Status Codes & Response Payloads:** + - **`200 OK`**: + ```json + { + "success": true, + "tier": "Al_Coach", + "message": "Payment verified and tier upgraded successfully." + } + ``` + - **`400 Bad Request`**: `{"detail": "Payment verification failed. Invalid signature."}` + +--- + +#### `POST /api/billing/webhook/razorpay` +- **Purpose:** Asynchronous payment confirmation webhook from Razorpay for recurring charges or asynchronous UPI captures. +- **Access:** Public (Protected by HMAC Webhook Secret) +- **Request Headers:** + - `Accept: application/json` + - `Content-Type: application/json` + - `X-Razorpay-Signature: ` +- **Request Body Schema:** Standard Razorpay Webhook Event payload: + ```json + { + "entity": "event", + "event": "payment.captured", + "payload": { + "payment": { + "entity": { + "id": "pay_29QQoUBi66xm2f", + "amount": 239920, + "status": "captured", + "order_id": "order_EKf34dhu72ndD2" + } + } + } + } + ``` +- **Response Headers:** + - `Content-Type: application/json` +- **Status Codes & Response Payloads:** + - **`200 OK`**: `{"status": "ok"}` + - **`400 Bad Request`**: `{"detail": "Invalid webhook signature."}` + +--- + +### 3.6 Admin Control Panel Subsystem + +All Admin endpoints require superadmin access: +- **Required Cookie:** `Cookie: auth_token=admin` +- **Default Superadmin:** `admin@pragativeer.com` / `admin123` + +--- + +#### `GET /api/admin/settings` +- **Purpose:** Retrieve system-wide runtime configuration (maintenance mode, free trial flags, AI model selection). +- **Access:** Admin +- **Request Headers:** + - `Accept: application/json` + - `Cookie: auth_token=admin` +- **Response Headers:** + - `Content-Type: application/json` +- **Status Codes & Response Payloads:** + - **`200 OK`**: + ```json + { + "maintenance_mode": false, + "allow_free_trials": true, + "ai_model": "gemini-2.5-pro", + "max_daily_questions": 150 + } + ``` + - **`401 Unauthorized`**: If admin cookie is missing or invalid. + +--- + +#### `PUT /api/admin/settings` +- **Purpose:** Update system-wide configuration settings. +- **Access:** Admin +- **Request Headers:** + - `Accept: application/json` + - `Content-Type: application/json` + - `Cookie: auth_token=admin` +- **Request Body Schema:** + ```json + { + "maintenance_mode": false, + "allow_free_trials": true, + "ai_model": "gemini-2.5-pro" + } + ``` +- **Response Headers:** + - `Content-Type: application/json` +- **Status Codes & Response Payloads:** + - **`200 OK`**: Returns updated settings object. + - **`401 Unauthorized`**: If unauthorized. + +--- + +#### `GET /api/admin/tests` +- **Purpose:** List all tests, PYQs, and DPS (including drafts and inactive tests). +- **Access:** Admin +- **Request Headers:** + - `Accept: application/json` + - `Cookie: auth_token=admin` +- **Response Headers:** + - `Content-Type: application/json` +- **Status Codes & Response Payloads:** + - **`200 OK`**: Array of all `models.Test` records. + +--- + +#### `POST /api/admin/tests/upload` +- **Purpose:** Upload PDF exam paper to MinIO storage and dispatch background AI parsing task to Celery worker. +- **Access:** Admin +- **Request Headers:** + - `Accept: application/json` + - `Content-Type: multipart/form-data; boundary=----WebKitFormBoundary...` + - `Cookie: auth_token=admin` +- **Request Form Data:** + - `file`: PDF binary (`application/pdf`) + - `exam_pattern`: `JEE_Main` | `JEE_Advanced` | `NEET` + - `title`: Test name string +- **Response Headers:** + - `Content-Type: application/json` +- **Status Codes & Response Payloads:** + - **`200 OK`**: + ```json + { + "task_id": "c56d78ef-12ab-34cd-56ef-7890abcdef12", + "status": "Processing", + "message": "PDF uploaded to MinIO. Celery AI parser dispatched." + } + ``` + - **`400 Bad Request`**: If file is not a valid PDF. + +--- + +#### `POST /api/admin/tests/create_manual` +- **Purpose:** Manually create a CBT mock test structure and map questions. +- **Access:** Admin +- **Request Headers:** + - `Accept: application/json` + - `Content-Type: application/json` + - `Cookie: auth_token=admin` +- **Request Body Schema:** + ```json + { + "title": "All India Grand Mock Test #5", + "type": "Mock", + "exam_pattern": "JEE_Main", + "total_time": 10800, + "difficulty": "Hard", + "sections": { + "Physics": { "questions": [], "time_limit": 3600 } + } + } + ``` +- **Response Headers:** + - `Content-Type: application/json` +- **Status Codes & Response Payloads:** + - **`200 OK`**: Created `models.Test` record. + +--- + +#### `PUT /api/admin/tests/{test_id}/toggle` +- **Purpose:** Toggle test `is_active` status (publish/unpublish). +- **Access:** Admin +- **Request Headers:** + - `Accept: application/json` + - `Cookie: auth_token=admin` +- **Request Path Parameters:** + - `test_id` (UUID, required) +- **Response Headers:** + - `Content-Type: application/json` +- **Status Codes & Response Payloads:** + - **`200 OK`**: `{"id": "...", "is_active": true}` + - **`404 Not Found`**: If test does not exist. + +--- + +#### `DELETE /api/admin/tests/{test_id}` +- **Purpose:** Permanently delete a test and dissociate question mappings. +- **Access:** Admin +- **Request Headers:** + - `Accept: application/json` + - `Cookie: auth_token=admin` +- **Request Path Parameters:** + - `test_id` (UUID, required) +- **Response Headers:** + - `Content-Type: application/json` +- **Status Codes & Response Payloads:** + - **`200 OK`**: `{"success": true, "message": "Test deleted successfully."}` + - **`404 Not Found`**: If test does not exist. + +--- + +#### `GET /api/admin/coupons` +- **Purpose:** List all promotional discount coupons. +- **Access:** Admin +- **Request Headers:** + - `Accept: application/json` + - `Cookie: auth_token=admin` +- **Response Headers:** + - `Content-Type: application/json` +- **Status Codes & Response Payloads:** + - **`200 OK`**: Array of `models.Coupon` records. + +--- + +#### `POST /api/admin/coupons` +- **Purpose:** Create a new promotional discount coupon. +- **Access:** Admin +- **Request Headers:** + - `Accept: application/json` + - `Content-Type: application/json` + - `Cookie: auth_token=admin` +- **Request Body Schema:** + ```json + { + "code": "FESTIVE25", + "discount_percentage": 25, + "max_uses": 500, + "valid_until": "2026-12-31T23:59:59.000000" + } + ``` +- **Response Headers:** + - `Content-Type: application/json` +- **Status Codes & Response Payloads:** + - **`200 OK`**: Returns created `models.Coupon` entity. + +--- + +#### `DELETE /api/admin/coupons/{coupon_id}` +- **Purpose:** Delete promotional coupon. +- **Access:** Admin +- **Request Headers:** + - `Accept: application/json` + - `Cookie: auth_token=admin` +- **Request Path Parameters:** + - `coupon_id` (UUID, required) +- **Response Headers:** + - `Content-Type: application/json` +- **Status Codes & Response Payloads:** + - **`200 OK`**: `{"success": true, "message": "Coupon deleted"}` + - **`404 Not Found`**: `{"detail": "Coupon not found"}` + +--- + +#### `GET /api/admin/students` +- **Purpose:** List all registered students with attempts and accuracy metrics. +- **Access:** Admin +- **Request Headers:** + - `Accept: application/json` + - `Cookie: auth_token=admin` +- **Response Headers:** + - `Content-Type: application/json` +- **Status Codes & Response Payloads:** + - **`200 OK`**: Array of student summary objects. + +--- + +#### `GET /api/admin/students/{user_id}` +- **Purpose:** Detailed student profile with weak topics, recent tests, transactions. +- **Access:** Admin +- **Request Headers:** + - `Accept: application/json` + - `Cookie: auth_token=admin` +- **Request Path Parameters:** + - `user_id` (UUID, required) +- **Response Headers:** + - `Content-Type: application/json` +- **Status Codes & Response Payloads:** + - **`200 OK`**: Detailed student dossier. + - **`404 Not Found`**: Student not found. + +--- + +#### `PATCH /api/admin/students/{user_id}` +- **Purpose:** Modify student tier, streak count, school, city, or exam. +- **Access:** Admin +- **Request Headers:** + - `Accept: application/json` + - `Content-Type: application/json` + - `Cookie: auth_token=admin` +- **Request Path Parameters:** + - `user_id` (UUID, required) +- **Request Body Schema:** + ```json + { + "tier": "Al_Coach", + "streak_count": 15 + } + ``` +- **Response Headers:** + - `Content-Type: application/json` +- **Status Codes & Response Payloads:** + - **`200 OK`**: Returns modified `models.User` record. + - **`404 Not Found`**: Student not found. + +--- + +### 3.7 Parent Subsystem & Viral Growth + +#### `POST /api/parent/register` +- **Purpose:** Register parent user profile in `models.Parent`. +- **Access:** Public +- **Request Headers:** + - `Accept: application/json` + - `Content-Type: application/json` +- **Request Body Schema:** + ```json + { + "name": "Rajesh Kumar", + "phone": "9876543210", + "email": "parent@example.com", + "password_hash": "hashed_password" + } + ``` +- **Response Headers:** + - `Content-Type: application/json` +- **Status Codes & Response Payloads:** + - **`200 OK`**: + ```json + { + "id": "c1d2e3f4-5a6b-7c8d-9e0f-1a2b3c4d5e6f", + "name": "Rajesh Kumar", + "phone": "9876543210", + "email": "parent@example.com" + } + ``` + - **`400 Bad Request`**: If phone or email is already registered. + +--- + +#### `POST /api/parent/link-student` +- **Purpose:** Link a student's profile to a parent account using student's phone or email. +- **Access:** Parent User +- **Request Headers:** + - `Accept: application/json` +- **Request Query Parameters:** + - `parent_id` (UUID, required): Parent UUID + - `student_phone_or_email` (string, required): Student's registered phone or email +- **Response Headers:** + - `Content-Type: application/json` +- **Status Codes & Response Payloads:** + - **`200 OK`**: + ```json + { + "success": true, + "message": "Student linked to parent account successfully." + } + ``` + - **`404 Not Found`**: `{"detail": "Student or parent account not found."}` + +--- + +#### `GET /api/parent/{parent_id}/dashboard/{student_id}` +- **Purpose:** Generate complete parent report with test summary, syllabus progress, and AI recommendations. +- **Access:** Parent User +- **Request Headers:** + - `Accept: application/json` +- **Request Path Parameters:** + - `parent_id` (UUID, required) + - `student_id` (UUID, required) +- **Response Headers:** + - `Content-Type: application/json` +- **Status Codes & Response Payloads:** + - **`200 OK`**: + ```json + { + "student_name": "Rahul Sharma", + "target_exam": "JEE_Main", + "tests_attempted": 8, + "average_accuracy": 74.2, + "current_streak": 6, + "ai_insight": "Rahul is performing exceptionally well in Kinematics but needs focused revision on Thermodynamics." + } + ``` + - **`404 Not Found`**: If parent or student record does not exist. + +--- + +#### `GET /api/growth/referral/{user_id}` +- **Purpose:** Generates referral code, viral share URL, referral count, and milestone unlocks. +- **Access:** Authenticated User +- **Request Headers:** + - `Accept: application/json` + - `Cookie: auth_token=` +- **Request Path Parameters:** + - `user_id` (UUID, required): Student UUID +- **Response Headers:** + - `Content-Type: application/json` +- **Status Codes & Response Payloads:** + - **`200 OK`**: + ```json + { + "referral_code": "PV-RAHUL-1A2B", + "referral_url": "http://localhost:3000/signup?ref=PV-RAHUL-1A2B", + "total_referred": 3, + "unlocked_rewards": ["1 Free Mock Test", "7 Days AI Coach Trial"] + } + ``` + - **`404 Not Found`**: If student does not exist. + +--- + +### 3.8 Next.js Route Handlers & Edge Handlers + +#### `POST /api/auth/forgot-password` +- **Location:** `src/app/api/auth/forgot-password/route.ts` +- **Runtime:** Node.js App Router +- **Access:** Public +- **Request Headers:** + - `Accept: application/json` + - `Content-Type: application/json` +- **Request Body Schema:** + ```json + { + "email": "student@pragativeer.com" + } + ``` +- **Response Headers:** + - `Content-Type: application/json` +- **Status Codes & Response Payloads:** + - **`200 OK`**: + ```json + { + "success": true, + "message": "Reset instructions sent." + } + ``` + - **`400 Bad Request`**: `{"detail": "Email address is required."}` + +--- + +#### `GET /api/og/progress` +- **Location:** `src/app/api/og/progress/route.tsx` +- **Runtime:** Edge (`@vercel/og` ImageResponse) +- **Access:** Public +- **Request Headers:** + - `Accept: image/png, image/*` +- **Request Query Parameters:** + - `streak` (integer, optional): Day streak count (e.g. `12`) + - `accuracy` (number, optional): Accuracy percentage (e.g. `78`) + - `name` (string, optional): Student name (e.g. `Rahul`) +- **Response Headers:** + - `Content-Type: image/png` + - `Cache-Control: public, max-age=31536000, immutable` +- **Status Codes & Response Payloads:** + - **`200 OK`**: Dynamic 1200x630 PNG social card image. + +--- + +## 4. Planned APIs (Roadmap / Frontend Mock Status) + +The following endpoints are specified in [PRD.md](file:///c:/Users/naman/OneDrive/Desktop/pragativeer-v2/PRD.md) or rendered using client-side mock data in the UI: + +| Planned API | Expected Route | Frontend UI File | Backend Provisioning Needed | +| :--- | :--- | :--- | :--- | +| **Leaderboard API** | `GET /api/leaderboard` | [leaderboard/page.tsx](file:///c:/Users/naman/OneDrive/Desktop/pragativeer-v2/src/app/dashboard/leaderboard/page.tsx) | Redis sorted set queries (`ZREVRANGE`) by exam/state | +| **Student Analytics API** | `GET /api/users/{id}/analytics` | [analytics/page.tsx](file:///c:/Users/naman/OneDrive/Desktop/pragativeer-v2/src/app/dashboard/analytics/page.tsx) | Weekly score trajectories and topic heatmap accuracies | +| **Streak Rewards API** | `GET /api/streaks/rewards` | [streaks/page.tsx](file:///c:/Users/naman/OneDrive/Desktop/pragativeer-v2/src/app/dashboard/streaks/page.tsx) | Gamification milestone reward unlocks | +| **Syllabus Progress Tracker** | `GET/PUT /api/syllabus/progress` | [progress/page.tsx](file:///c:/Users/naman/OneDrive/Desktop/pragativeer-v2/src/app/dashboard/progress/page.tsx) | Chapter-by-chapter completion % per subject | +| **Community Doubts Feed** | `GET/POST /api/community/doubts` | [community/page.tsx](file:///c:/Users/naman/OneDrive/Desktop/pragativeer-v2/src/app/dashboard/community/page.tsx) | Discussion forum posts, replies, and upvotes | +| **Study Materials Library** | `GET /api/materials` | [materials/page.tsx](file:///c:/Users/naman/OneDrive/Desktop/pragativeer-v2/src/app/dashboard/materials/page.tsx) | MinIO PDF formula sheet catalog and access controls | +| **Voice AI & Live Battle** | `/ws/battles`, `/api/ai-coach/voice` | Future Scope | WebSockets for multiplayer CBT battles & Whisper voice | + +--- + +## 5. Seeded Credentials Reference + +| Role | Email | Password | Tier / Features | +| :--- | :--- | :--- | :--- | +| **AI Coach Student** | `demo@pragativeer.com` | `Pv9#kL2$mQ8!vX4@` | AI Coach (AI Tutor, College Predictor, Spaced Repetition) | +| **AI Coach Student** | `student@pragativeer.com` | `Password123!` | Rahul Sharma (Default AI Coach Profile) | +| **AI Coach Student** | `neha@pragativeer.com` | `Password123!` | Neha Verma (62-Day Streak presentation account) | +| **Pro Tier Student** | `pro@pragativeer.com` | `Password123!` | Priya Singh (Pro Tier, Full CBT engine access) | +| **Free Tier Student** | `free@pragativeer.com` | `Password123!` | Amit Kumar (Free Tier, baseline practice) | +| **Super Admin** | `admin@pragativeer.com` | `admin123` | Full access to `/admin` management console | diff --git a/backend/main.py b/backend/main.py index 47ce98a..b561846 100644 --- a/backend/main.py +++ b/backend/main.py @@ -123,22 +123,33 @@ def register(payload: RegisterPayload, session: Session = Depends(get_session)): def login(payload: LoginPayload, session: Session = Depends(get_session)): import bcrypt - user = session.exec(select(models.User).where(models.User.email == payload.email)).first() - # Allow demo/admin mock logic to still work if needed if payload.email == "demo@pragativeer.com" and payload.password == "Pv9#kL2$mQ8!vX4@": - return {"success": True, "role": "user", "token": "mock_token"} + demo_user = session.exec(select(models.User).where(models.User.email == "student@pragativeer.com")).first() + token_id = str(demo_user.id) if demo_user else "b0e17378-864d-439e-8806-708dc48a891b" + return {"success": True, "role": "user", "token": token_id} if payload.email == "admin@pragativeer.com" and payload.password == "admin123": - return {"success": True, "role": "admin", "token": "mock_admin_token"} + return {"success": True, "role": "admin", "token": "admin"} + user = session.exec(select(models.User).where(models.User.email == payload.email)).first() if not user: raise HTTPException(status_code=401, detail="Invalid email or password") + # Handle legacy mock_hash from initial seed + if user.password_hash == "mock_hash": + if payload.password in ("Password123!", "Pv9#kL2$mQ8!vX4@", "admin123"): + 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)} + 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": "user", "token": str(user.id)} + return {"success": True, "role": "admin" if (user.email and "admin" in user.email) else "user", "token": str(user.id)} # --- CBT ENGINE --- @@ -154,7 +165,7 @@ 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: UUID, session: Session = Depends(get_session)): +def get_test_by_id(test_id: UUID, user_id: Optional[UUID] = None, session: Session = Depends(get_session)): test = session.get(models.Test, test_id) if not test: raise HTTPException(status_code=404, detail="Requested examination structure not found.") @@ -444,20 +455,6 @@ def get_parent_dashboard_metrics(parent_id: UUID, student_id: UUID, session: Ses # --- BILLING & SUBSCRIPTION --- -@app.post("/api/billing/create-order", tags=["Subscription Engine"]) -def create_subscription_order(user_id: UUID, plan_tier: str, session: Session = Depends(get_session)): - user = session.get(models.User, user_id) - if not user: - raise HTTPException(status_code=404, detail="User not found.") - - pricing_map = {"Pro": 29900, "Al_Coach": 59900} - amount = pricing_map.get(plan_tier) - if not amount: - raise HTTPException(status_code=400, detail="Invalid subscription tier requested.") - - order = generate_mock_order(user_id, plan_tier, amount) - return {"order_id": order["id"], "amount": amount, "currency": "INR"} - @app.post("/api/billing/webhook/razorpay", tags=["Subscription Engine"]) async def razorpay_webhook_handler(request: Request, session: Session = Depends(get_session)): payload_bytes = await request.body() @@ -943,7 +940,9 @@ def delete_coupon(coupon_id: UUID, session: Session = Depends(get_session)): session.delete(coupon) session.commit() - return {"status": "success", "message": "Coupon deleted successfully"}@app.post("/api/users/{user_id}/avatar", tags=["Users"]) + return {"status": "success", "message": "Coupon deleted successfully"} + +@app.post("/api/users/{user_id}/avatar", tags=["Users"]) async def upload_avatar(user_id: UUID, file: UploadFile = File(...), session: Session = Depends(get_session)): user = session.get(models.User, user_id) if not user: @@ -1186,21 +1185,19 @@ def verify_payment(payload: VerifyPaymentPayload, session: Session = Depends(get raise HTTPException(status_code=400, detail="Invalid gateway") # Update user's tier - if payload.tier == "Al_Coach": - user.tier = models.TierEnum.al_coach + if payload.tier in ("Al_Coach", "AI_Coach"): + user.tier = models.TierEnum.AI_COACH elif payload.tier == "Pro": - user.tier = models.TierEnum.pro + user.tier = models.TierEnum.PRO session.add(user) # Log transaction txn = models.Transaction( user_id=user.id, - amount=11999.00 if payload.tier == "Al_Coach" else 3499.00, - currency="INR", - status="completed", - razorpay_order_id=payload.razorpay_order_id if payload.gateway == "razorpay" else None, - razorpay_payment_id=payload.razorpay_payment_id if payload.gateway == "razorpay" else (payload.phonepe_transaction_id or payload.stripe_session_id) + amount=11999.00 if payload.tier in ("Al_Coach", "AI_Coach") else 3499.00, + tier=payload.tier, + status="completed" ) session.add(txn) session.commit() diff --git a/backend/seed.py b/backend/seed.py index a31a4d0..ff21350 100644 --- a/backend/seed.py +++ b/backend/seed.py @@ -5,6 +5,9 @@ import os from database import engine, init_db import models +import bcrypt + +DEFAULT_SEED_HASH = bcrypt.hashpw("Password123!".encode('utf-8'), bcrypt.gensalt()).decode('utf-8') def seed_local_database(): # Ensure tables are created @@ -31,7 +34,7 @@ def seed_local_database(): id=free_student_id, phone="8888888888", email="free@pragativeer.com", - password_hash="mock_hash", + password_hash=DEFAULT_SEED_HASH, name="Amit Kumar (Free Tier)", student_class=models.ClassEnum.TWELFTH, medium=models.MediumEnum.ENGLISH, @@ -49,7 +52,7 @@ def seed_local_database(): id=pro_student_id, phone="7777777777", email="pro@pragativeer.com", - password_hash="mock_hash", + password_hash=DEFAULT_SEED_HASH, name="Priya Singh (Pro Tier)", student_class=models.ClassEnum.TWELFTH, medium=models.MediumEnum.ENGLISH, @@ -68,7 +71,7 @@ def seed_local_database(): id=neha_id, phone="6666666666", email="neha@pragativeer.com", - password_hash="mock_hash", + password_hash=DEFAULT_SEED_HASH, name="Neha Verma", student_class=models.ClassEnum.TWELFTH, medium=models.MediumEnum.ENGLISH, @@ -117,7 +120,7 @@ def seed_local_database(): id=student_id, phone="9999999999", email="student@pragativeer.com", - password_hash="mock_hash", + password_hash=DEFAULT_SEED_HASH, name="Rahul Sharma (AI Coach Tier)", student_class=models.ClassEnum.TWELFTH, medium=models.MediumEnum.ENGLISH, diff --git a/src/app/dashboard/layout.tsx b/src/app/dashboard/layout.tsx index dacf9b7..0e4a5e8 100644 --- a/src/app/dashboard/layout.tsx +++ b/src/app/dashboard/layout.tsx @@ -20,7 +20,7 @@ export default function DashboardLayout({ children }: { children: ReactNode }) { const [dropdownOpen, setDropdownOpen] = useState(false) const dropdownRef = useRef(null) - const { userId, user, globalSettings, switchUser, hasAccessTo, loading } = useActiveUser(); + const { userId, user, globalSettings, switchUser, logout, hasAccessTo, loading } = useActiveUser(); const [maintenance, setMaintenance] = useState(false); const [checkingMaintenance, setCheckingMaintenance] = useState(true); @@ -264,6 +264,13 @@ export default function DashboardLayout({ children }: { children: ReactNode }) { > Reset Demo +
+
)} diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index c5e67f4..e349b6d 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -30,22 +30,32 @@ export default function LoginPage() { if (data.success) { if (typeof window !== "undefined") { - localStorage.setItem("activeUserId", data.token) + // Set auth_token cookie required by Next.js 16 proxy.ts middleware + 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("active_session_token", sessionToken); - fetch(`/api/users/${data.token}/session`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ session_token: sessionToken }) - }).finally(() => { - if (data.role === "admin") { - window.location.href = "/admin"; - } else { - window.location.href = "/dashboard"; + if (data.token && data.token !== "admin") { + try { + await fetch(`/api/users/${data.token}/session`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ session_token: sessionToken }) + }); + } catch (e) { + console.warn("Session update skipped", e); } - }); + } + + if (data.role === "admin") { + window.location.href = "/admin"; + } else { + window.location.href = "/dashboard"; + } } } else { setError(data.error || "Invalid email or password.") @@ -54,6 +64,7 @@ export default function LoginPage() { setError("An error occurred. Please try again.") } } + return (
diff --git a/src/app/signup/page.tsx b/src/app/signup/page.tsx index 545cb99..da4ec26 100644 --- a/src/app/signup/page.tsx +++ b/src/app/signup/page.tsx @@ -46,6 +46,7 @@ export default function SignupPage() { const loginData = await loginRes.json(); 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("active_session_token", sessionToken); diff --git a/src/hooks/useActiveUser.ts b/src/hooks/useActiveUser.ts index bddb4b3..b381571 100644 --- a/src/hooks/useActiveUser.ts +++ b/src/hooks/useActiveUser.ts @@ -123,6 +123,7 @@ export function useActiveUser() { const switchUser = useCallback(async (newId: string) => { if (typeof window !== "undefined") { + document.cookie = `auth_token=${newId}; path=/; max-age=2592000; SameSite=Lax`; const sessionToken = crypto.randomUUID(); localStorage.setItem("active_session_token", sessionToken); @@ -142,6 +143,15 @@ export function useActiveUser() { } }, []); + const logout = useCallback(() => { + if (typeof window !== "undefined") { + document.cookie = "auth_token=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT"; + localStorage.removeItem("activeUserId"); + localStorage.removeItem("active_session_token"); + window.location.href = "/login"; + } + }, []); + const hasAccessTo = useCallback((feature: string): boolean => { if (!user) return false; let tier = user.tier; @@ -178,14 +188,11 @@ export function useActiveUser() { case "pdf-export": return tier === "Pro" || tier === "Al_Coach"; - case "ai-mistake-tracking": - case "spaced-repetition": + case "ai-coach-custom-schedules": case "personalized-study-plan": - case "ai-tutor-chat": - case "exam-readiness-score": - case "exam-day-strategy": - case "college-predictor": - case "study-time-optimization": + case "ai-tutor-doubt-resolution": + case "ai-mistake-tracking": + case "ai-readiness-prediction": return tier === "Al_Coach"; default: @@ -201,6 +208,7 @@ export function useActiveUser() { loading, error, switchUser, + logout, hasAccessTo, refreshUser: async () => { await fetchUserProfile(userId);