mohsin-devs commited on
Commit
24cf0c4
·
1 Parent(s): 57083db

feat: login/register page, auth guard, working logout, real user identity, fraud detection UI, remove hardcoded API key

Browse files
frontend/src/app/chat/page.tsx CHANGED
@@ -8,6 +8,7 @@ import {
8
  Wifi, WifiOff, RefreshCw
9
  } from "lucide-react";
10
  import { useWebSocketChat, WsStatus } from "@/lib/hooks/useWebSocketChat";
 
11
 
12
  // ─── Types ────────────────────────────────────────────────────────────────────
13
  interface Message {
@@ -112,7 +113,7 @@ function MessageBubble({ message, onCopy }: { message: Message; onCopy: (text: s
112
  ? "bg-gradient-to-br from-blue-500 to-purple-600 text-white text-xs font-bold"
113
  : "bg-gradient-to-br from-emerald-400 to-cyan-500"
114
  }`}>
115
- {isUser ? "A" : <Sparkles className="h-4 w-4 text-white" />}
116
  </div>
117
 
118
  <div className={`max-w-[75%] ${isUser ? "items-end" : "items-start"} flex flex-col gap-1`}>
@@ -151,6 +152,8 @@ function MessageBubble({ message, onCopy }: { message: Message; onCopy: (text: s
151
 
152
  // ─── Main Chat Page ───────────────────────────────────────────────────────────
153
  export default function ChatPage() {
 
 
154
  const [messages, setMessages] = useState<Message[]>([
155
  {
156
  id: "welcome",
@@ -217,6 +220,7 @@ export default function ChatPage() {
217
  }, []);
218
 
219
  const { status, sendMessage: wsSend, reconnect } = useWebSocketChat({
 
220
  onChunk: handleChunk,
221
  onStart: handleStart,
222
  onEnd: handleEnd,
@@ -241,7 +245,7 @@ export default function ChatPage() {
241
  setIsThinking(true);
242
  try {
243
  const { aiApi } = await import("@/lib/api");
244
- const res = await aiApi.chat(content);
245
  setIsThinking(false);
246
  const id = `ai-${Date.now()}`;
247
  setMessages((prev) => [
 
8
  Wifi, WifiOff, RefreshCw
9
  } from "lucide-react";
10
  import { useWebSocketChat, WsStatus } from "@/lib/hooks/useWebSocketChat";
11
+ import { useAuthStore } from "@/lib/stores/authStore";
12
 
13
  // ─── Types ────────────────────────────────────────────────────────────────────
14
  interface Message {
 
113
  ? "bg-gradient-to-br from-blue-500 to-purple-600 text-white text-xs font-bold"
114
  : "bg-gradient-to-br from-emerald-400 to-cyan-500"
115
  }`}>
116
+ {isUser ? userInitial : <Sparkles className="h-4 w-4 text-white" />}
117
  </div>
118
 
119
  <div className={`max-w-[75%] ${isUser ? "items-end" : "items-start"} flex flex-col gap-1`}>
 
152
 
153
  // ─── Main Chat Page ───────────────────────────────────────────────────────────
154
  export default function ChatPage() {
155
+ const { user } = useAuthStore();
156
+ const userInitial = user?.name?.charAt(0).toUpperCase() || "U";
157
  const [messages, setMessages] = useState<Message[]>([
158
  {
159
  id: "welcome",
 
220
  }, []);
221
 
222
  const { status, sendMessage: wsSend, reconnect } = useWebSocketChat({
223
+ userId: user?.user_id,
224
  onChunk: handleChunk,
225
  onStart: handleStart,
226
  onEnd: handleEnd,
 
245
  setIsThinking(true);
246
  try {
247
  const { aiApi } = await import("@/lib/api");
248
+ const res = await aiApi.chat(content, user?.user_id);
249
  setIsThinking(false);
250
  const id = `ai-${Date.now()}`;
251
  setMessages((prev) => [
frontend/src/app/layout.tsx CHANGED
@@ -1,7 +1,7 @@
1
  import type { Metadata } from "next";
2
  import { Inter } from "next/font/google";
3
  import "./globals.css";
4
- import { DashboardLayout } from "@/components/layout/DashboardLayout";
5
 
6
  const inter = Inter({ subsets: ["latin"], variable: "--font-sans" });
7
 
@@ -18,9 +18,7 @@ export default function RootLayout({
18
  return (
19
  <html lang="en" className="dark">
20
  <body className={`${inter.variable} font-sans bg-[#050505] text-white antialiased`}>
21
- <DashboardLayout>
22
- {children}
23
- </DashboardLayout>
24
  </body>
25
  </html>
26
  );
 
1
  import type { Metadata } from "next";
2
  import { Inter } from "next/font/google";
3
  import "./globals.css";
4
+ import { AppShell } from "@/components/layout/AppShell";
5
 
6
  const inter = Inter({ subsets: ["latin"], variable: "--font-sans" });
7
 
 
18
  return (
19
  <html lang="en" className="dark">
20
  <body className={`${inter.variable} font-sans bg-[#050505] text-white antialiased`}>
21
+ <AppShell>{children}</AppShell>
 
 
22
  </body>
23
  </html>
24
  );
frontend/src/app/login/page.tsx ADDED
@@ -0,0 +1,232 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useState, useEffect } from "react";
4
+ import { useRouter } from "next/navigation";
5
+ import { motion, AnimatePresence } from "framer-motion";
6
+ import { Sparkles, Eye, EyeOff, AlertCircle, Loader2, ArrowRight } from "lucide-react";
7
+ import { useAuthStore } from "@/lib/stores/authStore";
8
+
9
+ export default function LoginPage() {
10
+ const router = useRouter();
11
+ const { login, register, isAuthenticated, isLoading, error, clearError } = useAuthStore();
12
+
13
+ const [mode, setMode] = useState<"login" | "register">("login");
14
+ const [email, setEmail] = useState("");
15
+ const [password, setPassword] = useState("");
16
+ const [name, setName] = useState("");
17
+ const [showPassword, setShowPassword] = useState(false);
18
+ const [localError, setLocalError] = useState<string | null>(null);
19
+
20
+ // Redirect if already authenticated
21
+ useEffect(() => {
22
+ if (isAuthenticated) router.replace("/");
23
+ }, [isAuthenticated, router]);
24
+
25
+ // Clear errors when switching modes
26
+ useEffect(() => {
27
+ clearError();
28
+ setLocalError(null);
29
+ }, [mode, clearError]);
30
+
31
+ const handleSubmit = async (e: React.FormEvent) => {
32
+ e.preventDefault();
33
+ setLocalError(null);
34
+ clearError();
35
+
36
+ if (!email || !password) {
37
+ setLocalError("Email and password are required.");
38
+ return;
39
+ }
40
+ if (mode === "register" && !name.trim()) {
41
+ setLocalError("Name is required.");
42
+ return;
43
+ }
44
+ if (password.length < 6) {
45
+ setLocalError("Password must be at least 6 characters.");
46
+ return;
47
+ }
48
+
49
+ try {
50
+ if (mode === "login") {
51
+ await login(email, password);
52
+ } else {
53
+ await register(email, password, name.trim());
54
+ }
55
+ router.replace("/");
56
+ } catch {
57
+ // error is set in the store
58
+ }
59
+ };
60
+
61
+ const fillDemo = () => {
62
+ setEmail("alex@bankbot.dev");
63
+ setPassword("BankBot2026!");
64
+ setMode("login");
65
+ clearError();
66
+ setLocalError(null);
67
+ };
68
+
69
+ const displayError = localError || error;
70
+
71
+ return (
72
+ <div className="flex min-h-screen items-center justify-center bg-[#050505] px-4">
73
+ {/* Ambient gradients */}
74
+ <div className="fixed inset-0 z-0 pointer-events-none overflow-hidden">
75
+ <div className="absolute top-[-20%] left-[-10%] w-[50%] h-[50%] rounded-full bg-blue-600/10 blur-[160px]" />
76
+ <div className="absolute bottom-[-20%] right-[-10%] w-[50%] h-[50%] rounded-full bg-emerald-600/10 blur-[160px]" />
77
+ <div className="absolute top-[40%] left-[40%] w-[30%] h-[30%] rounded-full bg-purple-600/5 blur-[120px]" />
78
+ </div>
79
+
80
+ <motion.div
81
+ initial={{ opacity: 0, y: 24 }}
82
+ animate={{ opacity: 1, y: 0 }}
83
+ transition={{ duration: 0.5, ease: "easeOut" }}
84
+ className="relative z-10 w-full max-w-md"
85
+ >
86
+ {/* Logo */}
87
+ <div className="flex flex-col items-center mb-8">
88
+ <div className="flex h-14 w-14 items-center justify-center rounded-2xl bg-gradient-to-br from-emerald-400 to-cyan-500 shadow-2xl shadow-emerald-500/30 mb-4">
89
+ <Sparkles className="h-7 w-7 text-white" />
90
+ </div>
91
+ <h1 className="text-2xl font-bold text-white">BankBot AI</h1>
92
+ <p className="text-sm text-zinc-400 mt-1">AI-Native Financial Operating System</p>
93
+ </div>
94
+
95
+ {/* Card */}
96
+ <div className="rounded-2xl border border-white/10 bg-white/[0.04] backdrop-blur-xl p-8 shadow-2xl">
97
+ {/* Mode toggle */}
98
+ <div className="flex rounded-xl border border-white/10 bg-white/5 p-1 mb-6">
99
+ {(["login", "register"] as const).map((m) => (
100
+ <button
101
+ key={m}
102
+ onClick={() => setMode(m)}
103
+ className={`flex-1 rounded-lg py-2 text-sm font-medium capitalize transition-all ${
104
+ mode === m
105
+ ? "bg-white/10 text-white shadow-sm"
106
+ : "text-zinc-500 hover:text-zinc-300"
107
+ }`}
108
+ >
109
+ {m === "login" ? "Sign In" : "Register"}
110
+ </button>
111
+ ))}
112
+ </div>
113
+
114
+ <form onSubmit={handleSubmit} className="space-y-4">
115
+ {/* Name field (register only) */}
116
+ <AnimatePresence>
117
+ {mode === "register" && (
118
+ <motion.div
119
+ initial={{ opacity: 0, height: 0 }}
120
+ animate={{ opacity: 1, height: "auto" }}
121
+ exit={{ opacity: 0, height: 0 }}
122
+ transition={{ duration: 0.2 }}
123
+ >
124
+ <label className="block text-xs font-medium text-zinc-400 mb-1.5">
125
+ Full Name
126
+ </label>
127
+ <input
128
+ type="text"
129
+ value={name}
130
+ onChange={(e) => setName(e.target.value)}
131
+ placeholder="Alex Doe"
132
+ autoComplete="name"
133
+ className="w-full rounded-xl border border-white/10 bg-white/5 px-4 py-2.5 text-sm text-white placeholder:text-zinc-600 focus:outline-none focus:border-emerald-500/50 focus:bg-white/8 transition-all"
134
+ />
135
+ </motion.div>
136
+ )}
137
+ </AnimatePresence>
138
+
139
+ {/* Email */}
140
+ <div>
141
+ <label className="block text-xs font-medium text-zinc-400 mb-1.5">
142
+ Email Address
143
+ </label>
144
+ <input
145
+ type="email"
146
+ value={email}
147
+ onChange={(e) => setEmail(e.target.value)}
148
+ placeholder="you@example.com"
149
+ autoComplete="email"
150
+ className="w-full rounded-xl border border-white/10 bg-white/5 px-4 py-2.5 text-sm text-white placeholder:text-zinc-600 focus:outline-none focus:border-emerald-500/50 focus:bg-white/8 transition-all"
151
+ />
152
+ </div>
153
+
154
+ {/* Password */}
155
+ <div>
156
+ <label className="block text-xs font-medium text-zinc-400 mb-1.5">
157
+ Password
158
+ </label>
159
+ <div className="relative">
160
+ <input
161
+ type={showPassword ? "text" : "password"}
162
+ value={password}
163
+ onChange={(e) => setPassword(e.target.value)}
164
+ placeholder={mode === "register" ? "Min. 6 characters" : "••••••••"}
165
+ autoComplete={mode === "login" ? "current-password" : "new-password"}
166
+ className="w-full rounded-xl border border-white/10 bg-white/5 px-4 py-2.5 pr-10 text-sm text-white placeholder:text-zinc-600 focus:outline-none focus:border-emerald-500/50 focus:bg-white/8 transition-all"
167
+ />
168
+ <button
169
+ type="button"
170
+ onClick={() => setShowPassword((v) => !v)}
171
+ className="absolute right-3 top-1/2 -translate-y-1/2 text-zinc-500 hover:text-zinc-300 transition-colors"
172
+ >
173
+ {showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
174
+ </button>
175
+ </div>
176
+ </div>
177
+
178
+ {/* Error */}
179
+ <AnimatePresence>
180
+ {displayError && (
181
+ <motion.div
182
+ initial={{ opacity: 0, y: -4 }}
183
+ animate={{ opacity: 1, y: 0 }}
184
+ exit={{ opacity: 0, y: -4 }}
185
+ className="flex items-center gap-2 rounded-xl border border-red-500/20 bg-red-500/10 px-3 py-2.5"
186
+ >
187
+ <AlertCircle className="h-4 w-4 text-red-400 flex-shrink-0" />
188
+ <p className="text-xs text-red-300">{displayError}</p>
189
+ </motion.div>
190
+ )}
191
+ </AnimatePresence>
192
+
193
+ {/* Submit */}
194
+ <motion.button
195
+ type="submit"
196
+ disabled={isLoading}
197
+ whileHover={{ scale: isLoading ? 1 : 1.01 }}
198
+ whileTap={{ scale: isLoading ? 1 : 0.99 }}
199
+ className="w-full flex items-center justify-center gap-2 rounded-xl bg-gradient-to-r from-emerald-500 to-cyan-500 py-2.5 text-sm font-semibold text-white shadow-lg shadow-emerald-500/20 hover:shadow-emerald-500/30 disabled:opacity-60 disabled:cursor-not-allowed transition-all"
200
+ >
201
+ {isLoading ? (
202
+ <Loader2 className="h-4 w-4 animate-spin" />
203
+ ) : (
204
+ <>
205
+ {mode === "login" ? "Sign In" : "Create Account"}
206
+ <ArrowRight className="h-4 w-4" />
207
+ </>
208
+ )}
209
+ </motion.button>
210
+ </form>
211
+
212
+ {/* Demo account */}
213
+ <div className="mt-5 pt-5 border-t border-white/8">
214
+ <p className="text-xs text-zinc-500 text-center mb-3">Try the demo account</p>
215
+ <button
216
+ onClick={fillDemo}
217
+ className="w-full rounded-xl border border-emerald-500/20 bg-emerald-500/5 py-2.5 text-xs font-medium text-emerald-400 hover:bg-emerald-500/10 hover:border-emerald-500/30 transition-all"
218
+ >
219
+ <span className="font-mono">alex@bankbot.dev</span>
220
+ <span className="text-zinc-500 mx-2">/</span>
221
+ <span className="font-mono">BankBot2026!</span>
222
+ </button>
223
+ </div>
224
+ </div>
225
+
226
+ <p className="text-center text-xs text-zinc-600 mt-6">
227
+ AI responses are for informational purposes only. Not financial advice.
228
+ </p>
229
+ </motion.div>
230
+ </div>
231
+ );
232
+ }
frontend/src/app/page.tsx CHANGED
@@ -12,6 +12,7 @@ import {
12
  ResponsiveContainer, PieChart, Pie, Cell
13
  } from "recharts";
14
  import { useDashboardStore } from "@/lib/stores/dashboardStore";
 
15
  import Link from "next/link";
16
 
17
  // ─── Category colors ──────────────────────────────────────────────────────────
@@ -107,11 +108,14 @@ function StatCard({ title, value, change, positive, icon: Icon, gradient, iconCo
107
  // ─── Main Page ────────────────────────────────────────────────────────────────
108
  export default function Home() {
109
  const { data, isLoading, error, fetch } = useDashboardStore();
 
110
 
111
  useEffect(() => {
112
  fetch();
113
  }, [fetch]);
114
 
 
 
115
  const greeting = () => {
116
  const h = new Date().getHours();
117
  if (h < 12) return "Good morning";
@@ -142,7 +146,7 @@ export default function Home() {
142
  {/* Header */}
143
  <motion.div variants={itemVariants}>
144
  <h1 className="text-3xl font-bold tracking-tight text-white">
145
- {greeting()}, <span className="gradient-text">Alex</span> 👋
146
  </h1>
147
  <p className="text-zinc-400 mt-1 text-sm">
148
  Here&apos;s your financial overview for today.
 
12
  ResponsiveContainer, PieChart, Pie, Cell
13
  } from "recharts";
14
  import { useDashboardStore } from "@/lib/stores/dashboardStore";
15
+ import { useAuthStore } from "@/lib/stores/authStore";
16
  import Link from "next/link";
17
 
18
  // ─── Category colors ──────────────────────────────────────────────────────────
 
108
  // ─── Main Page ────────────────────────────────────────────────────────────────
109
  export default function Home() {
110
  const { data, isLoading, error, fetch } = useDashboardStore();
111
+ const { user } = useAuthStore();
112
 
113
  useEffect(() => {
114
  fetch();
115
  }, [fetch]);
116
 
117
+ const firstName = user?.name?.split(" ")[0] || "there";
118
+
119
  const greeting = () => {
120
  const h = new Date().getHours();
121
  if (h < 12) return "Good morning";
 
146
  {/* Header */}
147
  <motion.div variants={itemVariants}>
148
  <h1 className="text-3xl font-bold tracking-tight text-white">
149
+ {greeting()}, <span className="gradient-text">{firstName}</span> 👋
150
  </h1>
151
  <p className="text-zinc-400 mt-1 text-sm">
152
  Here&apos;s your financial overview for today.
frontend/src/app/security/page.tsx CHANGED
@@ -1,4 +1,374 @@
1
- import { ComingSoon } from "@/components/ui/ComingSoon";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  export default function SecurityPage() {
3
- return <ComingSoon title="Security Center" description="Real-time fraud monitoring, device management, and AI threat detection dashboard." />;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  }
 
1
+ "use client";
2
+
3
+ import { useState, useEffect, useCallback } from "react";
4
+ import { motion } from "framer-motion";
5
+ import {
6
+ Shield, AlertTriangle, CheckCircle, RefreshCw,
7
+ TrendingUp, Clock, Zap, Eye, XCircle, Activity
8
+ } from "lucide-react";
9
+ import { aiApi } from "@/lib/api";
10
+ import { useAuthStore } from "@/lib/stores/authStore";
11
+
12
+ interface FraudAlert {
13
+ id: string;
14
+ risk_score: number;
15
+ details: string;
16
+ status?: string;
17
+ transaction_id?: string;
18
+ merchant?: string;
19
+ amount?: number;
20
+ timestamp?: string;
21
+ }
22
+
23
+ interface FraudData {
24
+ total_alerts: number;
25
+ pending_reviews: number;
26
+ alerts: FraudAlert[];
27
+ }
28
+
29
+ function Skeleton({ className }: { className?: string }) {
30
+ return <div className={`shimmer rounded-lg ${className}`} />;
31
+ }
32
+
33
+ function RiskBadge({ score }: { score: number }) {
34
+ const level =
35
+ score >= 70 ? { label: "High Risk", color: "text-red-400", bg: "bg-red-500/10 border-red-500/20" }
36
+ : score >= 40 ? { label: "Suspicious", color: "text-amber-400", bg: "bg-amber-500/10 border-amber-500/20" }
37
+ : { label: "Low Risk", color: "text-emerald-400", bg: "bg-emerald-500/10 border-emerald-500/20" };
38
+
39
+ return (
40
+ <span className={`rounded-full border px-2.5 py-1 text-xs font-semibold ${level.color} ${level.bg}`}>
41
+ {level.label}
42
+ </span>
43
+ );
44
+ }
45
+
46
+ function RiskBar({ score }: { score: number }) {
47
+ const color = score >= 70 ? "#ef4444" : score >= 40 ? "#f59e0b" : "#10b981";
48
+ return (
49
+ <div className="flex items-center gap-2">
50
+ <div className="h-1.5 w-24 rounded-full bg-white/10 overflow-hidden">
51
+ <motion.div
52
+ initial={{ width: 0 }}
53
+ animate={{ width: `${score}%` }}
54
+ transition={{ duration: 0.8, ease: "easeOut" }}
55
+ className="h-full rounded-full"
56
+ style={{ background: color }}
57
+ />
58
+ </div>
59
+ <span className="text-xs font-mono" style={{ color }}>{score}</span>
60
+ </div>
61
+ );
62
+ }
63
+
64
+ const containerVariants = {
65
+ hidden: { opacity: 0 },
66
+ visible: { opacity: 1, transition: { staggerChildren: 0.07 } },
67
+ };
68
+ const itemVariants = {
69
+ hidden: { opacity: 0, y: 16 },
70
+ visible: { opacity: 1, y: 0, transition: { duration: 0.4 } },
71
+ };
72
+
73
  export default function SecurityPage() {
74
+ const { user } = useAuthStore();
75
+ const [data, setData] = useState<FraudData | null>(null);
76
+ const [loading, setLoading] = useState(true);
77
+ const [error, setError] = useState<string | null>(null);
78
+ const [filter, setFilter] = useState<"all" | "high" | "pending">("all");
79
+
80
+ const load = useCallback(async () => {
81
+ setLoading(true);
82
+ setError(null);
83
+ try {
84
+ const res = await aiApi.fraudAnalysis(user?.user_id);
85
+ // Normalize response shape
86
+ const raw = res as unknown as Record<string, unknown>;
87
+ const alerts: FraudAlert[] = (raw.alerts as FraudAlert[]) || [];
88
+ setData({
89
+ total_alerts: (raw.total_alerts as number) ?? alerts.length,
90
+ pending_reviews: (raw.pending_reviews as number) ?? alerts.filter((a) => a.status === "pending").length,
91
+ alerts,
92
+ });
93
+ } catch (err) {
94
+ setError((err as Error).message);
95
+ } finally {
96
+ setLoading(false);
97
+ }
98
+ }, [user?.user_id]);
99
+
100
+ useEffect(() => { load(); }, [load]);
101
+
102
+ const alerts = data?.alerts || [];
103
+ const filtered = alerts.filter((a) => {
104
+ if (filter === "high") return a.risk_score >= 70;
105
+ if (filter === "pending") return a.status === "pending" || !a.status;
106
+ return true;
107
+ });
108
+
109
+ const highRiskCount = alerts.filter((a) => a.risk_score >= 70).length;
110
+ const suspiciousCount = alerts.filter((a) => a.risk_score >= 40 && a.risk_score < 70).length;
111
+ const safeCount = alerts.filter((a) => a.risk_score < 40).length;
112
+ const avgScore = alerts.length
113
+ ? Math.round(alerts.reduce((s, a) => s + a.risk_score, 0) / alerts.length)
114
+ : 0;
115
+
116
+ return (
117
+ <motion.div
118
+ variants={containerVariants}
119
+ initial="hidden"
120
+ animate="visible"
121
+ className="flex flex-col gap-6"
122
+ >
123
+ {/* Header */}
124
+ <motion.div variants={itemVariants} className="flex items-center justify-between">
125
+ <div>
126
+ <h1 className="text-3xl font-bold tracking-tight text-white">Security Center</h1>
127
+ <p className="text-zinc-400 mt-1 text-sm">
128
+ AI-powered fraud detection and transaction monitoring
129
+ </p>
130
+ </div>
131
+ <button
132
+ onClick={load}
133
+ className="flex items-center gap-2 rounded-xl border border-white/10 bg-white/5 px-3 py-2 text-xs text-zinc-400 hover:text-white transition-colors"
134
+ >
135
+ <RefreshCw className={`h-3.5 w-3.5 ${loading ? "animate-spin" : ""}`} />
136
+ Refresh
137
+ </button>
138
+ </motion.div>
139
+
140
+ {/* Error */}
141
+ {error && (
142
+ <motion.div variants={itemVariants} className="flex items-center gap-3 rounded-2xl border border-amber-500/20 bg-amber-500/5 px-5 py-3">
143
+ <AlertTriangle className="h-4 w-4 text-amber-400 flex-shrink-0" />
144
+ <p className="text-xs text-zinc-400">
145
+ <span className="text-amber-400 font-medium">Could not load fraud data</span> — {error}
146
+ </p>
147
+ </motion.div>
148
+ )}
149
+
150
+ {/* AI Shield Banner */}
151
+ <motion.div
152
+ variants={itemVariants}
153
+ className="flex items-center gap-4 rounded-2xl border border-emerald-500/20 bg-gradient-to-r from-emerald-500/10 via-cyan-500/5 to-blue-500/10 px-5 py-4"
154
+ >
155
+ <div className="flex h-10 w-10 items-center justify-center rounded-xl bg-emerald-500/20 border border-emerald-500/30 flex-shrink-0">
156
+ <Shield className="h-5 w-5 text-emerald-400" />
157
+ </div>
158
+ <div className="flex-1">
159
+ <p className="text-sm font-semibold text-white">AI Fraud Shield Active</p>
160
+ <p className="text-xs text-zinc-400 mt-0.5">
161
+ Monitoring amount spikes, late-night activity, rapid transactions, and duplicate payments in real-time.
162
+ </p>
163
+ </div>
164
+ <div className="flex items-center gap-1.5">
165
+ <div className="h-2 w-2 rounded-full bg-emerald-400 animate-pulse" />
166
+ <span className="text-xs text-emerald-400 font-medium">Live</span>
167
+ </div>
168
+ </motion.div>
169
+
170
+ {/* Stats */}
171
+ <div className="grid grid-cols-2 gap-4 sm:grid-cols-4">
172
+ {[
173
+ {
174
+ label: "Total Alerts",
175
+ value: loading ? "—" : String(data?.total_alerts ?? 0),
176
+ icon: Activity,
177
+ color: "text-blue-400",
178
+ border: "border-blue-500/20",
179
+ bg: "from-blue-500/10 to-blue-500/5",
180
+ },
181
+ {
182
+ label: "High Risk",
183
+ value: loading ? "—" : String(highRiskCount),
184
+ icon: XCircle,
185
+ color: "text-red-400",
186
+ border: "border-red-500/20",
187
+ bg: "from-red-500/10 to-red-500/5",
188
+ },
189
+ {
190
+ label: "Suspicious",
191
+ value: loading ? "—" : String(suspiciousCount),
192
+ icon: AlertTriangle,
193
+ color: "text-amber-400",
194
+ border: "border-amber-500/20",
195
+ bg: "from-amber-500/10 to-amber-500/5",
196
+ },
197
+ {
198
+ label: "Avg Risk Score",
199
+ value: loading ? "—" : `${avgScore}/100`,
200
+ icon: TrendingUp,
201
+ color: "text-purple-400",
202
+ border: "border-purple-500/20",
203
+ bg: "from-purple-500/10 to-purple-500/5",
204
+ },
205
+ ].map((stat) => (
206
+ <motion.div
207
+ key={stat.label}
208
+ variants={itemVariants}
209
+ className={`rounded-2xl border ${stat.border} bg-gradient-to-br ${stat.bg} p-5`}
210
+ >
211
+ <div className="flex items-start justify-between">
212
+ <div>
213
+ <p className="text-xs text-zinc-400 uppercase tracking-wider">{stat.label}</p>
214
+ <p className={`mt-2 text-2xl font-bold ${stat.color}`}>{stat.value}</p>
215
+ </div>
216
+ <stat.icon className={`h-5 w-5 ${stat.color} opacity-60`} />
217
+ </div>
218
+ </motion.div>
219
+ ))}
220
+ </div>
221
+
222
+ {/* Detection Rules */}
223
+ <motion.div variants={itemVariants} className="glass-card p-6">
224
+ <h2 className="text-base font-semibold text-white mb-4">Active Detection Rules</h2>
225
+ <div className="grid gap-3 sm:grid-cols-2">
226
+ {[
227
+ { icon: TrendingUp, label: "Amount Spike Detection", desc: "Flags transactions 3.5× above your average", active: true },
228
+ { icon: Clock, label: "Late-Night Monitoring", desc: "Alerts on activity between 11PM – 4AM", active: true },
229
+ { icon: Zap, label: "Rapid Transaction Guard", desc: "Detects multiple transactions within 3 minutes", active: true },
230
+ { icon: Eye, label: "Duplicate Payment Check", desc: "Catches same merchant + amount within 10 minutes", active: true },
231
+ ].map((rule) => (
232
+ <div key={rule.label} className="flex items-start gap-3 rounded-xl border border-white/8 bg-white/[0.02] p-4">
233
+ <div className="flex h-8 w-8 items-center justify-center rounded-lg bg-emerald-500/10 border border-emerald-500/20 flex-shrink-0">
234
+ <rule.icon className="h-4 w-4 text-emerald-400" />
235
+ </div>
236
+ <div className="flex-1 min-w-0">
237
+ <div className="flex items-center justify-between gap-2">
238
+ <p className="text-sm font-medium text-white">{rule.label}</p>
239
+ <div className="flex items-center gap-1 flex-shrink-0">
240
+ <div className="h-1.5 w-1.5 rounded-full bg-emerald-400" />
241
+ <span className="text-[10px] text-emerald-400">ON</span>
242
+ </div>
243
+ </div>
244
+ <p className="text-xs text-zinc-500 mt-0.5">{rule.desc}</p>
245
+ </div>
246
+ </div>
247
+ ))}
248
+ </div>
249
+ </motion.div>
250
+
251
+ {/* Alerts Table */}
252
+ <motion.div variants={itemVariants} className="glass-card overflow-hidden">
253
+ <div className="flex items-center justify-between px-6 py-4 border-b border-white/8">
254
+ <div>
255
+ <h2 className="text-base font-semibold text-white">Fraud Alerts</h2>
256
+ <p className="text-xs text-zinc-400 mt-0.5">
257
+ {loading ? "Loading..." : `${filtered.length} alert${filtered.length !== 1 ? "s" : ""}`}
258
+ </p>
259
+ </div>
260
+ <div className="flex items-center gap-1 rounded-xl border border-white/10 bg-white/5 p-1">
261
+ {(["all", "high", "pending"] as const).map((f) => (
262
+ <button
263
+ key={f}
264
+ onClick={() => setFilter(f)}
265
+ className={`rounded-lg px-3 py-1.5 text-xs font-medium capitalize transition-all ${
266
+ filter === f ? "bg-white/10 text-white" : "text-zinc-500 hover:text-zinc-300"
267
+ }`}
268
+ >
269
+ {f === "all" ? "All" : f === "high" ? "High Risk" : "Pending"}
270
+ </button>
271
+ ))}
272
+ </div>
273
+ </div>
274
+
275
+ {loading ? (
276
+ <div className="space-y-px p-4">
277
+ {[1, 2, 3].map((i) => <Skeleton key={i} className="h-16 w-full rounded-xl" />)}
278
+ </div>
279
+ ) : filtered.length === 0 ? (
280
+ <div className="flex flex-col items-center gap-3 py-16 text-center">
281
+ <CheckCircle className="h-12 w-12 text-emerald-500/40" />
282
+ <p className="text-sm font-medium text-zinc-400">
283
+ {alerts.length === 0 ? "No fraud alerts detected" : "No alerts match this filter"}
284
+ </p>
285
+ <p className="text-xs text-zinc-600">
286
+ {alerts.length === 0
287
+ ? "All your transactions look clean. AI Shield is monitoring in real-time."
288
+ : "Try a different filter to see more alerts."}
289
+ </p>
290
+ </div>
291
+ ) : (
292
+ <div className="divide-y divide-white/5">
293
+ {filtered.map((alert, i) => (
294
+ <motion.div
295
+ key={alert.id}
296
+ initial={{ opacity: 0, x: -8 }}
297
+ animate={{ opacity: 1, x: 0 }}
298
+ transition={{ delay: i * 0.04 }}
299
+ className="flex items-center gap-4 px-6 py-4 hover:bg-white/[0.02] transition-colors"
300
+ >
301
+ {/* Risk indicator */}
302
+ <div
303
+ className={`flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-xl border ${
304
+ alert.risk_score >= 70
305
+ ? "bg-red-500/10 border-red-500/20"
306
+ : alert.risk_score >= 40
307
+ ? "bg-amber-500/10 border-amber-500/20"
308
+ : "bg-emerald-500/10 border-emerald-500/20"
309
+ }`}
310
+ >
311
+ {alert.risk_score >= 70 ? (
312
+ <XCircle className="h-5 w-5 text-red-400" />
313
+ ) : alert.risk_score >= 40 ? (
314
+ <AlertTriangle className="h-5 w-5 text-amber-400" />
315
+ ) : (
316
+ <CheckCircle className="h-5 w-5 text-emerald-400" />
317
+ )}
318
+ </div>
319
+
320
+ {/* Details */}
321
+ <div className="flex-1 min-w-0">
322
+ <div className="flex items-center gap-2 flex-wrap">
323
+ <p className="text-sm font-medium text-white truncate">
324
+ {alert.merchant || `Transaction ${alert.transaction_id?.slice(0, 8) || alert.id.slice(0, 8)}...`}
325
+ </p>
326
+ <RiskBadge score={alert.risk_score} />
327
+ {alert.status && (
328
+ <span className="rounded-full border border-white/10 bg-white/5 px-2 py-0.5 text-[10px] text-zinc-400 capitalize">
329
+ {alert.status}
330
+ </span>
331
+ )}
332
+ </div>
333
+ <p className="text-xs text-zinc-500 mt-0.5 truncate">
334
+ {typeof alert.details === "string"
335
+ ? alert.details
336
+ : Array.isArray(alert.details)
337
+ ? (alert.details as string[]).join(" · ")
338
+ : "Suspicious activity detected"}
339
+ </p>
340
+ </div>
341
+
342
+ {/* Score bar */}
343
+ <div className="flex-shrink-0 hidden sm:block">
344
+ <RiskBar score={alert.risk_score} />
345
+ </div>
346
+
347
+ {/* Amount */}
348
+ {alert.amount != null && (
349
+ <div className="flex-shrink-0 text-right">
350
+ <p className="text-sm font-semibold text-white">${Math.abs(alert.amount).toFixed(2)}</p>
351
+ </div>
352
+ )}
353
+ </motion.div>
354
+ ))}
355
+ </div>
356
+ )}
357
+ </motion.div>
358
+
359
+ {/* Safe status */}
360
+ {!loading && safeCount > 0 && (
361
+ <motion.div
362
+ variants={itemVariants}
363
+ className="flex items-center gap-3 rounded-2xl border border-emerald-500/20 bg-emerald-500/5 px-5 py-3"
364
+ >
365
+ <CheckCircle className="h-4 w-4 text-emerald-400 flex-shrink-0" />
366
+ <p className="text-xs text-zinc-400">
367
+ <span className="text-emerald-400 font-medium">{safeCount} transaction{safeCount !== 1 ? "s" : ""} verified safe</span>
368
+ {" "}— risk score below threshold. No action required.
369
+ </p>
370
+ </motion.div>
371
+ )}
372
+ </motion.div>
373
+ );
374
  }
frontend/src/components/layout/AppShell.tsx ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ /**
4
+ * AppShell — top-level client wrapper.
5
+ * - Login page renders without the dashboard chrome.
6
+ * - All other pages are guarded: unauthenticated users are redirected to /login.
7
+ * - Restores session from localStorage on first mount.
8
+ */
9
+
10
+ import { useEffect, useState } from "react";
11
+ import { usePathname, useRouter } from "next/navigation";
12
+ import { DashboardLayout } from "./DashboardLayout";
13
+ import { useAuthStore } from "@/lib/stores/authStore";
14
+ import { Loader2, Sparkles } from "lucide-react";
15
+
16
+ const PUBLIC_PATHS = ["/login"];
17
+
18
+ function FullPageSpinner() {
19
+ return (
20
+ <div className="flex min-h-screen items-center justify-center bg-[#050505]">
21
+ <div className="flex flex-col items-center gap-4">
22
+ <div className="flex h-14 w-14 items-center justify-center rounded-2xl bg-gradient-to-br from-emerald-400 to-cyan-500 shadow-2xl shadow-emerald-500/30">
23
+ <Sparkles className="h-7 w-7 text-white" />
24
+ </div>
25
+ <Loader2 className="h-5 w-5 animate-spin text-emerald-400" />
26
+ <p className="text-xs text-zinc-500">Loading BankBot AI...</p>
27
+ </div>
28
+ </div>
29
+ );
30
+ }
31
+
32
+ export function AppShell({ children }: { children: React.ReactNode }) {
33
+ const pathname = usePathname();
34
+ const router = useRouter();
35
+ const { isAuthenticated, restoreSession } = useAuthStore();
36
+ const [hydrated, setHydrated] = useState(false);
37
+
38
+ const isPublic = PUBLIC_PATHS.includes(pathname);
39
+
40
+ useEffect(() => {
41
+ // Restore session from localStorage token, then mark hydrated
42
+ restoreSession().finally(() => setHydrated(true));
43
+ }, [restoreSession]);
44
+
45
+ // Wait for hydration before making routing decisions
46
+ if (!hydrated) return <FullPageSpinner />;
47
+
48
+ // Public pages (login) — render without dashboard chrome
49
+ if (isPublic) return <>{children}</>;
50
+
51
+ // Protected pages — redirect to login if not authenticated
52
+ if (!isAuthenticated) {
53
+ router.replace("/login");
54
+ return <FullPageSpinner />;
55
+ }
56
+
57
+ // Authenticated — render with full dashboard layout
58
+ return <DashboardLayout>{children}</DashboardLayout>;
59
+ }
frontend/src/components/layout/Navbar.tsx CHANGED
@@ -4,8 +4,19 @@ import { Search, User } from "lucide-react";
4
  import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
5
  import { NotificationBell } from "@/components/notifications/NotificationPanel";
6
  import { motion } from "framer-motion";
 
7
 
8
  export function Navbar() {
 
 
 
 
 
 
 
 
 
 
9
  return (
10
  <header className="sticky top-0 z-40 flex h-16 w-full items-center justify-between bg-black/40 backdrop-blur-xl border-b border-white/10 px-8">
11
  {/* Search */}
@@ -43,14 +54,16 @@ export function Navbar() {
43
  >
44
  <div className="flex flex-col text-right">
45
  <span className="text-sm font-semibold text-white group-hover:text-emerald-400 transition-colors leading-tight">
46
- Alex Doe
 
 
 
47
  </span>
48
- <span className="text-xs text-emerald-500 leading-tight">Premium</span>
49
  </div>
50
  <Avatar className="h-9 w-9 border border-emerald-500/30 ring-2 ring-emerald-500/10">
51
- <AvatarImage src="https://github.com/shadcn.png" alt="@user" />
52
- <AvatarFallback className="bg-emerald-900 text-emerald-400 text-xs">
53
- <User className="h-4 w-4" />
54
  </AvatarFallback>
55
  </Avatar>
56
  </motion.div>
 
4
  import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
5
  import { NotificationBell } from "@/components/notifications/NotificationPanel";
6
  import { motion } from "framer-motion";
7
+ import { useAuthStore } from "@/lib/stores/authStore";
8
 
9
  export function Navbar() {
10
+ const { user } = useAuthStore();
11
+
12
+ const displayName = user?.name || "User";
13
+ const initials = displayName
14
+ .split(" ")
15
+ .map((n) => n[0])
16
+ .join("")
17
+ .toUpperCase()
18
+ .slice(0, 2);
19
+
20
  return (
21
  <header className="sticky top-0 z-40 flex h-16 w-full items-center justify-between bg-black/40 backdrop-blur-xl border-b border-white/10 px-8">
22
  {/* Search */}
 
54
  >
55
  <div className="flex flex-col text-right">
56
  <span className="text-sm font-semibold text-white group-hover:text-emerald-400 transition-colors leading-tight">
57
+ {displayName}
58
+ </span>
59
+ <span className="text-xs text-emerald-500 leading-tight">
60
+ {user?.financial_personality || "Premium"}
61
  </span>
 
62
  </div>
63
  <Avatar className="h-9 w-9 border border-emerald-500/30 ring-2 ring-emerald-500/10">
64
+ <AvatarImage src="" alt={displayName} />
65
+ <AvatarFallback className="bg-emerald-900 text-emerald-400 text-xs font-bold">
66
+ {initials || <User className="h-4 w-4" />}
67
  </AvatarFallback>
68
  </Avatar>
69
  </motion.div>
frontend/src/components/layout/Sidebar.tsx CHANGED
@@ -1,7 +1,7 @@
1
  "use client";
2
 
3
  import Link from "next/link";
4
- import { usePathname } from "next/navigation";
5
  import { cn } from "@/lib/utils";
6
  import { motion } from "framer-motion";
7
  import {
@@ -18,6 +18,7 @@ import {
18
  Sparkles,
19
  Activity,
20
  } from "lucide-react";
 
21
 
22
  const navigation = [
23
  { name: "Overview", href: "/", icon: LayoutDashboard },
@@ -34,6 +35,13 @@ const navigation = [
34
 
35
  export function Sidebar() {
36
  const pathname = usePathname();
 
 
 
 
 
 
 
37
 
38
  return (
39
  <div className="flex h-full w-60 flex-col overflow-y-auto bg-black/60 backdrop-blur-2xl border-r border-white/8 text-white">
@@ -114,7 +122,9 @@ export function Sidebar() {
114
  </div>
115
  </div>
116
 
117
- <button className="group flex w-full items-center rounded-xl px-3 py-2.5 text-sm font-medium text-zinc-500 hover:bg-red-500/8 hover:text-red-400 transition-all duration-200">
 
 
118
  <LogOut className="mr-3 h-4 w-4 flex-shrink-0 transition-colors" />
119
  Sign Out
120
  </button>
 
1
  "use client";
2
 
3
  import Link from "next/link";
4
+ import { usePathname, useRouter } from "next/navigation";
5
  import { cn } from "@/lib/utils";
6
  import { motion } from "framer-motion";
7
  import {
 
18
  Sparkles,
19
  Activity,
20
  } from "lucide-react";
21
+ import { useAuthStore } from "@/lib/stores/authStore";
22
 
23
  const navigation = [
24
  { name: "Overview", href: "/", icon: LayoutDashboard },
 
35
 
36
  export function Sidebar() {
37
  const pathname = usePathname();
38
+ const router = useRouter();
39
+ const { logout } = useAuthStore();
40
+
41
+ const handleLogout = () => {
42
+ logout();
43
+ router.replace("/login");
44
+ };
45
 
46
  return (
47
  <div className="flex h-full w-60 flex-col overflow-y-auto bg-black/60 backdrop-blur-2xl border-r border-white/8 text-white">
 
122
  </div>
123
  </div>
124
 
125
+ <button
126
+ onClick={handleLogout}
127
+ className="group flex w-full items-center rounded-xl px-3 py-2.5 text-sm font-medium text-zinc-500 hover:bg-red-500/8 hover:text-red-400 transition-all duration-200">
128
  <LogOut className="mr-3 h-4 w-4 flex-shrink-0 transition-colors" />
129
  Sign Out
130
  </button>