• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mozilla / blurts-server / 5a9450c3-a9ae-4aa5-b101-c6809a4d3fbe

pending completion
5a9450c3-a9ae-4aa5-b101-c6809a4d3fbe

push

circleci

GitHub
MNTOR-1143 - limit how often users can verify email addresses to 5 minutes, with generic rate limiting for all APIs (#2807)

282 of 1299 branches covered (21.71%)

Branch coverage included in aggregate %.

14 of 14 new or added lines in 3 files covered. (100.0%)

959 of 3504 relevant lines covered (27.37%)

4.44 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/app.js
1
/* This Source Code Form is subject to the terms of the Mozilla Public
2
 * License, v. 2.0. If a copy of the MPL was not distributed with this
3
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4

5
import express from 'express'
6
import session from 'express-session'
7
import connectRedis from 'connect-redis'
8
import helmet from 'helmet'
9
import accepts from 'accepts'
10
import redis from 'redis'
11
import cookieParser from 'cookie-parser'
12
import rateLimit from 'express-rate-limit'
13

14
import AppConstants from './app-constants.js'
15
import { localStorage } from './utils/local-storage.js'
16
import { errorHandler } from './middleware/error.js'
17
import { doubleCsrfProtection } from './utils/csrf.js'
18
import { initFluentBundles, updateLocale } from './utils/fluent.js'
19
import { loadBreachesIntoApp } from './utils/hibp.js'
20
import { initEmail } from './utils/email.js'
21
import indexRouter from './routes/index.js'
22

23
const app = express()
×
24
const isDev = AppConstants.NODE_ENV === 'dev'
×
25

26
// Determine from where to serve client code/assets:
27
// Build script is triggered for `npm start` and assets are served from /dist.
28
// Build script is NOT run for `npm run dev`, assets are served from /src, and nodemon restarts server without build (faster dev).
29
const staticPath =
30
  process.env.npm_lifecycle_event === 'start' ? '../dist' : './client'
×
31

32
await initFluentBundles()
×
33

34
async function getRedisStore () {
35
  const RedisStoreConstructor = connectRedis(session)
×
36
  if (['', 'redis-mock'].includes(AppConstants.REDIS_URL)) {
×
37
    const redisMock = await import('redis-mock') // for devs without local redis
×
38
    return new RedisStoreConstructor({
×
39
      client: redisMock.default.createClient()
40
    })
41
  }
42
  return new RedisStoreConstructor({
×
43
    client: redis.createClient({ url: AppConstants.REDIS_URL })
44
  })
45
}
46

47
// middleware
48
app.use(
×
49
  helmet({
50
    crossOriginEmbedderPolicy: false
51
  })
52
)
53

54
const imgSrc = [
×
55
  "'self'"
56
]
57

58
if (AppConstants.FXA_ENABLED) {
×
59
  const fxaSrc = new URL(AppConstants.OAUTH_PROFILE_URI).origin
×
60
  imgSrc.push(fxaSrc)
×
61
}
62

63
// disable forced https to allow localhost on Safari
64
app.use(
×
65
  helmet.contentSecurityPolicy({
66
    directives: {
67
      imgSrc,
68
      upgradeInsecureRequests: isDev ? null : []
×
69
    }
70
  })
71
)
72

73
// fallback to default 'no-referrer' only when 'strict-origin-when-cross-origin' not available
74
app.use(
×
75
  helmet.referrerPolicy({
76
    policy: ['no-referrer', 'strict-origin-when-cross-origin']
77
  })
78
)
79

80
// When a text/html request is received, negotiate and store the requested language
81
// Using asyncLocalStorage avoids having to pass req context down through every function (e.g. getMessage())
82
app.use((req, res, next) => {
×
83
  if (!req.headers.accept?.startsWith('text/html')) return next()
×
84

85
  localStorage.run(new Map(), () => {
×
86
    req.locale = updateLocale(accepts(req).languages())
×
87
    localStorage.getStore().set('locale', req.locale)
×
88
    next()
×
89
  })
90
})
91

92
// MNTOR-1009, 1117:
93
// Because of proxy settings, request / cookies are not persisted between calls
94
// Setting the trust proxy to high and securing the cookie allowed the cookie to persist
95
// If cookie.secure is set as true, for nodejs behind proxy, "trust proxy" needs to be set
96
app.set('trust proxy', 1)
×
97

98
// session
99
const SESSION_DURATION_HOURS = AppConstants.SESSION_DURATION_HOURS || 48
×
100
app.use(
×
101
  session({
102
    cookie: {
103
      maxAge: SESSION_DURATION_HOURS * 60 * 60 * 1000, // 48 hours
104
      rolling: true,
105
      sameSite: 'lax',
106
      secure: !isDev
107
    },
108
    resave: false,
109
    saveUninitialized: true,
110
    secret: AppConstants.COOKIE_SECRET,
111
    store: await getRedisStore()
112
  })
113
)
114

115
// Load breaches into namespaced cache
116
try {
×
117
  await loadBreachesIntoApp(app)
×
118
} catch (error) {
119
  console.error('Error loading breaches into app.locals', error)
×
120
}
121

122
app.use(express.static(staticPath))
×
123
app.use(express.json())
×
124
app.use(cookieParser(AppConstants.COOKIE_SECRET))
×
125
app.use(doubleCsrfProtection)
×
126

127
const apiLimiter = rateLimit({
×
128
  windowMs: 15 * 60 * 1000, // 15 minutes
129
  max: 100, // Limit each IP to 100 requests per `window` (here, per 15 minutes)
130
  standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
131
  legacyHeaders: false // Disable the `X-RateLimit-*` headers
132
})
133

134
app.use('/api', apiLimiter)
×
135

136
// routing
137
app.use('/', indexRouter)
×
138
app.use(errorHandler)
×
139

140
app.listen(AppConstants.PORT, async function () {
×
141
  console.info(`MONITOR V2: Server listening at ${this.address().port}`)
×
142
  console.info(`Static files served from ${staticPath}`)
×
143
  try {
×
144
    await initEmail()
×
145
    console.info('Email initialized')
×
146
  } catch (ex) {
147
    console.error('try-initialize-email-error', { ex })
×
148
  }
149
})
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc