feat: Add Bulk Registration, Change Password, and update Auth UI
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user