Skip to content
This repository has been archived by the owner on Apr 3, 2019. It is now read-only.

Commit

Permalink
feat(emails): Add secondary emails api support Part 2 (#1768) r=vladi…
Browse files Browse the repository at this point in the history
…koff
  • Loading branch information
vbudhram authored and vladikoff committed Apr 17, 2017
1 parent 82bd9b5 commit 7ecad75
Show file tree
Hide file tree
Showing 38 changed files with 2,443 additions and 204 deletions.
9 changes: 9 additions & 0 deletions config/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -748,6 +748,14 @@ var conf = convict({
format: Number,
env: 'SMS_THROTTLE_WAIT_TIME'
}
},
secondaryEmail: {
enabled: {
doc: 'Indicates whether secondary email APIs are enabled',
default: false,
format: Boolean,
env: 'SECONDARY_EMAIL_ENABLED'
}
}
})

Expand All @@ -765,6 +773,7 @@ conf.validate({ strict: true })
conf.set('domain', url.parse(conf.get('publicUrl')).host)

// derive fxa-auth-mailer configuration from our content-server url
conf.set('smtp.accountSettingsUrl', conf.get('contentServer.url') + '/settings')
conf.set('smtp.verificationUrl', conf.get('contentServer.url') + '/verify_email')
conf.set('smtp.passwordResetUrl', conf.get('contentServer.url') + '/complete_reset_password')
conf.set('smtp.initiatePasswordResetUrl', conf.get('contentServer.url') + '/reset_password')
Expand Down
131 changes: 130 additions & 1 deletion docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,12 @@ The currently-defined error responses are:
* status code 400, errno 133: email sent complaint
* status code 400, errno 134: email hard bounced
* status code 400, errno 135: email soft bounced
* status code 400, errno 136: email already exists
* status code 400, errno 137: can not delete primary email
* status code 400, errno 138: can not add email with unverified session
* status code 400, errno 139: can not add email that is the same as your primary email
* status code 400, errno 140: verified primary email already exists
* status code 400, errno 141: newly created unverified primary email exists
* status code 503, errno 201: service temporarily unavailable to due high load (see [backoff protocol](#backoff-protocol))
* status code 503, errno 202: feature has been disabled for operational reasons
* any status code, errno 999: unknown error
Expand Down Expand Up @@ -144,6 +150,9 @@ Since this is a HTTP-based protocol, clients should be prepared to gracefully ha
* [GET /v1/recovery_email/status (:lock: sessionToken)](#get-v1recovery_emailstatus)
* [POST /v1/recovery_email/resend_code (:lock: sessionToken)](#post-v1recovery_emailresend_code)
* [POST /v1/recovery_email/verify_code](#post-v1recovery_emailverify_code)
* [GET /v1/recovery_emails (:lock: sessionToken)](#get-v1recovery_emails)
* [POST /v1/recovery_email (:lock: sessionToken)](#post-v1recovery_email)
* [POST /v1/recovery_email/destroy (:lock: sessionToken)](#post-v1recovery_emaildestroy)

* Certificate Signing
* [POST /v1/certificate/sign (:lock: sessionToken) (verf-required)](#post-v1certificatesign)
Expand Down Expand Up @@ -855,7 +864,7 @@ Failing requests may be due to the following errors:

Not HAWK-authenticated.

This is an endpoint that is used to verify tokens and recovery emails for an account. If a valid token code is detected, the account email and tokens will be set to verified. If a valid email code is detected, the email will be marked as verified.
This is an endpoint that is used to verify tokens and additional emails for an account. If a valid token code is detected, the account email and tokens will be set to verified. If a valid email code is detected, the email will be marked as verified.

The verification code will be a random token, delivered in the fragment portion of a URL sent to the user's email address. The URL will lead to a page that extracts the code from the URL fragment, and performs a POST to `/recovery_email/verify_code`. The link can be clicked from any browser, not just the one being attached to the Firefox account.

Expand All @@ -865,6 +874,9 @@ ___Parameters___

* uid - account identifier
* code - the verification code (recovery email or token verification id)
* service - the service issuing request
* reminder - (optional) the reminder email associated with code
* type - (optional) the type of code being verified

```sh
curl -v \
Expand Down Expand Up @@ -896,6 +908,123 @@ Failing requests may be due to the following errors:
* status code 411, errno 112: content-length header was not provided
* status code 413, errno 113: request body too large

## GET /v1/recovery_emails

This endpoint is used to get all the emails associated with the logged in user. The primary email address, currently, will always be the email address on the accounts table.

### Request

```sh
curl -v \
-X GET \
-H "Host: api-accounts.dev.lcip.org" \
-H "Content-Type: application/json" \
-H 'Authorization: Hawk id="d4c5b1e3f5791ef83896c27519979b93a45e6d0da34c7509c5632ac35b28b48d", ts="1373391043", nonce="ohQjqb", hash="vBODPWhDhiRWM4tmI9qp+np+3aoqEFzdGuGk0h7bh9w=", mac="LAnpP3P2PXelC6hUoUaHP72nCqY5Iibaa3eeiGBqIIU="' \
https://api-accounts.dev.lcip.org/v1/recovery_emails \
```

### Response

Successful requests will produce a "200 OK" response with JSON body:

```json
[
{
"isPrimary":true,
"verified":true,
"email":"primary@email.com"
},
{
"isPrimary":false,
"verified":false,
"email":"anotherone@email.com"
}
]
```

## POST /v1/recovery_email

This endpoint is used add a secondary email address to the logged in user account. The address is created `unverified` and marked as not the primary address.

### Request

___Parameters___

* email - email address to add to account

```sh
curl -v \
-X POST \
-H "Host: api-accounts.dev.lcip.org" \
-H "Content-Type: application/json" \
-H 'Authorization: Hawk id="d4c5b1e3f5791ef83896c27519979b93a45e6d0da34c7509c5632ac35b28b48d", ts="1373391043", nonce="ohQjqb", hash="vBODPWhDhiRWM4tmI9qp+np+3aoqEFzdGuGk0h7bh9w=", mac="LAnpP3P2PXelC6hUoUaHP72nCqY5Iibaa3eeiGBqIIU="' \
https://api-accounts.dev.lcip.org/v1/recovery_email \
-d '{
"email": "another@email.com"
}'
```

### Response

Successful requests will produce a "200 OK" response with an empty JSON body:

```json
{}
```

Failing requests may be due to the following errors:

* status code 400, errno 102: attempt to access an account that does not exist
* status code 400, errno 105: invalid verification code
* status code 400, errno 106: request body was not valid json
* status code 400, errno 107: request body contains invalid parameters
* status code 400, errno 108: request body missing required parameters
* status code 411, errno 112: content-length header was not provided
* status code 413, errno 113: request body too large
* status code 400, errno 138: session is not verified
* status code 400, errno 139: cannot add your primary email address
* status code 400, errno 140: verified primary email address exist
* status code 400, errno 141: newly unverified primary account email exist

## POST /v1/recovery_email/destroy

This endpoint is used to delete an email address from the logged in user.

### Request

___Parameters___

* email - email address to add to account

```sh
curl -v \
-X POST \
-H "Host: api-accounts.dev.lcip.org" \
-H "Content-Type: application/json" \
-H 'Authorization: Hawk id="d4c5b1e3f5791ef83896c27519979b93a45e6d0da34c7509c5632ac35b28b48d", ts="1373391043", nonce="ohQjqb", hash="vBODPWhDhiRWM4tmI9qp+np+3aoqEFzdGuGk0h7bh9w=", mac="LAnpP3P2PXelC6hUoUaHP72nCqY5Iibaa3eeiGBqIIU="' \
https://api-accounts.dev.lcip.org/v1/recovery_email/destroy \
-d '{
"email": "another@email.com"
}'
```

### Response

Successful requests will produce a "200 OK" response with an empty JSON body:

```json
{}
```

Failing requests may be due to the following errors:

* status code 400, errno 102: attempt to access an account that does not exist
* status code 400, errno 105: invalid verification code
* status code 400, errno 106: request body was not valid json
* status code 400, errno 107: request body contains invalid parameters
* status code 400, errno 108: request body missing required parameters
* status code 411, errno 112: content-length header was not provided
* status code 413, errno 113: request body too large

## POST /v1/certificate/sign

Expand Down
62 changes: 59 additions & 3 deletions lib/db.js
Original file line number Diff line number Diff line change
Expand Up @@ -685,9 +685,13 @@ module.exports = (
)
}

DB.prototype.verifyEmail = function (account) {
log.trace({ op: 'DB.verifyEmail', uid: account && account.uid })
return this.pool.post('/account/' + account.uid.toString('hex') + '/verifyEmail/' + account.emailCode.toString('hex'))
DB.prototype.verifyEmail = function (account, emailCode) {
log.trace({
op: 'DB.verifyEmail',
uid: account && account.uid,
emailCode: emailCode
})
return this.pool.post('/account/' + account.uid.toString('hex') + '/verifyEmail/' + emailCode.toString('hex'))
}

DB.prototype.verifyTokens = function (tokenVerificationId, accountData) {
Expand Down Expand Up @@ -843,6 +847,50 @@ module.exports = (
return this.pool.get('/emailBounces/' + Buffer(email, 'utf8').toString('hex'))
}

DB.prototype.accountEmails = function (uid) {
log.trace({
op: 'DB.accountEmails',
uid: uid
})

return this.pool.get('/account/' + uid.toString('hex') + '/emails')
}

DB.prototype.createEmail = function (uid, emailData) {
log.trace({
email: emailData.email,
op: 'DB.createEmail',
uid: emailData.uid
})

return this.pool.post('/account/' + uid.toString('hex') + '/emails', emailData)
.catch(
function (err) {
if (isEmailAlreadyExistsError(err)) {
throw error.emailExists()
}
throw err
}
)
}

DB.prototype.deleteEmail = function (uid, email) {
log.trace({
op: 'DB.deleteEmail',
uid: uid
})

return this.pool.del('/account/' + uid.toString('hex') + '/emails/' + email)
.catch(
function (err) {
if (isEmailDeletePrimaryError(err)) {
throw error.cannotDeletePrimaryEmail()
}
throw err
}
)
}

function wrapTokenNotFoundError (err) {
if (isNotFoundError(err)) {
err = error.invalidToken('The authentication token could not be found')
Expand All @@ -867,3 +915,11 @@ function isIncorrectPasswordError (err) {
function isNotFoundError (err) {
return err.statusCode === 404 && err.errno === 116
}

function isEmailAlreadyExistsError (err) {
return err.statusCode === 409 && err.errno === 101
}

function isEmailDeletePrimaryError (err) {
return err.statusCode === 400 && err.errno === 136
}
86 changes: 77 additions & 9 deletions lib/error.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,17 @@ var ERRNO = {
BOUNCE_COMPLAINT: 133,
BOUNCE_HARD: 134,
BOUNCE_SOFT: 135,
EMAIL_EXISTS: 136,
EMAIL_DELETE_PRIMARY: 137,
SESSION_UNVERIFIED: 138,
USER_PRIMARY_EMAIL_EXISTS: 139,
VERIFIED_PRIMARY_EMAIL_EXISTS: 140,

// If there exists an account that was created under 24hrs and
// has not verified their email address, this error is thrown
// if another user attempts to add that email to their account
// as a secondary email.
UNVERIFIED_PRIMARY_EMAIL_NEWLY_CREATED: 141,
SERVER_BUSY: 201,
FEATURE_NOT_ENABLED: 202,
UNEXPECTED_ERROR: 999
Expand Down Expand Up @@ -99,9 +110,9 @@ AppError.prototype.backtrace = function (traced) {
this.output.payload.log = traced
}

/*/
/**
Translates an error from Hapi format to our format
/*/
*/
AppError.translate = function (response) {
var error
if (response instanceof AppError) {
Expand Down Expand Up @@ -512,6 +523,18 @@ AppError.invalidMessageId = () => {
})
}

AppError.messageRejected = (reason, reasonCode) => {
return new AppError({
code: 500,
error: 'Bad Request',
errno: ERRNO.MESSAGE_REJECTED,
message: 'Message rejected'
}, {
reason,
reasonCode
})
}

AppError.emailComplaint = () => {
return new AppError({
code: 400,
Expand Down Expand Up @@ -539,15 +562,60 @@ AppError.emailBouncedSoft = () => {
})
}

AppError.messageRejected = (reason, reasonCode) => {
AppError.emailExists = () => {
return new AppError({
code: 500,
code: 400,
error: 'Bad Request',
errno: ERRNO.MESSAGE_REJECTED,
message: 'Message rejected'
}, {
reason,
reasonCode
errno: ERRNO.EMAIL_EXISTS,
message: 'Email already exists'
})
}

AppError.cannotDeletePrimaryEmail = () => {
return new AppError({
code: 400,
error: 'Bad Request',
errno: ERRNO.EMAIL_DELETE_PRIMARY,
message: 'Can not delete primary email'
})
}

AppError.unverifiedSession = function () {
return new AppError({
code: 400,
error: 'Bad Request',
errno: ERRNO.SESSION_UNVERIFIED,
message: 'Unverified session'
})
}

AppError.yourPrimaryEmailExists = () => {
return new AppError({
code: 400,
error: 'Bad Request',
errno: ERRNO.USER_PRIMARY_EMAIL_EXISTS,
message: 'Can not add secondary email that is same as your primary'
})
}

AppError.verifiedPrimaryEmailAlreadyExists = () => {
return new AppError({
code: 400,
error: 'Bad Request',
errno: ERRNO.VERIFIED_PRIMARY_EMAIL_EXISTS,
message: 'Email already exists'
})
}

// This error is thrown when someone attempts to add a secondary email
// that is the same as the primary email of another account, but the account
// was recently created ( < 24hrs).
AppError.unverifiedPrimaryEmailNewlyCreated = () => {
return new AppError({
code: 400,
error: 'Bad Request',
errno: ERRNO.UNVERIFIED_PRIMARY_EMAIL_NEWLY_CREATED,
message: 'Email already exists'
})
}

Expand Down
Loading

0 comments on commit 7ecad75

Please sign in to comment.