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:
parent
a1b58ba916
commit
7187cbcf61
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,3 +1,4 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
.next/
|
.next/
|
||||||
out/
|
out/
|
||||||
|
.env.local
|
||||||
|
|||||||
29
src/app/api/auth/route.ts
Normal file
29
src/app/api/auth/route.ts
Normal 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
76
src/app/login/page.tsx
Normal 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
14
src/middleware.ts
Normal 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).*)"],
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue
Block a user