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

mozilla / blurts-server / #13312

pending completion
#13312

push

circleci

jswinarton
MNTOR-1385 Basic backend flow for Premium featureset

Add the backend elements necessary to run the basic Premium subscription flow.

Some important notes:

    This creates two new routes, /oauth/premium/upgrade and /oauth/premium/confirmed, which drive the flow. They are not real views but are responsible for redirects and validation.
    The whole feature is behind a feature flag. Set FXA_SUBSCRIPTION_ENABLED=1 in your .env to enable.

282 of 1774 branches covered (15.9%)

Branch coverage included in aggregate %.

22 of 22 new or added lines in 4 files covered. (100.0%)

959 of 4683 relevant lines covered (20.48%)

1.71 hits per line

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

0.0
/src/controllers/auth.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 { URL } from 'url'
6
import { randomBytes } from 'crypto'
7

8
import AppConstants from '../appConstants.js'
9
import {
10
  getSubscriberByEmail,
11
  removeFxAData,
12
  updateFxAData,
13
  updateFxAProfileData
14
} from '../db/tables/subscribers.js'
15
import { addSubscriber } from '../db/tables/emailAddresses.js'
16

17
import { getTemplate } from '../views/emails/email2022.js'
18
import {
19
  signupReportEmailPartial
20
} from '../views/emails/emailSignupReport.js'
21

22
import { getBreachesForEmail } from '../utils/hibp.js'
23
import { getMessage } from '../utils/fluent.js'
24
import { getProfileData, FxAOAuthClient, getSha1 } from '../utils/fxa.js'
25
import { getEmailCtaHref, sendEmail } from '../utils/email.js'
26
import { isSubscribed } from '../utils/subscriber.js'
27
import { UnauthorizedError } from '../utils/error.js'
28
import mozlog from '../utils/log.js'
29

30
const {
31
  FXA_SUBSCRIPTION_ENABLED,
32
  FXA_SUBSCRIPTION_PLAN_ID,
33
  FXA_SUBSCRIPTION_PRODUCT_ID,
34
  FXA_SUBSCRIPTION_URL,
35
  SERVER_URL
36
} = AppConstants
×
37

38
const log = mozlog('controllers.auth')
×
39

40
function init (req, res, next, client = FxAOAuthClient) {
×
41
  // Set a random state string in a cookie so that we can verify
42
  // the user when they're redirected back to us after auth.
43
  const state = randomBytes(40).toString('hex')
×
44
  req.session.state = state
×
45
  const url = new URL(client.code.getUri({ state }))
×
46
  const fxaParams = new URL(req.url, SERVER_URL)
×
47

48
  req.session.utmContents = {}
×
49
  url.searchParams.append('prompt', 'login')
×
50
  url.searchParams.append('max_age', 0)
×
51
  url.searchParams.append('access_type', 'offline')
×
52
  url.searchParams.append('action', 'email')
×
53

54
  for (const param of fxaParams.searchParams.keys()) {
×
55
    url.searchParams.append(param, fxaParams.searchParams.get(param))
×
56
  }
57

58
  res.redirect(url)
×
59
}
60

