diff --git a/.dockerignore b/.dockerignore index f2dcb54..fa010af 100644 --- a/.dockerignore +++ b/.dockerignore @@ -4,4 +4,4 @@ npm-debug.log* .git .idea coverage -dist \ No newline at end of file +dist diff --git a/.env.example b/.env.example index 9f9b37f..4976b7c 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,8 @@ PORT=3030 API_HOST_PORT=8030 +PUBLIC_API_URL=https://dune.api.coppnic.cc +PUBLIC_UI_URL=https://ui.dune.api.coppnic.cc +FORCE_HTTPS=true MONGODB_URI=mongodb://root:change-me@37.60.245.70:27017 MONGODB_DB=duneawa MAX_PAGES= diff --git a/README.md b/README.md index 5ba7e7a..e586846 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ npm run import:smoke npm start ``` -Open Swagger UI at `http://localhost:3030/docs`. +Open Swagger UI locally at `http://localhost:3030/docs`. ## Docker @@ -18,7 +18,26 @@ Open Swagger UI at `http://localhost:3030/docs`. docker compose up --build ``` -The API listens on `http://localhost:8030` by default when run through Docker Compose. Set `API_HOST_PORT=3031` if your machine needs the alternate host port. +The container listens on `3030` and Docker Compose exposes it on `8030` by default. Set `API_HOST_PORT=3031` if your machine needs the alternate host port. + +## Public HTTPS Domains + +This app is configured for Dokploy/Traefik HTTPS by default. Traefik should terminate TLS and route both public HTTPS domains to the app container: + +```text +https://dune.api.coppnic.cc -> container port 3030 +https://ui.dune.api.coppnic.cc -> container port 3030 +``` + +The public API URL is `https://dune.api.coppnic.cc`. Swagger UI is available at `https://ui.dune.api.coppnic.cc/docs`. + +The OpenAPI document advertises the HTTPS API domain by default, so Swagger requests go to the right public API host. These values can be adjusted through environment variables: + +```env +PUBLIC_API_URL=https://dune.api.coppnic.cc +PUBLIC_UI_URL=https://ui.dune.api.coppnic.cc +FORCE_HTTPS=true +``` ## Import All Data diff --git a/docker-compose.yml b/docker-compose.yml index 6e56de9..cb0d121 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,6 +7,9 @@ services: - "${API_HOST_PORT:-8030}:${PORT:-3030}" environment: PORT: ${PORT:-3030} + PUBLIC_API_URL: ${PUBLIC_API_URL:-https://dune.api.coppnic.cc} + PUBLIC_UI_URL: ${PUBLIC_UI_URL:-https://ui.dune.api.coppnic.cc} + FORCE_HTTPS: ${FORCE_HTTPS:-true} MONGODB_URI: ${MONGODB_URI} MONGODB_DB: ${MONGODB_DB:-duneawa} MAX_PAGES: ${MAX_PAGES:-} diff --git a/src/app.js b/src/app.js index 3bf3762..aa547dd 100644 --- a/src/app.js +++ b/src/app.js @@ -1,6 +1,7 @@ const cors = require("cors"); const express = require("express"); const swaggerUi = require("swagger-ui-express"); +const { config } = require("./config"); const { pingMongo } = require("./db/client"); const { router: apiRouter } = require("./routes/api"); const { openApiDocument } = require("./swagger/openapi"); @@ -8,11 +9,30 @@ const { openApiDocument } = require("./swagger/openapi"); function createApp() { const app = express(); + app.set("trust proxy", true); app.use(cors()); app.use(express.json({ limit: "2mb" })); + app.use((request, response, next) => { + const forwardedProto = request.get("x-forwarded-proto"); + const isSecure = request.secure || forwardedProto === "https"; + const isLocalhost = ["localhost", "127.0.0.1", "::1"].includes( + request.hostname, + ); + + if (config.public.forceHttps && !isSecure && !isLocalhost) { + const baseUrl = + request.hostname === "ui.dune.api.coppnic.cc" + ? config.public.uiUrl + : config.public.apiUrl; + response.redirect(308, new URL(request.originalUrl, baseUrl).toString()); + return; + } + + next(); + }); app.get("/", (request, response) => { - response.redirect("/docs"); + response.redirect(308, `${config.public.uiUrl}/docs`); }); app.get("/health", async (request, response) => { diff --git a/src/config.js b/src/config.js index 8496ca6..cc0cd80 100644 --- a/src/config.js +++ b/src/config.js @@ -15,8 +15,21 @@ function parseOptionalPositiveInteger(value, name) { return parsed; } +function parseBoolean(value, fallback = false) { + if (value === undefined || value === null || value === "") { + return fallback; + } + + return ["1", "true", "yes", "on"].includes(String(value).toLowerCase()); +} + const config = { port: parseOptionalPositiveInteger(process.env.PORT, "PORT") || 3030, + public: { + apiUrl: process.env.PUBLIC_API_URL || "https://dune.api.coppnic.cc", + uiUrl: process.env.PUBLIC_UI_URL || "https://ui.dune.api.coppnic.cc", + forceHttps: parseBoolean(process.env.FORCE_HTTPS, true), + }, mongodb: { uri: process.env.MONGODB_URI || "mongodb://root:63eba009@37.60.245.70:27017", @@ -32,4 +45,4 @@ const config = { }, }; -module.exports = { config, parseOptionalPositiveInteger }; +module.exports = { config, parseBoolean, parseOptionalPositiveInteger }; diff --git a/src/server.js b/src/server.js index 2d851ec..fc185c9 100644 --- a/src/server.js +++ b/src/server.js @@ -10,7 +10,8 @@ async function start() { const app = createApp(); const server = app.listen(config.port, () => { console.log(`Dune API listening on http://localhost:${config.port}`); - console.log(`Swagger UI available at http://localhost:${config.port}/docs`); + console.log(`Public API URL: ${config.public.apiUrl}`); + console.log(`Public Swagger UI URL: ${config.public.uiUrl}/docs`); }); async function shutdown(signal) { diff --git a/src/swagger/openapi.js b/src/swagger/openapi.js index b1f8daf..7efd9e2 100644 --- a/src/swagger/openapi.js +++ b/src/swagger/openapi.js @@ -1,4 +1,5 @@ const { DATASETS, LANGUAGES } = require("../datasets"); +const { config } = require("../config"); const datasetKeys = Object.keys(DATASETS); @@ -9,7 +10,7 @@ const openApiDocument = { version: "1.0.0", description: "API for Dune: Awakening Questlog data stored in MongoDB.", }, - servers: [{ url: "/" }], + servers: [{ url: config.public.apiUrl }], tags: [{ name: "Health" }, { name: "Data" }, { name: "Import" }], paths: { "/health": {