Full MVP implementation: - Next.js 16 + React 19 + TypeScript + Tailwind CSS v4 - LiveKit integration (rooms, tokens, webhooks, moderation) - better-auth (email/password, sessions, roles) - Prisma 7 + PostgreSQL (9 models, 3 enums) - 15 API routes (auth, rooms, lobby, chat, files, moderation, hand-raise) - 7 pages (landing, auth, dashboard, join, video room) - SSE-based waiting room with host approval flow - Security: PIN rate limiting, session fingerprint bans, chat/files auth - Python AI Agent (Deepgram STT + OpenAI summarization) - Docker Compose (local + production with Traefik + Let's Encrypt) - Interactive setup script (setup.sh) - Dev protection middleware (DEV_ACCESS_KEY, ALLOWED_IPS) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
72 lines
2.2 KiB
TypeScript
72 lines
2.2 KiB
TypeScript
import { NextRequest, NextResponse } from "next/server";
|
||
|
||
/**
|
||
* Защита для локальной разработки.
|
||
* Если задана env ALLOWED_IPS — пропускает только эти IP.
|
||
* Если задана env DEV_ACCESS_KEY — требует ?key=... или cookie dev_access_key.
|
||
* В проде (DOMAIN задан) — middleware пропускает всё.
|
||
*/
|
||
export function middleware(req: NextRequest) {
|
||
// В проде — пропускаем
|
||
if (process.env.DOMAIN) {
|
||
return NextResponse.next();
|
||
}
|
||
|
||
const devAccessKey = process.env.DEV_ACCESS_KEY;
|
||
const allowedIps = process.env.ALLOWED_IPS; // "192.168.1.10,192.168.1.11"
|
||
|
||
// Проверка IP whitelist
|
||
if (allowedIps) {
|
||
const clientIp =
|
||
req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ||
|
||
req.headers.get("x-real-ip") ||
|
||
"unknown";
|
||
|
||
const allowed = allowedIps.split(",").map((ip) => ip.trim());
|
||
|
||
// localhost всегда разрешён
|
||
if (
|
||
!allowed.includes(clientIp) &&
|
||
clientIp !== "127.0.0.1" &&
|
||
clientIp !== "::1" &&
|
||
clientIp !== "unknown"
|
||
) {
|
||
return new NextResponse("Forbidden", { status: 403 });
|
||
}
|
||
}
|
||
|
||
// Проверка access key
|
||
if (devAccessKey) {
|
||
const url = new URL(req.url);
|
||
const keyParam = url.searchParams.get("key");
|
||
const keyCookie = req.cookies.get("dev_access_key")?.value;
|
||
|
||
// Если ключ в query — ставим cookie и редиректим без key
|
||
if (keyParam === devAccessKey) {
|
||
url.searchParams.delete("key");
|
||
const res = NextResponse.redirect(url);
|
||
res.cookies.set("dev_access_key", devAccessKey, {
|
||
httpOnly: true,
|
||
maxAge: 60 * 60 * 24 * 7, // 7 дней
|
||
path: "/",
|
||
});
|
||
return res;
|
||
}
|
||
|
||
// Если нет валидной cookie — блокируем
|
||
if (keyCookie !== devAccessKey) {
|
||
return new NextResponse(
|
||
"Access denied. Add ?key=YOUR_DEV_ACCESS_KEY to URL.",
|
||
{ status: 403 }
|
||
);
|
||
}
|
||
}
|
||
|
||
return NextResponse.next();
|
||
}
|
||
|
||
export const config = {
|
||
// Защищаем всё кроме статики и API вебхуков
|
||
matcher: ["/((?!_next/static|_next/image|favicon.ico|api/livekit/webhook).*)"],
|
||
};
|