feat: simple login/password auth (cookie-based)

- Login page with form
- Middleware redirects unauthenticated to /login
- Cookie session (7 days)
- Credentials via AUTH_USER/AUTH_PASS env vars
- Default: admin/teamboard
This commit is contained in:
Markov 2026-02-15 19:03:28 +01:00
parent a1b58ba916
commit 7187cbcf61
4 changed files with 120 additions and 0 deletions

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
node_modules/ node_modules/
.next/ .next/
out/ out/
.env.local

29
src/app/api/auth/route.ts Normal file
View File

@ -0,0 +1,29 @@
import { cookies } from "next/headers";
import { NextRequest, NextResponse } from "next/server";
const AUTH_USER = process.env.AUTH_USER || "admin";
const AUTH_PASS = process.env.AUTH_PASS || "teamboard";
const COOKIE_NAME = "tb_session";
const SESSION_TOKEN = "authenticated";
export async function POST(req: NextRequest) {
const { username, password } = await req.json();
if (username === AUTH_USER && password === AUTH_PASS) {
const jar = await cookies();
jar.set(COOKIE_NAME, SESSION_TOKEN, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
maxAge: 60 * 60 * 24 * 7, // 7 days
path: "/",
});
return NextResponse.json({ ok: true });
}
return NextResponse.json({ ok: false, error: "Invalid credentials" }, { status: 401 });
}
export async function DELETE() {
const jar = await cookies();
jar.delete(COOKIE_NAME);
return NextResponse.json({ ok: true });
}

76
src/app/login/page.tsx Normal file
View File

@ -0,0 +1,76 @@
"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", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username, password }),
});
if (res.ok) {
router.push("/");
router.refresh();
} else {
setError("Неверный логин или пароль");
}
} 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>
</form>
</div>
);
}

14
src/middleware.ts Normal file
View File

@ -0,0 +1,14 @@
import { NextRequest, NextResponse } from "next/server";
export function middleware(req: NextRequest) {
const session = req.cookies.get("tb_session");
if (!session || session.value !== "authenticated") {
const loginUrl = new URL("/login", req.url);
return NextResponse.redirect(loginUrl);
}
return NextResponse.next();
}
export const config = {
matcher: ["/((?!login|api/auth|_next/static|_next/image|favicon.ico).*)"],
};