269 lines
7.6 KiB
TypeScript
269 lines
7.6 KiB
TypeScript
"use client";
|
||
|
||
import { useState } from "react";
|
||
import { useRouter } from "next/navigation";
|
||
|
||
const API_BASE_URL =
|
||
process.env.NEXT_PUBLIC_API_BASE_URL || "http://127.0.0.1:18000";
|
||
|
||
export default function LoginPage() {
|
||
const [login, setLogin] = useState("");
|
||
const [password, setPassword] = useState("");
|
||
const [error, setError] = useState("");
|
||
const [showPassword, setShowPassword] = useState(false);
|
||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||
const router = useRouter();
|
||
|
||
const handleSubmit = async (e) => {
|
||
e.preventDefault();
|
||
setError("");
|
||
setIsSubmitting(true);
|
||
|
||
try {
|
||
const res = await fetch(`${API_BASE_URL}/api/auth/login`, {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
credentials: "include",
|
||
body: JSON.stringify({ login, password }),
|
||
});
|
||
|
||
if (!res.ok) {
|
||
let message = "Неверный логин или пароль";
|
||
try {
|
||
const data = await res.json();
|
||
if (data?.detail && typeof data.detail === "string") {
|
||
message = data.detail;
|
||
}
|
||
} catch {
|
||
// ignore malformed/non-json body
|
||
}
|
||
throw new Error(message);
|
||
}
|
||
|
||
router.push("/");
|
||
router.refresh();
|
||
} catch (err) {
|
||
setError(err?.message || "Ошибка входа");
|
||
} finally {
|
||
setIsSubmitting(false);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<div
|
||
style={{
|
||
display: "flex",
|
||
flexDirection: "column",
|
||
minHeight: "100vh",
|
||
backgroundColor: "var(--bg-color)",
|
||
alignItems: "center",
|
||
justifyContent: "center",
|
||
position: "relative",
|
||
}}
|
||
>
|
||
<div
|
||
style={{
|
||
position: "absolute",
|
||
top: 24,
|
||
left: 32,
|
||
display: "flex",
|
||
alignItems: "center",
|
||
gap: 8,
|
||
fontSize: 20,
|
||
fontWeight: 600,
|
||
color: "var(--text-main)",
|
||
}}
|
||
>
|
||
<svg
|
||
width="24"
|
||
height="24"
|
||
viewBox="0 0 24 24"
|
||
fill="none"
|
||
xmlns="http://www.w3.org/2000/svg"
|
||
>
|
||
<path
|
||
d="M12 2C6.477 2 2 6.477 2 12C2 17.523 6.477 22 12 22C17.523 22 22 17.523 22 12C22 6.477 17.523 2 12 2ZM12 4C16.418 4 20 7.582 20 12C20 16.418 16.418 20 12 20C7.582 20 4 16.418 4 12C4 7.582 7.582 4 12 4Z"
|
||
fill="var(--primary)"
|
||
/>
|
||
<path
|
||
d="M15 13H9C8.448 13 8 12.552 8 12C8 11.448 8.448 11 9 11H15C15.552 11 16 11.448 16 12C16 12.552 15.552 13 15 13Z"
|
||
fill="var(--primary)"
|
||
/>
|
||
</svg>
|
||
AI Chat MVP
|
||
</div>
|
||
|
||
<div
|
||
style={{
|
||
background: "#fff",
|
||
padding: "40px",
|
||
borderRadius: "16px",
|
||
boxShadow: "0 4px 20px rgba(0,0,0,0.05)",
|
||
width: "100%",
|
||
maxWidth: "420px",
|
||
textAlign: "center",
|
||
}}
|
||
>
|
||
<h1
|
||
style={{
|
||
fontSize: "24px",
|
||
fontWeight: 500,
|
||
marginBottom: "32px",
|
||
color: "var(--text-main)",
|
||
}}
|
||
>
|
||
Вход в систему
|
||
</h1>
|
||
|
||
<form
|
||
onSubmit={handleSubmit}
|
||
style={{
|
||
display: "flex",
|
||
flexDirection: "column",
|
||
gap: "20px",
|
||
textAlign: "left",
|
||
}}
|
||
>
|
||
<div>
|
||
<label
|
||
style={{
|
||
display: "block",
|
||
fontSize: "14px",
|
||
marginBottom: "8px",
|
||
color: "var(--text-main)",
|
||
}}
|
||
>
|
||
Логин
|
||
</label>
|
||
<input
|
||
value={login}
|
||
onChange={(e) => setLogin(e.target.value)}
|
||
type="text"
|
||
placeholder="admin"
|
||
required
|
||
style={{
|
||
width: "100%",
|
||
padding: "12px 16px",
|
||
borderRadius: "8px",
|
||
border: "1px solid var(--border-color)",
|
||
fontSize: "15px",
|
||
outline: "none",
|
||
transition: "border-color 0.2s",
|
||
}}
|
||
onFocus={(e) => (e.target.style.borderColor = "var(--primary)")}
|
||
onBlur={(e) =>
|
||
(e.target.style.borderColor = "var(--border-color)")
|
||
}
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<div
|
||
style={{
|
||
display: "flex",
|
||
justifyContent: "space-between",
|
||
marginBottom: "8px",
|
||
}}
|
||
>
|
||
<label style={{ fontSize: "14px", color: "var(--text-main)" }}>
|
||
Пароль
|
||
</label>
|
||
<button
|
||
type="button"
|
||
onClick={() => setShowPassword(!showPassword)}
|
||
style={{
|
||
background: "none",
|
||
border: "none",
|
||
fontSize: "13px",
|
||
color: "var(--text-muted)",
|
||
display: "flex",
|
||
alignItems: "center",
|
||
gap: "4px",
|
||
padding: 0,
|
||
cursor: "pointer",
|
||
}}
|
||
>
|
||
{showPassword ? "Скрыть" : "Показать"}
|
||
</button>
|
||
</div>
|
||
|
||
<input
|
||
value={password}
|
||
onChange={(e) => setPassword(e.target.value)}
|
||
type={showPassword ? "text" : "password"}
|
||
placeholder="••••••••"
|
||
required
|
||
style={{
|
||
width: "100%",
|
||
padding: "12px 16px",
|
||
borderRadius: "8px",
|
||
border: "1px solid var(--border-color)",
|
||
fontSize: "15px",
|
||
outline: "none",
|
||
transition: "border-color 0.2s",
|
||
}}
|
||
onFocus={(e) => (e.target.style.borderColor = "var(--primary)")}
|
||
onBlur={(e) =>
|
||
(e.target.style.borderColor = "var(--border-color)")
|
||
}
|
||
/>
|
||
</div>
|
||
|
||
{error ? (
|
||
<div
|
||
style={{
|
||
color: "#d32f2f",
|
||
fontSize: "14px",
|
||
textAlign: "center",
|
||
}}
|
||
>
|
||
{error}
|
||
</div>
|
||
) : null}
|
||
|
||
<button
|
||
type="submit"
|
||
disabled={isSubmitting}
|
||
style={{
|
||
padding: "14px",
|
||
marginTop: "8px",
|
||
backgroundColor: isSubmitting
|
||
? "var(--border-color)"
|
||
: "var(--primary)",
|
||
color: "#fff",
|
||
border: "none",
|
||
borderRadius: "8px",
|
||
fontSize: "15px",
|
||
fontWeight: 500,
|
||
transition: "background-color 0.2s",
|
||
cursor: isSubmitting ? "not-allowed" : "pointer",
|
||
}}
|
||
onMouseOver={(e) => {
|
||
if (!isSubmitting) {
|
||
e.currentTarget.style.backgroundColor = "var(--primary-hover)";
|
||
}
|
||
}}
|
||
onMouseOut={(e) => {
|
||
if (!isSubmitting) {
|
||
e.currentTarget.style.backgroundColor = "var(--primary)";
|
||
}
|
||
}}
|
||
>
|
||
{isSubmitting ? "Вход..." : "Войти"}
|
||
</button>
|
||
</form>
|
||
|
||
<div
|
||
style={{
|
||
marginTop: "24px",
|
||
fontSize: "14px",
|
||
color: "var(--text-muted)",
|
||
}}
|
||
>
|
||
Вход доступен только для пользователей, созданных администратором.
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|