Add email notifications via Resend

- src/lib/email.ts: HTML templates for 4 email types (Second Brain design)
- Welcome email on user registration (Better Auth databaseHooks)
- Course access email when admin grants enrollment
- Homework submitted email to all admins/curators (first submission only)
- Feedback received email to student with feedback text and lesson link
- Update TECHNICAL.md: Resend domain, from-address, email vars, Stage 3 summary

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-07 14:46:46 +05:00
parent 9bc18247df
commit 6975a9f97e
6 changed files with 195 additions and 4 deletions
@@ -4,6 +4,7 @@ import { prisma } from "@/lib/prisma";
import { auth } from "@/lib/auth";
import { headers } from "next/headers";
import { revalidatePath } from "next/cache";
import { sendHomeworkSubmittedEmail } from "@/lib/email";
interface HomeworkFile {
name: string;
@@ -31,15 +32,37 @@ export async function submitHomework(
throw new Error("Работа уже проверена");
}
let submissionId: string;
if (existing) {
await prisma.homeworkSubmission.update({
const updated = await prisma.homeworkSubmission.update({
where: { id: existing.id },
data: { text, files: files as object[], submittedAt: new Date() },
});
submissionId = updated.id;
} else {
await prisma.homeworkSubmission.create({
const created = await prisma.homeworkSubmission.create({
data: { homeworkId, userId: session.user.id, text, files: files as object[] },
});
submissionId = created.id;
// Notify admins/curators on first submission only
const [lesson, admins] = await Promise.all([
prisma.homework.findUnique({
where: { id: homeworkId },
include: { lesson: { select: { title: true } } },
}),
prisma.user.findMany({
where: { role: { in: ["admin", "curator"] } },
select: { email: true, name: true },
}),
]);
if (lesson) {
await Promise.all(
admins.map((a) =>
sendHomeworkSubmittedEmail(a.email, a.name, session.user.name, lesson.lesson.title, submissionId)
)
);
}
}
revalidatePath(`/courses/${slug}/lessons/${lessonId}`);
@@ -5,6 +5,7 @@ import { headers } from "next/headers";
import { auth } from "@/lib/auth";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import { sendCourseAccessEmail } from "@/lib/email";
async function requireAdmin() {
const session = await auth.api.getSession({ headers: await headers() });
@@ -70,6 +71,16 @@ export async function grantAccess(
note: note || null,
},
});
// Send email notification
const [user, course] = await Promise.all([
prisma.user.findUnique({ where: { id: userId }, select: { email: true, name: true } }),
prisma.course.findUnique({ where: { id: courseId }, select: { title: true } }),
]);
if (user && course) {
await sendCourseAccessEmail(user.email, user.name, course.title);
}
revalidatePath(`/admin/courses/${courseId}`);
}
@@ -4,6 +4,7 @@ import { prisma } from "@/lib/prisma";
import { auth } from "@/lib/auth";
import { headers } from "next/headers";
import { revalidatePath } from "next/cache";
import { sendFeedbackReceivedEmail } from "@/lib/email";
export async function submitFeedback(submissionId: string, text: string) {
const session = await auth.api.getSession({ headers: await headers() });
@@ -15,6 +16,36 @@ export async function submitFeedback(submissionId: string, text: string) {
data: { submissionId, curatorId: session.user.id, text },
});
// Send email to student
const submission = await prisma.homeworkSubmission.findUnique({
where: { id: submissionId },
include: {
user: { select: { email: true, name: true } },
homework: {
include: {
lesson: {
select: {
title: true,
module: { select: { course: { select: { slug: true } } } },
},
},
},
},
},
});
if (submission) {
const { lesson } = submission.homework;
const lessonUrl = `${process.env.BETTER_AUTH_URL ?? "https://school.second-brain.ru"}/courses/${lesson.module.course.slug}/lessons/${submission.homework.lessonId}`;
await sendFeedbackReceivedEmail(
submission.user.email,
submission.user.name,
lesson.title,
text,
lessonUrl
);
}
revalidatePath("/curator/homework");
revalidatePath(`/curator/homework/${submissionId}`);
}