Add platform settings (Stage 9)

- Settings key-value table in Prisma with migration
- getSettings() / getSetting() helpers in lib/settings.ts
- Admin UI at /admin/settings with 6 sections: General, Notifications,
  Student profile, Legal docs, Curator permissions, Code injection
- saveSettings() server action with admin-only guard
- Maintenance mode: non-admin users redirected to /maintenance page
- schoolName propagated to page metadata and all email templates
- headCode / bodyCode injected into root layout <head> and <body>

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-08 11:18:37 +05:00
parent 093e403f5f
commit e77588deb8
14 changed files with 834 additions and 29 deletions
+21 -11
View File
@@ -1,4 +1,5 @@
import { Resend } from "resend";
import { getSetting } from "./settings";
function getResend() {
return new Resend(process.env.RESEND_API_KEY);
@@ -6,9 +7,13 @@ function getResend() {
const FROM = process.env.EMAIL_FROM ?? "noreply@mailsend.second-brain.ru";
const BASE_URL = process.env.BETTER_AUTH_URL ?? "https://school.second-brain.ru";
async function getSchoolName() {
return getSetting("schoolName");
}
// ── HTML template (inline styles for maximum email client compatibility) ───────
function base(content: string) {
function base(content: string, schoolName = "Second Brain") {
return `<!DOCTYPE html>
<html lang="ru" xmlns="http://www.w3.org/1999/xhtml">
<head>
@@ -36,7 +41,7 @@ function base(content: string) {
<!-- Header -->
<tr>
<td style="padding:20px 28px;border-bottom:2px solid #AAAAAA;background-color:#F5F5F0;">
<p style="margin:0;font-family:Arial,Helvetica,sans-serif;font-size:13px;font-weight:700;letter-spacing:0.08em;text-transform:uppercase;color:#323232;">Second Brain · Образовательная платформа</p>
<p style="margin:0;font-family:Arial,Helvetica,sans-serif;font-size:13px;font-weight:700;letter-spacing:0.08em;text-transform:uppercase;color:#323232;">${schoolName} · Образовательная платформа</p>
</td>
</tr>
@@ -97,6 +102,7 @@ function quote(text: string) {
// ── Email senders ─────────────────────────────────────────────────────────────
export async function sendCourseAccessEmail(to: string, name: string, courseTitle: string) {
const school = await getSchoolName();
await getResend().emails.send({
from: FROM,
to,
@@ -106,7 +112,7 @@ export async function sendCourseAccessEmail(to: string, name: string, courseTitl
<p ${p}>Вам открыт доступ к курсу <strong>«${courseTitle}»</strong>.</p>
<p ${pLast}>Перейдите на платформу чтобы начать обучение:</p>
${btn(`${BASE_URL}/dashboard`, "Перейти к курсам")}
`),
`, school),
}).catch((e) => console.error("[email] sendCourseAccessEmail:", e));
}
@@ -117,6 +123,7 @@ export async function sendHomeworkSubmittedEmail(
lessonTitle: string,
submissionId: string
) {
const school = await getSchoolName();
await getResend().emails.send({
from: FROM,
to,
@@ -126,7 +133,7 @@ export async function sendHomeworkSubmittedEmail(
<p ${p}>Студент <strong>${studentName}</strong> сдал работу по уроку <strong>«${lessonTitle}»</strong>.</p>
<p ${pLast}>Откройте работу чтобы проверить и оставить фидбек:</p>
${btn(`${BASE_URL}/curator/homework/${submissionId}`, "Проверить работу")}
`),
`, school),
}).catch((e) => console.error("[email] sendHomeworkSubmittedEmail:", e));
}
@@ -137,6 +144,7 @@ export async function sendFeedbackReceivedEmail(
feedbackText: string,
lessonUrl: string
) {
const school = await getSchoolName();
await getResend().emails.send({
from: FROM,
to,
@@ -147,35 +155,37 @@ export async function sendFeedbackReceivedEmail(
${quote(feedbackText)}
<p ${pLast}>Вернитесь к уроку чтобы увидеть полный фидбек:</p>
${btn(lessonUrl, "Открыть урок")}
`),
`, school),
}).catch((e) => console.error("[email] sendFeedbackReceivedEmail:", e));
}
export async function sendWelcomeEmail(to: string, name: string) {
const school = await getSchoolName();
await getResend().emails.send({
from: FROM,
to,
subject: "Добро пожаловать в Second Brain",
subject: `Добро пожаловать в ${school}`,
html: base(`
<p ${p}>Привет, ${name}!</p>
<p ${p}>Ваш аккаунт на образовательной платформе <strong>Second Brain</strong> подтверждён.</p>
<p ${p}>Ваш аккаунт на образовательной платформе <strong>${school}</strong> подтверждён.</p>
<p ${pLast}>После того как администратор откроет вам доступ к курсу, вы получите письмо и сможете начать обучение.</p>
${btn(`${BASE_URL}/dashboard`, "Перейти на платформу")}
`),
`, school),
}).catch((e) => console.error("[email] sendWelcomeEmail:", e));
}
export async function sendTestEmail(to: string) {
const school = await getSchoolName();
await getResend().emails.send({
from: FROM,
to,
subject: "Тест — Second Brain LMS",
subject: `Тест — ${school} LMS`,
html: base(`
<p ${p}>Привет!</p>
<p ${p}>Это тестовое письмо от платформы <strong>Second Brain LMS</strong>.</p>
<p ${p}>Это тестовое письмо от платформы <strong>${school} LMS</strong>.</p>
<p ${p}>Так будут выглядеть все уведомления — доступ к курсу, фидбек по ДЗ и другие.</p>
<p ${pLast}>Если письмо отображается корректно, значит email-уведомления настроены и работают.</p>
${btn(`${BASE_URL}/dashboard`, "Открыть платформу")}
`),
`, school),
}).catch((e) => console.error("[email] sendTestEmail:", e));
}