Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,13 @@ services:
BODY_SIZE_LIMIT: Infinity
PUBLIC_POCKETBASE_URL: http://db:8090
PUBLIC_DISABLE_SIGNUP: "false"
PUBLIC_VALHALLA_ENABLED: "true"
UPLOAD_FOLDER: /app/uploads
UPLOAD_USER:
UPLOAD_PASSWORD:
PUBLIC_OVERPASS_API_URL: https://overpass-api.de
PUBLIC_VALHALLA_URL: https://valhalla1.openstreetmap.de
PUBLIC_NOMINATIM_URL: https://nominatim.openstreetmap.org
PRIVATE_OVERPASS_API_URL: https://overpass-api.de
PRIVATE_VALHALLA_URL: https://valhalla1.openstreetmap.de
PRIVATE_NOMINATIM_URL: https://nominatim.openstreetmap.org
volumes:
- ./data/uploads:/app/uploads
# - ./data/about.md:/app/build/client/md/about.md
Expand Down
7 changes: 4 additions & 3 deletions docker/docker-compose.dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,13 @@ services:
BODY_SIZE_LIMIT: Infinity
PUBLIC_POCKETBASE_URL: http://db:8090
PUBLIC_DISABLE_SIGNUP: "false"
PUBLIC_VALHALLA_ENABLED: "true"
UPLOAD_FOLDER: /app/uploads
UPLOAD_USER:
UPLOAD_PASSWORD:
PUBLIC_OVERPASS_API_URL: https://overpass-api.de
PUBLIC_VALHALLA_URL: https://valhalla1.openstreetmap.de
PUBLIC_NOMINATIM_URL: https://nominatim.openstreetmap.org
PRIVATE_OVERPASS_API_URL: https://overpass-api.de
PRIVATE_VALHALLA_URL: https://valhalla1.openstreetmap.de
PRIVATE_NOMINATIM_URL: https://nominatim.openstreetmap.org
volumes:
- uploads:/app/uploads
# - ./data/about.md:/app/build/client/md/about.md
Expand Down
7 changes: 4 additions & 3 deletions docker/docker-compose.prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,13 @@ services:
BODY_SIZE_LIMIT: Infinity
PUBLIC_POCKETBASE_URL: http://db:8090
PUBLIC_DISABLE_SIGNUP: "false"
PUBLIC_VALHALLA_ENABLED: "true"
UPLOAD_FOLDER: /app/uploads
UPLOAD_USER:
UPLOAD_PASSWORD:
PUBLIC_OVERPASS_API_URL: https://overpass-api.de
PUBLIC_VALHALLA_URL: https://valhalla1.openstreetmap.de
PUBLIC_NOMINATIM_URL: https://nominatim.openstreetmap.org
PRIVATE_OVERPASS_API_URL: https://overpass-api.de
PRIVATE_VALHALLA_URL: https://valhalla1.openstreetmap.de
PRIVATE_NOMINATIM_URL: https://nominatim.openstreetmap.org
volumes:
- uploads:/app/uploads
# - ./data/about.md:/app/build/client/md/about.md
Expand Down
2 changes: 1 addition & 1 deletion docs/src/content/docs/develop/local-development.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export ORIGIN=http://localhost:5173
export MEILI_URL=http://127.0.0.1:7700
export MEILI_MASTER_KEY=p2gYZAWODOrwTPr4AYoahCZ9CI8y9bUd0yQLGk-E3m8
export PUBLIC_POCKETBASE_URL=http://127.0.0.1:8090
export PUBLIC_VALHALLA_URL=https://valhalla1.openstreetmap.de
export PRIVATE_VALHALLA_URL=https://valhalla1.openstreetmap.de
export POCKETBASE_ENCRYPTION_KEY=9ada3c93163812101e50e2bf49e880bc

cd search && ./meilisearch --master-key $MEILI_MASTER_KEY &
Expand Down
35 changes: 32 additions & 3 deletions docs/src/content/docs/run/environment-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,39 @@ Since we use an unmodified installation of meilisearch you can use all variables
| BODY_SIZE_LIMIT | Maximum allowed upload size | Infinity |
| PUBLIC_POCKETBASE_URL | IP or hostname (including the port) of your pocketbase instance | http://db:8090 |
| PUBLIC_DISABLE_SIGNUP | Disables signup option for new users | false |
| PUBLIC_VALHALLA_URL | Public IP or hostname (including the port) of a valhalla instance | https://valhalla1.openstreetmap.de |
| PUBLIC_NOMINATIM_URL | Public IP or hostname (including the port) of a nominatim instance | https://nominatim.openstreetmap.org |
| PUBLIC_PRIVATE_INSTANCE | Setting this to true will block visitors from viewing content without an account | false |
| UPLOAD_FOLDER | Folder from which <span class="-tracking-[0.075em]">wanderer</span> auto-uploads trails | /app/uploads |
| UPLOAD_USER | Username for the account with which <span class="-tracking-[0.075em]">wanderer</span> auto-uploads trails | |
| UPLOAD_PASSWORD | Password for the account with which <span class="-tracking-[0.075em]">wanderer</span> auto-uploads trails | |
| PUBLIC_OVERPASS_API_URL | Overpass API URL used for map points of interest | https://overpass-api.de |

