diff --git a/src/app/api/planner/start/route.ts b/src/app/api/planner/start/route.ts new file mode 100644 index 0000000..72c63c6 --- /dev/null +++ b/src/app/api/planner/start/route.ts @@ -0,0 +1,60 @@ +import { NextRequest, NextResponse } from "next/server"; +import { headers } from "next/headers"; +import { createHmac } from "crypto"; +import { auth } from "@/lib/auth"; +import { prisma } from "@/lib/prisma"; + +/** + * Вход в конструктор планнеров (planner.second-brain.ru). + * + * Своей авторизации у конструктора нет: школа проверяет сессию и доступ, + * подписывает короткоживущий токен, конструктор его сверяет тем же секретом. + * Формат токена: base64url(payload).base64url(hmacSha256(payload)) — + * зеркало packages/web/src/auth.mjs в репозитории планнера. Менять оба конца. + */ + +const PLANNER_URL = process.env.PLANNER_URL ?? "https://planner.second-brain.ru"; +const COURSE_SLUG = "planner"; +const TTL_SECONDS = 900; + +const b64 = (input: Buffer | string) => Buffer.from(input).toString("base64url"); + +export async function GET(req: NextRequest) { + const secret = process.env.PLANNER_TOKEN_SECRET; + if (!secret) { + // Лучше честная ошибка, чем редирект в конструктор с заведомо негодным + // токеном: там человек увидит невнятное «подпись не сходится». + return NextResponse.json( + { error: "PLANNER_TOKEN_SECRET не задан" }, + { status: 503 }, + ); + } + + const session = await auth.api.getSession({ headers: await headers() }); + if (!session?.user) { + return NextResponse.redirect(new URL("/login", req.url)); + } + + const enrolled = await prisma.courseEnrollment.findFirst({ + where: { userId: session.user.id, course: { slug: COURSE_SLUG } }, + select: { expiresAt: true }, + }); + if (!enrolled) { + return NextResponse.redirect(new URL("/dashboard", req.url)); + } + if (enrolled.expiresAt && enrolled.expiresAt < new Date()) { + return NextResponse.redirect(new URL("/dashboard", req.url)); + } + + const payload = { + userId: session.user.id, + email: session.user.email, + exp: Math.floor(Date.now() / 1000) + TTL_SECONDS, + }; + const body = b64(JSON.stringify(payload)); + const signature = b64(createHmac("sha256", secret).update(body).digest()); + + // Токен во фрагменте, а не в строке запроса: фрагмент не попадает ни в + // логи сервера, ни в заголовок Referer при переходах со страницы. + return NextResponse.redirect(`${PLANNER_URL}/build.html#token=${body}.${signature}`); +}