feat: Add Bulk Registration, Change Password, and update Auth UI

This commit is contained in:
2026-09-24 20:53:21 +05:30
parent 3ed7028bda
commit 85974b97f5
7 changed files with 204 additions and 14 deletions

View File

@@ -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

View File

@@ -12,7 +12,7 @@ export default function AboutPage() {
<p className="text-xl text-muted-foreground max-w-4xl leading-relaxed mb-8 mx-auto text-left md:text-center">
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.
</p>
<Link href="/signup">
<Link href="/login">
<Button size="lg" className="rounded-full font-bold px-8 shadow-xl">Join the Revolution</Button>
</Link>
</main>

View File

@@ -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<string, string> = {
"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<HTMLInputElement>(null)
const handleBulkUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
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,10 +109,19 @@ export default function StudentsPage() {
{loading ? "Loading..." : `${students.length} students enrolled`}
</p>
</div>
<div className="flex gap-2">
<input type="file" accept=".csv" className="hidden" ref={fileInputRef} onChange={handleBulkUpload} />
<Button variant="outline" className="border-border hover:bg-secondary text-[var(--cyan-ai)]" onClick={() => fileInputRef.current?.click()}>
<Upload size={16} className="mr-2" /> Bulk Register CSV
</Button>
<Button variant="outline" className="border-border hover:bg-secondary" onClick={downloadTemplate}>
<Download size={16} className="mr-2" /> Download Template
</Button>
<Button variant="outline" className="border-border hover:bg-secondary">
<Download size={16} className="mr-2" /> Export CSV
</Button>
</div>
</div>
<div className="bg-secondary/10 border border-border rounded-xl overflow-hidden backdrop-blur-sm">
<div className="p-4 border-b border-border flex items-center gap-4">

View File

@@ -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);

View File

@@ -138,7 +138,7 @@ export default function LoginPage() {
</Card>
<p className="text-center text-sm text-muted-foreground font-medium">
Don't have an account? <Link href="/signup" className="font-bold text-foreground hover:text-primary transition-colors">Sign up</Link>
For registration contact us: support@pragativeer.com
</p>
</div>
</div>

View File

@@ -83,7 +83,7 @@ export default function MarketingPage() {
<div className="flex items-center gap-4">
<ThemeToggle />
<Link href="/login" className="text-sm font-bold hover:text-primary transition-colors hidden sm:block">Log in</Link>
<Link href="/signup">
<Link href="/login">
<Button className="rounded-full font-bold px-6 shadow-[0_0_15px_-5px_var(--color-primary)] hover:shadow-[0_0_25px_-5px_var(--color-primary)] transition-all">Get Started</Button>
</Link>
</div>
@@ -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"
>
<Link href="/signup">
<Link href="/login">
<Button size="lg" className="group rounded-full h-14 px-8 text-base font-bold shadow-[0_0_40px_-10px_var(--color-primary)] hover:shadow-[0_0_60px_-10px_var(--color-primary)] transition-all w-full sm:w-auto relative overflow-hidden">
<span className="relative z-10 flex items-center">Start Free Trial <ArrowRight className="ml-2 group-hover:translate-x-1 transition-transform" size={18} /></span>
<div className="absolute inset-0 -translate-x-full animate-shine bg-gradient-to-r from-transparent via-white/20 to-transparent z-0"></div>
@@ -437,8 +437,8 @@ export default function MarketingPage() {
<li className="flex items-center gap-4"><CheckCircle2 size={24} className="text-white" /> Basic Performance Analytics</li>
<li className="flex items-center gap-4"><CheckCircle2 size={24} className="text-white" /> Community Access</li>
</ul>
<Link href="/signup">
<Button variant="outline" size="lg" className="w-full rounded-2xl h-12 border-white text-white font-bold text-lg hover:bg-secondary hover:text-white">Get Started Free</Button>
<Link href="/login">
<Button variant="outline" size="lg" className="w-full rounded-2xl h-12 border-white text-white font-bold text-lg hover:bg-secondary hover:text-white">Login</Button>
</Link>
</Card>
</motion.div>
@@ -461,7 +461,7 @@ export default function MarketingPage() {
<li className="flex items-center gap-4"><CheckCircle2 size={24} className="text-yellow-500" /> Detailed Performance Analytics</li>
<li className="flex items-center gap-4"><CheckCircle2 size={24} className="text-yellow-500" /> Parent Dashboard Access</li>
</ul>
<Link href="/signup">
<Link href="/login">
<Button size="lg" className="w-full rounded-2xl h-12 bg-yellow-500 text-black font-bold text-lg hover:bg-yellow-400">Get Pro</Button>
</Link>
</Card>
@@ -485,7 +485,7 @@ export default function MarketingPage() {
<li className="flex items-center gap-4"><CheckCircle2 size={24} className="text-[var(--cyan-ai)]" /> 24/7 Streaming AI Tutor Chat</li>
<li className="flex items-center gap-4"><CheckCircle2 size={24} className="text-[var(--cyan-ai)]" /> Predictive Rank & College Predictor</li>
</ul>
<Link href="/signup">
<Link href="/login">
<Button size="lg" className="w-full rounded-2xl h-12 font-bold text-lg bg-[var(--cyan-ai)] text-black hover:bg-[var(--cyan-ai-80)] transition-all">Get AI Coach</Button>
</Link>
</Card>

View File

@@ -26,7 +26,7 @@ export function PublicLayout({ children }: { children: React.ReactNode }) {
<div className="flex items-center gap-4">
<ThemeToggle />
<Link href="/login" className="text-sm font-bold hover:text-primary transition-colors hidden sm:block">Log in</Link>
<Link href="/signup">
<Link href="/login">
<Button className="rounded-full font-bold px-6 shadow-[0_0_15px_-5px_var(--color-primary)] hover:shadow-[0_0_25px_-5px_var(--color-primary)] transition-all">Get Started</Button>
</Link>
</div>