refactor(api): ♻️ Serve frontend from static files instead of proxying another process

This commit is contained in:
Jesse Wierzbinski 2025-03-27 18:51:22 +01:00
parent 5f8c57b3e1
commit 58b4d7454f
No known key found for this signature in database
8 changed files with 133 additions and 107 deletions

View file

@ -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"

58
app.ts
View file

@ -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<OpenAPIHono<HonoEnv>> => {
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) {

View file

@ -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"),

View file

@ -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"

View file

@ -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",

View file

@ -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,

View file

@ -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 <username> --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

View file

@ -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,