web-client/src/app/login/page.tsx
Markov e655bba89b feat: JWT token auth (local + future Authentik)
- JWT via jose (HS256, 7d expiry)
- Login API: POST /api/auth/login → returns token
- Verify API: GET /api/auth/me
- Middleware checks Bearer header or cookie
- Token stored in localStorage + cookie (for SSR)
- Authentik button (disabled, placeholder)
- Auth headers auto-added to API requests
2026-02-15 19:05:37 +01:00

94 lines
3.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
export default function LoginPage() {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
const router = useRouter();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError("");
setLoading(true);
try {
const res = await fetch("/api/auth/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username, password }),
});
const data = await res.json();
if (res.ok && data.token) {
// Store token
localStorage.setItem("tb_token", data.token);
// Also set cookie for SSR middleware
document.cookie = `tb_token=${data.token}; path=/; max-age=${7 * 24 * 3600}; samesite=lax`;
router.push("/");
router.refresh();
} else {
setError(data.error || "Ошибка авторизации");
}
} catch {
setError("Ошибка соединения");
} finally {
setLoading(false);
}
};
return (
<div className="flex h-screen items-center justify-center">
<form onSubmit={handleSubmit} className="w-80">
<h1 className="text-3xl font-bold mb-1 text-center">Team Board</h1>
<p className="text-[var(--muted)] mb-6 text-center text-sm">AI Agent Collaboration Platform</p>
{error && (
<div className="mb-4 p-2 bg-red-500/10 border border-red-500/30 rounded text-red-400 text-sm text-center">
{error}
</div>
)}
<input
type="text"
placeholder="Логин"
value={username}
onChange={(e) => setUsername(e.target.value)}
className="w-full mb-3 px-4 py-2.5 bg-[var(--card)] border border-[var(--border)] rounded-lg
outline-none focus:border-[var(--accent)] text-sm"
autoFocus
/>
<input
type="password"
placeholder="Пароль"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full mb-4 px-4 py-2.5 bg-[var(--card)] border border-[var(--border)] rounded-lg
outline-none focus:border-[var(--accent)] text-sm"
/>
<button
type="submit"
disabled={loading}
className="w-full py-2.5 bg-[var(--accent)] text-white rounded-lg hover:opacity-90
transition-opacity text-sm font-medium disabled:opacity-50"
>
{loading ? "..." : "Войти"}
</button>
<div className="mt-6 text-center">
<div className="text-xs text-[var(--muted)] mb-2">или</div>
<button
type="button"
disabled
className="w-full py-2.5 bg-[var(--card)] border border-[var(--border)] text-[var(--muted)]
rounded-lg text-sm cursor-not-allowed opacity-50"
>
Войти через Authentik (скоро)
</button>
</div>
</form>
</div>
);
}