Fix backend bulk delete, separate password page, remove admin dummy data, swap dashboard rows
This commit is contained in:
@@ -298,8 +298,8 @@ def bulk_delete_students(payload: DeleteUsersRequest, session: Session = Depends
|
|||||||
user = session.get(models.User, uid)
|
user = session.get(models.User, uid)
|
||||||
if user:
|
if user:
|
||||||
# Delete associated records manually since foreign key cascade might not be configured in sqlmodel by default
|
# Delete associated records manually since foreign key cascade might not be configured in sqlmodel by default
|
||||||
session.exec(delete(models.Attempt).where(models.Attempt.user_id == uid))
|
|
||||||
session.exec(delete(models.Mistake).where(models.Mistake.user_id == uid))
|
session.exec(delete(models.Mistake).where(models.Mistake.user_id == uid))
|
||||||
|
session.exec(delete(models.Attempt).where(models.Attempt.user_id == uid))
|
||||||
session.exec(delete(models.Transaction).where(models.Transaction.user_id == uid))
|
session.exec(delete(models.Transaction).where(models.Transaction.user_id == uid))
|
||||||
session.exec(delete(models.Subscription).where(models.Subscription.user_id == uid))
|
session.exec(delete(models.Subscription).where(models.Subscription.user_id == uid))
|
||||||
session.exec(delete(models.StudyPlan).where(models.StudyPlan.user_id == uid))
|
session.exec(delete(models.StudyPlan).where(models.StudyPlan.user_id == uid))
|
||||||
|
|||||||
@@ -1,20 +1,8 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
import { Badge } from "@/components/ui/badge"
|
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Terminal, Users, Database, Activity, RefreshCw } from "lucide-react"
|
import { Terminal, Users, Database, Activity, RefreshCw } from "lucide-react"
|
||||||
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, AreaChart, Area } from 'recharts'
|
|
||||||
|
|
||||||
const data = [
|
|
||||||
{ name: 'Mon', accuracy: 88, tokens: 1.2 },
|
|
||||||
{ name: 'Tue', accuracy: 90, tokens: 1.5 },
|
|
||||||
{ name: 'Wed', accuracy: 89, tokens: 1.8 },
|
|
||||||
{ name: 'Thu', accuracy: 92, tokens: 2.1 },
|
|
||||||
{ name: 'Fri', accuracy: 95, tokens: 2.4 },
|
|
||||||
{ name: 'Sat', accuracy: 94, tokens: 2.2 },
|
|
||||||
{ name: 'Sun', accuracy: 96, tokens: 2.8 },
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function AdminDashboard() {
|
export default function AdminDashboard() {
|
||||||
return (
|
return (
|
||||||
@@ -22,14 +10,14 @@ export default function AdminDashboard() {
|
|||||||
<header className="flex items-center justify-between border-b border-border pb-6">
|
<header className="flex items-center justify-between border-b border-border pb-6">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold tracking-tighter text-foreground">Command Center</h1>
|
<h1 className="text-3xl font-bold tracking-tighter text-foreground">Command Center</h1>
|
||||||
<p className="text-muted-foreground mt-1 font-medium">Real-time system monitoring and agent status.</p>
|
<p className="text-muted-foreground mt-1 font-medium">Real-time system monitoring and management.</p>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="outline" className="gap-2 border-[var(--cyan-ai-10)] text-[var(--cyan-ai)] bg-[var(--cyan-ai-10)] hover:bg-[var(--cyan-ai-10)] hover:text-[var(--cyan-ai)] rounded-xl font-medium shadow-[0_0_15px_-5px_var(--cyan-ai)]">
|
<Button variant="outline" className="gap-2 border-[var(--cyan-ai-10)] text-[var(--cyan-ai)] bg-[var(--cyan-ai-10)] hover:bg-[var(--cyan-ai-10)] hover:text-[var(--cyan-ai)] rounded-xl font-medium shadow-[0_0_15px_-5px_var(--cyan-ai)]">
|
||||||
<RefreshCw size={16} /> Refresh Data
|
<RefreshCw size={16} /> Refresh Data
|
||||||
</Button>
|
</Button>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{/* KPI Stats */}
|
{/* KPI Stats Placeholder */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
|
||||||
<Card className="bg-secondary/10 border-border/50 rounded-2xl">
|
<Card className="bg-secondary/10 border-border/50 rounded-2xl">
|
||||||
<CardHeader className="pb-2 flex flex-row items-center justify-between">
|
<CardHeader className="pb-2 flex flex-row items-center justify-between">
|
||||||
@@ -37,8 +25,8 @@ export default function AdminDashboard() {
|
|||||||
<div className="p-2 bg-primary/20 text-primary rounded-lg"><Activity size={16}/></div>
|
<div className="p-2 bg-primary/20 text-primary rounded-lg"><Activity size={16}/></div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="text-2xl font-bold">₹12.4M</div>
|
<div className="text-2xl font-bold">--</div>
|
||||||
<p className="text-xs text-primary mt-1 font-medium">+14.2% from last month</p>
|
<p className="text-xs text-primary mt-1 font-medium">No live data available</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
<Card className="bg-secondary/10 border-border/50 rounded-2xl">
|
<Card className="bg-secondary/10 border-border/50 rounded-2xl">
|
||||||
@@ -47,18 +35,18 @@ export default function AdminDashboard() {
|
|||||||
<div className="p-2 bg-secondary text-foreground rounded-lg"><Users size={16}/></div>
|
<div className="p-2 bg-secondary text-foreground rounded-lg"><Users size={16}/></div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="text-2xl font-bold">45,231</div>
|
<div className="text-2xl font-bold">--</div>
|
||||||
<p className="text-xs text-muted-foreground mt-1 font-medium">+2,401 this week</p>
|
<p className="text-xs text-muted-foreground mt-1 font-medium">Manage from Students tab</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
<Card className="bg-[var(--cyan-ai-10)] border-[var(--cyan-ai-30)] shadow-[0_0_20px_-10px_var(--cyan-ai)] rounded-2xl">
|
<Card className="bg-[var(--cyan-ai-10)] border-[var(--cyan-ai-30)] shadow-[0_0_20px_-10px_var(--cyan-ai)] rounded-2xl">
|
||||||
<CardHeader className="pb-2 flex flex-row items-center justify-between">
|
<CardHeader className="pb-2 flex flex-row items-center justify-between">
|
||||||
<CardTitle className="text-sm font-medium text-muted-foreground">AI Agent Accuracy</CardTitle>
|
<CardTitle className="text-sm font-medium text-muted-foreground">System Status</CardTitle>
|
||||||
<div className="p-2 bg-[var(--cyan-ai-20)] text-[var(--cyan-ai)] rounded-lg"><Terminal size={16}/></div>
|
<div className="p-2 bg-[var(--cyan-ai-20)] text-[var(--cyan-ai)] rounded-lg"><Terminal size={16}/></div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="text-2xl font-bold text-[var(--cyan-ai)]">96.4%</div>
|
<div className="text-2xl font-bold text-[var(--cyan-ai)]">Online</div>
|
||||||
<p className="text-xs text-[var(--cyan-ai-80)] mt-1 font-medium">Based on 1.2M interactions</p>
|
<p className="text-xs text-[var(--cyan-ai-80)] mt-1 font-medium">All services operational</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
<Card className="bg-secondary/10 border-border/50 rounded-2xl">
|
<Card className="bg-secondary/10 border-border/50 rounded-2xl">
|
||||||
@@ -67,134 +55,16 @@ export default function AdminDashboard() {
|
|||||||
<div className="p-2 bg-secondary text-foreground rounded-lg"><Database size={16}/></div>
|
<div className="p-2 bg-secondary text-foreground rounded-lg"><Database size={16}/></div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="text-2xl font-bold">142k</div>
|
<div className="text-2xl font-bold">--</div>
|
||||||
<p className="text-xs text-muted-foreground mt-1 font-medium">1.2k added today</p>
|
<p className="text-xs text-muted-foreground mt-1 font-medium">Manage from Tests tab</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
<div className="flex flex-col items-center justify-center py-20 text-center">
|
||||||
{/* Charts */}
|
<h2 className="text-xl font-bold mb-2">Welcome to the Admin Portal</h2>
|
||||||
<Card className="lg:col-span-2 bg-background border-border rounded-3xl">
|
<p className="text-muted-foreground max-w-md">Use the sidebar to navigate to Students, Tests, Questions, and System Settings. Live analytics charts will be available here in the future.</p>
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>AI Token Usage vs Accuracy</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="h-[300px] w-full">
|
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
|
||||||
<AreaChart data={data} margin={{ top: 10, right: 30, left: 0, bottom: 0 }}>
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="colorTokens" x1="0" y1="0" x2="0" y2="1">
|
|
||||||
<stop offset="5%" stopColor="var(--cyan-ai)" stopOpacity={0.3}/>
|
|
||||||
<stop offset="95%" stopColor="var(--cyan-ai)" stopOpacity={0}/>
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
<CartesianGrid strokeDasharray="3 3" stroke="#262626" vertical={false} />
|
|
||||||
<XAxis dataKey="name" stroke="#737373" fontSize={12} tickLine={false} axisLine={false} />
|
|
||||||
<YAxis stroke="#737373" fontSize={12} tickLine={false} axisLine={false} tickFormatter={(value) => `${value}M`} />
|
|
||||||
<Tooltip
|
|
||||||
contentStyle={{ backgroundColor: '#0a0a0a', borderColor: '#262626', borderRadius: '12px' }}
|
|
||||||
itemStyle={{ color: 'var(--cyan-ai)', fontWeight: 'bold' }}
|
|
||||||
/>
|
|
||||||
<Area type="monotone" dataKey="tokens" stroke="var(--cyan-ai)" strokeWidth={3} fillOpacity={1} fill="url(#colorTokens)" />
|
|
||||||
</AreaChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Live Agent Status */}
|
|
||||||
<Card className="bg-background border-border flex flex-col rounded-3xl">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Live Agent Status</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="flex-1 overflow-y-auto pr-2 space-y-4">
|
|
||||||
<div className="flex items-center justify-between p-4 rounded-2xl border border-border bg-secondary/10">
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<div className="relative flex h-3 w-3">
|
|
||||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
|
|
||||||
<span className="relative inline-flex rounded-full h-3 w-3 bg-emerald-500"></span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h4 className="text-sm font-bold">DPP Generator</h4>
|
|
||||||
<p className="text-xs text-muted-foreground font-mono mt-1">Latency: 142ms</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Badge variant="outline" className="text-emerald-500 border-emerald-500/30 bg-emerald-500/10 font-bold">Healthy</Badge>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-between p-4 rounded-2xl border border-border bg-secondary/10">
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<div className="relative flex h-3 w-3">
|
|
||||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
|
|
||||||
<span className="relative inline-flex rounded-full h-3 w-3 bg-emerald-500"></span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h4 className="text-sm font-bold">Tutor Agent</h4>
|
|
||||||
<p className="text-xs text-muted-foreground font-mono mt-1">Latency: 840ms</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Badge variant="outline" className="text-emerald-500 border-emerald-500/30 bg-emerald-500/10 font-bold">Healthy</Badge>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-between p-4 rounded-2xl border border-amber-500/30 bg-amber-500/5">
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<div className="relative flex h-3 w-3">
|
|
||||||
<span className="relative inline-flex rounded-full h-3 w-3 bg-amber-500"></span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h4 className="text-sm font-bold text-amber-500">Evaluator Node</h4>
|
|
||||||
<p className="text-xs text-amber-500/70 font-mono mt-1">Latency: 2100ms</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Badge variant="outline" className="text-amber-500 border-amber-500/30 bg-amber-500/10 font-bold">Degraded</Badge>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Question Bank Manager Preview */}
|
|
||||||
<Card className="bg-background border-border rounded-3xl">
|
|
||||||
<CardHeader className="flex flex-row items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<CardTitle>Recent Question Uploads</CardTitle>
|
|
||||||
</div>
|
|
||||||
<Button size="sm" className="bg-[var(--cyan-ai)] text-black hover:bg-[var(--cyan-ai-80)] font-bold rounded-xl h-10 px-4">Add Question</Button>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="rounded-2xl border border-border overflow-hidden">
|
|
||||||
<table className="w-full text-sm text-left">
|
|
||||||
<thead className="bg-secondary/50 text-muted-foreground font-medium border-b border-border">
|
|
||||||
<tr>
|
|
||||||
<th className="px-5 py-4">ID</th>
|
|
||||||
<th className="px-5 py-4">Subject</th>
|
|
||||||
<th className="px-5 py-4">Topic</th>
|
|
||||||
<th className="px-5 py-4">Preview</th>
|
|
||||||
<th className="px-5 py-4 text-right">Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody className="divide-y divide-border">
|
|
||||||
<tr className="hover:bg-secondary/20 transition-colors">
|
|
||||||
<td className="px-5 py-4 font-mono text-muted-foreground font-medium">#Q-8942</td>
|
|
||||||
<td className="px-5 py-4"><Badge variant="outline" className="bg-background">Physics</Badge></td>
|
|
||||||
<td className="px-5 py-4 font-medium">Thermodynamics</td>
|
|
||||||
<td className="px-5 py-4 text-muted-foreground truncate max-w-[300px]">An ideal gas undergoes an isothermal expansion...</td>
|
|
||||||
<td className="px-5 py-4 text-right">
|
|
||||||
<Button variant="ghost" size="sm" className="text-[var(--cyan-ai)] hover:text-[var(--cyan-ai)] hover:bg-[var(--cyan-ai-10)] font-bold rounded-lg">Edit</Button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr className="hover:bg-secondary/20 transition-colors">
|
|
||||||
<td className="px-5 py-4 font-mono text-muted-foreground font-medium">#Q-8941</td>
|
|
||||||
<td className="px-5 py-4"><Badge variant="outline" className="bg-background">Math</Badge></td>
|
|
||||||
<td className="px-5 py-4 font-medium">Calculus</td>
|
|
||||||
<td className="px-5 py-4 text-muted-foreground truncate max-w-[300px]">Evaluate the integral of e^x sin(x) dx...</td>
|
|
||||||
<td className="px-5 py-4 text-right">
|
|
||||||
<Button variant="ghost" size="sm" className="text-[var(--cyan-ai)] hover:text-[var(--cyan-ai)] hover:bg-[var(--cyan-ai-10)] font-bold rounded-lg">Edit</Button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import { StreakCounter } from "@/components/StreakCounter"
|
|||||||
import { AITutorChat } from "@/components/AI/AITutorChat"
|
import { AITutorChat } from "@/components/AI/AITutorChat"
|
||||||
import { useActiveUser, DEMO_USERS, isDemoUser } from "@/hooks/useActiveUser"
|
import { useActiveUser, DEMO_USERS, isDemoUser } from "@/hooks/useActiveUser"
|
||||||
import { StreakOnboardingModal } from "@/components/Dashboard/StreakOnboardingModal"
|
import { StreakOnboardingModal } from "@/components/Dashboard/StreakOnboardingModal"
|
||||||
import { BulkUserPasswordModal } from "@/components/Dashboard/BulkUserPasswordModal"
|
|
||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge"
|
||||||
import MaintenanceScreen from "@/components/MaintenanceScreen"
|
import MaintenanceScreen from "@/components/MaintenanceScreen"
|
||||||
|
|
||||||
@@ -320,6 +319,9 @@ export default function DashboardLayout({ children }: { children: ReactNode }) {
|
|||||||
<Link href="/dashboard/settings" className="flex items-center gap-2 px-3 py-2 text-sm text-foreground hover:bg-secondary rounded-lg transition-colors">
|
<Link href="/dashboard/settings" className="flex items-center gap-2 px-3 py-2 text-sm text-foreground hover:bg-secondary rounded-lg transition-colors">
|
||||||
<User size={16} /> Edit Profile
|
<User size={16} /> Edit Profile
|
||||||
</Link>
|
</Link>
|
||||||
|
<Link href="/dashboard/settings/password" className="flex items-center gap-2 px-3 py-2 text-sm text-foreground hover:bg-secondary rounded-lg transition-colors">
|
||||||
|
<Lock size={16} /> Change Password
|
||||||
|
</Link>
|
||||||
{isDemoUser(userId) && (
|
{isDemoUser(userId) && (
|
||||||
<button
|
<button
|
||||||
onClick={() => switchUser(userId, true)}
|
onClick={() => switchUser(userId, true)}
|
||||||
|
|||||||
@@ -218,15 +218,6 @@ export default function DashboardPage() {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
|
||||||
<section>
|
|
||||||
<AdaptiveStudyPlan />
|
|
||||||
</section>
|
|
||||||
<section>
|
|
||||||
<MistakeHeatmap />
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
|
||||||
{/* Dynamic DPP Modules */}
|
{/* Dynamic DPP Modules */}
|
||||||
<section>
|
<section>
|
||||||
<div className="flex items-center justify-between mb-5">
|
<div className="flex items-center justify-between mb-5">
|
||||||
@@ -335,6 +326,15 @@ export default function DashboardPage() {
|
|||||||
)}
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
</section>
|
</section>
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||||
|
<section>
|
||||||
|
<AdaptiveStudyPlan />
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<MistakeHeatmap />
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -70,45 +70,9 @@ export default function SettingsPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const [currentPassword, setCurrentPassword] = useState("");
|
|
||||||
const [newPassword, setNewPassword] = useState("");
|
|
||||||
const [confirmPassword, setConfirmPassword] = useState("");
|
|
||||||
const [passwordSaving, setPasswordSaving] = useState(false);
|
|
||||||
const [showCurrent, setShowCurrent] = useState(false);
|
|
||||||
const [showNew, setShowNew] = useState(false);
|
|
||||||
const [showConfirm, setShowConfirm] = 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 () => {
|
const handleSave = async () => {
|
||||||
if (!userId) return;
|
if (!userId) return;
|
||||||
@@ -363,56 +327,7 @@ export default function SettingsPage() {
|
|||||||
</Button> */}
|
</Button> */}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
<Card className="border-border bg-background shadow-sm">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Change Password</CardTitle>
|
|
||||||
<CardDescription>Update your account password</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-6">
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label className="text-sm font-bold">Current Password</label>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
value={currentPassword}
|
|
||||||
onChange={(e) => setCurrentPassword(e.target.value)}
|
|
||||||
className="w-full bg-secondary/50 border border-border rounded-lg px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-[var(--cyan-ai)]"
|
|
||||||
placeholder="Enter current password"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="hidden md:block"></div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label className="text-sm font-bold">New Password</label>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
value={newPassword}
|
|
||||||
onChange={(e) => setNewPassword(e.target.value)}
|
|
||||||
className="w-full bg-secondary/50 border border-border rounded-lg px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-[var(--cyan-ai)]"
|
|
||||||
placeholder="Enter new password"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label className="text-sm font-bold">Confirm New Password</label>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
value={confirmPassword}
|
|
||||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
|
||||||
className="w-full bg-secondary/50 border border-border rounded-lg px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-[var(--cyan-ai)]"
|
|
||||||
placeholder="Confirm new password"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex pt-2 border-t border-border mt-6">
|
|
||||||
<Button
|
|
||||||
className="bg-foreground text-background hover:bg-foreground/90 font-bold px-6 rounded-xl"
|
|
||||||
onClick={handleChangePassword}
|
|
||||||
disabled={passwordSaving || !currentPassword || !newPassword || !confirmPassword}
|
|
||||||
>
|
|
||||||
{passwordSaving ? "Updating..." : "Update Password"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
115
src/app/dashboard/settings/password/page.tsx
Normal file
115
src/app/dashboard/settings/password/page.tsx
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { fetchWithAuth } from "@/lib/apiClient";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Lock } from "lucide-react";
|
||||||
|
|
||||||
|
export default function PasswordPage() {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-8 space-y-8 animate-in fade-in zoom-in-95 origin-top duration-500 max-w-3xl mx-auto">
|
||||||
|
<div className="flex flex-col md:flex-row md:items-end justify-between gap-4">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl md:text-4xl font-black tracking-tight flex items-center gap-3 mb-1">
|
||||||
|
<span className="p-2.5 rounded-xl bg-purple-50 dark:bg-[var(--cyan-ai-10)] border border-purple-300/60 dark:border-[var(--cyan-ai-20)]">
|
||||||
|
<Lock className="text-purple-600 dark:text-[var(--cyan-ai)]" size={28} />
|
||||||
|
</span>
|
||||||
|
Change Password
|
||||||
|
</h1>
|
||||||
|
<p className="text-muted-foreground text-base font-medium max-w-2xl pl-1">
|
||||||
|
Update your account password securely.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card className="border-border bg-background shadow-sm">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Change Password</CardTitle>
|
||||||
|
<CardDescription>Update your account password</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-6">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm font-bold">Current Password</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={currentPassword}
|
||||||
|
onChange={(e) => setCurrentPassword(e.target.value)}
|
||||||
|
className="w-full bg-secondary/50 border border-border rounded-lg px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-[var(--cyan-ai)]"
|
||||||
|
placeholder="Enter current password"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="hidden md:block"></div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm font-bold">New Password</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={newPassword}
|
||||||
|
onChange={(e) => setNewPassword(e.target.value)}
|
||||||
|
className="w-full bg-secondary/50 border border-border rounded-lg px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-[var(--cyan-ai)]"
|
||||||
|
placeholder="Enter new password"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm font-bold">Confirm New Password</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={confirmPassword}
|
||||||
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||||
|
className="w-full bg-secondary/50 border border-border rounded-lg px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-[var(--cyan-ai)]"
|
||||||
|
placeholder="Confirm new password"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex pt-2 border-t border-border mt-6">
|
||||||
|
<Button
|
||||||
|
className="bg-foreground text-background hover:bg-foreground/90 font-bold px-6 rounded-xl"
|
||||||
|
onClick={handleChangePassword}
|
||||||
|
disabled={passwordSaving || !currentPassword || !newPassword || !confirmPassword}
|
||||||
|
>
|
||||||
|
{passwordSaving ? "Updating..." : "Update Password"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,129 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useState } from "react";
|
|
||||||
import { Lock, Eye, EyeOff } from "lucide-react";
|
|
||||||
import { fetchWithAuth } from "@/lib/apiClient";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
|
|
||||||
interface BulkUserPasswordModalProps {
|
|
||||||
isOpen: boolean;
|
|
||||||
onSuccess: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function BulkUserPasswordModal({ isOpen, onSuccess }: BulkUserPasswordModalProps) {
|
|
||||||
const [currentPassword, setCurrentPassword] = useState("");
|
|
||||||
const [newPassword, setNewPassword] = useState("");
|
|
||||||
const [confirmPassword, setConfirmPassword] = useState("");
|
|
||||||
const [showCurrent, setShowCurrent] = useState(false);
|
|
||||||
const [showNew, setShowNew] = useState(false);
|
|
||||||
const [showConfirm, setShowConfirm] = useState(false);
|
|
||||||
const [saving, setSaving] = useState(false);
|
|
||||||
const [error, setError] = useState("");
|
|
||||||
|
|
||||||
if (!isOpen) return null;
|
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
if (newPassword !== confirmPassword) {
|
|
||||||
setError("New passwords do not match.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setSaving(true);
|
|
||||||
setError("");
|
|
||||||
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) {
|
|
||||||
onSuccess();
|
|
||||||
} else {
|
|
||||||
setError("Failed: " + (data.detail || "Unknown error"));
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
setError("Network error. Could not update password.");
|
|
||||||
} finally {
|
|
||||||
setSaving(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="fixed inset-0 z-[100] flex items-center justify-center bg-black/80 backdrop-blur-sm animate-in fade-in zoom-in-95 duration-300">
|
|
||||||
<div className="bg-background border border-border rounded-2xl w-full max-w-md p-8 shadow-2xl space-y-6">
|
|
||||||
<div className="flex flex-col items-center text-center space-y-2">
|
|
||||||
<div className="w-16 h-16 rounded-full bg-[var(--cyan-ai-10)] flex items-center justify-center mb-2 shadow-[0_0_20px_-5px_var(--cyan-ai)]">
|
|
||||||
<Lock className="w-8 h-8 text-[var(--cyan-ai)]" />
|
|
||||||
</div>
|
|
||||||
<h2 className="text-2xl font-black">Set Your Password</h2>
|
|
||||||
<p className="text-muted-foreground text-sm">
|
|
||||||
Welcome! Since your account was created by the admin, please set a new personal password to continue.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{error && <p className="text-red-500 text-sm font-semibold text-center">{error}</p>}
|
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
|
||||||
<div className="space-y-2 text-left">
|
|
||||||
<label className="text-sm font-bold">Current Password (Given by Admin)</label>
|
|
||||||
<div className="relative">
|
|
||||||
<input
|
|
||||||
type={showCurrent ? "text" : "password"}
|
|
||||||
value={currentPassword}
|
|
||||||
onChange={(e) => setCurrentPassword(e.target.value)}
|
|
||||||
className="w-full bg-secondary/50 border border-border rounded-lg px-4 py-2 pr-10 text-sm focus:outline-none focus:ring-2 focus:ring-[var(--cyan-ai)]"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<button type="button" onClick={() => setShowCurrent(!showCurrent)} className="absolute right-3 top-2.5 text-muted-foreground hover:text-foreground">
|
|
||||||
{showCurrent ? <EyeOff size={16} /> : <Eye size={16} />}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2 text-left">
|
|
||||||
<label className="text-sm font-bold">New Password</label>
|
|
||||||
<div className="relative">
|
|
||||||
<input
|
|
||||||
type={showNew ? "text" : "password"}
|
|
||||||
value={newPassword}
|
|
||||||
onChange={(e) => setNewPassword(e.target.value)}
|
|
||||||
className="w-full bg-secondary/50 border border-border rounded-lg px-4 py-2 pr-10 text-sm focus:outline-none focus:ring-2 focus:ring-[var(--cyan-ai)]"
|
|
||||||
required
|
|
||||||
minLength={8}
|
|
||||||
/>
|
|
||||||
<button type="button" onClick={() => setShowNew(!showNew)} className="absolute right-3 top-2.5 text-muted-foreground hover:text-foreground">
|
|
||||||
{showNew ? <EyeOff size={16} /> : <Eye size={16} />}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2 text-left">
|
|
||||||
<label className="text-sm font-bold">Confirm New Password</label>
|
|
||||||
<div className="relative">
|
|
||||||
<input
|
|
||||||
type={showConfirm ? "text" : "password"}
|
|
||||||
value={confirmPassword}
|
|
||||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
|
||||||
className="w-full bg-secondary/50 border border-border rounded-lg px-4 py-2 pr-10 text-sm focus:outline-none focus:ring-2 focus:ring-[var(--cyan-ai)]"
|
|
||||||
required
|
|
||||||
minLength={8}
|
|
||||||
/>
|
|
||||||
<button type="button" onClick={() => setShowConfirm(!showConfirm)} className="absolute right-3 top-2.5 text-muted-foreground hover:text-foreground">
|
|
||||||
{showConfirm ? <EyeOff size={16} /> : <Eye size={16} />}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button type="submit" disabled={saving || !currentPassword || !newPassword || !confirmPassword} className="w-full bg-[var(--cyan-ai)] hover:bg-cyan-400 text-black font-bold py-3 mt-4">
|
|
||||||
{saving ? "Saving..." : "Set Password & Continue"}
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user