## Geocoding & Routing

These variables configure server-side requests to Valhalla, Nominatim and Overpass.

| Environment Variable | Description | Default |
| ------------------------ | --------------------------------------------------------------------------- | ----------------------------------- |
| PUBLIC_VALHALLA_ENABLED | Enables Valhalla route editing in the UI when set to `true` | true |
| PRIVATE_VALHALLA_URL | Valhalla API URL used for auto-routing and elevation data | https://valhalla1.openstreetmap.de |
| PRIVATE_NOMINATIM_URL | Nominatim API URL used for (reverse) geocoding | https://nominatim.openstreetmap.org |
| PRIVATE_OVERPASS_API_URL | Overpass API URL used for map points of interest | https://overpass-api.de |

When `PRIVATE_*_URL` is unset, the backend falls back to legacy `PUBLIC_*_URL`.

## Custom CA certificates

If your API endpoints use certificates signed by a private CA, add the CA bundle and set `NODE_EXTRA_CA_CERTS` for the `web` service.

| Environment Variable | Description | Default |
| -------------------- | ------------------------------------------------------------------------ | ------- |
| NODE_EXTRA_CA_CERTS | Path to an additional CA bundle used by Node.js TLS connections | |

Example (`docker-compose.yml`):

```yaml
services:
web:
environment:
NODE_EXTRA_CA_CERTS: /etc/ssl/private-ca/ca.pem
volumes:
- ./certs/ca.pem:/etc/ssl/private-ca/ca.pem:ro
```
10 changes: 7 additions & 3 deletions docs/src/content/docs/run/installation/docker.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -108,14 +108,18 @@ services:
BODY_SIZE_LIMIT: Infinity
PUBLIC_POCKETBASE_URL: http://db:8090
PUBLIC_DISABLE_SIGNUP: "false"
PUBLIC_VALHALLA_ENABLED: "true"
UPLOAD_FOLDER: /app/uploads
UPLOAD_USER:
UPLOAD_PASSWORD:
PUBLIC_OVERPASS_API_URL: https://overpass-api.de
PUBLIC_VALHALLA_URL: https://valhalla1.openstreetmap.de
PUBLIC_NOMINATIM_URL: https://nominatim.openstreetmap.org
PRIVATE_OVERPASS_API_URL: https://overpass-api.de
PRIVATE_VALHALLA_URL: https://valhalla1.openstreetmap.de
PRIVATE_NOMINATIM_URL: https://nominatim.openstreetmap.org
# Optional: trust private CAs for server-side API calls
# NODE_EXTRA_CA_CERTS: /etc/ssl/private-ca/ca.pem
volumes:
- ./data/uploads:/app/uploads
# - ./certs/ca.pem:/etc/ssl/private-ca/ca.pem:ro
# - ./data/about.md:/app/build/client/md/about.md
ports:
- "3000:3000"
Expand Down
4 changes: 3 additions & 1 deletion docs/src/content/docs/run/installation/from-source.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ export ORIGIN=http://localhost:3000
export MEILI_URL=http://127.0.0.1:7700
export MEILI_MASTER_KEY=YOU_SHOULD_DEFINITELY_CHANGE_ME
export PUBLIC_POCKETBASE_URL=http://127.0.0.1:8090
export PUBLIC_VALHALLA_URL=https://valhalla1.openstreetmap.de
export PUBLIC_VALHALLA_ENABLED=true
export PRIVATE_VALHALLA_URL=https://valhalla1.openstreetmap.de
export POCKETBASE_ENCRYPTION_KEY=YOUR_ENCRYPTION_KEY_HERE

# Optional configuration
Expand All @@ -76,6 +77,7 @@ export POCKETBASE_ENCRYPTION_KEY=YOUR_ENCRYPTION_KEY_HERE
# export UPLOAD_FOLDER=/app/uploads
# export UPLOAD_USER=
# export UPLOAD_PASSWORD=
# export NODE_EXTRA_CA_CERTS=/absolute/path/to/ca.pem

