Permalink
Browse files

feat(api): add an endpoint for sending SMS messages

#1648

r=vbudhram
  • Loading branch information...
1 parent 22734a1 commit d35d4420ce9a13713e6dd4232fba4b7e2f3caa4a @philbooth philbooth committed on GitHub Feb 16, 2017
View
@@ -64,7 +64,7 @@ function main() {
var Server = require('../lib/server')
var server = null
- var mailer = null
+ var senders = null
var statsInterval = null
var database = null
var customs = null
@@ -74,10 +74,10 @@ function main() {
log.stat(Password.stat())
}
- require('../lib/mailer')(config, log)
+ require('../lib/senders')(config, log)
.done(
- function(m) {
- mailer = m
+ function(result) {
+ senders = result
var DB = require('../lib/db')(
config,
@@ -102,7 +102,8 @@ function main() {
serverPublicKeys,
signer,
db,
- mailer,
+ senders.email,
+ senders.sms,
Password,
config,
customs
@@ -148,7 +149,7 @@ function main() {
function () {
customs.close()
try {
- mailer.stop()
+ senders.email.stop()
} catch (e) {
// XXX: simplesmtp module may quit early and set socket to `false`, stopping it may fail
log.warn({ op: 'shutdown', message: 'Mailer client already disconnected' })
View
@@ -567,6 +567,32 @@ var conf = convict({
format: Array,
env: 'HPKP_PIN_SHA256'
}
+ },
+ sms: {
+ enabled: {
+ doc: 'Indicates whether POST /sms is enabled',
+ default: true,
+ format: Boolean,
+ env: 'SMS_ENABLED'
+ },
+ apiKey: {
+ doc: 'API key for the SMS service',
+ default: 'YOU MUST CHANGE ME',
+ format: String,
+ env: 'SMS_API_KEY'
+ },
+ apiSecret: {
+ doc: 'API secret for the SMS service',
+ default: 'YOU MUST CHANGE ME',
+ format: String,
+ env: 'SMS_API_SECRET'
+ },
+ installFirefoxLink: {
+ doc: 'Link for the installFirefox SMS template',
+ format: 'url',
+ default: 'https://mzl.la/1HOd4ec',
+ env: 'SMS_INSTALL_FIREFOX_LINK'
+ }
}
})
@@ -0,0 +1,5 @@
+[
+ [ "CA", "16474909977" ],
+ [ "GB", "Firefox" ],
+ [ "US", "15036789977" ]
+]
@@ -56,6 +56,7 @@ in a sign-in or sign-up flow:
|`email.verification.resent`|A sign-up verification email has been re-sent to a user.|
|`email.verify_code.clicked`|A user has clicked on the link in a confirmation/verification email.|
|`email.${templateName}.delivered`|An email was delivered to a user.|
+|`sms.${templateName}.sent`|An SMS message has been sent to a user's phone.|
|`account.confirmed`|Sign-in to an existing account has been confirmed via email.|
|`account.reminder`|A new account has been verified via a reminder email.|
|`account.verified`|A new account has been verified via email.|
View
@@ -32,6 +32,10 @@ var ERRNO = {
ACCOUNT_RESET: 126,
INVALID_UNBLOCK_CODE: 127,
// MISSING_TOKEN: 128,
+ INVALID_PHONE_NUMBER: 129,
+ INVALID_REGION: 130,
+ INVALID_MESSAGE_ID: 131,
+ MESSAGE_REJECTED: 132,
SERVER_BUSY: 201,
FEATURE_NOT_ENABLED: 202,
UNEXPECTED_ERROR: 999
@@ -476,5 +480,50 @@ AppError.invalidUnblockCode = function () {
})
}
+AppError.invalidPhoneNumber = () => {
+ return new AppError({
+ code: 400,
+ error: 'Bad Request',
+ errno: ERRNO.INVALID_PHONE_NUMBER,
+ message: 'Invalid phone number'
+ })
+}
+
+AppError.invalidRegion = region => {
+ return new AppError({
+ code: 400,
+ error: 'Bad Request',
+ errno: ERRNO.INVALID_REGION,
+ message: 'Invalid region'
+ }, {
+ region
+ })
+}
+
+AppError.invalidMessageId = () => {
+ return new AppError({
+ code: 400,
+ error: 'Bad Request',
+ errno: ERRNO.INVALID_MESSAGE_ID,
+ message: 'Invalid message id'
+ })
+}
+
+AppError.messageRejected = (reason, reasonCode) => {
+ return new AppError({
+ code: 500,
+ error: 'Bad Request',
+ errno: ERRNO.MESSAGE_REJECTED,
+ message: 'Message rejected'
+ }, {
+ reason,
+ reasonCode
+ })
+}
+
+AppError.unexpectedError = () => {
+ return new AppError({})
+}
+
module.exports = AppError
module.exports.ERRNO = ERRNO
@@ -54,7 +54,8 @@ const FLOW_EVENT_ROUTES = new Set([
'/account/login/send_unblock_code',
'/account/reset',
'/recovery_email/resend_code',
- '/recovery_email/verify_code'
+ '/recovery_email/verify_code',
+ '/sms'
])
const PATH_PREFIX = /^\/v1/
View
@@ -17,6 +17,7 @@ module.exports = function (
signer,
db,
mailer,
+ smsImpl,
Password,
config,
customs
@@ -59,6 +60,7 @@ module.exports = function (
)
const session = require('./session')(log, isA, error, db)
const sign = require('./sign')(log, P, isA, error, signer, db, config.domain, devices)
+ const smsRoute = require('./sms')(log, isA, error, config, customs, smsImpl)
const util = require('./util')(
log,
random,
@@ -75,6 +77,7 @@ module.exports = function (
password,
session,
sign,
+ smsRoute,
util
)
v1Routes.forEach(r => { r.path = basePath + '/v1' + r.path })
View
@@ -0,0 +1,102 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+'use strict'
+
+const PhoneNumberUtil = require('google-libphonenumber').PhoneNumberUtil
+const validators = require('./validators')
+
+const METRICS_CONTEXT_SCHEMA = require('../metrics/context').schema
+const SENDER_IDS = new Map(require('../../config/sms-sender-ids.json'))
+
+module.exports = (log, isA, error, config, customs, sms) => {
+ if (! config.sms.enabled) {
+ return []
+ }
+
+ return [
+ {
+ method: 'POST',
+ path: '/sms',
+ config: {
+ auth: {
+ strategy: 'sessionToken'
+ },
+ validate: {
+ payload: {
+ phoneNumber: isA.string().regex(validators.E164_NUMBER).required(),
+ messageId: isA.number().positive().required(),
+ metricsContext: METRICS_CONTEXT_SCHEMA
+ }
+ }
+ },
+ handler (request, reply) {
+ log.begin('sms.send', request)
+ request.validateMetricsContext()
+
+ const sessionToken = request.auth.credentials
+ const phoneNumber = request.payload.phoneNumber
+ const messageId = request.payload.messageId
+ const acceptLanguage = request.app.acceptLanguage
+
+ let phoneNumberUtil, parsedPhoneNumber
+
+ customs.check(request, sessionToken.email, 'connectDeviceSms')
+ .then(parsePhoneNumber)
+ .then(validatePhoneNumber)
+ .then(getRegionSpecificSenderId)
+ .then(sendMessage)
+ .then(logSuccess)
+ .then(createResponse)
+ .then(reply, reply)
+
+ function parsePhoneNumber () {
+ phoneNumberUtil = PhoneNumberUtil.getInstance()
+ parsedPhoneNumber = phoneNumberUtil.parse(phoneNumber)
+ }
+
+ function validatePhoneNumber () {
+ if (! phoneNumberUtil.isValidNumber(parsedPhoneNumber)) {
+ throw error.invalidPhoneNumber()
+ }
+ }
+
+ function getRegionSpecificSenderId () {
+ const region = phoneNumberUtil.getRegionCodeForNumber(parsedPhoneNumber)
+ const senderId = SENDER_IDS.get(region)
+
+ if (! senderId) {
+ throw error.invalidRegion(region)
+ }
+
+ return senderId
+ }
+
+ function sendMessage (senderId) {
+ return sms.send(phoneNumber, senderId, messageId, acceptLanguage)
+ .catch(err => {
+ if (err.status === 500) {
+ throw error.messageRejected(err.reason, err.reasonCode)
+ }
+
+ if (err.status === 400) {
+ throw error.invalidMessageId()
+ }
+
+ throw new error.unexpectedError()
+ })
+ }
+
+ function logSuccess () {
+ return request.emitMetricsEvent(`sms.${messageId}.sent`)
+ }
+
+ function createResponse () {
+ return {}
+ }
+ }
+ }
+ ]
+}
+
@@ -16,6 +16,9 @@ module.exports.URLSAFEBASE64 = /^[a-zA-Z0-9-_]*$/
module.exports.BASE_36 = /^[a-zA-Z0-9]*$/
+// Crude phone number validation. The handler code does it more thoroughly.
+exports.E164_NUMBER = /^\+[1-9]\d{1,14}$/
+
// Match display-safe unicode characters.
// We're pretty liberal with what's allowed in a unicode string,
// but we exclude the following classes of characters:
@@ -2,22 +2,26 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-var P = require('./promise')
-var createMailer = require('fxa-auth-mailer')
+'use strict'
-module.exports = function (config, log) {
- var defaultLanguage = config.i18n.defaultLanguage
+const P = require('./promise')
+const createSenders = require('fxa-auth-mailer')
- return createMailer(
+module.exports = (config, log) => {
+ const defaultLanguage = config.i18n.defaultLanguage
+
+ return createSenders(
log,
{
locales: config.i18n.supportedLanguages,
defaultLanguage: defaultLanguage,
- mail: config.smtp
+ mail: config.smtp,
+ sms: config.sms
}
)
.then(
- function (mailer) {
+ senders => {
+ const mailer = senders.email
mailer.sendVerifyCode = function (account, code, opts) {
return P.resolve(mailer.verifyEmail(
{
@@ -151,7 +155,7 @@ module.exports = function (config, log) {
}
))
}
- return mailer
+ return senders
}
)
}
Oops, something went wrong.

0 comments on commit d35d442

Please sign in to comment.