diff --git a/.env.example b/.env.example index 591ecb63..9890a24c 100644 --- a/.env.example +++ b/.env.example @@ -3,6 +3,12 @@ JWT_SECRET="" POSTGRES_URL="postgres://postgres:password@localhost:5432/officer" MAIL_TRANSPORT="smtp://localhost:1025" PUBLIC_URL=http://localhost:9000 + +# Guards (CORS origin checks, rate limits, password-strength rules) are ON unless this is set to +# "dev" or "development". Leave it unset or set it to "production" for a real deployment; only set +# it to "dev" on a local machine you trust, since that disables all three. +PUBLIC_BUILD_ENV=production + DATA_PATH=/path/to/data OFFICER_ITEMS_DIR=/path/to/officer-items HOME_DIR=/home/user diff --git a/src/servers/_middlewares/origin-validation.ts b/src/servers/_middlewares/origin-validation.ts index 025def78..65fcfcb7 100644 --- a/src/servers/_middlewares/origin-validation.ts +++ b/src/servers/_middlewares/origin-validation.ts @@ -1,7 +1,8 @@ import type { MiddlewareHandler } from 'hono'; import * as errors from '../custom-errors'; +import { IS_DEV_BUILD } from '../build-env'; -const { PUBLIC_BUILD_ENV, PUBLIC_URL, EXPO_PUBLIC_CLIENT_ORIGIN } = process.env; +const { PUBLIC_URL, EXPO_PUBLIC_CLIENT_ORIGIN } = process.env; // The allowed production web origin comes from PUBLIC_URL in .env (e.g. https://officer.pastilhas.dev), // not a hardcoded domain. @@ -13,9 +14,7 @@ const PUBLIC_ORIGIN = (() => { } })(); -const WEB_ORIGINS: Record = { - production: PUBLIC_ORIGIN ? [PUBLIC_ORIGIN] : [], -}; +const WEB_ORIGINS: string[] = PUBLIC_ORIGIN ? [PUBLIC_ORIGIN] : []; const CHROME_EXTENSIONS: string[] = [ // 'chrome-extension://' @@ -27,9 +26,7 @@ const APP_ORIGINS: string[] = [ ].filter((o): o is string => Boolean(o)); export function isOriginAllowed(origin: string | undefined, host?: string): boolean { - if (!PUBLIC_BUILD_ENV || PUBLIC_BUILD_ENV === 'dev' || PUBLIC_BUILD_ENV === 'development') { - return true; - } + if (IS_DEV_BUILD) return true; if (origin) { if (origin.startsWith('chrome-extension://')) { @@ -40,13 +37,11 @@ export function isOriginAllowed(origin: string | undefined, host?: string): bool return true; } - const allowed = WEB_ORIGINS[PUBLIC_BUILD_ENV]; - return !!allowed?.includes(origin); + return WEB_ORIGINS.includes(origin); } if (host) { - const allowed = WEB_ORIGINS[PUBLIC_BUILD_ENV]; - return !!allowed?.some((o) => o.endsWith(host)); + return WEB_ORIGINS.some((o) => o.endsWith(host)); } return false; diff --git a/src/servers/_middlewares/rate-limiter.ts b/src/servers/_middlewares/rate-limiter.ts index e87136e5..75a8a3a0 100644 --- a/src/servers/_middlewares/rate-limiter.ts +++ b/src/servers/_middlewares/rate-limiter.ts @@ -1,5 +1,6 @@ import type { Context, MiddlewareHandler } from 'hono'; import * as errors from '../custom-errors'; +import { IS_DEV_BUILD } from '../build-env'; type RateLimitEntry = { count: number; @@ -24,9 +25,6 @@ function cleanupStore(store: Map) { } } -const { PUBLIC_BUILD_ENV } = process.env; -const isProduction = PUBLIC_BUILD_ENV === 'production' || PUBLIC_BUILD_ENV === 'staging'; - export function rateLimiter(options: RateLimiterOptions): MiddlewareHandler { const { windowMs, @@ -43,7 +41,7 @@ export function rateLimiter(options: RateLimiterOptions): MiddlewareHandler { setInterval(() => cleanupStore(store), 60_000); return async (ctx, next) => { - if (!isProduction) return next(); + if (IS_DEV_BUILD) return next(); const key = keyGenerator(ctx); const now = Date.now(); diff --git a/src/servers/api/auth/change-password.ts b/src/servers/api/auth/change-password.ts index d4dd9c64..ce221d55 100644 --- a/src/servers/api/auth/change-password.ts +++ b/src/servers/api/auth/change-password.ts @@ -4,13 +4,11 @@ import argon2 from 'argon2'; import { sign } from '@@/jwt'; import * as errors from '@@/custom-errors'; import { validatePassword } from './validate-password'; - -const { PUBLIC_BUILD_ENV } = process.env; -const isProduction = PUBLIC_BUILD_ENV === 'production' || PUBLIC_BUILD_ENV === 'staging'; +import { IS_DEV_BUILD } from '@@/build-env'; export const changePasswordHandler: Handler = async function (ctx) { const { password, newPassword } = ctx.get('body'); - if (isProduction) validatePassword(newPassword); + if (!IS_DEV_BUILD) validatePassword(newPassword); const reqUser = ctx.get('user'); const dbUser = await getUserById(reqUser.id); diff --git a/src/servers/build-env.ts b/src/servers/build-env.ts new file mode 100644 index 00000000..1af662bf --- /dev/null +++ b/src/servers/build-env.ts @@ -0,0 +1,6 @@ +// Officer relaxes its guards (origin checks, rate limits, password rules) only when PUBLIC_BUILD_ENV +// explicitly asks for it. Anything else — including an unset variable — gets the hardened path, so a +// deployment that forgets to set it fails closed rather than silently opening up. +const { PUBLIC_BUILD_ENV } = process.env; + +export const IS_DEV_BUILD = PUBLIC_BUILD_ENV === 'dev' || PUBLIC_BUILD_ENV === 'development';