Allow specifying a custom app name, calculate base URL for opensearch.xml

pull/256/head
Candid Dauth 2024-02-17 04:57:21 +01:00
rodzic 485aa98e2b
commit aa4b82a69f
21 zmienionych plików z 114 dodań i 31 usunięć

Wyświetl plik

@ -1,6 +1,15 @@
# HTTP requests made by the backend will send this User-Agent header. Please adapt to your URL and e-mail address.
#USER_AGENT=FacilMap (https://facilmap.org/, cdauth@cdauth.eu)
# Whether to trust the X-Forwarded-* headers. Can be "true" or a comma-separated list of IP subnets.
# See https://expressjs.com/en/guide/behind-proxies.html for details.
#TRUST_PROXY=true
# Alternatively, manually set the base URL where FacilMap will be publicly reachable.
#BASE_URL=https://facilmap.org/
# The name of the app that should be displayed throughout the UI.
#APP_NAME=FacilMap
# On which IP the HTTP server will listen. Leave empty to listen to all IPs.
#HOST=
# On which port the HTTP server will listen.

Wyświetl plik

@ -5,6 +5,9 @@ The config of the FacilMap server can be set either by using environment variabl
| Variable | Required | Default | Meaning |
|-----------------------|----------|-------------|----------------------------------------------------------------------------------------------------------------------------------|
| `USER_AGENT` | * | | Will be used for all HTTP requests (search, routing, GPX/KML/OSM/GeoJSON files). You better provide your e-mail address in here. |
| `APP_NAME` | | | If specified, will replace “FacilMap” as the name of the app throughout the UI. |
| `TRUST_PROXY` | | | Whether to trust the X-Forwarded-* headers. Can be `true` or a comma-separated list of IP subnets (see the [express documentation](https://expressjs.com/en/guide/behind-proxies.html)). Currently only used to calculate the base URL for the `opensearch.xml` file. |
| `BASE_URL` | | | If `TRUST_PROXY` does not work for your particular setup, you can manually specify the base URL where FacilMap can be publicly reached here. |
| `HOST` | | | The ip address to listen on (leave empty to listen on all addresses) |
| `PORT` | | `8080` | The port to listen on. |
| `DB_TYPE` | | `mysql` | The type of database. Either `mysql`, `postgres`, `mariadb`, `sqlite`, or `mssql`. |
@ -18,8 +21,10 @@ The config of the FacilMap server can be set either by using environment variabl
| `MAPZEN_TOKEN` | | | [Mapzen API key](https://mapzen.com/developers/sign_up). |
| `MAXMIND_USER_ID` | | | [MaxMind user ID](https://www.maxmind.com/en/geolite2/signup). |
| `MAXMIND_LICENSE_KEY` | | | MaxMind license key. |
| `LIMA_LABS_TOKEN` | | | [Lima Labs](https://maps.lima-labs.com/) API key
FacilMap makes use of several third-party services that require you to register (for free) and generate an API key:
* Mapbox and OpenRouteService are used for calculating routes. Mapbox is used for basic routes, OpenRouteService is used when custom route mode settings are made. If these API keys are not defined, calculating routes will fail.
* Maxmind provides a free database that maps IP addresses to approximate locations. FacilMap downloads this database to decide the initial map view for users (IP addresses are looked up in FacilMaps copy of the database, on IP addresses are sent to Maxmind). This API key is optional, if it is not set, the default view will be the whole world.
* Mapzen is used to look up the elevation info for search results. The API key is optional, if it is not set, no elevation info will be available for search results.
* Mapzen is used to look up the elevation info for search results. The API key is optional, if it is not set, no elevation info will be available for search results.
* Lima Labs is used for nicer and higher resolution map tiles than Mapnik. The API key is optional, if it is not set, Mapnik will be the default map style instead.

Wyświetl plik

@ -23,6 +23,7 @@ services:
- db
environment:
USER_AGENT: My FacilMap (https://facilmap.example.org/, facilmap@example.org)
TRUST_PROXY: "true"
DB_TYPE: mysql
DB_HOST: db
DB_NAME: facilmap
@ -59,6 +60,7 @@ services:
- db
environment:
USER_AGENT: My FacilMap (https://facilmap.example.org/, facilmap@example.org)
TRUST_PROXY: "true"
DB_TYPE: postgres
DB_HOST: db
DB_NAME: facilmap
@ -88,5 +90,5 @@ To manually create the necessary docker containers, use these commands:
```bash
docker create --name=facilmap_db -e MYSQL_DATABASE=facilmap -e MYSQL_USER=facilmap -e MYSQL_PASSWORD=password -e MYSQL_RANDOM_ROOT_PASSWORD=true --restart=unless-stopped mariadb --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
docker create --link=facilmap_db -p 8080 --name=facilmap -e "USER_AGENT=My FacilMap (https://facilmap.example.org/, facilmap@example.org)" -e DB_TYPE=mysql -e DB_HOST=facilmap_db -e DB_NAME=facilmap -e DB_USER=facilmap -e DB_PASSWORD=facilmap -e ORS_TOKEN= -e MAPBOX_TOKEN= -e MAPZEN_TOKEN= -e MAXMIND_USER_ID= -e MAXMIND_LICENSE_KEY= -e LIMA_LABS_TOKEN= --restart=unless-stopped facilmap/facilmap
docker create --link=facilmap_db -p 8080 --name=facilmap -e "USER_AGENT=My FacilMap (https://facilmap.example.org/, facilmap@example.org)" -e TRUST_PROXY=true -e DB_TYPE=mysql -e DB_HOST=facilmap_db -e DB_NAME=facilmap -e DB_USER=facilmap -e DB_PASSWORD=facilmap -e ORS_TOKEN= -e MAPBOX_TOKEN= -e MAPZEN_TOKEN= -e MAXMIND_USER_ID= -e MAXMIND_LICENSE_KEY= -e LIMA_LABS_TOKEN= --restart=unless-stopped facilmap/facilmap
```

4
frontend/build.d.ts vendored
Wyświetl plik

@ -8,7 +8,9 @@ export const paths: {
mapEjs: string;
tableEntry: string;
tableEjs: string;
manifest: string;
viteManifest: string;
pwaManifest: string;
opensearchXmlEjs: string;
};
export function serve(inlineConfig?: InlineConfig): Promise<ViteDevServer>;

Wyświetl plik

@ -13,7 +13,9 @@ export const paths = {
mapEjs: `${root}/src/map/map.ejs`,
tableEntry: "src/table/table.ts",
tableEjs: `${root}/src/table/table.ejs`,
manifest: `${dist}/.vite/manifest.json`,
viteManifest: `${dist}/.vite/manifest.json`,
pwaManifest: `${root}/src/manifest.json`,
opensearchXmlEjs: `${root}/src/opensearch.xml.ejs`,
};
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types

Wyświetl plik

@ -36,10 +36,13 @@
</script>
<script setup lang="ts">
const props = defineProps<{
const props = withDefaults(defineProps<{
baseUrl: string;
settings?: Partial<FacilMapSettings>
}>();
appName?: string;
settings?: Partial<FacilMapSettings>;
}>(), {
appName: "FacilMap"
});
const isNarrow = useMaxBreakpoint("sm");
@ -62,6 +65,7 @@
const context: FacilMapContext = shallowReadonly(reactive({
id: idCounter++,
baseUrl: toRef(() => props.baseUrl),
appName: toRef(() => props.appName),
isNarrow,
settings: readonly(toRef(() => ({
toolbox: true,

Wyświetl plik

@ -30,6 +30,7 @@ export interface FacilMapComponents {
export interface WritableFacilMapContext {
id: number;
baseUrl: string;
appName: string;
isNarrow: boolean;
settings: FacilMapSettings;
components: FacilMapComponents;

Wyświetl plik

@ -21,6 +21,7 @@
baseUrl: string;
serverUrl: string;
padId: string | undefined;
appName?: string;
settings?: Partial<FacilMapSettings>;
}>();
@ -61,6 +62,7 @@
<div class="fm-facilmap">
<FacilMapContextProvider
:baseUrl="props.baseUrl"
:appName="props.appName"
:settings="props.settings"
ref="contextRef"
>

Wyświetl plik

@ -57,7 +57,7 @@
:href="selfUrl"
target="_blank"
class="fm-open-external"
v-tooltip.right="'Open FacilMap in full size'"
v-tooltip.right="`Open ${context.appName} in full size`"
></a>
<div class="fm-logo">
<img src="./logo.png"/>

Wyświetl plik

@ -76,7 +76,7 @@
function copyEmbedCode(): void {
copyToClipboard(embedCode.value);
toasts.showToast(undefined, "Embed code copied", "The code to embed FacilMap was copied to the clipboard.", { variant: "success", autoHide: true });
toasts.showToast(undefined, "Embed code copied", `The code to embed ${context.appName} was copied to the clipboard.`, { variant: "success", autoHide: true });
}
</script>
@ -185,7 +185,7 @@
<textarea class="form-control" :value="embedCode" readonly></textarea>
<button type="button" class="btn btn-secondary" @click="copyEmbedCode()">Copy</button>
</div>
<p class="mt-2">Add this HTML code to a web page to embed FacilMap. <a href="https://docs.facilmap.org/developers/embed.html" target="_blank">Learn more</a></p>
<p class="mt-2">Add this HTML code to a web page to embed {{context.appName}}. <a href="https://docs.facilmap.org/developers/embed.html" target="_blank">Learn more</a></p>
</template>
</ModalDialog>
</template>

Wyświetl plik

@ -2,6 +2,9 @@
import AboutDialog from "../about-dialog.vue";
import { ref } from "vue";
import DropdownMenu from "../ui/dropdown-menu.vue";
import { injectContextRequired } from "../facil-map-context-provider/facil-map-context-provider.vue";
const context = injectContextRequired();
const emit = defineEmits<{
"hide-sidebar": [];
@ -63,7 +66,7 @@
@click="dialog = 'about'; emit('hide-sidebar')"
href="javascript:"
draggable="false"
>About FacilMap</a>
>About {{context.appName}}</a>
</li>
</DropdownMenu>

Wyświetl plik

@ -1,13 +1,13 @@
{
"name": "FacilMap",
"short_name": "FacilMap",
"name": "%APP_NAME%",
"short_name": "%APP_NAME%",
"icons": [{
"src": "./app-512.png",
"src": "./static/app-512.png",
"sizes": "512x512",
"type": "image/png"
}],
"background_color": "#ffffff",
"theme_color": "#ffffff",
"display": "standalone",
"start_url": "../../"
"start_url": "../"
}

Wyświetl plik

@ -2,7 +2,7 @@
<html>
<head>
<meta charset="utf-8">
<title><%= padData && padData.name ? `${padData.name} – ` : ''%>FacilMap</title>
<title><%= padData && padData.name ? `${padData.name} – ` : ''%><%=appName%></title>
<meta name="description" content="<%= padData && padData.description || "A fully-featured OpenStreetMap-based map where markers and lines can be added with live collaboration." %>" />
<% if(!padData || (isReadOnly && padData.searchEngines)) { -%>
<meta name="robots" content="index,nofollow" />
@ -14,8 +14,8 @@
<link rel="icon" href="<%=paths.base%>static/favicon.svg">
<link rel="mask-icon" href="<%=paths.base%>static/favicon.svg" color="#00272a">
<link rel="apple-touch-icon" href="<%=paths.base%>static/app-180.png">
<link rel="manifest" href="<%=paths.base%>static/manifest.json">
<link rel="search" type="application/opensearchdescription+xml" title="FacilMap" href="<%=paths.base%>static/opensearch.xml">
<link rel="manifest" href="<%=paths.base%>manifest.json">
<link rel="search" type="application/opensearchdescription+xml" title="<%=appName%>" href="<%=paths.base%>opensearch.xml">
<style type="text/css">
html, body, #app {
margin: 0;
@ -73,7 +73,7 @@
<% } -%>
</head>
<body>
<noscript><p><strong>FacilMap requires JavaScript to work.</strong></p></noscript>
<noscript><p><strong><%=appName%> requires JavaScript to work.</strong></p></noscript>
<div id="loading">
Loading...
<div id="spinner"></div>
@ -88,7 +88,7 @@
var loading = document.getElementById("loading");
if(loading) {
loading.className += " error";
loading.innerHTML = "Could not load FacilMap!";
loading.innerHTML = "Could not load <%=appName%>!";
}
};
})();

Wyświetl plik

@ -59,7 +59,7 @@ const Root = defineComponent({
});
watch(padName, () => {
const title = padName.value != null ? `${normalizePadName(padName.value)}FacilMap` : 'FacilMap';
const title = padName.value != null ? `${normalizePadName(padName.value)}${config.appName}` : config.appName;
// We have to call history.replaceState() in order for the new title to end up in the browser history
window.history && history.replaceState({ }, title);
@ -70,6 +70,7 @@ const Root = defineComponent({
baseUrl,
serverUrl: baseUrl,
padId: padId.value,
appName: config.appName,
settings: {
toolbox: toBoolean(queryParams.toolbox, true),
search: toBoolean(queryParams.search, true),

Wyświetl plik

@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/" xmlns:moz="http://www.mozilla.org/2006/browser/search/">
<ShortName>FacilMap</ShortName>
<ShortName><%=appName%></ShortName>
<Description>A privacy-friendly, open-source versatile online map that combines different services based on OpenStreetMap and makes it easy to find places, plan routes and create custom maps full of markers, lines and routes.</Description>
<Image width="32" height="32" type="image/x-icon">./favicon.ico</Image>
<Image width="64" height="64" type="image/png">./favicon-64.png</Image>
<Url type="text/html" template="https://facilmap.org/#q={searchTerms}" />
<moz:SearchForm>https://facilmap.org/</moz:SearchForm>
<Url type="text/html" template="<%=baseUrl%>#q={searchTerms}" />
<moz:SearchForm><%=baseUrl%></moz:SearchForm>
</OpenSearchDescription>

Wyświetl plik

@ -2,7 +2,7 @@
<html>
<head>
<meta charset="utf-8">
<title><%=normalizePadName(padData.name)%> – FacilMap</title>
<title><%=normalizePadName(padData.name)%> – <%=appName%></title>
<base href="../" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<%
@ -36,7 +36,7 @@
</head>
<body>
<div class="container-fluid">
<h1><%=normalizePadName(padData.name)%> – FacilMap</h1>
<h1><%=normalizePadName(padData.name)%> – <%=appName%></h1>
<%
for(let type of Object.values(types)) {
-%>

Wyświetl plik

@ -11,7 +11,10 @@ export interface DbConfig {
}
export interface Config {
appName: string;
userAgent: string;
trustProxy?: boolean | string | string[] | number | ((ip: string) => boolean);
baseUrl?: string;
host?: string;
port: number;
db: DbConfig;
@ -24,7 +27,15 @@ export interface Config {
}
const config: Config = {
appName: process.env.APP_NAME || "FacilMap",
userAgent: process.env.USER_AGENT || 'FacilMap',
trustProxy: (
!process.env.TRUST_PROXY ? undefined :
process.env.TRUST_PROXY === "true" ? true :
process.env.TRUST_PROXY.match(/^\d+$/) ? Number(process.env.TRUST_PROXY) :
process.env.TRUST_PROXY
),
baseUrl: process.env.BASE_URL ? (process.env.BASE_URL.endsWith("/") ? process.env.BASE_URL : `${process.env.BASE_URL}/`) : undefined,
host: process.env.HOST || undefined,
port: process.env.PORT ? Number(process.env.PORT) : 8080,
db : {

Wyświetl plik

@ -8,11 +8,12 @@ import { Router, type RequestHandler } from "express";
import { static as expressStatic } from "express";
import { normalizeLineName, normalizeMarkerName, normalizePadName, type InjectedConfig } from "facilmap-utils";
import config from "./config";
import { asyncIteratorToArray, jsonStream } from "./utils/streams";
export const isDevMode = !!process.env.FM_DEV;
async function getManifest(): Promise<Manifest> {
const manifest = await readFile(paths.manifest);
async function getViteManifest(): Promise<Manifest> {
const manifest = await readFile(paths.viteManifest);
return JSON.parse(manifest.toString());
}
@ -35,7 +36,7 @@ async function getScripts(entry: "mapEntry" | "tableEntry"): Promise<Scripts> {
styles: []
};
} else {
const manifest = await getManifest();
const manifest = await getViteManifest();
let referencedChunks = [paths[entry]];
for (let i = 0; i < referencedChunks.length; i++) {
@ -73,7 +74,9 @@ export async function renderMap(params: RenderMapParams): Promise<string> {
]);
return ejs.render(template, {
appName: config.appName,
config: {
appName: config.appName,
limaLabsToken: config.limaLabsToken
} satisfies InjectedConfig,
...injections,
@ -94,6 +97,7 @@ export async function renderTable(params: {
return ejs.render(template, {
...injections,
appName: config.appName,
paths,
utils,
normalizeMarkerName,
@ -122,4 +126,21 @@ export async function getStaticFrontendMiddleware(): Promise<RequestHandler> {
router.use(paths.base, expressStatic(paths.dist));
return router;
}
}
export async function getPwaManifest(): Promise<string> {
const template = await readFile(paths.pwaManifest).then((t) => t.toString());
const chunks = await asyncIteratorToArray(jsonStream(JSON.parse(template), {
APP_NAME: config.appName
}));
return chunks.join("");
}
export async function getOpensearchXml(baseUrl: string): Promise<string> {
const template = await readFile(paths.opensearchXmlEjs).then((t) => t.toString());
return ejs.render(template, {
appName: config.appName,
baseUrl
});
}

Wyświetl plik

@ -1,6 +1,6 @@
import { ReadableStream, TransformStream } from "stream/web";
export async function asyncIteratorToArray<T>(iterator: AsyncGenerator<T, any, void>): Promise<Array<T>> {
export async function asyncIteratorToArray<T>(iterator: AsyncIterable<T>): Promise<Array<T>> {
const result: T[] = [];
for await (const it of iterator) {
result.push(it);

Wyświetl plik

@ -8,13 +8,19 @@ import { exportGeoJson } from "./export/geojson.js";
import { exportGpx } from "./export/gpx.js";
import domainMiddleware from "express-domain-middleware";
import { Readable, Writable } from "stream";
import { getStaticFrontendMiddleware, renderMap, type RenderMapParams } from "./frontend";
import { getOpensearchXml, getPwaManifest, getStaticFrontendMiddleware, renderMap, type RenderMapParams } from "./frontend";
import { normalizePadName } from "facilmap-utils";
import { paths } from "facilmap-frontend/build.js";
import config from "./config";
type PathParams = {
padId: PadId
}
function getBaseUrl(req: Request): string {
return config.baseUrl ?? `${req.protocol}://${req.host}/`;
}
export async function initWebserver(database: Database, port: number, host?: string): Promise<HttpServer> {
const padMiddleware = async (req: Request<PathParams>, res: Response<string>) => {
let params: RenderMapParams;
@ -52,11 +58,24 @@ export async function initWebserver(database: Database, port: number, host?: str
};
const app = express();
app.set("trust proxy", config.trustProxy ?? false);
app.use(domainMiddleware);
app.use(compression());
app.get("/", padMiddleware);
app.get(`${paths.base}manifest.json`, async (req, res) => {
res.set("Content-type", "application/manifest+json");
res.send(await getPwaManifest());
});
app.get(`${paths.base}opensearch.xml`, async (req, res) => {
res.set("Content-type", "application/opensearchdescription+xml");
res.send(await getOpensearchXml(getBaseUrl(req)));
});
app.use(await getStaticFrontendMiddleware());
// If no file with this name has been found, we render a pad

Wyświetl plik

@ -8,5 +8,6 @@ export function isPromise(object: any): object is Promise<unknown> {
* The config that the backend injects into the EJS template to be read by the frontend.
*/
export interface InjectedConfig {
appName: string;
limaLabsToken?: string;
}