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

mozilla / blurts-server / 3301e8fe-9868-476c-990a-ab44e51f4a2d

pending completion
3301e8fe-9868-476c-990a-ab44e51f4a2d

push

circleci

GitHub
Merge pull request #3001 from mozilla/main

282 of 1768 branches covered (15.95%)

Branch coverage included in aggregate %.

451 of 451 new or added lines in 56 files covered. (100.0%)

959 of 4670 relevant lines covered (20.54%)

3.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 helmet from 'helmet'
8
import accepts from 'accepts'
9
import { createClient } from 'redis'
10
import RedisStore from 'connect-redis'
11
import cookieParser from 'cookie-parser'
12
import rateLimit from 'express-rate-limit'
13
import Sentry from '@sentry/node'
14
import '@sentry/tracing'
15

16
import AppConstants from './appConstants.js'
17
import { localStorage } from './utils/localStorage.js'
18
import { errorHandler } from './middleware/error.js'
19
import { initFluentBundles, updateLocale, getMessageWithLocale, getMessage } from './utils/fluent.js'
20
import { loadBreachesIntoApp } from './utils/hibp.js'
21
import { RateLimitError } from './utils/error.js'
22
import { initEmail } from './utils/email.js'
23
import indexRouter from './routes/index.js'
24
import { noSearchEngineIndex } from './middleware/noSearchEngineIndex.js'
25

26
const app = express()
×
27
const isDev = AppConstants.NODE_ENV === 'dev'
×
28

29
// init sentry
30
Sentry.init({
×
31
  dsn: AppConstants.SENTRY_DSN,
32
  environment: AppConstants.NODE_ENV,
33
  debug: isDev,
34
  beforeSend (event, hint) {
35
    if (!hint.originalException.locales || hint.originalException.locales[0] === 'en') return event // return if no localization or localization is in english
×
36

37
    // try to force an english translation for the error message if localized
38
    if (hint.originalException.fluentID) {
×
39
      event.exception.values[0].value = getMessageWithLocale(hint.originalException.fluentID, 'en') || getMessage(hint.originalException.fluentID)
×
40
    }
41

42
    return event
×
43
  }
44
})
45

46
// Determine from where to serve client code/assets:
47
// Build script is triggered for `npm start` and assets are served from /dist.
48
// Build script is NOT run for `npm run dev`, assets are served from /src, and nodemon restarts server without build (faster dev).
49
const staticPath =
50
  process.env.npm_lifecycle_event === 'start' ? '../dist' : './client'
×
51

52
await initFluentBundles()
×
53

54
async function getRedisStore () {
55
  if (['', 'redis-mock'].includes(AppConstants.REDIS_URL)) {
×
56
    // allow mock redis for setups without local redis server
57
    const { redisMockClient } = await import('./utils/redisMock.js')
×
58
    return new RedisStore({ client: redisMockClient })
×
59
  }
60

61
  const redisClient = createClient({ url: AppConstants.REDIS_URL })
×
62
  // the following event handlers are currently required for Heroku server stability: https://github.com/Shopify/shopify-app-js/issues/129
63
  redisClient.on('error', err => console.error('Redis client error', err))
×
64
  redisClient.on('connect', () => console.log('Redis client is connecting'))
×
65
  redisClient.on('reconnecting', () => console.log('Redis client is reconnecting'))
×
66
  redisClient.on('ready', () => console.log('Redis client is ready'))
×
67
  await redisClient.connect().catch(console.error)
×
68
  return new RedisStore({ client: redisClient })
×
69
}
70

71
// middleware
72
app.use(
×
73
  helmet({
74
    crossOriginResourcePolicy: { policy: 'cross-origin' },
75
    crossOriginEmbedderPolicy: false
76
  })
77
)
78

79
app.use(
×
80
  Sentry.Handlers.requestHandler({
81
    request: ['headers', 'method', 'url'], // omit cookies, data, query_string
82
    user: ['id'] // omit username, email
83
  })
84
)
85

86
const imgSrc = [
×
87
  "'self'",
88
  // Support GA4 per https://developers.google.com/tag-platform/tag-manager/web/csp
89
  'https://*.google-analytics.com',
90
  'https://*.googletagmanager.com',
91
  'https://firefoxusercontent.com',
92
  'https://mozillausercontent.com/',
93
  'https://monitor.cdn.mozilla.net/'
94
]
95

