From dcb97e13d8da92c9ef903d4e6af3a1e18ead33bd Mon Sep 17 00:00:00 2001 From: sanjanamanivannan Date: Mon, 2 Mar 2026 17:35:33 -0800 Subject: [PATCH 1/3] BSL-38: add public events list page --- app/api/events/route.ts | 5 ++++ app/events/page.tsx | 59 +++++++++++++++++++++++++++++++++++++---- 2 files changed, 59 insertions(+), 5 deletions(-) diff --git a/app/api/events/route.ts b/app/api/events/route.ts index c8bf166..47cbf06 100644 --- a/app/api/events/route.ts +++ b/app/api/events/route.ts @@ -3,7 +3,12 @@ import { NextResponse } from "next/server"; import { prisma } from "@/lib/prisma"; export async function GET() { + const now = new Date(); + const events = await prisma.event.findMany({ + where: { + startAt: { gte: now }, + }, orderBy: { startAt: "asc" }, }); diff --git a/app/events/page.tsx b/app/events/page.tsx index 14cc3ff..8557559 100644 --- a/app/events/page.tsx +++ b/app/events/page.tsx @@ -1,12 +1,61 @@ -import React from "react"; -import PublicLayout from '@/components/layout/PublicLayout'; +"use client"; + +import { useEffect, useState } from "react"; +import PublicLayout from "@/components/layout/PublicLayout"; + +type Event = { + id: string; + title?: string | null; + name?: string | null; + startAt: string; // ISO string coming from JSON + location?: string | null; +}; export default function EventsPage() { + const [events, setEvents] = useState(null); + + useEffect(() => { + async function load() { + const res = await fetch("/api/events"); + if (!res.ok) { + setEvents([]); + return; + } + const data = (await res.json()) as Event[]; + setEvents(data); + } + load(); + }, []); + + const items = events ?? []; + return ( -
-

Events

-

No events yet — this is a placeholder page so the navbar link doesn't 404.

+
+

Events

+

Upcoming events

+ + {events === null ? ( +
Loading…
+ ) : items.length === 0 ? ( +
+ No upcoming events right now. Check back soon. +
+ ) : ( +
    + {items.map((e) => ( +
  • +
    + {e.title ?? e.name ?? "Untitled event"} +
    +
    + {new Date(e.startAt).toLocaleString()} + {e.location ? ` • ${e.location}` : ""} +
    +
  • + ))} +
+ )}
); From 527919fbff795da2ed1d71e7455327717f8fe90d Mon Sep 17 00:00:00 2001 From: sanjanamanivannan Date: Tue, 3 Mar 2026 15:55:56 -0800 Subject: [PATCH 2/3] BSL-39: admin events API --- app/api/admin/events/route.ts | 57 ++++++++++++++++++++++++++++++++--- 1 file changed, 52 insertions(+), 5 deletions(-) diff --git a/app/api/admin/events/route.ts b/app/api/admin/events/route.ts index 71c81e7..86ee30d 100644 --- a/app/api/admin/events/route.ts +++ b/app/api/admin/events/route.ts @@ -1,21 +1,62 @@ import { NextResponse } from "next/server"; import { prisma } from "@/lib/prisma"; +import { auth } from "@/auth"; +function isAmbassadorOrHigher(role: string) { + return role === "AMBASSADOR" || role === "SUPER_ADMIN"; +} + +async function requireAmbassadorOrHigher() { + const session = await auth(); + const email = session?.user?.email; + + if (!email) { + return { ok: false as const, status: 401 as const, error: "Unauthorized" }; + } + + const user = await prisma.user.findUnique({ + where: { email }, + select: { id: true, role: true }, + }); + + if (!user) { + return { ok: false as const, status: 401 as const, error: "Unauthorized" }; + } + + if (!isAmbassadorOrHigher(user.role)) { + return { ok: false as const, status: 403 as const, error: "Forbidden" }; + } + + return { ok: true as const, user }; +} + +// GET /api/admin/events (admin list) export async function GET() { - // TODO: require admin (RBAC) + const gate = await requireAmbassadorOrHigher(); + if (!gate.ok) { + return NextResponse.json({ error: gate.error }, { status: gate.status }); + } + const events = await prisma.event.findMany({ orderBy: { startAt: "asc" }, }); + return NextResponse.json(events); } +// POST /api/admin/events (create) export async function POST(req: Request) { - // TODO: require admin (RBAC) + const gate = await requireAmbassadorOrHigher(); + if (!gate.ok) { + return NextResponse.json({ error: gate.error }, { status: gate.status }); + } + const body = await req.json(); - if (!body.title || !body.startAt || !body.createdByUserId) { + // createdByUserId should NOT come from client + if (!body.title || !body.startAt) { return NextResponse.json( - { error: "title, startAt, createdByUserId are required" }, + { error: "title and startAt are required" }, { status: 400 } ); } @@ -23,6 +64,12 @@ export async function POST(req: Request) { const startAt = new Date(body.startAt); const endAt = body.endAt ? new Date(body.endAt) : null; + if (isNaN(startAt.getTime())) { + return NextResponse.json({ error: "Invalid startAt" }, { status: 400 }); + } + if (endAt && isNaN(endAt.getTime())) { + return NextResponse.json({ error: "Invalid endAt" }, { status: 400 }); + } if (endAt && endAt < startAt) { return NextResponse.json( { error: "endAt cannot be before startAt" }, @@ -38,7 +85,7 @@ export async function POST(req: Request) { endAt, location: body.location ?? null, link: body.link ?? null, - createdByUserId: body.createdByUserId, // later: get from session + createdByUserId: gate.user.id, // imported from db }, }); From db5a5d74999957df3bb0f4b0425132e64ece5bb5 Mon Sep 17 00:00:00 2001 From: sanjanamanivannan Date: Tue, 3 Mar 2026 16:02:04 -0800 Subject: [PATCH 3/3] BSL-39: admin events API --- app/api/admin/events/route.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/api/admin/events/route.ts b/app/api/admin/events/route.ts index 86ee30d..8ffe734 100644 --- a/app/api/admin/events/route.ts +++ b/app/api/admin/events/route.ts @@ -53,7 +53,7 @@ export async function POST(req: Request) { const body = await req.json(); - // createdByUserId should NOT come from client + if (!body.title || !body.startAt) { return NextResponse.json( { error: "title and startAt are required" },