From 8e33a5497ac78894431103f84ef30062e7f7c7a1 Mon Sep 17 00:00:00 2001 From: Sean Date: Mon, 30 Mar 2026 16:46:49 -0400 Subject: [PATCH] feat: add API proxy routes for feedback and roadmap votes --- app/api/admin/feedback/[id]/route.ts | 39 ++++++++++++++++++++++++ app/api/admin/feedback/route.ts | 29 ++++++++++++++++++ app/api/feedback/route.ts | 33 ++++++++++++++++++++ app/api/roadmap/[sanityId]/vote/route.ts | 34 +++++++++++++++++++++ app/api/roadmap/votes/route.ts | 28 +++++++++++++++++ 5 files changed, 163 insertions(+) create mode 100644 app/api/admin/feedback/[id]/route.ts create mode 100644 app/api/admin/feedback/route.ts create mode 100644 app/api/feedback/route.ts create mode 100644 app/api/roadmap/[sanityId]/vote/route.ts create mode 100644 app/api/roadmap/votes/route.ts diff --git a/app/api/admin/feedback/[id]/route.ts b/app/api/admin/feedback/[id]/route.ts new file mode 100644 index 0000000..19a847d --- /dev/null +++ b/app/api/admin/feedback/[id]/route.ts @@ -0,0 +1,39 @@ +import { NextRequest, NextResponse } from 'next/server' +import { cookies } from 'next/headers' + +const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL + +export async function PATCH( + req: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + const cookieStore = await cookies() + const token = cookieStore.get('session_token')?.value + if (!token) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const { id } = await params + const body = await req.json() + + try { + const res = await fetch(`${API_BASE_URL}/api/v1/admin/feedback/${id}`, { + method: 'PATCH', + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + body: JSON.stringify(body), + cache: 'no-store', + }) + if (!res.ok) { + return NextResponse.json({ error: await res.text() }, { status: res.status }) + } + return res.status === 204 + ? new NextResponse(null, { status: 204 }) + : NextResponse.json(await res.json()) + } catch (e) { + return NextResponse.json({ error: 'Request failed' }, { status: 500 }) + } +} diff --git a/app/api/admin/feedback/route.ts b/app/api/admin/feedback/route.ts new file mode 100644 index 0000000..51e8b64 --- /dev/null +++ b/app/api/admin/feedback/route.ts @@ -0,0 +1,29 @@ +import { NextRequest, NextResponse } from 'next/server' +import { cookies } from 'next/headers' + +const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL + +export async function GET(req: NextRequest) { + const cookieStore = await cookies() + const token = cookieStore.get('session_token')?.value + if (!token) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const params = req.nextUrl.searchParams.toString() + try { + const res = await fetch( + `${API_BASE_URL}/api/v1/admin/feedback${params ? `?${params}` : ''}`, + { + headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' }, + cache: 'no-store', + } + ) + if (!res.ok) { + return NextResponse.json({ error: await res.text() }, { status: res.status }) + } + return NextResponse.json(await res.json()) + } catch (e) { + return NextResponse.json({ error: 'Request failed' }, { status: 500 }) + } +} diff --git a/app/api/feedback/route.ts b/app/api/feedback/route.ts new file mode 100644 index 0000000..0f0abef --- /dev/null +++ b/app/api/feedback/route.ts @@ -0,0 +1,33 @@ +import { NextRequest, NextResponse } from 'next/server' + +const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL + +export async function POST(req: NextRequest) { + try { + const body = await req.json() + + const ip = + req.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ?? + req.headers.get('x-real-ip') ?? + 'unknown' + const userAgent = req.headers.get('user-agent') ?? 'unknown' + + const res = await fetch(`${API_BASE_URL}/api/v1/feedback`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ ...body, ipAddress: ip, userAgent }), + cache: 'no-store', + }) + + if (!res.ok) { + const text = await res.text().catch(() => '') + console.error('[/api/feedback] Spring Boot error:', res.status, text) + return NextResponse.json({ error: 'Failed to submit' }, { status: res.status }) + } + + return NextResponse.json({ ok: true }) + } catch (e) { + console.error('[/api/feedback] proxy error:', e) + return NextResponse.json({ error: 'Invalid request' }, { status: 400 }) + } +} diff --git a/app/api/roadmap/[sanityId]/vote/route.ts b/app/api/roadmap/[sanityId]/vote/route.ts new file mode 100644 index 0000000..65c8d5d --- /dev/null +++ b/app/api/roadmap/[sanityId]/vote/route.ts @@ -0,0 +1,34 @@ +import { NextRequest, NextResponse } from 'next/server' +import { cookies } from 'next/headers' + +const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL + +export async function POST( + req: NextRequest, + { params }: { params: Promise<{ sanityId: string }> } +) { + const cookieStore = await cookies() + const token = cookieStore.get('session_token')?.value + if (!token) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const { sanityId } = await params + + try { + const res = await fetch( + `${API_BASE_URL}/api/v1/roadmap/${encodeURIComponent(sanityId)}/vote`, + { + method: 'POST', + headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' }, + cache: 'no-store', + } + ) + if (!res.ok) { + return NextResponse.json({ error: await res.text() }, { status: res.status }) + } + return NextResponse.json(await res.json()) + } catch (e) { + return NextResponse.json({ error: 'Request failed' }, { status: 500 }) + } +} diff --git a/app/api/roadmap/votes/route.ts b/app/api/roadmap/votes/route.ts new file mode 100644 index 0000000..20908ca --- /dev/null +++ b/app/api/roadmap/votes/route.ts @@ -0,0 +1,28 @@ +import { NextRequest, NextResponse } from 'next/server' +import { cookies } from 'next/headers' + +const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL + +export async function GET(req: NextRequest) { + const cookieStore = await cookies() + const token = cookieStore.get('session_token')?.value + + const headers: Record = { Accept: 'application/json' } + if (token) headers['Authorization'] = `Bearer ${token}` + + const ids = req.nextUrl.searchParams.get('ids') ?? '' + + try { + const res = await fetch( + `${API_BASE_URL}/api/v1/roadmap/votes?ids=${encodeURIComponent(ids)}`, + { headers, cache: 'no-store' } + ) + if (!res.ok) { + // Degrade gracefully — return empty map so UI shows 0 votes + return NextResponse.json({}) + } + return NextResponse.json(await res.json()) + } catch { + return NextResponse.json({}) + } +}