From de9d23154272ec33081c245fbbbe0f5124628db2 Mon Sep 17 00:00:00 2001 From: ajvpot <553597+ajvpot@users.noreply.github.com> Date: Fri, 1 Aug 2025 00:00:00 +0000 Subject: [PATCH] prep oss release: allow development with remote api --- README.md | 31 +++++++++++++++++++++++++++++ src/app/stats/page.tsx | 7 ++++--- src/components/ChatBox.tsx | 3 ++- src/components/MapView.tsx | 3 ++- src/lib/api.ts | 26 +++++++++++++++++++++++++ src/middleware.ts | 40 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 105 insertions(+), 5 deletions(-) create mode 100644 src/lib/api.ts create mode 100644 src/middleware.ts diff --git a/README.md b/README.md index dc7c351..f44f13b 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,37 @@ Open [http://localhost:3000](http://localhost:3000) with your browser to see the You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. +## Environment Variables + +### `NEXT_PUBLIC_API_URL` + +This environment variable allows you to override the API base URL for frontend development purposes. When set, all API calls will be made to the specified URL instead of using relative URLs. + +**Use case**: This is useful when you want to develop the frontend without direct access to the ClickHouse database, by pointing to a remote API endpoint. + +**Example**: +```bash +NEXT_PUBLIC_API_URL=https://map.w0z.is +``` + +**Important**: When this environment variable is set, the local API routes (`/api/*`) will not work. Make sure the remote API endpoint provides the same API structure and endpoints. + +**Default behavior**: If not set, the application uses relative URLs and works with the local Next.js API routes. + +### CORS Support + +The application includes middleware (`middleware.ts`) that automatically adds CORS headers to all API routes. This allows: + +- Cross-origin requests from localhost to production APIs +- Cross-protocol requests (HTTP on localhost to HTTPS in production) +- Preflight OPTIONS requests are handled automatically + +The middleware applies the following CORS headers to all `/api/*` routes: +- `Access-Control-Allow-Origin: *` +- `Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS` +- `Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With` +- `Access-Control-Allow-Credentials: true` + ## Learn More - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. diff --git a/src/app/stats/page.tsx b/src/app/stats/page.tsx index 5eb596e..f159c2b 100644 --- a/src/app/stats/page.tsx +++ b/src/app/stats/page.tsx @@ -1,6 +1,7 @@ "use client"; import { useEffect, useState } from "react"; +import { buildApiUrl } from "@/lib/api"; export default function StatsPage() { const [totalNodes, setTotalNodes] = useState(null); @@ -12,9 +13,9 @@ export default function StatsPage() { async function fetchStats() { setLoading(true); const [totalNodesRes, nodesOverTimeRes, popularChannelsRes] = await Promise.all([ - fetch("/api/stats/total-nodes").then(r => r.json()), - fetch("/api/stats/nodes-over-time").then(r => r.json()), - fetch("/api/stats/popular-channels").then(r => r.json()), + fetch(buildApiUrl("/api/stats/total-nodes")).then(r => r.json()), + fetch(buildApiUrl("/api/stats/nodes-over-time")).then(r => r.json()), + fetch(buildApiUrl("/api/stats/popular-channels")).then(r => r.json()), ]); setTotalNodes(totalNodesRes.total_nodes ?? null); setNodesOverTime(nodesOverTimeRes.data ?? []); diff --git a/src/components/ChatBox.tsx b/src/components/ChatBox.tsx index f241802..032a974 100644 --- a/src/components/ChatBox.tsx +++ b/src/components/ChatBox.tsx @@ -6,6 +6,7 @@ import { decryptMeshcoreGroupMessage } from "../lib/meshcore"; import { getChannelIdFromKey } from "../lib/meshcore"; import ChatMessageItem, { ChatMessage } from "./ChatMessageItem"; import RefreshButton from "./RefreshButton"; +import { buildApiUrl } from "../lib/api"; const PAGE_SIZE = 20; @@ -96,7 +97,7 @@ export default function ChatBox({ showAllMessagesTab = false, className = "", st url += `&before=${encodeURIComponent(before)}`; } - const res = await fetch(url); + const res = await fetch(buildApiUrl(url)); const data = await res.json(); if (Array.isArray(data)) { if (fetchNewer && data.length > 0) { diff --git a/src/components/MapView.tsx b/src/components/MapView.tsx index 601a9b2..4dbd0f0 100644 --- a/src/components/MapView.tsx +++ b/src/components/MapView.tsx @@ -11,6 +11,7 @@ import { useConfig } from "./ConfigContext"; import RefreshButton from "@/components/RefreshButton"; import { NodeMarker, ClusterMarker, PopupContent } from "./MapIcons"; import { renderToString } from "react-dom/server"; +import { buildApiUrl } from "../lib/api"; const DEFAULT = { lat: 47.6062, // Seattle @@ -155,7 +156,7 @@ export default function MapView() { if (params.length > 0) { url += `?${params.join("&")}`; } - fetch(url, { signal: controller.signal }) + fetch(buildApiUrl(url), { signal: controller.signal }) .then((res) => res.json()) .then((data) => { if (Array.isArray(data)) { diff --git a/src/lib/api.ts b/src/lib/api.ts new file mode 100644 index 0000000..08545af --- /dev/null +++ b/src/lib/api.ts @@ -0,0 +1,26 @@ +/** + * Get the base API URL for the application. + * If NEXT_PUBLIC_API_URL is set, it will be used as the base URL. + * Otherwise, relative URLs will be used (default behavior). + */ +export function getApiBaseUrl(): string { + return process.env.NEXT_PUBLIC_API_URL || ''; +} + +/** + * Build a full API URL by combining the base URL with the endpoint path. + * @param endpoint - The API endpoint path (e.g., '/api/stats/total-nodes') + * @returns The full URL to use for API calls + */ +export function buildApiUrl(endpoint: string): string { + const baseUrl = getApiBaseUrl(); + if (!baseUrl) { + return endpoint; + } + + // Ensure the base URL doesn't end with a slash and the endpoint starts with one + const cleanBaseUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl; + const cleanEndpoint = endpoint.startsWith('/') ? endpoint : `/${endpoint}`; + + return `${cleanBaseUrl}${cleanEndpoint}`; +} \ No newline at end of file diff --git a/src/middleware.ts b/src/middleware.ts new file mode 100644 index 0000000..6fdb1bd --- /dev/null +++ b/src/middleware.ts @@ -0,0 +1,40 @@ +import { NextRequest, NextResponse } from 'next/server'; + +export function middleware(request: NextRequest) { + // Only apply CORS headers to API routes + if (request.nextUrl.pathname.startsWith('/api/')) { + // Handle preflight requests + if (request.method === 'OPTIONS') { + return new NextResponse(null, { + status: 200, + headers: { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Requested-With', + 'Access-Control-Allow-Credentials': 'true', + }, + }); + } + + // For non-OPTIONS requests, get the response and add CORS headers + const response = NextResponse.next(); + + // Allow all origins for development + response.headers.set('Access-Control-Allow-Origin', '*'); + + // Allow common HTTP methods + response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); + + // Allow common headers + response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With'); + + // Allow credentials if needed + response.headers.set('Access-Control-Allow-Credentials', 'true'); + return response; + } + return NextResponse.next(); +} + +export const config = { + matcher: '/api/:path*', +}; \ No newline at end of file