96
if (AppConstants.FXA_ENABLED) {
×
97
  const fxaSrc = new URL(AppConstants.OAUTH_PROFILE_URI).origin
×
98
  imgSrc.push(fxaSrc)
×
99
}
100

101
app.use((_req, res, _next) => {
×
102
  helmet.contentSecurityPolicy({
×
103
    directives: {
104
      upgradeInsecureRequests: isDev ? null : [], // disable forced https to allow localhost on Safari
×
105
      scriptSrc: [
106
        "'self'",
107
        // Support GA4 per https://developers.google.com/tag-platform/tag-manager/web/csp
108
        'https://*.googletagmanager.com'
109
      ],
110
      imgSrc,
111
      connectSrc: [
112
        "'self'",
113
        // Support GA4 per https://developers.google.com/tag-platform/tag-manager/web/csp
114
        'https://*.google-analytics.com',
115
        'https://*.analytics.google.com',
116
        'https://*.googletagmanager.com'
117
      ]
118
    }
119
  })(_req, res, _next)
120
})
121

122
// fallback to default 'no-referrer' only when 'strict-origin-when-cross-origin' not available
123
app.use(
×
124
  helmet.referrerPolicy({
125
    policy: ['no-referrer', 'strict-origin-when-cross-origin']
126
  })
127
)
128

129
// For text/html or */* (if Accept has not been set), negotiate and store the requested language.
130
// This filter avoids running unecessary locale functions for every image/webp request, for example.
131
// Using AsyncLocalStorage avoids having to pass req context down through every function (e.g. for getMessage())
132
app.use((req, res, next) => {
×
133
  if (!['text/html', '*/*'].includes(accepts(req).types()[0])) return next()
×
134

135
  req.locale = updateLocale(accepts(req).languages())
×
136
  localStorage.run(new Map(), () => {
×
137
    localStorage.getStore().set('locale', req.locale)
×
138
    next() // call next() inside this function to pass asyncLocalStorage context to other middleware.
×
139
  })
140
})
141

142
// MNTOR-1009, 1117:
143
// Because of proxy settings, request / cookies are not persisted between calls
144
// Setting the trust proxy to high and securing the cookie allowed the cookie to persist
145
// If cookie.secure is set as true, for nodejs behind proxy, "trust proxy" needs to be set
146
app.set('trust proxy', 1)
×
147

148
// session
149
const SESSION_DURATION_HOURS = AppConstants.SESSION_DURATION_HOURS || 48
×
150
app.use(
×
151
  session({
152
    cookie: {
153
      maxAge: SESSION_DURATION_HOURS * 60 * 60 * 1000, // 48 hours
154
      rolling: true,
155
      sameSite: 'lax',
156
      secure: !isDev
157
    },
158
    resave: false,
159
    saveUninitialized: true,
160
    secret: AppConstants.COOKIE_SECRET,
161
    store: await getRedisStore()
162
  })
163
)
164

165
// Load breaches into namespaced cache
166
try {
×
167
  await loadBreachesIntoApp(app)
×
168
} catch (error) {
169
  console.error('Error loading breaches into app.locals', error)
×
170
}
171

172
app.use(noSearchEngineIndex)
×
173
app.use(express.static(staticPath))
×
174
app.use(express.json())
×
175
app.use(cookieParser(AppConstants.COOKIE_SECRET))
×
176

177
const apiLimiter = rateLimit({
×
178
  windowMs: 15 * 60 * 1000, // 15 minutes
179
  max: 100, // Limit each IP to 100 requests per `window` (here, per 15 minutes)
180
  standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
181
  legacyHeaders: false // Disable the `X-RateLimit-*` headers
182
})
183

184
app.use('/api', apiLimiter)
×
185

186
// routing
187
app.use('/', indexRouter)
×
188

189
// sentry error handler
190
app.use(Sentry.Handlers.errorHandler({
×
191
  shouldHandleError (error) {
192
    if (error instanceof RateLimitError) return true
×
193
  }
194
}))
195

196
// app error handler
197
app.use(errorHandler)
×
198

199
app.listen(AppConstants.PORT, async function () {
×
200
  console.info(`MONITOR V2: Server listening at ${this.address().port}`)
×
201
  console.info(`Static files served from ${staticPath}`)
×
202
  try {
×
203
    await initEmail()
×
204
    console.info('Email initialized')
×
205
  } catch (ex) {
206
    console.error('try-initialize-email-error', { ex })
×
207
  }
208
})
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