From 58b4d7454f8e85260c750a94d493671f1cb8ec3b Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Thu, 27 Mar 2025 18:51:22 +0100 Subject: [PATCH] refactor(api): :recycle: Serve frontend from static files instead of proxying another process --- .github/config.workflow.toml | 11 ++-- app.ts | 58 ++++++--------------- classes/config/schema.ts | 5 +- config/config.example.toml | 11 ++-- config/config.schema.json | 97 +++++++++++++++++++++++++++++------- entrypoints/api/setup.ts | 16 +----- scripts/versia-install.sh | 40 +++++++-------- tests/utils.ts | 2 + 8 files changed, 133 insertions(+), 107 deletions(-) diff --git a/.github/config.workflow.toml b/.github/config.workflow.toml index 882e2e8e..a88391b8 100644 --- a/.github/config.workflow.toml +++ b/.github/config.workflow.toml @@ -75,7 +75,7 @@ banned_user_agents = [ # URL to an eventual HTTP proxy # Will be used for all outgoing requests -# proxy_address = "http://localhost:8118" +# proxy_address = "http://localhost:8118" # TLS configuration. You should probably be using a reverse proxy instead of this # [http.tls] @@ -89,8 +89,11 @@ banned_user_agents = [ # Enable custom frontends (warning: not enabling this will make Versia Server only accessible via the Mastodon API) # Frontends also control the OpenID flow, so if you disable this, you will need to use the Mastodon frontend enabled = true -# The URL to reach the frontend at (should be on a local network) -url = "http://localhost:3000" +# Path that frontend files are served from +# Edit this property to serve custom frontends +# If this is not set, Versia Server will also check +# the VERSIA_FRONTEND_PATH environment variable +# path = "" [frontend.routes] # Special routes for your frontend, below are the defaults for Versia-FE @@ -328,7 +331,7 @@ remove_after_failure_seconds = 31536000 # Time in seconds to remove completed jobs remove_after_complete_seconds = 31536000 # Time in seconds to remove failed jobs -remove_after_failure_seconds = 31536000 +remove_after_failure_seconds = 31536000 [federation] # This is a list of domain names, such as "mastodon.social" or "pleroma.site" diff --git a/app.ts b/app.ts index 28200841..521b2582 100644 --- a/app.ts +++ b/app.ts @@ -9,6 +9,7 @@ import { getLogger } from "@logtape/logtape"; import { apiReference } from "@scalar/hono-api-reference"; import { inspect } from "bun"; import chalk from "chalk"; +import { serveStatic } from "hono/bun"; import { cors } from "hono/cors"; import { createMiddleware } from "hono/factory"; import { prettyJSON } from "hono/pretty-json"; @@ -163,49 +164,20 @@ export const appFactory = async (): Promise> => { return context.body(null, 204); }); - app.all("*", async (context) => { - const replacedUrl = new URL( - new URL(context.req.url).pathname, - config.frontend.url, - ).toString(); - - serverLogger.debug`Proxying ${replacedUrl}`; - - const proxy = await fetch(replacedUrl, { - headers: { - // Include for SSR - "X-Forwarded-Host": `${config.http.bind}:${config.http.bind_port}`, - "Accept-Encoding": "identity", - }, - redirect: "manual", - }).catch((e) => { - serverLogger.error`${e}`; - sentry?.captureException(e); - serverLogger.error`The Frontend is not running or the route is not found: ${replacedUrl}`; - return null; - }); - - proxy?.headers.set("Cache-Control", "max-age=31536000"); - - if (!proxy || proxy.status === 404) { - throw new ApiError( - 404, - "Route not found on proxy or API route. Are you using the correct HTTP method?", - ); - } - - // Disable CSP upgrade-insecure-requests if an .onion domain is used - if (new URL(context.req.url).hostname.endsWith(".onion")) { - proxy.headers.set( - "Content-Security-Policy", - proxy.headers - .get("Content-Security-Policy") - ?.replace("upgrade-insecure-requests;", "") ?? "", - ); - } - - return proxy; - }); + app.all( + "*", + serveStatic({ + root: config.frontend.path, + }), + ); + // Fallback for SPAs, in case we've hit a route that is client-side + app.all( + "*", + serveStatic({ + root: config.frontend.path, + path: "index.html", + }), + ); app.onError((error, c) => { if (error instanceof ApiError) { diff --git a/classes/config/schema.ts b/classes/config/schema.ts index fc179007..8bb0ebda 100644 --- a/classes/config/schema.ts +++ b/classes/config/schema.ts @@ -1,5 +1,6 @@ +import { cwd } from "node:process"; import { z } from "@hono/zod-openapi"; -import { type BunFile, file } from "bun"; +import { type BunFile, env, file } from "bun"; import ISO6391 from "iso-639-1"; import { types as mimeTypes } from "mime-types"; import { generateVAPIDKeys } from "web-push"; @@ -400,7 +401,7 @@ export const ConfigSchema = z }), frontend: z.strictObject({ enabled: z.boolean().default(true), - url: url.default("http://localhost:3000"), + path: z.string().default(env.VERSIA_FRONTEND_PATH || cwd()), routes: z.strictObject({ home: urlPath.default("/"), login: urlPath.default("/oauth/authorize"), diff --git a/config/config.example.toml b/config/config.example.toml index 4fcc9289..55552858 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -75,7 +75,7 @@ banned_user_agents = [ # URL to an eventual HTTP proxy # Will be used for all outgoing requests -# proxy_address = "http://localhost:8118" +# proxy_address = "http://localhost:8118" # TLS configuration. You should probably be using a reverse proxy instead of this # [http.tls] @@ -89,8 +89,11 @@ banned_user_agents = [ # Enable custom frontends (warning: not enabling this will make Versia Server only accessible via the Mastodon API) # Frontends also control the OpenID flow, so if you disable this, you will need to use the Mastodon frontend enabled = true -# The URL to reach the frontend at (should be on a local network) -url = "http://localhost:3000" +# Path that frontend files are served from +# Edit this property to serve custom frontends +# If this is not set, Versia Server will also check +# the VERSIA_FRONTEND_PATH environment variable +# path = "" [frontend.routes] # Special routes for your frontend, below are the defaults for Versia-FE @@ -328,7 +331,7 @@ remove_after_failure_seconds = 31536000 # Time in seconds to remove completed jobs remove_after_complete_seconds = 31536000 # Time in seconds to remove failed jobs -remove_after_failure_seconds = 31536000 +remove_after_failure_seconds = 31536000 [federation] # This is a list of domain names, such as "mastodon.social" or "pleroma.site" diff --git a/config/config.schema.json b/config/config.schema.json index 988b1cd2..ad403bf3 100644 --- a/config/config.schema.json +++ b/config/config.schema.json @@ -273,9 +273,9 @@ "type": "boolean", "default": true }, - "url": { - "$ref": "#/properties/http/properties/proxy_address", - "default": "http://localhost:3000" + "path": { + "type": "string", + "default": "/home/jessew/Dev/versia-server" }, "routes": { "type": "object", @@ -317,7 +317,7 @@ "default": {} } }, - "required": ["enabled", "url", "routes", "settings"], + "required": ["enabled", "path", "routes", "settings"], "additionalProperties": false }, "email": { @@ -597,8 +597,12 @@ }, "default": [ "application/vnd.lotus-1-2-3", + "model/step", "application/andrew-inset", + "application/appinstaller", "application/applixware", + "application/appx", + "application/appxbundle", "application/atom+xml", "application/atomcat+xml", "application/atomdeleted+xml", @@ -606,6 +610,8 @@ "application/atsc-dwd+xml", "application/atsc-held+xml", "application/atsc-rsat+xml", + "application/automationml-aml+xml", + "application/automationml-amlx+zip", "application/bdoc", "application/calendar+xml", "application/ccxml+xml", @@ -617,19 +623,21 @@ "application/cdmi-queue", "application/cpl+xml", "application/cu-seeme", + "application/cwl", "application/dash+xml", "application/dash-patch+xml", "application/davmount+xml", + "application/dicom", "application/docbook+xml", "application/dssc+der", "application/dssc+xml", "application/ecmascript", - "application/ecmascript", "application/emma+xml", "application/emotionml+xml", "application/epub+zip", "application/exi", "application/express", + "application/fdf", "application/fdt+xml", "application/font-tdpfr", "application/geo+json", @@ -648,8 +656,7 @@ "application/java-archive", "application/java-serialized-object", "application/java-vm", - "application/javascript", - "application/javascript", + "text/javascript", "application/json", "application/json", "application/json5", @@ -680,6 +687,10 @@ "application/mp21", "application/mp4", "application/mp4", + "application/mp4", + "application/mp4", + "application/msix", + "application/msixbundle", "application/msword", "application/msword", "application/mxf", @@ -716,6 +727,8 @@ "application/onenote", "application/onenote", "application/onenote", + "application/onenote", + "application/onenote", "application/oxps", "application/p2p-overlay+xml", "application/patch-ops-error+xml", @@ -740,6 +753,7 @@ "application/postscript", "application/provenance+xml", "application/prs.cww", + "application/prs.xsf+xml", "application/pskc+xml", "application/raml+yaml", "application/rdf+xml", @@ -775,6 +789,7 @@ "application/smil+xml", "application/sparql-query", "application/sparql-results+xml", + "application/sql", "application/srgs", "application/srgs+xml", "application/sru+xml", @@ -807,7 +822,7 @@ "application/vnd.adobe.fxp", "application/vnd.adobe.fxp", "application/vnd.adobe.xdp+xml", - "application/vnd.adobe.xfdf", + "application/xfdf", "application/vnd.age", "application/vnd.ahead.space", "application/vnd.airzip.filesecure.azf", @@ -828,6 +843,7 @@ "application/vnd.aristanetworks.swi", "application/vnd.astraea-software.iota", "application/vnd.audiograph", + "application/vnd.autodesk.fbx", "application/vnd.balsamiq.bmml+xml", "application/vnd.blueice.multipass", "application/vnd.bmi", @@ -861,6 +877,7 @@ "application/vnd.dart", "application/vnd.data-vision.rdz", "application/vnd.dbf", + "application/vnd.dcmp+xml", "application/vnd.dece.data", "application/vnd.dece.data", "application/vnd.dece.data", @@ -891,7 +908,6 @@ "application/vnd.eszigno3+xml", "application/vnd.ezpix-album", "application/vnd.ezpix-package", - "application/vnd.fdf", "application/vnd.fdsn.mseed", "application/vnd.fdsn.seed", "application/vnd.fdsn.seed", @@ -915,6 +931,7 @@ "application/vnd.fuzzysheet", "application/vnd.genomatix.tuxedo", "application/vnd.geogebra.file", + "application/vnd.geogebra.slides", "application/vnd.geogebra.tool", "application/vnd.geometry-explorer", "application/vnd.geometry-explorer", @@ -923,10 +940,17 @@ "application/vnd.geospace", "application/vnd.gmx", "application/vnd.google-apps.document", + "application/vnd.google-apps.drawing", + "application/vnd.google-apps.form", + "application/vnd.google-apps.jam", + "application/vnd.google-apps.map", "application/vnd.google-apps.presentation", + "application/vnd.google-apps.script", + "application/vnd.google-apps.site", "application/vnd.google-apps.spreadsheet", "application/vnd.google-earth.kml+xml", "application/vnd.google-earth.kmz", + "application/vnd.gov.sk.xmldatacontainer+xml", "application/vnd.grafeq", "application/vnd.grafeq", "application/vnd.groove-account", @@ -1051,6 +1075,7 @@ "application/vnd.ms-powerpoint.slideshow.macroenabled.12", "application/vnd.ms-powerpoint.template.macroenabled.12", "application/vnd.ms-project", + "application/vnd.ms-visio.viewer", "application/vnd.ms-word.document.macroenabled.12", "application/vnd.ms-word.template.macroenabled.12", "application/vnd.ms-works", @@ -1063,6 +1088,7 @@ "application/vnd.musician", "application/vnd.muvee.style", "application/vnd.mynfc", + "application/vnd.nato.bindingdataobject+xml", "application/vnd.neurolanguage.nlu", "application/vnd.nitf", "application/vnd.nitf", @@ -1120,9 +1146,13 @@ "application/vnd.pocketlearn", "application/vnd.powerbuilder6", "application/vnd.previewsystems.box", + "application/vnd.procrate.brushset", + "application/vnd.procreate.brush", + "application/vnd.procreate.dream", "application/vnd.proteus.magazine", "application/vnd.publishare-delta-tree", "application/vnd.pvi.ptid1", + "application/vnd.pwg-xhtml-print+xml", "application/vnd.quark.quarkxpress", "application/vnd.quark.quarkxpress", "application/vnd.quark.quarkxpress", @@ -1199,11 +1229,14 @@ "application/vnd.umajin", "application/vnd.unity", "application/vnd.uoml+xml", + "application/vnd.uoml+xml", "application/vnd.vcx", "application/vnd.visio", "application/vnd.visio", "application/vnd.visio", "application/vnd.visio", + "application/vnd.visio", + "application/vnd.visio", "application/vnd.visionary", "application/vnd.vsf", "application/vnd.wap.wbxml", @@ -1246,6 +1279,7 @@ "application/x-authorware-seg", "application/x-bcpio", "application/x-bittorrent", + "application/x-blender", "application/x-blorb", "application/x-blorb", "application/x-bzip", @@ -1302,6 +1336,7 @@ "application/x-hdf", "application/x-httpd-php", "application/x-install-instructions", + "application/x-ipynb+json", "application/x-java-archive-diff", "application/x-java-jnlp-file", "application/x-keepass2", @@ -1311,7 +1346,7 @@ "application/x-lzh-compressed", "application/x-makeself", "application/x-mie", - "application/x-mobipocket-ebook", + "model/prc", "application/x-mobipocket-ebook", "application/x-ms-application", "application/x-ms-shortcut", @@ -1353,7 +1388,6 @@ "application/x-shar", "application/x-shockwave-flash", "application/x-silverlight-app", - "application/x-sql", "application/x-stuffit", "application/x-stuffitx", "application/x-subrip", @@ -1387,6 +1421,7 @@ "application/xliff+xml", "application/x-xpinstall", "application/x-xz", + "application/zip", "application/x-zmachine", "application/x-zmachine", "application/x-zmachine", @@ -1419,8 +1454,10 @@ "application/xv+xml", "application/yang", "application/yin+xml", - "application/zip", + "application/zip+dotlottie", "video/3gpp", + "audio/aac", + "audio/aac", "audio/adpcm", "audio/amr", "audio/basic", @@ -1433,6 +1470,7 @@ "audio/mpeg", "audio/mp4", "audio/mp4", + "audio/mp4", "audio/mpeg", "audio/mpeg", "audio/mpeg", @@ -1456,9 +1494,8 @@ "audio/vnd.nuera.ecelp7470", "audio/vnd.nuera.ecelp9600", "audio/vnd.rip", - "audio/wave", + "audio/wav", "audio/webm", - "audio/x-aac", "audio/x-aiff", "audio/x-aiff", "audio/x-aiff", @@ -1489,8 +1526,10 @@ "image/avcs", "image/avif", "image/bmp", + "image/bmp", "image/cgm", "image/dicom-rle", + "image/dpx", "image/fits", "image/g3fax", "image/gif", @@ -1499,8 +1538,9 @@ "image/heif", "image/heif-sequence", "image/hej2k", - "image/hsj2", "image/ief", + "image/jaii", + "image/jais", "image/jls", "image/jp2", "image/jp2", @@ -1510,8 +1550,10 @@ "image/jph", "image/jphc", "image/jpm", + "image/jpm", "image/jpx", "image/jpx", + "image/jxl", "image/jxr", "image/jxra", "image/jxrs", @@ -1521,8 +1563,10 @@ "image/jxss", "image/ktx", "image/ktx2", + "image/pjpeg", "image/png", "image/prs.btif", + "image/prs.btif", "image/prs.pti", "image/sgi", "image/svg+xml", @@ -1560,6 +1604,7 @@ "image/vnd.zbrush.pcx", "image/webp", "image/x-3ds", + "image/x-adobe-dng", "image/x-cmu-raster", "image/x-cmx", "image/x-freehand", @@ -1587,28 +1632,41 @@ "message/global-headers", "message/rfc822", "message/rfc822", + "message/rfc822", + "message/rfc822", "message/vnd.wfa.wsc", "model/3mf", "model/gltf+json", "model/gltf-binary", "model/iges", "model/iges", + "model/jt", "model/mesh", "model/mesh", "model/mesh", "model/mtl", + "model/step", + "model/step", + "model/step", + "model/step", "model/step+xml", "model/step+zip", "model/step-xml+zip", + "model/u3d", + "model/vnd.bary", + "model/vnd.cld", "model/vnd.collada+xml", "model/vnd.dwf", "model/vnd.gdl", "model/vnd.gtw", - "model/vnd.mts", + "video/mp2t", "model/vnd.opengex", "model/vnd.parasolid.transmit.binary", "model/vnd.parasolid.transmit.text", + "model/vnd.pytha.pyox", + "model/vnd.pytha.pyox", "model/vnd.sap.vds", + "model/vnd.usda", "model/vnd.usdz+zip", "model/vnd.valve.source.compiled-map", "model/vnd.vtu", @@ -1632,6 +1690,7 @@ "text/html", "text/html", "text/jade", + "text/javascript", "text/jsx", "text/less", "text/markdown", @@ -1683,6 +1742,7 @@ "text/vnd.wap.wml", "text/vnd.wap.wmlscript", "text/vtt", + "text/wgsl", "text/x-asm", "text/x-asm", "text/x-c", @@ -1723,12 +1783,11 @@ "video/h264", "video/iso.segment", "video/jpeg", - "video/jpm", "video/mj2", "video/mj2", "video/mp2t", - "video/mp4", - "video/mp4", + "video/mp2t", + "video/mp2t", "video/mp4", "video/mpeg", "video/mpeg", diff --git a/entrypoints/api/setup.ts b/entrypoints/api/setup.ts index 68c9d573..fda9587e 100644 --- a/entrypoints/api/setup.ts +++ b/entrypoints/api/setup.ts @@ -13,7 +13,7 @@ await configureLoggers(); const serverLogger = getLogger("server"); console.info(` -██╗ ██╗███████╗██████╗ ███████╗██╗ █████╗ +██╗ ██╗███████╗██████╗ ███████╗██╗ █████╗ ██║ ██║██╔════╝██╔══██╗██╔════╝██║██╔══██╗ ██║ ██║█████╗ ██████╔╝███████╗██║███████║ ╚██╗ ██╔╝██╔══╝ ██╔══██╗╚════██║██║██╔══██║ @@ -36,20 +36,6 @@ serverLogger.info`Versia Server started at ${config.http.bind}:${config.http.bin serverLogger.info`Database is online, now serving ${postCount} posts`; -if (config.frontend.enabled) { - // Check if frontend is reachable - const response = await fetch(new URL("/", config.frontend.url)) - .then((res) => res.ok) - .catch(() => false); - - if (!response) { - serverLogger.error`Frontend is unreachable at ${config.frontend.url}`; - serverLogger.error`Please ensure the frontend is online and reachable`; - } -} else { - serverLogger.warn`Frontend is disabled, skipping check`; -} - // Check if Redis is reachable const connection = new IORedis({ host: config.redis.queue.host, diff --git a/scripts/versia-install.sh b/scripts/versia-install.sh index 0bdabcba..04c7a2a6 100755 --- a/scripts/versia-install.sh +++ b/scripts/versia-install.sh @@ -37,7 +37,7 @@ error() { # Check for required commands check_requirements() { local required_commands=("docker" "docker-compose" "curl" "mkcert") - + for cmd in "${required_commands[@]}"; do if ! command -v "$cmd" >/dev/null 2>&1; then error "$cmd is required but not installed. Please install it first." @@ -54,14 +54,14 @@ setup_directories() { # Generate SSL certificates using mkcert setup_ssl() { log "Setting up SSL certificates..." - + # Initialize mkcert if not already done mkcert -install - + # Generate certificates for the domain cd "${INSTALL_DIR}" mkcert "${DOMAIN}" - + # Create nginx config directory if it doesn't exist mkdir -p "${INSTALL_DIR}/nginx" } @@ -69,10 +69,10 @@ setup_ssl() { # Download necessary files download_files() { log "Downloading configuration files..." - + # Download docker-compose.yml curl -sSL "https://raw.githubusercontent.com/versia-pub/server/${VERSION}/docker-compose.yml" -o "${COMPOSE_FILE}" - + # Download config.example.toml curl -sSL "https://raw.githubusercontent.com/versia-pub/server/${VERSION}/config/config.example.toml" -o "${INSTALL_DIR}/config/config.example.toml" } @@ -86,7 +86,7 @@ generate_passwords() { # Configure Versia config.toml configure_config_file() { log "Configuring config.toml..." - + cat > "${CONFIG_FILE}" << EOF [database] host = "db" @@ -176,7 +176,7 @@ cert = "/app/dist/config/${DOMAIN}.pem" [frontend] enabled = true -url = "http://fe:3000" +path = "/app/dist/frontend" [media] backend = "local" @@ -293,7 +293,7 @@ services: restart: unless-stopped networks: - ${CONTAINER_PREFIX}-net - + networks: ${CONTAINER_PREFIX}-net: EOF @@ -303,7 +303,7 @@ EOF create_user() { local username="$1" local password="$2" - + log "Creating user: ${username}" # Set the password using a heredoc to provide input docker exec -i "${CONTAINER_NAMES[0]}" /bin/sh /app/entrypoint.sh cli user create "${username}" --password "${password}" @@ -312,13 +312,13 @@ create_user() { # Configure the services configure_services() { log "Configuring services..." - + # Configure config.toml configure_config_file - + # Create new docker-compose.yml with our modifications create_docker_compose - + # Copy SSL certificates to config directory cp "${INSTALL_DIR}/${DOMAIN}.pem" "${INSTALL_DIR}/config/" cp "${INSTALL_DIR}/${DOMAIN}-key.pem" "${INSTALL_DIR}/config/" @@ -343,27 +343,27 @@ handle_interrupt() { # Main installation function install_versia() { log "Starting Versia installation..." - + check_requirements setup_directories generate_passwords download_files setup_ssl configure_services - + # Start the services log "Starting Versia services..." cd "${INSTALL_DIR}" docker-compose up -d - + # Wait for services to be ready sleep 5 - + # Create a default test user TEST_USER="testuser_${RANDOM_SUFFIX}" TEST_PASSWORD=$(openssl rand -base64 12) create_user "${TEST_USER}" "${TEST_PASSWORD}" - + log "Installation complete! Versia is now available at https://${DOMAIN}:${PORT}" log "Installation Details:" log "---------------------" @@ -383,10 +383,10 @@ install_versia() { log "docker-compose exec -it ${CONTAINER_NAMES[0]} /bin/sh /app/entrypoint.sh cli user create --set-password" log "---------------------" log "Press Ctrl+C to stop and cleanup the installation" - + # Set up interrupt handler trap handle_interrupt SIGINT - + # Wait indefinitely while true; do sleep 1 diff --git a/tests/utils.ts b/tests/utils.ts index 4cbdbd8f..be396411 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -57,6 +57,8 @@ export const generateClient = async ( token?.data.accessToken, ); + // @ts-expect-error This doesn't include fetch.preconnect, which is a custom property + // added by Bun // biome-ignore lint/complexity/useLiteralKeys: Overriding private properties client["fetch"] = ( input: RequestInfo | string | URL | Request,