mirror of
https://github.com/ajvpot/meshexplorer.git
synced 2026-08-06 16:42:43 +02:00
prep oss release: allow development with remote api
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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 ?? []);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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}`;
|
||||
}
|
||||
@@ -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*',
|
||||
};
|
||||
Reference in New Issue
Block a user