cd search && ./meilisearch --master-key $MEILI_MASTER_KEY &
cd db && ./pocketbase serve &
Expand Down
18 changes: 18 additions & 0 deletions web/src/lib/server/http.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { json } from "@sveltejs/kit";

function safeJson(text: string): any {
try {
return JSON.parse(text);
} catch {
return { message: text };
}
}

export async function proxyJsonResponse(response: Response) {
const text = await response.text();
const payload = text.length ? safeJson(text) : {};
if (!response.ok) {
return json(payload, { status: response.status });
}
return json(payload);
}
63 changes: 63 additions & 0 deletions web/src/lib/server/nominatim.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { version } from "$app/environment";
import { resolveBaseUrl } from "$lib/server/url";
import type { RequestEvent } from "@sveltejs/kit";

const NOMINATIM_RATE_LIMIT_MS = 1000;
const NOMINATIM_MAX_RETRIES = 2;
let lastNominatimCall = 0;

function getNominatimBaseUrl(): string {
return resolveBaseUrl("NOMINATIM_URL", "https://nominatim.openstreetmap.org");
}

function needsRateLimiting(baseUrl: string): boolean {
return baseUrl.includes("nominatim.openstreetmap.org");
}

const waitTimer = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));

async function nominatimRateLimiter(baseUrl: string) {
if (!needsRateLimiting(baseUrl)) {
return;
}

const elapsedTimeMs = Date.now() - lastNominatimCall;
const waitTime = NOMINATIM_RATE_LIMIT_MS - elapsedTimeMs;
if (waitTime > 0) {
await waitTimer(waitTime);
}

lastNominatimCall = Date.now();
}

export async function fetchNominatim(event: RequestEvent, path: string, params: URLSearchParams): Promise<Response> {
const baseUrl = getNominatimBaseUrl();
const base = new URL(baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`);
const cleanPath = path.replace(/^\/+/, "");
const url = new URL(cleanPath, base);
const query = params.toString();
if (query.length) {
url.search = query;
}

let attempt = 0;

while (true) {
await nominatimRateLimiter(baseUrl);

try {
return await event.fetch(url.toString(), {
method: "GET",
headers: {
"User-Agent": `wanderer/${version}`,
},
});
} catch (error) {
if (attempt < NOMINATIM_MAX_RETRIES) {
attempt++;
continue;
}
throw new Error(`Nominatim fetch failed for ${url.toString()}`, { cause: error });
}
}
}
34 changes: 34 additions & 0 deletions web/src/lib/server/overpass.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { resolveBaseUrl } from "$lib/server/url";
import type { RequestEvent } from "@sveltejs/kit";

const OVERPASS_MAX_RETRIES = 2;

function getOverpassBaseUrl(): string {
return resolveBaseUrl("OVERPASS_API_URL", "https://overpass-api.de");
}

export async function fetchOverpass(event: RequestEvent, params: URLSearchParams): Promise<Response> {
const baseUrl = getOverpassBaseUrl();
const base = new URL(baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`);
const url = new URL("api/interpreter", base);
const query = params.toString();
if (query.length) {
url.search = query;
}

let attempt = 0;

while (true) {
try {
return await event.fetch(url.toString(), {
method: "GET",
});
} catch (error) {
if (attempt < OVERPASS_MAX_RETRIES) {
attempt++;
continue;
}
throw error;
}
}
}
21 changes: 21 additions & 0 deletions web/src/lib/server/url.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { env as privateEnv } from "$env/dynamic/private";
import { env as publicEnv } from "$env/dynamic/public";

export type ExternalServiceUrlKey = "VALHALLA_URL" | "NOMINATIM_URL" | "OVERPASS_API_URL";

export function normalizeBaseUrl(url: string): string {
if (!/^https?:\/\//i.test(url)) {
return `https://${url}`;
}
return url;
}

export function resolveBaseUrl(
key: ExternalServiceUrlKey,
fallback: string = "",
): string {
const privateKey = `PRIVATE_${key}` as `PRIVATE_${string}`;
const publicKey = `PUBLIC_${key}` as `PUBLIC_${string}`;
const rawUrl = privateEnv[privateKey] ?? publicEnv[publicKey] ?? fallback;
return normalizeBaseUrl(rawUrl);
}
5 changes: 5 additions & 0 deletions web/src/lib/server/valhalla.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { resolveBaseUrl } from "$lib/server/url";