61
async function confirmed (req, res, next, client = FxAOAuthClient) {
×
62
  if (!req.session.state) {
×
63
    log.error('oauth-invalid-session', 'req.session.state missing')
×
64
    throw new UnauthorizedError(getMessage('oauth-invalid-session'))
×
65
  }
66

67
  if (req.session.state !== req.query.state) {
×
68
    log.error('oauth-invalid-session', 'req.session does not match req.query')
×
69
    throw new UnauthorizedError(getMessage('oauth-invalid-session'))
×
70
  }
71

72
  const fxaUser = await client.code.getToken(req.originalUrl, {
×
73
    state: req.session.state
74
  })
75
  // Clear the session.state to clean up and avoid any replays
76
  req.session.state = null
×
77
  log.debug('fxa-confirmed-fxaUser', fxaUser)
×
78
  const fxaProfileData = await getProfileData(fxaUser.accessToken)
×
79
  log.debug('fxa-confirmed-profile-data', fxaProfileData)
×
80
  const email = JSON.parse(fxaProfileData).email
×
81

82
  const existingUser = await getSubscriberByEmail(email)
×
83
  req.session.user = existingUser
×
84

85
  const returnURL = new URL('user/breaches', SERVER_URL)
×
86
  const originalURL = new URL(req.originalUrl, SERVER_URL)
×
87

88
  for (const [key, value] of originalURL.searchParams.entries()) {
×
89
    if (key.startsWith('utm_')) returnURL.searchParams.append(key, value)
×
90
  }
91

92
  // Check if user is signing up or signing in,
93
  // then add new users to db and send email.
94
  if (!existingUser) {
×
95
    // req.session.newUser determines whether or not we show `fxa_new_user_bar`
96
    // in template
97
    req.session.newUser = true
×
98
    const signupLanguage = req.locale
×
99
    const verifiedSubscriber = await addSubscriber(
×
100
      email,
101
      signupLanguage,
102
      fxaUser.accessToken,
103
      fxaUser.refreshToken,
104
      fxaProfileData
105
    )
106

107
    // Get breaches for email the user signed-up with
108
    const allBreaches = req.app.locals.breaches
×
109
    const unsafeBreachesForEmail = await getBreachesForEmail(
×
110
      getSha1(email),
111
      allBreaches,
112
      true
113
    )
114

115
    // Send report email
116
    const utmCampaignId = 'report'
×
117
    const subject = unsafeBreachesForEmail?.length
×
118
      ? getMessage('email-subject-found-breaches')
119
      : getMessage('email-subject-no-breaches')
120

121
    const data = {
×
122
      breachedEmail: email,
123
      breachLogos: req.app.locals.breachLogoMap,
124
      ctaHref: getEmailCtaHref(utmCampaignId, 'dashboard-cta'),
125
      heading: getMessage('email-breach-summary'),
126
      recipientEmail: email,
127
      subscriberId: verifiedSubscriber,
128
      unsafeBreachesForEmail,
129
      utmCampaign: utmCampaignId
130
    }
131
    const emailTemplate = getTemplate(data, signupReportEmailPartial)
×
132

133
    await sendEmail(data.recipientEmail, subject, emailTemplate)
×
134

135
    req.session.user = verifiedSubscriber
×
136

137
    return res.redirect(returnURL.pathname + returnURL.search)
×
138
  }
139
  // Update existing user's FxA data
140
  const { accessToken, refreshToken } = fxaUser
×
141
  await updateFxAData(existingUser, accessToken, refreshToken, fxaProfileData)
×
142

143
  res.redirect(returnURL.pathname + returnURL.search)
×
144
}
145

146
/**
147
 * Controller to trigger a logout for user
148
 *
149
 * @param {object} req Contains session.user
150
 * @param {object} res Redirects to homepage
151
 */
152
async function logout (req, res) {
153
  const subscriber = req.session?.user
×
154
  log.info('logout', subscriber?.primary_email)
×
155

156
  // delete oauth session info in database
157
  await removeFxAData(subscriber)
×
158

159
  // clear session cache
160
  req.session.destroy(s => {
×
161
    delete req.session
×
162
    res.redirect('/')
×
163
  })
164
}
165

166
/**
167
 * Controller that initiates the premium upgrade flow.
168
 *
169
 * Redirects to subplat. If the user is already subscribed, they are redirected
170
 * back to the dashboard.
171
 *
172
 * @param {object} req The express request object
173
 * @param {object} res The express response object
174
 */
175
async function premiumUpgrade (req, res) {
176
  if (!FXA_SUBSCRIPTION_ENABLED) {
×
177
    return res.sendStatus(404)
×
178
  }
179

180
  if (
×
181
    isSubscribed(req.user.fxa_profile_json)) {
182
    return res.redirect('/user/breaches')
×
183
  }
184

185
  const subscribeUrl = `${FXA_SUBSCRIPTION_URL}/${FXA_SUBSCRIPTION_PRODUCT_ID}?plan=${FXA_SUBSCRIPTION_PLAN_ID}`
×
186

187
  res.redirect(subscribeUrl)
×
188
}
189

190
/**
191
 * Controller that completes the premium upgrade flow.
192
 *
193
 * Pulls down updated profile data from FxA (which should now contain a
194
 * subscription) and redirects the user back to the dashboard.
195
 *
196
 * @param {object} req The express request object
197
 * @param {object} res The express response object
198
 */
199
async function premiumConfirmed (req, res) {
200
  if (!FXA_SUBSCRIPTION_ENABLED) {
×
201
    return res.sendStatus(404)
×
202
  }
203

204
  const fxaProfileData = await getProfileData(req.user.fxa_access_token)
×
205
  await updateFxAProfileData(req.user, fxaProfileData)
×
206

207
  res.redirect('/user/breaches')
×
208
}
209

210
export { init, confirmed, logout, premiumUpgrade, premiumConfirmed }
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