diff --git a/app/admin/broadcast/BroadcastForm.tsx b/app/admin/broadcast/BroadcastForm.tsx new file mode 100644 index 0000000..6953502 --- /dev/null +++ b/app/admin/broadcast/BroadcastForm.tsx @@ -0,0 +1,56 @@ +"use client"; + +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Textarea } from "@/components/ui/textarea"; +import { broadcastAll } from "./actions"; + +const BODY_LIMIT = 160; + +export default function BroadcastForm({ + hackerCount, +}: { + hackerCount: number; +}) { + const [bodyLength, setBodyLength] = useState(0); + + return ( +
+ ); +} diff --git a/app/admin/broadcast/actions.ts b/app/admin/broadcast/actions.ts new file mode 100644 index 0000000..e6e4332 --- /dev/null +++ b/app/admin/broadcast/actions.ts @@ -0,0 +1,37 @@ +"use server"; + +import { redirect } from "next/navigation"; +import { eq } from "drizzle-orm"; +import { db } from "@/lib/db"; +import { users } from "@/lib/db/schema/users"; +import { broadcastLogs } from "@/lib/db/schema/broadcasts"; +import { getSessionUser } from "@/lib/auth/session"; +import { sendBulkEmail } from "@/lib/aws/ses.placeholder"; + +export async function broadcastAll(formData: FormData) { + const sessionUser = await getSessionUser(); + if (!sessionUser || sessionUser.role !== "organizer") { + throw new Error("Unauthorized"); + } + + const subject = formData.get("subject") as string; + const body = formData.get("body") as string; + + const emailRows = await db + .select({ email: users.email }) + .from(users) + .where(eq(users.role, "hacker")); + + const emails = emailRows.map((r) => r.email).filter((e) => e.length > 0); + + const emailResults = await sendBulkEmail(emails, subject, body); + + await db.insert(broadcastLogs).values({ + subject, + body, + sentBy: sessionUser.id, + broadcastedToEmail: emailResults.succeeded, + }); + + redirect("/admin/broadcast/success"); +} diff --git a/app/admin/broadcast/logs/[id]/recipients/route.ts b/app/admin/broadcast/logs/[id]/recipients/route.ts new file mode 100644 index 0000000..39b0c24 --- /dev/null +++ b/app/admin/broadcast/logs/[id]/recipients/route.ts @@ -0,0 +1,31 @@ +import { NextResponse } from "next/server"; +import { eq } from "drizzle-orm"; +import { db } from "@/lib/db"; +import { broadcastLogs } from "@/lib/db/schema/broadcasts"; +import { getSessionUser } from "@/lib/auth/session"; + +export async function GET( + _request: Request, + { params }: { params: Promise<{ id: string }> }, +) { + const user = await getSessionUser(); + if (!user || user.role !== "organizer") { + return new NextResponse("Unauthorized", { status: 401 }); + } + + const { id } = await params; + + const [log] = await db + .select({ broadcastedToEmail: broadcastLogs.broadcastedToEmail }) + .from(broadcastLogs) + .where(eq(broadcastLogs.id, id)) + .limit(1); + + if (!log) { + return new NextResponse("Not found", { status: 404 }); + } + + return new NextResponse((log.broadcastedToEmail ?? []).join("\n"), { + headers: { "Content-Type": "text/plain; charset=utf-8" }, + }); +} diff --git a/app/admin/broadcast/logs/page.tsx b/app/admin/broadcast/logs/page.tsx new file mode 100644 index 0000000..5239829 --- /dev/null +++ b/app/admin/broadcast/logs/page.tsx @@ -0,0 +1,90 @@ +import { desc, eq } from "drizzle-orm"; +import { db } from "@/lib/db"; +import { broadcastLogs } from "@/lib/db/schema/broadcasts"; +import { users } from "@/lib/db/schema/users"; +import { Button } from "@/components/ui/button"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { AdminPageHeader } from "../../components/admin-page-header"; +import { AdminPageShell } from "../../components/admin-page-shell"; + +export default async function BroadcastLogsPage() { + const logs = await db + .select({ + id: broadcastLogs.id, + subject: broadcastLogs.subject, + body: broadcastLogs.body, + sentAt: broadcastLogs.sentAt, + broadcastedToEmail: broadcastLogs.broadcastedToEmail, + operatorEmail: users.email, + }) + .from(broadcastLogs) + .leftJoin(users, eq(broadcastLogs.sentBy, users.id)) + .orderBy(desc(broadcastLogs.sentAt)); + + return ( +