From 85974b97f5217a7bf5700c37936c2de8af66e340 Mon Sep 17 00:00:00 2001
From: ectasy663
Date: Thu, 24 Sep 2026 20:53:21 +0530
Subject: [PATCH] feat: Add Bulk Registration, Change Password, and update Auth
UI
---
backend/main.py | 87 +++++++++++++++++++++++++
src/app/about/page.tsx | 2 +-
src/app/admin/students/page.tsx | 75 +++++++++++++++++++--
src/app/dashboard/settings/page.tsx | 38 +++++++++++
src/app/login/page.tsx | 2 +-
src/app/page.tsx | 12 ++--
src/components/Landing/PublicLayout.tsx | 2 +-
7 files changed, 204 insertions(+), 14 deletions(-)
diff --git a/backend/main.py b/backend/main.py
index 3d16ff9..f81a53c 100644
--- a/backend/main.py
+++ b/backend/main.py
@@ -195,6 +195,93 @@ class LoginPayload(BaseModel):
email: str
password: str
+
+import csv
+import io
+import secrets
+import string
+import bcrypt
+
+class ChangePasswordRequest(BaseModel):
+ current_password: str
+ new_password: str
+
+@app.post("/api/auth/change-password", tags=["Authentication"])
+def change_password(payload: ChangePasswordRequest, session: Session = Depends(get_session), current_user: models.User = Depends(get_current_user)):
+ # Check demo account
+ if current_user.email == "demo@pragativeer.com":
+ if payload.current_password != "Pv9#kL2$mQ8!vX4@":
+ raise HTTPException(status_code=400, detail="Incorrect current password")
+ # Don't actually change demo password
+ return {"success": True, "message": "Demo password unchanged"}
+
+ if not current_user.password_hash or current_user.password_hash == "mock_hash":
+ raise HTTPException(status_code=400, detail="User has no password set")
+
+ if not bcrypt.checkpw(payload.current_password.encode('utf-8'), current_user.password_hash.encode('utf-8')):
+ raise HTTPException(status_code=400, detail="Incorrect current password")
+
+ hashed_password = bcrypt.hashpw(payload.new_password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
+ current_user.password_hash = hashed_password
+ session.add(current_user)
+ session.commit()
+ return {"success": True, "message": "Password updated successfully"}
+
+@app.post("/api/admin/students/bulk", tags=["Admin"])
+async def bulk_register_students(file: UploadFile = File(...), session: Session = Depends(get_session)):
+ # Simple check for now (we can rely on frontend calling it correctly or auth middleware if we add it)
+ content = await file.read()
+ decoded = content.decode('utf-8')
+ csv_reader = csv.DictReader(io.StringIO(decoded))
+
+ results = []
+
+ for row in csv_reader:
+ email = row.get("email", "").strip()
+ name = row.get("name", "").strip()
+ phone = row.get("phone", "").strip()
+ target_exam_str = row.get("target_exam", "JEE_Main").strip()
+ tier_str = row.get("tier", "Free").strip()
+
+ if not email or not name:
+ continue
+
+ existing = session.exec(select(models.User).where(models.User.email == email)).first()
+ if existing:
+ continue
+
+ alphabet = string.ascii_letters + string.digits
+ password = ''.join(secrets.choice(alphabet) for i in range(8))
+
+ exam = models.ExamEnum.JEE_Main
+ if target_exam_str == "NEET": exam = models.ExamEnum.NEET
+ elif target_exam_str == "JEE_Advanced": exam = models.ExamEnum.JEE_Advanced
+ elif target_exam_str == "JEE_NEET": exam = models.ExamEnum.JEE_NEET
+ elif target_exam_str == "JEE + NEET": exam = models.ExamEnum.JEE_NEET
+
+ tier = models.TierEnum.Free
+ if tier_str == "Pro": tier = models.TierEnum.Pro
+ elif tier_str in ("Al_Coach", "AI Coach"): tier = models.TierEnum.Al_Coach
+
+ hashed_password = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
+ user = models.User(
+ email=email,
+ name=name,
+ phone=phone,
+ password_hash=hashed_password,
+ target_exam=exam,
+ tier=tier
+ )
+ session.add(user)
+ results.append({
+ "name": name,
+ "email": email,
+ "password": password
+ })
+
+ session.commit()
+ return results
+
@app.post("/api/auth/register", tags=["Authentication"])
def register(payload: RegisterPayload, session: Session = Depends(get_session)):
import bcrypt
diff --git a/src/app/about/page.tsx b/src/app/about/page.tsx
index ceb109a..364aca6 100644
--- a/src/app/about/page.tsx
+++ b/src/app/about/page.tsx
@@ -12,7 +12,7 @@ export default function AboutPage() {
PragatiVeer is India's first AI-powered daily practice platform for JEE and NEET aspirants — a product of Gyannetra Pvt Ltd, the first company to bring the power of artificial intelligence into Indian exam preparation. We combine daily practice sheets, exam-realistic test series, and an AI coach that personalises every student's learning journey, making high-quality preparation accessible and affordable for every Indian student.
-
+
Join the Revolution
diff --git a/src/app/admin/students/page.tsx b/src/app/admin/students/page.tsx
index 2b95604..ae258f8 100644
--- a/src/app/admin/students/page.tsx
+++ b/src/app/admin/students/page.tsx
@@ -1,11 +1,12 @@
-"use client"
+"use client"
import { fetchWithAuth } from "@/lib/apiClient";
import { useEffect, useState } from "react"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
-import { Search, Download, Flame, RefreshCw } from "lucide-react"
+import { Search, Download, Flame, RefreshCw, Upload } from "lucide-react"
import { useRouter } from "next/navigation"
+import { useRef } from "react"
const TIER_STYLE: Record = {
"Al_Coach": "bg-[var(--cyan-ai-10)] text-[var(--cyan-ai)] border-[var(--cyan-ai-20)] font-bold",
@@ -25,6 +26,61 @@ export default function StudentsPage() {
const [loading, setLoading] = useState(true)
const [search, setSearch] = useState("")
+ const fileInputRef = useRef(null)
+
+ const handleBulkUpload = async (e: React.ChangeEvent) => {
+ const file = e.target.files?.[0]
+ if (!file) return
+ setLoading(true)
+ try {
+ const formData = new FormData()
+ formData.append("file", file)
+ const rawRes = await fetch(process.env.NEXT_PUBLIC_API_URL ? `${process.env.NEXT_PUBLIC_API_URL}/api/admin/students/bulk` : "http://localhost:8000/api/admin/students/bulk", {
+ method: "POST",
+ body: formData,
+ headers: {
+ "Authorization": `Bearer ${localStorage.getItem("active_session_token")}`
+ }
+ })
+
+ if (!rawRes.ok) throw new Error("Upload failed")
+
+ const results = await rawRes.json()
+ alert(`Successfully registered ${results.length} students!`)
+
+ // Download CSV with passwords
+ let csvContent = "data:text/csv;charset=utf-8,name,email,password\\n"
+ results.forEach((row: any) => {
+ csvContent += `${row.name},${row.email},${row.password}\\n`
+ })
+ const encodedUri = encodeURI(csvContent)
+ const link = document.createElement("a")
+ link.setAttribute("href", encodedUri)
+ link.setAttribute("download", "bulk_registered_passwords.csv")
+ document.body.appendChild(link)
+ link.click()
+
+ // Refresh students
+ const fresh = await fetchWithAuth("/api/admin/students").then(r => r.json())
+ setStudents(fresh)
+ } catch (err: any) {
+ alert("Error: " + err.message)
+ } finally {
+ setLoading(false)
+ if (fileInputRef.current) fileInputRef.current.value = ""
+ }
+ }
+
+ const downloadTemplate = () => {
+ const template = "data:text/csv;charset=utf-8,name,email,phone,target_exam,tier\\nJohn Doe,john@example.com,9876543210,JEE_Main,Free\\nJane Smith,jane@example.com,9876543211,NEET,Pro"
+ const encodedUri = encodeURI(template)
+ const link = document.createElement("a")
+ link.setAttribute("href", encodedUri)
+ link.setAttribute("download", "bulk_register_template.csv")
+ document.body.appendChild(link)
+ link.click()
+ }
+
useEffect(() => {
fetchWithAuth("/api/admin/students")
.then(r => r.json())
@@ -53,9 +109,18 @@ export default function StudentsPage() {
{loading ? "Loading..." : `${students.length} students enrolled`}
-
- Export CSV
-
+
+
+ fileInputRef.current?.click()}>
+ Bulk Register CSV
+
+
+ Download Template
+
+
+ Export CSV
+
+
diff --git a/src/app/dashboard/settings/page.tsx b/src/app/dashboard/settings/page.tsx
index d3fe554..5b03cc2 100644
--- a/src/app/dashboard/settings/page.tsx
+++ b/src/app/dashboard/settings/page.tsx
@@ -69,6 +69,44 @@ export default function SettingsPage() {
}
};
+
+ const [currentPassword, setCurrentPassword] = useState("");
+ const [newPassword, setNewPassword] = useState("");
+ const [confirmPassword, setConfirmPassword] = useState("");
+ const [passwordSaving, setPasswordSaving] = useState(false);
+
+ const handleChangePassword = async () => {
+ if (newPassword !== confirmPassword) {
+ alert("New passwords do not match.");
+ return;
+ }
+ setPasswordSaving(true);
+ try {
+ const res = await fetchWithAuth("/api/auth/change-password", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ current_password: currentPassword,
+ new_password: newPassword,
+ }),
+ });
+ const data = await res.json();
+ if (res.ok) {
+ alert(data.message || "Password updated successfully!");
+ setCurrentPassword("");
+ setNewPassword("");
+ setConfirmPassword("");
+ } else {
+ alert("Failed to change password: " + (data.detail || "Unknown error"));
+ }
+ } catch (err) {
+ console.error(err);
+ alert("Network error. Could not change password.");
+ } finally {
+ setPasswordSaving(false);
+ }
+ };
+
const handleSave = async () => {
if (!userId) return;
setSaving(true);
diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx
index 0035ac8..d590772 100644
--- a/src/app/login/page.tsx
+++ b/src/app/login/page.tsx
@@ -138,7 +138,7 @@ export default function LoginPage() {
- Don't have an account? Sign up
+ For registration contact us: support@pragativeer.com
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 72e16d3..eb409d3 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -83,7 +83,7 @@ export default function MarketingPage() {
Log in
-
+
Get Started
@@ -128,7 +128,7 @@ export default function MarketingPage() {
transition={{ duration: 0.8, delay: 0.4 }}
className="flex flex-col sm:flex-row items-center justify-center gap-4 pt-8"
>
-
+
Start Free Trial
@@ -437,8 +437,8 @@ export default function MarketingPage() {
Basic Performance Analytics
Community Access
-
- Get Started Free
+
+ Login
@@ -461,7 +461,7 @@ export default function MarketingPage() {
Detailed Performance Analytics
Parent Dashboard Access
-
+
Get Pro
@@ -485,7 +485,7 @@ export default function MarketingPage() {
24/7 Streaming AI Tutor Chat
Predictive Rank & College Predictor
-
+
Get AI Coach
diff --git a/src/components/Landing/PublicLayout.tsx b/src/components/Landing/PublicLayout.tsx
index 27bc5cf..6f1ab05 100644
--- a/src/components/Landing/PublicLayout.tsx
+++ b/src/components/Landing/PublicLayout.tsx
@@ -26,7 +26,7 @@ export function PublicLayout({ children }: { children: React.ReactNode }) {
Log in
-
+
Get Started