6975a9f97e
- 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>
104 lines
3.5 KiB
TypeScript
104 lines
3.5 KiB
TypeScript
"use server";
|
|
|
|
import { prisma } from "@/lib/prisma";
|
|
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() });
|
|
if (!session || session.user.role !== "admin") throw new Error("Forbidden");
|
|
return session;
|
|
}
|
|
|
|
// ── Modules ──────────────────────────────────────────────────────────────────
|
|
|
|
export async function createModule(courseId: string, formData: FormData) {
|
|
await requireAdmin();
|
|
const title = formData.get("title") as string;
|
|
const count = await prisma.module.count({ where: { courseId } });
|
|
const mod = await prisma.module.create({ data: { courseId, title, order: count } });
|
|
revalidatePath(`/admin/courses/${courseId}`);
|
|
redirect(`/admin/courses/${courseId}/modules/${mod.id}`);
|
|
}
|
|
|
|
export async function updateModule(moduleId: string, courseId: string, formData: FormData) {
|
|
await requireAdmin();
|
|
const title = formData.get("title") as string;
|
|
await prisma.module.update({ where: { id: moduleId }, data: { title } });
|
|
revalidatePath(`/admin/courses/${courseId}`);
|
|
}
|
|
|
|
export async function deleteModule(moduleId: string, courseId: string) {
|
|
await requireAdmin();
|
|
await prisma.module.delete({ where: { id: moduleId } });
|
|
revalidatePath(`/admin/courses/${courseId}`);
|
|
}
|
|
|
|
export async function reorderModules(courseId: string, orderedIds: string[]) {
|
|
await requireAdmin();
|
|
await Promise.all(
|
|
orderedIds.map((id, index) =>
|
|
prisma.module.update({ where: { id }, data: { order: index } })
|
|
)
|
|
);
|
|
revalidatePath(`/admin/courses/${courseId}`);
|
|
}
|
|
|
|
// ── Enrollment ───────────────────────────────────────────────────────────────
|
|
|
|
export async function grantAccess(
|
|
courseId: string,
|
|
userId: string,
|
|
expiresAt?: string | null,
|
|
note?: string
|
|
) {
|
|
const session = await requireAdmin();
|
|
await prisma.courseEnrollment.upsert({
|
|
where: { userId_courseId: { userId, courseId } },
|
|
update: { expiresAt: expiresAt ? new Date(expiresAt) : null },
|
|
create: { userId, courseId, expiresAt: expiresAt ? new Date(expiresAt) : null },
|
|
});
|
|
await prisma.accessLog.create({
|
|
data: {
|
|
courseId,
|
|
userId,
|
|
action: "granted",
|
|
method: "manual",
|
|
grantedById: session.user.id,
|
|
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}`);
|
|
}
|
|
|
|
export async function revokeAccess(courseId: string, userId: string, note?: string) {
|
|
const session = await requireAdmin();
|
|
await prisma.courseEnrollment.delete({
|
|
where: { userId_courseId: { userId, courseId } },
|
|
});
|
|
await prisma.accessLog.create({
|
|
data: {
|
|
courseId,
|
|
userId,
|
|
action: "revoked",
|
|
method: "manual",
|
|
grantedById: session.user.id,
|
|
note: note || null,
|
|
},
|
|
});
|
|
revalidatePath(`/admin/courses/${courseId}`);
|
|
}
|