From 6780dcd5388d81813ef392d9dc6db423fce04d1c Mon Sep 17 00:00:00 2001 From: Mikaal Naik Date: Mon, 29 Jun 2026 10:07:14 -0400 Subject: [PATCH] Add dev open-access mode, improve bill fetch error handling - Add DEV_OPEN_ACCESS guard (NODE_ENV !== production) so admin/edit access is open locally; auth/allowlist checks are skipped in dev across the edit link, reprocess route, update route, and auth-guards. - Dev sign-in now auto-creates and allows any account instead of denying. - Point Civics Project API at https://api.civicsproject.org. - Surface detailed error context (status, body, URL) when a bill fetch to the Civics Project API fails. - Instruct the summary prompt to never mention tenets in output. Co-Authored-By: Claude Opus 4.8 --- src/app/[id]/page.tsx | 3 ++- src/app/api/[id]/reprocess/route.ts | 27 +++++++++++++++----------- src/app/api/[id]/route.ts | 28 ++++++++++++++++----------- src/env.ts | 2 +- src/lib/auth-guards.ts | 12 ++++++++++++ src/lib/auth.ts | 14 +++++++++++++- src/prompt/summary-and-vote-prompt.ts | 1 + src/services/billApi.ts | 13 ++++++++++++- 8 files changed, 74 insertions(+), 26 deletions(-) diff --git a/src/app/[id]/page.tsx b/src/app/[id]/page.tsx index cd5772f..5e7cbf6 100644 --- a/src/app/[id]/page.tsx +++ b/src/app/[id]/page.tsx @@ -20,6 +20,7 @@ import { BillQuestions } from "@/components/BillDetail/BillQuestions"; import { Separator } from "@/components/ui/separator"; import { getServerSession } from "next-auth"; import { authOptions } from "@/lib/auth"; +import { DEV_OPEN_ACCESS } from "@/lib/auth-guards"; import { BillTenets } from "@/components/BillDetail/BillTenets"; import { JudgementValue } from "@/components/Judgement/judgement.component"; import { buildAbsoluteUrl, buildRelativePath } from "@/utils/basePath"; @@ -89,7 +90,7 @@ export default async function BillDetail({ params }: Params) { ← Back to bills - {session?.user && ( + {(session?.user || DEV_OPEN_ACCESS) && ( Edit diff --git a/src/app/api/[id]/reprocess/route.ts b/src/app/api/[id]/reprocess/route.ts index 63a50ad..4255100 100644 --- a/src/app/api/[id]/reprocess/route.ts +++ b/src/app/api/[id]/reprocess/route.ts @@ -4,6 +4,7 @@ import { connectToDatabase } from "@/lib/mongoose"; import { Bill, type BillDocument } from "@/models/Bill"; import { User } from "@/models/User"; import { authOptions } from "@/lib/auth"; +import { DEV_OPEN_ACCESS } from "@/lib/auth-guards"; import { type ApiBillDetail, fetchBillMarkdown, @@ -24,21 +25,25 @@ export async function POST( _request: Request, { params }: { params: Promise<{ id: string }> }, ) { - const session = await getServerSession(authOptions); - if (!session?.user?.email) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + // DEV ONLY: open access — skip the session/allowlist checks entirely. + if (!DEV_OPEN_ACCESS) { + const session = await getServerSession(authOptions); + if (!session?.user?.email) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + // Verify user is on the allowlist; do not create + await connectToDatabase(); + const dbUser = await User.findOne({ + emailLower: session.user.email.toLowerCase(), + }); + if (!dbUser) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } } await connectToDatabase(); - // Verify user is on the allowlist; do not create - const dbUser = await User.findOne({ - emailLower: session.user.email.toLowerCase(), - }); - if (!dbUser) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } - const { id } = await params; const existing = (await Bill.findOne({ billId: id }) diff --git a/src/app/api/[id]/route.ts b/src/app/api/[id]/route.ts index e953709..f1c4f07 100644 --- a/src/app/api/[id]/route.ts +++ b/src/app/api/[id]/route.ts @@ -4,27 +4,33 @@ import { connectToDatabase } from "@/lib/mongoose"; import { Bill } from "@/models/Bill"; import { User } from "@/models/User"; import { authOptions } from "@/lib/auth"; +import { DEV_OPEN_ACCESS } from "@/lib/auth-guards"; import { BASE_PATH } from "@/utils/basePath"; export async function POST( request: Request, { params }: { params: Promise<{ id: string }> }, ) { - const session = await getServerSession(authOptions); - if (!session?.user?.email) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } + // DEV ONLY: open access — skip the session/allowlist checks entirely. + if (!DEV_OPEN_ACCESS) { + const session = await getServerSession(authOptions); + if (!session?.user?.email) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } - await connectToDatabase(); + await connectToDatabase(); - // Verify user exists in DB; do not create - const dbUser = await User.findOne({ - emailLower: session.user.email.toLowerCase(), - }); - if (!dbUser) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + // Verify user exists in DB; do not create + const dbUser = await User.findOne({ + emailLower: session.user.email.toLowerCase(), + }); + if (!dbUser) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } } + await connectToDatabase(); + const contentType = request.headers.get("content-type")?.toLowerCase() || ""; let title: string | undefined; diff --git a/src/env.ts b/src/env.ts index b12aa9d..f571e5b 100644 --- a/src/env.ts +++ b/src/env.ts @@ -14,7 +14,7 @@ function optional( return value && value.trim() !== "" ? value : undefined; } -const ENDPOINT = "https://civics-project-kiyv.vercel.app"; +const ENDPOINT = "https://api.civicsproject.org"; export const env = { NODE_ENV: process.env.NODE_ENV || "development", diff --git a/src/lib/auth-guards.ts b/src/lib/auth-guards.ts index 0f0510e..12c82c5 100644 --- a/src/lib/auth-guards.ts +++ b/src/lib/auth-guards.ts @@ -1,9 +1,16 @@ import { redirect } from "next/navigation"; import { getServerSession } from "next-auth"; import { authOptions } from "@/lib/auth"; +import { env } from "@/env"; import { connectToDatabase } from "@/lib/mongoose"; import { User } from "@/models/User"; +/** + * DEV ONLY: when true, admin/edit access is open to everyone — including users + * who are not signed in. Never enabled in production. + */ +export const DEV_OPEN_ACCESS = env.NODE_ENV !== "production"; + /** * Server-side authentication guard that requires a valid authenticated user. * Redirects to /unauthorized if: @@ -15,6 +22,11 @@ import { User } from "@/models/User"; * @throws Redirects to /unauthorized if authentication fails */ export async function requireAuthenticatedUser() { + // DEV ONLY: open access — skip the session/allowlist checks entirely. + if (DEV_OPEN_ACCESS) { + return { session: null, dbUser: null }; + } + const session = await getServerSession(authOptions); if (!session?.user?.email) { diff --git a/src/lib/auth.ts b/src/lib/auth.ts index 936a21e..eb2a2d1 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -40,10 +40,20 @@ export const authOptions: NextAuthOptions = { const now = new Date(); const existing = await User.findOne({ emailLower: email }); if (!existing) { + // DEV ONLY: auto-create + allow any signed-in account so every user + // has admin access locally. Never runs in production. if (env.NODE_ENV !== "production") { console.warn( - `[auth] User ${email} not found. No auto-creation. Denying sign-in.`, + `[auth] DEV: auto-creating + allowing ${email} (admin access on for all users).`, ); + await User.create({ + email: user?.email, + emailLower: email, + name: user?.name ?? null, + allowed: true, + lastLoginAt: now, + }); + return true; } return false; } @@ -51,6 +61,8 @@ export const authOptions: NextAuthOptions = { (existing as any).image = (user as any)?.image ?? existing.image; existing.lastLoginAt = now; await existing.save(); + // DEV ONLY: allow regardless of the allowlist flag. + if (env.NODE_ENV !== "production") return true; return !!existing.allowed; } catch (err) { if (env.NODE_ENV !== "production") { diff --git a/src/prompt/summary-and-vote-prompt.ts b/src/prompt/summary-and-vote-prompt.ts index ce92140..7a3ad9a 100644 --- a/src/prompt/summary-and-vote-prompt.ts +++ b/src/prompt/summary-and-vote-prompt.ts @@ -78,6 +78,7 @@ You are analyzing Canadian legislation. You must assess whether the bill aligns - tenet_evaluations.alignment: aligns|conflicts|neutral - final_judgment: yes|no|abstain - is_social_issue: yes|no + - Never mention the tenents in the summary, questions, or rationale. Output format (return valid JSON only): diff --git a/src/services/billApi.ts b/src/services/billApi.ts index aa3eaaf..d5763fa 100644 --- a/src/services/billApi.ts +++ b/src/services/billApi.ts @@ -61,6 +61,7 @@ export async function getBillFromCivicsProjectApi( billId: string, ): Promise { const URL = `${env.CIVICS_PROJECT_BASE_URL}/canada/bills/${CANADIAN_PARLIAMENT_NUMBER}/${billId}`; + console.log({ URL }); const response = await fetch(URL, { // Cache individual bills. ...(process.env.NODE_ENV === "production" @@ -72,7 +73,17 @@ export async function getBillFromCivicsProjectApi( }, }); if (!response.ok) { - throw new Error("Failed to fetch bill details"); + const body = await response.text().catch(() => ""); + console.error("Failed to fetch bill details from Civics Project API", { + billId, + url: URL, + status: response.status, + statusText: response.statusText, + body: body.slice(0, 1000), + }); + throw new Error( + `Failed to fetch bill details for "${billId}": ${response.status} ${response.statusText} (${URL})`, + ); } const json = await response.json(); const data = (json?.data?.bill ?? json?.data ?? json) as