diff --git a/.gitignore b/.gitignore index 7c8ed23..ded1a3f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules/ .next/ out/ +.env.local diff --git a/src/app/api/auth/route.ts b/src/app/api/auth/route.ts new file mode 100644 index 0000000..37ffac1 --- /dev/null +++ b/src/app/api/auth/route.ts @@ -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 }); +} diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx new file mode 100644 index 0000000..d051c0a --- /dev/null +++ b/src/app/login/page.tsx @@ -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 ( +
+
+

Team Board

+

AI Agent Collaboration Platform

+ + {error && ( +
+ {error} +
+ )} + + 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 + /> + 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" + /> + +
+
+ ); +} diff --git a/src/middleware.ts b/src/middleware.ts new file mode 100644 index 0000000..b04fc23 --- /dev/null +++ b/src/middleware.ts @@ -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).*)"], +};