feat: Add JWT authentication to backend, configure frontend API client, fix streak layout

This commit is contained in:
2026-09-23 12:11:44 +05:30
parent ea03846b9c
commit a3e430cb67
27 changed files with 210 additions and 120 deletions

View File

@@ -1,5 +1,6 @@
"use client"
import { fetchWithAuth } from "@/lib/apiClient";
import { useState, useEffect } from "react"
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
@@ -25,7 +26,7 @@ export default function CouponsAdmin() {
const fetchCoupons = async () => {
try {
const res = await fetch('/api/admin/coupons')
const res = await fetchWithAuth('/api/admin/coupons')
const data = await res.json()
setCoupons(data)
} catch (e) {
@@ -39,7 +40,7 @@ export default function CouponsAdmin() {
if (!newCoupon.code) return
setSubmitting(true)
try {
await fetch('/api/admin/coupons', {
await fetchWithAuth('/api/admin/coupons', {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(newCoupon)
@@ -56,7 +57,7 @@ export default function CouponsAdmin() {
const handleDelete = async (id: string) => {
if (!confirm("Delete this coupon?")) return
try {
await fetch(`/api/admin/coupons/${id}`, { method: "DELETE" })
await fetchWithAuth(`/api/admin/coupons/${id}`, { method: "DELETE" })
await fetchCoupons()
} catch (e) {
console.error(e)

View File

@@ -1,5 +1,6 @@
"use client";
import { fetchWithAuth } from "@/lib/apiClient";
import { useState, useEffect } from "react";
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
@@ -15,7 +16,7 @@ export default function SettingsPage() {
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch("/api/admin/settings")
fetchWithAuth("/api/admin/settings")
.then(res => res.json())
.then(data => {
if(data && !data.detail) {
@@ -32,7 +33,7 @@ export default function SettingsPage() {
const saveSettings = async (section: string, overrides: Partial<typeof settings> = {}) => {
try {
const payload = { ...settings, ...overrides };
const res = await fetch("/api/admin/settings", {
const res = await fetchWithAuth("/api/admin/settings", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)

View File

@@ -1,5 +1,6 @@
"use client"
import { fetchWithAuth } from "@/lib/apiClient";
import { useEffect, useState } from "react"
import { useParams, useRouter } from "next/navigation"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
@@ -46,7 +47,7 @@ export default function StudentKundliPage() {
const [editExam, setEditExam] = useState("")
useEffect(() => {
fetch(`/api/admin/students/${studentId}`)
fetchWithAuth(`/api/admin/students/${studentId}`)
.then(r => { if (!r.ok) throw new Error("Not found"); return r.json() })
.then(d => {
setData(d)
@@ -62,7 +63,7 @@ export default function StudentKundliPage() {
const handleSave = async () => {
setSaving(true)
try {
const res = await fetch(`/api/admin/students/${studentId}`, {
const res = await fetchWithAuth(`/api/admin/students/${studentId}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
@@ -74,7 +75,7 @@ export default function StudentKundliPage() {
})
if (!res.ok) throw new Error("Failed to save")
// Refresh data
const fresh = await fetch(`/api/admin/students/${studentId}`).then(r => r.json())
const fresh = await fetchWithAuth(`/api/admin/students/${studentId}`).then(r => r.json())
setData(fresh)
setEditing(false)
} catch (e: any) {

View File

@@ -1,5 +1,6 @@
"use client"
import { fetchWithAuth } from "@/lib/apiClient";
import { useEffect, useState } from "react"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
@@ -25,7 +26,7 @@ export default function StudentsPage() {
const [search, setSearch] = useState("")
useEffect(() => {
fetch("/api/admin/students")
fetchWithAuth("/api/admin/students")
.then(r => r.json())
.then(data => setStudents(data))
.catch(console.error)

View File

@@ -1,5 +1,6 @@
"use client"
import { fetchWithAuth } from "@/lib/apiClient";
import { useState, useEffect, useRef } from "react"
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
@@ -55,7 +56,7 @@ export default function AdminTestsPage() {
const fetchTests = async () => {
try {
const res = await fetch("/api/admin/tests")
const res = await fetchWithAuth("/api/admin/tests")
if (res.ok) {
const data = await res.json()
setTests(data)
@@ -74,7 +75,7 @@ export default function AdminTestsPage() {
const handleToggle = async (testId: string) => {
try {
const res = await fetch(`/api/admin/tests/${testId}/toggle`, {
const res = await fetchWithAuth(`/api/admin/tests/${testId}/toggle`, {
method: "PUT"
})
if (res.ok) {
@@ -88,7 +89,7 @@ export default function AdminTestsPage() {
const handleDelete = async (testId: string) => {
if (!confirm("Are you sure you want to delete this test and all its questions? This action cannot be undone.")) return
try {
const res = await fetch(`/api/admin/tests/${testId}`, {
const res = await fetchWithAuth(`/api/admin/tests/${testId}`, {
method: "DELETE"
})
if (res.ok) {
@@ -191,7 +192,7 @@ export default function AdminTestsPage() {
};
try {
const res = await fetch("/api/admin/tests/create_manual", {
const res = await fetchWithAuth("/api/admin/tests/create_manual", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)

View File

@@ -121,20 +121,20 @@ export async function GET(req: NextRequest) {
</div>
<div style={{ display: 'flex', flexDirection: 'column' }}>
<div style={{ fontSize: 44, fontWeight: 900, color: textColor, letterSpacing: '-0.02em' }}>{name}</div>
<div style={{ fontSize: 20, color: textSec, marginTop: 4 }}>Generated: {exportDate}</div>
<div style={{ display: 'flex', fontSize: 44, fontWeight: 900, color: textColor, letterSpacing: '-0.02em' }}>{name}</div>
<div style={{ display: 'flex', fontSize: 20, color: textSec, marginTop: 4 }}>{`Generated: ${exportDate}`}</div>
<div style={{ display: 'flex', marginTop: 8 }}>
<div style={{ fontSize: 22, color: '#06b6d4', fontWeight: 700, marginRight: 16 }}>{`${target} Aspirant`}</div>
<div style={{ fontSize: 22, color: textSec, fontWeight: 600 }}>•</div>
<div style={{ fontSize: 22, color: textTert, fontWeight: 600, marginLeft: 16 }}>{`${plan} Plan`}</div>
<div style={{ display: 'flex', fontSize: 22, color: '#06b6d4', fontWeight: 700, marginRight: 16 }}>{`${target} Aspirant`}</div>
<div style={{ display: 'flex', fontSize: 22, color: textSec, fontWeight: 600 }}>•</div>
<div style={{ display: 'flex', fontSize: 22, color: textTert, fontWeight: 600, marginLeft: 16 }}>{`${plan} Plan`}</div>
</div>
</div>
</div>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: '4px' }}>
<div style={{ fontSize: 22, color: textTert, fontWeight: 600 }}>pragativeer.com</div>
<div style={{ fontSize: 22, color: textSec, fontWeight: 600 }}>contact@pragativeer.com</div>
<div style={{ fontSize: 22, color: textSec, fontWeight: 600 }}>+91 9079337961</div>
<div style={{ display: 'flex', fontSize: 22, color: textTert, fontWeight: 600 }}>pragativeer.com</div>
<div style={{ display: 'flex', fontSize: 22, color: textSec, fontWeight: 600 }}>contact@pragativeer.com</div>
<div style={{ display: 'flex', fontSize: 22, color: textSec, fontWeight: 600 }}>+91 9079337961</div>
</div>
</div>
</div>

View File

@@ -1,5 +1,6 @@
"use client"
import { fetchWithAuth } from "@/lib/apiClient";
import { Button } from "@/components/ui/button"
import { Card, CardContent } from "@/components/ui/card"
import { BrainCircuit, Play, CheckCircle2, Download, Filter, Lock, FileText, Eye, ShieldAlert, X, Sparkles, Share2, Maximize2, Minimize2 } from "lucide-react"
@@ -25,14 +26,14 @@ export default function AIDailyPracticePage() {
try {
const pattern = user.target_exam || "JEE_Main";
// Also fetch from the correct Next.js proxied route instead of localhost:8000
const res = await fetch(`/api/tests?type=DPS&exam_pattern=${pattern}`);
const res = await fetchWithAuth(`/api/tests?type=DPS&exam_pattern=${pattern}`);
if (res.ok) {
const data = await res.json()
setDpps(data)
}
if (userId) {
const attemptsRes = await fetch(`/api/users/${userId}/attempts`);
const attemptsRes = await fetchWithAuth(`/api/users/${userId}/attempts`);
if (attemptsRes.ok) {
const attemptsData = await attemptsRes.json();
setAttempts(attemptsData);

View File

@@ -1,5 +1,6 @@
"use client";
import { fetchWithAuth } from "@/lib/apiClient";
import { useEffect, useState } from "react"
import { MistakeHeatmap } from "@/components/AI/MistakeHeatmap"
import { StudyPlanCard } from "@/components/AI/StudyPlanCard"
@@ -39,7 +40,7 @@ export default function AICoachDashboard() {
useEffect(() => {
if (!userLoading && userId) {
fetch(`/api/users/${userId}/stats`)
fetchWithAuth(`/api/users/${userId}/stats`)
.then(res => res.ok ? res.json() : null)
.then(data => setStats(data))
.catch(() => setStats(null))

View File

@@ -1,5 +1,6 @@
"use client";
import { fetchWithAuth } from "@/lib/apiClient";
import { useEffect, useState } from "react";
import Link from "next/link";
import { Button } from "@/components/ui/button";
@@ -37,7 +38,7 @@ export default function CBTPage() {
async function loadUser() {
try {
const userRes = await fetch(`/api/users/${userId}`);
const userRes = await fetchWithAuth(`/api/users/${userId}`);
if (!userRes.ok) throw new Error("Failed to load user target exam");
const userData = await userRes.json();
const pattern = userData.target_exam;
@@ -75,7 +76,7 @@ export default function CBTPage() {
setExams(testsData);
}
const attemptsRes = await fetch(`/api/users/${userId}/attempts`);
const attemptsRes = await fetchWithAuth(`/api/users/${userId}/attempts`);
if (attemptsRes.ok) {
const attemptsData = await attemptsRes.json();
setAttempts(attemptsData);

View File

@@ -1,5 +1,6 @@
"use client"
import { fetchWithAuth } from "@/lib/apiClient";
import { Button } from "@/components/ui/button"
import { Card, CardContent } from "@/components/ui/card"
import { BrainCircuit, Play, CheckCircle2, Download, Filter, Lock, FileText, Eye, ShieldAlert, X, Sparkles, Share2, Maximize2, Minimize2 } from "lucide-react"
@@ -25,14 +26,14 @@ export default function DPPPage() {
try {
const pattern = user.target_exam || "JEE_Main";
// Also fetch from the correct Next.js proxied route instead of localhost:8000
const res = await fetch(`/api/tests?type=DPS&exam_pattern=${pattern}`);
const res = await fetchWithAuth(`/api/tests?type=DPS&exam_pattern=${pattern}`);
if (res.ok) {
const data = await res.json()
setDpps(data)
}
if (userId) {
const attemptsRes = await fetch(`/api/users/${userId}/attempts`);
const attemptsRes = await fetchWithAuth(`/api/users/${userId}/attempts`);
if (attemptsRes.ok) {
const attemptsData = await attemptsRes.json();
setAttempts(attemptsData);

View File

@@ -335,7 +335,7 @@ export default function DashboardLayout({ children }: { children: ReactNode }) {
<button
type="button"
onClick={() => window.dispatchEvent(new CustomEvent("pragativeer-show-streak-animation"))}
className="hover:opacity-85 transition-opacity cursor-pointer border-none bg-transparent p-0"
className="flex items-center justify-center hover:opacity-85 transition-opacity cursor-pointer border-none bg-transparent p-0"
title="View Streak Celebration"
>
<StreakCounter streak={user.streak_count} />

View File

@@ -1,5 +1,6 @@
"use client"
import { fetchWithAuth } from "@/lib/apiClient";
import { useRef, useState, useEffect } from "react"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
@@ -26,7 +27,7 @@ export default function DashboardPage() {
}
async function fetchStats() {
try {
const res = await fetch(`/api/users/${currentUserId}/stats`)
const res = await fetchWithAuth(`/api/users/${currentUserId}/stats`)
if (res.ok) {
const data = await res.json()
setUserStats(data)

View File

@@ -1,5 +1,6 @@
"use client";
import { fetchWithAuth } from "@/lib/apiClient";
import { useEffect, useState } from "react";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
@@ -50,7 +51,7 @@ export default function SettingsPage() {
formData.append("file", file);
try {
const res = await fetch(`/api/users/${userId}/avatar`, {
const res = await fetchWithAuth(`/api/users/${userId}/avatar`, {
method: "POST",
body: formData,
});
@@ -72,7 +73,7 @@ export default function SettingsPage() {
if (!userId) return;
setSaving(true);
try {
const res = await fetch(`/api/users/${userId}`, {
const res = await fetchWithAuth(`/api/users/${userId}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({

View File

@@ -1,5 +1,6 @@
"use client"
import { fetchWithAuth } from "@/lib/apiClient";
import { useState, useEffect } from "react"
import { Card, CardContent } from "@/components/ui/card"
import { Flame, Gift, Star, Trophy, Target, ArrowRight, CheckCircle2 } from "lucide-react"
@@ -20,7 +21,7 @@ export default function StreaksPage() {
}
async function loadStats() {
try {
const res = await fetch(`/api/users/${user?.id}/stats`)
const res = await fetchWithAuth(`/api/users/${user?.id}/stats`)
if (res.ok) {
const stats = await res.json()
if (!stats.is_new_user) {

View File

@@ -1,5 +1,6 @@
"use client"
"use client"
import { fetchWithAuth } from "@/lib/apiClient";
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { ArrowLeft, Eye, EyeOff } from "lucide-react"
@@ -34,14 +35,14 @@ export default function LoginPage() {
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("activeUserId", data.user_id || data.token);
// Store the JWT as the active session token
const sessionToken = data.token;
localStorage.setItem("active_session_token", sessionToken);
if (data.token && data.token !== "admin") {
try {
await fetch(`/api/users/${data.token}/session`, {
await fetchWithAuth(`/api/users/${data.user_id}/session`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ session_token: sessionToken })

View File

@@ -1,5 +1,6 @@
"use client"
import { fetchWithAuth } from "@/lib/apiClient";
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { ArrowLeft, Eye, EyeOff, CheckCircle2 } from "lucide-react"
@@ -61,10 +62,10 @@ export default function SignupPage() {
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("activeUserId", loginData.user_id || loginData.token);
const sessionToken = loginData.token;
localStorage.setItem("active_session_token", sessionToken);
await fetch(`/api/users/${loginData.token}/session`, {
await fetchWithAuth(`/api/users/${loginData.user_id}/session`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ session_token: sessionToken })