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

Commit 7ecad75

Browse files
vbudhramvladikoff
authored andcommitted
feat(emails): Add secondary emails api support Part 2 (#1768) r=vladikoff
1 parent 82bd9b5 commit 7ecad75

38 files changed

+2443
-204
lines changed

config/index.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -748,6 +748,14 @@ var conf = convict({
748748
format: Number,
749749
env: 'SMS_THROTTLE_WAIT_TIME'
750750
}
751+
},
752+
secondaryEmail: {
753+
enabled: {
754+
doc: 'Indicates whether secondary email APIs are enabled',
755+
default: false,
756+
format: Boolean,
757+
env: 'SECONDARY_EMAIL_ENABLED'
758+
}
751759
}
752760
})
753761

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

767775
// derive fxa-auth-mailer configuration from our content-server url
776+
conf.set('smtp.accountSettingsUrl', conf.get('contentServer.url') + '/settings')
768777
conf.set('smtp.verificationUrl', conf.get('contentServer.url') + '/verify_email')
769778
conf.set('smtp.passwordResetUrl', conf.get('contentServer.url') + '/complete_reset_password')
770779
conf.set('smtp.initiatePasswordResetUrl', conf.get('contentServer.url') + '/reset_password')

docs/api.md

Lines changed: 130 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,12 @@ The currently-defined error responses are:
9595
* status code 400, errno 133: email sent complaint
9696
* status code 400, errno 134: email hard bounced
9797
* status code 400, errno 135: email soft bounced
98+
* status code 400, errno 136: email already exists
99+
* status code 400, errno 137: can not delete primary email
100+
* status code 400, errno 138: can not add email with unverified session
101+
* status code 400, errno 139: can not add email that is the same as your primary email
102+
* status code 400, errno 140: verified primary email already exists
103+
* status code 400, errno 141: newly created unverified primary email exists
98104
* status code 503, errno 201: service temporarily unavailable to due high load (see [backoff protocol](#backoff-protocol))
99105
* status code 503, errno 202: feature has been disabled for operational reasons
100106
* any status code, errno 999: unknown error
@@ -144,6 +150,9 @@ Since this is a HTTP-based protocol, clients should be prepared to gracefully ha
144150
* [GET /v1/recovery_email/status (:lock: sessionToken)](#get-v1recovery_emailstatus)
145151
* [POST /v1/recovery_email/resend_code (:lock: sessionToken)](#post-v1recovery_emailresend_code)
146152
* [POST /v1/recovery_email/verify_code](#post-v1recovery_emailverify_code)
153+
* [GET /v1/recovery_emails (:lock: sessionToken)](#get-v1recovery_emails)
154+
* [POST /v1/recovery_email (:lock: sessionToken)](#post-v1recovery_email)
155+
* [POST /v1/recovery_email/destroy (:lock: sessionToken)](#post-v1recovery_emaildestroy)
147156

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

856865
Not HAWK-authenticated.
857866

858-
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.
867+
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.
859868

860869
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.
861870

@@ -865,6 +874,9 @@ ___Parameters___
865874

866875
* uid - account identifier
867876
* code - the verification code (recovery email or token verification id)
877+
* service - the service issuing request
878+
* reminder - (optional) the reminder email associated with code
879+
* type - (optional) the type of code being verified
868880

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

911+
## GET /v1/recovery_emails
912+
913+
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.
914+
915+
### Request
916+
917+
```sh
918+
curl -v \
919+
-X GET \
920+
-H "Host: api-accounts.dev.lcip.org" \
921+
-H "Content-Type: application/json" \
922+
-H 'Authorization: Hawk id="d4c5b1e3f5791ef83896c27519979b93a45e6d0da34c7509c5632ac35b28b48d", ts="1373391043", nonce="ohQjqb", hash="vBODPWhDhiRWM4tmI9qp+np+3aoqEFzdGuGk0h7bh9w=", mac="LAnpP3P2PXelC6hUoUaHP72nCqY5Iibaa3eeiGBqIIU="' \
923+
https://api-accounts.dev.lcip.org/v1/recovery_emails \
924+
```
925+
926+
### Response
927+
928+
Successful requests will produce a "200 OK" response with JSON body:
929+
930+
```json
931+
[
932+
{
933+
"isPrimary":true,
934+
"verified":true,
935+
"email":"primary@email.com"
936+
},
937+
{
938+
"isPrimary":false,
939+
"verified":false,
940+
"email":"anotherone@email.com"
941+
}
942+
]
943+
```
944+
945+
## POST /v1/recovery_email
946+
947+
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.
948+
949+
### Request
950+
951+
___Parameters___
952+
953+
* email - email address to add to account
954+
955+
```sh
956+
curl -v \
957+
-X POST \
958+
-H "Host: api-accounts.dev.lcip.org" \
959+
-H "Content-Type: application/json" \
960+
-H 'Authorization: Hawk id="d4c5b1e3f5791ef83896c27519979b93a45e6d0da34c7509c5632ac35b28b48d", ts="1373391043", nonce="ohQjqb", hash="vBODPWhDhiRWM4tmI9qp+np+3aoqEFzdGuGk0h7bh9w=", mac="LAnpP3P2PXelC6hUoUaHP72nCqY5Iibaa3eeiGBqIIU="' \
961+
https://api-accounts.dev.lcip.org/v1/recovery_email \
962+
-d '{
963+
"email": "another@email.com"
964+
}'
965+
```
966+
967+
### Response
968+
969+
Successful requests will produce a "200 OK" response with an empty JSON body:
970+
971+
```json
972+
{}
973+
```
974+
975+
Failing requests may be due to the following errors:
976+
977+
* status code 400, errno 102: attempt to access an account that does not exist
978+
* status code 400, errno 105: invalid verification code
979+
* status code 400, errno 106: request body was not valid json
980+
* status code 400, errno 107: request body contains invalid parameters
981+
* status code 400, errno 108: request body missing required parameters
982+
* status code 411, errno 112: content-length header was not provided
983+
* status code 413, errno 113: request body too large
984+
* status code 400, errno 138: session is not verified
985+
* status code 400, errno 139: cannot add your primary email address
986+
* status code 400, errno 140: verified primary email address exist
987+
* status code 400, errno 141: newly unverified primary account email exist
988+
989+
## POST /v1/recovery_email/destroy
990+
991+
This endpoint is used to delete an email address from the logged in user.
992+
993+
### Request
994+
995+
___Parameters___
996+
997+
* email - email address to add to account
998+
999+
```sh
1000+
curl -v \
1001+
-X POST \
1002+
-H "Host: api-accounts.dev.lcip.org" \
1003+
-H "Content-Type: application/json" \
1004+
-H 'Authorization: Hawk id="d4c5b1e3f5791ef83896c27519979b93a45e6d0da34c7509c5632ac35b28b48d", ts="1373391043", nonce="ohQjqb", hash="vBODPWhDhiRWM4tmI9qp+np+3aoqEFzdGuGk0h7bh9w=", mac="LAnpP3P2PXelC6hUoUaHP72nCqY5Iibaa3eeiGBqIIU="' \
1005+
https://api-accounts.dev.lcip.org/v1/recovery_email/destroy \
1006+
-d '{
1007+
"email": "another@email.com"
1008+
}'
1009+
```
1010+
1011+
### Response
1012+
1013+
Successful requests will produce a "200 OK" response with an empty JSON body:
1014+
1015+
```json
1016+
{}
1017+
```
1018+
1019+
Failing requests may be due to the following errors:
1020+
1021+
* status code 400, errno 102: attempt to access an account that does not exist
1022+
* status code 400, errno 105: invalid verification code
1023+
* status code 400, errno 106: request body was not valid json
1024+
* status code 400, errno 107: request body contains invalid parameters
1025+
* status code 400, errno 108: request body missing required parameters
1026+
* status code 411, errno 112: content-length header was not provided
1027+
* status code 413, errno 113: request body too large
8991028

9001029
## POST /v1/certificate/sign
9011030

lib/db.js

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -685,9 +685,13 @@ module.exports = (
685685
)
686686
}
687687

688-
DB.prototype.verifyEmail = function (account) {
689-
log.trace({ op: 'DB.verifyEmail', uid: account && account.uid })
690-
return this.pool.post('/account/' + account.uid.toString('hex') + '/verifyEmail/' + account.emailCode.toString('hex'))
688+
DB.prototype.verifyEmail = function (account, emailCode) {
689+
log.trace({
690+
op: 'DB.verifyEmail',
691+
uid: account && account.uid,
692+
emailCode: emailCode
693+
})
694+
return this.pool.post('/account/' + account.uid.toString('hex') + '/verifyEmail/' + emailCode.toString('hex'))
691695
}
692696

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

850+
DB.prototype.accountEmails = function (uid) {
851+
log.trace({
852+
op: 'DB.accountEmails',
853+
uid: uid
854+
})
855+
856+
return this.pool.get('/account/' + uid.toString('hex') + '/emails')
857+
}
858+
859+
DB.prototype.createEmail = function (uid, emailData) {
860+
log.trace({
861+
email: emailData.email,
862+
op: 'DB.createEmail',
863+
uid: emailData.uid
864+
})
865+
866+
return this.pool.post('/account/' + uid.toString('hex') + '/emails', emailData)
867+
.catch(
868+
function (err) {
869+
if (isEmailAlreadyExistsError(err)) {
870+
throw error.emailExists()
871+
}
872+
throw err
873+
}
874+
)
875+
}
876+
877+
DB.prototype.deleteEmail = function (uid, email) {
878+
log.trace({
879+
op: 'DB.deleteEmail',
880+
uid: uid
881+
})
882+
883+
return this.pool.del('/account/' + uid.toString('hex') + '/emails/' + email)
884+
.catch(
885+
function (err) {
886+
if (isEmailDeletePrimaryError(err)) {
887+
throw error.cannotDeletePrimaryEmail()
888+
}
889+
throw err
890+
}
891+
)
892+
}
893+
846894
function wrapTokenNotFoundError (err) {
847895
if (isNotFoundError(err)) {
848896
err = error.invalidToken('The authentication token could not be found')
@@ -867,3 +915,11 @@ function isIncorrectPasswordError (err) {
867915
function isNotFoundError (err) {
868916
return err.statusCode === 404 && err.errno === 116
869917
}
918+
919+
function isEmailAlreadyExistsError (err) {
920+
return err.statusCode === 409 && err.errno === 101
921+
}
922+
923+
function isEmailDeletePrimaryError (err) {
924+
return err.statusCode === 400 && err.errno === 136
925+
}

lib/error.js

Lines changed: 77 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,17 @@ var ERRNO = {
3939
BOUNCE_COMPLAINT: 133,
4040
BOUNCE_HARD: 134,
4141
BOUNCE_SOFT: 135,
42+
EMAIL_EXISTS: 136,
43+
EMAIL_DELETE_PRIMARY: 137,
44+
SESSION_UNVERIFIED: 138,
45+
USER_PRIMARY_EMAIL_EXISTS: 139,
46+
VERIFIED_PRIMARY_EMAIL_EXISTS: 140,
47+
48+
// If there exists an account that was created under 24hrs and
49+
// has not verified their email address, this error is thrown
50+
// if another user attempts to add that email to their account
51+
// as a secondary email.
52+
UNVERIFIED_PRIMARY_EMAIL_NEWLY_CREATED: 141,
4253
SERVER_BUSY: 201,
4354
FEATURE_NOT_ENABLED: 202,
4455
UNEXPECTED_ERROR: 999
@@ -99,9 +110,9 @@ AppError.prototype.backtrace = function (traced) {
99110
this.output.payload.log = traced
100111
}
101112

102-
/*/
113+
/**
103114
Translates an error from Hapi format to our format
104-
/*/
115+
*/
105116
AppError.translate = function (response) {
106117
var error
107118
if (response instanceof AppError) {
@@ -512,6 +523,18 @@ AppError.invalidMessageId = () => {
512523
})
513524
}
514525

526+
AppError.messageRejected = (reason, reasonCode) => {
527+
return new AppError({
528+
code: 500,
529+
error: 'Bad Request',
530+
errno: ERRNO.MESSAGE_REJECTED,
531+
message: 'Message rejected'
532+
}, {
533+
reason,
534+
reasonCode
535+
})
536+
}
537+
515538
AppError.emailComplaint = () => {
516539
return new AppError({
517540
code: 400,
@@ -539,15 +562,60 @@ AppError.emailBouncedSoft = () => {
539562
})
540563
}
541564

542-
AppError.messageRejected = (reason, reasonCode) => {
565+
AppError.emailExists = () => {
543566
return new AppError({
544-
code: 500,
567+
code: 400,
545568
error: 'Bad Request',
546-
errno: ERRNO.MESSAGE_REJECTED,
547-
message: 'Message rejected'
548-
}, {
549-
reason,
550-
reasonCode
569+
errno: ERRNO.EMAIL_EXISTS,
570+
message: 'Email already exists'
571+
})
572+
}
573+
574+
AppError.cannotDeletePrimaryEmail = () => {
575+
return new AppError({
576+
code: 400,
577+
error: 'Bad Request',
578+
errno: ERRNO.EMAIL_DELETE_PRIMARY,
579+
message: 'Can not delete primary email'
580+
})
581+
}
582+
583+
AppError.unverifiedSession = function () {
584+
return new AppError({
585+
code: 400,
586+
error: 'Bad Request',
587+
errno: ERRNO.SESSION_UNVERIFIED,
588+
message: 'Unverified session'
589+
})
590+
}
591+
592+
AppError.yourPrimaryEmailExists = () => {
593+
return new AppError({
594+
code: 400,
595+
error: 'Bad Request',
596+
errno: ERRNO.USER_PRIMARY_EMAIL_EXISTS,
597+
message: 'Can not add secondary email that is same as your primary'
598+
})
599+
}
600+
601+
AppError.verifiedPrimaryEmailAlreadyExists = () => {
602+
return new AppError({
603+
code: 400,
604+
error: 'Bad Request',
605+
errno: ERRNO.VERIFIED_PRIMARY_EMAIL_EXISTS,
606+
message: 'Email already exists'
607+
})
608+
}
609+
610+
// This error is thrown when someone attempts to add a secondary email
611+
// that is the same as the primary email of another account, but the account
612+
// was recently created ( < 24hrs).
613+
AppError.unverifiedPrimaryEmailNewlyCreated = () => {
614+
return new AppError({
615+
code: 400,
616+
error: 'Bad Request',
617+
errno: ERRNO.UNVERIFIED_PRIMARY_EMAIL_NEWLY_CREATED,
618+
message: 'Email already exists'
551619
})
552620
}
553621

0 commit comments

Comments
 (0)