зеркало из https://github.com/mozilla/fxa.git
feat(push): Add views for confirming a push notification login
This commit is contained in:
Родитель
334d907346
Коммит
5610838255
|
@ -34,6 +34,7 @@ secrets.json
|
|||
storybooks-publish
|
||||
tsconfig.tsbuildinfo
|
||||
tailwind.out.*
|
||||
.idea
|
||||
|
||||
# Dependencies
|
||||
**/node_modules
|
||||
|
|
|
@ -1238,4 +1238,8 @@ export default class AuthClient {
|
|||
new Headers(headers)
|
||||
);
|
||||
}
|
||||
|
||||
async sendPushLoginRequest(sessionToken: string) {
|
||||
return this.sessionPost('/session/verify/send_push', sessionToken, {});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -111,8 +111,7 @@ const conf = convict({
|
|||
},
|
||||
memcached: {
|
||||
address: {
|
||||
doc:
|
||||
'Address:port of the memcached server (or `none` to disable memcached)',
|
||||
doc: 'Address:port of the memcached server (or `none` to disable memcached)',
|
||||
default: 'localhost:11211',
|
||||
env: 'MEMCACHE_METRICS_CONTEXT_ADDRESS',
|
||||
},
|
||||
|
@ -180,8 +179,7 @@ const conf = convict({
|
|||
connectionTimeout: {
|
||||
default: '5 minutes',
|
||||
env: 'DB_CONNECTION_TIMEOUT',
|
||||
doc:
|
||||
'Timeout in milliseconds after which the mailer will stop trying to connect to the database',
|
||||
doc: 'Timeout in milliseconds after which the mailer will stop trying to connect to the database',
|
||||
format: 'duration',
|
||||
},
|
||||
poolee: {
|
||||
|
@ -246,8 +244,7 @@ const conf = convict({
|
|||
env: 'EMAIL_SERVICE_PORT',
|
||||
},
|
||||
forcedEmailAddresses: {
|
||||
doc:
|
||||
'force usage of fxa-email-service when sending emails to addresses that match this pattern',
|
||||
doc: 'force usage of fxa-email-service when sending emails to addresses that match this pattern',
|
||||
format: RegExp,
|
||||
default: /emailservice\.[A-Za-z0-9._%+-]+@restmail\.net$/,
|
||||
env: 'EMAIL_SERVICE_FORCE_EMAIL_REGEX',
|
||||
|
@ -560,8 +557,7 @@ const conf = convict({
|
|||
default: '100 milliseconds',
|
||||
env: 'REDIS_POOL_TIMEOUT',
|
||||
format: 'duration',
|
||||
doc:
|
||||
'Initial backoff for Redis connection retries, increases exponentially with each attempt',
|
||||
doc: 'Initial backoff for Redis connection retries, increases exponentially with each attempt',
|
||||
},
|
||||
},
|
||||
subhubServerMessaging: {
|
||||
|
@ -572,8 +568,7 @@ const conf = convict({
|
|||
default: '',
|
||||
},
|
||||
subhubUpdatesQueueUrl: {
|
||||
doc:
|
||||
'The queue URL to use (should include https://sqs.<region>.amazonaws.com/<account-id>/<queue-name>)',
|
||||
doc: 'The queue URL to use (should include https://sqs.<region>.amazonaws.com/<account-id>/<queue-name>)',
|
||||
format: String,
|
||||
env: 'SUBHUB_QUEUE_URL',
|
||||
default: '',
|
||||
|
@ -596,8 +591,7 @@ const conf = convict({
|
|||
default: '15 minutes',
|
||||
},
|
||||
sessionTokenWithoutDevice: {
|
||||
doc:
|
||||
'Maximum age for session tokens without a device record, specify zero to disable',
|
||||
doc: 'Maximum age for session tokens without a device record, specify zero to disable',
|
||||
format: 'duration',
|
||||
env: 'SESSION_TOKEN_WITHOUT_DEVICE_TTL',
|
||||
default: '4 weeks',
|
||||
|
@ -624,8 +618,7 @@ const conf = convict({
|
|||
default: 1,
|
||||
},
|
||||
snsTopicArn: {
|
||||
doc:
|
||||
'Amazon SNS topic on which to send account event notifications. Set to "disabled" to turn off the notifier',
|
||||
doc: 'Amazon SNS topic on which to send account event notifications. Set to "disabled" to turn off the notifier',
|
||||
format: String,
|
||||
env: 'SNS_TOPIC_ARN',
|
||||
default: '',
|
||||
|
@ -638,36 +631,31 @@ const conf = convict({
|
|||
},
|
||||
emailNotifications: {
|
||||
region: {
|
||||
doc:
|
||||
'The region where the queues live, most likely the same region we are sending email e.g. us-east-1, us-west-2',
|
||||
doc: '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,
|
||||
env: 'BOUNCE_REGION',
|
||||
default: '',
|
||||
},
|
||||
bounceQueueUrl: {
|
||||
doc:
|
||||
'The bounce queue URL to use (should include https://sqs.<region>.amazonaws.com/<account-id>/<queue-name>)',
|
||||
doc: 'The bounce queue URL to use (should include https://sqs.<region>.amazonaws.com/<account-id>/<queue-name>)',
|
||||
format: String,
|
||||
env: 'BOUNCE_QUEUE_URL',
|
||||
default: '',
|
||||
},
|
||||
complaintQueueUrl: {
|
||||
doc:
|
||||
'The complaint queue URL to use (should include https://sqs.<region>.amazonaws.com/<account-id>/<queue-name>)',
|
||||
doc: 'The complaint queue URL to use (should include https://sqs.<region>.amazonaws.com/<account-id>/<queue-name>)',
|
||||
format: String,
|
||||
env: 'COMPLAINT_QUEUE_URL',
|
||||
default: '',
|
||||
},
|
||||
deliveryQueueUrl: {
|
||||
doc:
|
||||
'The email delivery queue URL to use (should include https://sqs.<region>.amazonaws.com/<account-id>/<queue-name>)',
|
||||
doc: 'The email delivery queue URL to use (should include https://sqs.<region>.amazonaws.com/<account-id>/<queue-name>)',
|
||||
format: String,
|
||||
env: 'DELIVERY_QUEUE_URL',
|
||||
default: '',
|
||||
},
|
||||
notificationQueueUrl: {
|
||||
doc:
|
||||
'Queue URL for notifications from fxa-email-service (eventually this will be the only email-related queue)',
|
||||
doc: 'Queue URL for notifications from fxa-email-service (eventually this will be the only email-related queue)',
|
||||
format: String,
|
||||
env: 'NOTIFICATION_QUEUE_URL',
|
||||
default: '',
|
||||
|
@ -681,8 +669,7 @@ const conf = convict({
|
|||
default: '',
|
||||
},
|
||||
profileUpdatesQueueUrl: {
|
||||
doc:
|
||||
'The queue URL to use (should include https://sqs.<region>.amazonaws.com/<account-id>/<queue-name>)',
|
||||
doc: 'The queue URL to use (should include https://sqs.<region>.amazonaws.com/<account-id>/<queue-name>)',
|
||||
format: String,
|
||||
env: 'PROFILE_UPDATES_QUEUE_URL',
|
||||
default: '',
|
||||
|
@ -697,8 +684,7 @@ const conf = convict({
|
|||
},
|
||||
secretBearerToken: {
|
||||
default: 'YOU MUST CHANGE ME',
|
||||
doc:
|
||||
'Secret for server-to-server bearer token auth for fxa-profile-server',
|
||||
doc: 'Secret for server-to-server bearer token auth for fxa-profile-server',
|
||||
env: 'PROFILE_SERVER_AUTH_SECRET_BEARER_TOKEN',
|
||||
format: 'String',
|
||||
},
|
||||
|
@ -748,8 +734,7 @@ const conf = convict({
|
|||
env: 'SUBHUB_ENABLED',
|
||||
},
|
||||
useStubs: {
|
||||
doc:
|
||||
'Indicates whether to use stub methods for SubHub instead of talking to the server',
|
||||
doc: 'Indicates whether to use stub methods for SubHub instead of talking to the server',
|
||||
format: Boolean,
|
||||
default: false,
|
||||
env: 'SUBHUB_USE_STUBS',
|
||||
|
@ -835,8 +820,7 @@ const conf = convict({
|
|||
},
|
||||
},
|
||||
sharedSecret: {
|
||||
doc:
|
||||
'Shared secret for authentication between backend subscription services',
|
||||
doc: 'Shared secret for authentication between backend subscription services',
|
||||
format: String,
|
||||
default: 'YOU MUST CHANGE ME',
|
||||
env: 'SUBSCRIPTIONS_SHARED_SECRET',
|
||||
|
@ -856,8 +840,7 @@ const conf = convict({
|
|||
transactionalEmails: {
|
||||
// See also: https://jira.mozilla.com/browse/FXA-1148
|
||||
enabled: {
|
||||
doc:
|
||||
'Indicates whether FxA sends transactional lifecycle emails for subscriptions (i.e. versus Marketing Cloud)',
|
||||
doc: 'Indicates whether FxA sends transactional lifecycle emails for subscriptions (i.e. versus Marketing Cloud)',
|
||||
format: Boolean,
|
||||
env: 'SUBSCRIPTIONS_TRANSACTIONAL_EMAILS_ENABLED',
|
||||
default: false,
|
||||
|
@ -865,8 +848,7 @@ const conf = convict({
|
|||
},
|
||||
},
|
||||
currenciesToCountries: {
|
||||
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.',
|
||||
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.',
|
||||
format: Object,
|
||||
default: {
|
||||
USD: ['US', 'GB', 'NZ', 'MY', 'SG', 'CA', 'AS', 'GU', 'MP', 'PR', 'VI'],
|
||||
|
@ -887,8 +869,7 @@ const conf = convict({
|
|||
default: false,
|
||||
},
|
||||
clientIds: {
|
||||
doc:
|
||||
'Mappings from client id to service name: { "id1": "name-1", "id2": "name-2" }',
|
||||
doc: 'Mappings from client id to service name: { "id1": "name-1", "id2": "name-2" }',
|
||||
format: Object,
|
||||
default: {},
|
||||
env: 'OAUTH_CLIENT_IDS',
|
||||
|
@ -902,8 +883,7 @@ const conf = convict({
|
|||
// A safety switch for disabling new signins/signups from particular clients,
|
||||
// as a hedge against unexpected client behaviour.
|
||||
disableNewConnectionsForClients: {
|
||||
doc:
|
||||
'Comma-separated list of oauth client ids for which new connections should be temporarily refused',
|
||||
doc: 'Comma-separated list of oauth client ids for which new connections should be temporarily refused',
|
||||
env: 'OAUTH_DISABLE_NEW_CONNECTIONS_FOR_CLIENTS',
|
||||
format: Array,
|
||||
default: [],
|
||||
|
@ -935,8 +915,7 @@ const conf = convict({
|
|||
default: 'megaz0rd',
|
||||
},
|
||||
jwtSecretKeys: {
|
||||
doc:
|
||||
'Comma-separated list of secret keys for verifying oauth-to-auth server JWTs',
|
||||
doc: 'Comma-separated list of secret keys for verifying oauth-to-auth server JWTs',
|
||||
env: 'OAUTH_SERVER_SECRETS',
|
||||
format: Array,
|
||||
default: ['megaz0rd'],
|
||||
|
@ -1011,8 +990,7 @@ const conf = convict({
|
|||
},
|
||||
},
|
||||
authServerSecrets: {
|
||||
doc:
|
||||
'Comma-separated list of secret keys for verifying server-to-server JWTs',
|
||||
doc: 'Comma-separated list of secret keys for verifying server-to-server JWTs',
|
||||
env: 'AUTH_SERVER_SECRETS',
|
||||
format: 'Array',
|
||||
default: [],
|
||||
|
@ -1043,8 +1021,7 @@ const conf = convict({
|
|||
},
|
||||
clientManagement: {
|
||||
enabled: {
|
||||
doc:
|
||||
'Enable client management in OAuth server routes. Do NOT set this to true in production.',
|
||||
doc: 'Enable client management in OAuth server routes. Do NOT set this to true in production.',
|
||||
default: false,
|
||||
format: 'Boolean',
|
||||
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
|
||||
// can store them separately in mysql.
|
||||
// It's also used for amplitude stats
|
||||
doc:
|
||||
'Mappings from client id to service name: { "id1": "name-1", "id2": "name-2" }',
|
||||
doc: 'Mappings from client id to service name: { "id1": "name-1", "id2": "name-2" }',
|
||||
default: {},
|
||||
format: 'Object',
|
||||
env: 'OAUTH_CLIENT_IDS',
|
||||
},
|
||||
disabledClients: {
|
||||
doc:
|
||||
'Comma-separated list of client ids for which service should be temporarily refused',
|
||||
doc: 'Comma-separated list of client ids for which service should be temporarily refused',
|
||||
env: 'OAUTH_CLIENTS_DISABLED',
|
||||
format: 'Array',
|
||||
default: [],
|
||||
|
@ -1106,8 +1081,7 @@ const conf = convict({
|
|||
},
|
||||
events: {
|
||||
enabled: {
|
||||
doc:
|
||||
'Whether or not config.events has to be properly set in production',
|
||||
doc: 'Whether or not config.events has to be properly set in production',
|
||||
default: true,
|
||||
format: 'Boolean',
|
||||
env: 'EVENTS_ENABLED',
|
||||
|
@ -1203,8 +1177,7 @@ const conf = convict({
|
|||
},
|
||||
timezone: {
|
||||
default: 'Z',
|
||||
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.',
|
||||
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.',
|
||||
env: 'MYSQL_TIMEZONE',
|
||||
format: 'String',
|
||||
},
|
||||
|
@ -1217,8 +1190,7 @@ const conf = convict({
|
|||
env: 'FXA_OPENID_KEYFILE',
|
||||
},
|
||||
newKeyFile: {
|
||||
doc:
|
||||
'Path to private key JWK that will be used to sign JWTs in the future',
|
||||
doc: 'Path to private key JWK that will be used to sign JWTs in the future',
|
||||
default: '',
|
||||
format: 'String',
|
||||
env: 'FXA_OPENID_NEWKEYFILE',
|
||||
|
@ -1245,8 +1217,7 @@ const conf = convict({
|
|||
env: 'FXA_OPENID_OLDKEY',
|
||||
},
|
||||
unsafelyAllowMissingActiveKey: {
|
||||
doc:
|
||||
'Do not error out if there is no active key; should only be used when initializing keys',
|
||||
doc: 'Do not error out if there is no active key; should only be used when initializing keys',
|
||||
default: false,
|
||||
format: 'Boolean',
|
||||
env: 'FXA_OPENID_UNSAFELY_ALLOW_MISSING_ACTIVE_KEY',
|
||||
|
@ -1277,8 +1248,7 @@ const conf = convict({
|
|||
env: 'PPID_CLIENT_IDS',
|
||||
},
|
||||
rotatingClientIds: {
|
||||
doc:
|
||||
'client_ids that receive automatically rotating PPIDs based on server time',
|
||||
doc: 'client_ids that receive automatically rotating PPIDs based on server time',
|
||||
default: [],
|
||||
format: 'Array',
|
||||
env: 'PPID_ROTATING_CLIENT_IDS',
|
||||
|
@ -1332,8 +1302,7 @@ const conf = convict({
|
|||
developerId: { doc: 'Bytes of generated developer ids', default: 16 },
|
||||
},
|
||||
cacheControl: {
|
||||
doc:
|
||||
'Hapi: a string with the value of the "Cache-Control" header when caching is disabled',
|
||||
doc: 'Hapi: a string with the value of the "Cache-Control" header when caching is disabled',
|
||||
format: 'String',
|
||||
default: 'private, no-cache, no-store, must-revalidate',
|
||||
},
|
||||
|
@ -1410,15 +1379,13 @@ const conf = convict({
|
|||
},
|
||||
signinConfirmation: {
|
||||
forcedEmailAddresses: {
|
||||
doc:
|
||||
'Force sign-in confirmation for email addresses matching this regex.',
|
||||
doc: 'Force sign-in confirmation for email addresses matching this regex.',
|
||||
format: RegExp,
|
||||
default: /.+@mozilla\.com$/,
|
||||
env: 'SIGNIN_CONFIRMATION_FORCE_EMAIL_REGEX',
|
||||
},
|
||||
skipForEmailAddresses: {
|
||||
doc:
|
||||
'Comma separated list of email addresses that will always skip any non TOTP sign-in confirmation',
|
||||
doc: 'Comma separated list of email addresses that will always skip any non TOTP sign-in confirmation',
|
||||
format: Array,
|
||||
default: [],
|
||||
env: 'SIGNIN_CONFIRMATION_SKIP_FOR_EMAIL_ADDRESS',
|
||||
|
@ -1467,8 +1434,7 @@ const conf = convict({
|
|||
securityHistory: {
|
||||
ipProfiling: {
|
||||
allowedRecency: {
|
||||
doc:
|
||||
'Length of time since previously verified event to allow skipping confirmation',
|
||||
doc: 'Length of time since previously verified event to allow skipping confirmation',
|
||||
default: '72 hours',
|
||||
format: 'duration',
|
||||
env: 'IP_PROFILING_RECENCY',
|
||||
|
@ -1483,15 +1449,13 @@ const conf = convict({
|
|||
env: 'LASTACCESSTIME_UPDATES_ENABLED',
|
||||
},
|
||||
sampleRate: {
|
||||
doc:
|
||||
'sample rate for updates to the lastAccessTime session token property, in the range 0..1',
|
||||
doc: 'sample rate for updates to the lastAccessTime session token property, in the range 0..1',
|
||||
format: Number,
|
||||
default: 0.3,
|
||||
env: 'LASTACCESSTIME_UPDATES_SAMPLE_RATE',
|
||||
},
|
||||
earliestSaneTimestamp: {
|
||||
doc:
|
||||
'timestamp used as the basis of the fallback value for lastAccessTimeFormatted, currently pinned to the deployment of 1.96.4 / a0940d7dc51e2ba20fa18aa3a830810e35c9a9d9',
|
||||
doc: 'timestamp used as the basis of the fallback value for lastAccessTimeFormatted, currently pinned to the deployment of 1.96.4 / a0940d7dc51e2ba20fa18aa3a830810e35c9a9d9',
|
||||
format: 'timestamp',
|
||||
default: 1507081020000,
|
||||
env: 'LASTACCESSTIME_EARLIEST_SANE_TIMESTAMP',
|
||||
|
@ -1510,8 +1474,7 @@ const conf = convict({
|
|||
env: 'SIGNIN_UNBLOCK_CODE_LIFETIME',
|
||||
},
|
||||
forcedEmailAddresses: {
|
||||
doc:
|
||||
'If feature enabled, force sign-in unblock for email addresses matching this regex.',
|
||||
doc: 'If feature enabled, force sign-in unblock for email addresses matching this regex.',
|
||||
format: RegExp,
|
||||
default: /^$/, // default is no one
|
||||
env: 'SIGNIN_UNBLOCK_FORCED_EMAILS',
|
||||
|
@ -1522,7 +1485,8 @@ const conf = convict({
|
|||
doc: 'RegExp that validates the URI format of the Push Server',
|
||||
format: RegExp,
|
||||
// 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: {
|
||||
|
@ -1604,22 +1568,19 @@ const conf = convict({
|
|||
env: 'SMS_INSTALL_FIREFOX_LINK',
|
||||
},
|
||||
installFirefoxWithSigninCodeBaseUri: {
|
||||
doc:
|
||||
'Base URI for the SMS template when the signinCodes feature is active',
|
||||
doc: 'Base URI for the SMS template when the signinCodes feature is active',
|
||||
default: 'https://accounts.firefox.com/m',
|
||||
format: 'url',
|
||||
env: 'SMS_SIGNIN_CODES_BASE_URI',
|
||||
},
|
||||
enableBudgetChecks: {
|
||||
doc:
|
||||
'enable checks of the monthly SMS spend against the available budget',
|
||||
doc: 'enable checks of the monthly SMS spend against the available budget',
|
||||
default: true,
|
||||
format: Boolean,
|
||||
env: 'SMS_ENABLE_BUDGET_CHECKS',
|
||||
},
|
||||
minimumCreditThresholdUSD: {
|
||||
doc:
|
||||
'The minimum amount of available credit that is necessary to enable SMS, in US dollars',
|
||||
doc: 'The minimum amount of available credit that is necessary to enable SMS, in US dollars',
|
||||
default: 200,
|
||||
format: 'nat',
|
||||
env: 'SMS_MINIMUM_CREDIT_THRESHOLD',
|
||||
|
@ -1631,8 +1592,7 @@ const conf = convict({
|
|||
env: 'SMS_POLL_CURRENT_SPEND_INTERVAL',
|
||||
},
|
||||
smsType: {
|
||||
doc:
|
||||
'AWS.SNS.SMS.SMSType argument value. "Promotional" or "Transactional".',
|
||||
doc: 'AWS.SNS.SMS.SMSType argument value. "Promotional" or "Transactional".',
|
||||
default: 'Promotional',
|
||||
format: 'String',
|
||||
env: 'SMS_AWS_SNS_SMSTYPE',
|
||||
|
@ -1640,8 +1600,7 @@ const conf = convict({
|
|||
},
|
||||
secondaryEmail: {
|
||||
minUnverifiedAccountTime: {
|
||||
doc:
|
||||
'The minimum amount of time an account can be unverified before another account can use it for secondary email',
|
||||
doc: 'The minimum amount of time an account can be unverified before another account can use it for secondary email',
|
||||
default: '1 day',
|
||||
format: 'duration',
|
||||
env: 'SECONDARY_EMAIL_MIN_UNVERIFIED_ACCOUNT_TIME',
|
||||
|
@ -1691,8 +1650,7 @@ const conf = convict({
|
|||
env: 'RECOVERY_CODE_COUNT',
|
||||
},
|
||||
notifyLowCount: {
|
||||
doc:
|
||||
'Notify the user when there are less than these many recovery codes',
|
||||
doc: 'Notify the user when there are less than these many recovery codes',
|
||||
default: 2,
|
||||
env: 'RECOVERY_CODE_NOTIFY_LOW_COUNT',
|
||||
},
|
||||
|
@ -1712,8 +1670,7 @@ const conf = convict({
|
|||
format: 'duration',
|
||||
},
|
||||
secondInterval: {
|
||||
doc:
|
||||
'Time since account creation after which the second reminder is sent',
|
||||
doc: 'Time since account creation after which the second reminder is sent',
|
||||
default: '5 days',
|
||||
env: 'VERIFICATION_REMINDERS_SECOND_INTERVAL',
|
||||
format: 'duration',
|
||||
|
@ -1727,15 +1684,13 @@ const conf = convict({
|
|||
},
|
||||
maxConnections: {
|
||||
default: 10,
|
||||
doc:
|
||||
'Maximum connection count for the verification reminders Redis pool',
|
||||
doc: 'Maximum connection count for the verification reminders Redis pool',
|
||||
env: 'VERIFICATION_REMINDERS_REDIS_MAX_CONNECTIONS',
|
||||
format: 'nat',
|
||||
},
|
||||
minConnections: {
|
||||
default: 1,
|
||||
doc:
|
||||
'Minimum connection count for the verification reminders Redis pool',
|
||||
doc: 'Minimum connection count for the verification reminders Redis pool',
|
||||
env: 'VERIFICATION_REMINDERS_REDIS_MIN_CONNECTIONS',
|
||||
format: 'nat',
|
||||
},
|
||||
|
@ -1813,15 +1768,13 @@ const conf = convict({
|
|||
format: Number,
|
||||
},
|
||||
locationStateFieldId: {
|
||||
doc:
|
||||
'Zendesk support ticket custom field for the state/region of the location',
|
||||
doc: 'Zendesk support ticket custom field for the state/region of the location',
|
||||
default: 360026463491,
|
||||
env: 'ZENDESK_LOCATION_STATE_FIELD_ID',
|
||||
format: Number,
|
||||
},
|
||||
locationCountryFieldId: {
|
||||
doc:
|
||||
'Zendesk support ticket custom field for the country of the location',
|
||||
doc: 'Zendesk support ticket custom field for the country of the location',
|
||||
default: 360026463511,
|
||||
env: 'ZENDESK_LOCATION_COUNTRY_FIELD_ID',
|
||||
format: Number,
|
||||
|
@ -1833,8 +1786,7 @@ const conf = convict({
|
|||
format: Number,
|
||||
},
|
||||
appFieldId: {
|
||||
doc:
|
||||
'Zendesk support ticket custom field for product specific app or service',
|
||||
doc: 'Zendesk support ticket custom field for product specific app or service',
|
||||
default: 360030780972,
|
||||
env: 'ZENDESK_APP_FIELD_ID',
|
||||
format: Number,
|
||||
|
@ -1863,8 +1815,7 @@ const conf = convict({
|
|||
supportPanel: {
|
||||
secretBearerToken: {
|
||||
default: 'YOU MUST CHANGE ME',
|
||||
doc:
|
||||
'Shared secret to access certain endpoints. Please only use for GET. No state mutation allowed!',
|
||||
doc: 'Shared secret to access certain endpoints. Please only use for GET. No state mutation allowed!',
|
||||
env: 'SUPPORT_PANEL_AUTH_SECRET_BEARER_TOKEN',
|
||||
format: 'String',
|
||||
},
|
||||
|
@ -1898,6 +1849,7 @@ conf.set(
|
|||
`${baseUri}/settings/two_step_authentication/replace_codes`
|
||||
);
|
||||
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.initiatePasswordResetUrl', `${baseUri}/reset_password`);
|
||||
conf.set(
|
||||
|
|
|
@ -447,25 +447,27 @@ module.exports = function (
|
|||
const sessionToken = request.auth.credentials;
|
||||
const { uid, tokenVerificationId } = sessionToken;
|
||||
|
||||
const devices = await db.devices(uid);
|
||||
const geoData = request.app.geo;
|
||||
const allDevices = await db.devices(uid);
|
||||
const location = request.app.geo.location || {};
|
||||
|
||||
const { ua } = request.app;
|
||||
const uaInfo = {
|
||||
uaBrowser: ua.browser,
|
||||
uaBrowserVersion: ua.browserVersion,
|
||||
uaOS: ua.os,
|
||||
uaOSVersion: ua.osVersion,
|
||||
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 = `${
|
||||
config.smtp.verificationUrl
|
||||
config.smtp.pushVerificationUrl
|
||||
}?type=push_login_verification&code=${tokenVerificationId}&ua=${encodeURIComponent(
|
||||
JSON.stringify(uaInfo)
|
||||
)}&location=${encodeURIComponent(
|
||||
JSON.stringify(geoData.location)
|
||||
JSON.stringify(location)
|
||||
)}&ip=${encodeURIComponent(request.app.clientAddress)}`;
|
||||
|
||||
const options = {
|
||||
|
@ -475,7 +477,7 @@ module.exports = function (
|
|||
};
|
||||
|
||||
try {
|
||||
await push.notifyVerifyLoginRequest(uid, devices, options);
|
||||
await push.notifyVerifyLoginRequest(uid, filteredDevices, options);
|
||||
} catch (err) {
|
||||
log.error('Session.verify.send_push', {
|
||||
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;
|
||||
|
|
|
@ -184,6 +184,9 @@ const Router = Backbone.Router.extend({
|
|||
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, {
|
||||
type: VerificationReasons.PRIMARY_EMAIL_VERIFIED,
|
||||
}),
|
||||
|
@ -208,11 +211,8 @@ const Router = Backbone.Router.extend({
|
|||
'settings(/)': function () {
|
||||
// Because settings is a separate js app, we need to ensure navigating
|
||||
// from the content-server app passes along flow parameters.
|
||||
const {
|
||||
deviceId,
|
||||
flowBeginTime,
|
||||
flowId,
|
||||
} = this.metrics.getFlowEventMetadata();
|
||||
const { deviceId, flowBeginTime, flowId } =
|
||||
this.metrics.getFlowEventMetadata();
|
||||
|
||||
const {
|
||||
broker,
|
||||
|
@ -343,9 +343,8 @@ const Router = Backbone.Router.extend({
|
|||
url = this.broker.transformLink(url);
|
||||
|
||||
if (options.replace && this._viewModelStack.length) {
|
||||
this._viewModelStack[this._viewModelStack.length - 1] = createViewModel(
|
||||
nextViewData
|
||||
);
|
||||
this._viewModelStack[this._viewModelStack.length - 1] =
|
||||
createViewModel(nextViewData);
|
||||
} else {
|
||||
this._viewModelStack.push(createViewModel(nextViewData));
|
||||
}
|
||||
|
|
|
@ -1116,30 +1116,28 @@ const Account = Backbone.Model.extend(
|
|||
*
|
||||
* @method set
|
||||
*/
|
||||
set: _.wrap(Backbone.Model.prototype.set, function (
|
||||
func,
|
||||
attribute,
|
||||
value,
|
||||
options
|
||||
) {
|
||||
let attributes;
|
||||
// Handle both `"key", value` and `{key: value}` -style arguments.
|
||||
if (_.isObject(attribute)) {
|
||||
attributes = attribute;
|
||||
} 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');
|
||||
set: _.wrap(
|
||||
Backbone.Model.prototype.set,
|
||||
function (func, attribute, value, options) {
|
||||
let attributes;
|
||||
// Handle both `"key", value` and `{key: value}` -style arguments.
|
||||
if (_.isObject(attribute)) {
|
||||
attributes = attribute;
|
||||
} else {
|
||||
attributes = {};
|
||||
attributes[attribute] = value;
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
@ -1820,6 +1818,15 @@ const Account = Backbone.Model.extend(
|
|||
createCadReminder() {
|
||||
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,
|
||||
|
|
|
@ -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 proto = FormView.prototype;
|
||||
|
||||
const View = FormView.extend({
|
||||
className: 'sign-in-token-code',
|
||||
template: Template,
|
||||
|
@ -37,9 +39,18 @@ const View = FormView.extend({
|
|||
// is deleted. If the account no longer exists, redirects the user to
|
||||
// sign up, if the account exists, then notifies them their account
|
||||
// has been blocked.
|
||||
this.waitForSessionVerification(this.getAccount(), () => {
|
||||
// don't do anything on verification, that's taken care of in the submit handler.
|
||||
});
|
||||
const account = this.getSignedInAccount();
|
||||
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) {
|
||||
|
|
|
@ -239,3 +239,7 @@
|
|||
font-family: monospace;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.push-confirm-login-device {
|
||||
padding: 15px 0 45px 0;
|
||||
}
|
|
@ -138,3 +138,8 @@
|
|||
.graphic-pair-failure {
|
||||
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', () => {
|
||||
it('starts polling in case the email bounces', () => {
|
||||
it('starts polling in case the email bounces', async () => {
|
||||
const account = { uid: 'uid' };
|
||||
|
||||
sinon.stub(view, 'waitForSessionVerification');
|
||||
sinon.stub(view, 'getAccount').returns(account);
|
||||
|
||||
view.afterVisible();
|
||||
await view.afterVisible();
|
||||
|
||||
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/confirm_secondary_email');
|
||||
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/report_sign_in');
|
||||
require('./spec/views/reset_password');
|
||||
|
|
|
@ -49,6 +49,9 @@ module.exports = function () {
|
|||
'post_verify/secondary_email/confirm_secondary_email',
|
||||
'post_verify/secondary_email/verified_secondary_email',
|
||||
'primary_email_verified',
|
||||
'push/completed',
|
||||
'push/confirm_login',
|
||||
'push/send_login',
|
||||
'report_signin',
|
||||
'reset_password',
|
||||
'reset_password_confirmed',
|
||||
|
|
Загрузка…
Ссылка в новой задаче