export function getValhallaBaseUrl(): string {
return resolveBaseUrl("VALHALLA_URL");
}
58 changes: 34 additions & 24 deletions web/src/lib/stores/search_store.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import { env } from "$env/dynamic/public";
import type { Actor } from "$lib/models/activitypub/actor";
import { defaultTrailSearchAttributes, type TrailSearchResult } from "$lib/models/trail";
import { APIError } from "$lib/util/api_util";
import type { Hits, MultiSearchParams, MultiSearchResponse, MultiSearchResult, SearchParams, SearchResponse } from "meilisearch";
import type { ListResult } from "pocketbase";
import { version } from "$app/environment";

export type LocationSearchResult = {
name: string;
Expand Down Expand Up @@ -104,14 +102,17 @@ export async function searchTrails(q: string, options: SearchParams): Promise<Hi
return response.hits
}

export async function searchLocations(q: string, limit?: number): Promise<Hits<LocationSearchResult>> {
const nominatimURL = env.PUBLIC_NOMINATIM_URL ?? "https://nominatim.openstreetmap.org"
const r = await fetch(`${nominatimURL}/search?q=${q}&format=geojson&addressdetails=1${limit ? '&limit=' + limit : ''}`, {
method: "GET",
headers: new Headers({
"User-Agent": "wanderer/" + version
})
});
export async function searchLocations(q: string, limit?: number, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch): Promise<Hits<LocationSearchResult>> {
if (!q.trim()) {
return [];
}

const params = new URLSearchParams({
q,
format: "geojson",
addressdetails: "1",
});
const r = await fetchGeocoding("search", params, f);
if (!r.ok) {
const response = await r.json();
throw new APIError(r.status, response.message, response.detail)
Expand All @@ -127,14 +128,20 @@ export async function searchLocations(q: string, limit?: number): Promise<Hits<L
}))
}

export async function searchLocationReverse(lat: number, lon: number) {
const nominatimURL = env.PUBLIC_NOMINATIM_URL ?? "https://nominatim.openstreetmap.org"
const r = await fetch(`${nominatimURL}/reverse?lat=${lat}&lon=${lon}&format=geojson&addressdetails=1`, {
method: "GET",
headers: new Headers({
"User-Agent": "wanderer/" + version
})
});
async function fetchGeocoding(path: string, params: URLSearchParams, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch): Promise<Response> {
const query = params.toString();
const url = query.length ? `/api/v1/geocoding/${path}?${query}` : `/api/v1/geocoding/${path}`;
return await f(url);
}

export async function searchLocationReverse(lat: number, lon: number, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch) {
const params = new URLSearchParams({
lat: String(lat),
lon: String(lon),
format: "geojson",
addressdetails: "1",
});
const r = await fetchGeocoding("reverse", params, f);
if (!r.ok) {
const response = await r.json();
throw new APIError(r.status, response.message, response.detail)
Expand Down Expand Up @@ -170,14 +177,17 @@ function getLocationDescription(address: Address) {

export async function searchMulti(options: MultiSearchParams): Promise<MultiSearchResult<any>[]> {

const locationQuery = options.queries.find(q => q.indexUid === "locations");
const locationQueryIndex = locationQuery ? options.queries.indexOf(locationQuery) : -1
if (locationQueryIndex >= 0) {
options.queries.splice(locationQueryIndex, 1)
}
const locationQueryIndex = options.queries.findIndex(q => q.indexUid === "locations");
const locationQuery = locationQueryIndex >= 0 ? options.queries[locationQueryIndex] : undefined;
const queries = locationQueryIndex >= 0
? options.queries.filter((_, index) => index !== locationQueryIndex)
: options.queries;
const r = await fetch("/api/v1/search/multi", {
method: "POST",
body: JSON.stringify(options),
body: JSON.stringify({
...options,
queries,
}),
});

if (!r.ok) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import type { OverpassResponse } from "./types";
import { env } from '$env/dynamic/public'

export class OverpassLayer implements BaseLayer {
private overpassApiURL: string = (env.PUBLIC_OVERPASS_API_URL && env.PUBLIC_OVERPASS_API_URL.length > 0 ? env.PUBLIC_OVERPASS_API_URL : "https://overpass-api.de") + "/api/interpreter";
private overpassApiURL: string = "/api/v1/overpass/interpreter";

data: GeoJSON.FeatureCollection = ({ type: 'FeatureCollection', features: [] });

Expand Down
Loading
Loading