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:
2026-03-22 13:57:53 +03:00
parent e5bb4c99ec
commit 3846e3e00d
59 changed files with 12792 additions and 240 deletions

71
src/middleware.ts Normal file
View 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).*)"],
};