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.

- + 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`}

- +
+ + + + +
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 - +
@@ -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" > - + + + @@ -461,7 +461,7 @@ export default function MarketingPage() {
  • Detailed Performance Analytics
  • Parent Dashboard Access
  • - + @@ -485,7 +485,7 @@ export default function MarketingPage() {
  • 24/7 Streaming AI Tutor Chat
  • Predictive Rank & College Predictor
  • - + 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 - +