prep oss release: allow development with remote api

This commit is contained in:
ajvpot
2025-08-01 00:00:00 +00:00
parent 571ee122f6
commit de9d231542
6 changed files with 105 additions and 5 deletions
+31
View File
@@ -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.
+4 -3
View File
@@ -1,6 +1,7 @@
"use client";
import { useEffect, useState } from "react";
import { buildApiUrl } from "@/lib/api";
export default function StatsPage() {
const [totalNodes, setTotalNodes] = useState<number | null>(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 ?? []);
+2 -1
View File
@@ -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) {
+2 -1
View File
@@ -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)) {
+26
View File
@@ -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}`;
}
+40
View File
@@ -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*',
};