feat: LiveServer-M1 v1 — educational video conferencing platform
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>
This commit is contained in:
71
src/middleware.ts
Normal file
71
src/middleware.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
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).*)"],
|
||||
};
|
||||
Reference in New Issue
Block a user