Bromeo777 commited on
Commit
c8d063c
·
unverified ·
1 Parent(s): 07f556e

Create page.tsx

Browse files
Files changed (1) hide show
  1. src/app/login/page.tsx +151 -0
src/app/login/page.tsx ADDED
@@ -0,0 +1,151 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { useRouter } from "next/navigation";
5
+ import { Lock, Mail, Sparkles, AlertCircle } from "lucide-react";
6
+ import { Button } from "@/components/atoms/Button";
7
+ import { Icon } from "@/components/atoms/Icon";
8
+ import { Spinner } from "@/components/atoms/Spinner";
9
+
10
+ /**
11
+ * Login Page
12
+ * Handles JWT authentication with the FastAPI backend.
13
+ * Optimized for a secure and professional academic entry-point.
14
+ */
15
+ export default function LoginPage() {
16
+ const router = useRouter();
17
+ const [isLoading, setIsLoading] = React.useState(false);
18
+ const [error, setError] = React.useState<string | null>(null);
19
+
20
+ const [formData, setFormData] = React.useState({
21
+ username: "", // FastAPI OAuth2 expects 'username' (which is the email)
22
+ password: "",
23
+ });
24
+
25
+ const handleLogin = async (e: React.FormEvent) => {
26
+ e.preventDefault();
27
+ setIsLoading(true);
28
+ setError(null);
29
+
30
+ try {
31
+ // 1. Prepare form data for OAuth2 Password Flow
32
+ const loginData = new URLSearchParams();
33
+ loginData.append("username", formData.username);
34
+ loginData.append("password", formData.password);
35
+
36
+ const res = await fetch("/api/v1/auth/login", {
37
+ method: "POST",
38
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
39
+ body: loginData,
40
+ });
41
+
42
+ if (res.ok) {
43
+ const data = await res.json();
44
+ // 2. Persist the JWT
45
+ localStorage.setItem("token", data.access_token);
46
+ // 3. Redirect to the Discovery engine
47
+ router.push("/explore");
48
+ } else {
49
+ const errorData = await res.json();
50
+ setError(errorData.detail || "Invalid credentials. Please try again.");
51
+ }
52
+ } catch (err) {
53
+ setError("Connection to research server failed.");
54
+ } finally {
55
+ setIsLoading(false);
56
+ }
57
+ };
58
+
59
+ return (
60
+ <div className="flex min-h-screen items-center justify-center bg-background p-6">
61
+ <div className="w-full max-w-sm space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-700">
62
+
63
+ {/* Branding Header */}
64
+ <div className="flex flex-col items-center text-center space-y-2">
65
+ <div className="flex h-12 w-12 items-center justify-center rounded-xl bg-primary/10 text-primary shadow-inner">
66
+ <Icon icon={Sparkles} size={28} />
67
+ </div>
68
+ <div className="space-y-1">
69
+ <h1 className="text-2xl font-bold tracking-tight">RM Research Assistant</h1>
70
+ <p className="text-xs text-muted-foreground font-medium uppercase tracking-widest">
71
+ Scientific Workspace
72
+ </p>
73
+ </div>
74
+ </div>
75
+
76
+ {/* Login Form */}
77
+ <form onSubmit={handleLogin} className="space-y-4">
78
+ <div className="space-y-2">
79
+ <div className="relative">
80
+ <Icon
81
+ icon={Mail}
82
+ size={16}
83
+ className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground"
84
+ />
85
+ <input
86
+ required
87
+ type="email"
88
+ placeholder="Institutional Email"
89
+ className="w-full rounded-lg border bg-card py-2.5 pl-10 pr-4 text-sm outline-none transition-all focus:ring-2 focus:ring-primary/20"
90
+ value={formData.username}
91
+ onChange={(e) => setFormData({ ...formData, username: e.target.value })}
92
+ />
93
+ </div>
94
+ </div>
95
+
96
+ <div className="space-y-2">
97
+ <div className="relative">
98
+ <Icon
99
+ icon={Lock}
100
+ size={16}
101
+ className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground"
102
+ />
103
+ <input
104
+ required
105
+ type="password"
106
+ placeholder="Password"
107
+ className="w-full rounded-lg border bg-card py-2.5 pl-10 pr-4 text-sm outline-none transition-all focus:ring-2 focus:ring-primary/20"
108
+ value={formData.password}
109
+ onChange={(e) => setFormData({ ...formData, password: e.target.value })}
110
+ />
111
+ </div>
112
+ </div>
113
+
114
+ {/* Error Display */}
115
+ {error && (
116
+ <div className="flex items-center gap-2 rounded-lg bg-destructive/10 p-3 text-xs font-medium text-destructive animate-in shake-1">
117
+ <Icon icon={AlertCircle} size={14} />
118
+ {error}
119
+ </div>
120
+ )}
121
+
122
+ <Button
123
+ type="submit"
124
+ className="w-full py-6 font-bold tracking-wide"
125
+ disabled={isLoading}
126
+ >
127
+ {isLoading ? <Spinner size={18} /> : "Sign In to Workspace"}
128
+ </Button>
129
+ </form>
130
+
131
+ {/* Footer Links */}
132
+ <div className="text-center space-y-4">
133
+ <p className="text-xs text-muted-foreground">
134
+ Don't have an account?{" "}
135
+ <button
136
+ onClick={() => router.push("/register")}
137
+ className="font-bold text-primary hover:underline"
138
+ >
139
+ Request Access
140
+ </button>
141
+ </p>
142
+ <div className="h-px bg-border w-full" />
143
+ <p className="text-[10px] text-muted-foreground leading-relaxed italic">
144
+ Secure access for authorized research personnel only.
145
+ By signing in, you agree to the scientific data protocols.
146
+ </p>
147
+ </div>
148
+ </div>
149
+ </div>
150
+ );
151
+ }