-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapp.js
More file actions
73 lines (61 loc) · 2.76 KB
/
Copy pathapp.js
File metadata and controls
73 lines (61 loc) · 2.76 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
const path = require('path')
const express = require('express')
const cookieParser = require('cookie-parser')
const helmet = require('helmet')
const rateLimit = require('express-rate-limit')
const logger = require('morgan')
const mainRoutes = require('./routes/main')
const listRoutes = require('./routes/list')
// Drive-by spam is the main abuse vector for a public, no-account app: anyone can
// POST /l. Cap list creation per IP. ~5/hour is generous for a human organizer
// but throttles a script. Default is overridable for ops tuning.
const CREATE_RATE_LIMIT = Number(process.env.CREATE_RATE_LIMIT) || 5
const CREATE_RATE_WINDOW_MS = 60 * 60 * 1000 // 1 hour
// Build the Express app without connecting to a database or binding a port,
// so tests (supertest) and `server.js` can each wire up their own lifecycle.
function createApp() {
const app = express()
app.set('view engine', 'ejs')
app.set('views', path.join(__dirname, 'views'))
// Standard security headers. The default Content-Security-Policy forbids the
// eval htmx uses for `hx-on` handlers and the inline styles it injects for
// request indicators, so widen script/style to keep the vendored htmx working.
// 'self' still blocks third-party/CDN scripts — consistent with the build-free,
// everything-vendored stance — and every other helmet default stays on.
app.use(
helmet({
contentSecurityPolicy: {
directives: {
...helmet.contentSecurityPolicy.getDefaultDirectives(),
'script-src': ["'self'", "'unsafe-eval'"],
'style-src': ["'self'", "'unsafe-inline'"],
},
},
}),
)
app.use(express.static(path.join(__dirname, 'public')))
app.use(express.urlencoded({ extended: true }))
app.use(express.json())
// Signed cookies are how identity (member + owner tokens) will be carried.
// The secret comes from configuration, never hardcoded.
app.use(cookieParser(process.env.COOKIE_SECRET))
if (process.env.NODE_ENV !== 'test') {
app.use(logger('dev'))
}
// The limiter holds its counters in-process, so it must be built per-app (here,
// not in the shared routes module) or every test app would share one store.
// Mounted only on POST /l — creation is the spam vector; joining and editing an
// existing list you already hold the link to are not throttled. Per-IP keying
// uses req.ip; behind a reverse proxy (deploy concern, out of PRD scope) the
// app must set `trust proxy` so this keys on the real client, not the proxy.
const createListLimiter = rateLimit({
windowMs: CREATE_RATE_WINDOW_MS,
max: CREATE_RATE_LIMIT,
handler: (req, res) => res.status(429).render('429'),
})
app.post('/l', createListLimiter)
app.use('/', mainRoutes)
app.use('/l', listRoutes)
return app
}
module.exports = createApp