feat(push): Add views for confirming a push notification login

This commit is contained in:
Vijay Budhram 2021-06-10 13:03:17 -04:00
Родитель 334d907346
Коммит 5610838255
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: EBAEC5D86596C9EE
22 изменённых файлов: 718 добавлений и 144 удалений

1
.gitignore поставляемый
Просмотреть файл

@ -34,6 +34,7 @@ secrets.json
storybooks-publish storybooks-publish
tsconfig.tsbuildinfo tsconfig.tsbuildinfo
tailwind.out.* tailwind.out.*
.idea
# Dependencies # Dependencies
**/node_modules **/node_modules

Просмотреть файл

@ -1238,4 +1238,8 @@ export default class AuthClient {
new Headers(headers) new Headers(headers)
); );
} }
async sendPushLoginRequest(sessionToken: string) {
return this.sessionPost('/session/verify/send_push', sessionToken, {});
}
} }

Просмотреть файл

@ -111,8 +111,7 @@ const conf = convict({
}, },
memcached: { memcached: {
address: { address: {
doc: doc: 'Address:port of the memcached server (or `none` to disable memcached)',
'Address:port of the memcached server (or `none` to disable memcached)',
default: 'localhost:11211', default: 'localhost:11211',
env: 'MEMCACHE_METRICS_CONTEXT_ADDRESS', env: 'MEMCACHE_METRICS_CONTEXT_ADDRESS',
}, },
@ -180,8 +179,7 @@ const conf = convict({
connectionTimeout: { connectionTimeout: {
default: '5 minutes', default: '5 minutes',
env: 'DB_CONNECTION_TIMEOUT', env: 'DB_CONNECTION_TIMEOUT',
doc: doc: 'Timeout in milliseconds after which the mailer will stop trying to connect to the database',
'Timeout in milliseconds after which the mailer will stop trying to connect to the database',
format: 'duration', format: 'duration',
}, },
poolee: { poolee: {
@ -246,8 +244,7 @@ const conf = convict({
env: 'EMAIL_SERVICE_PORT', env: 'EMAIL_SERVICE_PORT',
}, },
forcedEmailAddresses: { forcedEmailAddresses: {
doc: doc: 'force usage of fxa-email-service when sending emails to addresses that match this pattern',
'force usage of fxa-email-service when sending emails to addresses that match this pattern',
format: RegExp, format: RegExp,
default: /emailservice\.[A-Za-z0-9._%+-]+@restmail\.net$/, default: /emailservice\.[A-Za-z0-9._%+-]+@restmail\.net$/,
env: 'EMAIL_SERVICE_FORCE_EMAIL_REGEX', env: 'EMAIL_SERVICE_FORCE_EMAIL_REGEX',
@ -560,8 +557,7 @@ const conf = convict({
default: '100 milliseconds', default: '100 milliseconds',
env: 'REDIS_POOL_TIMEOUT', env: 'REDIS_POOL_TIMEOUT',
format: 'duration', format: 'duration',
doc: doc: 'Initial backoff for Redis connection retries, increases exponentially with each attempt',
'Initial backoff for Redis connection retries, increases exponentially with each attempt',
}, },
}, },
subhubServerMessaging: { subhubServerMessaging: {
@ -572,8 +568,7 @@ const conf = convict({
default: '', default: '',
}, },
subhubUpdatesQueueUrl: { subhubUpdatesQueueUrl: {
doc: doc: 'The queue URL to use (should include https://sqs.<region>.amazonaws.com/<account-id>/<queue-name>)',
'The queue URL to use (should include https://sqs.<region>.amazonaws.com/<account-id>/<queue-name>)',
format: String, format: String,
env: 'SUBHUB_QUEUE_URL', env: 'SUBHUB_QUEUE_URL',
default: '', default: '',
@ -596,8 +591,7 @@ const conf = convict({
default: '15 minutes', default: '15 minutes',
}, },
sessionTokenWithoutDevice: { sessionTokenWithoutDevice: {
doc: doc: 'Maximum age for session tokens without a device record, specify zero to disable',
'Maximum age for session tokens without a device record, specify zero to disable',
format: 'duration', format: 'duration',
env: 'SESSION_TOKEN_WITHOUT_DEVICE_TTL', env: 'SESSION_TOKEN_WITHOUT_DEVICE_TTL',
default: '4 weeks', default: '4 weeks',
@ -624,8 +618,7 @@ const conf = convict({
default: 1, default: 1,
}, },
snsTopicArn: { snsTopicArn: {
doc: doc: 'Amazon SNS topic on which to send account event notifications. Set to "disabled" to turn off the notifier',
'Amazon SNS topic on which to send account event notifications. Set to "disabled" to turn off the notifier',
format: String, format: String,
env: 'SNS_TOPIC_ARN', env: 'SNS_TOPIC_ARN',
default: '', default: '',
@ -638,36 +631,31 @@ const conf = convict({
}, },
emailNotifications: { emailNotifications: {
region: { region: {
doc: doc: 'The region where the queues live, most likely the same region we are sending email e.g. us-east-1, us-west-2',
'The region where the queues live, most likely the same region we are sending email e.g. us-east-1, us-west-2',
format: String, format: String,
env: 'BOUNCE_REGION', env: 'BOUNCE_REGION',
default: '', default: '',
}, },
bounceQueueUrl: { bounceQueueUrl: {
doc: doc: 'The bounce queue URL to use (should include https://sqs.<region>.amazonaws.com/<account-id>/<queue-name>)',
'The bounce queue URL to use (should include https://sqs.<region>.amazonaws.com/<account-id>/<queue-name>)',
format: String, format: String,
env: 'BOUNCE_QUEUE_URL', env: 'BOUNCE_QUEUE_URL',
default: '', default: '',
}, },
complaintQueueUrl: { complaintQueueUrl: {
doc: doc: 'The complaint queue URL to use (should include https://sqs.<region>.amazonaws.com/<account-id>/<queue-name>)',
'The complaint queue URL to use (should include https://sqs.<region>.amazonaws.com/<account-id>/<queue-name>)',
format: String, format: String,
env: 'COMPLAINT_QUEUE_URL', env: 'COMPLAINT_QUEUE_URL',
default: '', default: '',
}, },
deliveryQueueUrl: { deliveryQueueUrl: {
doc: doc: 'The email delivery queue URL to use (should include https://sqs.<region>.amazonaws.com/<account-id>/<queue-name>)',
'The email delivery queue URL to use (should include https://sqs.<region>.amazonaws.com/<account-id>/<queue-name>)',
format: String, format: String,
env: 'DELIVERY_QUEUE_URL', env: 'DELIVERY_QUEUE_URL',
default: '', default: '',
}, },
notificationQueueUrl: { notificationQueueUrl: {
doc: doc: 'Queue URL for notifications from fxa-email-service (eventually this will be the only email-related queue)',
'Queue URL for notifications from fxa-email-service (eventually this will be the only email-related queue)',
format: String, format: String,
env: 'NOTIFICATION_QUEUE_URL', env: 'NOTIFICATION_QUEUE_URL',
default: '', default: '',
@ -681,8 +669,7 @@ const conf = convict({
default: '', default: '',
}, },
profileUpdatesQueueUrl: { profileUpdatesQueueUrl: {
doc: doc: 'The queue URL to use (should include https://sqs.<region>.amazonaws.com/<account-id>/<queue-name>)',
'The queue URL to use (should include https://sqs.<region>.amazonaws.com/<account-id>/<queue-name>)',
format: String, format: String,
env: 'PROFILE_UPDATES_QUEUE_URL', env: 'PROFILE_UPDATES_QUEUE_URL',
default: '', default: '',
@ -697,8 +684,7 @@ const conf = convict({
}, },
secretBearerToken: { secretBearerToken: {
default: 'YOU MUST CHANGE ME', default: 'YOU MUST CHANGE ME',
doc: doc: 'Secret for server-to-server bearer token auth for fxa-profile-server',
'Secret for server-to-server bearer token auth for fxa-profile-server',
env: 'PROFILE_SERVER_AUTH_SECRET_BEARER_TOKEN', env: 'PROFILE_SERVER_AUTH_SECRET_BEARER_TOKEN',
format: 'String', format: 'String',
}, },
@ -748,8 +734,7 @@ const conf = convict({
env: 'SUBHUB_ENABLED', env: 'SUBHUB_ENABLED',
}, },
useStubs: { useStubs: {
doc: doc: 'Indicates whether to use stub methods for SubHub instead of talking to the server',
'Indicates whether to use stub methods for SubHub instead of talking to the server',
format: Boolean, format: Boolean,
default: false, default: false,
env: 'SUBHUB_USE_STUBS', env: 'SUBHUB_USE_STUBS',
@ -835,8 +820,7 @@ const conf = convict({
}, },
}, },
sharedSecret: { sharedSecret: {
doc: doc: 'Shared secret for authentication between backend subscription services',
'Shared secret for authentication between backend subscription services',
format: String, format: String,
default: 'YOU MUST CHANGE ME', default: 'YOU MUST CHANGE ME',
env: 'SUBSCRIPTIONS_SHARED_SECRET', env: 'SUBSCRIPTIONS_SHARED_SECRET',
@ -856,8 +840,7 @@ const conf = convict({
transactionalEmails: { transactionalEmails: {
// See also: https://jira.mozilla.com/browse/FXA-1148 // See also: https://jira.mozilla.com/browse/FXA-1148
enabled: { enabled: {
doc: doc: 'Indicates whether FxA sends transactional lifecycle emails for subscriptions (i.e. versus Marketing Cloud)',
'Indicates whether FxA sends transactional lifecycle emails for subscriptions (i.e. versus Marketing Cloud)',
format: Boolean, format: Boolean,
env: 'SUBSCRIPTIONS_TRANSACTIONAL_EMAILS_ENABLED', env: 'SUBSCRIPTIONS_TRANSACTIONAL_EMAILS_ENABLED',
default: false, default: false,
@ -865,8 +848,7 @@ const conf = convict({
}, },
}, },
currenciesToCountries: { currenciesToCountries: {
doc: doc: 'Mapping from ISO 4217 three-letter currency codes to list of ISO 3166-1 alpha-2 two-letter country codes: {"EUR": ["DE", "FR"], "USD": ["CA", "GB", "US" ]} Requirement for only one currency per country. Tested at runtime. Must be uppercased.',
'Mapping from ISO 4217 three-letter currency codes to list of ISO 3166-1 alpha-2 two-letter country codes: {"EUR": ["DE", "FR"], "USD": ["CA", "GB", "US" ]} Requirement for only one currency per country. Tested at runtime. Must be uppercased.',
format: Object, format: Object,
default: { default: {
USD: ['US', 'GB', 'NZ', 'MY', 'SG', 'CA', 'AS', 'GU', 'MP', 'PR', 'VI'], USD: ['US', 'GB', 'NZ', 'MY', 'SG', 'CA', 'AS', 'GU', 'MP', 'PR', 'VI'],
@ -887,8 +869,7 @@ const conf = convict({
default: false, default: false,
}, },
clientIds: { clientIds: {
doc: doc: 'Mappings from client id to service name: { "id1": "name-1", "id2": "name-2" }',
'Mappings from client id to service name: { "id1": "name-1", "id2": "name-2" }',
format: Object, format: Object,
default: {}, default: {},
env: 'OAUTH_CLIENT_IDS', env: 'OAUTH_CLIENT_IDS',
@ -902,8 +883,7 @@ const conf = convict({
// A safety switch for disabling new signins/signups from particular clients, // A safety switch for disabling new signins/signups from particular clients,
// as a hedge against unexpected client behaviour. // as a hedge against unexpected client behaviour.
disableNewConnectionsForClients: { disableNewConnectionsForClients: {
doc: doc: 'Comma-separated list of oauth client ids for which new connections should be temporarily refused',
'Comma-separated list of oauth client ids for which new connections should be temporarily refused',
env: 'OAUTH_DISABLE_NEW_CONNECTIONS_FOR_CLIENTS', env: 'OAUTH_DISABLE_NEW_CONNECTIONS_FOR_CLIENTS',
format: Array, format: Array,
default: [], default: [],
@ -935,8 +915,7 @@ const conf = convict({
default: 'megaz0rd', default: 'megaz0rd',
}, },
jwtSecretKeys: { jwtSecretKeys: {
doc: doc: 'Comma-separated list of secret keys for verifying oauth-to-auth server JWTs',
'Comma-separated list of secret keys for verifying oauth-to-auth server JWTs',
env: 'OAUTH_SERVER_SECRETS', env: 'OAUTH_SERVER_SECRETS',
format: Array, format: Array,
default: ['megaz0rd'], default: ['megaz0rd'],
@ -1011,8 +990,7 @@ const conf = convict({
}, },
}, },
authServerSecrets: { authServerSecrets: {
doc: doc: 'Comma-separated list of secret keys for verifying server-to-server JWTs',
'Comma-separated list of secret keys for verifying server-to-server JWTs',
env: 'AUTH_SERVER_SECRETS', env: 'AUTH_SERVER_SECRETS',
format: 'Array', format: 'Array',
default: [], default: [],
@ -1043,8 +1021,7 @@ const conf = convict({
}, },
clientManagement: { clientManagement: {
enabled: { enabled: {
doc: doc: 'Enable client management in OAuth server routes. Do NOT set this to true in production.',
'Enable client management in OAuth server routes. Do NOT set this to true in production.',
default: false, default: false,
format: 'Boolean', format: 'Boolean',
env: 'CLIENT_MANAGEMENT_ENABLED', env: 'CLIENT_MANAGEMENT_ENABLED',
@ -1054,15 +1031,13 @@ const conf = convict({
// This is used by oauth/db/index.js to identify pocket client ids so that it // This is used by oauth/db/index.js to identify pocket client ids so that it
// can store them separately in mysql. // can store them separately in mysql.
// It's also used for amplitude stats // It's also used for amplitude stats
doc: doc: 'Mappings from client id to service name: { "id1": "name-1", "id2": "name-2" }',
'Mappings from client id to service name: { "id1": "name-1", "id2": "name-2" }',
default: {}, default: {},
format: 'Object', format: 'Object',
env: 'OAUTH_CLIENT_IDS', env: 'OAUTH_CLIENT_IDS',
}, },
disabledClients: { disabledClients: {
doc: doc: 'Comma-separated list of client ids for which service should be temporarily refused',
'Comma-separated list of client ids for which service should be temporarily refused',
env: 'OAUTH_CLIENTS_DISABLED', env: 'OAUTH_CLIENTS_DISABLED',
format: 'Array', format: 'Array',
default: [], default: [],
@ -1106,8 +1081,7 @@ const conf = convict({
}, },
events: { events: {
enabled: { enabled: {
doc: doc: 'Whether or not config.events has to be properly set in production',
'Whether or not config.events has to be properly set in production',
default: true, default: true,
format: 'Boolean', format: 'Boolean',
env: 'EVENTS_ENABLED', env: 'EVENTS_ENABLED',
@ -1203,8 +1177,7 @@ const conf = convict({
}, },
timezone: { timezone: {
default: 'Z', default: 'Z',
doc: doc: 'The timezone configured on the MySQL server. This is used to type cast server date/time values to JavaScript `Date` object. Can be `local`, `Z`, or an offset in the form of or an offset in the form +HH:MM or -HH:MM.',
'The timezone configured on the MySQL server. This is used to type cast server date/time values to JavaScript `Date` object. Can be `local`, `Z`, or an offset in the form of or an offset in the form +HH:MM or -HH:MM.',
env: 'MYSQL_TIMEZONE', env: 'MYSQL_TIMEZONE',
format: 'String', format: 'String',
}, },
@ -1217,8 +1190,7 @@ const conf = convict({
env: 'FXA_OPENID_KEYFILE', env: 'FXA_OPENID_KEYFILE',
}, },
newKeyFile: { newKeyFile: {
doc: doc: 'Path to private key JWK that will be used to sign JWTs in the future',
'Path to private key JWK that will be used to sign JWTs in the future',
default: '', default: '',
format: 'String', format: 'String',
env: 'FXA_OPENID_NEWKEYFILE', env: 'FXA_OPENID_NEWKEYFILE',
@ -1245,8 +1217,7 @@ const conf = convict({
env: 'FXA_OPENID_OLDKEY', env: 'FXA_OPENID_OLDKEY',
}, },
unsafelyAllowMissingActiveKey: { unsafelyAllowMissingActiveKey: {
doc: doc: 'Do not error out if there is no active key; should only be used when initializing keys',
'Do not error out if there is no active key; should only be used when initializing keys',
default: false, default: false,
format: 'Boolean', format: 'Boolean',
env: 'FXA_OPENID_UNSAFELY_ALLOW_MISSING_ACTIVE_KEY', env: 'FXA_OPENID_UNSAFELY_ALLOW_MISSING_ACTIVE_KEY',
@ -1277,8 +1248,7 @@ const conf = convict({
env: 'PPID_CLIENT_IDS', env: 'PPID_CLIENT_IDS',
}, },
rotatingClientIds: { rotatingClientIds: {
doc: doc: 'client_ids that receive automatically rotating PPIDs based on server time',
'client_ids that receive automatically rotating PPIDs based on server time',
default: [], default: [],
format: 'Array', format: 'Array',
env: 'PPID_ROTATING_CLIENT_IDS', env: 'PPID_ROTATING_CLIENT_IDS',
@ -1332,8 +1302,7 @@ const conf = convict({
developerId: { doc: 'Bytes of generated developer ids', default: 16 }, developerId: { doc: 'Bytes of generated developer ids', default: 16 },
}, },
cacheControl: { cacheControl: {
doc: doc: 'Hapi: a string with the value of the "Cache-Control" header when caching is disabled',
'Hapi: a string with the value of the "Cache-Control" header when caching is disabled',
format: 'String', format: 'String',
default: 'private, no-cache, no-store, must-revalidate', default: 'private, no-cache, no-store, must-revalidate',
}, },
@ -1410,15 +1379,13 @@ const conf = convict({
}, },
signinConfirmation: { signinConfirmation: {
forcedEmailAddresses: { forcedEmailAddresses: {
doc: doc: 'Force sign-in confirmation for email addresses matching this regex.',
'Force sign-in confirmation for email addresses matching this regex.',
format: RegExp, format: RegExp,
default: /.+@mozilla\.com$/, default: /.+@mozilla\.com$/,
env: 'SIGNIN_CONFIRMATION_FORCE_EMAIL_REGEX', env: 'SIGNIN_CONFIRMATION_FORCE_EMAIL_REGEX',
}, },
skipForEmailAddresses: { skipForEmailAddresses: {
doc: doc: 'Comma separated list of email addresses that will always skip any non TOTP sign-in confirmation',
'Comma separated list of email addresses that will always skip any non TOTP sign-in confirmation',
format: Array, format: Array,
default: [], default: [],
env: 'SIGNIN_CONFIRMATION_SKIP_FOR_EMAIL_ADDRESS', env: 'SIGNIN_CONFIRMATION_SKIP_FOR_EMAIL_ADDRESS',
@ -1467,8 +1434,7 @@ const conf = convict({
securityHistory: { securityHistory: {
ipProfiling: { ipProfiling: {
allowedRecency: { allowedRecency: {
doc: doc: 'Length of time since previously verified event to allow skipping confirmation',
'Length of time since previously verified event to allow skipping confirmation',
default: '72 hours', default: '72 hours',
format: 'duration', format: 'duration',
env: 'IP_PROFILING_RECENCY', env: 'IP_PROFILING_RECENCY',
@ -1483,15 +1449,13 @@ const conf = convict({
env: 'LASTACCESSTIME_UPDATES_ENABLED', env: 'LASTACCESSTIME_UPDATES_ENABLED',
}, },
sampleRate: { sampleRate: {
doc: doc: 'sample rate for updates to the lastAccessTime session token property, in the range 0..1',
'sample rate for updates to the lastAccessTime session token property, in the range 0..1',
format: Number, format: Number,
default: 0.3, default: 0.3,
env: 'LASTACCESSTIME_UPDATES_SAMPLE_RATE', env: 'LASTACCESSTIME_UPDATES_SAMPLE_RATE',
}, },
earliestSaneTimestamp: { earliestSaneTimestamp: {
doc: doc: 'timestamp used as the basis of the fallback value for lastAccessTimeFormatted, currently pinned to the deployment of 1.96.4 / a0940d7dc51e2ba20fa18aa3a830810e35c9a9d9',
'timestamp used as the basis of the fallback value for lastAccessTimeFormatted, currently pinned to the deployment of 1.96.4 / a0940d7dc51e2ba20fa18aa3a830810e35c9a9d9',
format: 'timestamp', format: 'timestamp',
default: 1507081020000, default: 1507081020000,
env: 'LASTACCESSTIME_EARLIEST_SANE_TIMESTAMP', env: 'LASTACCESSTIME_EARLIEST_SANE_TIMESTAMP',
@ -1510,8 +1474,7 @@ const conf = convict({
env: 'SIGNIN_UNBLOCK_CODE_LIFETIME', env: 'SIGNIN_UNBLOCK_CODE_LIFETIME',
}, },
forcedEmailAddresses: { forcedEmailAddresses: {
doc: doc: 'If feature enabled, force sign-in unblock for email addresses matching this regex.',
'If feature enabled, force sign-in unblock for email addresses matching this regex.',
format: RegExp, format: RegExp,
default: /^$/, // default is no one default: /^$/, // default is no one
env: 'SIGNIN_UNBLOCK_FORCED_EMAILS', env: 'SIGNIN_UNBLOCK_FORCED_EMAILS',
@ -1522,7 +1485,8 @@ const conf = convict({
doc: 'RegExp that validates the URI format of the Push Server', doc: 'RegExp that validates the URI format of the Push Server',
format: RegExp, format: RegExp,
// eslint-disable-next-line no-useless-escape // eslint-disable-next-line no-useless-escape
default: /^https:\/\/[a-zA-Z0-9._-]+(\.services\.mozilla\.com|autopush\.dev\.mozaws\.net|autopush\.stage\.mozaws\.net)(?:\:\d+)?(\/.*)?$/, default:
/^https:\/\/[a-zA-Z0-9._-]+(\.services\.mozilla\.com|autopush\.dev\.mozaws\.net|autopush\.stage\.mozaws\.net)(?:\:\d+)?(\/.*)?$/,
}, },
}, },
pushbox: { pushbox: {
@ -1604,22 +1568,19 @@ const conf = convict({
env: 'SMS_INSTALL_FIREFOX_LINK', env: 'SMS_INSTALL_FIREFOX_LINK',
}, },
installFirefoxWithSigninCodeBaseUri: { installFirefoxWithSigninCodeBaseUri: {
doc: doc: 'Base URI for the SMS template when the signinCodes feature is active',
'Base URI for the SMS template when the signinCodes feature is active',
default: 'https://accounts.firefox.com/m', default: 'https://accounts.firefox.com/m',
format: 'url', format: 'url',
env: 'SMS_SIGNIN_CODES_BASE_URI', env: 'SMS_SIGNIN_CODES_BASE_URI',
}, },
enableBudgetChecks: { enableBudgetChecks: {
doc: doc: 'enable checks of the monthly SMS spend against the available budget',
'enable checks of the monthly SMS spend against the available budget',
default: true, default: true,
format: Boolean, format: Boolean,
env: 'SMS_ENABLE_BUDGET_CHECKS', env: 'SMS_ENABLE_BUDGET_CHECKS',
}, },
minimumCreditThresholdUSD: { minimumCreditThresholdUSD: {
doc: doc: 'The minimum amount of available credit that is necessary to enable SMS, in US dollars',
'The minimum amount of available credit that is necessary to enable SMS, in US dollars',
default: 200, default: 200,
format: 'nat', format: 'nat',
env: 'SMS_MINIMUM_CREDIT_THRESHOLD', env: 'SMS_MINIMUM_CREDIT_THRESHOLD',
@ -1631,8 +1592,7 @@ const conf = convict({
env: 'SMS_POLL_CURRENT_SPEND_INTERVAL', env: 'SMS_POLL_CURRENT_SPEND_INTERVAL',
}, },
smsType: { smsType: {
doc: doc: 'AWS.SNS.SMS.SMSType argument value. "Promotional" or "Transactional".',
'AWS.SNS.SMS.SMSType argument value. "Promotional" or "Transactional".',
default: 'Promotional', default: 'Promotional',
format: 'String', format: 'String',
env: 'SMS_AWS_SNS_SMSTYPE', env: 'SMS_AWS_SNS_SMSTYPE',
@ -1640,8 +1600,7 @@ const conf = convict({
}, },
secondaryEmail: { secondaryEmail: {
minUnverifiedAccountTime: { minUnverifiedAccountTime: {
doc: doc: 'The minimum amount of time an account can be unverified before another account can use it for secondary email',
'The minimum amount of time an account can be unverified before another account can use it for secondary email',
default: '1 day', default: '1 day',
format: 'duration', format: 'duration',
env: 'SECONDARY_EMAIL_MIN_UNVERIFIED_ACCOUNT_TIME', env: 'SECONDARY_EMAIL_MIN_UNVERIFIED_ACCOUNT_TIME',
@ -1691,8 +1650,7 @@ const conf = convict({
env: 'RECOVERY_CODE_COUNT', env: 'RECOVERY_CODE_COUNT',
}, },
notifyLowCount: { notifyLowCount: {
doc: doc: 'Notify the user when there are less than these many recovery codes',
'Notify the user when there are less than these many recovery codes',
default: 2, default: 2,
env: 'RECOVERY_CODE_NOTIFY_LOW_COUNT', env: 'RECOVERY_CODE_NOTIFY_LOW_COUNT',
}, },
@ -1712,8 +1670,7 @@ const conf = convict({
format: 'duration', format: 'duration',
}, },
secondInterval: { secondInterval: {
doc: doc: 'Time since account creation after which the second reminder is sent',
'Time since account creation after which the second reminder is sent',
default: '5 days', default: '5 days',
env: 'VERIFICATION_REMINDERS_SECOND_INTERVAL', env: 'VERIFICATION_REMINDERS_SECOND_INTERVAL',
format: 'duration', format: 'duration',
@ -1727,15 +1684,13 @@ const conf = convict({
}, },
maxConnections: { maxConnections: {
default: 10, default: 10,
doc: doc: 'Maximum connection count for the verification reminders Redis pool',
'Maximum connection count for the verification reminders Redis pool',
env: 'VERIFICATION_REMINDERS_REDIS_MAX_CONNECTIONS', env: 'VERIFICATION_REMINDERS_REDIS_MAX_CONNECTIONS',
format: 'nat', format: 'nat',
}, },
minConnections: { minConnections: {
default: 1, default: 1,
doc: doc: 'Minimum connection count for the verification reminders Redis pool',
'Minimum connection count for the verification reminders Redis pool',
env: 'VERIFICATION_REMINDERS_REDIS_MIN_CONNECTIONS', env: 'VERIFICATION_REMINDERS_REDIS_MIN_CONNECTIONS',
format: 'nat', format: 'nat',
}, },
@ -1813,15 +1768,13 @@ const conf = convict({
format: Number, format: Number,
}, },
locationStateFieldId: { locationStateFieldId: {
doc: doc: 'Zendesk support ticket custom field for the state/region of the location',
'Zendesk support ticket custom field for the state/region of the location',
default: 360026463491, default: 360026463491,
env: 'ZENDESK_LOCATION_STATE_FIELD_ID', env: 'ZENDESK_LOCATION_STATE_FIELD_ID',
format: Number, format: Number,
}, },
locationCountryFieldId: { locationCountryFieldId: {
doc: doc: 'Zendesk support ticket custom field for the country of the location',
'Zendesk support ticket custom field for the country of the location',
default: 360026463511, default: 360026463511,
env: 'ZENDESK_LOCATION_COUNTRY_FIELD_ID', env: 'ZENDESK_LOCATION_COUNTRY_FIELD_ID',
format: Number, format: Number,
@ -1833,8 +1786,7 @@ const conf = convict({
format: Number, format: Number,
}, },
appFieldId: { appFieldId: {
doc: doc: 'Zendesk support ticket custom field for product specific app or service',
'Zendesk support ticket custom field for product specific app or service',
default: 360030780972, default: 360030780972,
env: 'ZENDESK_APP_FIELD_ID', env: 'ZENDESK_APP_FIELD_ID',
format: Number, format: Number,
@ -1863,8 +1815,7 @@ const conf = convict({
supportPanel: { supportPanel: {
secretBearerToken: { secretBearerToken: {
default: 'YOU MUST CHANGE ME', default: 'YOU MUST CHANGE ME',
doc: doc: 'Shared secret to access certain endpoints. Please only use for GET. No state mutation allowed!',
'Shared secret to access certain endpoints. Please only use for GET. No state mutation allowed!',
env: 'SUPPORT_PANEL_AUTH_SECRET_BEARER_TOKEN', env: 'SUPPORT_PANEL_AUTH_SECRET_BEARER_TOKEN',
format: 'String', format: 'String',
}, },
@ -1898,6 +1849,7 @@ conf.set(
`${baseUri}/settings/two_step_authentication/replace_codes` `${baseUri}/settings/two_step_authentication/replace_codes`
); );
conf.set('smtp.verificationUrl', `${baseUri}/verify_email`); conf.set('smtp.verificationUrl', `${baseUri}/verify_email`);
conf.set('smtp.pushVerificationUrl', `${baseUri}/push/confirm_login`);
conf.set('smtp.passwordResetUrl', `${baseUri}/complete_reset_password`); conf.set('smtp.passwordResetUrl', `${baseUri}/complete_reset_password`);
conf.set('smtp.initiatePasswordResetUrl', `${baseUri}/reset_password`); conf.set('smtp.initiatePasswordResetUrl', `${baseUri}/reset_password`);
conf.set( conf.set(

Просмотреть файл

@ -447,25 +447,27 @@ module.exports = function (
const sessionToken = request.auth.credentials; const sessionToken = request.auth.credentials;
const { uid, tokenVerificationId } = sessionToken; const { uid, tokenVerificationId } = sessionToken;
const devices = await db.devices(uid); const allDevices = await db.devices(uid);
const geoData = request.app.geo; const location = request.app.geo.location || {};
const { ua } = request.app; const { ua } = request.app;
const uaInfo = { const uaInfo = {
uaBrowser: ua.browser, uaBrowser: ua.browser,
uaBrowserVersion: ua.browserVersion,
uaOS: ua.os, uaOS: ua.os,
uaOSVersion: ua.osVersion,
uaDeviceType: ua.deviceType, uaDeviceType: ua.deviceType,
uaFormFactor: ua.formFactor,
}; };
// Don't send notification to current device
const filteredDevices = allDevices.filter((d) => {
return d.sessionTokenId !== sessionToken.id;
});
const url = `${ const url = `${
config.smtp.verificationUrl config.smtp.pushVerificationUrl
}?type=push_login_verification&code=${tokenVerificationId}&ua=${encodeURIComponent( }?type=push_login_verification&code=${tokenVerificationId}&ua=${encodeURIComponent(
JSON.stringify(uaInfo) JSON.stringify(uaInfo)
)}&location=${encodeURIComponent( )}&location=${encodeURIComponent(
JSON.stringify(geoData.location) JSON.stringify(location)
)}&ip=${encodeURIComponent(request.app.clientAddress)}`; )}&ip=${encodeURIComponent(request.app.clientAddress)}`;
const options = { const options = {
@ -475,7 +477,7 @@ module.exports = function (
}; };
try { try {
await push.notifyVerifyLoginRequest(uid, devices, options); await push.notifyVerifyLoginRequest(uid, filteredDevices, options);
} catch (err) { } catch (err) {
log.error('Session.verify.send_push', { log.error('Session.verify.send_push', {
uid: uid, uid: uid,

Просмотреть файл

@ -1416,6 +1416,14 @@ FxaClientWrapper.prototype = {
); );
} }
), ),
/**
* Sends a push notification to compatible devices that can verify a login
* request
*
* @returns {Promise} resolves with response when complete.
*/
sendPushLoginRequest: createClientDelegate('sendPushLoginRequest'),
}; };
export default FxaClientWrapper; export default FxaClientWrapper;

Просмотреть файл

@ -184,6 +184,9 @@ const Router = Backbone.Router.extend({
type: VerificationReasons.SECONDARY_EMAIL_VERIFIED, type: VerificationReasons.SECONDARY_EMAIL_VERIFIED,
} }
), ),
'push/confirm_login(/)': createViewHandler('push/confirm_login'),
'push/send_login(/)': createViewHandler('push/send_login'),
'push/completed(/)': createViewHandler('push/completed'),
'primary_email_verified(/)': createViewHandler(ReadyView, { 'primary_email_verified(/)': createViewHandler(ReadyView, {
type: VerificationReasons.PRIMARY_EMAIL_VERIFIED, type: VerificationReasons.PRIMARY_EMAIL_VERIFIED,
}), }),
@ -208,11 +211,8 @@ const Router = Backbone.Router.extend({
'settings(/)': function () { 'settings(/)': function () {
// Because settings is a separate js app, we need to ensure navigating // Because settings is a separate js app, we need to ensure navigating
// from the content-server app passes along flow parameters. // from the content-server app passes along flow parameters.
const { const { deviceId, flowBeginTime, flowId } =
deviceId, this.metrics.getFlowEventMetadata();
flowBeginTime,
flowId,
} = this.metrics.getFlowEventMetadata();
const { const {
broker, broker,
@ -343,9 +343,8 @@ const Router = Backbone.Router.extend({
url = this.broker.transformLink(url); url = this.broker.transformLink(url);
if (options.replace && this._viewModelStack.length) { if (options.replace && this._viewModelStack.length) {
this._viewModelStack[this._viewModelStack.length - 1] = createViewModel( this._viewModelStack[this._viewModelStack.length - 1] =
nextViewData createViewModel(nextViewData);
);
} else { } else {
this._viewModelStack.push(createViewModel(nextViewData)); this._viewModelStack.push(createViewModel(nextViewData));
} }

Просмотреть файл

@ -1116,30 +1116,28 @@ const Account = Backbone.Model.extend(
* *
* @method set * @method set
*/ */
set: _.wrap(Backbone.Model.prototype.set, function ( set: _.wrap(
func, Backbone.Model.prototype.set,
attribute, function (func, attribute, value, options) {
value, let attributes;
options // Handle both `"key", value` and `{key: value}` -style arguments.
) { if (_.isObject(attribute)) {
let attributes; attributes = attribute;
// Handle both `"key", value` and `{key: value}` -style arguments. } else {
if (_.isObject(attribute)) { attributes = {};
attributes = attribute; attributes[attribute] = value;
} else {
attributes = {};
attributes[attribute] = value;
}
// eslint-disable-next-line no-unused-vars
for (const key in attributes) {
if (!_.contains(ALLOWED_KEYS, key)) {
throw new Error(key + ' cannot be set on an Account');
} }
}
return func.call(this, attribute, value, options); // eslint-disable-next-line no-unused-vars
}), for (const key in attributes) {
if (!_.contains(ALLOWED_KEYS, key)) {
throw new Error(key + ' cannot be set on an Account');
}
}
return func.call(this, attribute, value, options);
}
),
/** /**
* Complete a password reset * Complete a password reset
@ -1820,6 +1818,15 @@ const Account = Backbone.Model.extend(
createCadReminder() { createCadReminder() {
return this._fxaClient.createCadReminder(this.get('sessionToken')); return this._fxaClient.createCadReminder(this.get('sessionToken'));
}, },
/**
* Sends a push notification to verify a login request.
*
* @returns {Promise} resolves with response when complete.
*/
sendPushLoginRequest() {
return this._fxaClient.sendPushLoginRequest(this.get('sessionToken'));
},
}, },
{ {
ALLOWED_KEYS: ALLOWED_KEYS, ALLOWED_KEYS: ALLOWED_KEYS,

Просмотреть файл

@ -0,0 +1,14 @@
<div id="main-content" class="card pair-auth push-auth-complete">
<header>
<h1 id="push-auth-complete-header">{{#t}}Sign-in confirmed{{/t}}</h1>
</header>
<section>
<div class="error"></div>
<div class="graphic graphic-checkbox" role="img" aria-label="{{#t}}Successfully connected{{/t}}"></div>
<p class="verification-message">{{#t}}Please close this page and continue on the other device.{{/t}}</p>
</section>
</div>

Просмотреть файл

@ -0,0 +1,25 @@
<div id="main-content" class="card push-confirm-login">
<header>
<h1 id="fxa-push-confirm-login-header">{{#t}}New sign-in to Firefox{{/t}}</h1>
</header>
<section>
<div class="error"></div>
<div class="success"></div>
<form novalidate>
<p class="verification-message">{{#t}}For added security, please confirm this sign-in to begin syncing with this device:{{/t}}</p>
<div class="push-confirm-login-device">
{{{ unsafeDeviceBeingPairedHTML }}}
</div>
<div class="button-row">
<button id="submit-btn" type="submit">{{#t}}Confirm sign-in{{/t}}</button>
</div>
<p>{{#unsafeTranslate}}If you suspect that someone is trying to gain access to your account, <a id="change-password" href="/settings/change_password">please change your password.</a>{{/unsafeTranslate}}</p>
</form>
</section>
</div>

Просмотреть файл

@ -0,0 +1,21 @@
<div id="main-content" class="card push-send-login">
<header>
<h1 id="fxa-push-send-login-header">{{#t}}Authorize this sign-in{{/t}}</span></h1>
</header>
<section>
<div class="error"></div>
<div class="success"></div>
<form novalidate>
<div class="graphic graphic-send-tab-complete" role="img" aria-label="{{#t}}Send login push notification{{/t}}"></div>
<p class="verification-message">{{#t}}Check your connected Firefox devices for the sign-in confirmation tab we've sent.{{/t}}</p>
<div class="links">
<a id="resend" href="#" data-flow-event="link.create-account">{{#t}}Resend tab{{/t}}</a>
<a id="send-email" class="delayed-fadein" href="#">{{#t}}Tab not arriving? Send an email instead{{/t}}</a>
</div>
</form>
</section>
</div>

Просмотреть файл

@ -0,0 +1,22 @@
/* 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/. */
import FormView from '../form';
import Template from '../../templates/push/completed.mustache';
class CompletedPushLoginView extends FormView {
template = Template;
beforeRender() {
const account = this.getSignedInAccount();
// If no user is logged in redirect to the login page and set the `redirectTo` property
// to current url. After a user has logged in, they will be redirected back to this page.
if (account && account.isDefault()) {
this.relier.set('redirectTo', this.window.location.href);
return this.navigate('/');
}
}
}
export default CompletedPushLoginView;

Просмотреть файл

@ -0,0 +1,62 @@
/* 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/. */
import { assign } from 'underscore';
import FormView from '../form';
import preventDefaultThen from '../decorators/prevent_default_then';
import Template from '../../templates/push/confirm_login.mustache';
import DeviceBeingPairedTemplate from '../../templates/partial/device-being-paired.mustache';
import Url from '../../lib/url';
class ConfirmPushLoginView extends FormView {
template = Template;
events = assign(this.events, {
'click #change-password': preventDefaultThen('changePassword'),
});
initialize(options = {}) {
const params = Url.searchParams(this.window.location.search);
const ua = options.ua || JSON.parse(params.ua);
const location = params.location ? JSON.parse(params.location) : {};
const ip = options.ip || params.ip;
this.code = options.code || params.code;
this.deviceContext = {
family: ua.uaBrowser,
OS: ua.uaOS,
ipAddress: ip,
...location,
};
}
setInitialContext(context) {
context.set({
unsafeDeviceBeingPairedHTML: this.renderTemplate(
DeviceBeingPairedTemplate,
this.deviceContext
),
});
}
changePassword() {}
submit() {
// TODO: Unfortunately, we need to use `/recovery_email/verify_code`
// endpoint to verify our tokenVerificationCode because that code
// is linked directly to a specific session.
const account = this.getSignedInAccount();
return account
.verifySignUp(this.code)
.then(() => {
return this.navigate('/push/completed');
})
.catch((err) => {
this.displayError(err);
});
}
}
export default ConfirmPushLoginView;

Просмотреть файл

@ -0,0 +1,77 @@
/* 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/. */
import { assign } from 'underscore';
import FormView from '../form';
import preventDefaultThen from '../decorators/prevent_default_then';
import Template from '../../templates/push/send_login.mustache';
import SessionVerificationPollMixin from '../mixins/session-verification-poll-mixin';
import Cocktail from '../../lib/cocktail';
import FlowEventsMixin from '../mixins/flow-events-mixin';
const proto = FormView.prototype;
class SendPushLoginView extends FormView {
template = Template;
events = assign(this.events, {
'click #resend': preventDefaultThen('resend'),
'click #send-email': preventDefaultThen('useEmailCode'),
});
beforeRender() {
const account = this.getSignedInAccount();
return account
.sendPushLoginRequest()
.then(() => this.invokeBrokerMethod('beforeSignIn', account));
}
afterVisible() {
const account = this.getSignedInAccount();
return proto.afterVisible
.call(this)
.then(() => this.broker.persistVerificationData(account))
.then(() =>
this.invokeBrokerMethod('beforeSignUpConfirmationPoll', account)
)
.then(() => {
return this.waitForSessionVerification(account, () => {
this.logViewEvent('verification.success');
this.notifier.trigger('verification.success');
return this.invokeBrokerMethod(
'afterCompleteSignInWithCode',
account
);
});
});
}
resend() {
const account = this.getSignedInAccount();
return account
.sendPushLoginRequest()
.then(() => {
this.displaySuccess('Notification sent');
})
.catch(() => {
this.displayError('Something went wrong');
});
}
useEmailCode() {
const account = this.getSignedInAccount();
return account.verifySessionResendCode().then(() => {
return this.navigate('/signin_token_code');
});
}
}
Cocktail.mixin(
SendPushLoginView,
FlowEventsMixin,
SessionVerificationPollMixin
);
export default SendPushLoginView;

Просмотреть файл

@ -16,6 +16,8 @@ import SessionVerificationPollMixin from './mixins/session-verification-poll-mix
const CODE_INPUT_SELECTOR = 'input.otp-code'; const CODE_INPUT_SELECTOR = 'input.otp-code';
const proto = FormView.prototype;
const View = FormView.extend({ const View = FormView.extend({
className: 'sign-in-token-code', className: 'sign-in-token-code',
template: Template, template: Template,
@ -37,9 +39,18 @@ const View = FormView.extend({
// is deleted. If the account no longer exists, redirects the user to // is deleted. If the account no longer exists, redirects the user to
// sign up, if the account exists, then notifies them their account // sign up, if the account exists, then notifies them their account
// has been blocked. // has been blocked.
this.waitForSessionVerification(this.getAccount(), () => { const account = this.getSignedInAccount();
// don't do anything on verification, that's taken care of in the submit handler. return proto.afterVisible
}); .call(this)
.then(() => this.broker.persistVerificationData(account))
.then(() =>
this.invokeBrokerMethod('beforeSignUpConfirmationPoll', account)
)
.then(() => {
this.waitForSessionVerification(this.getAccount(), () => {
// don't do anything on verification, that's taken care of in the submit handler.
});
});
}, },
setInitialContext(context) { setInitialContext(context) {

Просмотреть файл

@ -239,3 +239,7 @@
font-family: monospace; font-family: monospace;
font-weight: bold; font-weight: bold;
} }
.push-confirm-login-device {
padding: 15px 0 45px 0;
}

Просмотреть файл

@ -138,3 +138,8 @@
.graphic-pair-failure { .graphic-pair-failure {
background-image: image-url('graphic_hearts_broken.svg'); background-image: image-url('graphic_hearts_broken.svg');
} }
.graphic-send-tab-complete {
background-image: image-url('send_tab_complete.svg');
}

Просмотреть файл

@ -0,0 +1,76 @@
/* 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/. */
import { assert } from 'chai';
import $ from 'jquery';
import Account from 'models/account';
import Backbone from 'backbone';
import Metrics from 'lib/metrics';
import Relier from 'models/reliers/relier';
import View from 'views/push/completed';
import _ from 'underscore';
import sinon from 'sinon';
describe('views/push/completed', () => {
let account;
let model;
let relier;
let view;
let notifier;
let metrics;
beforeEach(() => {
account = new Account({
email: 'a@a.com',
uid: 'uid',
});
relier = new Relier({});
model = new Backbone.Model({
account,
});
notifier = _.extend({}, Backbone.Events);
metrics = new Metrics({ notifier });
view = new View({
model,
relier,
notifier,
metrics,
});
sinon.stub(view, 'getSignedInAccount').callsFake(() => account);
return view.render().then(() => $('#container').html(view.$el));
});
afterEach(() => {
view.remove();
view.destroy();
view = null;
});
describe('render', () => {
it('renders the view', () => {
assert.lengthOf(view.$('#push-auth-complete-header'), 1);
assert.include(
view.$('.verification-message').text(),
'Please close this page'
);
});
describe('without an account', () => {
beforeEach(() => {
account = new Account({});
sinon.spy(view, 'navigate');
return view.render();
});
it('redirects to the email first page', () => {
assert.isTrue(view.navigate.calledWith('/'));
});
});
});
});

Просмотреть файл

@ -0,0 +1,123 @@
/* 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/. */
import _ from 'underscore';
import { assert } from 'chai';
import Account from 'models/account';
import Backbone from 'backbone';
import BaseBroker from 'models/auth_brokers/base';
import Metrics from 'lib/metrics';
import Relier from 'models/reliers/relier';
import sinon from 'sinon';
import User from 'models/user';
import View from 'views/push/confirm_login';
import WindowMock from '../../../mocks/window';
import $ from 'jquery';
describe('views/push/confirm_login', () => {
let account;
let broker;
let metrics;
let model;
let notifier;
let relier;
let user;
let view;
let windowMock;
const ua = {
uaBrowser: 'Firefox',
uaOS: 'OSX',
};
beforeEach(() => {
windowMock = new WindowMock();
relier = new Relier({
window: windowMock,
});
broker = new BaseBroker({
relier: relier,
window: windowMock,
});
account = new Account({
email: 'a@a.com',
uid: 'uid',
});
model = new Backbone.Model({
account,
});
notifier = _.extend({}, Backbone.Events);
metrics = new Metrics({ notifier });
user = new User();
view = new View({
broker,
metrics,
model,
notifier,
relier,
user,
window: windowMock,
ua,
ip: '123.123.123.123',
code: 'validCode',
});
sinon.stub(view, 'getSignedInAccount').callsFake(() => account);
return view.render().then(() => $('#container').html(view.$el));
});
afterEach(() => {
metrics.destroy();
view.remove();
view.destroy();
view = metrics = null;
});
describe('render', () => {
it('renders the view', () => {
assert.lengthOf(view.$('#fxa-push-confirm-login-header'), 1);
assert.include(
view.$('.verification-message').text(),
'please confirm this sign-in'
);
assert.lengthOf(view.$('#submit-btn'), 1);
assert.lengthOf(view.$('#change-password'), 1);
assert.include(view.$('.push-confirm-login-device').text(), 'Firefox');
assert.include(
view.$('.push-confirm-login-device').text(),
'123.123.123.123'
);
assert.include(
view.$('.push-confirm-login-device').text(),
'Location unknown'
);
});
});
describe('submit', () => {
describe('success', () => {
beforeEach(() => {
sinon.stub(account, 'verifySignUp').callsFake(() => Promise.resolve());
sinon.spy(view, 'navigate');
return view.submit();
});
it('calls correct methods', () => {
assert.isTrue(
account.verifySignUp.calledWith('validCode'),
'verify with correct code'
);
assert.isTrue(view.navigate.calledOnceWith('/push/completed'));
});
});
});
});

Просмотреть файл

@ -0,0 +1,155 @@
/* 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/. */
import _ from 'underscore';
import { assert } from 'chai';
import Account from 'models/account';
import Backbone from 'backbone';
import BaseBroker from 'models/auth_brokers/base';
import Metrics from 'lib/metrics';
import Relier from 'models/reliers/relier';
import sinon from 'sinon';
import User from 'models/user';
import View from 'views/push/send_login';
import WindowMock from '../../../mocks/window';
import SentryMetrics from 'lib/sentry';
import SessionVerificationPoll from 'models/polls/session-verification';
describe('views/push/send_login', () => {
let account;
let broker;
let metrics;
let model;
let notifier;
let relier;
let user;
let view;
let windowMock;
let sentryMetrics;
let sessionVerificationPoll;
beforeEach(async () => {
windowMock = new WindowMock();
relier = new Relier({
window: windowMock,
});
broker = new BaseBroker({
relier: relier,
window: windowMock,
});
account = new Account({
email: 'a@a.com',
uid: 'uid',
});
model = new Backbone.Model({
account: account,
lastPage: 'signin',
password: 'password',
});
notifier = _.extend({}, Backbone.Events);
sentryMetrics = new SentryMetrics();
metrics = new Metrics({ notifier, sentryMetrics });
user = new User();
sessionVerificationPoll = new SessionVerificationPoll(
{},
{
account,
pollIntervalInMS: 2,
window: windowMock,
}
);
view = new View({
broker,
canGoBack: true,
metrics,
model,
notifier,
relier,
user,
viewName: 'send-login',
window: windowMock,
sessionVerificationPoll,
});
sinon.stub(view, 'getSignedInAccount').callsFake(() => account);
sinon
.stub(account, 'sendPushLoginRequest')
.callsFake(() => Promise.resolve());
sinon
.stub(view, '_handleSessionVerificationPollErrors')
.callsFake(() => {});
sinon.stub(sessionVerificationPoll, 'start').callsFake(() => {});
return view.render();
});
afterEach(function () {
metrics.destroy();
view.remove();
view.destroy();
view = metrics = null;
});
describe('render', () => {
it('renders the view', () => {
assert.lengthOf(view.$('#fxa-push-send-login-header'), 1);
assert.include(
view.$('.verification-message').text(),
'Check your connected Firefox devices'
);
assert.lengthOf(view.$('#resend'), 1);
assert.lengthOf(view.$('#send-email'), 1);
});
it('sends push notification to account', () => {
assert.isTrue(account.sendPushLoginRequest.calledOnce);
});
});
describe('afterVisible', () => {
beforeEach(async () => {
sinon.spy(broker, 'persistVerificationData');
sinon.spy(view, 'waitForSessionVerification');
sinon.spy(view, 'invokeBrokerMethod');
return view.afterVisible().then(() => {
// simulate account being verified
return sessionVerificationPoll.trigger('verified');
});
});
it('starts polling for session to be verified', () => {
assert.isTrue(broker.persistVerificationData.calledOnceWith(account));
assert.isTrue(view.waitForSessionVerification.calledOnce);
assert.isTrue(view.invokeBrokerMethod.calledTwice);
const args = view.invokeBrokerMethod.args;
assert.equal(args[0][0], 'beforeSignUpConfirmationPoll');
assert.equal(args[1][0], 'afterCompleteSignInWithCode');
});
});
describe('resend', () => {
describe('success', () => {
beforeEach(() => {
sinon.spy(view, 'displaySuccess');
return view.render().then(() => {
account.sendPushLoginRequest.resetHistory();
return view.resend();
});
});
it('calls correct methods', () => {
assert.isTrue(account.sendPushLoginRequest.calledOnce);
assert.isTrue(view.displaySuccess.calledOnce);
});
});
});
});

Просмотреть файл

@ -107,13 +107,13 @@ describe('views/sign_in_token_code', () => {
}); });
describe('afterVisible', () => { describe('afterVisible', () => {
it('starts polling in case the email bounces', () => { it('starts polling in case the email bounces', async () => {
const account = { uid: 'uid' }; const account = { uid: 'uid' };
sinon.stub(view, 'waitForSessionVerification'); sinon.stub(view, 'waitForSessionVerification');
sinon.stub(view, 'getAccount').returns(account); sinon.stub(view, 'getAccount').returns(account);
view.afterVisible(); await view.afterVisible();
assert.isTrue(view.waitForSessionVerification.calledOnceWith(account)); assert.isTrue(view.waitForSessionVerification.calledOnceWith(account));
}); });

Просмотреть файл

@ -237,6 +237,9 @@ require('./spec/views/post_verify/password/force_password_change');
require('./spec/views/post_verify/secondary_email/add_secondary_email'); require('./spec/views/post_verify/secondary_email/add_secondary_email');
require('./spec/views/post_verify/secondary_email/confirm_secondary_email'); require('./spec/views/post_verify/secondary_email/confirm_secondary_email');
require('./spec/views/progress_indicator'); require('./spec/views/progress_indicator');
require('./spec/views/push/confirm_login');
require('./spec/views/push/send_login');
require('./spec/views/push/completed');
require('./spec/views/ready'); require('./spec/views/ready');
require('./spec/views/report_sign_in'); require('./spec/views/report_sign_in');
require('./spec/views/reset_password'); require('./spec/views/reset_password');

Просмотреть файл

@ -49,6 +49,9 @@ module.exports = function () {
'post_verify/secondary_email/confirm_secondary_email', 'post_verify/secondary_email/confirm_secondary_email',
'post_verify/secondary_email/verified_secondary_email', 'post_verify/secondary_email/verified_secondary_email',
'primary_email_verified', 'primary_email_verified',
'push/completed',
'push/confirm_login',
'push/send_login',
'report_signin', 'report_signin',
'reset_password', 'reset_password',
'reset_password_confirmed', 'reset_password_confirmed',