125 lines
4.7 KiB
TypeScript
125 lines
4.7 KiB
TypeScript
import { NextRequest } from "next/server";
|
|
import { z } from "zod";
|
|
import { getSession } from "../../../lib/auth/session";
|
|
import { getPrisma } from "../../../lib/db";
|
|
import { ok, fail } from "../../../lib/http";
|
|
import { getStripe } from "../../../lib/stripe";
|
|
import { loadSystemConfig } from "../../../lib/system-config";
|
|
import { sendEmail } from "../../../lib/email";
|
|
import { createWebinarCalendarEvent } from "../../../lib/calendar";
|
|
|
|
export const runtime = "nodejs";
|
|
|
|
const Body = z.object({
|
|
webinarId: z.string().uuid(),
|
|
});
|
|
|
|
export async function POST(req: NextRequest) {
|
|
const session = await getSession();
|
|
if (!session) return fail(new Error("Unauthorized"), { status: 401 });
|
|
|
|
const prisma = await getPrisma();
|
|
if (!prisma) return fail(new Error("Database not configured"), { status: 503 });
|
|
|
|
// Check if user is admin - prevent admin registration for webinars
|
|
const user = await prisma.user.findUnique({ where: { id: session.sub } });
|
|
if (user?.role === "ADMIN") {
|
|
return fail(new Error("Admins cannot register for webinars"), { status: 403 });
|
|
}
|
|
|
|
const parsed = Body.safeParse(await req.json().catch(() => ({})));
|
|
if (!parsed.success) return fail(new Error("Invalid input"));
|
|
|
|
const webinar = await prisma.webinar.findUnique({ where: { id: parsed.data.webinarId } });
|
|
if (!webinar || webinar.visibility !== "PUBLIC" || !webinar.isActive) return fail(new Error("Not found"), { status: 404 });
|
|
|
|
const count = await prisma.webinarRegistration.count({
|
|
where: { webinarId: webinar.id, status: { not: "CANCELLED" } },
|
|
});
|
|
if (count >= webinar.capacity) return fail(new Error("Webinar is full"), { status: 409 });
|
|
|
|
// Upsert registration
|
|
if (webinar.priceCents <= 0) {
|
|
const reg = await prisma.webinarRegistration.upsert({
|
|
where: { userId_webinarId: { userId: session.sub, webinarId: webinar.id } },
|
|
update: { status: "CONFIRMED" },
|
|
create: { userId: session.sub, webinarId: webinar.id, status: "CONFIRMED" },
|
|
});
|
|
|
|
// Send confirmation email with calendar invite for free webinars
|
|
const cfg = await loadSystemConfig();
|
|
if (cfg.email?.enabled && user) {
|
|
try {
|
|
const { icsContent } = await createWebinarCalendarEvent(webinar, user.email);
|
|
|
|
const meetingInfo = webinar.meetingInfo as any;
|
|
const meetingLink = meetingInfo?.meetingLink || "TBD";
|
|
const htmlContent = `
|
|
<h2>Webinar Registration Confirmed</h2>
|
|
<p>Hi ${user.firstName},</p>
|
|
<p>Thank you for registering for <strong>${webinar.title}</strong>.</p>
|
|
<p><strong>Date & Time:</strong> ${new Date(webinar.startAt).toLocaleString()}</p>
|
|
<p><strong>Duration:</strong> ${webinar.duration} minutes</p>
|
|
<p><strong>Join Link:</strong> <a href="${meetingLink}">${meetingLink}</a></p>
|
|
<p>A calendar invitation is attached to this email.</p>
|
|
<p>See you there!</p>
|
|
`;
|
|
|
|
await sendEmail({
|
|
to: user.email,
|
|
subject: `Registration Confirmed: ${webinar.title}`,
|
|
html: htmlContent,
|
|
attachments: [
|
|
{
|
|
filename: "webinar-invite.ics",
|
|
content: icsContent,
|
|
contentType: "text/calendar",
|
|
},
|
|
],
|
|
});
|
|
} catch (error) {
|
|
console.error("[REGISTRATION] Failed to send email:", error);
|
|
// Don't fail the registration if email fails
|
|
}
|
|
}
|
|
|
|
return ok({ registration: reg, next: "CONFIRMED" });
|
|
}
|
|
|
|
const cfg = await loadSystemConfig();
|
|
const stripe = await getStripe();
|
|
if (!stripe) return fail(new Error("Stripe not configured"), { status: 503 });
|
|
|
|
const baseUrl = cfg.app?.initialized ? (process.env.APP_BASE_URL || "http://localhost:3000") : (process.env.APP_BASE_URL || "http://localhost:3000");
|
|
|
|
const reg = await prisma.webinarRegistration.upsert({
|
|
where: { userId_webinarId: { userId: session.sub, webinarId: webinar.id } },
|
|
update: { status: "PAYMENT_PENDING" },
|
|
create: { userId: session.sub, webinarId: webinar.id, status: "PAYMENT_PENDING" },
|
|
});
|
|
|
|
const checkout = await stripe.checkout.sessions.create({
|
|
mode: "payment",
|
|
success_url: `${baseUrl}/webinars/${webinar.id}?payment=success`,
|
|
cancel_url: `${baseUrl}/webinars/${webinar.id}?payment=cancel`,
|
|
line_items: [
|
|
{
|
|
quantity: 1,
|
|
price_data: {
|
|
currency: "usd",
|
|
unit_amount: webinar.priceCents,
|
|
product_data: { name: webinar.title },
|
|
},
|
|
},
|
|
],
|
|
metadata: { registrationId: reg.id, userId: session.sub, webinarId: webinar.id },
|
|
});
|
|
|
|
await prisma.webinarRegistration.update({
|
|
where: { id: reg.id },
|
|
data: { stripeCheckoutSessionId: checkout.id },
|
|
});
|
|
|
|
return ok({ next: "STRIPE_CHECKOUT", url: checkout.url });
|
|
}
|