Внутренний метод /api/internal/access-check: есть ли у почты доступ к курсу

Для бота выдачи SB Access (payment-router): старый покупатель Second Brain
Vault оставляет почту на /tg/link, ключ входа в бота выдаётся, только если
доступ к курсу действительно есть. Тот же общий секрет, что у grant;
ответ только да/нет, почта без учёта регистра, истёкший доступ не считается.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-10-13 15:03:24 +05:00
co-authored by Claude Opus 5.5
parent 0a5368577c
commit 83f34ea679
3 changed files with 94 additions and 0 deletions
+34
View File
@@ -0,0 +1,34 @@
/**
* Проверка «у этой почты есть действующий доступ к курсу» для внутренних
* вызовов (бот выдачи SB Access в payment-router, 20261013): старый покупатель
* Second Brain Vault оставляет почту, бот получает ключ входа, только если
* доступ к курсу действительно есть.
*
* Ответ — только да/нет: ни имени, ни дат наружу не отдаём.
*/
export type AccessCheckInput = { email: string; courseSlug: string };
export function parseAccessCheck(body: unknown): AccessCheckInput | null {
if (!body || typeof body !== "object") return null;
const b = body as Record<string, unknown>;
const email = typeof b.email === "string" ? b.email.trim().toLowerCase() : "";
const courseSlug = typeof b.course_slug === "string" ? b.course_slug.trim() : "";
if (!email || email.length > 254 || !email.includes("@") || !courseSlug || courseSlug.length > 100) {
return null;
}
return { email, courseSlug };
}
/** Условие Prisma: запись о доступе к курсу у пользователя с этой почтой, не истёкшая. */
export function accessWhere(input: AccessCheckInput, now: Date) {
return {
user: { email: { equals: input.email, mode: "insensitive" as const } },
course: { slug: input.courseSlug },
OR: [{ expiresAt: null }, { expiresAt: { gt: now } }],
};
}
export function isAuthorized(header: string | null, secret: string | undefined): boolean {
return Boolean(secret) && header === secret;
}