Skip to content

Instantly share code, notes, and snippets.

@DennisKraaijeveld
Last active May 29, 2024 19:05
Show Gist options
  • Save DennisKraaijeveld/5d47ae3b0425f5b99c81c050722ba2fe to your computer and use it in GitHub Desktop.
Save DennisKraaijeveld/5d47ae3b0425f5b99c81c050722ba2fe to your computer and use it in GitHub Desktop.
import crypto from 'crypto'
import { createRequestHandler as _createRequestHandler } from '@remix-run/express'
import { type ServerBuild, installGlobals } from '@remix-run/node'
import * as Sentry from '@sentry/remix'
import chalk from 'chalk'
import closeWithGrace from 'close-with-grace'
import compression from 'compression'
import express from 'express'
import rateLimit from 'express-rate-limit'
import getPort, { portNumbers } from 'get-port'
import helmet from 'helmet'
import morgan from 'morgan'
import { createExpressApp } from 'remix-create-express-app'
const MODE = process.env.NODE_ENV ?? 'development'
const IS_PROD = MODE === 'production'
const IS_DEV = MODE === 'development'
const createRequestHandler = IS_PROD
? Sentry.wrapExpressCreateRequestHandler(_createRequestHandler)
: _createRequestHandler
installGlobals()
export const app = createExpressApp({
getLoadContext: (
_req: express.Request,
res: express.Response,
{ build },
) => ({
cspNonce: res.locals.cspNonce,
serverBuild: build,
}),
// TODO: Needs attention. Not confident that this is correct.
customRequestHandler: defaultCreateRequestHandler => {
return ({ getLoadContext, mode, build }) =>
createRequestHandler({
getLoadContext: getLoadContext,
mode: MODE,
build: build as unknown as ServerBuild,
})
},
configure: async app => {
const getHost = (req: { get: (key: string) => string | undefined }) =>
req.get('X-Forwarded-Host') ?? req.get('host') ?? ''
// fly is our proxy
app.set('trust proxy', true)
app.use((req, res, next) => {
const proto = req.get('X-Forwarded-Proto')
const host = getHost(req)
if (proto === 'http') {
res.set('X-Forwarded-Proto', 'https')
res.redirect(`https://${host}${req.originalUrl}`)
return
}
next()
})
// no ending slashes for SEO reasons
// https://github.com/epicweb-dev/epic-stack/discussions/108
app.get('*', (req, res, next) => {
if (req.path.endsWith('/') && req.path.length > 1) {
const query = req.url.slice(req.path.length)
const safepath = req.path.slice(0, -1).replace(/\/+/g, '/')
res.redirect(302, safepath + query)
} else {
next()
}
})
app.use(compression())
// http://expressjs.com/en/advanced/best-practice-security.html#at-a-minimum-disable-x-powered-by-header
app.disable('x-powered-by')
app.use(Sentry.Handlers.requestHandler())
app.use(Sentry.Handlers.tracingHandler())
app.use(
'/assets',
express.static('build/client/assets', { immutable: true, maxAge: '1y' }),
)
// Everything else (like favicon.ico) is cached for an hour. You may want to be
// more aggressive with this caching.
app.use(express.static('build/client', { maxAge: '1h' }))
app.get(['/img/*', '/favicons/*'], (_req, res) => {
// if we made it past the express.static for these, then we're missing something.
// So we'll just send a 404 and won't bother calling other middleware.
return res.status(404).send('Not found')
})
morgan.token('url', req => decodeURIComponent(req.url ?? ''))
app.use(
morgan('tiny', {
skip: (req, res) =>
res.statusCode === 200 &&
(req.url?.startsWith('/resources/note-images') ||
req.url?.startsWith('/resources/user-images') ||
req.url?.startsWith('/resources/healthcheck')),
}),
)
app.use((_, res, next) => {
res.locals.cspNonce = crypto.randomBytes(16).toString('hex')
next()
})
app.use(
helmet({
xPoweredBy: false,
referrerPolicy: { policy: 'same-origin' },
crossOriginEmbedderPolicy: false,
contentSecurityPolicy: {
// NOTE: Remove reportOnly when you're ready to enforce this CSP
reportOnly: true,
directives: {
'connect-src': [
MODE === 'development' ? 'ws:' : null,
process.env.SENTRY_DSN ? '*.sentry.io' : null,
'https://app.posthog.com/*',
'https://us.i.posthog.com/*',
'https://*.posthog.com',
'https://api.novu.co/v1/widgets/session/initialize',
'https://api.novu.co/v1/widgets/notifications/feed',
'https://api.novu.co/*',
'*.novu.co',
'wss://ws.novu.co/socket.io/',
"'self'",
].filter(Boolean),
'font-src': ["'self'"],
'frame-src': ["'self'"],
'img-src': ["'self'", 'data:'],
'script-src': [
"'strict-dynamic'",
"'self'",
// @ts-expect-error
(_, res) => `'nonce-${res.locals.cspNonce}'`,
],
'script-src-attr': [
// @ts-expect-error
(_, res) => `'nonce-${res.locals.cspNonce}'`,
],
'upgrade-insecure-requests': null,
},
},
}),
)
// When running tests or running in development, we want to effectively disable
// rate limiting because playwright tests are very fast and we don't want to
// have to wait for the rate limit to reset between tests.
const maxMultiple =
!IS_PROD || process.env.PLAYWRIGHT_TEST_BASE_URL ? 10_000 : 1
const rateLimitDefault = {
windowMs: 60 * 1000,
max: 1000 * maxMultiple,
standardHeaders: true,
legacyHeaders: false,
// Fly.io prevents spoofing of X-Forwarded-For
// so no need to validate the trustProxy config
validate: { trustProxy: false },
}
const strongestRateLimit = rateLimit({
...rateLimitDefault,
windowMs: 60 * 1000,
max: 10 * maxMultiple,
})
const strongRateLimit = rateLimit({
...rateLimitDefault,
windowMs: 60 * 1000,
max: 100 * maxMultiple,
})
const generalRateLimit = rateLimit(rateLimitDefault)
app.use((req, res, next) => {
const strongPaths = [
'/login',
'/signup',
'/verify',
'/admin',
'/onboarding',
'/reset-password',
'/settings/profile',
'/resources/login',
'/resources/verify',
]
if (req.method !== 'GET' && req.method !== 'HEAD') {
if (strongPaths.some(p => req.path.includes(p))) {
return strongestRateLimit(req, res, next)
}
return strongRateLimit(req, res, next)
}
// the verify route is a special case because it's a GET route that
// can have a token in the query string
if (req.path.includes('/verify')) {
return strongestRateLimit(req, res, next)
}
return generalRateLimit(req, res, next)
})
const desiredPort = Number(process.env.PORT || 3000)
const portToUse = await getPort({
port: portNumbers(desiredPort, desiredPort + 100),
})
const portAvailable = desiredPort === portToUse
if (!portAvailable && !IS_DEV) {
console.log(`⚠️ Port ${desiredPort} is not available.`)
process.exit(1)
}
const server = app.listen(portToUse, () => {
if (!portAvailable) {
console.warn(
chalk.yellow(
`⚠️ Port ${desiredPort} is not available, using ${portToUse} instead.`,
),
)
}
console.log(`🚀 We have liftoff!`)
})
closeWithGrace(async () => {
await new Promise((resolve, reject) => {
server.close(e => (e ? reject(e) : resolve('ok')))
})
})
},
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment