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:
@@ -0,0 +1,73 @@
|
||||
import { prisma } from "./prisma";
|
||||
|
||||
// ── Defaults ──────────────────────────────────────────────────────────────────
|
||||
|
||||
export const SETTINGS_DEFAULTS = {
|
||||
// Basic
|
||||
schoolName: "Second Brain",
|
||||
schoolDescription: "Образовательная платформа Second Brain",
|
||||
schoolKeywords: "",
|
||||
maintenanceMode: "false",
|
||||
registrationEnabled: "true",
|
||||
|
||||
// Notifications
|
||||
notificationEmails: "", // newline-separated list
|
||||
notifyOnHomework: "true",
|
||||
notifyOnRegistration: "true",
|
||||
notifyStudentOnFeedback: "true",
|
||||
|
||||
// Student profile
|
||||
requireEmailVerification: "true",
|
||||
lastNameField: "optional", // required | optional | hidden
|
||||
phoneField: "hidden", // required | optional | hidden
|
||||
|
||||
// Legal
|
||||
privacyPolicyUrl: "",
|
||||
termsUrl: "",
|
||||
offerUrl: "",
|
||||
showTermsCheckbox: "false",
|
||||
orgRequisites: "",
|
||||
|
||||
// Curator permissions
|
||||
curatorHomeworkScope: "all", // all | assigned
|
||||
curatorCanAnswerQuestions: "true",
|
||||
curatorCanSeeStudents: "true",
|
||||
|
||||
// Code injection
|
||||
headCode: "",
|
||||
bodyCode: "",
|
||||
} as const;
|
||||
|
||||
export type SettingsKey = keyof typeof SETTINGS_DEFAULTS;
|
||||
export type Settings = Record<SettingsKey, string>;
|
||||
|
||||
// ── Getters ───────────────────────────────────────────────────────────────────
|
||||
|
||||
export async function getSettings(): Promise<Settings> {
|
||||
const rows = await prisma.settings.findMany();
|
||||
const stored: Record<string, string> = {};
|
||||
for (const row of rows) stored[row.key] = row.value;
|
||||
return Object.fromEntries(
|
||||
Object.entries(SETTINGS_DEFAULTS).map(([k, v]) => [k, stored[k] ?? v])
|
||||
) as Settings;
|
||||
}
|
||||
|
||||
export async function getSetting(key: SettingsKey): Promise<string> {
|
||||
const row = await prisma.settings.findUnique({ where: { key } });
|
||||
return row?.value ?? SETTINGS_DEFAULTS[key];
|
||||
}
|
||||
|
||||
// ── Convenience helpers ───────────────────────────────────────────────────────
|
||||
|
||||
/** Parse a boolean setting ("true" / "false") */
|
||||
export function asBool(value: string): boolean {
|
||||
return value === "true";
|
||||
}
|
||||
|
||||
/** Parse notification emails (newline-separated string → array) */
|
||||
export function parseNotificationEmails(value: string): string[] {
|
||||
return value
|
||||
.split("\n")
|
||||
.map((e) => e.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
Reference in New Issue
Block a user