/** * The server's settings: those read from `GERBEUR_*` environment variables, * with their defaults, and the fixed limits the backend applies. * * @module */ /** The port the server listens on (`GERBEUR_PORT`, default 8000). */ export const PORT: number = Number(Deno.env.get("GERBEUR_PORT")) || 8000; /** * The SMTP server mail is sent through, as an `smtps://` URL * (`GERBEUR_SMTPS_URL`). Empty disables email. */ export const SMTPS_URL: string = Deno.env.get("GERBEUR_SMTPS_URL")?.trim() || ""; /** The sender address of outgoing mail (`GERBEUR_FROM_EMAIL`). */ export const FROM_EMAIL: string = Deno.env.get("GERBEUR_FROM_EMAIL")?.trim() || ""; const DEFAULT_WELCOME_EMAIL_BODY = `# Welcome to {{site_name}}! Hi **{{username}}**, Your account has been created successfully. Welcome aboard!`; /** * The Markdown body of the welcome email (`GERBEUR_WELCOME_EMAIL_BODY`), with * `{{site_name}}` and `{{username}}` placeholders. */ export const WELCOME_EMAIL_BODY: string = Deno.env.get("GERBEUR_WELCOME_EMAIL_BODY")?.trim() || DEFAULT_WELCOME_EMAIL_BODY; /** The key tokens are signed with (`GERBEUR_JWT_SECRET`). Required. */ export const JWT_SECRET: string = Deno.env.get("GERBEUR_JWT_SECRET")?.trim() || ""; /** * The network interface the server binds to (`GERBEUR_LISTEN_HOST`). Defaults * to 0.0.0.0 so Docker port-forwarding works out of the box; 127.0.0.1 * restricts it to loopback. */ export const LISTEN_HOST: string = Deno.env.get("GERBEUR_LISTEN_HOST") || "0.0.0.0"; /** * The server's public-facing URL (`GERBEUR_PUBLIC_URL`), without a trailing * slash — used for CORS, WebSocket origin checks, email links, and OG image * URLs. Behind a reverse proxy, set it to the externally-visible URL (e.g. * https://example.com). Defaults to http://localhost:PORT for local * development. */ export const PUBLIC_URL: string = Deno.env.get("GERBEUR_PUBLIC_URL")?.trim().replace(/\/$/, "") || `http://localhost:${PORT}`; /** * The frontend's URL (`GERBEUR_FRONTEND_URL`). In single-container deployments * the API serves the frontend, so it defaults to {@link PUBLIC_URL}; set it * when the frontend runs on a separate host or port (e.g. Vite dev server or a * CDN origin). */ export const FRONTEND_URL: string = Deno.env.get("GERBEUR_FRONTEND_URL")?.trim() || PUBLIC_URL; /** * The SQLite database file (`GERBEUR_DB_PATH`). Overridable so the test suite * can run against a throwaway database (see api/test/setup.ts). */ export const DB_PATH: string = Deno.env.get("GERBEUR_DB_PATH") || "api/sql/gerbeur.db"; // Upload/files /** * The directory uploads are stored under (`GERBEUR_UPLOADS_DIR`), one * subdirectory per kind. */ export const UPLOADS_DIR: string = Deno.env.get("GERBEUR_UPLOADS_DIR") || "api/uploads"; /** Where file dumps are stored. */ export const DUMPS_DIR = `${UPLOADS_DIR}/dumps`; /** Where avatars are stored. */ export const AVATARS_DIR = `${UPLOADS_DIR}/avatars`; /** Where playlist cover images are stored. */ export const PLAYLIST_IMAGES_DIR = `${UPLOADS_DIR}/playlist-images`; /** Where images pasted into chat, comments and descriptions are stored. */ export const ATTACHMENTS_DIR = `${UPLOADS_DIR}/attachments`; /** Where custom dump thumbnails are stored. */ export const THUMBNAILS_DIR = `${UPLOADS_DIR}/thumbnails`; /** The largest image upload accepted: 5 MB. */ export const MAX_IMAGE_SIZE_BYTES = 5 * 1024 * 1024; /** The image types accepted for avatars, covers, thumbnails and attachments. */ export const ALLOWED_IMAGE_MIMES: Set = new Set([ "image/jpeg", "image/png", "image/gif", "image/webp", ]); /** The MIME type prefixes a file dump may have. */ export const DUMP_ALLOWED_MIME_PREFIXES: string[] = [ "text/", "image/", "video/", "audio/", ]; /** The exact MIME types a file dump may have, beyond the prefixes. */ export const DUMP_ALLOWED_MIME_TYPES: Set = new Set([ "application/pdf", "application/json", "application/zip", "application/x-zip-compressed", "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "application/vnd.openxmlformats-officedocument.presentationml.presentation", "application/msword", "application/vnd.ms-excel", "application/vnd.ms-powerpoint", ]); // Pagination /** The page size when a request names none. */ export const PAGINATION_DEFAULT_LIMIT = 20; /** The largest page size a request may ask for. */ export const PAGINATION_MAX_LIMIT = 100; // Startup housekeeping /** How long an attachment nothing links to is kept before startup purges it. */ export const ORPHANED_ATTACHMENTS_RETENTION_HOURS = 1; /** How long an unused invite is kept before startup purges it. */ export const UNUSED_INVITES_RETENTION_DAYS = 7; // SEO/OG /** The site's name (`GERBEUR_SITE_NAME`), in page titles, OG tags and mail. */ export const OG_SITE_NAME: string = Deno.env.get("GERBEUR_SITE_NAME") || "gerbeur"; /** * The emoji the favicon set and app header logo are generated from * (`GERBEUR_SITE_EMOJI`). Applied at runtime (server startup and index.html * injection), so changing it only needs a restart — no rebuild. */ export const SITE_EMOJI: string = Deno.env.get("GERBEUR_SITE_EMOJI") || "🚚"; /** * A Bandcamp playback path: * - `"embed"` — the bandcamp.com iframe (cannot autoplay) * - `"native"` — resolve the page's mp3 streams and play them in the app's own * player, which autoplays and supports albums as a queue */ export type BandcampPlayer = "embed" | "native"; /** * The Bandcamp playback path the frontend uses (`GERBEUR_BANDCAMP_PLAYER`). * Anything unrecognised falls back to `"embed"`, so a typo degrades safely. */ export const BANDCAMP_PLAYER: BandcampPlayer = Deno.env.get("GERBEUR_BANDCAMP_PLAYER")?.trim() === "native" ? "native" : "embed"; /** * The background color of generated icons and the manifest. Mirrors the * hard-coded theme-color in index.html. */ export const THEME_COLOR = "#111827"; const rawOrigins = Deno.env.get("GERBEUR_ALLOWED_ORIGINS") ?? ""; /** * The origins CORS and the WebSocket accept: {@link PUBLIC_URL}, * {@link FRONTEND_URL} and the comma-separated `GERBEUR_ALLOWED_ORIGINS`. */ export const ALLOWED_ORIGINS: string[] = Array.from( new Set([ PUBLIC_URL, // FRONTEND_URL is auto-included so the separate-frontend case only needs // GERBEUR_FRONTEND_URL — no need to repeat it in GERBEUR_ALLOWED_ORIGINS. FRONTEND_URL, ...rawOrigins.split(",").map((o) => o.trim()).filter(Boolean), ]), );