blurts-server/middleware.js

267 строки
7.5 KiB
JavaScript

'use strict'
const { URLSearchParams } = require('url')
const { negotiateLanguages, acceptedLanguages } = require('fluent-langneg')
const AppConstants = require('./app-constants')
const DB = require('./db/DB')
const { FXA } = require('./lib/fxa')
const { FluentError } = require('./locale-utils')
const mozlog = require('./log')
const HIBP = require('./hibp')
const log = mozlog('middleware')
// adds the request object to a res.local var
function addRequestToResponse (req, res, next) {
res.locals.req = req
next()
}
// picks available language by Accept-Language and assigns to request
function pickLanguage (req, res, next) {
res.vary('Accept-Language')
const requestedLanguage = acceptedLanguages(req.headers['accept-language'])
const supportedLocales = negotiateLanguages(
requestedLanguage,
req.app.locals.AVAILABLE_LANGUAGES,
{ defaultLocale: 'en' }
)
req.supportedLocales = supportedLocales
req.fluentFormat = (id, args = null, errors = null) => {
for (const locale of supportedLocales) {
const bundle = req.app.locals.FLUENT_BUNDLES[locale]
if (bundle.hasMessage(id)) {
const message = bundle.getMessage(id)
return bundle.format(message, args)
}
}
return id
}
next()
}
async function recordVisitFromEmail (req, res, next) {
if (req.query.utm_source && req.query.utm_source !== 'fx-monitor') {
next()
return
}
if (req.query.utm_medium && req.query.utm_medium !== 'email') {
next()
return
}
const breachDetailsRE = /breach-details\/(\w*)$/
const capturedMatch = req.path.match(breachDetailsRE)
// Send engagement ping to FXA if req.query contains a valid subscriber ID
if (req.query.subscriber_id && Number.isInteger(Number(req.query.subscriber_id))) {
const subscriber = await DB.getSubscriberById(req.query.subscriber_id)
if (!subscriber.fxa_uid || subscriber.fxa_uid === '') {
next()
return
}
const utmContent = (capturedMatch) ? `&utm_content=${capturedMatch[1]}` : ''
const fxaMetricsFlowPath = `metrics-flow?utm_source=${req.query.utm_source}&utm_medium=${req.query.utm_medium}${utmContent}&event_type=engage&uid=${subscriber.fxa_uid}&service=${AppConstants.OAUTH_CLIENT_ID}`
const fxaResult = await FXA.sendMetricsFlowPing(fxaMetricsFlowPath)
log.info(`fxaResult: ${fxaResult}`)
}
// If user is already signed in, proceed
if (req.session.user) {
next()
return
}
// If user is returning from FXA sign-in, proceed
if (req.path === '/oauth/confirmed') {
next()
return
}
// Redirect users who have clicked "Go to Dashboard" from an email and aren't signed in to the /oauth flow.
if (
req.query.utm_campaign && req.query.utm_campaign === 'go-to-dashboard-link'
) {
const oauthUrl = new URL('/oauth/init', AppConstants.SERVER_URL);
['utm_source', 'utm_campaign', 'utm_medium'].forEach(param => {
if (req.query[param]) {
oauthUrl.searchParams.append(param, req.query[param])
}
})
req.url = `${oauthUrl.pathname}/${oauthUrl.search}`
next()
return
}
next()
}
// Helps handle errors for all async route controllers
// See https://medium.com/@Abazhenov/using-async-await-in-express-with-node-8-b8af872c0016
function asyncMiddleware (fn) {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next)
}
}
function logErrors (err, req, res, next) {
log.error('error', { stack: err.stack })
next(err)
}
function localizeErrorMessages (err, req, res, next) {
if (err instanceof FluentError) {
err.message = req.fluentFormat(err.fluentID)
err.locales = req.supportedLocales
}
next(err)
}
function clientErrorHandler (err, req, res, next) {
if (req.xhr || req.headers['content-type'] === 'application/json') {
res.status(500).send({ message: err.message })
} else {
next(err)
}
}
function errorHandler (err, req, res, next) {
res.status(500)
res.render('subpage', {
analyticsID: 'error',
headline: req.fluentFormat('error-headline'),
subhead: err.message
})
}
async function _getRequestSessionUser (req, res, next) {
if (req.session && req.session.user) {
// make sure the user object has all subscribers and email_addresses properties
return DB.getSubscriberById(req.session.user.id)
}
return null
}
async function requireSessionUser (req, res, next) {
const user = await _getRequestSessionUser(req)
if (!user) {
const queryParams = new URLSearchParams(req.query).toString()
return res.redirect(`/oauth/init?${queryParams}`)
}
const fxaProfileData = await FXA.getProfileData(user.fxa_access_token)
if (fxaProfileData.hasOwnProperty('name') && fxaProfileData.name === 'HTTPError') {
delete req.session.user
return res.redirect('/')
}
await DB.updateFxAProfileData(user, fxaProfileData)
req.session.user = user
req.user = user
next()
}
async function requireAdminUser (req, res, next) {
const user = await _getRequestSessionUser(req)
if (!user) {
const queryParams = new URLSearchParams(req.query).toString()
return res.redirect(`/oauth/init?${queryParams}`)
}
const fxaProfileData = await FXA.getProfileData(user.fxa_access_token)
const admins = AppConstants.ADMINS?.split(',') || []
const isAdmin = admins.includes(JSON.parse(fxaProfileData).email)
const hasFxaError = Object.prototype.hasOwnProperty.call(fxaProfileData, 'name') && fxaProfileData.name
if (hasFxaError) {
delete req.session.user
}
if (!isAdmin || hasFxaError) {
return res.sendStatus(401)
}
await DB.updateFxAProfileData(user, fxaProfileData)
req.session.user = user
req.user = user
next()
}
function getShareUTMs (req, res, next) {
// Step 1: See if the user needs to be redirected to the homepage or to a breach-detail page.
const generalShareUrls = [
'/share/orange', // Header
'/share/purple', // Footer
'/share/blue', // user/dashboard
'/share/'
]
if (generalShareUrls.includes(req.url)) {
// If not breach specific, redirect to "/"
req.session.redirectHome = true
}
// If user has no reference to experiment (default), add skip override
if (typeof (req.session.experimentFlags) === 'undefined') {
req.session.experimentFlags = {
excludeFromExperiment: true
}
}
const excludedFromExperiment = (req.session.experimentFlags.excludeFromExperiment)
// Excluse user from experiment if they don't have any experimentFlags set already.
if (excludedFromExperiment) {
// Step 2: Determine if user needs to have share-link UTMs set
const colors = [
'orange', // Header
'purple', // Footer
'blue' // user/dashboard
]
const urlArray = req.url.split('/')
const color = urlArray.slice(-1)[0]
req.session.utmOverrides = {
campaignName: 'shareLinkTraffic',
campaignTerm: 'default'
}
// Set Color Var in UTM
if (color.length && colors.includes(color)) {
req.session.utmOverrides.campaignTerm = color
}
if (color.length && !colors.includes(color)) {
const allBreaches = req.app.locals.breaches
const breachName = color
const featuredBreach = HIBP.getBreachByName(allBreaches, breachName)
if (featuredBreach) {
req.session.utmOverrides.campaignTerm = featuredBreach.Name
}
}
// Exclude share users
req.session.experimentFlags = {
excludeFromExperiment: true
}
}
next()
}
module.exports = {
addRequestToResponse,
pickLanguage,
recordVisitFromEmail,
asyncMiddleware,
logErrors,
localizeErrorMessages,
clientErrorHandler,
errorHandler,
requireSessionUser,
requireAdminUser,
getShareUTMs
}