From 4f3256211e82e56cbd3d3538158f9b737c96827c Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Sun, 17 Jan 2021 16:35:50 +0100 Subject: [PATCH 01/48] added localized pages; added refactored page templates; adapted test cases; introduced localization test cases --- public/choose_password.html | 60 + public/de-AT/invalid_link.html | 18 + public/de/invalid_link.html | 18 + public/invalid_link.html | 18 + public/invalid_verification_link.html | 22 + public/link_send_fail.html | 19 + public/link_send_success.html | 19 + public/password_reset_success.html | 19 + public/verify_email_success.html | 18 + public_html/invalid_link.html | 45 - public_html/invalid_verification_link.html | 68 -- public_html/link_send_fail.html | 45 - public_html/link_send_success.html | 45 - public_html/password_reset_success.html | 27 - public_html/verify_email_success.html | 27 - spec/EmailVerificationToken.spec.js | 45 +- spec/PasswordPolicy.spec.js | 85 +- spec/PublicAPI.spec.js | 4 +- spec/RegexVulnerabilities.spec.js | 11 +- spec/ValidationAndPasswordsReset.spec.js | 231 +++- src/Options/Definitions.js | 1197 ++++++++++---------- src/Options/docs.js | 2 + src/Options/index.js | 3 + src/Routers/PublicAPIRouter.js | 301 +++-- src/Utils.js | 87 ++ 25 files changed, 1321 insertions(+), 1113 deletions(-) create mode 100644 public/choose_password.html create mode 100644 public/de-AT/invalid_link.html create mode 100644 public/de/invalid_link.html create mode 100644 public/invalid_link.html create mode 100644 public/invalid_verification_link.html create mode 100644 public/link_send_fail.html create mode 100644 public/link_send_success.html create mode 100644 public/password_reset_success.html create mode 100644 public/verify_email_success.html delete mode 100644 public_html/invalid_link.html delete mode 100644 public_html/invalid_verification_link.html delete mode 100644 public_html/link_send_fail.html delete mode 100644 public_html/link_send_success.html delete mode 100644 public_html/password_reset_success.html delete mode 100644 public_html/verify_email_success.html create mode 100644 src/Utils.js diff --git a/public/choose_password.html b/public/choose_password.html new file mode 100644 index 0000000000..f5fd731490 --- /dev/null +++ b/public/choose_password.html @@ -0,0 +1,60 @@ + + + + +Password Reset + + + +

Reset Your Password

+ +

You can set a new Password for your {{appName}} account: {{username}}

+
+

{{error}}

+
+ + + + +

New Password

+ +

Confirm New Password

+ +
+

+
+ +
+ + + diff --git a/public/de-AT/invalid_link.html b/public/de-AT/invalid_link.html new file mode 100644 index 0000000000..2d9ac315a1 --- /dev/null +++ b/public/de-AT/invalid_link.html @@ -0,0 +1,18 @@ + + + + + + Invalid Link + + + +

Invalid Link

+ + + diff --git a/public/de/invalid_link.html b/public/de/invalid_link.html new file mode 100644 index 0000000000..2d9ac315a1 --- /dev/null +++ b/public/de/invalid_link.html @@ -0,0 +1,18 @@ + + + + + + Invalid Link + + + +

Invalid Link

+ + + diff --git a/public/invalid_link.html b/public/invalid_link.html new file mode 100644 index 0000000000..2d9ac315a1 --- /dev/null +++ b/public/invalid_link.html @@ -0,0 +1,18 @@ + + + + + + Invalid Link + + + +

Invalid Link

+ + + diff --git a/public/invalid_verification_link.html b/public/invalid_verification_link.html new file mode 100644 index 0000000000..45b3e9e1ba --- /dev/null +++ b/public/invalid_verification_link.html @@ -0,0 +1,22 @@ + + + + + + Email Verification + + + +

Invalid verification link!

+
+ + +
+ + + diff --git a/public/link_send_fail.html b/public/link_send_fail.html new file mode 100644 index 0000000000..a51e55242b --- /dev/null +++ b/public/link_send_fail.html @@ -0,0 +1,19 @@ + + + + + + Email Verification + + + +

Invalid link!

+

No link sent. User not found or email already verified.

+ + + diff --git a/public/link_send_success.html b/public/link_send_success.html new file mode 100644 index 0000000000..8b48da2e06 --- /dev/null +++ b/public/link_send_success.html @@ -0,0 +1,19 @@ + + + + + + Email Verification + + + +

Link sent!

+

A new link has been sent. Check your email.

+ + + diff --git a/public/password_reset_success.html b/public/password_reset_success.html new file mode 100644 index 0000000000..937dffe8c9 --- /dev/null +++ b/public/password_reset_success.html @@ -0,0 +1,19 @@ + + + + + + Password Reset + + + +

Success!

+

Your password has been updated.

+ + + diff --git a/public/verify_email_success.html b/public/verify_email_success.html new file mode 100644 index 0000000000..8cb4d3b902 --- /dev/null +++ b/public/verify_email_success.html @@ -0,0 +1,18 @@ + + + + + + Email Verification + + + +

Email verified!

+

Successfully verified your email for account: {{username}}.

+ + + diff --git a/public_html/invalid_link.html b/public_html/invalid_link.html deleted file mode 100644 index b19044e52f..0000000000 --- a/public_html/invalid_link.html +++ /dev/null @@ -1,45 +0,0 @@ - - - - - Invalid Link - - - - -
-

Invalid Link

-
- - diff --git a/public_html/invalid_verification_link.html b/public_html/invalid_verification_link.html deleted file mode 100644 index fe6914fc82..0000000000 --- a/public_html/invalid_verification_link.html +++ /dev/null @@ -1,68 +0,0 @@ - - - - - Invalid Link - - - - - -
-

Invalid Verification Link

-
- - -
-
- - diff --git a/public_html/link_send_fail.html b/public_html/link_send_fail.html deleted file mode 100644 index 7f817a2cc4..0000000000 --- a/public_html/link_send_fail.html +++ /dev/null @@ -1,45 +0,0 @@ - - - - - Invalid Link - - - - -
-

No link sent. User not found or email already verified

-
- - diff --git a/public_html/link_send_success.html b/public_html/link_send_success.html deleted file mode 100644 index 55d9cad6f6..0000000000 --- a/public_html/link_send_success.html +++ /dev/null @@ -1,45 +0,0 @@ - - - - - Invalid Link - - - - -
-

Link Sent! Check your email.

-
- - diff --git a/public_html/password_reset_success.html b/public_html/password_reset_success.html deleted file mode 100644 index 774cbb350c..0000000000 --- a/public_html/password_reset_success.html +++ /dev/null @@ -1,27 +0,0 @@ - - - - - Password Reset - - -

Successfully updated your password!

- - diff --git a/public_html/verify_email_success.html b/public_html/verify_email_success.html deleted file mode 100644 index 774ea38a0d..0000000000 --- a/public_html/verify_email_success.html +++ /dev/null @@ -1,27 +0,0 @@ - - - - - Email Verification - - -

Successfully verified your email!

- - diff --git a/spec/EmailVerificationToken.spec.js b/spec/EmailVerificationToken.spec.js index 50b626de0d..f2cb752df6 100644 --- a/spec/EmailVerificationToken.spec.js +++ b/spec/EmailVerificationToken.spec.js @@ -36,10 +36,9 @@ describe('Email Verification Token Expiration: ', () => { url: sendEmailOptions.link, followRedirects: false, }).then(response => { - expect(response.status).toEqual(302); - expect(response.text).toEqual( - 'Found. Redirecting to http://localhost:8378/1/apps/invalid_verification_link.html?username=testEmailVerifyTokenValidity&appId=test' - ); + expect(response.status).toEqual(200); + expect(response.text).toContain('testEmailVerifyTokenValidity'); + expect(response.text).toContain('/apps/test/resend_verification_email'); done(); }); }, 1000); @@ -82,7 +81,7 @@ describe('Email Verification Token Expiration: ', () => { url: sendEmailOptions.link, followRedirects: false, }).then(response => { - expect(response.status).toEqual(302); + expect(response.status).toEqual(200); user .fetch() .then(() => { @@ -130,10 +129,9 @@ describe('Email Verification Token Expiration: ', () => { url: sendEmailOptions.link, followRedirects: false, }).then(response => { - expect(response.status).toEqual(302); - expect(response.text).toEqual( - 'Found. Redirecting to http://localhost:8378/1/apps/verify_email_success.html?username=testEmailVerifyTokenValidity' - ); + expect(response.status).toEqual(200); + expect(response.text).toContain('Email verified'); + expect(response.text).toContain('testEmailVerifyTokenValidity'); done(); }); }) @@ -171,7 +169,7 @@ describe('Email Verification Token Expiration: ', () => { url: sendEmailOptions.link, followRedirects: false, }).then(response => { - expect(response.status).toEqual(302); + expect(response.status).toEqual(200); user .fetch() .then(() => { @@ -218,7 +216,7 @@ describe('Email Verification Token Expiration: ', () => { url: sendEmailOptions.link, followRedirects: false, }).then(response => { - expect(response.status).toEqual(302); + expect(response.status).toEqual(200); Parse.User.logIn('testEmailVerifyTokenValidity', 'expiringToken') .then(user => { expect(typeof user).toBe('object'); @@ -310,7 +308,7 @@ describe('Email Verification Token Expiration: ', () => { url: sendEmailOptions.link, followRedirects: false, }).then(response => { - expect(response.status).toEqual(302); + expect(response.status).toEqual(200); const config = Config.get('test'); return config.database .find('_User', { @@ -369,7 +367,7 @@ describe('Email Verification Token Expiration: ', () => { url: sendEmailOptions.link, followRedirects: false, }).then(response => { - expect(response.status).toEqual(302); + expect(response.status).toEqual(200); return user.fetch(); }); }) @@ -384,10 +382,9 @@ describe('Email Verification Token Expiration: ', () => { url: sendEmailOptions.link, followRedirects: false, }).then(response => { - expect(response.status).toEqual(302); - expect(response.text).toEqual( - 'Found. Redirecting to http://localhost:8378/1/apps/verify_email_success.html?username=testEmailVerifyTokenValidity' - ); + expect(response.status).toEqual(200); + expect(response.text).toContain('Email verified'); + expect(response.text).toContain('testEmailVerifyTokenValidity'); done(); }); }) @@ -437,10 +434,10 @@ describe('Email Verification Token Expiration: ', () => { url: sendEmailOptions.link, followRedirects: false, }).then(response => { - expect(response.status).toEqual(302); - expect(response.text).toEqual( - 'Found. Redirecting to http://localhost:8378/1/apps/invalid_verification_link.html?username=testEmailVerifyTokenValidity&appId=test' - ); + expect(response.status).toEqual(200); + expect(response.text).toContain('Invalid verification link'); + expect(response.text).toContain('testEmailVerifyTokenValidity'); + expect(response.text).toContain('/apps/test/resend_verification_email'); done(); }); }) @@ -738,7 +735,7 @@ describe('Email Verification Token Expiration: ', () => { url: sendEmailOptions.link, followRedirects: false, }).then(response => { - expect(response.status).toEqual(302); + expect(response.status).toEqual(200); }); }) .then(() => { @@ -976,7 +973,7 @@ describe('Email Verification Token Expiration: ', () => { url: sendEmailOptions.link, followRedirects: false, }).then(response => { - expect(response.status).toEqual(302); + expect(response.status).toEqual(200); Parse.User.logIn('testEmailVerifyTokenValidity', 'expiringToken') .then(user => { expect(typeof user).toBe('object'); @@ -995,7 +992,7 @@ describe('Email Verification Token Expiration: ', () => { url: sendEmailOptions.link, followRedirects: false, }).then(response => { - expect(response.status).toEqual(302); + expect(response.status).toEqual(200); done(); }); }) diff --git a/spec/PasswordPolicy.spec.js b/spec/PasswordPolicy.spec.js index 6d00ddfa28..e31699426c 100644 --- a/spec/PasswordPolicy.spec.js +++ b/spec/PasswordPolicy.spec.js @@ -46,10 +46,8 @@ describe('Password Policy: ', () => { resolveWithFullResponse: true, }) .then(response => { - expect(response.status).toEqual(302); - expect(response.text).toEqual( - 'Found. Redirecting to http://localhost:8378/1/apps/invalid_link.html' - ); + expect(response.status).toEqual(200); + expect(response.text).toContain('Invalid Link'); done(); }) .catch(error => { @@ -106,8 +104,9 @@ describe('Password Policy: ', () => { followRedirects: false, }) .then(response => { - expect(response.status).toEqual(302); - const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=[a-zA-Z0-9]+\&id=test\&username=testResetTokenValidity/; + expect(response.status).toEqual(200); + expect(response.text).toContain('http://localhost:8378/1/apps/test/request_password_reset'); + const re = /id="token" value="([a-zA-Z0-9]+)"/ expect(response.text.match(re)).not.toBe(null); done(); }) @@ -621,8 +620,9 @@ describe('Password Policy: ', () => { resolveWithFullResponse: true, }) .then(response => { - expect(response.status).toEqual(302); - const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=user1/; + expect(response.status).toEqual(200); + expect(response.text).toContain('http://localhost:8378/1/apps/test/request_password_reset'); + const re = /id="token" value="([a-zA-Z0-9]+)"/ const match = response.text.match(re); if (!match) { fail('should have a token'); @@ -643,10 +643,8 @@ describe('Password Policy: ', () => { resolveWithFullResponse: true, }) .then(response => { - expect(response.status).toEqual(302); - expect(response.text).toEqual( - 'Found. Redirecting to http://localhost:8378/1/apps/password_reset_success.html?username=user1' - ); + expect(response.status).toEqual(200); + expect(response.text).toContain('Your password has been updated'); Parse.User.logIn('user1', 'has2init') .then(function () { @@ -713,8 +711,9 @@ describe('Password Policy: ', () => { resolveWithFullResponse: true, }) .then(response => { - expect(response.status).toEqual(302); - const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=user1/; + expect(response.status).toEqual(200); + expect(response.text).toContain('http://localhost:8378/1/apps/test/request_password_reset'); + const re = /id="token" value="([a-zA-Z0-9]+)"/ const match = response.text.match(re); if (!match) { fail('should have a token'); @@ -735,10 +734,8 @@ describe('Password Policy: ', () => { resolveWithFullResponse: true, }) .then(response => { - expect(response.status).toEqual(302); - expect(response.text).toEqual( - `Found. Redirecting to http://localhost:8378/1/apps/choose_password?username=user1&token=${token}&id=test&error=Password%20should%20contain%20at%20least%20one%20digit.&app=passwordPolicy` - ); + expect(response.status).toEqual(200); + expect(response.text).toContain('Password should contain at least one digit'); Parse.User.logIn('user1', 'has 1 digit') .then(function () { @@ -899,8 +896,9 @@ describe('Password Policy: ', () => { resolveWithFullResponse: true, }) .then(response => { - expect(response.status).toEqual(302); - const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=user1/; + expect(response.status).toEqual(200); + expect(response.text).toContain('http://localhost:8378/1/apps/test/request_password_reset'); + const re = /id="token" value="([a-zA-Z0-9]+)"/ const match = response.text.match(re); if (!match) { fail('should have a token'); @@ -921,10 +919,8 @@ describe('Password Policy: ', () => { resolveWithFullResponse: true, }) .then(response => { - expect(response.status).toEqual(302); - expect(response.text).toEqual( - `Found. Redirecting to http://localhost:8378/1/apps/choose_password?username=user1&token=${token}&id=test&error=Password%20cannot%20contain%20your%20username.&app=passwordPolicy` - ); + expect(response.status).toEqual(200); + expect(response.text).toContain('Password cannot contain your username'); Parse.User.logIn('user1', 'r@nd0m') .then(function () { @@ -990,8 +986,9 @@ describe('Password Policy: ', () => { simple: false, resolveWithFullResponse: true, }); - expect(response.status).toEqual(302); - const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=user1/; + expect(response.status).toEqual(200); + expect(response.text).toContain('http://localhost:8378/1/apps/test/request_password_reset'); + const re = /id="token" value="([a-zA-Z0-9]+)"/ const match = response.text.match(re); if (!match) { fail('should have a token'); @@ -1050,8 +1047,9 @@ describe('Password Policy: ', () => { resolveWithFullResponse: true, }) .then(response => { - expect(response.status).toEqual(302); - const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=user1/; + expect(response.status).toEqual(200); + expect(response.text).toContain('http://localhost:8378/1/apps/test/request_password_reset'); + const re = /id="token" value="([a-zA-Z0-9]+)"/ const match = response.text.match(re); if (!match) { fail('should have a token'); @@ -1072,10 +1070,8 @@ describe('Password Policy: ', () => { resolveWithFullResponse: true, }) .then(response => { - expect(response.status).toEqual(302); - expect(response.text).toEqual( - 'Found. Redirecting to http://localhost:8378/1/apps/password_reset_success.html?username=user1' - ); + expect(response.status).toEqual(200); + expect(response.text).toContain('Your password has been updated'); Parse.User.logIn('user1', 'uuser11') .then(function () { @@ -1316,8 +1312,9 @@ describe('Password Policy: ', () => { resolveWithFullResponse: true, }) .then(response => { - expect(response.status).toEqual(302); - const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=user1/; + expect(response.status).toEqual(200); + expect(response.text).toContain('http://localhost:8378/1/apps/test/request_password_reset'); + const re = /id="token" value="([a-zA-Z0-9]+)"/ const match = response.text.match(re); if (!match) { fail('should have a token'); @@ -1338,10 +1335,8 @@ describe('Password Policy: ', () => { resolveWithFullResponse: true, }) .then(response => { - expect(response.status).toEqual(302); - expect(response.text).toEqual( - 'Found. Redirecting to http://localhost:8378/1/apps/password_reset_success.html?username=user1' - ); + expect(response.status).toEqual(200); + expect(response.text).toContain('Your password has been updated'); Parse.User.logIn('user1', 'uuser11') .then(function () { @@ -1471,8 +1466,9 @@ describe('Password Policy: ', () => { followRedirects: false, }) .then(response => { - expect(response.status).toEqual(302); - const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=user1/; + expect(response.status).toEqual(200); + expect(response.text).toContain('http://localhost:8378/1/apps/test/request_password_reset'); + const re = /id="token" value="([a-zA-Z0-9]+)"/ const match = response.text.match(re); if (!match) { fail('should have a token'); @@ -1498,10 +1494,11 @@ describe('Password Policy: ', () => { .then(data => { const response = data[0]; const token = data[1]; - expect(response.status).toEqual(302); - expect(response.text).toEqual( - `Found. Redirecting to http://localhost:8378/1/apps/choose_password?username=user1&token=${token}&id=test&error=New%20password%20should%20not%20be%20the%20same%20as%20last%201%20passwords.&app=passwordPolicy` - ); + expect(response.status).toEqual(200); + expect(response.text).toContain(token); + expect(response.text).toContain(user.getUsername()); + expect(response.text).toContain('http://localhost:8378/1/apps/test/request_password_reset'); + expect(response.text).toContain('New password should not be the same as last 1 passwords'); done(); return Promise.resolve(); }) diff --git a/spec/PublicAPI.spec.js b/spec/PublicAPI.spec.js index 545662914f..9add351cb3 100644 --- a/spec/PublicAPI.spec.js +++ b/spec/PublicAPI.spec.js @@ -88,7 +88,7 @@ describe('public API', () => { appName: 'unused', publicServerURL: 'http://localhost:8378/1', }).then(() => { - request('http://localhost:8378/1/apps/choose_password?id=test', (err, httpResponse) => { + request('http://localhost:8378/1/apps/choose_password?appId=test', (err, httpResponse) => { expect(httpResponse.status).toBe(200); done(); }); @@ -122,7 +122,7 @@ describe('public API without publicServerURL', () => { }); it('should get 404 choose_password', done => { - request('http://localhost:8378/1/apps/choose_password?id=test', (err, httpResponse) => { + request('http://localhost:8378/1/apps/choose_password?appId=test', (err, httpResponse) => { expect(httpResponse.status).toBe(404); done(); }); diff --git a/spec/RegexVulnerabilities.spec.js b/spec/RegexVulnerabilities.spec.js index 1a96ebfdf5..60b701a866 100644 --- a/spec/RegexVulnerabilities.spec.js +++ b/spec/RegexVulnerabilities.spec.js @@ -132,8 +132,8 @@ describe('Regex Vulnerabilities', function () { url: `${serverURL}/apps/test/request_password_reset?username=someemail@somedomain.com&token[$regex]=`, method: 'GET', }); - expect(passwordResetResponse.status).toEqual(302); - expect(passwordResetResponse.headers.location).toMatch(`\\/invalid\\_link\\.html`); + expect(passwordResetResponse.status).toEqual(200); + expect(passwordResetResponse.text).toContain('Invalid Link'); await request({ url: `${serverURL}/apps/test/request_password_reset`, method: 'POST', @@ -170,10 +170,9 @@ describe('Regex Vulnerabilities', function () { url: `${serverURL}/apps/test/request_password_reset?username=someemail@somedomain.com&token=${token}`, method: 'GET', }); - expect(passwordResetResponse.status).toEqual(302); - expect(passwordResetResponse.headers.location).toMatch( - `\\/choose\\_password\\?token\\=${token}\\&` - ); + expect(passwordResetResponse.status).toEqual(200); + expect(passwordResetResponse.text).toContain(token); + expect(passwordResetResponse.text).toContain('http://localhost:8378/1/apps/test/request_password_reset'); await request({ url: `${serverURL}/apps/test/request_password_reset`, method: 'POST', diff --git a/spec/ValidationAndPasswordsReset.spec.js b/spec/ValidationAndPasswordsReset.spec.js index 94d9793a39..8dde98bbbd 100644 --- a/spec/ValidationAndPasswordsReset.spec.js +++ b/spec/ValidationAndPasswordsReset.spec.js @@ -1,8 +1,10 @@ 'use strict'; +const { PublicAPIRouter, pages } = require('../lib/Routers/PublicAPIRouter'); const MockEmailAdapterWithOptions = require('./MockEmailAdapterWithOptions'); const request = require('../lib/request'); const Config = require('../lib/Config'); +const Utils = require('../lib/Utils'); describe('Custom Pages, Email Verification, Password Reset', () => { it('should set the custom pages', done => { @@ -271,10 +273,9 @@ describe('Custom Pages, Email Verification, Password Reset', () => { url: sendEmailOptions.link, followRedirects: false, }).then(response => { - expect(response.status).toEqual(302); - expect(response.text).toEqual( - 'Found. Redirecting to http://localhost:8378/1/apps/verify_email_success.html?username=user' - ); + expect(response.status).toEqual(200); + expect(response.text).toContain('Successfully verified your email'); + expect(response.text).toContain('account: user'); user .fetch() .then( @@ -596,7 +597,7 @@ describe('Custom Pages, Email Verification, Password Reset', () => { }) .then(() => { user.setPassword('other-password'); - user.setUsername('user'); + user.setUsername('exampleUsername'); user.set('email', 'user@parse.com'); return user.signUp(); }) @@ -606,10 +607,9 @@ describe('Custom Pages, Email Verification, Password Reset', () => { url: sendEmailOptions.link, followRedirects: false, }).then(response => { - expect(response.status).toEqual(302); - expect(response.text).toEqual( - 'Found. Redirecting to http://localhost:8378/1/apps/verify_email_success.html?username=user' - ); + expect(response.status).toEqual(200); + expect(response.text).toContain('Successfully verified your email'); + expect(response.text).toContain('exampleUsername'); user .fetch() .then( @@ -646,10 +646,8 @@ describe('Custom Pages, Email Verification, Password Reset', () => { url: 'http://localhost:8378/1/apps/test/verify_email', followRedirects: false, }).then(response => { - expect(response.status).toEqual(302); - expect(response.text).toEqual( - 'Found. Redirecting to http://localhost:8378/1/apps/invalid_link.html' - ); + expect(response.status).toEqual(200); + expect(response.text).toContain('Invalid Link'); done(); }); }); @@ -667,13 +665,12 @@ describe('Custom Pages, Email Verification, Password Reset', () => { publicServerURL: 'http://localhost:8378/1', }).then(() => { request({ - url: 'http://localhost:8378/1/apps/test/verify_email?token=asdfasdf&username=sadfasga', + url: 'http://localhost:8378/1/apps/test/verify_email?token=exampleToken&username=exampleUsername', followRedirects: false, }).then(response => { - expect(response.status).toEqual(302); - expect(response.text).toEqual( - 'Found. Redirecting to http://localhost:8378/1/apps/invalid_verification_link.html?username=sadfasga&appId=test' - ); + expect(response.status).toEqual(200); + expect(response.text).toContain('value="exampleUsername"'); + expect(response.text).toContain('action="/apps/test/resend_verification_email"'); done(); }); }); @@ -695,12 +692,12 @@ describe('Custom Pages, Email Verification, Password Reset', () => { method: 'POST', followRedirects: false, body: { - username: 'sadfasga', + username: 'exampleUsername', }, }).then(response => { - expect(response.status).toEqual(302); + expect(response.status).toEqual(303); expect(response.text).toEqual( - 'Found. Redirecting to http://localhost:8378/1/apps/link_send_fail.html' + 'Found. Redirecting to http://localhost:8378/apps/link_send_fail.html' ); done(); }); @@ -712,13 +709,12 @@ describe('Custom Pages, Email Verification, Password Reset', () => { const emailAdapter = { sendVerificationEmail: () => { request({ - url: 'http://localhost:8378/1/apps/test/verify_email?token=invalid&username=zxcv', + url: 'http://localhost:8378/1/apps/test/verify_email?token=invalidToken&username=exampleUsername', followRedirects: false, }).then(response => { - expect(response.status).toEqual(302); - expect(response.text).toEqual( - 'Found. Redirecting to http://localhost:8378/1/apps/invalid_verification_link.html?username=zxcv&appId=test' - ); + expect(response.status).toEqual(200); + expect(response.text).toContain('value="exampleUsername"'); + expect(response.text).toContain('action="/apps/test/resend_verification_email"'); user.fetch().then(() => { expect(user.get('emailVerified')).toEqual(false); done(); @@ -729,13 +725,13 @@ describe('Custom Pages, Email Verification, Password Reset', () => { sendMail: () => {}, }; reconfigureServer({ - appName: 'emailing app', + appName: 'ExampleApp', verifyUserEmails: true, emailAdapter: emailAdapter, publicServerURL: 'http://localhost:8378/1', }).then(() => { - user.setPassword('asdf'); - user.setUsername('zxcv'); + user.setPassword('examplePassword'); + user.setUsername('exampleUsername'); user.set('email', 'user@parse.com'); user.signUp(null, { success: () => {}, @@ -756,22 +752,23 @@ describe('Custom Pages, Email Verification, Password Reset', () => { url: options.link, followRedirects: false, }).then(response => { - expect(response.status).toEqual(302); - const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=[a-zA-Z0-9]+\&id=test\&username=zxcv%2Bzxcv/; - expect(response.text.match(re)).not.toBe(null); + expect(response.status).toEqual(200); + expect(response.text).toContain('ExampleApp'); + expect(response.text).toContain('exampleUsername'); + expect(response.text).toContain('http://localhost:8378/1/apps/test/request_password_reset'); done(); }); }, sendMail: () => {}, }; reconfigureServer({ - appName: 'emailing app', + appName: 'ExampleApp', verifyUserEmails: true, emailAdapter: emailAdapter, publicServerURL: 'http://localhost:8378/1', }).then(() => { - user.setPassword('asdf'); - user.setUsername('zxcv+zxcv'); + user.setPassword('examplePassword'); + user.setUsername('exampleUsername'); user.set('email', 'user@parse.com'); user.signUp().then(() => { Parse.User.requestPasswordReset('user@parse.com', { @@ -801,10 +798,8 @@ describe('Custom Pages, Email Verification, Password Reset', () => { 'http://localhost:8378/1/apps/test/request_password_reset?token=asdfasdf&username=sadfasga', followRedirects: false, }).then(response => { - expect(response.status).toEqual(302); - expect(response.text).toEqual( - 'Found. Redirecting to http://localhost:8378/1/apps/invalid_link.html' - ); + expect(response.status).toEqual(200); + expect(response.text).toContain('Invalid Link'); done(); }); }); @@ -819,8 +814,9 @@ describe('Custom Pages, Email Verification, Password Reset', () => { url: options.link, followRedirects: false, }).then(response => { - expect(response.status).toEqual(302); - const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=zxcv/; + expect(response.status).toEqual(200); + expect(response.text).toContain('http://localhost:8378/1/apps/test/request_password_reset'); + const re = /id="token" value="([a-zA-Z0-9]+)"/ const match = response.text.match(re); if (!match) { fail('should have a token'); @@ -838,10 +834,8 @@ describe('Custom Pages, Email Verification, Password Reset', () => { }, followRedirects: false, }).then(response => { - expect(response.status).toEqual(302); - expect(response.text).toEqual( - 'Found. Redirecting to http://localhost:8378/1/apps/password_reset_success.html?username=zxcv' - ); + expect(response.status).toEqual(200); + expect(response.text).toContain('Your password has been updated.'); Parse.User.logIn('zxcv', 'hello').then( function () { @@ -896,8 +890,9 @@ describe('Custom Pages, Email Verification, Password Reset', () => { url: options.link, followRedirects: false, }).then(response => { - expect(response.status).toEqual(302); - const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=zxcv%2B1/; + expect(response.status).toEqual(200); + expect(response.text).toContain('http://localhost:8378/1/apps/test/request_password_reset'); + const re = /id="token" value="([a-zA-Z0-9]+)"/ const match = response.text.match(re); if (!match) { fail('should have a token'); @@ -915,10 +910,8 @@ describe('Custom Pages, Email Verification, Password Reset', () => { }, followRedirects: false, }).then(response => { - expect(response.status).toEqual(302); - expect(response.text).toEqual( - 'Found. Redirecting to http://localhost:8378/1/apps/password_reset_success.html?username=zxcv%2B1' - ); + expect(response.status).toEqual(200); + expect(response.text).toContain('Your password has been updated.'); done(); }); }); @@ -955,8 +948,9 @@ describe('Custom Pages, Email Verification, Password Reset', () => { url: options.link, followRedirects: false, }); - expect(response.status).toEqual(302); - const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=zxcv/; + expect(response.status).toEqual(200); + expect(response.text).toContain('http://localhost:8378/1/apps/test/request_password_reset'); + const re = /id="token" value="([a-zA-Z0-9]+)"/ const match = response.text.match(re); if (!match) { fail('should have a token'); @@ -1082,4 +1076,135 @@ describe('Custom Pages, Email Verification, Password Reset', () => { done(); }); }); + + describe('localization of custom pages', () => { + let router = new PublicAPIRouter(); + let req; + let pageResponse; + let redirectResponse; + const config = { + appId: "test", + appName: 'ExampleAppName', + verifyUserEmails: true, + emailAdapter: { + sendVerificationEmail: () => Promise.resolve(), + sendPasswordResetEmail: () => Promise.resolve(), + sendMail: () => {}, + }, + publicServerURL: 'http://localhost:8378/1', + enablePageLocalization: true, + }; + + beforeEach(async () => { + router = new PublicAPIRouter(); + pageResponse = spyOn(router, 'pageResponse').and.callThrough(); + redirectResponse = spyOn(router, 'redirectResponse').and.callThrough(); + req = { + method: 'GET', + config: { + customPages: {}, + enablePageLocalization: true, + publicServerURL: 'http://example.com', + }, + query: { + locale: 'de-AT', + } + } + }); + + it('returns default file if localization is disabled', async () => { + delete req.config.enablePageLocalization; + + await expectAsync(router.goToPage(req, pages.invalidLink)).toBeResolved(); + expect(pageResponse.calls.all()[0].args[1]).toBeDefined(); + expect(pageResponse.calls.all()[0].args[1]).not.toMatch(new RegExp(`\/de(-AT)?\/${pages.invalidLink.defaultFile}`)); + }); + + it('returns default file if no locale is specified', async () => { + delete req.query.locale; + + await expectAsync(router.goToPage(req, pages.invalidLink)).toBeResolved(); + expect(pageResponse.calls.all()[0].args[1]).toBeDefined(); + expect(pageResponse.calls.all()[0].args[1]).not.toMatch(new RegExp(`\/de(-AT)?\/${pages.invalidLink.defaultFile}`)); + }); + + it('returns custom page regardless of localization enabled', async () => { + req.config.customPages = { invalidLink: 'http://invalid-link.example.com' }; + + await expectAsync(router.goToPage(req, pages.invalidLink)).toBeResolved(); + expect(pageResponse).not.toHaveBeenCalled(); + expect(redirectResponse.calls.all()[0].args[0]).toBe(req.config.customPages.invalidLink); + }); + + it('returns file for locale match', async () => { + await expectAsync(router.goToPage(req, pages.invalidLink)).toBeResolved(); + expect(pageResponse.calls.all()[0].args[1]).toBeDefined(); + expect(pageResponse.calls.all()[0].args[1]).toMatch(new RegExp(`\/de-AT\/${pages.invalidLink.defaultFile}`)); + }); + + it('returns file for language match', async () => { + // Pretend no locale matching file exists + spyOn(Utils, 'fileExists').and.callFake(async (path) => { + return !path.includes(`/de-AT/${pages.invalidLink.defaultFile}`); + }); + + await expectAsync(router.goToPage(req, pages.invalidLink)).toBeResolved(); + expect(pageResponse.calls.all()[0].args[1]).toBeDefined(); + expect(pageResponse.calls.all()[0].args[1]).toMatch(new RegExp(`\/de\/${pages.invalidLink.defaultFile}`)); + }); + + it('returns default file for neither locale nor language match', async () => { + req.query.locale = 'yo-LO'; + + await expectAsync(router.goToPage(req, pages.invalidLink)).toBeResolved(); + expect(pageResponse.calls.all()[0].args[1]).toBeDefined(); + expect(pageResponse.calls.all()[0].args[1]).not.toMatch(new RegExp(`\/yo(-LO)?\/${pages.invalidLink.defaultFile}`)); + }); + + it('returns a file for GET request', async () => { + await expectAsync(router.goToPage(req, pages.invalidLink)).toBeResolved(); + expect(pageResponse).toHaveBeenCalled(); + expect(redirectResponse).not.toHaveBeenCalled(); + }); + + it('returns a redirect for POST request', async () => { + req.method = 'POST'; + await expectAsync(router.goToPage(req, pages.invalidLink)).toBeResolved(); + expect(pageResponse).not.toHaveBeenCalled(); + expect(redirectResponse).toHaveBeenCalled(); + }); + + it('returns a redirect for custom pages for GET and POST', async () => { + req.config.customPages = { invalidLink: 'http://invalid-link.example.com' }; + + for (const method of ['GET', 'POST']) { + req.method = method; + await expectAsync(router.goToPage(req, pages.invalidLink)).toBeResolved(); + expect(pageResponse).not.toHaveBeenCalled(); + expect(redirectResponse).toHaveBeenCalled(); + } + }); + + it('localizes invalid link page with file response (e2e test)', async () => { + await reconfigureServer(config); + const response = await request({ + url: 'http://localhost:8378/1/apps/test/request_password_reset?token=exampleToken&username=exampleUsername&locale=de-AT', + followRedirects: false, + method: 'POST' + }); + expect(response.status).toEqual(303); + expect(response.headers.location).toEqual('http://localhost:8378/apps/de-AT/invalid_link.html'); + }); + + it('localizes invalid link page with redirect response (e2e test)', async () => { + await reconfigureServer(config); + const response = await request({ + url: 'http://localhost:8378/1/apps/test/request_password_reset?token=exampleToken&username=exampleUsername&locale=de-AT', + followRedirects: false, + method: 'GET' + }); + expect(response.status).toEqual(200); + expect(response.text).toContain(''); + }); + }); }); diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index 22e0680fce..2359123568 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -3,626 +3,611 @@ This code has been generated by resources/buildConfigDefinitions.js Do not edit manually, but update Options/index.js */ -var parsers = require('./parsers'); +var parsers = require("./parsers"); module.exports.ParseServerOptions = { - accountLockout: { - env: 'PARSE_SERVER_ACCOUNT_LOCKOUT', - help: 'account lockout policy for failed login attempts', - action: parsers.objectParser, - }, - allowClientClassCreation: { - env: 'PARSE_SERVER_ALLOW_CLIENT_CLASS_CREATION', - help: 'Enable (or disable) client class creation, defaults to true', - action: parsers.booleanParser, - default: true, - }, - allowCustomObjectId: { - env: 'PARSE_SERVER_ALLOW_CUSTOM_OBJECT_ID', - help: 'Enable (or disable) custom objectId', - action: parsers.booleanParser, - default: false, - }, - allowHeaders: { - env: 'PARSE_SERVER_ALLOW_HEADERS', - help: 'Add headers to Access-Control-Allow-Headers', - action: parsers.arrayParser, - }, - allowOrigin: { - env: 'PARSE_SERVER_ALLOW_ORIGIN', - help: 'Sets the origin to Access-Control-Allow-Origin', - }, - analyticsAdapter: { - env: 'PARSE_SERVER_ANALYTICS_ADAPTER', - help: 'Adapter module for the analytics', - action: parsers.moduleOrObjectParser, - }, - appId: { - env: 'PARSE_SERVER_APPLICATION_ID', - help: 'Your Parse Application ID', - required: true, - }, - appName: { - env: 'PARSE_SERVER_APP_NAME', - help: 'Sets the app name', - }, - auth: { - env: 'PARSE_SERVER_AUTH_PROVIDERS', - help: - 'Configuration for your authentication providers, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#oauth-and-3rd-party-authentication', - action: parsers.objectParser, - }, - cacheAdapter: { - env: 'PARSE_SERVER_CACHE_ADAPTER', - help: 'Adapter module for the cache', - action: parsers.moduleOrObjectParser, - }, - cacheMaxSize: { - env: 'PARSE_SERVER_CACHE_MAX_SIZE', - help: 'Sets the maximum size for the in memory cache, defaults to 10000', - action: parsers.numberParser('cacheMaxSize'), - default: 10000, - }, - cacheTTL: { - env: 'PARSE_SERVER_CACHE_TTL', - help: 'Sets the TTL for the in memory cache (in ms), defaults to 5000 (5 seconds)', - action: parsers.numberParser('cacheTTL'), - default: 5000, - }, - clientKey: { - env: 'PARSE_SERVER_CLIENT_KEY', - help: 'Key for iOS, MacOS, tvOS clients', - }, - cloud: { - env: 'PARSE_SERVER_CLOUD', - help: 'Full path to your cloud code main.js', - }, - cluster: { - env: 'PARSE_SERVER_CLUSTER', - help: 'Run with cluster, optionally set the number of processes default to os.cpus().length', - action: parsers.numberOrBooleanParser, - }, - collectionPrefix: { - env: 'PARSE_SERVER_COLLECTION_PREFIX', - help: 'A collection prefix for the classes', - default: '', - }, - customPages: { - env: 'PARSE_SERVER_CUSTOM_PAGES', - help: 'custom pages for password validation and reset', - action: parsers.objectParser, - default: {}, - }, - databaseAdapter: { - env: 'PARSE_SERVER_DATABASE_ADAPTER', - help: 'Adapter module for the database', - action: parsers.moduleOrObjectParser, - }, - databaseOptions: { - env: 'PARSE_SERVER_DATABASE_OPTIONS', - help: 'Options to pass to the mongodb client', - action: parsers.objectParser, - }, - databaseURI: { - env: 'PARSE_SERVER_DATABASE_URI', - help: 'The full URI to your database. Supported databases are mongodb or postgres.', - required: true, - default: 'mongodb://localhost:27017/parse', - }, - directAccess: { - env: 'PARSE_SERVER_ENABLE_EXPERIMENTAL_DIRECT_ACCESS', - help: - 'Replace HTTP Interface when using JS SDK in current node runtime, defaults to false. Caution, this is an experimental feature that may not be appropriate for production.', - action: parsers.booleanParser, - default: false, - }, - dotNetKey: { - env: 'PARSE_SERVER_DOT_NET_KEY', - help: 'Key for Unity and .Net SDK', - }, - emailAdapter: { - env: 'PARSE_SERVER_EMAIL_ADAPTER', - help: 'Adapter module for email sending', - action: parsers.moduleOrObjectParser, - }, - emailVerifyTokenReuseIfValid: { - env: 'PARSE_SERVER_EMAIL_VERIFY_TOKEN_REUSE_IF_VALID', - help: - 'an existing email verify token should be reused when resend verification email is requested', - action: parsers.booleanParser, - default: false, - }, - emailVerifyTokenValidityDuration: { - env: 'PARSE_SERVER_EMAIL_VERIFY_TOKEN_VALIDITY_DURATION', - help: 'Email verification token validity duration, in seconds', - action: parsers.numberParser('emailVerifyTokenValidityDuration'), - }, - enableAnonymousUsers: { - env: 'PARSE_SERVER_ENABLE_ANON_USERS', - help: 'Enable (or disable) anonymous users, defaults to true', - action: parsers.booleanParser, - default: true, - }, - enableExpressErrorHandler: { - env: 'PARSE_SERVER_ENABLE_EXPRESS_ERROR_HANDLER', - help: 'Enables the default express error handler for all errors', - action: parsers.booleanParser, - default: false, - }, - enableSingleSchemaCache: { - env: 'PARSE_SERVER_ENABLE_SINGLE_SCHEMA_CACHE', - help: - 'Use a single schema cache shared across requests. Reduces number of queries made to _SCHEMA, defaults to false, i.e. unique schema cache per request.', - action: parsers.booleanParser, - default: false, - }, - encryptionKey: { - env: 'PARSE_SERVER_ENCRYPTION_KEY', - help: 'Key for encrypting your files', - }, - expireInactiveSessions: { - env: 'PARSE_SERVER_EXPIRE_INACTIVE_SESSIONS', - help: 'Sets wether we should expire the inactive sessions, defaults to true', - action: parsers.booleanParser, - default: true, - }, - fileKey: { - env: 'PARSE_SERVER_FILE_KEY', - help: 'Key for your files', - }, - filesAdapter: { - env: 'PARSE_SERVER_FILES_ADAPTER', - help: 'Adapter module for the files sub-system', - action: parsers.moduleOrObjectParser, - }, - fileUpload: { - env: 'PARSE_SERVER_FILE_UPLOAD_OPTIONS', - help: 'Options for file uploads', - action: parsers.objectParser, - default: {}, - }, - graphQLPath: { - env: 'PARSE_SERVER_GRAPHQL_PATH', - help: 'Mount path for the GraphQL endpoint, defaults to /graphql', - default: '/graphql', - }, - graphQLSchema: { - env: 'PARSE_SERVER_GRAPH_QLSCHEMA', - help: 'Full path to your GraphQL custom schema.graphql file', - }, - host: { - env: 'PARSE_SERVER_HOST', - help: 'The host to serve ParseServer on, defaults to 0.0.0.0', - default: '0.0.0.0', - }, - idempotencyOptions: { - env: 'PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_OPTIONS', - help: - 'Options for request idempotency to deduplicate identical requests that may be caused by network issues. Caution, this is an experimental feature that may not be appropriate for production.', - action: parsers.objectParser, - default: {}, - }, - javascriptKey: { - env: 'PARSE_SERVER_JAVASCRIPT_KEY', - help: 'Key for the Javascript SDK', - }, - jsonLogs: { - env: 'JSON_LOGS', - help: 'Log as structured JSON objects', - action: parsers.booleanParser, - }, - liveQuery: { - env: 'PARSE_SERVER_LIVE_QUERY', - help: "parse-server's LiveQuery configuration object", - action: parsers.objectParser, - }, - liveQueryServerOptions: { - env: 'PARSE_SERVER_LIVE_QUERY_SERVER_OPTIONS', - help: 'Live query server configuration options (will start the liveQuery server)', - action: parsers.objectParser, - }, - loggerAdapter: { - env: 'PARSE_SERVER_LOGGER_ADAPTER', - help: 'Adapter module for the logging sub-system', - action: parsers.moduleOrObjectParser, - }, - logLevel: { - env: 'PARSE_SERVER_LOG_LEVEL', - help: 'Sets the level for logs', - }, - logsFolder: { - env: 'PARSE_SERVER_LOGS_FOLDER', - help: "Folder for the logs (defaults to './logs'); set to null to disable file based logging", - default: './logs', - }, - masterKey: { - env: 'PARSE_SERVER_MASTER_KEY', - help: 'Your Parse Master Key', - required: true, - }, - masterKeyIps: { - env: 'PARSE_SERVER_MASTER_KEY_IPS', - help: 'Restrict masterKey to be used by only these ips, defaults to [] (allow all ips)', - action: parsers.arrayParser, - default: [], - }, - maxLimit: { - env: 'PARSE_SERVER_MAX_LIMIT', - help: 'Max value for limit option on queries, defaults to unlimited', - action: parsers.numberParser('maxLimit'), - }, - maxLogFiles: { - env: 'PARSE_SERVER_MAX_LOG_FILES', - help: - "Maximum number of logs to keep. If not set, no logs will be removed. This can be a number of files or number of days. If using days, add 'd' as the suffix. (default: null)", - action: parsers.objectParser, - }, - maxUploadSize: { - env: 'PARSE_SERVER_MAX_UPLOAD_SIZE', - help: 'Max file size for uploads, defaults to 20mb', - default: '20mb', - }, - middleware: { - env: 'PARSE_SERVER_MIDDLEWARE', - help: 'middleware for express server, can be string or function', - }, - mountGraphQL: { - env: 'PARSE_SERVER_MOUNT_GRAPHQL', - help: 'Mounts the GraphQL endpoint', - action: parsers.booleanParser, - default: false, - }, - mountPath: { - env: 'PARSE_SERVER_MOUNT_PATH', - help: 'Mount path for the server, defaults to /parse', - default: '/parse', - }, - mountPlayground: { - env: 'PARSE_SERVER_MOUNT_PLAYGROUND', - help: 'Mounts the GraphQL Playground - never use this option in production', - action: parsers.booleanParser, - default: false, - }, - objectIdSize: { - env: 'PARSE_SERVER_OBJECT_ID_SIZE', - help: "Sets the number of characters in generated object id's, default 10", - action: parsers.numberParser('objectIdSize'), - default: 10, - }, - passwordPolicy: { - env: 'PARSE_SERVER_PASSWORD_POLICY', - help: 'Password policy for enforcing password related rules', - action: parsers.objectParser, - }, - playgroundPath: { - env: 'PARSE_SERVER_PLAYGROUND_PATH', - help: 'Mount path for the GraphQL Playground, defaults to /playground', - default: '/playground', - }, - port: { - env: 'PORT', - help: 'The port to run the ParseServer, defaults to 1337.', - action: parsers.numberParser('port'), - default: 1337, - }, - preserveFileName: { - env: 'PARSE_SERVER_PRESERVE_FILE_NAME', - help: 'Enable (or disable) the addition of a unique hash to the file names', - action: parsers.booleanParser, - default: false, - }, - preventLoginWithUnverifiedEmail: { - env: 'PARSE_SERVER_PREVENT_LOGIN_WITH_UNVERIFIED_EMAIL', - help: - 'Prevent user from login if email is not verified and PARSE_SERVER_VERIFY_USER_EMAILS is true, defaults to false', - action: parsers.booleanParser, - default: false, - }, - protectedFields: { - env: 'PARSE_SERVER_PROTECTED_FIELDS', - help: 'Protected fields that should be treated with extra security when fetching details.', - action: parsers.objectParser, - default: { - _User: { - '*': ['email'], - }, - }, - }, - publicServerURL: { - env: 'PARSE_PUBLIC_SERVER_URL', - help: 'Public URL to your parse server with http:// or https://.', - }, - push: { - env: 'PARSE_SERVER_PUSH', - help: - 'Configuration for push, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#push-notifications', - action: parsers.objectParser, - }, - readOnlyMasterKey: { - env: 'PARSE_SERVER_READ_ONLY_MASTER_KEY', - help: 'Read-only key, which has the same capabilities as MasterKey without writes', - }, - restAPIKey: { - env: 'PARSE_SERVER_REST_API_KEY', - help: 'Key for REST calls', - }, - revokeSessionOnPasswordReset: { - env: 'PARSE_SERVER_REVOKE_SESSION_ON_PASSWORD_RESET', - help: - "When a user changes their password, either through the reset password email or while logged in, all sessions are revoked if this is true. Set to false if you don't want to revoke sessions.", - action: parsers.booleanParser, - default: true, - }, - scheduledPush: { - env: 'PARSE_SERVER_SCHEDULED_PUSH', - help: 'Configuration for push scheduling, defaults to false.', - action: parsers.booleanParser, - default: false, - }, - schemaCacheTTL: { - env: 'PARSE_SERVER_SCHEMA_CACHE_TTL', - help: - 'The TTL for caching the schema for optimizing read/write operations. You should put a long TTL when your DB is in production. default to 5000; set 0 to disable.', - action: parsers.numberParser('schemaCacheTTL'), - default: 5000, - }, - serverCloseComplete: { - env: 'PARSE_SERVER_SERVER_CLOSE_COMPLETE', - help: 'Callback when server has closed', - }, - serverStartComplete: { - env: 'PARSE_SERVER_SERVER_START_COMPLETE', - help: 'Callback when server has started', - }, - serverURL: { - env: 'PARSE_SERVER_URL', - help: 'URL to your parse server with http:// or https://.', - required: true, - }, - sessionLength: { - env: 'PARSE_SERVER_SESSION_LENGTH', - help: 'Session duration, in seconds, defaults to 1 year', - action: parsers.numberParser('sessionLength'), - default: 31536000, - }, - silent: { - env: 'SILENT', - help: 'Disables console output', - action: parsers.booleanParser, - }, - startLiveQueryServer: { - env: 'PARSE_SERVER_START_LIVE_QUERY_SERVER', - help: 'Starts the liveQuery server', - action: parsers.booleanParser, - }, - userSensitiveFields: { - env: 'PARSE_SERVER_USER_SENSITIVE_FIELDS', - help: - 'Personally identifiable information fields in the user table the should be removed for non-authorized users. Deprecated @see protectedFields', - action: parsers.arrayParser, - }, - verbose: { - env: 'VERBOSE', - help: 'Set the logging to verbose', - action: parsers.booleanParser, - }, - verifyUserEmails: { - env: 'PARSE_SERVER_VERIFY_USER_EMAILS', - help: 'Enable (or disable) user email validation, defaults to false', - action: parsers.booleanParser, - default: false, - }, - webhookKey: { - env: 'PARSE_SERVER_WEBHOOK_KEY', - help: 'Key sent with outgoing webhook calls', - }, + "accountLockout": { + "env": "PARSE_SERVER_ACCOUNT_LOCKOUT", + "help": "account lockout policy for failed login attempts", + "action": parsers.objectParser + }, + "allowClientClassCreation": { + "env": "PARSE_SERVER_ALLOW_CLIENT_CLASS_CREATION", + "help": "Enable (or disable) client class creation, defaults to true", + "action": parsers.booleanParser, + "default": true + }, + "allowCustomObjectId": { + "env": "PARSE_SERVER_ALLOW_CUSTOM_OBJECT_ID", + "help": "Enable (or disable) custom objectId", + "action": parsers.booleanParser, + "default": false + }, + "allowHeaders": { + "env": "PARSE_SERVER_ALLOW_HEADERS", + "help": "Add headers to Access-Control-Allow-Headers", + "action": parsers.arrayParser + }, + "allowOrigin": { + "env": "PARSE_SERVER_ALLOW_ORIGIN", + "help": "Sets the origin to Access-Control-Allow-Origin" + }, + "analyticsAdapter": { + "env": "PARSE_SERVER_ANALYTICS_ADAPTER", + "help": "Adapter module for the analytics", + "action": parsers.moduleOrObjectParser + }, + "appId": { + "env": "PARSE_SERVER_APPLICATION_ID", + "help": "Your Parse Application ID", + "required": true + }, + "appName": { + "env": "PARSE_SERVER_APP_NAME", + "help": "Sets the app name" + }, + "auth": { + "env": "PARSE_SERVER_AUTH_PROVIDERS", + "help": "Configuration for your authentication providers, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#oauth-and-3rd-party-authentication", + "action": parsers.objectParser + }, + "cacheAdapter": { + "env": "PARSE_SERVER_CACHE_ADAPTER", + "help": "Adapter module for the cache", + "action": parsers.moduleOrObjectParser + }, + "cacheMaxSize": { + "env": "PARSE_SERVER_CACHE_MAX_SIZE", + "help": "Sets the maximum size for the in memory cache, defaults to 10000", + "action": parsers.numberParser("cacheMaxSize"), + "default": 10000 + }, + "cacheTTL": { + "env": "PARSE_SERVER_CACHE_TTL", + "help": "Sets the TTL for the in memory cache (in ms), defaults to 5000 (5 seconds)", + "action": parsers.numberParser("cacheTTL"), + "default": 5000 + }, + "clientKey": { + "env": "PARSE_SERVER_CLIENT_KEY", + "help": "Key for iOS, MacOS, tvOS clients" + }, + "cloud": { + "env": "PARSE_SERVER_CLOUD", + "help": "Full path to your cloud code main.js" + }, + "cluster": { + "env": "PARSE_SERVER_CLUSTER", + "help": "Run with cluster, optionally set the number of processes default to os.cpus().length", + "action": parsers.numberOrBooleanParser + }, + "collectionPrefix": { + "env": "PARSE_SERVER_COLLECTION_PREFIX", + "help": "A collection prefix for the classes", + "default": "" + }, + "customPages": { + "env": "PARSE_SERVER_CUSTOM_PAGES", + "help": "custom pages for password validation and reset", + "action": parsers.objectParser, + "default": {} + }, + "databaseAdapter": { + "env": "PARSE_SERVER_DATABASE_ADAPTER", + "help": "Adapter module for the database", + "action": parsers.moduleOrObjectParser + }, + "databaseOptions": { + "env": "PARSE_SERVER_DATABASE_OPTIONS", + "help": "Options to pass to the mongodb client", + "action": parsers.objectParser + }, + "databaseURI": { + "env": "PARSE_SERVER_DATABASE_URI", + "help": "The full URI to your database. Supported databases are mongodb or postgres.", + "required": true, + "default": "mongodb://localhost:27017/parse" + }, + "directAccess": { + "env": "PARSE_SERVER_ENABLE_EXPERIMENTAL_DIRECT_ACCESS", + "help": "Replace HTTP Interface when using JS SDK in current node runtime, defaults to false. Caution, this is an experimental feature that may not be appropriate for production.", + "action": parsers.booleanParser, + "default": false + }, + "dotNetKey": { + "env": "PARSE_SERVER_DOT_NET_KEY", + "help": "Key for Unity and .Net SDK" + }, + "emailAdapter": { + "env": "PARSE_SERVER_EMAIL_ADAPTER", + "help": "Adapter module for email sending", + "action": parsers.moduleOrObjectParser + }, + "emailVerifyTokenReuseIfValid": { + "env": "PARSE_SERVER_EMAIL_VERIFY_TOKEN_REUSE_IF_VALID", + "help": "an existing email verify token should be reused when resend verification email is requested", + "action": parsers.booleanParser, + "default": false + }, + "emailVerifyTokenValidityDuration": { + "env": "PARSE_SERVER_EMAIL_VERIFY_TOKEN_VALIDITY_DURATION", + "help": "Email verification token validity duration, in seconds", + "action": parsers.numberParser("emailVerifyTokenValidityDuration") + }, + "enableAnonymousUsers": { + "env": "PARSE_SERVER_ENABLE_ANON_USERS", + "help": "Enable (or disable) anonymous users, defaults to true", + "action": parsers.booleanParser, + "default": true + }, + "enableExpressErrorHandler": { + "env": "PARSE_SERVER_ENABLE_EXPRESS_ERROR_HANDLER", + "help": "Enables the default express error handler for all errors", + "action": parsers.booleanParser, + "default": false + }, + "enablePageLocalization": { + "env": "PARSE_SERVER_ENABLE_PAGE_LOCALIZATION", + "help": "Is true if pages should be localized; customPages must not be set.", + "action": parsers.booleanParser, + "default": false + }, + "enableSingleSchemaCache": { + "env": "PARSE_SERVER_ENABLE_SINGLE_SCHEMA_CACHE", + "help": "Use a single schema cache shared across requests. Reduces number of queries made to _SCHEMA, defaults to false, i.e. unique schema cache per request.", + "action": parsers.booleanParser, + "default": false + }, + "encryptionKey": { + "env": "PARSE_SERVER_ENCRYPTION_KEY", + "help": "Key for encrypting your files" + }, + "expireInactiveSessions": { + "env": "PARSE_SERVER_EXPIRE_INACTIVE_SESSIONS", + "help": "Sets wether we should expire the inactive sessions, defaults to true", + "action": parsers.booleanParser, + "default": true + }, + "fileKey": { + "env": "PARSE_SERVER_FILE_KEY", + "help": "Key for your files" + }, + "filesAdapter": { + "env": "PARSE_SERVER_FILES_ADAPTER", + "help": "Adapter module for the files sub-system", + "action": parsers.moduleOrObjectParser + }, + "fileUpload": { + "env": "PARSE_SERVER_FILE_UPLOAD_OPTIONS", + "help": "Options for file uploads", + "action": parsers.objectParser, + "default": {} + }, + "graphQLPath": { + "env": "PARSE_SERVER_GRAPHQL_PATH", + "help": "Mount path for the GraphQL endpoint, defaults to /graphql", + "default": "/graphql" + }, + "graphQLSchema": { + "env": "PARSE_SERVER_GRAPH_QLSCHEMA", + "help": "Full path to your GraphQL custom schema.graphql file" + }, + "host": { + "env": "PARSE_SERVER_HOST", + "help": "The host to serve ParseServer on, defaults to 0.0.0.0", + "default": "0.0.0.0" + }, + "idempotencyOptions": { + "env": "PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_OPTIONS", + "help": "Options for request idempotency to deduplicate identical requests that may be caused by network issues. Caution, this is an experimental feature that may not be appropriate for production.", + "action": parsers.objectParser, + "default": {} + }, + "javascriptKey": { + "env": "PARSE_SERVER_JAVASCRIPT_KEY", + "help": "Key for the Javascript SDK" + }, + "jsonLogs": { + "env": "JSON_LOGS", + "help": "Log as structured JSON objects", + "action": parsers.booleanParser + }, + "liveQuery": { + "env": "PARSE_SERVER_LIVE_QUERY", + "help": "parse-server's LiveQuery configuration object", + "action": parsers.objectParser + }, + "liveQueryServerOptions": { + "env": "PARSE_SERVER_LIVE_QUERY_SERVER_OPTIONS", + "help": "Live query server configuration options (will start the liveQuery server)", + "action": parsers.objectParser + }, + "loggerAdapter": { + "env": "PARSE_SERVER_LOGGER_ADAPTER", + "help": "Adapter module for the logging sub-system", + "action": parsers.moduleOrObjectParser + }, + "logLevel": { + "env": "PARSE_SERVER_LOG_LEVEL", + "help": "Sets the level for logs" + }, + "logsFolder": { + "env": "PARSE_SERVER_LOGS_FOLDER", + "help": "Folder for the logs (defaults to './logs'); set to null to disable file based logging", + "default": "./logs" + }, + "masterKey": { + "env": "PARSE_SERVER_MASTER_KEY", + "help": "Your Parse Master Key", + "required": true + }, + "masterKeyIps": { + "env": "PARSE_SERVER_MASTER_KEY_IPS", + "help": "Restrict masterKey to be used by only these ips, defaults to [] (allow all ips)", + "action": parsers.arrayParser, + "default": [] + }, + "maxLimit": { + "env": "PARSE_SERVER_MAX_LIMIT", + "help": "Max value for limit option on queries, defaults to unlimited", + "action": parsers.numberParser("maxLimit") + }, + "maxLogFiles": { + "env": "PARSE_SERVER_MAX_LOG_FILES", + "help": "Maximum number of logs to keep. If not set, no logs will be removed. This can be a number of files or number of days. If using days, add 'd' as the suffix. (default: null)", + "action": parsers.objectParser + }, + "maxUploadSize": { + "env": "PARSE_SERVER_MAX_UPLOAD_SIZE", + "help": "Max file size for uploads, defaults to 20mb", + "default": "20mb" + }, + "middleware": { + "env": "PARSE_SERVER_MIDDLEWARE", + "help": "middleware for express server, can be string or function" + }, + "mountGraphQL": { + "env": "PARSE_SERVER_MOUNT_GRAPHQL", + "help": "Mounts the GraphQL endpoint", + "action": parsers.booleanParser, + "default": false + }, + "mountPath": { + "env": "PARSE_SERVER_MOUNT_PATH", + "help": "Mount path for the server, defaults to /parse", + "default": "/parse" + }, + "mountPlayground": { + "env": "PARSE_SERVER_MOUNT_PLAYGROUND", + "help": "Mounts the GraphQL Playground - never use this option in production", + "action": parsers.booleanParser, + "default": false + }, + "objectIdSize": { + "env": "PARSE_SERVER_OBJECT_ID_SIZE", + "help": "Sets the number of characters in generated object id's, default 10", + "action": parsers.numberParser("objectIdSize"), + "default": 10 + }, + "passwordPolicy": { + "env": "PARSE_SERVER_PASSWORD_POLICY", + "help": "Password policy for enforcing password related rules", + "action": parsers.objectParser + }, + "playgroundPath": { + "env": "PARSE_SERVER_PLAYGROUND_PATH", + "help": "Mount path for the GraphQL Playground, defaults to /playground", + "default": "/playground" + }, + "port": { + "env": "PORT", + "help": "The port to run the ParseServer, defaults to 1337.", + "action": parsers.numberParser("port"), + "default": 1337 + }, + "preserveFileName": { + "env": "PARSE_SERVER_PRESERVE_FILE_NAME", + "help": "Enable (or disable) the addition of a unique hash to the file names", + "action": parsers.booleanParser, + "default": false + }, + "preventLoginWithUnverifiedEmail": { + "env": "PARSE_SERVER_PREVENT_LOGIN_WITH_UNVERIFIED_EMAIL", + "help": "Prevent user from login if email is not verified and PARSE_SERVER_VERIFY_USER_EMAILS is true, defaults to false", + "action": parsers.booleanParser, + "default": false + }, + "protectedFields": { + "env": "PARSE_SERVER_PROTECTED_FIELDS", + "help": "Protected fields that should be treated with extra security when fetching details.", + "action": parsers.objectParser, + "default": { + "_User": { + "*": ["email"] + } + } + }, + "publicServerURL": { + "env": "PARSE_PUBLIC_SERVER_URL", + "help": "Public URL to your parse server with http:// or https://." + }, + "push": { + "env": "PARSE_SERVER_PUSH", + "help": "Configuration for push, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#push-notifications", + "action": parsers.objectParser + }, + "readOnlyMasterKey": { + "env": "PARSE_SERVER_READ_ONLY_MASTER_KEY", + "help": "Read-only key, which has the same capabilities as MasterKey without writes" + }, + "restAPIKey": { + "env": "PARSE_SERVER_REST_API_KEY", + "help": "Key for REST calls" + }, + "revokeSessionOnPasswordReset": { + "env": "PARSE_SERVER_REVOKE_SESSION_ON_PASSWORD_RESET", + "help": "When a user changes their password, either through the reset password email or while logged in, all sessions are revoked if this is true. Set to false if you don't want to revoke sessions.", + "action": parsers.booleanParser, + "default": true + }, + "scheduledPush": { + "env": "PARSE_SERVER_SCHEDULED_PUSH", + "help": "Configuration for push scheduling, defaults to false.", + "action": parsers.booleanParser, + "default": false + }, + "schemaCacheTTL": { + "env": "PARSE_SERVER_SCHEMA_CACHE_TTL", + "help": "The TTL for caching the schema for optimizing read/write operations. You should put a long TTL when your DB is in production. default to 5000; set 0 to disable.", + "action": parsers.numberParser("schemaCacheTTL"), + "default": 5000 + }, + "serverCloseComplete": { + "env": "PARSE_SERVER_SERVER_CLOSE_COMPLETE", + "help": "Callback when server has closed" + }, + "serverStartComplete": { + "env": "PARSE_SERVER_SERVER_START_COMPLETE", + "help": "Callback when server has started" + }, + "serverURL": { + "env": "PARSE_SERVER_URL", + "help": "URL to your parse server with http:// or https://.", + "required": true + }, + "sessionLength": { + "env": "PARSE_SERVER_SESSION_LENGTH", + "help": "Session duration, in seconds, defaults to 1 year", + "action": parsers.numberParser("sessionLength"), + "default": 31536000 + }, + "silent": { + "env": "SILENT", + "help": "Disables console output", + "action": parsers.booleanParser + }, + "startLiveQueryServer": { + "env": "PARSE_SERVER_START_LIVE_QUERY_SERVER", + "help": "Starts the liveQuery server", + "action": parsers.booleanParser + }, + "userSensitiveFields": { + "env": "PARSE_SERVER_USER_SENSITIVE_FIELDS", + "help": "Personally identifiable information fields in the user table the should be removed for non-authorized users. Deprecated @see protectedFields", + "action": parsers.arrayParser + }, + "verbose": { + "env": "VERBOSE", + "help": "Set the logging to verbose", + "action": parsers.booleanParser + }, + "verifyUserEmails": { + "env": "PARSE_SERVER_VERIFY_USER_EMAILS", + "help": "Enable (or disable) user email validation, defaults to false", + "action": parsers.booleanParser, + "default": false + }, + "webhookKey": { + "env": "PARSE_SERVER_WEBHOOK_KEY", + "help": "Key sent with outgoing webhook calls" + } }; module.exports.CustomPagesOptions = { - choosePassword: { - env: 'PARSE_SERVER_CUSTOM_PAGES_CHOOSE_PASSWORD', - help: 'choose password page path', - }, - invalidLink: { - env: 'PARSE_SERVER_CUSTOM_PAGES_INVALID_LINK', - help: 'invalid link page path', - }, - invalidVerificationLink: { - env: 'PARSE_SERVER_CUSTOM_PAGES_INVALID_VERIFICATION_LINK', - help: 'invalid verification link page path', - }, - linkSendFail: { - env: 'PARSE_SERVER_CUSTOM_PAGES_LINK_SEND_FAIL', - help: 'verification link send fail page path', - }, - linkSendSuccess: { - env: 'PARSE_SERVER_CUSTOM_PAGES_LINK_SEND_SUCCESS', - help: 'verification link send success page path', - }, - parseFrameURL: { - env: 'PARSE_SERVER_CUSTOM_PAGES_PARSE_FRAME_URL', - help: 'for masking user-facing pages', - }, - passwordResetSuccess: { - env: 'PARSE_SERVER_CUSTOM_PAGES_PASSWORD_RESET_SUCCESS', - help: 'password reset success page path', - }, - verifyEmailSuccess: { - env: 'PARSE_SERVER_CUSTOM_PAGES_VERIFY_EMAIL_SUCCESS', - help: 'verify email success page path', - }, + "choosePassword": { + "env": "PARSE_SERVER_CUSTOM_PAGES_CHOOSE_PASSWORD", + "help": "choose password page path" + }, + "invalidLink": { + "env": "PARSE_SERVER_CUSTOM_PAGES_INVALID_LINK", + "help": "invalid link page path" + }, + "invalidVerificationLink": { + "env": "PARSE_SERVER_CUSTOM_PAGES_INVALID_VERIFICATION_LINK", + "help": "invalid verification link page path" + }, + "linkSendFail": { + "env": "PARSE_SERVER_CUSTOM_PAGES_LINK_SEND_FAIL", + "help": "verification link send fail page path" + }, + "linkSendSuccess": { + "env": "PARSE_SERVER_CUSTOM_PAGES_LINK_SEND_SUCCESS", + "help": "verification link send success page path" + }, + "parseFrameURL": { + "env": "PARSE_SERVER_CUSTOM_PAGES_PARSE_FRAME_URL", + "help": "for masking user-facing pages" + }, + "passwordResetSuccess": { + "env": "PARSE_SERVER_CUSTOM_PAGES_PASSWORD_RESET_SUCCESS", + "help": "password reset success page path" + }, + "verifyEmailSuccess": { + "env": "PARSE_SERVER_CUSTOM_PAGES_VERIFY_EMAIL_SUCCESS", + "help": "verify email success page path" + } }; module.exports.LiveQueryOptions = { - classNames: { - env: 'PARSE_SERVER_LIVEQUERY_CLASSNAMES', - help: "parse-server's LiveQuery classNames", - action: parsers.arrayParser, - }, - pubSubAdapter: { - env: 'PARSE_SERVER_LIVEQUERY_PUB_SUB_ADAPTER', - help: 'LiveQuery pubsub adapter', - action: parsers.moduleOrObjectParser, - }, - redisOptions: { - env: 'PARSE_SERVER_LIVEQUERY_REDIS_OPTIONS', - help: "parse-server's LiveQuery redisOptions", - action: parsers.objectParser, - }, - redisURL: { - env: 'PARSE_SERVER_LIVEQUERY_REDIS_URL', - help: "parse-server's LiveQuery redisURL", - }, - wssAdapter: { - env: 'PARSE_SERVER_LIVEQUERY_WSS_ADAPTER', - help: 'Adapter module for the WebSocketServer', - action: parsers.moduleOrObjectParser, - }, + "classNames": { + "env": "PARSE_SERVER_LIVEQUERY_CLASSNAMES", + "help": "parse-server's LiveQuery classNames", + "action": parsers.arrayParser + }, + "pubSubAdapter": { + "env": "PARSE_SERVER_LIVEQUERY_PUB_SUB_ADAPTER", + "help": "LiveQuery pubsub adapter", + "action": parsers.moduleOrObjectParser + }, + "redisOptions": { + "env": "PARSE_SERVER_LIVEQUERY_REDIS_OPTIONS", + "help": "parse-server's LiveQuery redisOptions", + "action": parsers.objectParser + }, + "redisURL": { + "env": "PARSE_SERVER_LIVEQUERY_REDIS_URL", + "help": "parse-server's LiveQuery redisURL" + }, + "wssAdapter": { + "env": "PARSE_SERVER_LIVEQUERY_WSS_ADAPTER", + "help": "Adapter module for the WebSocketServer", + "action": parsers.moduleOrObjectParser + } }; module.exports.LiveQueryServerOptions = { - appId: { - env: 'PARSE_LIVE_QUERY_SERVER_APP_ID', - help: - 'This string should match the appId in use by your Parse Server. If you deploy the LiveQuery server alongside Parse Server, the LiveQuery server will try to use the same appId.', - }, - cacheTimeout: { - env: 'PARSE_LIVE_QUERY_SERVER_CACHE_TIMEOUT', - help: - "Number in milliseconds. When clients provide the sessionToken to the LiveQuery server, the LiveQuery server will try to fetch its ParseUser's objectId from parse server and store it in the cache. The value defines the duration of the cache. Check the following Security section and our protocol specification for details, defaults to 5 * 1000 ms (5 seconds).", - action: parsers.numberParser('cacheTimeout'), - }, - keyPairs: { - env: 'PARSE_LIVE_QUERY_SERVER_KEY_PAIRS', - help: - 'A JSON object that serves as a whitelist of keys. It is used for validating clients when they try to connect to the LiveQuery server. Check the following Security section and our protocol specification for details.', - action: parsers.objectParser, - }, - logLevel: { - env: 'PARSE_LIVE_QUERY_SERVER_LOG_LEVEL', - help: - 'This string defines the log level of the LiveQuery server. We support VERBOSE, INFO, ERROR, NONE, defaults to INFO.', - }, - masterKey: { - env: 'PARSE_LIVE_QUERY_SERVER_MASTER_KEY', - help: - 'This string should match the masterKey in use by your Parse Server. If you deploy the LiveQuery server alongside Parse Server, the LiveQuery server will try to use the same masterKey.', - }, - port: { - env: 'PARSE_LIVE_QUERY_SERVER_PORT', - help: 'The port to run the LiveQuery server, defaults to 1337.', - action: parsers.numberParser('port'), - default: 1337, - }, - pubSubAdapter: { - env: 'PARSE_LIVE_QUERY_SERVER_PUB_SUB_ADAPTER', - help: 'LiveQuery pubsub adapter', - action: parsers.moduleOrObjectParser, - }, - redisOptions: { - env: 'PARSE_LIVE_QUERY_SERVER_REDIS_OPTIONS', - help: "parse-server's LiveQuery redisOptions", - action: parsers.objectParser, - }, - redisURL: { - env: 'PARSE_LIVE_QUERY_SERVER_REDIS_URL', - help: "parse-server's LiveQuery redisURL", - }, - serverURL: { - env: 'PARSE_LIVE_QUERY_SERVER_SERVER_URL', - help: - 'This string should match the serverURL in use by your Parse Server. If you deploy the LiveQuery server alongside Parse Server, the LiveQuery server will try to use the same serverURL.', - }, - websocketTimeout: { - env: 'PARSE_LIVE_QUERY_SERVER_WEBSOCKET_TIMEOUT', - help: - 'Number of milliseconds between ping/pong frames. The WebSocket server sends ping/pong frames to the clients to keep the WebSocket alive. This value defines the interval of the ping/pong frame from the server to clients, defaults to 10 * 1000 ms (10 s).', - action: parsers.numberParser('websocketTimeout'), - }, - wssAdapter: { - env: 'PARSE_LIVE_QUERY_SERVER_WSS_ADAPTER', - help: 'Adapter module for the WebSocketServer', - action: parsers.moduleOrObjectParser, - }, + "appId": { + "env": "PARSE_LIVE_QUERY_SERVER_APP_ID", + "help": "This string should match the appId in use by your Parse Server. If you deploy the LiveQuery server alongside Parse Server, the LiveQuery server will try to use the same appId." + }, + "cacheTimeout": { + "env": "PARSE_LIVE_QUERY_SERVER_CACHE_TIMEOUT", + "help": "Number in milliseconds. When clients provide the sessionToken to the LiveQuery server, the LiveQuery server will try to fetch its ParseUser's objectId from parse server and store it in the cache. The value defines the duration of the cache. Check the following Security section and our protocol specification for details, defaults to 5 * 1000 ms (5 seconds).", + "action": parsers.numberParser("cacheTimeout") + }, + "keyPairs": { + "env": "PARSE_LIVE_QUERY_SERVER_KEY_PAIRS", + "help": "A JSON object that serves as a whitelist of keys. It is used for validating clients when they try to connect to the LiveQuery server. Check the following Security section and our protocol specification for details.", + "action": parsers.objectParser + }, + "logLevel": { + "env": "PARSE_LIVE_QUERY_SERVER_LOG_LEVEL", + "help": "This string defines the log level of the LiveQuery server. We support VERBOSE, INFO, ERROR, NONE, defaults to INFO." + }, + "masterKey": { + "env": "PARSE_LIVE_QUERY_SERVER_MASTER_KEY", + "help": "This string should match the masterKey in use by your Parse Server. If you deploy the LiveQuery server alongside Parse Server, the LiveQuery server will try to use the same masterKey." + }, + "port": { + "env": "PARSE_LIVE_QUERY_SERVER_PORT", + "help": "The port to run the LiveQuery server, defaults to 1337.", + "action": parsers.numberParser("port"), + "default": 1337 + }, + "pubSubAdapter": { + "env": "PARSE_LIVE_QUERY_SERVER_PUB_SUB_ADAPTER", + "help": "LiveQuery pubsub adapter", + "action": parsers.moduleOrObjectParser + }, + "redisOptions": { + "env": "PARSE_LIVE_QUERY_SERVER_REDIS_OPTIONS", + "help": "parse-server's LiveQuery redisOptions", + "action": parsers.objectParser + }, + "redisURL": { + "env": "PARSE_LIVE_QUERY_SERVER_REDIS_URL", + "help": "parse-server's LiveQuery redisURL" + }, + "serverURL": { + "env": "PARSE_LIVE_QUERY_SERVER_SERVER_URL", + "help": "This string should match the serverURL in use by your Parse Server. If you deploy the LiveQuery server alongside Parse Server, the LiveQuery server will try to use the same serverURL." + }, + "websocketTimeout": { + "env": "PARSE_LIVE_QUERY_SERVER_WEBSOCKET_TIMEOUT", + "help": "Number of milliseconds between ping/pong frames. The WebSocket server sends ping/pong frames to the clients to keep the WebSocket alive. This value defines the interval of the ping/pong frame from the server to clients, defaults to 10 * 1000 ms (10 s).", + "action": parsers.numberParser("websocketTimeout") + }, + "wssAdapter": { + "env": "PARSE_LIVE_QUERY_SERVER_WSS_ADAPTER", + "help": "Adapter module for the WebSocketServer", + "action": parsers.moduleOrObjectParser + } }; module.exports.IdempotencyOptions = { - paths: { - env: 'PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_PATHS', - help: - 'An array of paths for which the feature should be enabled. The mount path must not be included, for example instead of `/parse/functions/myFunction` specifiy `functions/myFunction`. The entries are interpreted as regular expression, for example `functions/.*` matches all functions, `jobs/.*` matches all jobs, `classes/.*` matches all classes, `.*` matches all paths.', - action: parsers.arrayParser, - default: [], - }, - ttl: { - env: 'PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_TTL', - help: - 'The duration in seconds after which a request record is discarded from the database, defaults to 300s.', - action: parsers.numberParser('ttl'), - default: 300, - }, + "paths": { + "env": "PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_PATHS", + "help": "An array of paths for which the feature should be enabled. The mount path must not be included, for example instead of `/parse/functions/myFunction` specifiy `functions/myFunction`. The entries are interpreted as regular expression, for example `functions/.*` matches all functions, `jobs/.*` matches all jobs, `classes/.*` matches all classes, `.*` matches all paths.", + "action": parsers.arrayParser, + "default": [] + }, + "ttl": { + "env": "PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_TTL", + "help": "The duration in seconds after which a request record is discarded from the database, defaults to 300s.", + "action": parsers.numberParser("ttl"), + "default": 300 + } }; module.exports.AccountLockoutOptions = { - duration: { - env: 'PARSE_SERVER_ACCOUNT_LOCKOUT_DURATION', - help: - 'number of minutes that a locked-out account remains locked out before automatically becoming unlocked.', - action: parsers.numberParser('duration'), - }, - threshold: { - env: 'PARSE_SERVER_ACCOUNT_LOCKOUT_THRESHOLD', - help: 'number of failed sign-in attempts that will cause a user account to be locked', - action: parsers.numberParser('threshold'), - }, + "duration": { + "env": "PARSE_SERVER_ACCOUNT_LOCKOUT_DURATION", + "help": "number of minutes that a locked-out account remains locked out before automatically becoming unlocked.", + "action": parsers.numberParser("duration") + }, + "threshold": { + "env": "PARSE_SERVER_ACCOUNT_LOCKOUT_THRESHOLD", + "help": "number of failed sign-in attempts that will cause a user account to be locked", + "action": parsers.numberParser("threshold") + } }; module.exports.PasswordPolicyOptions = { - doNotAllowUsername: { - env: 'PARSE_SERVER_PASSWORD_POLICY_DO_NOT_ALLOW_USERNAME', - help: 'disallow username in passwords', - action: parsers.booleanParser, - }, - maxPasswordAge: { - env: 'PARSE_SERVER_PASSWORD_POLICY_MAX_PASSWORD_AGE', - help: 'days for password expiry', - action: parsers.numberParser('maxPasswordAge'), - }, - maxPasswordHistory: { - env: 'PARSE_SERVER_PASSWORD_POLICY_MAX_PASSWORD_HISTORY', - help: 'setting to prevent reuse of previous n passwords', - action: parsers.numberParser('maxPasswordHistory'), - }, - resetTokenReuseIfValid: { - env: 'PARSE_SERVER_PASSWORD_POLICY_RESET_TOKEN_REUSE_IF_VALID', - help: "resend token if it's still valid", - action: parsers.booleanParser, - }, - resetTokenValidityDuration: { - env: 'PARSE_SERVER_PASSWORD_POLICY_RESET_TOKEN_VALIDITY_DURATION', - help: 'time for token to expire', - action: parsers.numberParser('resetTokenValidityDuration'), - }, - validatorCallback: { - env: 'PARSE_SERVER_PASSWORD_POLICY_VALIDATOR_CALLBACK', - help: 'a callback function to be invoked to validate the password', - }, - validatorPattern: { - env: 'PARSE_SERVER_PASSWORD_POLICY_VALIDATOR_PATTERN', - help: 'a RegExp object or a regex string representing the pattern to enforce', - }, + "doNotAllowUsername": { + "env": "PARSE_SERVER_PASSWORD_POLICY_DO_NOT_ALLOW_USERNAME", + "help": "disallow username in passwords", + "action": parsers.booleanParser + }, + "maxPasswordAge": { + "env": "PARSE_SERVER_PASSWORD_POLICY_MAX_PASSWORD_AGE", + "help": "days for password expiry", + "action": parsers.numberParser("maxPasswordAge") + }, + "maxPasswordHistory": { + "env": "PARSE_SERVER_PASSWORD_POLICY_MAX_PASSWORD_HISTORY", + "help": "setting to prevent reuse of previous n passwords", + "action": parsers.numberParser("maxPasswordHistory") + }, + "resetTokenReuseIfValid": { + "env": "PARSE_SERVER_PASSWORD_POLICY_RESET_TOKEN_REUSE_IF_VALID", + "help": "resend token if it's still valid", + "action": parsers.booleanParser + }, + "resetTokenValidityDuration": { + "env": "PARSE_SERVER_PASSWORD_POLICY_RESET_TOKEN_VALIDITY_DURATION", + "help": "time for token to expire", + "action": parsers.numberParser("resetTokenValidityDuration") + }, + "validatorCallback": { + "env": "PARSE_SERVER_PASSWORD_POLICY_VALIDATOR_CALLBACK", + "help": "a callback function to be invoked to validate the password" + }, + "validatorPattern": { + "env": "PARSE_SERVER_PASSWORD_POLICY_VALIDATOR_PATTERN", + "help": "a RegExp object or a regex string representing the pattern to enforce" + } }; module.exports.FileUploadOptions = { - enableForAnonymousUser: { - env: 'PARSE_SERVER_FILE_UPLOAD_ENABLE_FOR_ANONYMOUS_USER', - help: 'Is true if file upload should be allowed for anonymous users.', - action: parsers.booleanParser, - default: false, - }, - enableForAuthenticatedUser: { - env: 'PARSE_SERVER_FILE_UPLOAD_ENABLE_FOR_AUTHENTICATED_USER', - help: 'Is true if file upload should be allowed for authenticated users.', - action: parsers.booleanParser, - default: true, - }, - enableForPublic: { - env: 'PARSE_SERVER_FILE_UPLOAD_ENABLE_FOR_PUBLIC', - help: 'Is true if file upload should be allowed for anyone, regardless of user authentication.', - action: parsers.booleanParser, - default: false, - }, + "enableForAnonymousUser": { + "env": "PARSE_SERVER_FILE_UPLOAD_ENABLE_FOR_ANONYMOUS_USER", + "help": "Is true if file upload should be allowed for anonymous users.", + "action": parsers.booleanParser, + "default": false + }, + "enableForAuthenticatedUser": { + "env": "PARSE_SERVER_FILE_UPLOAD_ENABLE_FOR_AUTHENTICATED_USER", + "help": "Is true if file upload should be allowed for authenticated users.", + "action": parsers.booleanParser, + "default": true + }, + "enableForPublic": { + "env": "PARSE_SERVER_FILE_UPLOAD_ENABLE_FOR_PUBLIC", + "help": "Is true if file upload should be allowed for anyone, regardless of user authentication.", + "action": parsers.booleanParser, + "default": false + } }; diff --git a/src/Options/docs.js b/src/Options/docs.js index a70fa8bff2..a44a2df1bf 100644 --- a/src/Options/docs.js +++ b/src/Options/docs.js @@ -27,6 +27,7 @@ * @property {Number} emailVerifyTokenValidityDuration Email verification token validity duration, in seconds * @property {Boolean} enableAnonymousUsers Enable (or disable) anonymous users, defaults to true * @property {Boolean} enableExpressErrorHandler Enables the default express error handler for all errors + * @property {Boolean} enablePageLocalization Is true if pages should be localized; customPages must not be set. * @property {Boolean} enableSingleSchemaCache Use a single schema cache shared across requests. Reduces number of queries made to _SCHEMA, defaults to false, i.e. unique schema cache per request. * @property {String} encryptionKey Key for encrypting your files * @property {Boolean} expireInactiveSessions Sets wether we should expire the inactive sessions, defaults to true @@ -145,3 +146,4 @@ * @property {Boolean} enableForAuthenticatedUser Is true if file upload should be allowed for authenticated users. * @property {Boolean} enableForPublic Is true if file upload should be allowed for anyone, regardless of user authentication. */ + diff --git a/src/Options/index.js b/src/Options/index.js index 84a9283bbc..3fe43d721b 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -141,6 +141,9 @@ export interface ParseServerOptions { /* custom pages for password validation and reset :DEFAULT: {} */ customPages: ?CustomPagesOptions; + /* Is true if pages should be localized; customPages must not be set. + :DEFAULT: false */ + enablePageLocalization: ?boolean; /* parse-server's LiveQuery configuration object */ liveQuery: ?LiveQueryOptions; /* Session duration, in seconds, defaults to 1 year diff --git a/src/Routers/PublicAPIRouter.js b/src/Routers/PublicAPIRouter.js index 5009ee7d22..fbe58caf87 100644 --- a/src/Routers/PublicAPIRouter.js +++ b/src/Routers/PublicAPIRouter.js @@ -2,108 +2,93 @@ import PromiseRouter from '../PromiseRouter'; import Config from '../Config'; import express from 'express'; import path from 'path'; -import fs from 'fs'; +import { promises as fs } from 'fs'; import qs from 'querystring'; import { Parse } from 'parse/node'; - -const public_html = path.resolve(__dirname, '../../public_html'); -const views = path.resolve(__dirname, '../../views'); +import Utils from '../Utils'; + +const publicPath = path.resolve(__dirname, '../../public'); +const defaultPagePath = (file) => { return path.join(publicPath, file) }; +const defaultPageUrl = (file, serverUrl) => { return new URL('/apps/' + file, serverUrl).toString(); }; +const pages = Object.freeze({ + invalidLink: { customPageKey: 'invalidLink', defaultFile: 'invalid_link.html' }, + linkSendFail: { customPageKey: 'linkSendFail', defaultFile: 'link_send_fail.html' }, + choosePassword: { customPageKey: 'choosePassword', defaultFile: 'choose_password.html' }, + linkSendSuccess: { customPageKey: 'linkSendSuccess', defaultFile: 'link_send_success.html' }, + verifyEmailSuccess: { customPageKey: 'verifyEmailSuccess', defaultFile: 'verify_email_success.html' }, + passwordResetSuccess: { customPageKey: 'passwordResetSuccess', defaultFile: 'password_reset_success.html' }, + invalidVerificationLink: { customPageKey: 'invalidVerificationLink', defaultFile: 'invalid_verification_link.html' }, +}); +const pageParams = Object.freeze({ + appName: "appName", + appId: "appId", + token: "token", + username: "username", + error: "error", +}); export class PublicAPIRouter extends PromiseRouter { verifyEmail(req) { + const config = req.config; const { username, token: rawToken } = req.query; const token = rawToken && typeof rawToken !== 'string' ? rawToken.toString() : rawToken; - const appId = req.params.appId; - const config = Config.get(appId); - if (!config) { this.invalidRequest(); } - if (!config.publicServerURL) { - return this.missingPublicServerURL(); - } - if (!token || !username) { - return this.invalidLink(req); + return this.goToPage(req, pages.invalidLink); } const userController = config.userController; return userController.verifyEmail(username, token).then( () => { - const params = qs.stringify({ username }); - return Promise.resolve({ - status: 302, - location: `${config.verifyEmailSuccessURL}?${params}`, - }); + const params = { + [pageParams.username]: username + }; + return this.goToPage(req, pages.verifyEmailSuccess, params); }, () => { - return this.invalidVerificationLink(req); + if (req.query.username && req.params.appId) { + const params = { + [pageParams.username]: req.query.username, + [pageParams.appId]: req.params.appId, + }; + return this.goToPage(req, pages.invalidVerificationLink, params); + } else { + return this.goToPage(req, pages.invalidLink); + } } ); } resendVerificationEmail(req) { + const config = req.config; const username = req.body.username; - const appId = req.params.appId; - const config = Config.get(appId); if (!config) { this.invalidRequest(); } - if (!config.publicServerURL) { - return this.missingPublicServerURL(); - } - if (!username) { - return this.invalidLink(req); + return this.goToPage(req, pages.invalidLink); } const userController = config.userController; return userController.resendVerificationEmail(username).then( () => { - return Promise.resolve({ - status: 302, - location: `${config.linkSendSuccessURL}`, - }); + return this.goToPage(req, pages.linkSendSuccess); }, () => { - return Promise.resolve({ - status: 302, - location: `${config.linkSendFailURL}`, - }); + return this.goToPage(req, pages.linkSendFail); } ); } - changePassword(req) { - return new Promise((resolve, reject) => { - const config = Config.get(req.query.id); - - if (!config) { - this.invalidRequest(); - } - - if (!config.publicServerURL) { - return resolve({ - status: 404, - text: 'Not found.', - }); - } - // Should we keep the file in memory or leave like that? - fs.readFile(path.resolve(views, 'choose_password'), 'utf-8', (err, data) => { - if (err) { - return reject(err); - } - data = data.replace('PARSE_SERVER_URL', `'${config.publicServerURL}'`); - resolve({ - text: data, - }); - }); - }); + choosePassword(req) { + return this.goToPage(req, pages.choosePassword); } requestResetPassword(req) { @@ -113,32 +98,25 @@ export class PublicAPIRouter extends PromiseRouter { this.invalidRequest(); } - if (!config.publicServerURL) { - return this.missingPublicServerURL(); - } - const { username, token: rawToken } = req.query; const token = rawToken && typeof rawToken !== 'string' ? rawToken.toString() : rawToken; if (!username || !token) { - return this.invalidLink(req); + return this.goToPage(req, pages.invalidLink); } return config.userController.checkResetTokenValidity(username, token).then( () => { - const params = qs.stringify({ - token, - id: config.applicationId, - username, - app: config.appName, - }); - return Promise.resolve({ - status: 302, - location: `${config.choosePasswordURL}?${params}`, - }); + const params = { + [pageParams.token]: token, + [pageParams.username]: username, + [pageParams.appId]: config.applicationId, + [pageParams.appName]: config.appName, + }; + return this.goToPage(req, pages.choosePassword, params); }, () => { - return this.invalidLink(req); + return this.goToPage(req, pages.invalidLink) } ); } @@ -150,15 +128,11 @@ export class PublicAPIRouter extends PromiseRouter { this.invalidRequest(); } - if (!config.publicServerURL) { - return this.missingPublicServerURL(); - } - const { username, new_password, token: rawToken } = req.body; const token = rawToken && typeof rawToken !== 'string' ? rawToken.toString() : rawToken; if ((!username || !token || !new_password) && req.xhr === false) { - return this.invalidLink(req); + return this.goToPage(req, pages.invalidLink); } if (!username) { @@ -189,14 +163,6 @@ export class PublicAPIRouter extends PromiseRouter { } ) .then(result => { - const params = qs.stringify({ - username: username, - token: token, - id: config.applicationId, - error: result.err, - app: config.appName, - }); - if (req.xhr) { if (result.success) { return Promise.resolve({ @@ -210,45 +176,129 @@ export class PublicAPIRouter extends PromiseRouter { } const encodedUsername = encodeURIComponent(username); - const location = result.success - ? `${config.passwordResetSuccessURL}?username=${encodedUsername}` - : `${config.choosePasswordURL}?${params}`; - - return Promise.resolve({ - status: 302, - location, - }); + const query = result.success + ? { + [pageParams.username]: encodedUsername + } + : { + [pageParams.username]: username, + [pageParams.token]: token, + [pageParams.appId]: config.applicationId, + [pageParams.error]: result.err, + [pageParams.appName]: config.appName, + }; + const page = result.success + ? pages.passwordResetSuccess + : pages.choosePassword; + + return this.goToPage(req, page, query, false); }); } - invalidLink(req) { - return Promise.resolve({ - status: 302, - location: req.config.invalidLinkURL, - }); - } - - invalidVerificationLink(req) { + /** + * Returns page content if the page is a local file or returns a + * redirect to a custom page. + * @param {Object} req The express request. + * @param {Object} page The page to go to. + * @param {Object} params The query parameters to attach to the URL in case of + * HTTP redirect responses for POST requests, or the placeholders to fill into + * the response content in case of HTTP content responses for GET requests. + * @param {Boolean} responseType Is true if a redirect response should be forced, + * false if a content response should be forced, undefined if the response type + * should depend on the request type by default: + * - GET request -> content response + * - POST request -> redirect response (PRG pattern) + * @returns {Promise} The express response. + */ + goToPage(req, page, params, responseType) { const config = req.config; - if (req.query.username && req.params.appId) { - const params = qs.stringify({ - username: req.query.username, - appId: req.params.appId, - }); - return Promise.resolve({ - status: 302, - location: `${config.invalidVerificationLinkURL}?${params}`, - }); + const locale = req.query.locale; + const redirect = responseType !== undefined ? responseType : req.method == 'POST'; + + // Ensure required config + if ([ + config.publicServerURL, + ].includes(undefined)) { + return this.notFound(); + } + + // Compose paths and URLs + const customPage = config.customPages[page.customPageKey]; + const defaultFile = page.defaultFile; + const defaultPath = defaultPagePath(defaultFile); + const defaultUrl = defaultPageUrl(defaultFile, config.publicServerURL); + + // If custom page is set redirect to it without localization + if (customPage) { return this.redirectResponse(customPage, params); } + + // If localization is enabled + if (config.enablePageLocalization && locale) { + return Utils.getLocalizedPath(defaultPath, locale).then(({ path, subdir }) => redirect + ? this.redirectResponse(new URL(`/apps/${subdir}/${defaultFile}`, config.publicServerURL).toString(), params) + : this.pageResponse(req, path, params) + ); } else { - return this.invalidLink(req); + return redirect + ? this.redirectResponse(defaultUrl, params) + : this.pageResponse(req, defaultPath, params); + } + } + + /** + * Creates a response with file content. + * @param {Object} req The express request. + * @param {String} path The path of the file to return. + * @param {Object} placeholders The placeholders to fill in the + * content. + * @returns {Object} The express file response. + */ + async pageResponse(req, path, placeholders) { + + // Aggreate placeholders + placeholders = Object.assign({ + 'parseServerUrl': req.config.publicServerURL + }, placeholders); + + // If any of the placeholder values fails to resolve + if (Object.values(placeholders).includes(undefined)) { + return this.notFound(); + } + + // Get file content + let data; + try { + data = await fs.readFile(path, 'utf-8'); + } catch (e) { + return this.notFound(); + } + + // Fill placeholders in content + for (const placeholder of Object.entries(placeholders)) { + data = data.replace(`{{${placeholder[0]}}}`, placeholder[1]); } + + return { text: data }; + } + + /** + * Creates a response with http 303 rediret. + * @param {Object} req The express request. + * @param {String} path The path of the file to return. + * @returns {Object} The express file response. + */ + async redirectResponse(url, query) { + const location = query ? `${url}?${qs.stringify(query)}` : url; + return { + status: 303, + location: location, + }; } - missingPublicServerURL() { - return Promise.resolve({ + notFound() { + return { text: 'Not found.', status: 404, - }); + }; } invalidRequest() { @@ -259,7 +309,10 @@ export class PublicAPIRouter extends PromiseRouter { } setConfig(req) { - req.config = Config.get(req.params.appId); + req.config = Config.get(req.params.appId || req.query.appId); + if (!req.config) { + this.invalidRequest(); + } return Promise.resolve(); } @@ -286,9 +339,14 @@ export class PublicAPIRouter extends PromiseRouter { } ); - this.route('GET', '/apps/choose_password', req => { - return this.changePassword(req); - }); + this.route('GET', '/apps/choose_password', + req => { + this.setConfig(req); + }, + req => { + return this.choosePassword(req); + } + ); this.route( 'POST', @@ -315,10 +373,11 @@ export class PublicAPIRouter extends PromiseRouter { expressRouter() { const router = express.Router(); - router.use('/apps', express.static(public_html)); + router.use('/apps', express.static(publicPath)); router.use('/', super.expressRouter()); return router; } } export default PublicAPIRouter; +module.exports = { pages, PublicAPIRouter }; diff --git a/src/Utils.js b/src/Utils.js new file mode 100644 index 0000000000..095e5d605f --- /dev/null +++ b/src/Utils.js @@ -0,0 +1,87 @@ +/** + * utils.js + * @file General purpose utilities + * @description General purpose utilities. + */ + +const path = require('path'); +const fs = require('fs').promises; + +/** + * The general purpose utilities. + */ +class Utils { + + /** + * @function getLocalizedPath + * @description Returns a localized file path accoring to the locale. + * + * Localized files are searched in subfolders of a given path, e.g. + * + * root/ + * ├── base/ // base path to files + * │ ├── example.html // default file + * │ └── de/ // de language folder + * │ │ └── example.html // de localized file + * │ └── de-AT/ // de-AT locale folder + * │ │ └── example.html // de-AT localized file + * + * Files are matched with the locale in the following order: + * 1. Locale match, e.g. locale `de-AT` matches file in folder `de-AT`. + * 2. Language match, e.g. locale `de-AT` matches file in folder `de`. + * 3. Default; file in base folder is returned. + * + * @param {String} defaultPath The absolute file path, which is also + * the default path returned if localization is not available. + * @param {String} locale The locale. + * @returns {Promise} The object contains: + * - `path`: The path to the localized file, or the original path if + * localization is not available. + * - `subdir`: The subdirectory of the localized file, or undefined if + * there is no matching localized file. + */ + static async getLocalizedPath(defaultPath, locale) { + + // Get file name and paths + const file = path.basename(defaultPath); + const basePath = path.dirname(defaultPath); + + // If locale is not set return default file + if (!locale) { return { path: defaultPath }; } + + // Check file for locale exists + const localePath = path.join(basePath, locale, file); + const localeFileExists = await Utils.fileExists(localePath); + + // If file for locale exists return file + if (localeFileExists) { return { path: localePath, subdir: locale }; } + + // Check file for language exists + const language = locale.split("-")[0]; + const languagePath = path.join(basePath, language, file); + const languageFileExists = await Utils.fileExists(languagePath); + + // If file for language exists return file + if (languageFileExists) { return { path: languagePath, subdir: language }; } + + // Return default file + return { path: defaultPath }; + } + + /** + * @function fileExists + * @description Checks whether a file exists. + * @param {String} path The file path. + * @returns {Promise} Is true if the file can be accessed, false otherwise. + */ + static async fileExists(path) { + try { + await fs.access(path); + return true; + } catch (e) { + return false; + } + } +} + +module.exports = Utils; From e90f6c55d2539a741b5e540fe1c4d0637e9db141 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Sun, 17 Jan 2021 16:52:31 +0100 Subject: [PATCH 02/48] added changelog entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 251f179237..1bd26f4fc5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ [Full Changelog](https://github.com/parse-community/parse-server/compare/4.5.0...master) __BREAKING CHANGES:__ +- NEW: Added page localization. This PR contains breaking changes regarding password reset, email verification, page templating and the serving of pages in general. See PR for a detailed list of breaking changes [#6891](https://github.com/parse-community/parse-server/issues/6891). Thanks to [Manuel Trezza](https://github.com/mtrezza). - NEW: Added file upload restriction. File upload is now only allowed for authenticated users by default for improved security. To allow file upload also for Anonymous Users or Public, set the `fileUpload` parameter in the [Parse Server Options](https://parseplatform.org/parse-server/api/master/ParseServerOptions.html). [#7071](https://github.com/parse-community/parse-server/pull/7071). Thanks to [dblythy](https://github.com/dblythy). ___ - IMPROVE: Optimize queries on classes with pointer permissions. [#7061](https://github.com/parse-community/parse-server/pull/7061). Thanks to [Pedro Diaz](https://github.com/pdiaz) From af33f60dcca08a8fc522fe92492c1fbfd2876fb2 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Sun, 17 Jan 2021 16:58:49 +0100 Subject: [PATCH 03/48] fixed test description typo --- spec/ValidationAndPasswordsReset.spec.js | 70 ++++++++++++++++-------- 1 file changed, 46 insertions(+), 24 deletions(-) diff --git a/spec/ValidationAndPasswordsReset.spec.js b/spec/ValidationAndPasswordsReset.spec.js index 8dde98bbbd..bed2866bf7 100644 --- a/spec/ValidationAndPasswordsReset.spec.js +++ b/spec/ValidationAndPasswordsReset.spec.js @@ -665,7 +665,8 @@ describe('Custom Pages, Email Verification, Password Reset', () => { publicServerURL: 'http://localhost:8378/1', }).then(() => { request({ - url: 'http://localhost:8378/1/apps/test/verify_email?token=exampleToken&username=exampleUsername', + url: + 'http://localhost:8378/1/apps/test/verify_email?token=exampleToken&username=exampleUsername', followRedirects: false, }).then(response => { expect(response.status).toEqual(200); @@ -709,7 +710,8 @@ describe('Custom Pages, Email Verification, Password Reset', () => { const emailAdapter = { sendVerificationEmail: () => { request({ - url: 'http://localhost:8378/1/apps/test/verify_email?token=invalidToken&username=exampleUsername', + url: + 'http://localhost:8378/1/apps/test/verify_email?token=invalidToken&username=exampleUsername', followRedirects: false, }).then(response => { expect(response.status).toEqual(200); @@ -755,7 +757,9 @@ describe('Custom Pages, Email Verification, Password Reset', () => { expect(response.status).toEqual(200); expect(response.text).toContain('ExampleApp'); expect(response.text).toContain('exampleUsername'); - expect(response.text).toContain('http://localhost:8378/1/apps/test/request_password_reset'); + expect(response.text).toContain( + 'http://localhost:8378/1/apps/test/request_password_reset' + ); done(); }); }, @@ -815,8 +819,10 @@ describe('Custom Pages, Email Verification, Password Reset', () => { followRedirects: false, }).then(response => { expect(response.status).toEqual(200); - expect(response.text).toContain('http://localhost:8378/1/apps/test/request_password_reset'); - const re = /id="token" value="([a-zA-Z0-9]+)"/ + expect(response.text).toContain( + 'http://localhost:8378/1/apps/test/request_password_reset' + ); + const re = /id="token" value="([a-zA-Z0-9]+)"/; const match = response.text.match(re); if (!match) { fail('should have a token'); @@ -891,8 +897,10 @@ describe('Custom Pages, Email Verification, Password Reset', () => { followRedirects: false, }).then(response => { expect(response.status).toEqual(200); - expect(response.text).toContain('http://localhost:8378/1/apps/test/request_password_reset'); - const re = /id="token" value="([a-zA-Z0-9]+)"/ + expect(response.text).toContain( + 'http://localhost:8378/1/apps/test/request_password_reset' + ); + const re = /id="token" value="([a-zA-Z0-9]+)"/; const match = response.text.match(re); if (!match) { fail('should have a token'); @@ -950,7 +958,7 @@ describe('Custom Pages, Email Verification, Password Reset', () => { }); expect(response.status).toEqual(200); expect(response.text).toContain('http://localhost:8378/1/apps/test/request_password_reset'); - const re = /id="token" value="([a-zA-Z0-9]+)"/ + const re = /id="token" value="([a-zA-Z0-9]+)"/; const match = response.text.match(re); if (!match) { fail('should have a token'); @@ -1083,7 +1091,7 @@ describe('Custom Pages, Email Verification, Password Reset', () => { let pageResponse; let redirectResponse; const config = { - appId: "test", + appId: 'test', appName: 'ExampleAppName', verifyUserEmails: true, emailAdapter: { @@ -1108,8 +1116,8 @@ describe('Custom Pages, Email Verification, Password Reset', () => { }, query: { locale: 'de-AT', - } - } + }, + }; }); it('returns default file if localization is disabled', async () => { @@ -1117,7 +1125,9 @@ describe('Custom Pages, Email Verification, Password Reset', () => { await expectAsync(router.goToPage(req, pages.invalidLink)).toBeResolved(); expect(pageResponse.calls.all()[0].args[1]).toBeDefined(); - expect(pageResponse.calls.all()[0].args[1]).not.toMatch(new RegExp(`\/de(-AT)?\/${pages.invalidLink.defaultFile}`)); + expect(pageResponse.calls.all()[0].args[1]).not.toMatch( + new RegExp(`\/de(-AT)?\/${pages.invalidLink.defaultFile}`) + ); }); it('returns default file if no locale is specified', async () => { @@ -1125,7 +1135,9 @@ describe('Custom Pages, Email Verification, Password Reset', () => { await expectAsync(router.goToPage(req, pages.invalidLink)).toBeResolved(); expect(pageResponse.calls.all()[0].args[1]).toBeDefined(); - expect(pageResponse.calls.all()[0].args[1]).not.toMatch(new RegExp(`\/de(-AT)?\/${pages.invalidLink.defaultFile}`)); + expect(pageResponse.calls.all()[0].args[1]).not.toMatch( + new RegExp(`\/de(-AT)?\/${pages.invalidLink.defaultFile}`) + ); }); it('returns custom page regardless of localization enabled', async () => { @@ -1139,18 +1151,22 @@ describe('Custom Pages, Email Verification, Password Reset', () => { it('returns file for locale match', async () => { await expectAsync(router.goToPage(req, pages.invalidLink)).toBeResolved(); expect(pageResponse.calls.all()[0].args[1]).toBeDefined(); - expect(pageResponse.calls.all()[0].args[1]).toMatch(new RegExp(`\/de-AT\/${pages.invalidLink.defaultFile}`)); + expect(pageResponse.calls.all()[0].args[1]).toMatch( + new RegExp(`\/de-AT\/${pages.invalidLink.defaultFile}`) + ); }); it('returns file for language match', async () => { // Pretend no locale matching file exists - spyOn(Utils, 'fileExists').and.callFake(async (path) => { + spyOn(Utils, 'fileExists').and.callFake(async path => { return !path.includes(`/de-AT/${pages.invalidLink.defaultFile}`); }); await expectAsync(router.goToPage(req, pages.invalidLink)).toBeResolved(); expect(pageResponse.calls.all()[0].args[1]).toBeDefined(); - expect(pageResponse.calls.all()[0].args[1]).toMatch(new RegExp(`\/de\/${pages.invalidLink.defaultFile}`)); + expect(pageResponse.calls.all()[0].args[1]).toMatch( + new RegExp(`\/de\/${pages.invalidLink.defaultFile}`) + ); }); it('returns default file for neither locale nor language match', async () => { @@ -1158,7 +1174,9 @@ describe('Custom Pages, Email Verification, Password Reset', () => { await expectAsync(router.goToPage(req, pages.invalidLink)).toBeResolved(); expect(pageResponse.calls.all()[0].args[1]).toBeDefined(); - expect(pageResponse.calls.all()[0].args[1]).not.toMatch(new RegExp(`\/yo(-LO)?\/${pages.invalidLink.defaultFile}`)); + expect(pageResponse.calls.all()[0].args[1]).not.toMatch( + new RegExp(`\/yo(-LO)?\/${pages.invalidLink.defaultFile}`) + ); }); it('returns a file for GET request', async () => { @@ -1185,23 +1203,27 @@ describe('Custom Pages, Email Verification, Password Reset', () => { } }); - it('localizes invalid link page with file response (e2e test)', async () => { + it('responds to POST request with redirect response (e2e test)', async () => { await reconfigureServer(config); const response = await request({ - url: 'http://localhost:8378/1/apps/test/request_password_reset?token=exampleToken&username=exampleUsername&locale=de-AT', + url: + 'http://localhost:8378/1/apps/test/request_password_reset?token=exampleToken&username=exampleUsername&locale=de-AT', followRedirects: false, - method: 'POST' + method: 'POST', }); expect(response.status).toEqual(303); - expect(response.headers.location).toEqual('http://localhost:8378/apps/de-AT/invalid_link.html'); + expect(response.headers.location).toEqual( + 'http://localhost:8378/apps/de-AT/invalid_link.html' + ); }); - it('localizes invalid link page with redirect response (e2e test)', async () => { + it('responds to GET request with content response (e2e test)', async () => { await reconfigureServer(config); const response = await request({ - url: 'http://localhost:8378/1/apps/test/request_password_reset?token=exampleToken&username=exampleUsername&locale=de-AT', + url: + 'http://localhost:8378/1/apps/test/request_password_reset?token=exampleToken&username=exampleUsername&locale=de-AT', followRedirects: false, - method: 'GET' + method: 'GET', }); expect(response.status).toEqual(200); expect(response.text).toContain(''); From d7631509e9b19645fb518008ee8b0228c34d4d82 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Sun, 17 Jan 2021 18:03:56 +0100 Subject: [PATCH 04/48] fixed bug in PromiseRouter where headers are not added for text reponse --- src/PromiseRouter.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/PromiseRouter.js b/src/PromiseRouter.js index e1ec4eff9f..1f531025a9 100644 --- a/src/PromiseRouter.js +++ b/src/PromiseRouter.js @@ -161,6 +161,12 @@ function makeExpressHandler(appId, promiseHandler) { var status = result.status || 200; res.status(status); + if (result.headers) { + Object.keys(result.headers).forEach(header => { + res.set(header, result.headers[header]); + }); + } + if (result.text) { res.send(result.text); return; @@ -175,11 +181,6 @@ function makeExpressHandler(appId, promiseHandler) { return; } } - if (result.headers) { - Object.keys(result.headers).forEach(header => { - res.set(header, result.headers[header]); - }); - } res.json(result.response); }, error => { From 7021f1451d5ce701027ea9fc150955ad9085ddae Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Sun, 17 Jan 2021 18:04:30 +0100 Subject: [PATCH 05/48] added page parameters in page headers for programmatic use --- spec/EmailVerificationToken.spec.js | 1 - spec/PasswordPolicy.spec.js | 70 +++++++++----------- spec/ValidationAndPasswordsReset.spec.js | 25 +++---- src/Routers/PublicAPIRouter.js | 84 ++++++++++++++++-------- 4 files changed, 94 insertions(+), 86 deletions(-) diff --git a/spec/EmailVerificationToken.spec.js b/spec/EmailVerificationToken.spec.js index f2cb752df6..e76f40479e 100644 --- a/spec/EmailVerificationToken.spec.js +++ b/spec/EmailVerificationToken.spec.js @@ -435,7 +435,6 @@ describe('Email Verification Token Expiration: ', () => { followRedirects: false, }).then(response => { expect(response.status).toEqual(200); - expect(response.text).toContain('Invalid verification link'); expect(response.text).toContain('testEmailVerifyTokenValidity'); expect(response.text).toContain('/apps/test/resend_verification_email'); done(); diff --git a/spec/PasswordPolicy.spec.js b/spec/PasswordPolicy.spec.js index e31699426c..e96316ac48 100644 --- a/spec/PasswordPolicy.spec.js +++ b/spec/PasswordPolicy.spec.js @@ -105,9 +105,7 @@ describe('Password Policy: ', () => { }) .then(response => { expect(response.status).toEqual(200); - expect(response.text).toContain('http://localhost:8378/1/apps/test/request_password_reset'); - const re = /id="token" value="([a-zA-Z0-9]+)"/ - expect(response.text.match(re)).not.toBe(null); + expect(response.headers['x-parse-page-param-token']).toBeDefined(); done(); }) .catch(error => { @@ -621,15 +619,13 @@ describe('Password Policy: ', () => { }) .then(response => { expect(response.status).toEqual(200); - expect(response.text).toContain('http://localhost:8378/1/apps/test/request_password_reset'); - const re = /id="token" value="([a-zA-Z0-9]+)"/ - const match = response.text.match(re); - if (!match) { + + const token = response.headers['x-parse-page-param-token']; + if (!token) { fail('should have a token'); done(); return; } - const token = match[1]; request({ method: 'POST', @@ -712,15 +708,13 @@ describe('Password Policy: ', () => { }) .then(response => { expect(response.status).toEqual(200); - expect(response.text).toContain('http://localhost:8378/1/apps/test/request_password_reset'); - const re = /id="token" value="([a-zA-Z0-9]+)"/ - const match = response.text.match(re); - if (!match) { + + const token = response.headers['x-parse-page-param-token']; + if (!token) { fail('should have a token'); done(); return; } - const token = match[1]; request({ method: 'POST', @@ -897,15 +891,13 @@ describe('Password Policy: ', () => { }) .then(response => { expect(response.status).toEqual(200); - expect(response.text).toContain('http://localhost:8378/1/apps/test/request_password_reset'); - const re = /id="token" value="([a-zA-Z0-9]+)"/ - const match = response.text.match(re); - if (!match) { + + const token = response.headers['x-parse-page-param-token']; + if (!token) { fail('should have a token'); done(); return; } - const token = match[1]; request({ method: 'POST', @@ -987,14 +979,13 @@ describe('Password Policy: ', () => { resolveWithFullResponse: true, }); expect(response.status).toEqual(200); - expect(response.text).toContain('http://localhost:8378/1/apps/test/request_password_reset'); - const re = /id="token" value="([a-zA-Z0-9]+)"/ - const match = response.text.match(re); - if (!match) { + + const token = response.headers['x-parse-page-param-token']; + if (!token) { fail('should have a token'); + done(); return; } - const token = match[1]; try { await request({ @@ -1048,15 +1039,13 @@ describe('Password Policy: ', () => { }) .then(response => { expect(response.status).toEqual(200); - expect(response.text).toContain('http://localhost:8378/1/apps/test/request_password_reset'); - const re = /id="token" value="([a-zA-Z0-9]+)"/ - const match = response.text.match(re); - if (!match) { + + const token = response.headers['x-parse-page-param-token']; + if (!token) { fail('should have a token'); done(); return; } - const token = match[1]; request({ method: 'POST', @@ -1313,15 +1302,13 @@ describe('Password Policy: ', () => { }) .then(response => { expect(response.status).toEqual(200); - expect(response.text).toContain('http://localhost:8378/1/apps/test/request_password_reset'); - const re = /id="token" value="([a-zA-Z0-9]+)"/ - const match = response.text.match(re); - if (!match) { + + const token = response.headers['x-parse-page-param-token']; + if (!token) { fail('should have a token'); done(); return; } - const token = match[1]; request({ method: 'POST', @@ -1467,14 +1454,13 @@ describe('Password Policy: ', () => { }) .then(response => { expect(response.status).toEqual(200); - expect(response.text).toContain('http://localhost:8378/1/apps/test/request_password_reset'); - const re = /id="token" value="([a-zA-Z0-9]+)"/ - const match = response.text.match(re); - if (!match) { + + const token = response.headers['x-parse-page-param-token']; + if (!token) { fail('should have a token'); return Promise.reject('Invalid password link'); } - return Promise.resolve(match[1]); // token + return Promise.resolve(token); }) .then(token => { return request({ @@ -1497,8 +1483,12 @@ describe('Password Policy: ', () => { expect(response.status).toEqual(200); expect(response.text).toContain(token); expect(response.text).toContain(user.getUsername()); - expect(response.text).toContain('http://localhost:8378/1/apps/test/request_password_reset'); - expect(response.text).toContain('New password should not be the same as last 1 passwords'); + expect(response.text).toContain( + 'http://localhost:8378/1/apps/test/request_password_reset' + ); + expect(response.text).toContain( + 'New password should not be the same as last 1 passwords' + ); done(); return Promise.resolve(); }) diff --git a/spec/ValidationAndPasswordsReset.spec.js b/spec/ValidationAndPasswordsReset.spec.js index bed2866bf7..f38cabaeae 100644 --- a/spec/ValidationAndPasswordsReset.spec.js +++ b/spec/ValidationAndPasswordsReset.spec.js @@ -822,14 +822,13 @@ describe('Custom Pages, Email Verification, Password Reset', () => { expect(response.text).toContain( 'http://localhost:8378/1/apps/test/request_password_reset' ); - const re = /id="token" value="([a-zA-Z0-9]+)"/; - const match = response.text.match(re); - if (!match) { + + const token = response.headers['x-parse-page-param-token']; + if (!token) { fail('should have a token'); done(); return; } - const token = match[1]; request({ url: 'http://localhost:8378/1/apps/test/request_password_reset', @@ -897,17 +896,13 @@ describe('Custom Pages, Email Verification, Password Reset', () => { followRedirects: false, }).then(response => { expect(response.status).toEqual(200); - expect(response.text).toContain( - 'http://localhost:8378/1/apps/test/request_password_reset' - ); - const re = /id="token" value="([a-zA-Z0-9]+)"/; - const match = response.text.match(re); - if (!match) { + + const token = response.headers['x-parse-page-param-token']; + if (!token) { fail('should have a token'); done(); return; } - const token = match[1]; request({ url: 'http://localhost:8378/1/apps/test/request_password_reset', @@ -957,14 +952,12 @@ describe('Custom Pages, Email Verification, Password Reset', () => { followRedirects: false, }); expect(response.status).toEqual(200); - expect(response.text).toContain('http://localhost:8378/1/apps/test/request_password_reset'); - const re = /id="token" value="([a-zA-Z0-9]+)"/; - const match = response.text.match(re); - if (!match) { + + const token = response.headers['x-parse-page-param-token']; + if (!token) { fail('should have a token'); return; } - const token = match[1]; const resetResponse = await request({ url: 'http://localhost:8378/1/apps/test/request_password_reset', diff --git a/src/Routers/PublicAPIRouter.js b/src/Routers/PublicAPIRouter.js index fbe58caf87..e5004431f3 100644 --- a/src/Routers/PublicAPIRouter.js +++ b/src/Routers/PublicAPIRouter.js @@ -8,23 +8,36 @@ import { Parse } from 'parse/node'; import Utils from '../Utils'; const publicPath = path.resolve(__dirname, '../../public'); -const defaultPagePath = (file) => { return path.join(publicPath, file) }; -const defaultPageUrl = (file, serverUrl) => { return new URL('/apps/' + file, serverUrl).toString(); }; +const defaultPagePath = file => { + return path.join(publicPath, file); +}; +const defaultPageUrl = (file, serverUrl) => { + return new URL('/apps/' + file, serverUrl).toString(); +}; const pages = Object.freeze({ invalidLink: { customPageKey: 'invalidLink', defaultFile: 'invalid_link.html' }, linkSendFail: { customPageKey: 'linkSendFail', defaultFile: 'link_send_fail.html' }, choosePassword: { customPageKey: 'choosePassword', defaultFile: 'choose_password.html' }, linkSendSuccess: { customPageKey: 'linkSendSuccess', defaultFile: 'link_send_success.html' }, - verifyEmailSuccess: { customPageKey: 'verifyEmailSuccess', defaultFile: 'verify_email_success.html' }, - passwordResetSuccess: { customPageKey: 'passwordResetSuccess', defaultFile: 'password_reset_success.html' }, - invalidVerificationLink: { customPageKey: 'invalidVerificationLink', defaultFile: 'invalid_verification_link.html' }, + verifyEmailSuccess: { + customPageKey: 'verifyEmailSuccess', + defaultFile: 'verify_email_success.html', + }, + passwordResetSuccess: { + customPageKey: 'passwordResetSuccess', + defaultFile: 'password_reset_success.html', + }, + invalidVerificationLink: { + customPageKey: 'invalidVerificationLink', + defaultFile: 'invalid_verification_link.html', + }, }); const pageParams = Object.freeze({ - appName: "appName", - appId: "appId", - token: "token", - username: "username", - error: "error", + appName: 'appName', + appId: 'appId', + token: 'token', + username: 'username', + error: 'error', }); export class PublicAPIRouter extends PromiseRouter { @@ -45,7 +58,7 @@ export class PublicAPIRouter extends PromiseRouter { return userController.verifyEmail(username, token).then( () => { const params = { - [pageParams.username]: username + [pageParams.username]: username, }; return this.goToPage(req, pages.verifyEmailSuccess, params); }, @@ -116,7 +129,7 @@ export class PublicAPIRouter extends PromiseRouter { return this.goToPage(req, pages.choosePassword, params); }, () => { - return this.goToPage(req, pages.invalidLink) + return this.goToPage(req, pages.invalidLink); } ); } @@ -178,7 +191,7 @@ export class PublicAPIRouter extends PromiseRouter { const encodedUsername = encodeURIComponent(username); const query = result.success ? { - [pageParams.username]: encodedUsername + [pageParams.username]: encodedUsername, } : { [pageParams.username]: username, @@ -187,9 +200,7 @@ export class PublicAPIRouter extends PromiseRouter { [pageParams.error]: result.err, [pageParams.appName]: config.appName, }; - const page = result.success - ? pages.passwordResetSuccess - : pages.choosePassword; + const page = result.success ? pages.passwordResetSuccess : pages.choosePassword; return this.goToPage(req, page, query, false); }); @@ -216,9 +227,7 @@ export class PublicAPIRouter extends PromiseRouter { const redirect = responseType !== undefined ? responseType : req.method == 'POST'; // Ensure required config - if ([ - config.publicServerURL, - ].includes(undefined)) { + if ([config.publicServerURL].includes(undefined)) { return this.notFound(); } @@ -229,13 +238,19 @@ export class PublicAPIRouter extends PromiseRouter { const defaultUrl = defaultPageUrl(defaultFile, config.publicServerURL); // If custom page is set redirect to it without localization - if (customPage) { return this.redirectResponse(customPage, params); } + if (customPage) { + return this.redirectResponse(customPage, params); + } // If localization is enabled if (config.enablePageLocalization && locale) { - return Utils.getLocalizedPath(defaultPath, locale).then(({ path, subdir }) => redirect - ? this.redirectResponse(new URL(`/apps/${subdir}/${defaultFile}`, config.publicServerURL).toString(), params) - : this.pageResponse(req, path, params) + return Utils.getLocalizedPath(defaultPath, locale).then(({ path, subdir }) => + redirect + ? this.redirectResponse( + new URL(`/apps/${subdir}/${defaultFile}`, config.publicServerURL).toString(), + params + ) + : this.pageResponse(req, path, params) ); } else { return redirect @@ -253,11 +268,13 @@ export class PublicAPIRouter extends PromiseRouter { * @returns {Object} The express file response. */ async pageResponse(req, path, placeholders) { - // Aggreate placeholders - placeholders = Object.assign({ - 'parseServerUrl': req.config.publicServerURL - }, placeholders); + placeholders = Object.assign( + { + parseServerUrl: req.config.publicServerURL, + }, + placeholders + ); // If any of the placeholder values fails to resolve if (Object.values(placeholders).includes(undefined)) { @@ -277,7 +294,14 @@ export class PublicAPIRouter extends PromiseRouter { data = data.replace(`{{${placeholder[0]}}}`, placeholder[1]); } - return { text: data }; + // Add placeholers in header to allow parsing for programmatic use + // of response, instead of having to parse the HTML content. + const headers = Object.entries(placeholders).reduce((m, p) => { + m[`x-parse-page-param-${p[0].toLowerCase()}`] = p[1]; + return m; + }, {}); + + return { text: data, headers: headers }; } /** @@ -339,7 +363,9 @@ export class PublicAPIRouter extends PromiseRouter { } ); - this.route('GET', '/apps/choose_password', + this.route( + 'GET', + '/apps/choose_password', req => { this.setConfig(req); }, From 7a04836d2ed087cb8d65296f547c0926c007fcaa Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Sun, 17 Jan 2021 18:46:49 +0100 Subject: [PATCH 06/48] refactored tests for PublicAPIRouter --- spec/PublicAPI.spec.js | 293 +++++++++++++++++------ spec/ValidationAndPasswordsReset.spec.js | 147 ------------ 2 files changed, 215 insertions(+), 225 deletions(-) diff --git a/spec/PublicAPI.spec.js b/spec/PublicAPI.spec.js index 9add351cb3..793bc93d37 100644 --- a/spec/PublicAPI.spec.js +++ b/spec/PublicAPI.spec.js @@ -1,13 +1,8 @@ -const req = require('../lib/request'); +'use strict'; -const request = function (url, callback) { - return req({ - url, - }).then( - response => callback(null, response), - err => callback(err, err) - ); -}; +const request = require('../lib/request'); +const Utils = require('../lib/Utils'); +const { PublicAPIRouter, pages } = require('../lib/Routers/PublicAPIRouter'); describe('public API', () => { it('should return missing username error on ajax request without username provided', async () => { @@ -16,7 +11,7 @@ describe('public API', () => { }); try { - await req({ + await request({ method: 'POST', url: 'http://localhost:8378/1/apps/test/request_password_reset', body: `new_password=user1&token=43634643&username=`, @@ -38,7 +33,7 @@ describe('public API', () => { }); try { - await req({ + await request({ method: 'POST', url: 'http://localhost:8378/1/apps/test/request_password_reset', body: `new_password=user1&token=&username=Johnny`, @@ -60,7 +55,7 @@ describe('public API', () => { }); try { - await req({ + await request({ method: 'POST', url: 'http://localhost:8378/1/apps/test/request_password_reset', body: `new_password=&token=132414&username=Johnny`, @@ -76,109 +71,251 @@ describe('public API', () => { } }); - it('should get invalid_link.html', done => { - request('http://localhost:8378/1/apps/invalid_link.html', (err, httpResponse) => { - expect(httpResponse.status).toBe(200); - done(); + it('should get invalid_link.html', async () => { + const httpResponse = await request({ + url: 'http://localhost:8378/1/apps/invalid_link.html', }); + expect(httpResponse.status).toBe(200); }); - it('should get choose_password', done => { - reconfigureServer({ + it('should get choose_password', async () => { + await reconfigureServer({ appName: 'unused', publicServerURL: 'http://localhost:8378/1', - }).then(() => { - request('http://localhost:8378/1/apps/choose_password?appId=test', (err, httpResponse) => { - expect(httpResponse.status).toBe(200); - done(); - }); }); + const httpResponse = await request({ + url: 'http://localhost:8378/1/apps/choose_password?appId=test', + }); + expect(httpResponse.status).toBe(200); }); - it('should get verify_email_success.html', done => { - request('http://localhost:8378/1/apps/verify_email_success.html', (err, httpResponse) => { - expect(httpResponse.status).toBe(200); - done(); + it('should get verify_email_success.html', async () => { + const httpResponse = await request({ + url: 'http://localhost:8378/1/apps/verify_email_success.html', }); + expect(httpResponse.status).toBe(200); }); - it('should get password_reset_success.html', done => { - request('http://localhost:8378/1/apps/password_reset_success.html', (err, httpResponse) => { - expect(httpResponse.status).toBe(200); - done(); + it('should get password_reset_success.html', async () => { + const httpResponse = await request({ + url: 'http://localhost:8378/1/apps/password_reset_success.html', }); + expect(httpResponse.status).toBe(200); }); -}); -describe('public API without publicServerURL', () => { - beforeEach(done => { - reconfigureServer({ appName: 'unused' }).then(done, fail); - }); - it('should get 404 on verify_email', done => { - request('http://localhost:8378/1/apps/test/verify_email', (err, httpResponse) => { + describe('public API without publicServerURL', function () { + beforeEach(async () => { + await reconfigureServer({ appName: 'unused' }); + }); + + it('should get 404 on verify_email', async () => { + const httpResponse = await request({ + url: 'http://localhost:8378/1/apps/test/verify_email', + }).catch(e => e); expect(httpResponse.status).toBe(404); - done(); }); - }); - it('should get 404 choose_password', done => { - request('http://localhost:8378/1/apps/choose_password?appId=test', (err, httpResponse) => { + it('should get 404 choose_password', async () => { + const httpResponse = await request({ + url: 'http://localhost:8378/1/apps/choose_password?appId=test', + }).catch(e => e); expect(httpResponse.status).toBe(404); - done(); }); - }); - it('should get 404 on request_password_reset', done => { - request('http://localhost:8378/1/apps/test/request_password_reset', (err, httpResponse) => { + it('should get 404 on request_password_reset', async () => { + const httpResponse = await request({ + url: 'http://localhost:8378/1/apps/test/request_password_reset', + }).catch(e => e); expect(httpResponse.status).toBe(404); - done(); }); }); -}); -describe('public API supplied with invalid application id', () => { - beforeEach(done => { - reconfigureServer({ appName: 'unused' }).then(done, fail); - }); + describe('public API supplied with invalid application id', () => { + beforeEach(async () => { + await reconfigureServer({ appName: 'unused' }); + }); - it('should get 403 on verify_email', done => { - request('http://localhost:8378/1/apps/invalid/verify_email', (err, httpResponse) => { + it('should get 403 on verify_email', async () => { + const httpResponse = await request({ + url: 'http://localhost:8378/1/apps/invalid/verify_email', + }).catch(e => e); expect(httpResponse.status).toBe(403); - done(); }); - }); - it('should get 403 choose_password', done => { - request('http://localhost:8378/1/apps/choose_password?id=invalid', (err, httpResponse) => { + it('should get 403 choose_password', async () => { + const httpResponse = await request({ + url: 'http://localhost:8378/1/apps/choose_password?id=invalid', + }).catch(e => e); expect(httpResponse.status).toBe(403); - done(); }); - }); - it('should get 403 on get of request_password_reset', done => { - request('http://localhost:8378/1/apps/invalid/request_password_reset', (err, httpResponse) => { + it('should get 403 on get of request_password_reset', async () => { + const httpResponse = await request({ + url: 'http://localhost:8378/1/apps/invalid/request_password_reset', + }).catch(e => e); expect(httpResponse.status).toBe(403); - done(); }); - }); - it('should get 403 on post of request_password_reset', done => { - req({ - url: 'http://localhost:8378/1/apps/invalid/request_password_reset', - method: 'POST', - }).then(done.fail, httpResponse => { + it('should get 403 on post of request_password_reset', async () => { + const httpResponse = await request({ + url: 'http://localhost:8378/1/apps/invalid/request_password_reset', + method: 'POST', + }).catch(e => e); + expect(httpResponse.status).toBe(403); + }); + + it('should get 403 on resendVerificationEmail', async () => { + const httpResponse = await request({ + url: 'http://localhost:8378/1/apps/invalid/resend_verification_email', + }).catch(e => e); expect(httpResponse.status).toBe(403); - done(); }); }); - it('should get 403 on resendVerificationEmail', done => { - request( - 'http://localhost:8378/1/apps/invalid/resend_verification_email', - (err, httpResponse) => { - expect(httpResponse.status).toBe(403); - done(); - } - ); + describe('pages', () => { + let router = new PublicAPIRouter(); + let req; + let pageResponse; + let redirectResponse; + const config = { + appId: 'test', + appName: 'ExampleAppName', + verifyUserEmails: true, + emailAdapter: { + sendVerificationEmail: () => Promise.resolve(), + sendPasswordResetEmail: () => Promise.resolve(), + sendMail: () => {}, + }, + publicServerURL: 'http://localhost:8378/1', + enablePageLocalization: true, + }; + + beforeEach(async () => { + router = new PublicAPIRouter(); + pageResponse = spyOn(router, 'pageResponse').and.callThrough(); + redirectResponse = spyOn(router, 'redirectResponse').and.callThrough(); + req = { + method: 'GET', + config: { + customPages: {}, + enablePageLocalization: true, + publicServerURL: 'http://example.com', + }, + query: { + locale: 'de-AT', + }, + }; + }); + + describe('localization', () => { + it('returns default file if localization is disabled', async () => { + delete req.config.enablePageLocalization; + + await expectAsync(router.goToPage(req, pages.invalidLink)).toBeResolved(); + expect(pageResponse.calls.all()[0].args[1]).toBeDefined(); + expect(pageResponse.calls.all()[0].args[1]).not.toMatch( + new RegExp(`\/de(-AT)?\/${pages.invalidLink.defaultFile}`) + ); + }); + + it('returns default file if no locale is specified', async () => { + delete req.query.locale; + + await expectAsync(router.goToPage(req, pages.invalidLink)).toBeResolved(); + expect(pageResponse.calls.all()[0].args[1]).toBeDefined(); + expect(pageResponse.calls.all()[0].args[1]).not.toMatch( + new RegExp(`\/de(-AT)?\/${pages.invalidLink.defaultFile}`) + ); + }); + + it('returns custom page regardless of localization enabled', async () => { + req.config.customPages = { invalidLink: 'http://invalid-link.example.com' }; + + await expectAsync(router.goToPage(req, pages.invalidLink)).toBeResolved(); + expect(pageResponse).not.toHaveBeenCalled(); + expect(redirectResponse.calls.all()[0].args[0]).toBe(req.config.customPages.invalidLink); + }); + + it('returns file for locale match', async () => { + await expectAsync(router.goToPage(req, pages.invalidLink)).toBeResolved(); + expect(pageResponse.calls.all()[0].args[1]).toBeDefined(); + expect(pageResponse.calls.all()[0].args[1]).toMatch( + new RegExp(`\/de-AT\/${pages.invalidLink.defaultFile}`) + ); + }); + + it('returns file for language match', async () => { + // Pretend no locale matching file exists + spyOn(Utils, 'fileExists').and.callFake(async path => { + return !path.includes(`/de-AT/${pages.invalidLink.defaultFile}`); + }); + + await expectAsync(router.goToPage(req, pages.invalidLink)).toBeResolved(); + expect(pageResponse.calls.all()[0].args[1]).toBeDefined(); + expect(pageResponse.calls.all()[0].args[1]).toMatch( + new RegExp(`\/de\/${pages.invalidLink.defaultFile}`) + ); + }); + + it('returns default file for neither locale nor language match', async () => { + req.query.locale = 'yo-LO'; + + await expectAsync(router.goToPage(req, pages.invalidLink)).toBeResolved(); + expect(pageResponse.calls.all()[0].args[1]).toBeDefined(); + expect(pageResponse.calls.all()[0].args[1]).not.toMatch( + new RegExp(`\/yo(-LO)?\/${pages.invalidLink.defaultFile}`) + ); + }); + + it('returns a file for GET request', async () => { + await expectAsync(router.goToPage(req, pages.invalidLink)).toBeResolved(); + expect(pageResponse).toHaveBeenCalled(); + expect(redirectResponse).not.toHaveBeenCalled(); + }); + + it('returns a redirect for POST request', async () => { + req.method = 'POST'; + await expectAsync(router.goToPage(req, pages.invalidLink)).toBeResolved(); + expect(pageResponse).not.toHaveBeenCalled(); + expect(redirectResponse).toHaveBeenCalled(); + }); + + it('returns a redirect for custom pages for GET and POST', async () => { + req.config.customPages = { invalidLink: 'http://invalid-link.example.com' }; + + for (const method of ['GET', 'POST']) { + req.method = method; + await expectAsync(router.goToPage(req, pages.invalidLink)).toBeResolved(); + expect(pageResponse).not.toHaveBeenCalled(); + expect(redirectResponse).toHaveBeenCalled(); + } + }); + + it('responds to POST request with redirect response (e2e test)', async () => { + await reconfigureServer(config); + const response = await request({ + url: + 'http://localhost:8378/1/apps/test/request_password_reset?token=exampleToken&username=exampleUsername&locale=de-AT', + followRedirects: false, + method: 'POST', + }); + expect(response.status).toEqual(303); + expect(response.headers.location).toEqual( + 'http://localhost:8378/apps/de-AT/invalid_link.html' + ); + }); + + it('responds to GET request with content response (e2e test)', async () => { + await reconfigureServer(config); + const response = await request({ + url: + 'http://localhost:8378/1/apps/test/request_password_reset?token=exampleToken&username=exampleUsername&locale=de-AT', + followRedirects: false, + method: 'GET', + }); + expect(response.status).toEqual(200); + expect(response.text).toContain(''); + }); + }); }); }); diff --git a/spec/ValidationAndPasswordsReset.spec.js b/spec/ValidationAndPasswordsReset.spec.js index f38cabaeae..fc4041f37e 100644 --- a/spec/ValidationAndPasswordsReset.spec.js +++ b/spec/ValidationAndPasswordsReset.spec.js @@ -1,10 +1,8 @@ 'use strict'; -const { PublicAPIRouter, pages } = require('../lib/Routers/PublicAPIRouter'); const MockEmailAdapterWithOptions = require('./MockEmailAdapterWithOptions'); const request = require('../lib/request'); const Config = require('../lib/Config'); -const Utils = require('../lib/Utils'); describe('Custom Pages, Email Verification, Password Reset', () => { it('should set the custom pages', done => { @@ -1077,149 +1075,4 @@ describe('Custom Pages, Email Verification, Password Reset', () => { done(); }); }); - - describe('localization of custom pages', () => { - let router = new PublicAPIRouter(); - let req; - let pageResponse; - let redirectResponse; - const config = { - appId: 'test', - appName: 'ExampleAppName', - verifyUserEmails: true, - emailAdapter: { - sendVerificationEmail: () => Promise.resolve(), - sendPasswordResetEmail: () => Promise.resolve(), - sendMail: () => {}, - }, - publicServerURL: 'http://localhost:8378/1', - enablePageLocalization: true, - }; - - beforeEach(async () => { - router = new PublicAPIRouter(); - pageResponse = spyOn(router, 'pageResponse').and.callThrough(); - redirectResponse = spyOn(router, 'redirectResponse').and.callThrough(); - req = { - method: 'GET', - config: { - customPages: {}, - enablePageLocalization: true, - publicServerURL: 'http://example.com', - }, - query: { - locale: 'de-AT', - }, - }; - }); - - it('returns default file if localization is disabled', async () => { - delete req.config.enablePageLocalization; - - await expectAsync(router.goToPage(req, pages.invalidLink)).toBeResolved(); - expect(pageResponse.calls.all()[0].args[1]).toBeDefined(); - expect(pageResponse.calls.all()[0].args[1]).not.toMatch( - new RegExp(`\/de(-AT)?\/${pages.invalidLink.defaultFile}`) - ); - }); - - it('returns default file if no locale is specified', async () => { - delete req.query.locale; - - await expectAsync(router.goToPage(req, pages.invalidLink)).toBeResolved(); - expect(pageResponse.calls.all()[0].args[1]).toBeDefined(); - expect(pageResponse.calls.all()[0].args[1]).not.toMatch( - new RegExp(`\/de(-AT)?\/${pages.invalidLink.defaultFile}`) - ); - }); - - it('returns custom page regardless of localization enabled', async () => { - req.config.customPages = { invalidLink: 'http://invalid-link.example.com' }; - - await expectAsync(router.goToPage(req, pages.invalidLink)).toBeResolved(); - expect(pageResponse).not.toHaveBeenCalled(); - expect(redirectResponse.calls.all()[0].args[0]).toBe(req.config.customPages.invalidLink); - }); - - it('returns file for locale match', async () => { - await expectAsync(router.goToPage(req, pages.invalidLink)).toBeResolved(); - expect(pageResponse.calls.all()[0].args[1]).toBeDefined(); - expect(pageResponse.calls.all()[0].args[1]).toMatch( - new RegExp(`\/de-AT\/${pages.invalidLink.defaultFile}`) - ); - }); - - it('returns file for language match', async () => { - // Pretend no locale matching file exists - spyOn(Utils, 'fileExists').and.callFake(async path => { - return !path.includes(`/de-AT/${pages.invalidLink.defaultFile}`); - }); - - await expectAsync(router.goToPage(req, pages.invalidLink)).toBeResolved(); - expect(pageResponse.calls.all()[0].args[1]).toBeDefined(); - expect(pageResponse.calls.all()[0].args[1]).toMatch( - new RegExp(`\/de\/${pages.invalidLink.defaultFile}`) - ); - }); - - it('returns default file for neither locale nor language match', async () => { - req.query.locale = 'yo-LO'; - - await expectAsync(router.goToPage(req, pages.invalidLink)).toBeResolved(); - expect(pageResponse.calls.all()[0].args[1]).toBeDefined(); - expect(pageResponse.calls.all()[0].args[1]).not.toMatch( - new RegExp(`\/yo(-LO)?\/${pages.invalidLink.defaultFile}`) - ); - }); - - it('returns a file for GET request', async () => { - await expectAsync(router.goToPage(req, pages.invalidLink)).toBeResolved(); - expect(pageResponse).toHaveBeenCalled(); - expect(redirectResponse).not.toHaveBeenCalled(); - }); - - it('returns a redirect for POST request', async () => { - req.method = 'POST'; - await expectAsync(router.goToPage(req, pages.invalidLink)).toBeResolved(); - expect(pageResponse).not.toHaveBeenCalled(); - expect(redirectResponse).toHaveBeenCalled(); - }); - - it('returns a redirect for custom pages for GET and POST', async () => { - req.config.customPages = { invalidLink: 'http://invalid-link.example.com' }; - - for (const method of ['GET', 'POST']) { - req.method = method; - await expectAsync(router.goToPage(req, pages.invalidLink)).toBeResolved(); - expect(pageResponse).not.toHaveBeenCalled(); - expect(redirectResponse).toHaveBeenCalled(); - } - }); - - it('responds to POST request with redirect response (e2e test)', async () => { - await reconfigureServer(config); - const response = await request({ - url: - 'http://localhost:8378/1/apps/test/request_password_reset?token=exampleToken&username=exampleUsername&locale=de-AT', - followRedirects: false, - method: 'POST', - }); - expect(response.status).toEqual(303); - expect(response.headers.location).toEqual( - 'http://localhost:8378/apps/de-AT/invalid_link.html' - ); - }); - - it('responds to GET request with content response (e2e test)', async () => { - await reconfigureServer(config); - const response = await request({ - url: - 'http://localhost:8378/1/apps/test/request_password_reset?token=exampleToken&username=exampleUsername&locale=de-AT', - followRedirects: false, - method: 'GET', - }); - expect(response.status).toEqual(200); - expect(response.text).toContain(''); - }); - }); }); From d80e31bd7bb2c3bda20992fdbe5ab0284ab708af Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Sun, 17 Jan 2021 20:12:57 +0100 Subject: [PATCH 07/48] added mustache lib for template rendering --- package-lock.json | 5 ++++ package.json | 1 + public/choose_password.html | 9 ++++--- public/de-AT/invalid_link.html | 1 + public/de/invalid_link.html | 1 + public/invalid_link.html | 1 + public/invalid_verification_link.html | 5 ++-- public/link_send_fail.html | 1 + public/link_send_success.html | 1 + public/password_reset_success.html | 1 + public/verify_email_success.html | 1 + spec/PublicAPI.spec.js | 39 +++++++++++++++++++++++++-- src/Routers/PublicAPIRouter.js | 9 ++++--- 13 files changed, 63 insertions(+), 12 deletions(-) diff --git a/package-lock.json b/package-lock.json index b492d6897c..9328ac5184 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9763,6 +9763,11 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "mustache": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.1.0.tgz", + "integrity": "sha512-0FsgP/WVq4mKyjolIyX+Z9Bd+3WS8GOwoUTyKXT5cTYMGeauNTi2HPCwERqseC1IHAy0Z7MDZnJBfjabd4O8GQ==" + }, "mute-stream": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", diff --git a/package.json b/package.json index b60287b1c7..1a628cde5c 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "lru-cache": "5.1.1", "mime": "2.4.7", "mongodb": "3.6.3", + "mustache": "4.1.0", "parse": "2.19.0", "pg-promise": "10.8.6", "pluralize": "8.0.0", diff --git a/public/choose_password.html b/public/choose_password.html index f5fd731490..9e862f0f13 100644 --- a/public/choose_password.html +++ b/public/choose_password.html @@ -16,15 +16,16 @@ +

{{appName}}

Reset Your Password

-

You can set a new Password for your {{appName}} account: {{username}}

+

You can set a new Password for your account: {{username}}


{{error}}

-
+ - - + +

New Password

diff --git a/public/de-AT/invalid_link.html b/public/de-AT/invalid_link.html index 2d9ac315a1..07a204a8a7 100644 --- a/public/de-AT/invalid_link.html +++ b/public/de-AT/invalid_link.html @@ -12,6 +12,7 @@ +

{{appName}}

Invalid Link

diff --git a/public/de/invalid_link.html b/public/de/invalid_link.html index 2d9ac315a1..07a204a8a7 100644 --- a/public/de/invalid_link.html +++ b/public/de/invalid_link.html @@ -12,6 +12,7 @@ +

{{appName}}

Invalid Link

diff --git a/public/invalid_link.html b/public/invalid_link.html index 2d9ac315a1..07a204a8a7 100644 --- a/public/invalid_link.html +++ b/public/invalid_link.html @@ -12,6 +12,7 @@ +

{{appName}}

Invalid Link

diff --git a/public/invalid_verification_link.html b/public/invalid_verification_link.html index 45b3e9e1ba..6f2bd20ca6 100644 --- a/public/invalid_verification_link.html +++ b/public/invalid_verification_link.html @@ -12,9 +12,10 @@ +

{{appName}}

Invalid verification link!

- - + +
diff --git a/public/link_send_fail.html b/public/link_send_fail.html index a51e55242b..8e1ba0ca06 100644 --- a/public/link_send_fail.html +++ b/public/link_send_fail.html @@ -12,6 +12,7 @@ +

{{appName}}

Invalid link!

No link sent. User not found or email already verified.

diff --git a/public/link_send_success.html b/public/link_send_success.html index 8b48da2e06..fa2d7f2ee5 100644 --- a/public/link_send_success.html +++ b/public/link_send_success.html @@ -12,6 +12,7 @@ +

{{appName}}

Link sent!

A new link has been sent. Check your email.

diff --git a/public/password_reset_success.html b/public/password_reset_success.html index 937dffe8c9..618b0e29dd 100644 --- a/public/password_reset_success.html +++ b/public/password_reset_success.html @@ -12,6 +12,7 @@ +

{{appName}}

Success!

Your password has been updated.

diff --git a/public/verify_email_success.html b/public/verify_email_success.html index 8cb4d3b902..51896be56f 100644 --- a/public/verify_email_success.html +++ b/public/verify_email_success.html @@ -11,6 +11,7 @@ +

{{appName}}

Email verified!

Successfully verified your email for account: {{username}}.

diff --git a/spec/PublicAPI.spec.js b/spec/PublicAPI.spec.js index 793bc93d37..410c98a5d0 100644 --- a/spec/PublicAPI.spec.js +++ b/spec/PublicAPI.spec.js @@ -1,6 +1,7 @@ 'use strict'; const request = require('../lib/request'); +const fs = require('fs/promises'); const Utils = require('../lib/Utils'); const { PublicAPIRouter, pages } = require('../lib/Routers/PublicAPIRouter'); @@ -177,6 +178,7 @@ describe('public API', () => { let req; let pageResponse; let redirectResponse; + let readFile; const config = { appId: 'test', appName: 'ExampleAppName', @@ -188,18 +190,22 @@ describe('public API', () => { }, publicServerURL: 'http://localhost:8378/1', enablePageLocalization: true, + customPages: {}, }; beforeEach(async () => { router = new PublicAPIRouter(); + readFile = spyOn(fs, 'readFile').and.callThrough(); pageResponse = spyOn(router, 'pageResponse').and.callThrough(); redirectResponse = spyOn(router, 'redirectResponse').and.callThrough(); req = { method: 'GET', config: { - customPages: {}, + appId: 'test', + appName: 'ExampleAppName', + publicServerURL: 'http://localhost:8378/1', enablePageLocalization: true, - publicServerURL: 'http://example.com', + customPages: {}, }, query: { locale: 'de-AT', @@ -207,6 +213,35 @@ describe('public API', () => { }; }); + describe('placeholders', () => { + it('replaces placeholder in response content', async () => { + await expectAsync(router.goToPage(req, pages.invalidLink)).toBeResolved(); + + expect(readFile.calls.all()[0].returnValue).toBeDefined(); + const originalContent = await readFile.calls.all()[0].returnValue; + expect(originalContent).toContain('{{appName}}'); + + expect(pageResponse.calls.all()[0].returnValue).toBeDefined(); + const replacedContent = await pageResponse.calls.all()[0].returnValue; + expect(replacedContent.text).not.toContain('{{appName}}'); + expect(replacedContent.text).toContain(req.config.appName); + }); + + it('removes undefined placeholder in response content', async () => { + await expectAsync(router.goToPage(req, pages.choosePassword)).toBeResolved(); + + expect(readFile.calls.all()[0].returnValue).toBeDefined(); + const originalContent = await readFile.calls.all()[0].returnValue; + expect(originalContent).toContain('{{error}}'); + + // There is no error placeholder value set by default, so the + // {{error}} placeholder should just be removed from content + expect(pageResponse.calls.all()[0].returnValue).toBeDefined(); + const replacedContent = await pageResponse.calls.all()[0].returnValue; + expect(replacedContent.text).not.toContain('{{error}}'); + }); + }); + describe('localization', () => { it('returns default file if localization is disabled', async () => { delete req.config.enablePageLocalization; diff --git a/src/Routers/PublicAPIRouter.js b/src/Routers/PublicAPIRouter.js index e5004431f3..1bef05ea1d 100644 --- a/src/Routers/PublicAPIRouter.js +++ b/src/Routers/PublicAPIRouter.js @@ -6,6 +6,7 @@ import { promises as fs } from 'fs'; import qs from 'querystring'; import { Parse } from 'parse/node'; import Utils from '../Utils'; +import mustache from 'mustache'; const publicPath = path.resolve(__dirname, '../../public'); const defaultPagePath = file => { @@ -271,7 +272,9 @@ export class PublicAPIRouter extends PromiseRouter { // Aggreate placeholders placeholders = Object.assign( { + // Default placeholders available for every page parseServerUrl: req.config.publicServerURL, + appName: req.config.appName, }, placeholders ); @@ -289,10 +292,8 @@ export class PublicAPIRouter extends PromiseRouter { return this.notFound(); } - // Fill placeholders in content - for (const placeholder of Object.entries(placeholders)) { - data = data.replace(`{{${placeholder[0]}}}`, placeholder[1]); - } + // Fill placeholders + data = mustache.render(data, placeholders); // Add placeholers in header to allow parsing for programmatic use // of response, instead of having to parse the HTML content. From cd46b0dd7c71600943709bb5007e755bfefd541c Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Sun, 17 Jan 2021 20:44:10 +0100 Subject: [PATCH 08/48] fixed fs.promises module reference --- spec/PublicAPI.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/PublicAPI.spec.js b/spec/PublicAPI.spec.js index 410c98a5d0..7d2ea4ef56 100644 --- a/spec/PublicAPI.spec.js +++ b/spec/PublicAPI.spec.js @@ -1,7 +1,7 @@ 'use strict'; const request = require('../lib/request'); -const fs = require('fs/promises'); +const fs = require('fs').promises; const Utils = require('../lib/Utils'); const { PublicAPIRouter, pages } = require('../lib/Routers/PublicAPIRouter'); From 7a24c717b165c77f658ba0af93b8e4826ffc2d17 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Mon, 18 Jan 2021 04:02:16 +0100 Subject: [PATCH 09/48] fixed template placeholder typo --- public/choose_password.html | 5 ++- public/de-AT/choose_password.html | 62 +++++++++++++++++++++++++++++++ public/de/choose_password.html | 62 +++++++++++++++++++++++++++++++ 3 files changed, 127 insertions(+), 2 deletions(-) create mode 100644 public/de-AT/choose_password.html create mode 100644 public/de/choose_password.html diff --git a/public/choose_password.html b/public/choose_password.html index 9e862f0f13..d9a34699d6 100644 --- a/public/choose_password.html +++ b/public/choose_password.html @@ -22,10 +22,11 @@

Reset Your Password

You can set a new Password for your account: {{username}}


{{error}}

-
+ - + +

New Password

diff --git a/public/de-AT/choose_password.html b/public/de-AT/choose_password.html new file mode 100644 index 0000000000..d9a34699d6 --- /dev/null +++ b/public/de-AT/choose_password.html @@ -0,0 +1,62 @@ + + + + +Password Reset + + + +

{{appName}}

+

Reset Your Password

+ +

You can set a new Password for your account: {{username}}

+
+

{{error}}

+ + + + + + +

New Password

+ +

Confirm New Password

+ +
+

+
+ +
+ + + diff --git a/public/de/choose_password.html b/public/de/choose_password.html new file mode 100644 index 0000000000..d9a34699d6 --- /dev/null +++ b/public/de/choose_password.html @@ -0,0 +1,62 @@ + + + + +Password Reset + + + +

{{appName}}

+

Reset Your Password

+ +

You can set a new Password for your account: {{username}}

+
+

{{error}}

+
+ + + + + +

New Password

+ +

Confirm New Password

+ +
+

+
+ +
+ + + From e5e73a7ee548899cd4cd3706bb928e28ecae9fbb Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Mon, 18 Jan 2021 04:03:37 +0100 Subject: [PATCH 10/48] changed redirect response to provide headers instead of query parameters --- spec/PublicAPI.spec.js | 186 +++++++++++++++++---------------- src/Routers/PublicAPIRouter.js | 113 +++++++++++--------- 2 files changed, 158 insertions(+), 141 deletions(-) diff --git a/spec/PublicAPI.spec.js b/spec/PublicAPI.spec.js index 7d2ea4ef56..6c7759bae4 100644 --- a/spec/PublicAPI.spec.js +++ b/spec/PublicAPI.spec.js @@ -3,105 +3,107 @@ const request = require('../lib/request'); const fs = require('fs').promises; const Utils = require('../lib/Utils'); -const { PublicAPIRouter, pages } = require('../lib/Routers/PublicAPIRouter'); +const { PublicAPIRouter, pages, pageParams } = require('../lib/Routers/PublicAPIRouter'); describe('public API', () => { - it('should return missing username error on ajax request without username provided', async () => { - await reconfigureServer({ - publicServerURL: 'http://localhost:8378/1', - }); - - try { - await request({ - method: 'POST', - url: 'http://localhost:8378/1/apps/test/request_password_reset', - body: `new_password=user1&token=43634643&username=`, - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'X-Requested-With': 'XMLHttpRequest', - }, - followRedirects: false, + describe('basic request', () => { + it('should return missing username error on ajax request without username provided', async () => { + await reconfigureServer({ + publicServerURL: 'http://localhost:8378/1', }); - } catch (error) { - expect(error.status).not.toBe(302); - expect(error.text).toEqual('{"code":200,"error":"Missing username"}'); - } - }); - it('should return missing token error on ajax request without token provided', async () => { - await reconfigureServer({ - publicServerURL: 'http://localhost:8378/1', + try { + await request({ + method: 'POST', + url: 'http://localhost:8378/1/apps/test/request_password_reset', + body: `new_password=user1&token=43634643&username=`, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'X-Requested-With': 'XMLHttpRequest', + }, + followRedirects: false, + }); + } catch (error) { + expect(error.status).not.toBe(302); + expect(error.text).toEqual('{"code":200,"error":"Missing username"}'); + } }); - try { - await request({ - method: 'POST', - url: 'http://localhost:8378/1/apps/test/request_password_reset', - body: `new_password=user1&token=&username=Johnny`, - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'X-Requested-With': 'XMLHttpRequest', - }, - followRedirects: false, + it('should return missing token error on ajax request without token provided', async () => { + await reconfigureServer({ + publicServerURL: 'http://localhost:8378/1', }); - } catch (error) { - expect(error.status).not.toBe(302); - expect(error.text).toEqual('{"code":-1,"error":"Missing token"}'); - } - }); - it('should return missing password error on ajax request without password provided', async () => { - await reconfigureServer({ - publicServerURL: 'http://localhost:8378/1', + try { + await request({ + method: 'POST', + url: 'http://localhost:8378/1/apps/test/request_password_reset', + body: `new_password=user1&token=&username=Johnny`, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'X-Requested-With': 'XMLHttpRequest', + }, + followRedirects: false, + }); + } catch (error) { + expect(error.status).not.toBe(302); + expect(error.text).toEqual('{"code":-1,"error":"Missing token"}'); + } }); - try { - await request({ - method: 'POST', - url: 'http://localhost:8378/1/apps/test/request_password_reset', - body: `new_password=&token=132414&username=Johnny`, - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'X-Requested-With': 'XMLHttpRequest', - }, - followRedirects: false, + it('should return missing password error on ajax request without password provided', async () => { + await reconfigureServer({ + publicServerURL: 'http://localhost:8378/1', }); - } catch (error) { - expect(error.status).not.toBe(302); - expect(error.text).toEqual('{"code":201,"error":"Missing password"}'); - } - }); - it('should get invalid_link.html', async () => { - const httpResponse = await request({ - url: 'http://localhost:8378/1/apps/invalid_link.html', + try { + await request({ + method: 'POST', + url: 'http://localhost:8378/1/apps/test/request_password_reset', + body: `new_password=&token=132414&username=Johnny`, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'X-Requested-With': 'XMLHttpRequest', + }, + followRedirects: false, + }); + } catch (error) { + expect(error.status).not.toBe(302); + expect(error.text).toEqual('{"code":201,"error":"Missing password"}'); + } }); - expect(httpResponse.status).toBe(200); - }); - it('should get choose_password', async () => { - await reconfigureServer({ - appName: 'unused', - publicServerURL: 'http://localhost:8378/1', + it('should get invalid_link.html', async () => { + const httpResponse = await request({ + url: 'http://localhost:8378/1/apps/invalid_link.html', + }); + expect(httpResponse.status).toBe(200); }); - const httpResponse = await request({ - url: 'http://localhost:8378/1/apps/choose_password?appId=test', + + it('should get choose_password', async () => { + await reconfigureServer({ + appName: 'unused', + publicServerURL: 'http://localhost:8378/1', + }); + const httpResponse = await request({ + url: 'http://localhost:8378/1/apps/choose_password?appId=test', + }); + expect(httpResponse.status).toBe(200); }); - expect(httpResponse.status).toBe(200); - }); - it('should get verify_email_success.html', async () => { - const httpResponse = await request({ - url: 'http://localhost:8378/1/apps/verify_email_success.html', + it('should get verify_email_success.html', async () => { + const httpResponse = await request({ + url: 'http://localhost:8378/1/apps/verify_email_success.html', + }); + expect(httpResponse.status).toBe(200); }); - expect(httpResponse.status).toBe(200); - }); - it('should get password_reset_success.html', async () => { - const httpResponse = await request({ - url: 'http://localhost:8378/1/apps/password_reset_success.html', + it('should get password_reset_success.html', async () => { + const httpResponse = await request({ + url: 'http://localhost:8378/1/apps/password_reset_success.html', + }); + expect(httpResponse.status).toBe(200); }); - expect(httpResponse.status).toBe(200); }); describe('public API without publicServerURL', function () { @@ -196,8 +198,8 @@ describe('public API', () => { beforeEach(async () => { router = new PublicAPIRouter(); readFile = spyOn(fs, 'readFile').and.callThrough(); - pageResponse = spyOn(router, 'pageResponse').and.callThrough(); - redirectResponse = spyOn(router, 'redirectResponse').and.callThrough(); + pageResponse = spyOn(PublicAPIRouter.prototype, 'pageResponse').and.callThrough() + redirectResponse = spyOn(PublicAPIRouter.prototype, 'redirectResponse').and.callThrough(); req = { method: 'GET', config: { @@ -247,8 +249,8 @@ describe('public API', () => { delete req.config.enablePageLocalization; await expectAsync(router.goToPage(req, pages.invalidLink)).toBeResolved(); - expect(pageResponse.calls.all()[0].args[1]).toBeDefined(); - expect(pageResponse.calls.all()[0].args[1]).not.toMatch( + expect(pageResponse.calls.all()[0].args[0]).toBeDefined(); + expect(pageResponse.calls.all()[0].args[0]).not.toMatch( new RegExp(`\/de(-AT)?\/${pages.invalidLink.defaultFile}`) ); }); @@ -257,8 +259,8 @@ describe('public API', () => { delete req.query.locale; await expectAsync(router.goToPage(req, pages.invalidLink)).toBeResolved(); - expect(pageResponse.calls.all()[0].args[1]).toBeDefined(); - expect(pageResponse.calls.all()[0].args[1]).not.toMatch( + expect(pageResponse.calls.all()[0].args[0]).toBeDefined(); + expect(pageResponse.calls.all()[0].args[0]).not.toMatch( new RegExp(`\/de(-AT)?\/${pages.invalidLink.defaultFile}`) ); }); @@ -273,8 +275,8 @@ describe('public API', () => { it('returns file for locale match', async () => { await expectAsync(router.goToPage(req, pages.invalidLink)).toBeResolved(); - expect(pageResponse.calls.all()[0].args[1]).toBeDefined(); - expect(pageResponse.calls.all()[0].args[1]).toMatch( + expect(pageResponse.calls.all()[0].args[0]).toBeDefined(); + expect(pageResponse.calls.all()[0].args[0]).toMatch( new RegExp(`\/de-AT\/${pages.invalidLink.defaultFile}`) ); }); @@ -286,8 +288,8 @@ describe('public API', () => { }); await expectAsync(router.goToPage(req, pages.invalidLink)).toBeResolved(); - expect(pageResponse.calls.all()[0].args[1]).toBeDefined(); - expect(pageResponse.calls.all()[0].args[1]).toMatch( + expect(pageResponse.calls.all()[0].args[0]).toBeDefined(); + expect(pageResponse.calls.all()[0].args[0]).toMatch( new RegExp(`\/de\/${pages.invalidLink.defaultFile}`) ); }); @@ -296,8 +298,8 @@ describe('public API', () => { req.query.locale = 'yo-LO'; await expectAsync(router.goToPage(req, pages.invalidLink)).toBeResolved(); - expect(pageResponse.calls.all()[0].args[1]).toBeDefined(); - expect(pageResponse.calls.all()[0].args[1]).not.toMatch( + expect(pageResponse.calls.all()[0].args[0]).toBeDefined(); + expect(pageResponse.calls.all()[0].args[0]).not.toMatch( new RegExp(`\/yo(-LO)?\/${pages.invalidLink.defaultFile}`) ); }); @@ -315,7 +317,7 @@ describe('public API', () => { expect(redirectResponse).toHaveBeenCalled(); }); - it('returns a redirect for custom pages for GET and POST', async () => { + it('returns a redirect for custom pages for GET and POST request', async () => { req.config.customPages = { invalidLink: 'http://invalid-link.example.com' }; for (const method of ['GET', 'POST']) { diff --git a/src/Routers/PublicAPIRouter.js b/src/Routers/PublicAPIRouter.js index 1bef05ea1d..f9ac7d4cad 100644 --- a/src/Routers/PublicAPIRouter.js +++ b/src/Routers/PublicAPIRouter.js @@ -9,37 +9,30 @@ import Utils from '../Utils'; import mustache from 'mustache'; const publicPath = path.resolve(__dirname, '../../public'); -const defaultPagePath = file => { - return path.join(publicPath, file); -}; -const defaultPageUrl = (file, serverUrl) => { - return new URL('/apps/' + file, serverUrl).toString(); -}; +const defaultPagePath = file => { return path.join(publicPath, file); }; +const defaultPageUrl = (file, serverUrl) => { return new URL('/apps/' + file, serverUrl).toString(); }; +// All pages with custom page key for reference and file name const pages = Object.freeze({ invalidLink: { customPageKey: 'invalidLink', defaultFile: 'invalid_link.html' }, linkSendFail: { customPageKey: 'linkSendFail', defaultFile: 'link_send_fail.html' }, choosePassword: { customPageKey: 'choosePassword', defaultFile: 'choose_password.html' }, linkSendSuccess: { customPageKey: 'linkSendSuccess', defaultFile: 'link_send_success.html' }, - verifyEmailSuccess: { - customPageKey: 'verifyEmailSuccess', - defaultFile: 'verify_email_success.html', - }, - passwordResetSuccess: { - customPageKey: 'passwordResetSuccess', - defaultFile: 'password_reset_success.html', - }, - invalidVerificationLink: { - customPageKey: 'invalidVerificationLink', - defaultFile: 'invalid_verification_link.html', - }, + verifyEmailSuccess: { customPageKey: 'verifyEmailSuccess', defaultFile: 'verify_email_success.html', }, + passwordResetSuccess: { customPageKey: 'passwordResetSuccess', defaultFile: 'password_reset_success.html', }, + invalidVerificationLink: { customPageKey: 'invalidVerificationLink', defaultFile: 'invalid_verification_link.html', }, }); +// All page parameters for reference to be used as template placeholders or query params const pageParams = Object.freeze({ appName: 'appName', appId: 'appId', token: 'token', username: 'username', error: 'error', + locale: 'locale', + publicServerUrl: 'publicServerUrl', }); +// The header prefix to add page params as response headers +const pageParamHeaderPrefix = 'x-parse-page-param-'; export class PublicAPIRouter extends PromiseRouter { verifyEmail(req) { @@ -212,25 +205,36 @@ export class PublicAPIRouter extends PromiseRouter { * redirect to a custom page. * @param {Object} req The express request. * @param {Object} page The page to go to. - * @param {Object} params The query parameters to attach to the URL in case of + * @param {Object} [params={}] The query parameters to attach to the URL in case of * HTTP redirect responses for POST requests, or the placeholders to fill into * the response content in case of HTTP content responses for GET requests. - * @param {Boolean} responseType Is true if a redirect response should be forced, + * @param {Boolean} [responseType] Is true if a redirect response should be forced, * false if a content response should be forced, undefined if the response type * should depend on the request type by default: * - GET request -> content response * - POST request -> redirect response (PRG pattern) * @returns {Promise} The express response. */ - goToPage(req, page, params, responseType) { + goToPage(req, page, params = {}, responseType) { const config = req.config; - const locale = req.query.locale; + const locale = (req.query || {}).locale || (req.params || {}).locale; const redirect = responseType !== undefined ? responseType : req.method == 'POST'; - // Ensure required config - if ([config.publicServerURL].includes(undefined)) { + // Ensure default parameters required for every page + const requiredParams = { + [pageParams.appId]: config.appId, + [pageParams.appName]: config.appName, + [pageParams.publicServerUrl]: config.publicServerURL, + }; + if (Object.values(requiredParams).includes(undefined)) { return this.notFound(); } + params = Object.assign(params, requiredParams); + + // Add locale to params to ensure it is passed on with every request; + // that means, once a locale is set, it is passed on to any follow-up page, + // e.g. request_password_reset -> choose_password -> passwort_reset_success + params[pageParams.locale] = locale; // Compose paths and URLs const customPage = config.customPages[page.customPageKey]; @@ -251,39 +255,23 @@ export class PublicAPIRouter extends PromiseRouter { new URL(`/apps/${subdir}/${defaultFile}`, config.publicServerURL).toString(), params ) - : this.pageResponse(req, path, params) + : this.pageResponse(path, params) ); } else { return redirect ? this.redirectResponse(defaultUrl, params) - : this.pageResponse(req, defaultPath, params); + : this.pageResponse(defaultPath, params); } } /** * Creates a response with file content. - * @param {Object} req The express request. * @param {String} path The path of the file to return. * @param {Object} placeholders The placeholders to fill in the * content. - * @returns {Object} The express file response. + * @returns {Object} The Promise Router response. */ - async pageResponse(req, path, placeholders) { - // Aggreate placeholders - placeholders = Object.assign( - { - // Default placeholders available for every page - parseServerUrl: req.config.publicServerURL, - appName: req.config.appName, - }, - placeholders - ); - - // If any of the placeholder values fails to resolve - if (Object.values(placeholders).includes(undefined)) { - return this.notFound(); - } - + async pageResponse(path, placeholders) { // Get file content let data; try { @@ -298,7 +286,9 @@ export class PublicAPIRouter extends PromiseRouter { // Add placeholers in header to allow parsing for programmatic use // of response, instead of having to parse the HTML content. const headers = Object.entries(placeholders).reduce((m, p) => { - m[`x-parse-page-param-${p[0].toLowerCase()}`] = p[1]; + if (p[1] !== undefined) { + m[`${pageParamHeaderPrefix}${p[0].toLowerCase()}`] = p[1]; + } return m; }, {}); @@ -309,13 +299,34 @@ export class PublicAPIRouter extends PromiseRouter { * Creates a response with http 303 rediret. * @param {Object} req The express request. * @param {String} path The path of the file to return. - * @returns {Object} The express file response. + * @returns {Object} The Promise Router response. */ async redirectResponse(url, query) { - const location = query ? `${url}?${qs.stringify(query)}` : url; + // Remove any query parameters with undefined value + query = Object.entries(query).reduce((m, p) => { + if (p[1] !== undefined) { + m[p[0]] = p[1]; + } + return m; + }, {}); + + // Maybe use this in the future to add params as query instead of headers? + // const location = new URL(url); + // Object.entries(query).forEach(p => location.searchParams.set(p[0], p[1])); + + // Add placeholers in header to allow parsing for programmatic use + // of response, instead of having to parse the HTML content. + const headers = Object.entries(query).reduce((m, p) => { + if (p[1] !== undefined) { + m[`${pageParamHeaderPrefix}${p[0].toLowerCase()}`] = p[1]; + } + return m; + }, {}); + return { status: 303, - location: location, + location: url, + headers: headers, }; } @@ -407,4 +418,8 @@ export class PublicAPIRouter extends PromiseRouter { } export default PublicAPIRouter; -module.exports = { pages, PublicAPIRouter }; +module.exports = { + PublicAPIRouter, + pageParams, + pages, +}; From 212ad36441bbb48bed6e17ef389415ed78d29dba Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Mon, 18 Jan 2021 04:10:19 +0100 Subject: [PATCH 11/48] fix lint --- spec/PublicAPI.spec.js | 2 +- src/Routers/PublicAPIRouter.js | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/spec/PublicAPI.spec.js b/spec/PublicAPI.spec.js index 6c7759bae4..73f42d5efb 100644 --- a/spec/PublicAPI.spec.js +++ b/spec/PublicAPI.spec.js @@ -3,7 +3,7 @@ const request = require('../lib/request'); const fs = require('fs').promises; const Utils = require('../lib/Utils'); -const { PublicAPIRouter, pages, pageParams } = require('../lib/Routers/PublicAPIRouter'); +const { PublicAPIRouter, pages } = require('../lib/Routers/PublicAPIRouter'); describe('public API', () => { describe('basic request', () => { diff --git a/src/Routers/PublicAPIRouter.js b/src/Routers/PublicAPIRouter.js index f9ac7d4cad..41e809cdf5 100644 --- a/src/Routers/PublicAPIRouter.js +++ b/src/Routers/PublicAPIRouter.js @@ -3,7 +3,6 @@ import Config from '../Config'; import express from 'express'; import path from 'path'; import { promises as fs } from 'fs'; -import qs from 'querystring'; import { Parse } from 'parse/node'; import Utils from '../Utils'; import mustache from 'mustache'; From 2e8b228194c4ead2b2d1a042d7d71b96e10e478e Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Mon, 18 Jan 2021 05:12:45 +0100 Subject: [PATCH 12/48] fixed syntax errors and typos in html templates --- public/choose_password.html | 3 +++ public/de-AT/choose_password.html | 2 ++ public/de-AT/password_reset_success.html | 20 ++++++++++++++++++++ public/de/choose_password.html | 4 +++- public/de/password_reset_success.html | 20 ++++++++++++++++++++ public/password_reset_success.html | 4 ++-- public/verify_email_success.html | 2 +- 7 files changed, 51 insertions(+), 4 deletions(-) create mode 100644 public/de-AT/password_reset_success.html create mode 100644 public/de/password_reset_success.html diff --git a/public/choose_password.html b/public/choose_password.html index d9a34699d6..49cb65b1aa 100644 --- a/public/choose_password.html +++ b/public/choose_password.html @@ -11,6 +11,7 @@ add an 'error' query parameter. --> + Password Reset @@ -60,3 +61,5 @@

Reset Your Password

} + + \ No newline at end of file diff --git a/public/de-AT/choose_password.html b/public/de-AT/choose_password.html index d9a34699d6..f096693cf3 100644 --- a/public/de-AT/choose_password.html +++ b/public/de-AT/choose_password.html @@ -60,3 +60,5 @@

Reset Your Password

} + + \ No newline at end of file diff --git a/public/de-AT/password_reset_success.html b/public/de-AT/password_reset_success.html new file mode 100644 index 0000000000..7a90901f87 --- /dev/null +++ b/public/de-AT/password_reset_success.html @@ -0,0 +1,20 @@ + + + + + + Password Reset + + + +

{{appName}}

+

Success!

+

Your password has been updated.

+ + + diff --git a/public/de/choose_password.html b/public/de/choose_password.html index d9a34699d6..53c4eed65c 100644 --- a/public/de/choose_password.html +++ b/public/de/choose_password.html @@ -21,7 +21,7 @@

Reset Your Password

You can set a new Password for your account: {{username}}


-

{{error}}

+

{{error}}

@@ -60,3 +60,5 @@

Reset Your Password

} + + \ No newline at end of file diff --git a/public/de/password_reset_success.html b/public/de/password_reset_success.html new file mode 100644 index 0000000000..7a90901f87 --- /dev/null +++ b/public/de/password_reset_success.html @@ -0,0 +1,20 @@ + + + + + + Password Reset + + + +

{{appName}}

+

Success!

+

Your password has been updated.

+ + + diff --git a/public/password_reset_success.html b/public/password_reset_success.html index 618b0e29dd..7a90901f87 100644 --- a/public/password_reset_success.html +++ b/public/password_reset_success.html @@ -3,7 +3,7 @@ This page is displayed whenever someone has successfully reset their password. Pro and Enterprise accounts may edit this page and tell Parse to use that custom version in their Parse app. See the App Settigns page for more information. - This page will be called with the query param 'username' + This page will be called with the query param 'username'. --> @@ -14,7 +14,7 @@

{{appName}}

Success!

-

Your password has been updated.

+

Your password has been updated.

diff --git a/public/verify_email_success.html b/public/verify_email_success.html index 51896be56f..3d643e4f42 100644 --- a/public/verify_email_success.html +++ b/public/verify_email_success.html @@ -1,10 +1,10 @@ - + Email Verification From 34d5343c26346a8cbd338ee8504ef6c242f1cfde Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Mon, 18 Jan 2021 05:30:22 +0100 Subject: [PATCH 13/48] removed obsolete URI encoding --- src/Routers/PublicAPIRouter.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Routers/PublicAPIRouter.js b/src/Routers/PublicAPIRouter.js index 41e809cdf5..ece70c54ed 100644 --- a/src/Routers/PublicAPIRouter.js +++ b/src/Routers/PublicAPIRouter.js @@ -181,10 +181,9 @@ export class PublicAPIRouter extends PromiseRouter { } } - const encodedUsername = encodeURIComponent(username); const query = result.success ? { - [pageParams.username]: encodedUsername, + [pageParams.username]: username, } : { [pageParams.username]: username, From 9748fb323f8a8303bf727a1564ed3d88b52501ff Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Mon, 18 Jan 2021 05:31:47 +0100 Subject: [PATCH 14/48] added locale inferring from request body and header --- src/Routers/PublicAPIRouter.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Routers/PublicAPIRouter.js b/src/Routers/PublicAPIRouter.js index ece70c54ed..995e7a271e 100644 --- a/src/Routers/PublicAPIRouter.js +++ b/src/Routers/PublicAPIRouter.js @@ -215,7 +215,11 @@ export class PublicAPIRouter extends PromiseRouter { */ goToPage(req, page, params = {}, responseType) { const config = req.config; - const locale = (req.query || {}).locale || (req.params || {}).locale; + const locale = + (req.query || {})[pageParams.locale] + || (req.body || {})[pageParams.locale] + || (req.params || {})[pageParams.locale] + || (req.headers || {})[pageParamHeaderPrefix + pageParams.locale]; const redirect = responseType !== undefined ? responseType : req.method == 'POST'; // Ensure default parameters required for every page From 76fe0cae827ac979c9c4ad804f333bee665b70a1 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Mon, 18 Jan 2021 05:32:17 +0100 Subject: [PATCH 15/48] added end-to-end localizaton test --- spec/PublicAPI.spec.js | 61 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 58 insertions(+), 3 deletions(-) diff --git a/spec/PublicAPI.spec.js b/spec/PublicAPI.spec.js index 73f42d5efb..83e622fdfb 100644 --- a/spec/PublicAPI.spec.js +++ b/spec/PublicAPI.spec.js @@ -3,7 +3,7 @@ const request = require('../lib/request'); const fs = require('fs').promises; const Utils = require('../lib/Utils'); -const { PublicAPIRouter, pages } = require('../lib/Routers/PublicAPIRouter'); +const { PublicAPIRouter, pages, pageParams } = require('../lib/Routers/PublicAPIRouter'); describe('public API', () => { describe('basic request', () => { @@ -328,7 +328,7 @@ describe('public API', () => { } }); - it('responds to POST request with redirect response (e2e test)', async () => { + it('responds to POST request with redirect response', async () => { await reconfigureServer(config); const response = await request({ url: @@ -342,7 +342,7 @@ describe('public API', () => { ); }); - it('responds to GET request with content response (e2e test)', async () => { + it('responds to GET request with content response', async () => { await reconfigureServer(config); const response = await request({ url: @@ -353,6 +353,61 @@ describe('public API', () => { expect(response.status).toEqual(200); expect(response.text).toContain(''); }); + + it('localizes end-to-end for password reset success', async () => { + await reconfigureServer(config); + const sendPasswordResetEmail = spyOn(config.emailAdapter, 'sendPasswordResetEmail').and.callThrough(); + const user = new Parse.User(); + user.setUsername('exampleUsername'); + user.setPassword('examplePassword'); + user.set('email', 'mail@example.com'); + await user.signUp(); + await Parse.User.requestPasswordReset(user.getEmail()); + + const link = sendPasswordResetEmail.calls.all()[0].args[0].link; + const linkWithLocale = new URL(link); + linkWithLocale.searchParams.append(pageParams.locale, 'de-AT'); + + const linkResponse = await request({ + url: linkWithLocale.toString(), + followRedirects: false, + }); + expect(linkResponse.status).toBe(200); + + const appId = linkResponse.headers['x-parse-page-param-appid']; + const token = linkResponse.headers['x-parse-page-param-token']; + const locale = linkResponse.headers['x-parse-page-param-locale']; + const username = linkResponse.headers['x-parse-page-param-username']; + const publicServerUrl = linkResponse.headers['x-parse-page-param-publicserverurl']; + const choosePasswordPagePath = pageResponse.calls.all()[0].args[0]; + expect(appId).toBeDefined(); + expect(token).toBeDefined(); + expect(locale).toBeDefined(); + expect(username).toBeDefined(); + expect(publicServerUrl).toBeDefined(); + expect(choosePasswordPagePath).toMatch( + new RegExp(`\/de-AT\/${pages.choosePassword.defaultFile}`) + ); + pageResponse.calls.reset(); + + const formUrl = `${publicServerUrl}/apps/${appId}/request_password_reset`; + const formResponse = await request({ + url: formUrl, + method: 'POST', + body: { + token, + locale, + username, + new_password: 'newPassword', + }, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + followRedirects: false, + }); + expect(formResponse.status).toEqual(200); + expect(pageResponse.calls.all()[0].args[0]).toContain(`/${locale}/password_reset_success.html`); + }); }); }); }); From 597b25cf8d9e9226b793ad0462c86b74e1ccb6b4 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Mon, 18 Jan 2021 15:20:45 +0100 Subject: [PATCH 16/48] added server option validation; refactored pages server option --- resources/buildConfigDefinitions.js | 5 ++-- spec/PublicAPI.spec.js | 43 +++++++++++++++++++++++++++-- src/Config.js | 18 ++++++++++++ src/Options/Definitions.js | 20 ++++++++++---- src/Options/docs.js | 7 ++++- src/Options/index.js | 12 ++++++-- src/Routers/PublicAPIRouter.js | 2 +- 7 files changed, 91 insertions(+), 16 deletions(-) diff --git a/resources/buildConfigDefinitions.js b/resources/buildConfigDefinitions.js index aee5403613..6807a0dbd7 100644 --- a/resources/buildConfigDefinitions.js +++ b/resources/buildConfigDefinitions.js @@ -42,13 +42,14 @@ function getCommentValue(comment) { function getENVPrefix(iface) { const options = { 'ParseServerOptions' : 'PARSE_SERVER_', + 'PagesOptions' : 'PARSE_SERVER_PAGES_', 'CustomPagesOptions' : 'PARSE_SERVER_CUSTOM_PAGES_', 'LiveQueryServerOptions' : 'PARSE_LIVE_QUERY_SERVER_', 'LiveQueryOptions' : 'PARSE_SERVER_LIVEQUERY_', 'IdempotencyOptions' : 'PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_', 'AccountLockoutOptions' : 'PARSE_SERVER_ACCOUNT_LOCKOUT_', 'PasswordPolicyOptions' : 'PARSE_SERVER_PASSWORD_POLICY_', - 'FileUploadOptions' : 'PARSE_SERVER_FILE_UPLOAD_' + 'FileUploadOptions' : 'PARSE_SERVER_FILE_UPLOAD_', } if (options[iface.id.name]) { return options[iface.id.name] @@ -164,7 +165,7 @@ function parseDefaultValue(elt, value, t) { if (type == 'NumberOrBoolean') { literalValue = t.numericLiteral(parsers.numberOrBoolParser('')(value)); } - const literalTypes = ['IdempotencyOptions','FileUploadOptions','CustomPagesOptions']; + const literalTypes = ['IdempotencyOptions','FileUploadOptions','CustomPagesOptions', 'PagesOptions']; if (literalTypes.includes(type)) { const object = parsers.objectParser(value); const props = Object.keys(object).map((key) => { diff --git a/spec/PublicAPI.spec.js b/spec/PublicAPI.spec.js index 83e622fdfb..af7f166cce 100644 --- a/spec/PublicAPI.spec.js +++ b/spec/PublicAPI.spec.js @@ -3,6 +3,8 @@ const request = require('../lib/request'); const fs = require('fs').promises; const Utils = require('../lib/Utils'); +const Config = require('../lib/Config'); +const Definitions = require('../lib/Options/Definitions'); const { PublicAPIRouter, pages, pageParams } = require('../lib/Routers/PublicAPIRouter'); describe('public API', () => { @@ -191,9 +193,19 @@ describe('public API', () => { sendMail: () => {}, }, publicServerURL: 'http://localhost:8378/1', - enablePageLocalization: true, customPages: {}, + pages: { + enableLocalization: true, + }, }; + async function reconfigureServerWithPageOptions(options) { + await reconfigureServer({ + appId: Parse.applicationId, + masterKey: Parse.masterKey, + serverURL: Parse.serverURL, + pages: options, + }); + } beforeEach(async () => { router = new PublicAPIRouter(); @@ -206,8 +218,10 @@ describe('public API', () => { appId: 'test', appName: 'ExampleAppName', publicServerURL: 'http://localhost:8378/1', - enablePageLocalization: true, customPages: {}, + pages: { + enableLocalization: true, + }, }, query: { locale: 'de-AT', @@ -215,6 +229,29 @@ describe('public API', () => { }; }); + describe('server options', () => { + it('uses default configuration when none is set', async () => { + await reconfigureServerWithPageOptions({}); + expect(Config.get(Parse.applicationId).pages.enableLocalization).toBe( + Definitions.PagesOptions.enableLocalization.default + ); + }); + + it('throws on invalid configuration', async () => { + const options = [ + [], + 'a', + 0, + { enableLocalization: 'a' }, + { enableLocalization: 0 }, + { enableLocalization: {} }, + ]; + for (const option of options) { + await expectAsync(reconfigureServerWithPageOptions(option)).toBeRejected(); + } + }); + }); + describe('placeholders', () => { it('replaces placeholder in response content', async () => { await expectAsync(router.goToPage(req, pages.invalidLink)).toBeResolved(); @@ -246,7 +283,7 @@ describe('public API', () => { describe('localization', () => { it('returns default file if localization is disabled', async () => { - delete req.config.enablePageLocalization; + delete req.config.pages.enableLocalization; await expectAsync(router.goToPage(req, pages.invalidLink)).toBeResolved(); expect(pageResponse.calls.all()[0].args[0]).toBeDefined(); diff --git a/src/Config.js b/src/Config.js index cd2717a737..95f616ee24 100644 --- a/src/Config.js +++ b/src/Config.js @@ -9,7 +9,9 @@ import net from 'net'; import { IdempotencyOptions, FileUploadOptions, + PagesOptions, } from './Options/Definitions'; +import { isBoolean } from 'lodash'; function removeTrailingSlash(str) { if (!str) { @@ -75,6 +77,7 @@ export class Config { idempotencyOptions, emailVerifyTokenReuseIfValid, fileUpload, + pages, }) { if (masterKey === readOnlyMasterKey) { throw new Error('masterKey and readOnlyMasterKey should be different'); @@ -109,6 +112,21 @@ export class Config { this.validateMaxLimit(maxLimit); this.validateAllowHeaders(allowHeaders); this.validateIdempotencyOptions(idempotencyOptions); + this.validatePagesOptions(pages); + } + + static validatePagesOptions(pages) { + if (pages === undefined) { + return; + } + if (Object.prototype.toString.call(pages) !== '[object Object]') { + throw 'Parse Server option pages must be an object.'; + } + if (pages.enableLocalization === undefined) { + pages.enableLocalization = PagesOptions.enableLocalization.default; + } else if (!isBoolean(pages.enableLocalization)) { + throw 'Parse Server option pages.enableLocalization must be a boolean.'; + } } static validateIdempotencyOptions(idempotencyOptions) { diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index 2359123568..8c640b40ce 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -146,12 +146,6 @@ module.exports.ParseServerOptions = { "action": parsers.booleanParser, "default": false }, - "enablePageLocalization": { - "env": "PARSE_SERVER_ENABLE_PAGE_LOCALIZATION", - "help": "Is true if pages should be localized; customPages must not be set.", - "action": parsers.booleanParser, - "default": false - }, "enableSingleSchemaCache": { "env": "PARSE_SERVER_ENABLE_SINGLE_SCHEMA_CACHE", "help": "Use a single schema cache shared across requests. Reduces number of queries made to _SCHEMA, defaults to false, i.e. unique schema cache per request.", @@ -289,6 +283,12 @@ module.exports.ParseServerOptions = { "action": parsers.numberParser("objectIdSize"), "default": 10 }, + "pages": { + "env": "PARSE_SERVER_PAGES", + "help": "The options for pages such as password reset and email verification.", + "action": parsers.objectParser, + "default": {} + }, "passwordPolicy": { "env": "PARSE_SERVER_PASSWORD_POLICY", "help": "Password policy for enforcing password related rules", @@ -412,6 +412,14 @@ module.exports.ParseServerOptions = { "help": "Key sent with outgoing webhook calls" } }; +module.exports.PagesOptions = { + "enableLocalization": { + "env": "PARSE_SERVER_PAGES_ENABLE_LOCALIZATION", + "help": "Is true if pages should be localized; this has no effect on custom page redirects.", + "action": parsers.booleanParser, + "default": false + } +}; module.exports.CustomPagesOptions = { "choosePassword": { "env": "PARSE_SERVER_CUSTOM_PAGES_CHOOSE_PASSWORD", diff --git a/src/Options/docs.js b/src/Options/docs.js index a44a2df1bf..4b54841545 100644 --- a/src/Options/docs.js +++ b/src/Options/docs.js @@ -27,7 +27,6 @@ * @property {Number} emailVerifyTokenValidityDuration Email verification token validity duration, in seconds * @property {Boolean} enableAnonymousUsers Enable (or disable) anonymous users, defaults to true * @property {Boolean} enableExpressErrorHandler Enables the default express error handler for all errors - * @property {Boolean} enablePageLocalization Is true if pages should be localized; customPages must not be set. * @property {Boolean} enableSingleSchemaCache Use a single schema cache shared across requests. Reduces number of queries made to _SCHEMA, defaults to false, i.e. unique schema cache per request. * @property {String} encryptionKey Key for encrypting your files * @property {Boolean} expireInactiveSessions Sets wether we should expire the inactive sessions, defaults to true @@ -55,6 +54,7 @@ * @property {String} mountPath Mount path for the server, defaults to /parse * @property {Boolean} mountPlayground Mounts the GraphQL Playground - never use this option in production * @property {Number} objectIdSize Sets the number of characters in generated object id's, default 10 + * @property {PagesOptions} pages The options for pages such as password reset and email verification. * @property {PasswordPolicyOptions} passwordPolicy Password policy for enforcing password related rules * @property {String} playgroundPath Mount path for the GraphQL Playground, defaults to /playground * @property {Number} port The port to run the ParseServer, defaults to 1337. @@ -80,6 +80,11 @@ * @property {String} webhookKey Key sent with outgoing webhook calls */ +/** + * @interface PagesOptions + * @property {Boolean} enableLocalization Is true if pages should be localized; this has no effect on custom page redirects. + */ + /** * @interface CustomPagesOptions * @property {String} choosePassword choose password page path diff --git a/src/Options/index.js b/src/Options/index.js index 3fe43d721b..030f824790 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -138,12 +138,12 @@ export interface ParseServerOptions { /* Public URL to your parse server with http:// or https://. :ENV: PARSE_PUBLIC_SERVER_URL */ publicServerURL: ?string; + /* The options for pages such as password reset and email verification. + :DEFAULT:{} */ + pages: ?PagesOptions; /* custom pages for password validation and reset :DEFAULT: {} */ customPages: ?CustomPagesOptions; - /* Is true if pages should be localized; customPages must not be set. - :DEFAULT: false */ - enablePageLocalization: ?boolean; /* parse-server's LiveQuery configuration object */ liveQuery: ?LiveQueryOptions; /* Session duration, in seconds, defaults to 1 year @@ -229,6 +229,12 @@ export interface ParseServerOptions { serverCloseComplete: ?() => void; } +export interface PagesOptions { + /* Is true if pages should be localized; this has no effect on custom page redirects. + :DEFAULT: false */ + enableLocalization: ?boolean; +} + export interface CustomPagesOptions { /* invalid link page path */ invalidLink: ?string; diff --git a/src/Routers/PublicAPIRouter.js b/src/Routers/PublicAPIRouter.js index 995e7a271e..792b52b911 100644 --- a/src/Routers/PublicAPIRouter.js +++ b/src/Routers/PublicAPIRouter.js @@ -250,7 +250,7 @@ export class PublicAPIRouter extends PromiseRouter { } // If localization is enabled - if (config.enablePageLocalization && locale) { + if (config.pages.enableLocalization && locale) { return Utils.getLocalizedPath(defaultPath, locale).then(({ path, subdir }) => redirect ? this.redirectResponse( From 219634278d679ca3dbdfc2f8850274b831bff1d4 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Mon, 18 Jan 2021 17:34:46 +0100 Subject: [PATCH 17/48] fixed invalid redirect URL for no locale matching file --- src/Routers/PublicAPIRouter.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Routers/PublicAPIRouter.js b/src/Routers/PublicAPIRouter.js index 792b52b911..45dfe016dd 100644 --- a/src/Routers/PublicAPIRouter.js +++ b/src/Routers/PublicAPIRouter.js @@ -253,10 +253,11 @@ export class PublicAPIRouter extends PromiseRouter { if (config.pages.enableLocalization && locale) { return Utils.getLocalizedPath(defaultPath, locale).then(({ path, subdir }) => redirect - ? this.redirectResponse( - new URL(`/apps/${subdir}/${defaultFile}`, config.publicServerURL).toString(), - params - ) + ? this.redirectResponse(new URL( + subdir + ? `/apps/${subdir}/${defaultFile}` + : `/apps/${defaultFile}`, + config.publicServerURL).toString(), params) : this.pageResponse(path, params) ); } else { From 1002374de42b8e5ba302155e2f623a4cdc977072 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Mon, 18 Jan 2021 17:35:20 +0100 Subject: [PATCH 18/48] added end-to-end localizaton tests --- public/de-AT/invalid_verification_link.html | 24 +++ public/de-AT/link_send_fail.html | 20 ++ public/de-AT/link_send_success.html | 20 ++ public/de-AT/verify_email_success.html | 19 ++ public/de/invalid_verification_link.html | 24 +++ public/de/link_send_fail.html | 20 ++ public/de/link_send_success.html | 20 ++ public/de/verify_email_success.html | 19 ++ public/invalid_verification_link.html | 7 +- spec/PublicAPI.spec.js | 194 ++++++++++++++++++-- 10 files changed, 351 insertions(+), 16 deletions(-) create mode 100644 public/de-AT/invalid_verification_link.html create mode 100644 public/de-AT/link_send_fail.html create mode 100644 public/de-AT/link_send_success.html create mode 100644 public/de-AT/verify_email_success.html create mode 100644 public/de/invalid_verification_link.html create mode 100644 public/de/link_send_fail.html create mode 100644 public/de/link_send_success.html create mode 100644 public/de/verify_email_success.html diff --git a/public/de-AT/invalid_verification_link.html b/public/de-AT/invalid_verification_link.html new file mode 100644 index 0000000000..0f45d6b6b1 --- /dev/null +++ b/public/de-AT/invalid_verification_link.html @@ -0,0 +1,24 @@ + + + + + + Email Verification + + + +

{{appName}}

+

Invalid verification link!

+ + + + +
+ + + diff --git a/public/de-AT/link_send_fail.html b/public/de-AT/link_send_fail.html new file mode 100644 index 0000000000..8e1ba0ca06 --- /dev/null +++ b/public/de-AT/link_send_fail.html @@ -0,0 +1,20 @@ + + + + + + Email Verification + + + +

{{appName}}

+

Invalid link!

+

No link sent. User not found or email already verified.

+ + + diff --git a/public/de-AT/link_send_success.html b/public/de-AT/link_send_success.html new file mode 100644 index 0000000000..fa2d7f2ee5 --- /dev/null +++ b/public/de-AT/link_send_success.html @@ -0,0 +1,20 @@ + + + + + + Email Verification + + + +

{{appName}}

+

Link sent!

+

A new link has been sent. Check your email.

+ + + diff --git a/public/de-AT/verify_email_success.html b/public/de-AT/verify_email_success.html new file mode 100644 index 0000000000..3d643e4f42 --- /dev/null +++ b/public/de-AT/verify_email_success.html @@ -0,0 +1,19 @@ + + + + + + Email Verification + + + +

{{appName}}

+

Email verified!

+

Successfully verified your email for account: {{username}}.

+ + + diff --git a/public/de/invalid_verification_link.html b/public/de/invalid_verification_link.html new file mode 100644 index 0000000000..0f45d6b6b1 --- /dev/null +++ b/public/de/invalid_verification_link.html @@ -0,0 +1,24 @@ + + + + + + Email Verification + + + +

{{appName}}

+

Invalid verification link!

+
+ + + +
+ + + diff --git a/public/de/link_send_fail.html b/public/de/link_send_fail.html new file mode 100644 index 0000000000..8e1ba0ca06 --- /dev/null +++ b/public/de/link_send_fail.html @@ -0,0 +1,20 @@ + + + + + + Email Verification + + + +

{{appName}}

+

Invalid link!

+

No link sent. User not found or email already verified.

+ + + diff --git a/public/de/link_send_success.html b/public/de/link_send_success.html new file mode 100644 index 0000000000..fa2d7f2ee5 --- /dev/null +++ b/public/de/link_send_success.html @@ -0,0 +1,20 @@ + + + + + + Email Verification + + + +

{{appName}}

+

Link sent!

+

A new link has been sent. Check your email.

+ + + diff --git a/public/de/verify_email_success.html b/public/de/verify_email_success.html new file mode 100644 index 0000000000..3d643e4f42 --- /dev/null +++ b/public/de/verify_email_success.html @@ -0,0 +1,19 @@ + + + + + + Email Verification + + + +

{{appName}}

+

Email verified!

+

Successfully verified your email for account: {{username}}.

+ + + diff --git a/public/invalid_verification_link.html b/public/invalid_verification_link.html index 6f2bd20ca6..0f45d6b6b1 100644 --- a/public/invalid_verification_link.html +++ b/public/invalid_verification_link.html @@ -14,9 +14,10 @@

{{appName}}

Invalid verification link!

-
- - + + + +
diff --git a/spec/PublicAPI.spec.js b/spec/PublicAPI.spec.js index af7f166cce..18f33ade13 100644 --- a/spec/PublicAPI.spec.js +++ b/spec/PublicAPI.spec.js @@ -5,6 +5,7 @@ const fs = require('fs').promises; const Utils = require('../lib/Utils'); const Config = require('../lib/Config'); const Definitions = require('../lib/Options/Definitions'); +const UserController = require('../lib/Controllers/UserController').UserController; const { PublicAPIRouter, pages, pageParams } = require('../lib/Routers/PublicAPIRouter'); describe('public API', () => { @@ -183,6 +184,7 @@ describe('public API', () => { let pageResponse; let redirectResponse; let readFile; + const exampleLocale = 'de-AT'; const config = { appId: 'test', appName: 'ExampleAppName', @@ -224,7 +226,7 @@ describe('public API', () => { }, }, query: { - locale: 'de-AT', + locale: exampleLocale, }, }; }); @@ -314,14 +316,14 @@ describe('public API', () => { await expectAsync(router.goToPage(req, pages.invalidLink)).toBeResolved(); expect(pageResponse.calls.all()[0].args[0]).toBeDefined(); expect(pageResponse.calls.all()[0].args[0]).toMatch( - new RegExp(`\/de-AT\/${pages.invalidLink.defaultFile}`) + new RegExp(`\/${req.query.locale}\/${pages.invalidLink.defaultFile}`) ); }); it('returns file for language match', async () => { // Pretend no locale matching file exists spyOn(Utils, 'fileExists').and.callFake(async path => { - return !path.includes(`/de-AT/${pages.invalidLink.defaultFile}`); + return !path.includes(`/${req.query.locale}/${pages.invalidLink.defaultFile}`); }); await expectAsync(router.goToPage(req, pages.invalidLink)).toBeResolved(); @@ -374,9 +376,7 @@ describe('public API', () => { method: 'POST', }); expect(response.status).toEqual(303); - expect(response.headers.location).toEqual( - 'http://localhost:8378/apps/de-AT/invalid_link.html' - ); + expect(response.headers.location).toEqual('http://localhost:8378/apps/de-AT/invalid_link.html'); }); it('responds to GET request with content response', async () => { @@ -390,8 +390,10 @@ describe('public API', () => { expect(response.status).toEqual(200); expect(response.text).toContain(''); }); + }); - it('localizes end-to-end for password reset success', async () => { + describe('end-to-end tests', () => { + it('localizes end-to-end for password reset: success', async () => { await reconfigureServer(config); const sendPasswordResetEmail = spyOn(config.emailAdapter, 'sendPasswordResetEmail').and.callThrough(); const user = new Parse.User(); @@ -403,7 +405,7 @@ describe('public API', () => { const link = sendPasswordResetEmail.calls.all()[0].args[0].link; const linkWithLocale = new URL(link); - linkWithLocale.searchParams.append(pageParams.locale, 'de-AT'); + linkWithLocale.searchParams.append(pageParams.locale, exampleLocale); const linkResponse = await request({ url: linkWithLocale.toString(), @@ -423,7 +425,7 @@ describe('public API', () => { expect(username).toBeDefined(); expect(publicServerUrl).toBeDefined(); expect(choosePasswordPagePath).toMatch( - new RegExp(`\/de-AT\/${pages.choosePassword.defaultFile}`) + new RegExp(`\/${exampleLocale}\/${pages.choosePassword.defaultFile}`) ); pageResponse.calls.reset(); @@ -437,13 +439,179 @@ describe('public API', () => { username, new_password: 'newPassword', }, - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, followRedirects: false, }); expect(formResponse.status).toEqual(200); - expect(pageResponse.calls.all()[0].args[0]).toContain(`/${locale}/password_reset_success.html`); + expect(pageResponse.calls.all()[0].args[0]).toContain(`/${locale}/${pages.passwordResetSuccess.defaultFile}`); + }); + + it('localizes end-to-end for password reset: invalid link', async () => { + await reconfigureServer(config); + const sendPasswordResetEmail = spyOn(config.emailAdapter, 'sendPasswordResetEmail').and.callThrough(); + const user = new Parse.User(); + user.setUsername('exampleUsername'); + user.setPassword('examplePassword'); + user.set('email', 'mail@example.com'); + await user.signUp(); + await Parse.User.requestPasswordReset(user.getEmail()); + + const link = sendPasswordResetEmail.calls.all()[0].args[0].link; + const linkWithLocale = new URL(link); + linkWithLocale.searchParams.append(pageParams.locale, exampleLocale); + linkWithLocale.searchParams.set(pageParams.token, 'invalidToken'); + + const linkResponse = await request({ + url: linkWithLocale.toString(), + followRedirects: false, + }); + expect(linkResponse.status).toBe(200); + + const pagePath = pageResponse.calls.all()[0].args[0]; + expect(pagePath).toMatch( + new RegExp(`\/${exampleLocale}\/${pages.invalidLink.defaultFile}`) + ); + }); + + it('localizes end-to-end for verify email: success', async () => { + await reconfigureServer(config); + const sendVerificationEmail = spyOn(config.emailAdapter, 'sendVerificationEmail').and.callThrough(); + const user = new Parse.User(); + user.setUsername('exampleUsername'); + user.setPassword('examplePassword'); + user.set('email', 'mail@example.com'); + await user.signUp(); + + const link = sendVerificationEmail.calls.all()[0].args[0].link; + const linkWithLocale = new URL(link); + linkWithLocale.searchParams.append(pageParams.locale, exampleLocale); + + const linkResponse = await request({ + url: linkWithLocale.toString(), + followRedirects: false, + }); + expect(linkResponse.status).toBe(200); + + const pagePath = pageResponse.calls.all()[0].args[0]; + expect(pagePath).toMatch( + new RegExp(`\/${exampleLocale}\/${pages.verifyEmailSuccess.defaultFile}`) + ); + }); + + it('localizes end-to-end for verify email: invalid verification link - link send success', async () => { + await reconfigureServer(config); + const sendVerificationEmail = spyOn(config.emailAdapter, 'sendVerificationEmail').and.callThrough(); + const user = new Parse.User(); + user.setUsername('exampleUsername'); + user.setPassword('examplePassword'); + user.set('email', 'mail@example.com'); + await user.signUp(); + + const link = sendVerificationEmail.calls.all()[0].args[0].link; + const linkWithLocale = new URL(link); + linkWithLocale.searchParams.append(pageParams.locale, exampleLocale); + linkWithLocale.searchParams.set(pageParams.token, 'invalidToken'); + + const linkResponse = await request({ + url: linkWithLocale.toString(), + followRedirects: false, + }); + expect(linkResponse.status).toBe(200); + + const appId = linkResponse.headers['x-parse-page-param-appid']; + const locale = linkResponse.headers['x-parse-page-param-locale']; + const username = linkResponse.headers['x-parse-page-param-username']; + const publicServerUrl = linkResponse.headers['x-parse-page-param-publicserverurl']; + const invalidVerificationPagePath = pageResponse.calls.all()[0].args[0]; + expect(appId).toBeDefined(); + expect(locale).toBe(exampleLocale); + expect(username).toBeDefined(); + expect(publicServerUrl).toBeDefined(); + expect(invalidVerificationPagePath).toMatch( + new RegExp(`\/${exampleLocale}\/${pages.invalidVerificationLink.defaultFile}`) + ); + + const formUrl = `${publicServerUrl}/apps/${appId}/resend_verification_email`; + const formResponse = await request({ + url: formUrl, + method: 'POST', + body: { + locale, + username, + }, + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + followRedirects: false, + }); + expect(formResponse.status).toEqual(303); + expect(formResponse.text).toContain(`/${locale}/${pages.linkSendSuccess.defaultFile}`); + }); + + it('localizes end-to-end for verify email: invalid verification link - link send fail', async () => { + await reconfigureServer(config); + const sendVerificationEmail = spyOn(config.emailAdapter, 'sendVerificationEmail').and.callThrough(); + const user = new Parse.User(); + user.setUsername('exampleUsername'); + user.setPassword('examplePassword'); + user.set('email', 'mail@example.com'); + await user.signUp(); + + const link = sendVerificationEmail.calls.all()[0].args[0].link; + const linkWithLocale = new URL(link); + linkWithLocale.searchParams.append(pageParams.locale, exampleLocale); + linkWithLocale.searchParams.set(pageParams.token, 'invalidToken'); + + const linkResponse = await request({ + url: linkWithLocale.toString(), + followRedirects: false, + }); + expect(linkResponse.status).toBe(200); + + const appId = linkResponse.headers['x-parse-page-param-appid']; + const locale = linkResponse.headers['x-parse-page-param-locale']; + const username = linkResponse.headers['x-parse-page-param-username']; + const publicServerUrl = linkResponse.headers['x-parse-page-param-publicserverurl']; + const invalidVerificationPagePath = pageResponse.calls.all()[0].args[0]; + expect(appId).toBeDefined(); + expect(locale).toBe(exampleLocale); + expect(username).toBeDefined(); + expect(publicServerUrl).toBeDefined(); + expect(invalidVerificationPagePath).toMatch( + new RegExp(`\/${exampleLocale}\/${pages.invalidVerificationLink.defaultFile}`) + ); + + spyOn(UserController.prototype, 'resendVerificationEmail').and.callFake(() => Promise.reject( + 'failed to resend verification email' + )); + + const formUrl = `${publicServerUrl}/apps/${appId}/resend_verification_email`; + const formResponse = await request({ + url: formUrl, + method: 'POST', + body: { + locale, + username, + }, + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + followRedirects: false, + }); + expect(formResponse.status).toEqual(303); + expect(formResponse.text).toContain(`/${locale}/${pages.linkSendFail.defaultFile}`); + }); + + it('localizes end-to-end for verify email: invalid link', async () => { + await reconfigureServer(config); + const formUrl = `${config.publicServerURL}/apps/${config.appId}/resend_verification_email`; + const formResponse = await request({ + url: formUrl, + method: 'POST', + body: { + locale: exampleLocale, + }, + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + followRedirects: false, + }); + expect(formResponse.status).toEqual(303); + expect(formResponse.text).toContain(`/${exampleLocale}/${pages.invalidLink.defaultFile}`); }); }); }); From 389fb0695d9ccf16e4b9fbcd453f35754b0d97ab Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Mon, 18 Jan 2021 20:14:59 +0100 Subject: [PATCH 19/48] adapted tests to new response content --- spec/ValidationAndPasswordsReset.spec.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/ValidationAndPasswordsReset.spec.js b/spec/ValidationAndPasswordsReset.spec.js index fc4041f37e..cd7e007f02 100644 --- a/spec/ValidationAndPasswordsReset.spec.js +++ b/spec/ValidationAndPasswordsReset.spec.js @@ -668,8 +668,8 @@ describe('Custom Pages, Email Verification, Password Reset', () => { followRedirects: false, }).then(response => { expect(response.status).toEqual(200); - expect(response.text).toContain('value="exampleUsername"'); - expect(response.text).toContain('action="/apps/test/resend_verification_email"'); + expect(response.text).toContain('exampleUsername'); + expect(response.text).toContain('/apps/test/resend_verification_email'); done(); }); }); @@ -713,8 +713,8 @@ describe('Custom Pages, Email Verification, Password Reset', () => { followRedirects: false, }).then(response => { expect(response.status).toEqual(200); - expect(response.text).toContain('value="exampleUsername"'); - expect(response.text).toContain('action="/apps/test/resend_verification_email"'); + expect(response.text).toContain('exampleUsername'); + expect(response.text).toContain('/apps/test/resend_verification_email'); user.fetch().then(() => { expect(user.get('emailVerified')).toEqual(false); done(); From 25f62e7676401a9ec905eb3ca9719908d7f3831a Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Tue, 19 Jan 2021 01:45:35 +0100 Subject: [PATCH 20/48] re-added PublicAPIRouter; added PagesRouter as experimental feature --- public_html/invalid_link.html | 45 ++ public_html/invalid_verification_link.html | 68 ++ public_html/link_send_fail.html | 45 ++ public_html/link_send_success.html | 45 ++ public_html/password_reset_success.html | 27 + public_html/verify_email_success.html | 27 + spec/EmailVerificationToken.spec.js | 44 +- spec/PagesRouter.spec.js | 600 ++++++++++++++++++ spec/PasswordPolicy.spec.js | 117 ++-- spec/PublicAPI.spec.js | 696 ++++----------------- spec/RegexVulnerabilities.spec.js | 11 +- spec/ValidationAndPasswordsReset.spec.js | 117 ++-- src/Config.js | 10 + src/Options/Definitions.js | 14 +- src/Options/docs.js | 4 +- src/Options/index.js | 10 +- src/Page.js | 28 + src/ParseServer.js | 9 +- src/Routers/PagesRouter.js | 448 +++++++++++++ src/Routers/PublicAPIRouter.js | 350 ++++------- 20 files changed, 1785 insertions(+), 930 deletions(-) create mode 100644 public_html/invalid_link.html create mode 100644 public_html/invalid_verification_link.html create mode 100644 public_html/link_send_fail.html create mode 100644 public_html/link_send_success.html create mode 100644 public_html/password_reset_success.html create mode 100644 public_html/verify_email_success.html create mode 100644 spec/PagesRouter.spec.js create mode 100644 src/Page.js create mode 100644 src/Routers/PagesRouter.js diff --git a/public_html/invalid_link.html b/public_html/invalid_link.html new file mode 100644 index 0000000000..b19044e52f --- /dev/null +++ b/public_html/invalid_link.html @@ -0,0 +1,45 @@ + + + + + Invalid Link + + + + +
+

Invalid Link

+
+ + diff --git a/public_html/invalid_verification_link.html b/public_html/invalid_verification_link.html new file mode 100644 index 0000000000..fe6914fc82 --- /dev/null +++ b/public_html/invalid_verification_link.html @@ -0,0 +1,68 @@ + + + + + Invalid Link + + + + + +
+

Invalid Verification Link

+
+ + +
+
+ + diff --git a/public_html/link_send_fail.html b/public_html/link_send_fail.html new file mode 100644 index 0000000000..7f817a2cc4 --- /dev/null +++ b/public_html/link_send_fail.html @@ -0,0 +1,45 @@ + + + + + Invalid Link + + + + +
+

No link sent. User not found or email already verified

+
+ + diff --git a/public_html/link_send_success.html b/public_html/link_send_success.html new file mode 100644 index 0000000000..55d9cad6f6 --- /dev/null +++ b/public_html/link_send_success.html @@ -0,0 +1,45 @@ + + + + + Invalid Link + + + + +
+

Link Sent! Check your email.

+
+ + diff --git a/public_html/password_reset_success.html b/public_html/password_reset_success.html new file mode 100644 index 0000000000..774cbb350c --- /dev/null +++ b/public_html/password_reset_success.html @@ -0,0 +1,27 @@ + + + + + Password Reset + + +

Successfully updated your password!

+ + diff --git a/public_html/verify_email_success.html b/public_html/verify_email_success.html new file mode 100644 index 0000000000..774ea38a0d --- /dev/null +++ b/public_html/verify_email_success.html @@ -0,0 +1,27 @@ + + + + + Email Verification + + +

Successfully verified your email!

+ + diff --git a/spec/EmailVerificationToken.spec.js b/spec/EmailVerificationToken.spec.js index e76f40479e..50b626de0d 100644 --- a/spec/EmailVerificationToken.spec.js +++ b/spec/EmailVerificationToken.spec.js @@ -36,9 +36,10 @@ describe('Email Verification Token Expiration: ', () => { url: sendEmailOptions.link, followRedirects: false, }).then(response => { - expect(response.status).toEqual(200); - expect(response.text).toContain('testEmailVerifyTokenValidity'); - expect(response.text).toContain('/apps/test/resend_verification_email'); + expect(response.status).toEqual(302); + expect(response.text).toEqual( + 'Found. Redirecting to http://localhost:8378/1/apps/invalid_verification_link.html?username=testEmailVerifyTokenValidity&appId=test' + ); done(); }); }, 1000); @@ -81,7 +82,7 @@ describe('Email Verification Token Expiration: ', () => { url: sendEmailOptions.link, followRedirects: false, }).then(response => { - expect(response.status).toEqual(200); + expect(response.status).toEqual(302); user .fetch() .then(() => { @@ -129,9 +130,10 @@ describe('Email Verification Token Expiration: ', () => { url: sendEmailOptions.link, followRedirects: false, }).then(response => { - expect(response.status).toEqual(200); - expect(response.text).toContain('Email verified'); - expect(response.text).toContain('testEmailVerifyTokenValidity'); + expect(response.status).toEqual(302); + expect(response.text).toEqual( + 'Found. Redirecting to http://localhost:8378/1/apps/verify_email_success.html?username=testEmailVerifyTokenValidity' + ); done(); }); }) @@ -169,7 +171,7 @@ describe('Email Verification Token Expiration: ', () => { url: sendEmailOptions.link, followRedirects: false, }).then(response => { - expect(response.status).toEqual(200); + expect(response.status).toEqual(302); user .fetch() .then(() => { @@ -216,7 +218,7 @@ describe('Email Verification Token Expiration: ', () => { url: sendEmailOptions.link, followRedirects: false, }).then(response => { - expect(response.status).toEqual(200); + expect(response.status).toEqual(302); Parse.User.logIn('testEmailVerifyTokenValidity', 'expiringToken') .then(user => { expect(typeof user).toBe('object'); @@ -308,7 +310,7 @@ describe('Email Verification Token Expiration: ', () => { url: sendEmailOptions.link, followRedirects: false, }).then(response => { - expect(response.status).toEqual(200); + expect(response.status).toEqual(302); const config = Config.get('test'); return config.database .find('_User', { @@ -367,7 +369,7 @@ describe('Email Verification Token Expiration: ', () => { url: sendEmailOptions.link, followRedirects: false, }).then(response => { - expect(response.status).toEqual(200); + expect(response.status).toEqual(302); return user.fetch(); }); }) @@ -382,9 +384,10 @@ describe('Email Verification Token Expiration: ', () => { url: sendEmailOptions.link, followRedirects: false, }).then(response => { - expect(response.status).toEqual(200); - expect(response.text).toContain('Email verified'); - expect(response.text).toContain('testEmailVerifyTokenValidity'); + expect(response.status).toEqual(302); + expect(response.text).toEqual( + 'Found. Redirecting to http://localhost:8378/1/apps/verify_email_success.html?username=testEmailVerifyTokenValidity' + ); done(); }); }) @@ -434,9 +437,10 @@ describe('Email Verification Token Expiration: ', () => { url: sendEmailOptions.link, followRedirects: false, }).then(response => { - expect(response.status).toEqual(200); - expect(response.text).toContain('testEmailVerifyTokenValidity'); - expect(response.text).toContain('/apps/test/resend_verification_email'); + expect(response.status).toEqual(302); + expect(response.text).toEqual( + 'Found. Redirecting to http://localhost:8378/1/apps/invalid_verification_link.html?username=testEmailVerifyTokenValidity&appId=test' + ); done(); }); }) @@ -734,7 +738,7 @@ describe('Email Verification Token Expiration: ', () => { url: sendEmailOptions.link, followRedirects: false, }).then(response => { - expect(response.status).toEqual(200); + expect(response.status).toEqual(302); }); }) .then(() => { @@ -972,7 +976,7 @@ describe('Email Verification Token Expiration: ', () => { url: sendEmailOptions.link, followRedirects: false, }).then(response => { - expect(response.status).toEqual(200); + expect(response.status).toEqual(302); Parse.User.logIn('testEmailVerifyTokenValidity', 'expiringToken') .then(user => { expect(typeof user).toBe('object'); @@ -991,7 +995,7 @@ describe('Email Verification Token Expiration: ', () => { url: sendEmailOptions.link, followRedirects: false, }).then(response => { - expect(response.status).toEqual(200); + expect(response.status).toEqual(302); done(); }); }) diff --git a/spec/PagesRouter.spec.js b/spec/PagesRouter.spec.js new file mode 100644 index 0000000000..d5362415b0 --- /dev/null +++ b/spec/PagesRouter.spec.js @@ -0,0 +1,600 @@ +'use strict'; + +const request = require('../lib/request'); +const fs = require('fs').promises; +const Utils = require('../lib/Utils'); +const Config = require('../lib/Config'); +const Definitions = require('../lib/Options/Definitions'); +const UserController = require('../lib/Controllers/UserController').UserController; +const { PagesRouter, pages, pageParams } = require('../lib/Routers/PagesRouter'); + +describe('Pages Router', () => { + describe('basic request', () => { + beforeEach(async () => { + await reconfigureServer({ + appName: 'exampleAppname', + publicServerURL: 'http://localhost:8378/1', + pages: { enableRouter: true }, + }); + }); + + it('should return missing username error on ajax request without username provided', async () => { + try { + await request({ + method: 'POST', + url: 'http://localhost:8378/1/apps/test/request_password_reset', + body: `new_password=user1&token=43634643&username=`, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'X-Requested-With': 'XMLHttpRequest', + }, + followRedirects: false, + }); + } catch (error) { + expect(error.status).not.toBe(302); + expect(error.text).toEqual('{"code":200,"error":"Missing username"}'); + } + }); + + it('should return missing token error on ajax request without token provided', async () => { + try { + await request({ + method: 'POST', + url: 'http://localhost:8378/1/apps/test/request_password_reset', + body: `new_password=user1&token=&username=Johnny`, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'X-Requested-With': 'XMLHttpRequest', + }, + followRedirects: false, + }); + } catch (error) { + expect(error.status).not.toBe(302); + expect(error.text).toEqual('{"code":-1,"error":"Missing token"}'); + } + }); + + it('should return missing password error on ajax request without password provided', async () => { + try { + await request({ + method: 'POST', + url: 'http://localhost:8378/1/apps/test/request_password_reset', + body: `new_password=&token=132414&username=Johnny`, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'X-Requested-With': 'XMLHttpRequest', + }, + followRedirects: false, + }); + } catch (error) { + expect(error.status).not.toBe(302); + expect(error.text).toEqual('{"code":201,"error":"Missing password"}'); + } + }); + + it('responds with file content on direct page request', async () => { + const urls = [ + 'http://localhost:8378/1/apps/invalid_link.html', + 'http://localhost:8378/1/apps/choose_password?appId=test', + 'http://localhost:8378/1/apps/verify_email_success.html', + 'http://localhost:8378/1/apps/password_reset_success.html', + ]; + for (const url of urls) { + const response = await request({ url }).catch(e => e); + expect(response.status).toBe(200); + } + }); + + it('responds with 404 if publicServerURL is not confgured', async () => { + await reconfigureServer({ + appName: 'unused', + pages: { enableRouter: true } + }); + const urls = [ + 'http://localhost:8378/1/apps/test/verify_email', + 'http://localhost:8378/1/apps/choose_password?appId=test', + 'http://localhost:8378/1/apps/test/request_password_reset', + ]; + for (const url of urls) { + const response = await request({ url }).catch(e => e); + expect(response.status).toBe(404); + } + }); + }); + + describe('public API supplied with invalid application id', () => { + beforeEach(async () => { + await reconfigureServer({ + appName: 'unused', + pages: { enableRouter: true } + }); + }); + + it('should get 403 on verify_email', async () => { + const httpResponse = await request({ + url: 'http://localhost:8378/1/apps/invalid/verify_email', + }).catch(e => e); + expect(httpResponse.status).toBe(403); + }); + + it('should get 403 choose_password', async () => { + const httpResponse = await request({ + url: 'http://localhost:8378/1/apps/choose_password?id=invalid', + }).catch(e => e); + expect(httpResponse.status).toBe(403); + }); + + it('should get 403 on get of request_password_reset', async () => { + const httpResponse = await request({ + url: 'http://localhost:8378/1/apps/invalid/request_password_reset', + }).catch(e => e); + expect(httpResponse.status).toBe(403); + }); + + it('should get 403 on post of request_password_reset', async () => { + const httpResponse = await request({ + url: 'http://localhost:8378/1/apps/invalid/request_password_reset', + method: 'POST', + }).catch(e => e); + expect(httpResponse.status).toBe(403); + }); + + it('should get 403 on resendVerificationEmail', async () => { + const httpResponse = await request({ + url: 'http://localhost:8378/1/apps/invalid/resend_verification_email', + }).catch(e => e); + expect(httpResponse.status).toBe(403); + }); + }); + + describe('pages', () => { + let router = new PagesRouter(); + let req; + let pageResponse; + let redirectResponse; + let readFile; + const exampleLocale = 'de-AT'; + const config = { + appId: 'test', + appName: 'ExampleAppName', + verifyUserEmails: true, + emailAdapter: { + sendVerificationEmail: () => Promise.resolve(), + sendPasswordResetEmail: () => Promise.resolve(), + sendMail: () => {}, + }, + publicServerURL: 'http://localhost:8378/1', + customPages: {}, + pages: { + enableRouter: true, + enableLocalization: true, + }, + }; + async function reconfigureServerWithPageOptions(options) { + await reconfigureServer({ + appId: Parse.applicationId, + masterKey: Parse.masterKey, + serverURL: Parse.serverURL, + pages: options, + }); + } + + beforeEach(async () => { + router = new PagesRouter(); + readFile = spyOn(fs, 'readFile').and.callThrough(); + pageResponse = spyOn(PagesRouter.prototype, 'pageResponse').and.callThrough() + redirectResponse = spyOn(PagesRouter.prototype, 'redirectResponse').and.callThrough(); + req = { + method: 'GET', + config: { + appId: 'test', + appName: 'ExampleAppName', + publicServerURL: 'http://localhost:8378/1', + customPages: {}, + pages: { + enableLocalization: true, + }, + }, + query: { + locale: exampleLocale, + }, + }; + }); + + describe('server options', () => { + it('uses default configuration when none is set', async () => { + await reconfigureServerWithPageOptions({}); + expect(Config.get(Parse.applicationId).pages.enableRouter).toBe( + Definitions.PagesOptions.enableRouter.default + ); + expect(Config.get(Parse.applicationId).pages.enableLocalization).toBe( + Definitions.PagesOptions.enableLocalization.default + ); + expect(Config.get(Parse.applicationId).pages.forceRedirect).toBe( + Definitions.PagesOptions.forceRedirect.default + ); + }); + + it('throws on invalid configuration', async () => { + const options = [ + [], + 'a', + 0, + { enableRouter: 'a' }, + { enableRouter: 0 }, + { enableRouter: {} }, + { enableLocalization: 'a' }, + { enableLocalization: 0 }, + { enableLocalization: {} }, + { forceRedirect: 'a' }, + { forceRedirect: 0 }, + { forceRedirect: {} }, + ]; + for (const option of options) { + await expectAsync(reconfigureServerWithPageOptions(option)).toBeRejected(); + } + }); + }); + + describe('placeholders', () => { + it('replaces placeholder in response content', async () => { + await expectAsync(router.goToPage(req, pages.invalidLink)).toBeResolved(); + + expect(readFile.calls.all()[0].returnValue).toBeDefined(); + const originalContent = await readFile.calls.all()[0].returnValue; + expect(originalContent).toContain('{{appName}}'); + + expect(pageResponse.calls.all()[0].returnValue).toBeDefined(); + const replacedContent = await pageResponse.calls.all()[0].returnValue; + expect(replacedContent.text).not.toContain('{{appName}}'); + expect(replacedContent.text).toContain(req.config.appName); + }); + + it('removes undefined placeholder in response content', async () => { + await expectAsync(router.goToPage(req, pages.choosePassword)).toBeResolved(); + + expect(readFile.calls.all()[0].returnValue).toBeDefined(); + const originalContent = await readFile.calls.all()[0].returnValue; + expect(originalContent).toContain('{{error}}'); + + // There is no error placeholder value set by default, so the + // {{error}} placeholder should just be removed from content + expect(pageResponse.calls.all()[0].returnValue).toBeDefined(); + const replacedContent = await pageResponse.calls.all()[0].returnValue; + expect(replacedContent.text).not.toContain('{{error}}'); + }); + }); + + describe('localization', () => { + it('returns default file if localization is disabled', async () => { + delete req.config.pages.enableLocalization; + + await expectAsync(router.goToPage(req, pages.invalidLink)).toBeResolved(); + expect(pageResponse.calls.all()[0].args[0]).toBeDefined(); + expect(pageResponse.calls.all()[0].args[0]).not.toMatch( + new RegExp(`\/de(-AT)?\/${pages.invalidLink.defaultFile}`) + ); + }); + + it('returns default file if no locale is specified', async () => { + delete req.query.locale; + + await expectAsync(router.goToPage(req, pages.invalidLink)).toBeResolved(); + expect(pageResponse.calls.all()[0].args[0]).toBeDefined(); + expect(pageResponse.calls.all()[0].args[0]).not.toMatch( + new RegExp(`\/de(-AT)?\/${pages.invalidLink.defaultFile}`) + ); + }); + + it('returns custom page regardless of localization enabled', async () => { + req.config.customPages = { invalidLink: 'http://invalid-link.example.com' }; + + await expectAsync(router.goToPage(req, pages.invalidLink)).toBeResolved(); + expect(pageResponse).not.toHaveBeenCalled(); + expect(redirectResponse.calls.all()[0].args[0]).toBe(req.config.customPages.invalidLink); + }); + + it('returns file for locale match', async () => { + await expectAsync(router.goToPage(req, pages.invalidLink)).toBeResolved(); + expect(pageResponse.calls.all()[0].args[0]).toBeDefined(); + expect(pageResponse.calls.all()[0].args[0]).toMatch( + new RegExp(`\/${req.query.locale}\/${pages.invalidLink.defaultFile}`) + ); + }); + + it('returns file for language match', async () => { + // Pretend no locale matching file exists + spyOn(Utils, 'fileExists').and.callFake(async path => { + return !path.includes(`/${req.query.locale}/${pages.invalidLink.defaultFile}`); + }); + + await expectAsync(router.goToPage(req, pages.invalidLink)).toBeResolved(); + expect(pageResponse.calls.all()[0].args[0]).toBeDefined(); + expect(pageResponse.calls.all()[0].args[0]).toMatch( + new RegExp(`\/de\/${pages.invalidLink.defaultFile}`) + ); + }); + + it('returns default file for neither locale nor language match', async () => { + req.query.locale = 'yo-LO'; + + await expectAsync(router.goToPage(req, pages.invalidLink)).toBeResolved(); + expect(pageResponse.calls.all()[0].args[0]).toBeDefined(); + expect(pageResponse.calls.all()[0].args[0]).not.toMatch( + new RegExp(`\/yo(-LO)?\/${pages.invalidLink.defaultFile}`) + ); + }); + + it('returns a file for GET request', async () => { + await expectAsync(router.goToPage(req, pages.invalidLink)).toBeResolved(); + expect(pageResponse).toHaveBeenCalled(); + expect(redirectResponse).not.toHaveBeenCalled(); + }); + + it('returns a redirect for POST request', async () => { + req.method = 'POST'; + await expectAsync(router.goToPage(req, pages.invalidLink)).toBeResolved(); + expect(pageResponse).not.toHaveBeenCalled(); + expect(redirectResponse).toHaveBeenCalled(); + }); + + it('returns a redirect for custom pages for GET and POST request', async () => { + req.config.customPages = { invalidLink: 'http://invalid-link.example.com' }; + + for (const method of ['GET', 'POST']) { + req.method = method; + await expectAsync(router.goToPage(req, pages.invalidLink)).toBeResolved(); + expect(pageResponse).not.toHaveBeenCalled(); + expect(redirectResponse).toHaveBeenCalled(); + } + }); + + it('responds to POST request with redirect response', async () => { + await reconfigureServer(config); + const response = await request({ + url: + 'http://localhost:8378/1/apps/test/request_password_reset?token=exampleToken&username=exampleUsername&locale=de-AT', + followRedirects: false, + method: 'POST', + }); + expect(response.status).toEqual(303); + expect(response.headers.location).toContain('http://localhost:8378/1/apps/de-AT/invalid_link.html'); + }); + + it('responds to GET request with content response', async () => { + await reconfigureServer(config); + const response = await request({ + url: + 'http://localhost:8378/1/apps/test/request_password_reset?token=exampleToken&username=exampleUsername&locale=de-AT', + followRedirects: false, + method: 'GET', + }); + expect(response.status).toEqual(200); + expect(response.text).toContain(''); + }); + }); + + describe('end-to-end tests', () => { + it('localizes end-to-end for password reset: success', async () => { + await reconfigureServer(config); + const sendPasswordResetEmail = spyOn(config.emailAdapter, 'sendPasswordResetEmail').and.callThrough(); + const user = new Parse.User(); + user.setUsername('exampleUsername'); + user.setPassword('examplePassword'); + user.set('email', 'mail@example.com'); + await user.signUp(); + await Parse.User.requestPasswordReset(user.getEmail()); + + const link = sendPasswordResetEmail.calls.all()[0].args[0].link; + const linkWithLocale = new URL(link); + linkWithLocale.searchParams.append(pageParams.locale, exampleLocale); + + const linkResponse = await request({ + url: linkWithLocale.toString(), + followRedirects: false, + }); + expect(linkResponse.status).toBe(200); + + const appId = linkResponse.headers['x-parse-page-param-appid']; + const token = linkResponse.headers['x-parse-page-param-token']; + const locale = linkResponse.headers['x-parse-page-param-locale']; + const username = linkResponse.headers['x-parse-page-param-username']; + const publicServerUrl = linkResponse.headers['x-parse-page-param-publicserverurl']; + const choosePasswordPagePath = pageResponse.calls.all()[0].args[0]; + expect(appId).toBeDefined(); + expect(token).toBeDefined(); + expect(locale).toBeDefined(); + expect(username).toBeDefined(); + expect(publicServerUrl).toBeDefined(); + expect(choosePasswordPagePath).toMatch( + new RegExp(`\/${exampleLocale}\/${pages.choosePassword.defaultFile}`) + ); + pageResponse.calls.reset(); + + const formUrl = `${publicServerUrl}/apps/${appId}/request_password_reset`; + const formResponse = await request({ + url: formUrl, + method: 'POST', + body: { + token, + locale, + username, + new_password: 'newPassword', + }, + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + followRedirects: false, + }); + expect(formResponse.status).toEqual(200); + expect(pageResponse.calls.all()[0].args[0]).toContain(`/${locale}/${pages.passwordResetSuccess.defaultFile}`); + }); + + it('localizes end-to-end for password reset: invalid link', async () => { + await reconfigureServer(config); + const sendPasswordResetEmail = spyOn(config.emailAdapter, 'sendPasswordResetEmail').and.callThrough(); + const user = new Parse.User(); + user.setUsername('exampleUsername'); + user.setPassword('examplePassword'); + user.set('email', 'mail@example.com'); + await user.signUp(); + await Parse.User.requestPasswordReset(user.getEmail()); + + const link = sendPasswordResetEmail.calls.all()[0].args[0].link; + const linkWithLocale = new URL(link); + linkWithLocale.searchParams.append(pageParams.locale, exampleLocale); + linkWithLocale.searchParams.set(pageParams.token, 'invalidToken'); + + const linkResponse = await request({ + url: linkWithLocale.toString(), + followRedirects: false, + }); + expect(linkResponse.status).toBe(200); + + const pagePath = pageResponse.calls.all()[0].args[0]; + expect(pagePath).toMatch( + new RegExp(`\/${exampleLocale}\/${pages.invalidLink.defaultFile}`) + ); + }); + + it('localizes end-to-end for verify email: success', async () => { + await reconfigureServer(config); + const sendVerificationEmail = spyOn(config.emailAdapter, 'sendVerificationEmail').and.callThrough(); + const user = new Parse.User(); + user.setUsername('exampleUsername'); + user.setPassword('examplePassword'); + user.set('email', 'mail@example.com'); + await user.signUp(); + + const link = sendVerificationEmail.calls.all()[0].args[0].link; + const linkWithLocale = new URL(link); + linkWithLocale.searchParams.append(pageParams.locale, exampleLocale); + + const linkResponse = await request({ + url: linkWithLocale.toString(), + followRedirects: false, + }); + expect(linkResponse.status).toBe(200); + + const pagePath = pageResponse.calls.all()[0].args[0]; + expect(pagePath).toMatch( + new RegExp(`\/${exampleLocale}\/${pages.verifyEmailSuccess.defaultFile}`) + ); + }); + + it('localizes end-to-end for verify email: invalid verification link - link send success', async () => { + await reconfigureServer(config); + const sendVerificationEmail = spyOn(config.emailAdapter, 'sendVerificationEmail').and.callThrough(); + const user = new Parse.User(); + user.setUsername('exampleUsername'); + user.setPassword('examplePassword'); + user.set('email', 'mail@example.com'); + await user.signUp(); + + const link = sendVerificationEmail.calls.all()[0].args[0].link; + const linkWithLocale = new URL(link); + linkWithLocale.searchParams.append(pageParams.locale, exampleLocale); + linkWithLocale.searchParams.set(pageParams.token, 'invalidToken'); + + const linkResponse = await request({ + url: linkWithLocale.toString(), + followRedirects: false, + }); + expect(linkResponse.status).toBe(200); + + const appId = linkResponse.headers['x-parse-page-param-appid']; + const locale = linkResponse.headers['x-parse-page-param-locale']; + const username = linkResponse.headers['x-parse-page-param-username']; + const publicServerUrl = linkResponse.headers['x-parse-page-param-publicserverurl']; + const invalidVerificationPagePath = pageResponse.calls.all()[0].args[0]; + expect(appId).toBeDefined(); + expect(locale).toBe(exampleLocale); + expect(username).toBeDefined(); + expect(publicServerUrl).toBeDefined(); + expect(invalidVerificationPagePath).toMatch( + new RegExp(`\/${exampleLocale}\/${pages.invalidVerificationLink.defaultFile}`) + ); + + const formUrl = `${publicServerUrl}/apps/${appId}/resend_verification_email`; + const formResponse = await request({ + url: formUrl, + method: 'POST', + body: { + locale, + username, + }, + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + followRedirects: false, + }); + expect(formResponse.status).toEqual(303); + expect(formResponse.text).toContain(`/${locale}/${pages.linkSendSuccess.defaultFile}`); + }); + + it('localizes end-to-end for verify email: invalid verification link - link send fail', async () => { + await reconfigureServer(config); + const sendVerificationEmail = spyOn(config.emailAdapter, 'sendVerificationEmail').and.callThrough(); + const user = new Parse.User(); + user.setUsername('exampleUsername'); + user.setPassword('examplePassword'); + user.set('email', 'mail@example.com'); + await user.signUp(); + + const link = sendVerificationEmail.calls.all()[0].args[0].link; + const linkWithLocale = new URL(link); + linkWithLocale.searchParams.append(pageParams.locale, exampleLocale); + linkWithLocale.searchParams.set(pageParams.token, 'invalidToken'); + + const linkResponse = await request({ + url: linkWithLocale.toString(), + followRedirects: false, + }); + expect(linkResponse.status).toBe(200); + + const appId = linkResponse.headers['x-parse-page-param-appid']; + const locale = linkResponse.headers['x-parse-page-param-locale']; + const username = linkResponse.headers['x-parse-page-param-username']; + const publicServerUrl = linkResponse.headers['x-parse-page-param-publicserverurl']; + const invalidVerificationPagePath = pageResponse.calls.all()[0].args[0]; + expect(appId).toBeDefined(); + expect(locale).toBe(exampleLocale); + expect(username).toBeDefined(); + expect(publicServerUrl).toBeDefined(); + expect(invalidVerificationPagePath).toMatch( + new RegExp(`\/${exampleLocale}\/${pages.invalidVerificationLink.defaultFile}`) + ); + + spyOn(UserController.prototype, 'resendVerificationEmail').and.callFake(() => Promise.reject( + 'failed to resend verification email' + )); + + const formUrl = `${publicServerUrl}/apps/${appId}/resend_verification_email`; + const formResponse = await request({ + url: formUrl, + method: 'POST', + body: { + locale, + username, + }, + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + followRedirects: false, + }); + expect(formResponse.status).toEqual(303); + expect(formResponse.text).toContain(`/${locale}/${pages.linkSendFail.defaultFile}`); + }); + + it('localizes end-to-end for verify email: invalid link', async () => { + await reconfigureServer(config); + const formUrl = `${config.publicServerURL}/apps/${config.appId}/resend_verification_email`; + const formResponse = await request({ + url: formUrl, + method: 'POST', + body: { + locale: exampleLocale, + }, + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + followRedirects: false, + }); + expect(formResponse.status).toEqual(303); + expect(formResponse.text).toContain(`/${exampleLocale}/${pages.invalidLink.defaultFile}`); + }); + }); + }); +}); diff --git a/spec/PasswordPolicy.spec.js b/spec/PasswordPolicy.spec.js index e96316ac48..6d00ddfa28 100644 --- a/spec/PasswordPolicy.spec.js +++ b/spec/PasswordPolicy.spec.js @@ -46,8 +46,10 @@ describe('Password Policy: ', () => { resolveWithFullResponse: true, }) .then(response => { - expect(response.status).toEqual(200); - expect(response.text).toContain('Invalid Link'); + expect(response.status).toEqual(302); + expect(response.text).toEqual( + 'Found. Redirecting to http://localhost:8378/1/apps/invalid_link.html' + ); done(); }) .catch(error => { @@ -104,8 +106,9 @@ describe('Password Policy: ', () => { followRedirects: false, }) .then(response => { - expect(response.status).toEqual(200); - expect(response.headers['x-parse-page-param-token']).toBeDefined(); + expect(response.status).toEqual(302); + const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=[a-zA-Z0-9]+\&id=test\&username=testResetTokenValidity/; + expect(response.text.match(re)).not.toBe(null); done(); }) .catch(error => { @@ -618,14 +621,15 @@ describe('Password Policy: ', () => { resolveWithFullResponse: true, }) .then(response => { - expect(response.status).toEqual(200); - - const token = response.headers['x-parse-page-param-token']; - if (!token) { + expect(response.status).toEqual(302); + const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=user1/; + const match = response.text.match(re); + if (!match) { fail('should have a token'); done(); return; } + const token = match[1]; request({ method: 'POST', @@ -639,8 +643,10 @@ describe('Password Policy: ', () => { resolveWithFullResponse: true, }) .then(response => { - expect(response.status).toEqual(200); - expect(response.text).toContain('Your password has been updated'); + expect(response.status).toEqual(302); + expect(response.text).toEqual( + 'Found. Redirecting to http://localhost:8378/1/apps/password_reset_success.html?username=user1' + ); Parse.User.logIn('user1', 'has2init') .then(function () { @@ -707,14 +713,15 @@ describe('Password Policy: ', () => { resolveWithFullResponse: true, }) .then(response => { - expect(response.status).toEqual(200); - - const token = response.headers['x-parse-page-param-token']; - if (!token) { + expect(response.status).toEqual(302); + const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=user1/; + const match = response.text.match(re); + if (!match) { fail('should have a token'); done(); return; } + const token = match[1]; request({ method: 'POST', @@ -728,8 +735,10 @@ describe('Password Policy: ', () => { resolveWithFullResponse: true, }) .then(response => { - expect(response.status).toEqual(200); - expect(response.text).toContain('Password should contain at least one digit'); + expect(response.status).toEqual(302); + expect(response.text).toEqual( + `Found. Redirecting to http://localhost:8378/1/apps/choose_password?username=user1&token=${token}&id=test&error=Password%20should%20contain%20at%20least%20one%20digit.&app=passwordPolicy` + ); Parse.User.logIn('user1', 'has 1 digit') .then(function () { @@ -890,14 +899,15 @@ describe('Password Policy: ', () => { resolveWithFullResponse: true, }) .then(response => { - expect(response.status).toEqual(200); - - const token = response.headers['x-parse-page-param-token']; - if (!token) { + expect(response.status).toEqual(302); + const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=user1/; + const match = response.text.match(re); + if (!match) { fail('should have a token'); done(); return; } + const token = match[1]; request({ method: 'POST', @@ -911,8 +921,10 @@ describe('Password Policy: ', () => { resolveWithFullResponse: true, }) .then(response => { - expect(response.status).toEqual(200); - expect(response.text).toContain('Password cannot contain your username'); + expect(response.status).toEqual(302); + expect(response.text).toEqual( + `Found. Redirecting to http://localhost:8378/1/apps/choose_password?username=user1&token=${token}&id=test&error=Password%20cannot%20contain%20your%20username.&app=passwordPolicy` + ); Parse.User.logIn('user1', 'r@nd0m') .then(function () { @@ -978,14 +990,14 @@ describe('Password Policy: ', () => { simple: false, resolveWithFullResponse: true, }); - expect(response.status).toEqual(200); - - const token = response.headers['x-parse-page-param-token']; - if (!token) { + expect(response.status).toEqual(302); + const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=user1/; + const match = response.text.match(re); + if (!match) { fail('should have a token'); - done(); return; } + const token = match[1]; try { await request({ @@ -1038,14 +1050,15 @@ describe('Password Policy: ', () => { resolveWithFullResponse: true, }) .then(response => { - expect(response.status).toEqual(200); - - const token = response.headers['x-parse-page-param-token']; - if (!token) { + expect(response.status).toEqual(302); + const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=user1/; + const match = response.text.match(re); + if (!match) { fail('should have a token'); done(); return; } + const token = match[1]; request({ method: 'POST', @@ -1059,8 +1072,10 @@ describe('Password Policy: ', () => { resolveWithFullResponse: true, }) .then(response => { - expect(response.status).toEqual(200); - expect(response.text).toContain('Your password has been updated'); + expect(response.status).toEqual(302); + expect(response.text).toEqual( + 'Found. Redirecting to http://localhost:8378/1/apps/password_reset_success.html?username=user1' + ); Parse.User.logIn('user1', 'uuser11') .then(function () { @@ -1301,14 +1316,15 @@ describe('Password Policy: ', () => { resolveWithFullResponse: true, }) .then(response => { - expect(response.status).toEqual(200); - - const token = response.headers['x-parse-page-param-token']; - if (!token) { + expect(response.status).toEqual(302); + const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=user1/; + const match = response.text.match(re); + if (!match) { fail('should have a token'); done(); return; } + const token = match[1]; request({ method: 'POST', @@ -1322,8 +1338,10 @@ describe('Password Policy: ', () => { resolveWithFullResponse: true, }) .then(response => { - expect(response.status).toEqual(200); - expect(response.text).toContain('Your password has been updated'); + expect(response.status).toEqual(302); + expect(response.text).toEqual( + 'Found. Redirecting to http://localhost:8378/1/apps/password_reset_success.html?username=user1' + ); Parse.User.logIn('user1', 'uuser11') .then(function () { @@ -1453,14 +1471,14 @@ describe('Password Policy: ', () => { followRedirects: false, }) .then(response => { - expect(response.status).toEqual(200); - - const token = response.headers['x-parse-page-param-token']; - if (!token) { + expect(response.status).toEqual(302); + const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=user1/; + const match = response.text.match(re); + if (!match) { fail('should have a token'); return Promise.reject('Invalid password link'); } - return Promise.resolve(token); + return Promise.resolve(match[1]); // token }) .then(token => { return request({ @@ -1480,14 +1498,9 @@ describe('Password Policy: ', () => { .then(data => { const response = data[0]; const token = data[1]; - expect(response.status).toEqual(200); - expect(response.text).toContain(token); - expect(response.text).toContain(user.getUsername()); - expect(response.text).toContain( - 'http://localhost:8378/1/apps/test/request_password_reset' - ); - expect(response.text).toContain( - 'New password should not be the same as last 1 passwords' + expect(response.status).toEqual(302); + expect(response.text).toEqual( + `Found. Redirecting to http://localhost:8378/1/apps/choose_password?username=user1&token=${token}&id=test&error=New%20password%20should%20not%20be%20the%20same%20as%20last%201%20passwords.&app=passwordPolicy` ); done(); return Promise.resolve(); diff --git a/spec/PublicAPI.spec.js b/spec/PublicAPI.spec.js index 18f33ade13..545662914f 100644 --- a/spec/PublicAPI.spec.js +++ b/spec/PublicAPI.spec.js @@ -1,618 +1,184 @@ -'use strict'; +const req = require('../lib/request'); -const request = require('../lib/request'); -const fs = require('fs').promises; -const Utils = require('../lib/Utils'); -const Config = require('../lib/Config'); -const Definitions = require('../lib/Options/Definitions'); -const UserController = require('../lib/Controllers/UserController').UserController; -const { PublicAPIRouter, pages, pageParams } = require('../lib/Routers/PublicAPIRouter'); +const request = function (url, callback) { + return req({ + url, + }).then( + response => callback(null, response), + err => callback(err, err) + ); +}; describe('public API', () => { - describe('basic request', () => { - it('should return missing username error on ajax request without username provided', async () => { - await reconfigureServer({ - publicServerURL: 'http://localhost:8378/1', - }); - - try { - await request({ - method: 'POST', - url: 'http://localhost:8378/1/apps/test/request_password_reset', - body: `new_password=user1&token=43634643&username=`, - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'X-Requested-With': 'XMLHttpRequest', - }, - followRedirects: false, - }); - } catch (error) { - expect(error.status).not.toBe(302); - expect(error.text).toEqual('{"code":200,"error":"Missing username"}'); - } + it('should return missing username error on ajax request without username provided', async () => { + await reconfigureServer({ + publicServerURL: 'http://localhost:8378/1', }); - it('should return missing token error on ajax request without token provided', async () => { - await reconfigureServer({ - publicServerURL: 'http://localhost:8378/1', + try { + await req({ + method: 'POST', + url: 'http://localhost:8378/1/apps/test/request_password_reset', + body: `new_password=user1&token=43634643&username=`, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'X-Requested-With': 'XMLHttpRequest', + }, + followRedirects: false, }); + } catch (error) { + expect(error.status).not.toBe(302); + expect(error.text).toEqual('{"code":200,"error":"Missing username"}'); + } + }); - try { - await request({ - method: 'POST', - url: 'http://localhost:8378/1/apps/test/request_password_reset', - body: `new_password=user1&token=&username=Johnny`, - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'X-Requested-With': 'XMLHttpRequest', - }, - followRedirects: false, - }); - } catch (error) { - expect(error.status).not.toBe(302); - expect(error.text).toEqual('{"code":-1,"error":"Missing token"}'); - } + it('should return missing token error on ajax request without token provided', async () => { + await reconfigureServer({ + publicServerURL: 'http://localhost:8378/1', }); - it('should return missing password error on ajax request without password provided', async () => { - await reconfigureServer({ - publicServerURL: 'http://localhost:8378/1', + try { + await req({ + method: 'POST', + url: 'http://localhost:8378/1/apps/test/request_password_reset', + body: `new_password=user1&token=&username=Johnny`, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'X-Requested-With': 'XMLHttpRequest', + }, + followRedirects: false, }); + } catch (error) { + expect(error.status).not.toBe(302); + expect(error.text).toEqual('{"code":-1,"error":"Missing token"}'); + } + }); - try { - await request({ - method: 'POST', - url: 'http://localhost:8378/1/apps/test/request_password_reset', - body: `new_password=&token=132414&username=Johnny`, - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'X-Requested-With': 'XMLHttpRequest', - }, - followRedirects: false, - }); - } catch (error) { - expect(error.status).not.toBe(302); - expect(error.text).toEqual('{"code":201,"error":"Missing password"}'); - } + it('should return missing password error on ajax request without password provided', async () => { + await reconfigureServer({ + publicServerURL: 'http://localhost:8378/1', }); - it('should get invalid_link.html', async () => { - const httpResponse = await request({ - url: 'http://localhost:8378/1/apps/invalid_link.html', + try { + await req({ + method: 'POST', + url: 'http://localhost:8378/1/apps/test/request_password_reset', + body: `new_password=&token=132414&username=Johnny`, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'X-Requested-With': 'XMLHttpRequest', + }, + followRedirects: false, }); - expect(httpResponse.status).toBe(200); - }); + } catch (error) { + expect(error.status).not.toBe(302); + expect(error.text).toEqual('{"code":201,"error":"Missing password"}'); + } + }); - it('should get choose_password', async () => { - await reconfigureServer({ - appName: 'unused', - publicServerURL: 'http://localhost:8378/1', - }); - const httpResponse = await request({ - url: 'http://localhost:8378/1/apps/choose_password?appId=test', - }); + it('should get invalid_link.html', done => { + request('http://localhost:8378/1/apps/invalid_link.html', (err, httpResponse) => { expect(httpResponse.status).toBe(200); + done(); }); + }); - it('should get verify_email_success.html', async () => { - const httpResponse = await request({ - url: 'http://localhost:8378/1/apps/verify_email_success.html', + it('should get choose_password', done => { + reconfigureServer({ + appName: 'unused', + publicServerURL: 'http://localhost:8378/1', + }).then(() => { + request('http://localhost:8378/1/apps/choose_password?id=test', (err, httpResponse) => { + expect(httpResponse.status).toBe(200); + done(); }); - expect(httpResponse.status).toBe(200); }); + }); - it('should get password_reset_success.html', async () => { - const httpResponse = await request({ - url: 'http://localhost:8378/1/apps/password_reset_success.html', - }); + it('should get verify_email_success.html', done => { + request('http://localhost:8378/1/apps/verify_email_success.html', (err, httpResponse) => { expect(httpResponse.status).toBe(200); + done(); }); }); - describe('public API without publicServerURL', function () { - beforeEach(async () => { - await reconfigureServer({ appName: 'unused' }); + it('should get password_reset_success.html', done => { + request('http://localhost:8378/1/apps/password_reset_success.html', (err, httpResponse) => { + expect(httpResponse.status).toBe(200); + done(); }); + }); +}); - it('should get 404 on verify_email', async () => { - const httpResponse = await request({ - url: 'http://localhost:8378/1/apps/test/verify_email', - }).catch(e => e); +describe('public API without publicServerURL', () => { + beforeEach(done => { + reconfigureServer({ appName: 'unused' }).then(done, fail); + }); + it('should get 404 on verify_email', done => { + request('http://localhost:8378/1/apps/test/verify_email', (err, httpResponse) => { expect(httpResponse.status).toBe(404); + done(); }); + }); - it('should get 404 choose_password', async () => { - const httpResponse = await request({ - url: 'http://localhost:8378/1/apps/choose_password?appId=test', - }).catch(e => e); + it('should get 404 choose_password', done => { + request('http://localhost:8378/1/apps/choose_password?id=test', (err, httpResponse) => { expect(httpResponse.status).toBe(404); + done(); }); + }); - it('should get 404 on request_password_reset', async () => { - const httpResponse = await request({ - url: 'http://localhost:8378/1/apps/test/request_password_reset', - }).catch(e => e); + it('should get 404 on request_password_reset', done => { + request('http://localhost:8378/1/apps/test/request_password_reset', (err, httpResponse) => { expect(httpResponse.status).toBe(404); + done(); }); }); +}); - describe('public API supplied with invalid application id', () => { - beforeEach(async () => { - await reconfigureServer({ appName: 'unused' }); - }); - - it('should get 403 on verify_email', async () => { - const httpResponse = await request({ - url: 'http://localhost:8378/1/apps/invalid/verify_email', - }).catch(e => e); - expect(httpResponse.status).toBe(403); - }); +describe('public API supplied with invalid application id', () => { + beforeEach(done => { + reconfigureServer({ appName: 'unused' }).then(done, fail); + }); - it('should get 403 choose_password', async () => { - const httpResponse = await request({ - url: 'http://localhost:8378/1/apps/choose_password?id=invalid', - }).catch(e => e); + it('should get 403 on verify_email', done => { + request('http://localhost:8378/1/apps/invalid/verify_email', (err, httpResponse) => { expect(httpResponse.status).toBe(403); + done(); }); + }); - it('should get 403 on get of request_password_reset', async () => { - const httpResponse = await request({ - url: 'http://localhost:8378/1/apps/invalid/request_password_reset', - }).catch(e => e); + it('should get 403 choose_password', done => { + request('http://localhost:8378/1/apps/choose_password?id=invalid', (err, httpResponse) => { expect(httpResponse.status).toBe(403); + done(); }); + }); - it('should get 403 on post of request_password_reset', async () => { - const httpResponse = await request({ - url: 'http://localhost:8378/1/apps/invalid/request_password_reset', - method: 'POST', - }).catch(e => e); + it('should get 403 on get of request_password_reset', done => { + request('http://localhost:8378/1/apps/invalid/request_password_reset', (err, httpResponse) => { expect(httpResponse.status).toBe(403); + done(); }); + }); - it('should get 403 on resendVerificationEmail', async () => { - const httpResponse = await request({ - url: 'http://localhost:8378/1/apps/invalid/resend_verification_email', - }).catch(e => e); + it('should get 403 on post of request_password_reset', done => { + req({ + url: 'http://localhost:8378/1/apps/invalid/request_password_reset', + method: 'POST', + }).then(done.fail, httpResponse => { expect(httpResponse.status).toBe(403); + done(); }); }); - describe('pages', () => { - let router = new PublicAPIRouter(); - let req; - let pageResponse; - let redirectResponse; - let readFile; - const exampleLocale = 'de-AT'; - const config = { - appId: 'test', - appName: 'ExampleAppName', - verifyUserEmails: true, - emailAdapter: { - sendVerificationEmail: () => Promise.resolve(), - sendPasswordResetEmail: () => Promise.resolve(), - sendMail: () => {}, - }, - publicServerURL: 'http://localhost:8378/1', - customPages: {}, - pages: { - enableLocalization: true, - }, - }; - async function reconfigureServerWithPageOptions(options) { - await reconfigureServer({ - appId: Parse.applicationId, - masterKey: Parse.masterKey, - serverURL: Parse.serverURL, - pages: options, - }); - } - - beforeEach(async () => { - router = new PublicAPIRouter(); - readFile = spyOn(fs, 'readFile').and.callThrough(); - pageResponse = spyOn(PublicAPIRouter.prototype, 'pageResponse').and.callThrough() - redirectResponse = spyOn(PublicAPIRouter.prototype, 'redirectResponse').and.callThrough(); - req = { - method: 'GET', - config: { - appId: 'test', - appName: 'ExampleAppName', - publicServerURL: 'http://localhost:8378/1', - customPages: {}, - pages: { - enableLocalization: true, - }, - }, - query: { - locale: exampleLocale, - }, - }; - }); - - describe('server options', () => { - it('uses default configuration when none is set', async () => { - await reconfigureServerWithPageOptions({}); - expect(Config.get(Parse.applicationId).pages.enableLocalization).toBe( - Definitions.PagesOptions.enableLocalization.default - ); - }); - - it('throws on invalid configuration', async () => { - const options = [ - [], - 'a', - 0, - { enableLocalization: 'a' }, - { enableLocalization: 0 }, - { enableLocalization: {} }, - ]; - for (const option of options) { - await expectAsync(reconfigureServerWithPageOptions(option)).toBeRejected(); - } - }); - }); - - describe('placeholders', () => { - it('replaces placeholder in response content', async () => { - await expectAsync(router.goToPage(req, pages.invalidLink)).toBeResolved(); - - expect(readFile.calls.all()[0].returnValue).toBeDefined(); - const originalContent = await readFile.calls.all()[0].returnValue; - expect(originalContent).toContain('{{appName}}'); - - expect(pageResponse.calls.all()[0].returnValue).toBeDefined(); - const replacedContent = await pageResponse.calls.all()[0].returnValue; - expect(replacedContent.text).not.toContain('{{appName}}'); - expect(replacedContent.text).toContain(req.config.appName); - }); - - it('removes undefined placeholder in response content', async () => { - await expectAsync(router.goToPage(req, pages.choosePassword)).toBeResolved(); - - expect(readFile.calls.all()[0].returnValue).toBeDefined(); - const originalContent = await readFile.calls.all()[0].returnValue; - expect(originalContent).toContain('{{error}}'); - - // There is no error placeholder value set by default, so the - // {{error}} placeholder should just be removed from content - expect(pageResponse.calls.all()[0].returnValue).toBeDefined(); - const replacedContent = await pageResponse.calls.all()[0].returnValue; - expect(replacedContent.text).not.toContain('{{error}}'); - }); - }); - - describe('localization', () => { - it('returns default file if localization is disabled', async () => { - delete req.config.pages.enableLocalization; - - await expectAsync(router.goToPage(req, pages.invalidLink)).toBeResolved(); - expect(pageResponse.calls.all()[0].args[0]).toBeDefined(); - expect(pageResponse.calls.all()[0].args[0]).not.toMatch( - new RegExp(`\/de(-AT)?\/${pages.invalidLink.defaultFile}`) - ); - }); - - it('returns default file if no locale is specified', async () => { - delete req.query.locale; - - await expectAsync(router.goToPage(req, pages.invalidLink)).toBeResolved(); - expect(pageResponse.calls.all()[0].args[0]).toBeDefined(); - expect(pageResponse.calls.all()[0].args[0]).not.toMatch( - new RegExp(`\/de(-AT)?\/${pages.invalidLink.defaultFile}`) - ); - }); - - it('returns custom page regardless of localization enabled', async () => { - req.config.customPages = { invalidLink: 'http://invalid-link.example.com' }; - - await expectAsync(router.goToPage(req, pages.invalidLink)).toBeResolved(); - expect(pageResponse).not.toHaveBeenCalled(); - expect(redirectResponse.calls.all()[0].args[0]).toBe(req.config.customPages.invalidLink); - }); - - it('returns file for locale match', async () => { - await expectAsync(router.goToPage(req, pages.invalidLink)).toBeResolved(); - expect(pageResponse.calls.all()[0].args[0]).toBeDefined(); - expect(pageResponse.calls.all()[0].args[0]).toMatch( - new RegExp(`\/${req.query.locale}\/${pages.invalidLink.defaultFile}`) - ); - }); - - it('returns file for language match', async () => { - // Pretend no locale matching file exists - spyOn(Utils, 'fileExists').and.callFake(async path => { - return !path.includes(`/${req.query.locale}/${pages.invalidLink.defaultFile}`); - }); - - await expectAsync(router.goToPage(req, pages.invalidLink)).toBeResolved(); - expect(pageResponse.calls.all()[0].args[0]).toBeDefined(); - expect(pageResponse.calls.all()[0].args[0]).toMatch( - new RegExp(`\/de\/${pages.invalidLink.defaultFile}`) - ); - }); - - it('returns default file for neither locale nor language match', async () => { - req.query.locale = 'yo-LO'; - - await expectAsync(router.goToPage(req, pages.invalidLink)).toBeResolved(); - expect(pageResponse.calls.all()[0].args[0]).toBeDefined(); - expect(pageResponse.calls.all()[0].args[0]).not.toMatch( - new RegExp(`\/yo(-LO)?\/${pages.invalidLink.defaultFile}`) - ); - }); - - it('returns a file for GET request', async () => { - await expectAsync(router.goToPage(req, pages.invalidLink)).toBeResolved(); - expect(pageResponse).toHaveBeenCalled(); - expect(redirectResponse).not.toHaveBeenCalled(); - }); - - it('returns a redirect for POST request', async () => { - req.method = 'POST'; - await expectAsync(router.goToPage(req, pages.invalidLink)).toBeResolved(); - expect(pageResponse).not.toHaveBeenCalled(); - expect(redirectResponse).toHaveBeenCalled(); - }); - - it('returns a redirect for custom pages for GET and POST request', async () => { - req.config.customPages = { invalidLink: 'http://invalid-link.example.com' }; - - for (const method of ['GET', 'POST']) { - req.method = method; - await expectAsync(router.goToPage(req, pages.invalidLink)).toBeResolved(); - expect(pageResponse).not.toHaveBeenCalled(); - expect(redirectResponse).toHaveBeenCalled(); - } - }); - - it('responds to POST request with redirect response', async () => { - await reconfigureServer(config); - const response = await request({ - url: - 'http://localhost:8378/1/apps/test/request_password_reset?token=exampleToken&username=exampleUsername&locale=de-AT', - followRedirects: false, - method: 'POST', - }); - expect(response.status).toEqual(303); - expect(response.headers.location).toEqual('http://localhost:8378/apps/de-AT/invalid_link.html'); - }); - - it('responds to GET request with content response', async () => { - await reconfigureServer(config); - const response = await request({ - url: - 'http://localhost:8378/1/apps/test/request_password_reset?token=exampleToken&username=exampleUsername&locale=de-AT', - followRedirects: false, - method: 'GET', - }); - expect(response.status).toEqual(200); - expect(response.text).toContain(''); - }); - }); - - describe('end-to-end tests', () => { - it('localizes end-to-end for password reset: success', async () => { - await reconfigureServer(config); - const sendPasswordResetEmail = spyOn(config.emailAdapter, 'sendPasswordResetEmail').and.callThrough(); - const user = new Parse.User(); - user.setUsername('exampleUsername'); - user.setPassword('examplePassword'); - user.set('email', 'mail@example.com'); - await user.signUp(); - await Parse.User.requestPasswordReset(user.getEmail()); - - const link = sendPasswordResetEmail.calls.all()[0].args[0].link; - const linkWithLocale = new URL(link); - linkWithLocale.searchParams.append(pageParams.locale, exampleLocale); - - const linkResponse = await request({ - url: linkWithLocale.toString(), - followRedirects: false, - }); - expect(linkResponse.status).toBe(200); - - const appId = linkResponse.headers['x-parse-page-param-appid']; - const token = linkResponse.headers['x-parse-page-param-token']; - const locale = linkResponse.headers['x-parse-page-param-locale']; - const username = linkResponse.headers['x-parse-page-param-username']; - const publicServerUrl = linkResponse.headers['x-parse-page-param-publicserverurl']; - const choosePasswordPagePath = pageResponse.calls.all()[0].args[0]; - expect(appId).toBeDefined(); - expect(token).toBeDefined(); - expect(locale).toBeDefined(); - expect(username).toBeDefined(); - expect(publicServerUrl).toBeDefined(); - expect(choosePasswordPagePath).toMatch( - new RegExp(`\/${exampleLocale}\/${pages.choosePassword.defaultFile}`) - ); - pageResponse.calls.reset(); - - const formUrl = `${publicServerUrl}/apps/${appId}/request_password_reset`; - const formResponse = await request({ - url: formUrl, - method: 'POST', - body: { - token, - locale, - username, - new_password: 'newPassword', - }, - headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, - followRedirects: false, - }); - expect(formResponse.status).toEqual(200); - expect(pageResponse.calls.all()[0].args[0]).toContain(`/${locale}/${pages.passwordResetSuccess.defaultFile}`); - }); - - it('localizes end-to-end for password reset: invalid link', async () => { - await reconfigureServer(config); - const sendPasswordResetEmail = spyOn(config.emailAdapter, 'sendPasswordResetEmail').and.callThrough(); - const user = new Parse.User(); - user.setUsername('exampleUsername'); - user.setPassword('examplePassword'); - user.set('email', 'mail@example.com'); - await user.signUp(); - await Parse.User.requestPasswordReset(user.getEmail()); - - const link = sendPasswordResetEmail.calls.all()[0].args[0].link; - const linkWithLocale = new URL(link); - linkWithLocale.searchParams.append(pageParams.locale, exampleLocale); - linkWithLocale.searchParams.set(pageParams.token, 'invalidToken'); - - const linkResponse = await request({ - url: linkWithLocale.toString(), - followRedirects: false, - }); - expect(linkResponse.status).toBe(200); - - const pagePath = pageResponse.calls.all()[0].args[0]; - expect(pagePath).toMatch( - new RegExp(`\/${exampleLocale}\/${pages.invalidLink.defaultFile}`) - ); - }); - - it('localizes end-to-end for verify email: success', async () => { - await reconfigureServer(config); - const sendVerificationEmail = spyOn(config.emailAdapter, 'sendVerificationEmail').and.callThrough(); - const user = new Parse.User(); - user.setUsername('exampleUsername'); - user.setPassword('examplePassword'); - user.set('email', 'mail@example.com'); - await user.signUp(); - - const link = sendVerificationEmail.calls.all()[0].args[0].link; - const linkWithLocale = new URL(link); - linkWithLocale.searchParams.append(pageParams.locale, exampleLocale); - - const linkResponse = await request({ - url: linkWithLocale.toString(), - followRedirects: false, - }); - expect(linkResponse.status).toBe(200); - - const pagePath = pageResponse.calls.all()[0].args[0]; - expect(pagePath).toMatch( - new RegExp(`\/${exampleLocale}\/${pages.verifyEmailSuccess.defaultFile}`) - ); - }); - - it('localizes end-to-end for verify email: invalid verification link - link send success', async () => { - await reconfigureServer(config); - const sendVerificationEmail = spyOn(config.emailAdapter, 'sendVerificationEmail').and.callThrough(); - const user = new Parse.User(); - user.setUsername('exampleUsername'); - user.setPassword('examplePassword'); - user.set('email', 'mail@example.com'); - await user.signUp(); - - const link = sendVerificationEmail.calls.all()[0].args[0].link; - const linkWithLocale = new URL(link); - linkWithLocale.searchParams.append(pageParams.locale, exampleLocale); - linkWithLocale.searchParams.set(pageParams.token, 'invalidToken'); - - const linkResponse = await request({ - url: linkWithLocale.toString(), - followRedirects: false, - }); - expect(linkResponse.status).toBe(200); - - const appId = linkResponse.headers['x-parse-page-param-appid']; - const locale = linkResponse.headers['x-parse-page-param-locale']; - const username = linkResponse.headers['x-parse-page-param-username']; - const publicServerUrl = linkResponse.headers['x-parse-page-param-publicserverurl']; - const invalidVerificationPagePath = pageResponse.calls.all()[0].args[0]; - expect(appId).toBeDefined(); - expect(locale).toBe(exampleLocale); - expect(username).toBeDefined(); - expect(publicServerUrl).toBeDefined(); - expect(invalidVerificationPagePath).toMatch( - new RegExp(`\/${exampleLocale}\/${pages.invalidVerificationLink.defaultFile}`) - ); - - const formUrl = `${publicServerUrl}/apps/${appId}/resend_verification_email`; - const formResponse = await request({ - url: formUrl, - method: 'POST', - body: { - locale, - username, - }, - headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, - followRedirects: false, - }); - expect(formResponse.status).toEqual(303); - expect(formResponse.text).toContain(`/${locale}/${pages.linkSendSuccess.defaultFile}`); - }); - - it('localizes end-to-end for verify email: invalid verification link - link send fail', async () => { - await reconfigureServer(config); - const sendVerificationEmail = spyOn(config.emailAdapter, 'sendVerificationEmail').and.callThrough(); - const user = new Parse.User(); - user.setUsername('exampleUsername'); - user.setPassword('examplePassword'); - user.set('email', 'mail@example.com'); - await user.signUp(); - - const link = sendVerificationEmail.calls.all()[0].args[0].link; - const linkWithLocale = new URL(link); - linkWithLocale.searchParams.append(pageParams.locale, exampleLocale); - linkWithLocale.searchParams.set(pageParams.token, 'invalidToken'); - - const linkResponse = await request({ - url: linkWithLocale.toString(), - followRedirects: false, - }); - expect(linkResponse.status).toBe(200); - - const appId = linkResponse.headers['x-parse-page-param-appid']; - const locale = linkResponse.headers['x-parse-page-param-locale']; - const username = linkResponse.headers['x-parse-page-param-username']; - const publicServerUrl = linkResponse.headers['x-parse-page-param-publicserverurl']; - const invalidVerificationPagePath = pageResponse.calls.all()[0].args[0]; - expect(appId).toBeDefined(); - expect(locale).toBe(exampleLocale); - expect(username).toBeDefined(); - expect(publicServerUrl).toBeDefined(); - expect(invalidVerificationPagePath).toMatch( - new RegExp(`\/${exampleLocale}\/${pages.invalidVerificationLink.defaultFile}`) - ); - - spyOn(UserController.prototype, 'resendVerificationEmail').and.callFake(() => Promise.reject( - 'failed to resend verification email' - )); - - const formUrl = `${publicServerUrl}/apps/${appId}/resend_verification_email`; - const formResponse = await request({ - url: formUrl, - method: 'POST', - body: { - locale, - username, - }, - headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, - followRedirects: false, - }); - expect(formResponse.status).toEqual(303); - expect(formResponse.text).toContain(`/${locale}/${pages.linkSendFail.defaultFile}`); - }); - - it('localizes end-to-end for verify email: invalid link', async () => { - await reconfigureServer(config); - const formUrl = `${config.publicServerURL}/apps/${config.appId}/resend_verification_email`; - const formResponse = await request({ - url: formUrl, - method: 'POST', - body: { - locale: exampleLocale, - }, - headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, - followRedirects: false, - }); - expect(formResponse.status).toEqual(303); - expect(formResponse.text).toContain(`/${exampleLocale}/${pages.invalidLink.defaultFile}`); - }); - }); + it('should get 403 on resendVerificationEmail', done => { + request( + 'http://localhost:8378/1/apps/invalid/resend_verification_email', + (err, httpResponse) => { + expect(httpResponse.status).toBe(403); + done(); + } + ); }); }); diff --git a/spec/RegexVulnerabilities.spec.js b/spec/RegexVulnerabilities.spec.js index 60b701a866..1a96ebfdf5 100644 --- a/spec/RegexVulnerabilities.spec.js +++ b/spec/RegexVulnerabilities.spec.js @@ -132,8 +132,8 @@ describe('Regex Vulnerabilities', function () { url: `${serverURL}/apps/test/request_password_reset?username=someemail@somedomain.com&token[$regex]=`, method: 'GET', }); - expect(passwordResetResponse.status).toEqual(200); - expect(passwordResetResponse.text).toContain('Invalid Link'); + expect(passwordResetResponse.status).toEqual(302); + expect(passwordResetResponse.headers.location).toMatch(`\\/invalid\\_link\\.html`); await request({ url: `${serverURL}/apps/test/request_password_reset`, method: 'POST', @@ -170,9 +170,10 @@ describe('Regex Vulnerabilities', function () { url: `${serverURL}/apps/test/request_password_reset?username=someemail@somedomain.com&token=${token}`, method: 'GET', }); - expect(passwordResetResponse.status).toEqual(200); - expect(passwordResetResponse.text).toContain(token); - expect(passwordResetResponse.text).toContain('http://localhost:8378/1/apps/test/request_password_reset'); + expect(passwordResetResponse.status).toEqual(302); + expect(passwordResetResponse.headers.location).toMatch( + `\\/choose\\_password\\?token\\=${token}\\&` + ); await request({ url: `${serverURL}/apps/test/request_password_reset`, method: 'POST', diff --git a/spec/ValidationAndPasswordsReset.spec.js b/spec/ValidationAndPasswordsReset.spec.js index cd7e007f02..94d9793a39 100644 --- a/spec/ValidationAndPasswordsReset.spec.js +++ b/spec/ValidationAndPasswordsReset.spec.js @@ -271,9 +271,10 @@ describe('Custom Pages, Email Verification, Password Reset', () => { url: sendEmailOptions.link, followRedirects: false, }).then(response => { - expect(response.status).toEqual(200); - expect(response.text).toContain('Successfully verified your email'); - expect(response.text).toContain('account: user'); + expect(response.status).toEqual(302); + expect(response.text).toEqual( + 'Found. Redirecting to http://localhost:8378/1/apps/verify_email_success.html?username=user' + ); user .fetch() .then( @@ -595,7 +596,7 @@ describe('Custom Pages, Email Verification, Password Reset', () => { }) .then(() => { user.setPassword('other-password'); - user.setUsername('exampleUsername'); + user.setUsername('user'); user.set('email', 'user@parse.com'); return user.signUp(); }) @@ -605,9 +606,10 @@ describe('Custom Pages, Email Verification, Password Reset', () => { url: sendEmailOptions.link, followRedirects: false, }).then(response => { - expect(response.status).toEqual(200); - expect(response.text).toContain('Successfully verified your email'); - expect(response.text).toContain('exampleUsername'); + expect(response.status).toEqual(302); + expect(response.text).toEqual( + 'Found. Redirecting to http://localhost:8378/1/apps/verify_email_success.html?username=user' + ); user .fetch() .then( @@ -644,8 +646,10 @@ describe('Custom Pages, Email Verification, Password Reset', () => { url: 'http://localhost:8378/1/apps/test/verify_email', followRedirects: false, }).then(response => { - expect(response.status).toEqual(200); - expect(response.text).toContain('Invalid Link'); + expect(response.status).toEqual(302); + expect(response.text).toEqual( + 'Found. Redirecting to http://localhost:8378/1/apps/invalid_link.html' + ); done(); }); }); @@ -663,13 +667,13 @@ describe('Custom Pages, Email Verification, Password Reset', () => { publicServerURL: 'http://localhost:8378/1', }).then(() => { request({ - url: - 'http://localhost:8378/1/apps/test/verify_email?token=exampleToken&username=exampleUsername', + url: 'http://localhost:8378/1/apps/test/verify_email?token=asdfasdf&username=sadfasga', followRedirects: false, }).then(response => { - expect(response.status).toEqual(200); - expect(response.text).toContain('exampleUsername'); - expect(response.text).toContain('/apps/test/resend_verification_email'); + expect(response.status).toEqual(302); + expect(response.text).toEqual( + 'Found. Redirecting to http://localhost:8378/1/apps/invalid_verification_link.html?username=sadfasga&appId=test' + ); done(); }); }); @@ -691,12 +695,12 @@ describe('Custom Pages, Email Verification, Password Reset', () => { method: 'POST', followRedirects: false, body: { - username: 'exampleUsername', + username: 'sadfasga', }, }).then(response => { - expect(response.status).toEqual(303); + expect(response.status).toEqual(302); expect(response.text).toEqual( - 'Found. Redirecting to http://localhost:8378/apps/link_send_fail.html' + 'Found. Redirecting to http://localhost:8378/1/apps/link_send_fail.html' ); done(); }); @@ -708,13 +712,13 @@ describe('Custom Pages, Email Verification, Password Reset', () => { const emailAdapter = { sendVerificationEmail: () => { request({ - url: - 'http://localhost:8378/1/apps/test/verify_email?token=invalidToken&username=exampleUsername', + url: 'http://localhost:8378/1/apps/test/verify_email?token=invalid&username=zxcv', followRedirects: false, }).then(response => { - expect(response.status).toEqual(200); - expect(response.text).toContain('exampleUsername'); - expect(response.text).toContain('/apps/test/resend_verification_email'); + expect(response.status).toEqual(302); + expect(response.text).toEqual( + 'Found. Redirecting to http://localhost:8378/1/apps/invalid_verification_link.html?username=zxcv&appId=test' + ); user.fetch().then(() => { expect(user.get('emailVerified')).toEqual(false); done(); @@ -725,13 +729,13 @@ describe('Custom Pages, Email Verification, Password Reset', () => { sendMail: () => {}, }; reconfigureServer({ - appName: 'ExampleApp', + appName: 'emailing app', verifyUserEmails: true, emailAdapter: emailAdapter, publicServerURL: 'http://localhost:8378/1', }).then(() => { - user.setPassword('examplePassword'); - user.setUsername('exampleUsername'); + user.setPassword('asdf'); + user.setUsername('zxcv'); user.set('email', 'user@parse.com'); user.signUp(null, { success: () => {}, @@ -752,25 +756,22 @@ describe('Custom Pages, Email Verification, Password Reset', () => { url: options.link, followRedirects: false, }).then(response => { - expect(response.status).toEqual(200); - expect(response.text).toContain('ExampleApp'); - expect(response.text).toContain('exampleUsername'); - expect(response.text).toContain( - 'http://localhost:8378/1/apps/test/request_password_reset' - ); + expect(response.status).toEqual(302); + const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=[a-zA-Z0-9]+\&id=test\&username=zxcv%2Bzxcv/; + expect(response.text.match(re)).not.toBe(null); done(); }); }, sendMail: () => {}, }; reconfigureServer({ - appName: 'ExampleApp', + appName: 'emailing app', verifyUserEmails: true, emailAdapter: emailAdapter, publicServerURL: 'http://localhost:8378/1', }).then(() => { - user.setPassword('examplePassword'); - user.setUsername('exampleUsername'); + user.setPassword('asdf'); + user.setUsername('zxcv+zxcv'); user.set('email', 'user@parse.com'); user.signUp().then(() => { Parse.User.requestPasswordReset('user@parse.com', { @@ -800,8 +801,10 @@ describe('Custom Pages, Email Verification, Password Reset', () => { 'http://localhost:8378/1/apps/test/request_password_reset?token=asdfasdf&username=sadfasga', followRedirects: false, }).then(response => { - expect(response.status).toEqual(200); - expect(response.text).toContain('Invalid Link'); + expect(response.status).toEqual(302); + expect(response.text).toEqual( + 'Found. Redirecting to http://localhost:8378/1/apps/invalid_link.html' + ); done(); }); }); @@ -816,17 +819,15 @@ describe('Custom Pages, Email Verification, Password Reset', () => { url: options.link, followRedirects: false, }).then(response => { - expect(response.status).toEqual(200); - expect(response.text).toContain( - 'http://localhost:8378/1/apps/test/request_password_reset' - ); - - const token = response.headers['x-parse-page-param-token']; - if (!token) { + expect(response.status).toEqual(302); + const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=zxcv/; + const match = response.text.match(re); + if (!match) { fail('should have a token'); done(); return; } + const token = match[1]; request({ url: 'http://localhost:8378/1/apps/test/request_password_reset', @@ -837,8 +838,10 @@ describe('Custom Pages, Email Verification, Password Reset', () => { }, followRedirects: false, }).then(response => { - expect(response.status).toEqual(200); - expect(response.text).toContain('Your password has been updated.'); + expect(response.status).toEqual(302); + expect(response.text).toEqual( + 'Found. Redirecting to http://localhost:8378/1/apps/password_reset_success.html?username=zxcv' + ); Parse.User.logIn('zxcv', 'hello').then( function () { @@ -893,14 +896,15 @@ describe('Custom Pages, Email Verification, Password Reset', () => { url: options.link, followRedirects: false, }).then(response => { - expect(response.status).toEqual(200); - - const token = response.headers['x-parse-page-param-token']; - if (!token) { + expect(response.status).toEqual(302); + const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=zxcv%2B1/; + const match = response.text.match(re); + if (!match) { fail('should have a token'); done(); return; } + const token = match[1]; request({ url: 'http://localhost:8378/1/apps/test/request_password_reset', @@ -911,8 +915,10 @@ describe('Custom Pages, Email Verification, Password Reset', () => { }, followRedirects: false, }).then(response => { - expect(response.status).toEqual(200); - expect(response.text).toContain('Your password has been updated.'); + expect(response.status).toEqual(302); + expect(response.text).toEqual( + 'Found. Redirecting to http://localhost:8378/1/apps/password_reset_success.html?username=zxcv%2B1' + ); done(); }); }); @@ -949,13 +955,14 @@ describe('Custom Pages, Email Verification, Password Reset', () => { url: options.link, followRedirects: false, }); - expect(response.status).toEqual(200); - - const token = response.headers['x-parse-page-param-token']; - if (!token) { + expect(response.status).toEqual(302); + const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=zxcv/; + const match = response.text.match(re); + if (!match) { fail('should have a token'); return; } + const token = match[1]; const resetResponse = await request({ url: 'http://localhost:8378/1/apps/test/request_password_reset', diff --git a/src/Config.js b/src/Config.js index 95f616ee24..bd637c4077 100644 --- a/src/Config.js +++ b/src/Config.js @@ -122,11 +122,21 @@ export class Config { if (Object.prototype.toString.call(pages) !== '[object Object]') { throw 'Parse Server option pages must be an object.'; } + if (pages.enableRouter === undefined) { + pages.enableRouter = PagesOptions.enableRouter.default; + } else if (!isBoolean(pages.enableRouter)) { + throw 'Parse Server option pages.enableRouter must be a boolean.'; + } if (pages.enableLocalization === undefined) { pages.enableLocalization = PagesOptions.enableLocalization.default; } else if (!isBoolean(pages.enableLocalization)) { throw 'Parse Server option pages.enableLocalization must be a boolean.'; } + if (pages.forceRedirect === undefined) { + pages.forceRedirect = PagesOptions.forceRedirect.default; + } else if (!isBoolean(pages.forceRedirect)) { + throw 'Parse Server option pages.forceRedirect must be a boolean.'; + } } static validateIdempotencyOptions(idempotencyOptions) { diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index 8c640b40ce..061d21b7e0 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -285,7 +285,7 @@ module.exports.ParseServerOptions = { }, "pages": { "env": "PARSE_SERVER_PAGES", - "help": "The options for pages such as password reset and email verification.", + "help": "The options for pages such as password reset and email verification. Caution, this is an experimental feature that may not be appropriate for production.", "action": parsers.objectParser, "default": {} }, @@ -418,6 +418,18 @@ module.exports.PagesOptions = { "help": "Is true if pages should be localized; this has no effect on custom page redirects.", "action": parsers.booleanParser, "default": false + }, + "enableRouter": { + "env": "PARSE_SERVER_PAGES_ENABLE_ROUTER", + "help": "Is true if the pages router should be enabled; this is required for any of the pages options to take effect. Caution, this is an experimental feature that may not be appropriate for production.", + "action": parsers.booleanParser, + "default": false + }, + "forceRedirect": { + "env": "PARSE_SERVER_PAGES_FORCE_REDIRECT", + "help": "Is true if responses should always be redirects and never content, false if the response type should depend on the request type (GET request -> content response; POST request -> redirect response).", + "action": parsers.booleanParser, + "default": false } }; module.exports.CustomPagesOptions = { diff --git a/src/Options/docs.js b/src/Options/docs.js index 4b54841545..5d563f37ea 100644 --- a/src/Options/docs.js +++ b/src/Options/docs.js @@ -54,7 +54,7 @@ * @property {String} mountPath Mount path for the server, defaults to /parse * @property {Boolean} mountPlayground Mounts the GraphQL Playground - never use this option in production * @property {Number} objectIdSize Sets the number of characters in generated object id's, default 10 - * @property {PagesOptions} pages The options for pages such as password reset and email verification. + * @property {PagesOptions} pages The options for pages such as password reset and email verification. Caution, this is an experimental feature that may not be appropriate for production. * @property {PasswordPolicyOptions} passwordPolicy Password policy for enforcing password related rules * @property {String} playgroundPath Mount path for the GraphQL Playground, defaults to /playground * @property {Number} port The port to run the ParseServer, defaults to 1337. @@ -83,6 +83,8 @@ /** * @interface PagesOptions * @property {Boolean} enableLocalization Is true if pages should be localized; this has no effect on custom page redirects. + * @property {Boolean} enableRouter Is true if the pages router should be enabled; this is required for any of the pages options to take effect. Caution, this is an experimental feature that may not be appropriate for production. + * @property {Boolean} forceRedirect Is true if responses should always be redirects and never content, false if the response type should depend on the request type (GET request -> content response; POST request -> redirect response). */ /** diff --git a/src/Options/index.js b/src/Options/index.js index 030f824790..355270a387 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -138,8 +138,8 @@ export interface ParseServerOptions { /* Public URL to your parse server with http:// or https://. :ENV: PARSE_PUBLIC_SERVER_URL */ publicServerURL: ?string; - /* The options for pages such as password reset and email verification. - :DEFAULT:{} */ + /* The options for pages such as password reset and email verification. Caution, this is an experimental feature that may not be appropriate for production. + :DEFAULT: {} */ pages: ?PagesOptions; /* custom pages for password validation and reset :DEFAULT: {} */ @@ -230,9 +230,15 @@ export interface ParseServerOptions { } export interface PagesOptions { + /* Is true if the pages router should be enabled; this is required for any of the pages options to take effect. Caution, this is an experimental feature that may not be appropriate for production. + :DEFAULT: false */ + enableRouter: ?boolean; /* Is true if pages should be localized; this has no effect on custom page redirects. :DEFAULT: false */ enableLocalization: ?boolean; + /* Is true if responses should always be redirects and never content, false if the response type should depend on the request type (GET request -> content response; POST request -> redirect response). + :DEFAULT: false */ + forceRedirect: ?boolean; } export interface CustomPagesOptions { diff --git a/src/Page.js b/src/Page.js new file mode 100644 index 0000000000..2ed0339066 --- /dev/null +++ b/src/Page.js @@ -0,0 +1,28 @@ +/*eslint no-unused-vars: "off"*/ +/** + * @interface Page + * Page + * Page content that is returned by PageRouter. + */ +export class Page { + /** + * @description Creates a page. + * @param {Object} params The page parameters. + * @param {String} params.id The page identifier. + * @param {String} params.defaultFile The page file name. + * @returns {Page} The page. + */ + constructor(params) { + const { id, defaultFile } = params; + + // Ensure requried parameters + if ([id, defaultFile].includes(undefined)) { + throw 'missing parameters'; + } + + this.id = id; + this.defaultFile = defaultFile; + } +} + +export default Page; diff --git a/src/ParseServer.js b/src/ParseServer.js index a607ec15eb..0c2b458019 100644 --- a/src/ParseServer.js +++ b/src/ParseServer.js @@ -27,6 +27,7 @@ import { IAPValidationRouter } from './Routers/IAPValidationRouter'; import { InstallationsRouter } from './Routers/InstallationsRouter'; import { LogsRouter } from './Routers/LogsRouter'; import { ParseLiveQueryServer } from './LiveQuery/ParseLiveQueryServer'; +import { PagesRouter } from './Routers/PagesRouter'; import { PublicAPIRouter } from './Routers/PublicAPIRouter'; import { PushRouter } from './Routers/PushRouter'; import { CloudCodeRouter } from './Routers/CloudCodeRouter'; @@ -134,7 +135,8 @@ class ParseServer { * @static * Create an express app for the parse server * @param {Object} options let you specify the maxUploadSize when creating the express app */ - static app({ maxUploadSize = '20mb', appId, directAccess }) { + static app(options) { + const { maxUploadSize = '20mb', appId, directAccess, pages } = options; // This app serves the Parse API directly. // It's the equivalent of https://api.parse.com/1 in the hosted Parse API. var api = express(); @@ -154,7 +156,10 @@ class ParseServer { }); }); - api.use('/', bodyParser.urlencoded({ extended: false }), new PublicAPIRouter().expressRouter()); + api.use('/', bodyParser.urlencoded({ extended: false }), pages.enableRouter + ? new PagesRouter().expressRouter() + : new PublicAPIRouter().expressRouter() + ); api.use(bodyParser.json({ type: '*/*', limit: maxUploadSize })); api.use(middlewares.allowMethodOverride); diff --git a/src/Routers/PagesRouter.js b/src/Routers/PagesRouter.js new file mode 100644 index 0000000000..08f3dd431c --- /dev/null +++ b/src/Routers/PagesRouter.js @@ -0,0 +1,448 @@ +import PromiseRouter from '../PromiseRouter'; +import Config from '../Config'; +import express from 'express'; +import path from 'path'; +import { promises as fs } from 'fs'; +import { Parse } from 'parse/node'; +import Utils from '../Utils'; +import mustache from 'mustache'; +import Page from '../Page'; + +const publicPath = path.resolve(__dirname, '../../public'); +const defaultPagePath = file => { return path.join(publicPath, file); }; +const composePageUrl = (file, publicServerUrl, locale) => { + let url = publicServerUrl; + url += url.endsWith('/') ? '' : '/'; + url += 'apps/'; + url += locale === undefined ? '' : locale + '/'; + url += file; + return url; +}; +// All pages with custom page key for reference and file name +const pages = Object.freeze({ + invalidLink: new Page({ id: 'invalidLink', defaultFile: 'invalid_link.html' }), + linkSendFail: new Page({ id: 'linkSendFail', defaultFile: 'link_send_fail.html' }), + choosePassword: new Page({ id: 'choosePassword', defaultFile: 'choose_password.html' }), + linkSendSuccess: new Page({ id: 'linkSendSuccess', defaultFile: 'link_send_success.html' }), + verifyEmailSuccess: new Page({ id: 'verifyEmailSuccess', defaultFile: 'verify_email_success.html' }), + passwordResetSuccess: new Page({ id: 'passwordResetSuccess', defaultFile: 'password_reset_success.html' }), + invalidVerificationLink: new Page({ id: 'invalidVerificationLink', defaultFile: 'invalid_verification_link.html' }), +}); +// All page parameters for reference to be used as template placeholders or query params +const pageParams = Object.freeze({ + appName: 'appName', + appId: 'appId', + token: 'token', + username: 'username', + error: 'error', + locale: 'locale', + publicServerUrl: 'publicServerUrl', +}); +// The header prefix to add page params as response headers +const pageParamHeaderPrefix = 'x-parse-page-param-'; + +export class PagesRouter extends PromiseRouter { + verifyEmail(req) { + const config = req.config; + const { username, token: rawToken } = req.query; + const token = rawToken && typeof rawToken !== 'string' ? rawToken.toString() : rawToken; + + if (!config) { + this.invalidRequest(); + } + + if (!token || !username) { + return this.goToPage(req, pages.invalidLink); + } + + const userController = config.userController; + return userController.verifyEmail(username, token).then( + () => { + const params = { + [pageParams.username]: username, + }; + return this.goToPage(req, pages.verifyEmailSuccess, params); + }, + () => { + if (req.query.username && req.params.appId) { + const params = { + [pageParams.appId]: req.params.appId, + [pageParams.username]: req.query.username, + }; + return this.goToPage(req, pages.invalidVerificationLink, params); + } else { + return this.goToPage(req, pages.invalidLink); + } + } + ); + } + + resendVerificationEmail(req) { + const config = req.config; + const username = req.body.username; + + if (!config) { + this.invalidRequest(); + } + + if (!username) { + return this.goToPage(req, pages.invalidLink); + } + + const userController = config.userController; + + return userController.resendVerificationEmail(username).then( + () => { + return this.goToPage(req, pages.linkSendSuccess); + }, + () => { + return this.goToPage(req, pages.linkSendFail); + } + ); + } + + choosePassword(req) { + const config = req.config; + const params = { + [pageParams.appId]: req.params.appId, + [pageParams.appName]: config.appName, + [pageParams.token]: req.query.token, + [pageParams.username]: req.query.username, + [pageParams.publicServerUrl]: config.publicServerURL + }; + return this.goToPage(req, pages.choosePassword, params); + } + + requestResetPassword(req) { + const config = req.config; + + if (!config) { + this.invalidRequest(); + } + + const { username, token: rawToken } = req.query; + const token = rawToken && typeof rawToken !== 'string' ? rawToken.toString() : rawToken; + + if (!username || !token) { + return this.goToPage(req, pages.invalidLink); + } + + return config.userController.checkResetTokenValidity(username, token).then( + () => { + const params = { + [pageParams.token]: token, + [pageParams.username]: username, + [pageParams.appId]: config.applicationId, + [pageParams.appName]: config.appName, + }; + return this.goToPage(req, pages.choosePassword, params); + }, + () => { + return this.goToPage(req, pages.invalidLink); + } + ); + } + + resetPassword(req) { + const config = req.config; + + if (!config) { + this.invalidRequest(); + } + + const { username, new_password, token: rawToken } = req.body; + const token = rawToken && typeof rawToken !== 'string' ? rawToken.toString() : rawToken; + + if ((!username || !token || !new_password) && req.xhr === false) { + return this.goToPage(req, pages.invalidLink); + } + + if (!username) { + throw new Parse.Error(Parse.Error.USERNAME_MISSING, 'Missing username'); + } + + if (!token) { + throw new Parse.Error(Parse.Error.OTHER_CAUSE, 'Missing token'); + } + + if (!new_password) { + throw new Parse.Error(Parse.Error.PASSWORD_MISSING, 'Missing password'); + } + + return config.userController + .updatePassword(username, token, new_password) + .then( + () => { + return Promise.resolve({ + success: true, + }); + }, + err => { + return Promise.resolve({ + success: false, + err, + }); + } + ) + .then(result => { + if (req.xhr) { + if (result.success) { + return Promise.resolve({ + status: 200, + response: 'Password successfully reset', + }); + } + if (result.err) { + throw new Parse.Error(Parse.Error.OTHER_CAUSE, `${result.err}`); + } + } + + const query = result.success + ? { + [pageParams.username]: username, + } + : { + [pageParams.username]: username, + [pageParams.token]: token, + [pageParams.appId]: config.applicationId, + [pageParams.error]: result.err, + [pageParams.appName]: config.appName, + }; + const page = result.success ? pages.passwordResetSuccess : pages.choosePassword; + + return this.goToPage(req, page, query, false); + }); + } + + /** + * Returns page content if the page is a local file or returns a + * redirect to a custom page. + * @param {Object} req The express request. + * @param {Page} page The page to go to. + * @param {Object} [params={}] The query parameters to attach to the URL in case of + * HTTP redirect responses for POST requests, or the placeholders to fill into + * the response content in case of HTTP content responses for GET requests. + * @param {Boolean} [responseType] Is true if a redirect response should be forced, + * false if a content response should be forced, undefined if the response type + * should depend on the request type by default: + * - GET request -> content response + * - POST request -> redirect response (PRG pattern) + * @returns {Promise} The express response. + */ + goToPage(req, page, params = {}, responseType) { + const config = req.config; + + // Determine redirect either by force, response setting or request method + const redirect = config.pages.forceRedirect + ? true + : responseType !== undefined + ? responseType + : req.method == 'POST'; + + // Include default parameters + const defaultParams = { + [pageParams.appId]: config.appId, + [pageParams.appName]: config.appName, + [pageParams.publicServerUrl]: config.publicServerURL, + }; + if (Object.values(defaultParams).includes(undefined)) { + return this.notFound(); + } + params = Object.assign(params, defaultParams); + + // Add locale to params to ensure it is passed on with every request; + // that means, once a locale is set, it is passed on to any follow-up page, + // e.g. request_password_reset -> choose_password -> passwort_reset_success + const locale = + (req.query || {})[pageParams.locale] + || (req.body || {})[pageParams.locale] + || (req.params || {})[pageParams.locale] + || (req.headers || {})[pageParamHeaderPrefix + pageParams.locale]; + params[pageParams.locale] = locale; + + // Compose paths and URLs + const defaultFile = page.defaultFile; + const defaultPath = defaultPagePath(defaultFile); + const defaultUrl = composePageUrl(defaultFile, config.publicServerURL); + + // If custom page URL is set redirect to it without localization + const customPageUrl = config.customPages[page.id]; + if (customPageUrl) { + return this.redirectResponse(customPageUrl, params); + } + + // If localization is enabled + if (config.pages.enableLocalization && locale) { + return Utils.getLocalizedPath(defaultPath, locale).then(({ path, subdir }) => + redirect + ? this.redirectResponse(composePageUrl(defaultFile, config.publicServerURL, subdir), params) + : this.pageResponse(path, params) + ); + } else { + return redirect + ? this.redirectResponse(defaultUrl, params) + : this.pageResponse(defaultPath, params); + } + } + + /** + * Creates a response with file content. + * @param {String} path The path of the file to return. + * @param {Object} placeholders The placeholders to fill in the + * content. + * @returns {Object} The Promise Router response. + */ + async pageResponse(path, placeholders) { + // Get file content + let data; + try { + data = await fs.readFile(path, 'utf-8'); + } catch (e) { + return this.notFound(); + } + + // Fill placeholders + data = mustache.render(data, placeholders); + + // Add placeholers in header to allow parsing for programmatic use + // of response, instead of having to parse the HTML content. + const headers = Object.entries(placeholders).reduce((m, p) => { + if (p[1] !== undefined) { + m[`${pageParamHeaderPrefix}${p[0].toLowerCase()}`] = p[1]; + } + return m; + }, {}); + + return { text: data, headers: headers }; + } + + /** + * Creates a response with http rediret. + * @param {Object} req The express request. + * @param {String} path The path of the file to return. + * @param {Object} params The query parameters to include. + * @returns {Object} The Promise Router response. + */ + async redirectResponse(url, params) { + // Remove any parameters with undefined value + params = Object.entries(params).reduce((m, p) => { + if (p[1] !== undefined) { + m[p[0]] = p[1]; + } + return m; + }, {}); + + // Compose URL with parameters in query + const location = new URL(url); + Object.entries(params).forEach(p => location.searchParams.set(p[0], p[1])); + const locationString = location.toString(); + + // Add parameters to header to allow parsing for programmatic use + // of response, instead of having to parse the HTML content. + const headers = Object.entries(params).reduce((m, p) => { + if (p[1] !== undefined) { + m[`${pageParamHeaderPrefix}${p[0].toLowerCase()}`] = p[1]; + } + return m; + }, {}); + + return { + status: 303, + location: locationString, + headers: headers, + }; + } + + notFound() { + return { + text: 'Not found.', + status: 404, + }; + } + + invalidRequest() { + const error = new Error(); + error.status = 403; + error.message = 'unauthorized'; + throw error; + } + + setConfig(req) { + req.config = Config.get(req.params.appId || req.query.appId); + if (!req.config) { + this.invalidRequest(); + } + return Promise.resolve(); + } + + mountRoutes() { + this.route( + 'GET', + '/apps/:appId/verify_email', + req => { + this.setConfig(req); + }, + req => { + return this.verifyEmail(req); + } + ); + + this.route( + 'POST', + '/apps/:appId/resend_verification_email', + req => { + this.setConfig(req); + }, + req => { + return this.resendVerificationEmail(req); + } + ); + + this.route( + 'GET', + '/apps/choose_password', + req => { + this.setConfig(req); + }, + req => { + return this.choosePassword(req); + } + ); + + this.route( + 'POST', + '/apps/:appId/request_password_reset', + req => { + this.setConfig(req); + }, + req => { + return this.resetPassword(req); + } + ); + + this.route( + 'GET', + '/apps/:appId/request_password_reset', + req => { + this.setConfig(req); + }, + req => { + return this.requestResetPassword(req); + } + ); + } + + expressRouter() { + const router = express.Router(); + router.use('/apps', express.static(publicPath)); + router.use('/', super.expressRouter()); + return router; + } +} + +export default PagesRouter; +module.exports = { + PagesRouter, + pageParams, + pages, +}; diff --git a/src/Routers/PublicAPIRouter.js b/src/Routers/PublicAPIRouter.js index 45dfe016dd..5009ee7d22 100644 --- a/src/Routers/PublicAPIRouter.js +++ b/src/Routers/PublicAPIRouter.js @@ -2,99 +2,108 @@ import PromiseRouter from '../PromiseRouter'; import Config from '../Config'; import express from 'express'; import path from 'path'; -import { promises as fs } from 'fs'; +import fs from 'fs'; +import qs from 'querystring'; import { Parse } from 'parse/node'; -import Utils from '../Utils'; -import mustache from 'mustache'; - -const publicPath = path.resolve(__dirname, '../../public'); -const defaultPagePath = file => { return path.join(publicPath, file); }; -const defaultPageUrl = (file, serverUrl) => { return new URL('/apps/' + file, serverUrl).toString(); }; -// All pages with custom page key for reference and file name -const pages = Object.freeze({ - invalidLink: { customPageKey: 'invalidLink', defaultFile: 'invalid_link.html' }, - linkSendFail: { customPageKey: 'linkSendFail', defaultFile: 'link_send_fail.html' }, - choosePassword: { customPageKey: 'choosePassword', defaultFile: 'choose_password.html' }, - linkSendSuccess: { customPageKey: 'linkSendSuccess', defaultFile: 'link_send_success.html' }, - verifyEmailSuccess: { customPageKey: 'verifyEmailSuccess', defaultFile: 'verify_email_success.html', }, - passwordResetSuccess: { customPageKey: 'passwordResetSuccess', defaultFile: 'password_reset_success.html', }, - invalidVerificationLink: { customPageKey: 'invalidVerificationLink', defaultFile: 'invalid_verification_link.html', }, -}); -// All page parameters for reference to be used as template placeholders or query params -const pageParams = Object.freeze({ - appName: 'appName', - appId: 'appId', - token: 'token', - username: 'username', - error: 'error', - locale: 'locale', - publicServerUrl: 'publicServerUrl', -}); -// The header prefix to add page params as response headers -const pageParamHeaderPrefix = 'x-parse-page-param-'; + +const public_html = path.resolve(__dirname, '../../public_html'); +const views = path.resolve(__dirname, '../../views'); export class PublicAPIRouter extends PromiseRouter { verifyEmail(req) { - const config = req.config; const { username, token: rawToken } = req.query; const token = rawToken && typeof rawToken !== 'string' ? rawToken.toString() : rawToken; + const appId = req.params.appId; + const config = Config.get(appId); + if (!config) { this.invalidRequest(); } + if (!config.publicServerURL) { + return this.missingPublicServerURL(); + } + if (!token || !username) { - return this.goToPage(req, pages.invalidLink); + return this.invalidLink(req); } const userController = config.userController; return userController.verifyEmail(username, token).then( () => { - const params = { - [pageParams.username]: username, - }; - return this.goToPage(req, pages.verifyEmailSuccess, params); + const params = qs.stringify({ username }); + return Promise.resolve({ + status: 302, + location: `${config.verifyEmailSuccessURL}?${params}`, + }); }, () => { - if (req.query.username && req.params.appId) { - const params = { - [pageParams.username]: req.query.username, - [pageParams.appId]: req.params.appId, - }; - return this.goToPage(req, pages.invalidVerificationLink, params); - } else { - return this.goToPage(req, pages.invalidLink); - } + return this.invalidVerificationLink(req); } ); } resendVerificationEmail(req) { - const config = req.config; const username = req.body.username; + const appId = req.params.appId; + const config = Config.get(appId); if (!config) { this.invalidRequest(); } + if (!config.publicServerURL) { + return this.missingPublicServerURL(); + } + if (!username) { - return this.goToPage(req, pages.invalidLink); + return this.invalidLink(req); } const userController = config.userController; return userController.resendVerificationEmail(username).then( () => { - return this.goToPage(req, pages.linkSendSuccess); + return Promise.resolve({ + status: 302, + location: `${config.linkSendSuccessURL}`, + }); }, () => { - return this.goToPage(req, pages.linkSendFail); + return Promise.resolve({ + status: 302, + location: `${config.linkSendFailURL}`, + }); } ); } - choosePassword(req) { - return this.goToPage(req, pages.choosePassword); + changePassword(req) { + return new Promise((resolve, reject) => { + const config = Config.get(req.query.id); + + if (!config) { + this.invalidRequest(); + } + + if (!config.publicServerURL) { + return resolve({ + status: 404, + text: 'Not found.', + }); + } + // Should we keep the file in memory or leave like that? + fs.readFile(path.resolve(views, 'choose_password'), 'utf-8', (err, data) => { + if (err) { + return reject(err); + } + data = data.replace('PARSE_SERVER_URL', `'${config.publicServerURL}'`); + resolve({ + text: data, + }); + }); + }); } requestResetPassword(req) { @@ -104,25 +113,32 @@ export class PublicAPIRouter extends PromiseRouter { this.invalidRequest(); } + if (!config.publicServerURL) { + return this.missingPublicServerURL(); + } + const { username, token: rawToken } = req.query; const token = rawToken && typeof rawToken !== 'string' ? rawToken.toString() : rawToken; if (!username || !token) { - return this.goToPage(req, pages.invalidLink); + return this.invalidLink(req); } return config.userController.checkResetTokenValidity(username, token).then( () => { - const params = { - [pageParams.token]: token, - [pageParams.username]: username, - [pageParams.appId]: config.applicationId, - [pageParams.appName]: config.appName, - }; - return this.goToPage(req, pages.choosePassword, params); + const params = qs.stringify({ + token, + id: config.applicationId, + username, + app: config.appName, + }); + return Promise.resolve({ + status: 302, + location: `${config.choosePasswordURL}?${params}`, + }); }, () => { - return this.goToPage(req, pages.invalidLink); + return this.invalidLink(req); } ); } @@ -134,11 +150,15 @@ export class PublicAPIRouter extends PromiseRouter { this.invalidRequest(); } + if (!config.publicServerURL) { + return this.missingPublicServerURL(); + } + const { username, new_password, token: rawToken } = req.body; const token = rawToken && typeof rawToken !== 'string' ? rawToken.toString() : rawToken; if ((!username || !token || !new_password) && req.xhr === false) { - return this.goToPage(req, pages.invalidLink); + return this.invalidLink(req); } if (!username) { @@ -169,6 +189,14 @@ export class PublicAPIRouter extends PromiseRouter { } ) .then(result => { + const params = qs.stringify({ + username: username, + token: token, + id: config.applicationId, + error: result.err, + app: config.appName, + }); + if (req.xhr) { if (result.success) { return Promise.resolve({ @@ -181,163 +209,46 @@ export class PublicAPIRouter extends PromiseRouter { } } - const query = result.success - ? { - [pageParams.username]: username, - } - : { - [pageParams.username]: username, - [pageParams.token]: token, - [pageParams.appId]: config.applicationId, - [pageParams.error]: result.err, - [pageParams.appName]: config.appName, - }; - const page = result.success ? pages.passwordResetSuccess : pages.choosePassword; - - return this.goToPage(req, page, query, false); + const encodedUsername = encodeURIComponent(username); + const location = result.success + ? `${config.passwordResetSuccessURL}?username=${encodedUsername}` + : `${config.choosePasswordURL}?${params}`; + + return Promise.resolve({ + status: 302, + location, + }); }); } - /** - * Returns page content if the page is a local file or returns a - * redirect to a custom page. - * @param {Object} req The express request. - * @param {Object} page The page to go to. - * @param {Object} [params={}] The query parameters to attach to the URL in case of - * HTTP redirect responses for POST requests, or the placeholders to fill into - * the response content in case of HTTP content responses for GET requests. - * @param {Boolean} [responseType] Is true if a redirect response should be forced, - * false if a content response should be forced, undefined if the response type - * should depend on the request type by default: - * - GET request -> content response - * - POST request -> redirect response (PRG pattern) - * @returns {Promise} The express response. - */ - goToPage(req, page, params = {}, responseType) { - const config = req.config; - const locale = - (req.query || {})[pageParams.locale] - || (req.body || {})[pageParams.locale] - || (req.params || {})[pageParams.locale] - || (req.headers || {})[pageParamHeaderPrefix + pageParams.locale]; - const redirect = responseType !== undefined ? responseType : req.method == 'POST'; - - // Ensure default parameters required for every page - const requiredParams = { - [pageParams.appId]: config.appId, - [pageParams.appName]: config.appName, - [pageParams.publicServerUrl]: config.publicServerURL, - }; - if (Object.values(requiredParams).includes(undefined)) { - return this.notFound(); - } - params = Object.assign(params, requiredParams); - - // Add locale to params to ensure it is passed on with every request; - // that means, once a locale is set, it is passed on to any follow-up page, - // e.g. request_password_reset -> choose_password -> passwort_reset_success - params[pageParams.locale] = locale; - - // Compose paths and URLs - const customPage = config.customPages[page.customPageKey]; - const defaultFile = page.defaultFile; - const defaultPath = defaultPagePath(defaultFile); - const defaultUrl = defaultPageUrl(defaultFile, config.publicServerURL); - - // If custom page is set redirect to it without localization - if (customPage) { - return this.redirectResponse(customPage, params); - } - - // If localization is enabled - if (config.pages.enableLocalization && locale) { - return Utils.getLocalizedPath(defaultPath, locale).then(({ path, subdir }) => - redirect - ? this.redirectResponse(new URL( - subdir - ? `/apps/${subdir}/${defaultFile}` - : `/apps/${defaultFile}`, - config.publicServerURL).toString(), params) - : this.pageResponse(path, params) - ); - } else { - return redirect - ? this.redirectResponse(defaultUrl, params) - : this.pageResponse(defaultPath, params); - } + invalidLink(req) { + return Promise.resolve({ + status: 302, + location: req.config.invalidLinkURL, + }); } - /** - * Creates a response with file content. - * @param {String} path The path of the file to return. - * @param {Object} placeholders The placeholders to fill in the - * content. - * @returns {Object} The Promise Router response. - */ - async pageResponse(path, placeholders) { - // Get file content - let data; - try { - data = await fs.readFile(path, 'utf-8'); - } catch (e) { - return this.notFound(); + invalidVerificationLink(req) { + const config = req.config; + if (req.query.username && req.params.appId) { + const params = qs.stringify({ + username: req.query.username, + appId: req.params.appId, + }); + return Promise.resolve({ + status: 302, + location: `${config.invalidVerificationLinkURL}?${params}`, + }); + } else { + return this.invalidLink(req); } - - // Fill placeholders - data = mustache.render(data, placeholders); - - // Add placeholers in header to allow parsing for programmatic use - // of response, instead of having to parse the HTML content. - const headers = Object.entries(placeholders).reduce((m, p) => { - if (p[1] !== undefined) { - m[`${pageParamHeaderPrefix}${p[0].toLowerCase()}`] = p[1]; - } - return m; - }, {}); - - return { text: data, headers: headers }; } - /** - * Creates a response with http 303 rediret. - * @param {Object} req The express request. - * @param {String} path The path of the file to return. - * @returns {Object} The Promise Router response. - */ - async redirectResponse(url, query) { - // Remove any query parameters with undefined value - query = Object.entries(query).reduce((m, p) => { - if (p[1] !== undefined) { - m[p[0]] = p[1]; - } - return m; - }, {}); - - // Maybe use this in the future to add params as query instead of headers? - // const location = new URL(url); - // Object.entries(query).forEach(p => location.searchParams.set(p[0], p[1])); - - // Add placeholers in header to allow parsing for programmatic use - // of response, instead of having to parse the HTML content. - const headers = Object.entries(query).reduce((m, p) => { - if (p[1] !== undefined) { - m[`${pageParamHeaderPrefix}${p[0].toLowerCase()}`] = p[1]; - } - return m; - }, {}); - - return { - status: 303, - location: url, - headers: headers, - }; - } - - notFound() { - return { + missingPublicServerURL() { + return Promise.resolve({ text: 'Not found.', status: 404, - }; + }); } invalidRequest() { @@ -348,10 +259,7 @@ export class PublicAPIRouter extends PromiseRouter { } setConfig(req) { - req.config = Config.get(req.params.appId || req.query.appId); - if (!req.config) { - this.invalidRequest(); - } + req.config = Config.get(req.params.appId); return Promise.resolve(); } @@ -378,16 +286,9 @@ export class PublicAPIRouter extends PromiseRouter { } ); - this.route( - 'GET', - '/apps/choose_password', - req => { - this.setConfig(req); - }, - req => { - return this.choosePassword(req); - } - ); + this.route('GET', '/apps/choose_password', req => { + return this.changePassword(req); + }); this.route( 'POST', @@ -414,15 +315,10 @@ export class PublicAPIRouter extends PromiseRouter { expressRouter() { const router = express.Router(); - router.use('/apps', express.static(publicPath)); + router.use('/apps', express.static(public_html)); router.use('/', super.expressRouter()); return router; } } export default PublicAPIRouter; -module.exports = { - PublicAPIRouter, - pageParams, - pages, -}; From 87ec8e79f85069f1bf42b3660ce6f608088a1616 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Tue, 19 Jan 2021 23:34:41 +0100 Subject: [PATCH 21/48] refactored PagesRouter test structure --- spec/PagesRouter.spec.js | 147 +++++++++++++++++---------------------- 1 file changed, 64 insertions(+), 83 deletions(-) diff --git a/spec/PagesRouter.spec.js b/spec/PagesRouter.spec.js index d5362415b0..f302b6392a 100644 --- a/spec/PagesRouter.spec.js +++ b/spec/PagesRouter.spec.js @@ -10,6 +10,61 @@ const { PagesRouter, pages, pageParams } = require('../lib/Routers/PagesRouter') describe('Pages Router', () => { describe('basic request', () => { + const config = { + appName: 'exampleAppname', + publicServerURL: 'http://localhost:8378/1', + pages: { enableRouter: true }, + }; + + beforeEach(async () => { + await reconfigureServer(config); + }); + + it('responds with file content on direct page request', async () => { + const urls = [ + 'http://localhost:8378/1/apps/invalid_link.html', + 'http://localhost:8378/1/apps/choose_password?appId=test', + 'http://localhost:8378/1/apps/verify_email_success.html', + 'http://localhost:8378/1/apps/password_reset_success.html', + ]; + for (const url of urls) { + const response = await request({ url }).catch(e => e); + expect(response.status).toBe(200); + } + }); + + it('responds with 404 if publicServerURL is not confgured', async () => { + await reconfigureServer({ + appName: 'unused', + pages: { enableRouter: true } + }); + const urls = [ + 'http://localhost:8378/1/apps/test/verify_email', + 'http://localhost:8378/1/apps/choose_password?appId=test', + 'http://localhost:8378/1/apps/test/request_password_reset', + ]; + for (const url of urls) { + const response = await request({ url }).catch(e => e); + expect(response.status).toBe(404); + } + }); + + it('respones with 403 access denied with invalid appId', async () => { + const reqs = [ + { url: 'http://localhost:8378/1/apps/invalid/verify_email', method: 'GET' }, + { url: 'http://localhost:8378/1/apps/choose_password?id=invalid', method: 'GET' }, + { url: 'http://localhost:8378/1/apps/invalid/request_password_reset', method: 'GET' }, + { url: 'http://localhost:8378/1/apps/invalid/request_password_reset', method: 'POST' }, + { url: 'http://localhost:8378/1/apps/invalid/resend_verification_email', method: 'GET' }, + ]; + for (const req of reqs) { + const response = await request(req).catch(e => e); + expect(response.status).toBe(403); + } + }); + }); + + describe('AJAX requests', () => { beforeEach(async () => { await reconfigureServer({ appName: 'exampleAppname', @@ -18,12 +73,12 @@ describe('Pages Router', () => { }); }); - it('should return missing username error on ajax request without username provided', async () => { + it('should return missing password error on ajax request without password provided', async () => { try { await request({ method: 'POST', url: 'http://localhost:8378/1/apps/test/request_password_reset', - body: `new_password=user1&token=43634643&username=`, + body: `new_password=&token=132414&username=Johnny`, headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-Requested-With': 'XMLHttpRequest', @@ -32,16 +87,16 @@ describe('Pages Router', () => { }); } catch (error) { expect(error.status).not.toBe(302); - expect(error.text).toEqual('{"code":200,"error":"Missing username"}'); + expect(error.text).toEqual('{"code":201,"error":"Missing password"}'); } }); - it('should return missing token error on ajax request without token provided', async () => { + it('should return missing username error on ajax request without username provided', async () => { try { await request({ method: 'POST', url: 'http://localhost:8378/1/apps/test/request_password_reset', - body: `new_password=user1&token=&username=Johnny`, + body: `new_password=user1&token=43634643&username=`, headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-Requested-With': 'XMLHttpRequest', @@ -50,16 +105,16 @@ describe('Pages Router', () => { }); } catch (error) { expect(error.status).not.toBe(302); - expect(error.text).toEqual('{"code":-1,"error":"Missing token"}'); + expect(error.text).toEqual('{"code":200,"error":"Missing username"}'); } }); - it('should return missing password error on ajax request without password provided', async () => { + it('should return missing token error on ajax request without token provided', async () => { try { await request({ method: 'POST', url: 'http://localhost:8378/1/apps/test/request_password_reset', - body: `new_password=&token=132414&username=Johnny`, + body: `new_password=user1&token=&username=Johnny`, headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-Requested-With': 'XMLHttpRequest', @@ -68,85 +123,11 @@ describe('Pages Router', () => { }); } catch (error) { expect(error.status).not.toBe(302); - expect(error.text).toEqual('{"code":201,"error":"Missing password"}'); - } - }); - - it('responds with file content on direct page request', async () => { - const urls = [ - 'http://localhost:8378/1/apps/invalid_link.html', - 'http://localhost:8378/1/apps/choose_password?appId=test', - 'http://localhost:8378/1/apps/verify_email_success.html', - 'http://localhost:8378/1/apps/password_reset_success.html', - ]; - for (const url of urls) { - const response = await request({ url }).catch(e => e); - expect(response.status).toBe(200); - } - }); - - it('responds with 404 if publicServerURL is not confgured', async () => { - await reconfigureServer({ - appName: 'unused', - pages: { enableRouter: true } - }); - const urls = [ - 'http://localhost:8378/1/apps/test/verify_email', - 'http://localhost:8378/1/apps/choose_password?appId=test', - 'http://localhost:8378/1/apps/test/request_password_reset', - ]; - for (const url of urls) { - const response = await request({ url }).catch(e => e); - expect(response.status).toBe(404); + expect(error.text).toEqual('{"code":-1,"error":"Missing token"}'); } }); }); - describe('public API supplied with invalid application id', () => { - beforeEach(async () => { - await reconfigureServer({ - appName: 'unused', - pages: { enableRouter: true } - }); - }); - - it('should get 403 on verify_email', async () => { - const httpResponse = await request({ - url: 'http://localhost:8378/1/apps/invalid/verify_email', - }).catch(e => e); - expect(httpResponse.status).toBe(403); - }); - - it('should get 403 choose_password', async () => { - const httpResponse = await request({ - url: 'http://localhost:8378/1/apps/choose_password?id=invalid', - }).catch(e => e); - expect(httpResponse.status).toBe(403); - }); - - it('should get 403 on get of request_password_reset', async () => { - const httpResponse = await request({ - url: 'http://localhost:8378/1/apps/invalid/request_password_reset', - }).catch(e => e); - expect(httpResponse.status).toBe(403); - }); - - it('should get 403 on post of request_password_reset', async () => { - const httpResponse = await request({ - url: 'http://localhost:8378/1/apps/invalid/request_password_reset', - method: 'POST', - }).catch(e => e); - expect(httpResponse.status).toBe(403); - }); - - it('should get 403 on resendVerificationEmail', async () => { - const httpResponse = await request({ - url: 'http://localhost:8378/1/apps/invalid/resend_verification_email', - }).catch(e => e); - expect(httpResponse.status).toBe(403); - }); - }); - describe('pages', () => { let router = new PagesRouter(); let req; From 0a6cc5e0604f55f9394fadd4785a2dd0eecd25a0 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Tue, 19 Jan 2021 23:36:37 +0100 Subject: [PATCH 22/48] added configuration option for custom path to pages --- spec/PagesRouter.spec.js | 22 +++++++++++++++ src/Config.js | 7 ++++- src/Options/Definitions.js | 4 +++ src/Options/docs.js | 1 + src/Options/index.js | 2 ++ src/ParseServer.js | 2 +- src/Routers/PagesRouter.js | 58 ++++++++++++++++++++++++-------------- src/Utils.js | 9 ++++++ 8 files changed, 82 insertions(+), 23 deletions(-) diff --git a/spec/PagesRouter.spec.js b/spec/PagesRouter.spec.js index f302b6392a..1eadfd0901 100644 --- a/spec/PagesRouter.spec.js +++ b/spec/PagesRouter.spec.js @@ -33,6 +33,17 @@ describe('Pages Router', () => { } }); + it('can load file from custom pages path', async () => { + const _config = config; + _config.pages.pagesPath = './public'; + await reconfigureServer(_config); + + const response = await request({ + url: 'http://localhost:8378/1/apps/invalid_link.html' + }).catch(e => e); + expect(response.status).toBe(200); + }); + it('responds with 404 if publicServerURL is not confgured', async () => { await reconfigureServer({ appName: 'unused', @@ -194,6 +205,9 @@ describe('Pages Router', () => { expect(Config.get(Parse.applicationId).pages.forceRedirect).toBe( Definitions.PagesOptions.forceRedirect.default ); + expect(Config.get(Parse.applicationId).pages.pagesPath).toBe( + Definitions.PagesOptions.pagesPath.default + ); }); it('throws on invalid configuration', async () => { @@ -201,15 +215,23 @@ describe('Pages Router', () => { [], 'a', 0, + true, { enableRouter: 'a' }, { enableRouter: 0 }, { enableRouter: {} }, + { enableRouter: [] }, { enableLocalization: 'a' }, { enableLocalization: 0 }, { enableLocalization: {} }, + { enableLocalization: [] }, { forceRedirect: 'a' }, { forceRedirect: 0 }, { forceRedirect: {} }, + { forceRedirect: [] }, + { pagesPath: true }, + { pagesPath: 0 }, + { pagesPath: {} }, + { pagesPath: [] }, ]; for (const option of options) { await expectAsync(reconfigureServerWithPageOptions(option)).toBeRejected(); diff --git a/src/Config.js b/src/Config.js index bd637c4077..ac59321881 100644 --- a/src/Config.js +++ b/src/Config.js @@ -11,7 +11,7 @@ import { FileUploadOptions, PagesOptions, } from './Options/Definitions'; -import { isBoolean } from 'lodash'; +import { isBoolean, isString } from 'lodash'; function removeTrailingSlash(str) { if (!str) { @@ -137,6 +137,11 @@ export class Config { } else if (!isBoolean(pages.forceRedirect)) { throw 'Parse Server option pages.forceRedirect must be a boolean.'; } + if (pages.pagesPath === undefined) { + pages.pagesPath = PagesOptions.pagesPath.default; + } else if (!isString(pages.pagesPath)) { + throw 'Parse Server option pages.pagesPath must be a string.'; + } } static validateIdempotencyOptions(idempotencyOptions) { diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index 061d21b7e0..2939847e00 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -430,6 +430,10 @@ module.exports.PagesOptions = { "help": "Is true if responses should always be redirects and never content, false if the response type should depend on the request type (GET request -> content response; POST request -> redirect response).", "action": parsers.booleanParser, "default": false + }, + "pagesPath": { + "env": "PARSE_SERVER_PAGES_PAGES_PATH", + "help": "The path to the pages directory; this also defines where the static endpoint '/apps' points to. Default is the './public/' directory." } }; module.exports.CustomPagesOptions = { diff --git a/src/Options/docs.js b/src/Options/docs.js index 5d563f37ea..281b8518b1 100644 --- a/src/Options/docs.js +++ b/src/Options/docs.js @@ -85,6 +85,7 @@ * @property {Boolean} enableLocalization Is true if pages should be localized; this has no effect on custom page redirects. * @property {Boolean} enableRouter Is true if the pages router should be enabled; this is required for any of the pages options to take effect. Caution, this is an experimental feature that may not be appropriate for production. * @property {Boolean} forceRedirect Is true if responses should always be redirects and never content, false if the response type should depend on the request type (GET request -> content response; POST request -> redirect response). + * @property {String} pagesPath The path to the pages directory; this also defines where the static endpoint '/apps' points to. Default is the './public/' directory. */ /** diff --git a/src/Options/index.js b/src/Options/index.js index 355270a387..c9cc9b4b2c 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -239,6 +239,8 @@ export interface PagesOptions { /* Is true if responses should always be redirects and never content, false if the response type should depend on the request type (GET request -> content response; POST request -> redirect response). :DEFAULT: false */ forceRedirect: ?boolean; + /* The path to the pages directory; this also defines where the static endpoint '/apps' points to. Default is the './public/' directory. */ + pagesPath: ?string; } export interface CustomPagesOptions { diff --git a/src/ParseServer.js b/src/ParseServer.js index 0c2b458019..a79baad163 100644 --- a/src/ParseServer.js +++ b/src/ParseServer.js @@ -157,7 +157,7 @@ class ParseServer { }); api.use('/', bodyParser.urlencoded({ extended: false }), pages.enableRouter - ? new PagesRouter().expressRouter() + ? new PagesRouter(pages).expressRouter() : new PublicAPIRouter().expressRouter() ); diff --git a/src/Routers/PagesRouter.js b/src/Routers/PagesRouter.js index 08f3dd431c..bb4e90ee08 100644 --- a/src/Routers/PagesRouter.js +++ b/src/Routers/PagesRouter.js @@ -8,16 +8,6 @@ import Utils from '../Utils'; import mustache from 'mustache'; import Page from '../Page'; -const publicPath = path.resolve(__dirname, '../../public'); -const defaultPagePath = file => { return path.join(publicPath, file); }; -const composePageUrl = (file, publicServerUrl, locale) => { - let url = publicServerUrl; - url += url.endsWith('/') ? '' : '/'; - url += 'apps/'; - url += locale === undefined ? '' : locale + '/'; - url += file; - return url; -}; // All pages with custom page key for reference and file name const pages = Object.freeze({ invalidLink: new Page({ id: 'invalidLink', defaultFile: 'invalid_link.html' }), @@ -42,6 +32,19 @@ const pageParams = Object.freeze({ const pageParamHeaderPrefix = 'x-parse-page-param-'; export class PagesRouter extends PromiseRouter { + /** + * Constructs a PagesRouter. + * @param {Object} pages The pages options from the Parse Server configuration. + */ + constructor(pages = {}) { + super(); + this.pagesEndpoint = 'apps'; + this.pagesPath = pages.pagesPath + ? path.resolve('./', pages.pagesPath) + : path.resolve(__dirname, '../../public'); + this.mountPagesRoutes(); + } + verifyEmail(req) { const config = req.config; const { username, token: rawToken } = req.query; @@ -262,12 +265,12 @@ export class PagesRouter extends PromiseRouter { // Compose paths and URLs const defaultFile = page.defaultFile; - const defaultPath = defaultPagePath(defaultFile); - const defaultUrl = composePageUrl(defaultFile, config.publicServerURL); + const defaultPath = this.defaultPagePath(defaultFile); + const defaultUrl = this.composePageUrl(defaultFile, config.publicServerURL); // If custom page URL is set redirect to it without localization const customPageUrl = config.customPages[page.id]; - if (customPageUrl) { + if (customPageUrl && !Utils.isPath(customPageUrl)) { return this.redirectResponse(customPageUrl, params); } @@ -275,7 +278,7 @@ export class PagesRouter extends PromiseRouter { if (config.pages.enableLocalization && locale) { return Utils.getLocalizedPath(defaultPath, locale).then(({ path, subdir }) => redirect - ? this.redirectResponse(composePageUrl(defaultFile, config.publicServerURL, subdir), params) + ? this.redirectResponse(this.composePageUrl(defaultFile, config.publicServerURL, subdir), params) : this.pageResponse(path, params) ); } else { @@ -353,6 +356,19 @@ export class PagesRouter extends PromiseRouter { }; } + defaultPagePath(file) { + return path.join(this.pagesPath, file); + } + + composePageUrl(file, publicServerUrl, locale) { + let url = publicServerUrl; + url += url.endsWith('/') ? '' : '/'; + url += this.pagesEndpoint + '/'; + url += locale === undefined ? '' : locale + '/'; + url += file; + return url; + } + notFound() { return { text: 'Not found.', @@ -375,10 +391,10 @@ export class PagesRouter extends PromiseRouter { return Promise.resolve(); } - mountRoutes() { + mountPagesRoutes() { this.route( 'GET', - '/apps/:appId/verify_email', + `/${this.pagesEndpoint}/:appId/verify_email`, req => { this.setConfig(req); }, @@ -389,7 +405,7 @@ export class PagesRouter extends PromiseRouter { this.route( 'POST', - '/apps/:appId/resend_verification_email', + `/${this.pagesEndpoint}/:appId/resend_verification_email`, req => { this.setConfig(req); }, @@ -400,7 +416,7 @@ export class PagesRouter extends PromiseRouter { this.route( 'GET', - '/apps/choose_password', + `/${this.pagesEndpoint}/choose_password`, req => { this.setConfig(req); }, @@ -411,7 +427,7 @@ export class PagesRouter extends PromiseRouter { this.route( 'POST', - '/apps/:appId/request_password_reset', + `/${this.pagesEndpoint}/:appId/request_password_reset`, req => { this.setConfig(req); }, @@ -422,7 +438,7 @@ export class PagesRouter extends PromiseRouter { this.route( 'GET', - '/apps/:appId/request_password_reset', + `/${this.pagesEndpoint}/:appId/request_password_reset`, req => { this.setConfig(req); }, @@ -434,7 +450,7 @@ export class PagesRouter extends PromiseRouter { expressRouter() { const router = express.Router(); - router.use('/apps', express.static(publicPath)); + router.use(`/${this.pagesEndpoint}`, express.static(this.pagesPath)); router.use('/', super.expressRouter()); return router; } diff --git a/src/Utils.js b/src/Utils.js index 095e5d605f..3d58dec915 100644 --- a/src/Utils.js +++ b/src/Utils.js @@ -82,6 +82,15 @@ class Utils { return false; } } + + /** + * Evaluates whether a string is a file path (as opposed to a URL for example). + * @param {String} s The string to evaluate. + * @returns {Boolean} Returns true if the evaluated string is a path. + */ + static isPath(s) { + return /(^\/)|(^\.\/)|(^\.\.\/)/.test(s); + } } module.exports = Utils; From 3e00128c77c4e2b2d1b90d5ccee22f4fb05a86e9 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Wed, 20 Jan 2021 00:00:29 +0100 Subject: [PATCH 23/48] added configuration option for custom endpoint to pages --- spec/PagesRouter.spec.js | 18 ++++++++++++++++++ src/Config.js | 5 +++++ src/Options/Definitions.js | 8 +++++++- src/Options/docs.js | 1 + src/Options/index.js | 6 +++++- src/Routers/PagesRouter.js | 5 ++++- 6 files changed, 40 insertions(+), 3 deletions(-) diff --git a/spec/PagesRouter.spec.js b/spec/PagesRouter.spec.js index 1eadfd0901..07817e4eed 100644 --- a/spec/PagesRouter.spec.js +++ b/spec/PagesRouter.spec.js @@ -44,6 +44,17 @@ describe('Pages Router', () => { expect(response.status).toBe(200); }); + it('can load file from custom pages endpoint', async () => { + const _config = config; + _config.pages.pagesEndpoint = 'pages'; + await reconfigureServer(_config); + + const response = await request({ + url: `http://localhost:8378/1/pages/invalid_link.html` + }).catch(e => e); + expect(response.status).toBe(200); + }); + it('responds with 404 if publicServerURL is not confgured', async () => { await reconfigureServer({ appName: 'unused', @@ -208,6 +219,9 @@ describe('Pages Router', () => { expect(Config.get(Parse.applicationId).pages.pagesPath).toBe( Definitions.PagesOptions.pagesPath.default ); + expect(Config.get(Parse.applicationId).pages.pagesEndpoint).toBe( + Definitions.PagesOptions.pagesEndpoint.default + ); }); it('throws on invalid configuration', async () => { @@ -232,6 +246,10 @@ describe('Pages Router', () => { { pagesPath: 0 }, { pagesPath: {} }, { pagesPath: [] }, + { pagesEndpoint: true }, + { pagesEndpoint: 0 }, + { pagesEndpoint: {} }, + { pagesEndpoint: [] }, ]; for (const option of options) { await expectAsync(reconfigureServerWithPageOptions(option)).toBeRejected(); diff --git a/src/Config.js b/src/Config.js index ac59321881..dfca26f4d8 100644 --- a/src/Config.js +++ b/src/Config.js @@ -142,6 +142,11 @@ export class Config { } else if (!isString(pages.pagesPath)) { throw 'Parse Server option pages.pagesPath must be a string.'; } + if (pages.pagesEndpoint === undefined) { + pages.pagesEndpoint = PagesOptions.pagesEndpoint.default; + } else if (!isString(pages.pagesEndpoint)) { + throw 'Parse Server option pages.pagesEndpoint must be a string.'; + } } static validateIdempotencyOptions(idempotencyOptions) { diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index 2939847e00..3daa770f10 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -431,9 +431,15 @@ module.exports.PagesOptions = { "action": parsers.booleanParser, "default": false }, + "pagesEndpoint": { + "env": "PARSE_SERVER_PAGES_PAGES_ENDPOINT", + "help": "The API endoint for the pages. Default is the 'apps'.", + "default": "apps" + }, "pagesPath": { "env": "PARSE_SERVER_PAGES_PAGES_PATH", - "help": "The path to the pages directory; this also defines where the static endpoint '/apps' points to. Default is the './public/' directory." + "help": "The path to the pages directory; this also defines where the static endpoint '/apps' points to. Default is the './public/' directory.", + "default": "./public" } }; module.exports.CustomPagesOptions = { diff --git a/src/Options/docs.js b/src/Options/docs.js index 281b8518b1..5cb3c3d3b4 100644 --- a/src/Options/docs.js +++ b/src/Options/docs.js @@ -85,6 +85,7 @@ * @property {Boolean} enableLocalization Is true if pages should be localized; this has no effect on custom page redirects. * @property {Boolean} enableRouter Is true if the pages router should be enabled; this is required for any of the pages options to take effect. Caution, this is an experimental feature that may not be appropriate for production. * @property {Boolean} forceRedirect Is true if responses should always be redirects and never content, false if the response type should depend on the request type (GET request -> content response; POST request -> redirect response). + * @property {String} pagesEndpoint The API endoint for the pages. Default is the 'apps'. * @property {String} pagesPath The path to the pages directory; this also defines where the static endpoint '/apps' points to. Default is the './public/' directory. */ diff --git a/src/Options/index.js b/src/Options/index.js index c9cc9b4b2c..85da37ff55 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -239,8 +239,12 @@ export interface PagesOptions { /* Is true if responses should always be redirects and never content, false if the response type should depend on the request type (GET request -> content response; POST request -> redirect response). :DEFAULT: false */ forceRedirect: ?boolean; - /* The path to the pages directory; this also defines where the static endpoint '/apps' points to. Default is the './public/' directory. */ + /* The path to the pages directory; this also defines where the static endpoint '/apps' points to. Default is the './public/' directory. + :DEFAULT: ./public */ pagesPath: ?string; + /* The API endoint for the pages. Default is the 'apps'. + :DEFAULT: apps */ + pagesEndpoint: ?string; } export interface CustomPagesOptions { diff --git a/src/Routers/PagesRouter.js b/src/Routers/PagesRouter.js index bb4e90ee08..026209e7ef 100644 --- a/src/Routers/PagesRouter.js +++ b/src/Routers/PagesRouter.js @@ -38,7 +38,10 @@ export class PagesRouter extends PromiseRouter { */ constructor(pages = {}) { super(); - this.pagesEndpoint = 'apps'; + + this.pagesEndpoint = pages.pagesEndpoint + ? pages.pagesEndpoint + : 'apps'; this.pagesPath = pages.pagesPath ? path.resolve('./', pages.pagesPath) : path.resolve(__dirname, '../../public'); From 00547b3d7f199a0025533652d5630190bd863023 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Wed, 20 Jan 2021 00:08:44 +0100 Subject: [PATCH 24/48] fixed lint --- src/Options/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Options/index.js b/src/Options/index.js index 85da37ff55..c46ad35d06 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -239,7 +239,7 @@ export interface PagesOptions { /* Is true if responses should always be redirects and never content, false if the response type should depend on the request type (GET request -> content response; POST request -> redirect response). :DEFAULT: false */ forceRedirect: ?boolean; - /* The path to the pages directory; this also defines where the static endpoint '/apps' points to. Default is the './public/' directory. + /* The path to the pages directory; this also defines where the static endpoint '/apps' points to. Default is the './public/' directory. :DEFAULT: ./public */ pagesPath: ?string; /* The API endoint for the pages. Default is the 'apps'. From f2fcbd34aa58504199bcfa9978f4e5404ace40c2 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Wed, 20 Jan 2021 04:31:45 +0100 Subject: [PATCH 25/48] added tests --- spec/PagesRouter.spec.js | 98 +++++++++++++++++++++++++++++++++++++--- src/Config.js | 3 -- src/Page.js | 7 +-- src/Utils.js | 3 +- 4 files changed, 95 insertions(+), 16 deletions(-) diff --git a/spec/PagesRouter.spec.js b/spec/PagesRouter.spec.js index 07817e4eed..5e0e4f6a55 100644 --- a/spec/PagesRouter.spec.js +++ b/spec/PagesRouter.spec.js @@ -11,6 +11,7 @@ const { PagesRouter, pages, pageParams } = require('../lib/Routers/PagesRouter') describe('Pages Router', () => { describe('basic request', () => { const config = { + appId: 'test', appName: 'exampleAppname', publicServerURL: 'http://localhost:8378/1', pages: { enableRouter: true }, @@ -34,7 +35,7 @@ describe('Pages Router', () => { }); it('can load file from custom pages path', async () => { - const _config = config; + const _config = Object.assign({}, config); _config.pages.pagesPath = './public'; await reconfigureServer(_config); @@ -45,7 +46,7 @@ describe('Pages Router', () => { }); it('can load file from custom pages endpoint', async () => { - const _config = config; + const _config = Object.assign({}, config); _config.pages.pagesEndpoint = 'pages'; await reconfigureServer(_config); @@ -71,7 +72,7 @@ describe('Pages Router', () => { } }); - it('respones with 403 access denied with invalid appId', async () => { + it('responds with 403 access denied with invalid appId', async () => { const reqs = [ { url: 'http://localhost:8378/1/apps/invalid/verify_email', method: 'GET' }, { url: 'http://localhost:8378/1/apps/choose_password?id=invalid', method: 'GET' }, @@ -95,7 +96,23 @@ describe('Pages Router', () => { }); }); - it('should return missing password error on ajax request without password provided', async () => { + it('request_password_reset: responds with AJAX success', async () => { + spyOn(UserController.prototype, 'updatePassword').and.callFake(() => Promise.resolve()); + const res = await request({ + method: 'POST', + url: 'http://localhost:8378/1/apps/test/request_password_reset', + body: `new_password=user1&token=43634643&username=username`, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'X-Requested-With': 'XMLHttpRequest', + }, + followRedirects: false, + }).catch(e => e); + expect(res.status).toBe(200); + expect(res.text).toEqual('"Password successfully reset"'); + }); + + it('request_password_reset: responds with AJAX error on missing password', async () => { try { await request({ method: 'POST', @@ -113,7 +130,7 @@ describe('Pages Router', () => { } }); - it('should return missing username error on ajax request without username provided', async () => { + it('request_password_reset: responds with AJAX error on missing username', async () => { try { await request({ method: 'POST', @@ -131,7 +148,7 @@ describe('Pages Router', () => { } }); - it('should return missing token error on ajax request without token provided', async () => { + it('request_password_reset: responds with AJAX error on missing token', async () => { try { await request({ method: 'POST', @@ -153,6 +170,7 @@ describe('Pages Router', () => { describe('pages', () => { let router = new PagesRouter(); let req; + let goToPage; let pageResponse; let redirectResponse; let readFile; @@ -185,6 +203,7 @@ describe('Pages Router', () => { beforeEach(async () => { router = new PagesRouter(); readFile = spyOn(fs, 'readFile').and.callThrough(); + goToPage = spyOn(PagesRouter.prototype, 'goToPage').and.callThrough() pageResponse = spyOn(PagesRouter.prototype, 'pageResponse').and.callThrough() redirectResponse = spyOn(PagesRouter.prototype, 'redirectResponse').and.callThrough(); req = { @@ -617,5 +636,72 @@ describe('Pages Router', () => { expect(formResponse.text).toContain(`/${exampleLocale}/${pages.invalidLink.defaultFile}`); }); }); + + describe('failing with missing parameters', () => { + it('verifyEmail: throws on missing server configuration', async () => { + delete req.config; + const verifyEmail = (req) => (() => new PagesRouter().verifyEmail(req)).bind(null); + expect(verifyEmail(req)).toThrow(); + }); + + it('resendVerificationEmail: throws on missing server configuration', async () => { + delete req.config; + const resendVerificationEmail = (req) => (() => new PagesRouter().resendVerificationEmail(req)).bind(null); + expect(resendVerificationEmail(req)).toThrow(); + }); + + it('requestResetPassword: throws on missing server configuration', async () => { + delete req.config; + const requestResetPassword = (req) => (() => new PagesRouter().requestResetPassword(req)).bind(null); + expect(requestResetPassword(req)).toThrow(); + }); + + it('resetPassword: throws on missing server configuration', async () => { + delete req.config; + const resetPassword = (req) => (() => new PagesRouter().resetPassword(req)).bind(null); + expect(resetPassword(req)).toThrow(); + }); + + it('verifyEmail: responds with invalid link on missing appId', async () => { + req.query.token = 'exampleToken'; + req.query.username = 'exampleUsername'; + req.params = {}; + req.config.userController = { verifyEmail: () => Promise.reject() }; + const verifyEmail = (req) => new PagesRouter().verifyEmail(req); + + await verifyEmail(req); + expect(goToPage.calls.all()[0].args[1]).toBe(pages.invalidLink); + }); + + it('resetPassword: responds with page choose password with error message on failed password update', async () => { + req.body = { + token: 'exampleToken', + username: 'exampleUsername', + new_password: 'examplePassword', + }; + const error = 'exampleError'; + req.config.userController = { updatePassword: () => Promise.reject(error) }; + const resetPassword = (req) => new PagesRouter().resetPassword(req); + + await resetPassword(req); + expect(goToPage.calls.all()[0].args[1]).toBe(pages.choosePassword); + expect(goToPage.calls.all()[0].args[2].error).toBe(error); + }); + + it('resetPassword: responds with AJAX error with error message on failed password update', async () => { + req.xhr = true; + req.body = { + token: 'exampleToken', + username: 'exampleUsername', + new_password: 'examplePassword', + }; + const error = 'exampleError'; + req.config.userController = { updatePassword: () => Promise.reject(error) }; + const resetPassword = (req) => new PagesRouter().resetPassword(req).catch(e => e); + + const response = await resetPassword(req); + expect(response.code).toBe(Parse.Error.OTHER_CAUSE); + }); + }); }); }); diff --git a/src/Config.js b/src/Config.js index dfca26f4d8..f772ff3db6 100644 --- a/src/Config.js +++ b/src/Config.js @@ -116,9 +116,6 @@ export class Config { } static validatePagesOptions(pages) { - if (pages === undefined) { - return; - } if (Object.prototype.toString.call(pages) !== '[object Object]') { throw 'Parse Server option pages must be an object.'; } diff --git a/src/Page.js b/src/Page.js index 2ed0339066..006e72044b 100644 --- a/src/Page.js +++ b/src/Page.js @@ -12,14 +12,9 @@ export class Page { * @param {String} params.defaultFile The page file name. * @returns {Page} The page. */ - constructor(params) { + constructor(params = {}) { const { id, defaultFile } = params; - // Ensure requried parameters - if ([id, defaultFile].includes(undefined)) { - throw 'missing parameters'; - } - this.id = id; this.defaultFile = defaultFile; } diff --git a/src/Utils.js b/src/Utils.js index 3d58dec915..89753f5cac 100644 --- a/src/Utils.js +++ b/src/Utils.js @@ -84,7 +84,8 @@ class Utils { } /** - * Evaluates whether a string is a file path (as opposed to a URL for example). + * @function isPath + * @description Evaluates whether a string is a file path (as opposed to a URL for example). * @param {String} s The string to evaluate. * @returns {Boolean} Returns true if the evaluated string is a path. */ From 2a385323978b90dc047170db35c8779010612363 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Wed, 20 Jan 2021 18:12:00 +0100 Subject: [PATCH 26/48] added a distinct page for invalid password reset link --- public/de-AT/invalid_password_reset_link.html | 19 +++++++++++++ public/de/invalid_password_reset_link.html | 19 +++++++++++++ public/invalid_link.html | 5 ++-- public/invalid_password_reset_link.html | 19 +++++++++++++ spec/PagesRouter.spec.js | 8 +++--- src/Options/Definitions.js | 4 +++ src/Options/docs.js | 1 + src/Options/index.js | 14 +++++----- src/Routers/PagesRouter.js | 27 +++++++++---------- 9 files changed, 89 insertions(+), 27 deletions(-) create mode 100644 public/de-AT/invalid_password_reset_link.html create mode 100644 public/de/invalid_password_reset_link.html create mode 100644 public/invalid_password_reset_link.html diff --git a/public/de-AT/invalid_password_reset_link.html b/public/de-AT/invalid_password_reset_link.html new file mode 100644 index 0000000000..98653a1e40 --- /dev/null +++ b/public/de-AT/invalid_password_reset_link.html @@ -0,0 +1,19 @@ + + + + + + Password Reset + + + +

{{appName}}

+

Invalid password reset link!

+ + + diff --git a/public/de/invalid_password_reset_link.html b/public/de/invalid_password_reset_link.html new file mode 100644 index 0000000000..98653a1e40 --- /dev/null +++ b/public/de/invalid_password_reset_link.html @@ -0,0 +1,19 @@ + + + + + + Password Reset + + + +

{{appName}}

+

Invalid password reset link!

+ + + diff --git a/public/invalid_link.html b/public/invalid_link.html index 07a204a8a7..f949f30f89 100644 --- a/public/invalid_link.html +++ b/public/invalid_link.html @@ -1,9 +1,8 @@ diff --git a/public/invalid_password_reset_link.html b/public/invalid_password_reset_link.html new file mode 100644 index 0000000000..98653a1e40 --- /dev/null +++ b/public/invalid_password_reset_link.html @@ -0,0 +1,19 @@ + + + + + + Password Reset + + + +

{{appName}}

+

Invalid password reset link!

+ + + diff --git a/spec/PagesRouter.spec.js b/spec/PagesRouter.spec.js index 5e0e4f6a55..e3d740cc46 100644 --- a/spec/PagesRouter.spec.js +++ b/spec/PagesRouter.spec.js @@ -398,7 +398,7 @@ describe('Pages Router', () => { method: 'POST', }); expect(response.status).toEqual(303); - expect(response.headers.location).toContain('http://localhost:8378/1/apps/de-AT/invalid_link.html'); + expect(response.headers.location).toContain('http://localhost:8378/1/apps/de-AT/invalid_password_reset_link.html'); }); it('responds to GET request with content response', async () => { @@ -491,7 +491,7 @@ describe('Pages Router', () => { const pagePath = pageResponse.calls.all()[0].args[0]; expect(pagePath).toMatch( - new RegExp(`\/${exampleLocale}\/${pages.invalidLink.defaultFile}`) + new RegExp(`\/${exampleLocale}\/${pages.invalidPasswordResetLink.defaultFile}`) ); }); @@ -633,7 +633,7 @@ describe('Pages Router', () => { followRedirects: false, }); expect(formResponse.status).toEqual(303); - expect(formResponse.text).toContain(`/${exampleLocale}/${pages.invalidLink.defaultFile}`); + expect(formResponse.text).toContain(`/${exampleLocale}/${pages.invalidVerificationLink.defaultFile}`); }); }); @@ -670,7 +670,7 @@ describe('Pages Router', () => { const verifyEmail = (req) => new PagesRouter().verifyEmail(req); await verifyEmail(req); - expect(goToPage.calls.all()[0].args[1]).toBe(pages.invalidLink); + expect(goToPage.calls.all()[0].args[1]).toBe(pages.invalidVerificationLink); }); it('resetPassword: responds with page choose password with error message on failed password update', async () => { diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index 3daa770f10..4c051585a8 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -451,6 +451,10 @@ module.exports.CustomPagesOptions = { "env": "PARSE_SERVER_CUSTOM_PAGES_INVALID_LINK", "help": "invalid link page path" }, + "invalidPasswordResetLink": { + "env": "PARSE_SERVER_CUSTOM_PAGES_INVALID_PASSWORD_RESET_LINK", + "help": "invalid password reset link page path" + }, "invalidVerificationLink": { "env": "PARSE_SERVER_CUSTOM_PAGES_INVALID_VERIFICATION_LINK", "help": "invalid verification link page path" diff --git a/src/Options/docs.js b/src/Options/docs.js index 5cb3c3d3b4..533bf8c15b 100644 --- a/src/Options/docs.js +++ b/src/Options/docs.js @@ -93,6 +93,7 @@ * @interface CustomPagesOptions * @property {String} choosePassword choose password page path * @property {String} invalidLink invalid link page path + * @property {String} invalidPasswordResetLink invalid password reset link page path * @property {String} invalidVerificationLink invalid verification link page path * @property {String} linkSendFail verification link send fail page path * @property {String} linkSendSuccess verification link send success page path diff --git a/src/Options/index.js b/src/Options/index.js index c46ad35d06..ac276cb3bd 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -250,18 +250,20 @@ export interface PagesOptions { export interface CustomPagesOptions { /* invalid link page path */ invalidLink: ?string; - /* verify email success page path */ - verifyEmailSuccess: ?string; - /* invalid verification link page path */ - invalidVerificationLink: ?string; - /* verification link send success page path */ - linkSendSuccess: ?string; /* verification link send fail page path */ linkSendFail: ?string; /* choose password page path */ choosePassword: ?string; + /* verification link send success page path */ + linkSendSuccess: ?string; + /* verify email success page path */ + verifyEmailSuccess: ?string; /* password reset success page path */ passwordResetSuccess: ?string; + /* invalid verification link page path */ + invalidVerificationLink: ?string; + /* invalid password reset link page path */ + invalidPasswordResetLink: ?string; /* for masking user-facing pages */ parseFrameURL: ?string; } diff --git a/src/Routers/PagesRouter.js b/src/Routers/PagesRouter.js index 026209e7ef..8264532d33 100644 --- a/src/Routers/PagesRouter.js +++ b/src/Routers/PagesRouter.js @@ -17,6 +17,7 @@ const pages = Object.freeze({ verifyEmailSuccess: new Page({ id: 'verifyEmailSuccess', defaultFile: 'verify_email_success.html' }), passwordResetSuccess: new Page({ id: 'passwordResetSuccess', defaultFile: 'password_reset_success.html' }), invalidVerificationLink: new Page({ id: 'invalidVerificationLink', defaultFile: 'invalid_verification_link.html' }), + invalidPasswordResetLink: new Page({ id: 'invalidPasswordResetLink', defaultFile: 'invalid_password_reset_link.html' }), }); // All page parameters for reference to be used as template placeholders or query params const pageParams = Object.freeze({ @@ -58,7 +59,7 @@ export class PagesRouter extends PromiseRouter { } if (!token || !username) { - return this.goToPage(req, pages.invalidLink); + return this.goToPage(req, pages.invalidVerificationLink); } const userController = config.userController; @@ -70,15 +71,10 @@ export class PagesRouter extends PromiseRouter { return this.goToPage(req, pages.verifyEmailSuccess, params); }, () => { - if (req.query.username && req.params.appId) { - const params = { - [pageParams.appId]: req.params.appId, - [pageParams.username]: req.query.username, - }; - return this.goToPage(req, pages.invalidVerificationLink, params); - } else { - return this.goToPage(req, pages.invalidLink); - } + const params = { + [pageParams.username]: username, + }; + return this.goToPage(req, pages.invalidVerificationLink, params); } ); } @@ -92,7 +88,7 @@ export class PagesRouter extends PromiseRouter { } if (!username) { - return this.goToPage(req, pages.invalidLink); + return this.goToPage(req, pages.invalidVerificationLink); } const userController = config.userController; @@ -130,7 +126,7 @@ export class PagesRouter extends PromiseRouter { const token = rawToken && typeof rawToken !== 'string' ? rawToken.toString() : rawToken; if (!username || !token) { - return this.goToPage(req, pages.invalidLink); + return this.goToPage(req, pages.invalidPasswordResetLink); } return config.userController.checkResetTokenValidity(username, token).then( @@ -144,7 +140,10 @@ export class PagesRouter extends PromiseRouter { return this.goToPage(req, pages.choosePassword, params); }, () => { - return this.goToPage(req, pages.invalidLink); + const params = { + [pageParams.username]: username, + }; + return this.goToPage(req, pages.invalidPasswordResetLink, params); } ); } @@ -160,7 +159,7 @@ export class PagesRouter extends PromiseRouter { const token = rawToken && typeof rawToken !== 'string' ? rawToken.toString() : rawToken; if ((!username || !token || !new_password) && req.xhr === false) { - return this.goToPage(req, pages.invalidLink); + return this.goToPage(req, pages.invalidPasswordResetLink); } if (!username) { From 3d3db6a3d54d35668726cbb40cbbed4d89ce3e20 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Wed, 20 Jan 2021 20:54:41 +0100 Subject: [PATCH 27/48] renamed generic page invalidLink to expiredVerificationLink --- public/de-AT/choose_password.html | 1 + ...nk.html => expired_verification_link.html} | 9 +++- public/de-AT/invalid_verification_link.html | 14 ++--- public/de/choose_password.html | 3 +- ...nk.html => expired_verification_link.html} | 9 +++- public/de/invalid_verification_link.html | 14 ++--- public/expired_verification_link.html | 24 +++++++++ public/invalid_link.html | 18 ------- public/invalid_verification_link.html | 14 ++--- spec/PagesRouter.spec.js | 53 +++++++++---------- src/Options/index.js | 2 + src/Routers/PagesRouter.js | 4 +- 12 files changed, 86 insertions(+), 79 deletions(-) rename public/de-AT/{invalid_link.html => expired_verification_link.html} (55%) rename public/de/{invalid_link.html => expired_verification_link.html} (55%) create mode 100644 public/expired_verification_link.html delete mode 100644 public/invalid_link.html diff --git a/public/de-AT/choose_password.html b/public/de-AT/choose_password.html index f096693cf3..49cb65b1aa 100644 --- a/public/de-AT/choose_password.html +++ b/public/de-AT/choose_password.html @@ -11,6 +11,7 @@ add an 'error' query parameter. --> + Password Reset diff --git a/public/de-AT/invalid_link.html b/public/de-AT/expired_verification_link.html similarity index 55% rename from public/de-AT/invalid_link.html rename to public/de-AT/expired_verification_link.html index 07a204a8a7..88a8f06452 100644 --- a/public/de-AT/invalid_link.html +++ b/public/de-AT/expired_verification_link.html @@ -8,12 +8,17 @@ - Invalid Link + Email Verification

{{appName}}

-

Invalid Link

+

Expired verification link!

+
+ + + +
diff --git a/public/de-AT/invalid_verification_link.html b/public/de-AT/invalid_verification_link.html index 0f45d6b6b1..ccdbf12a56 100644 --- a/public/de-AT/invalid_verification_link.html +++ b/public/de-AT/invalid_verification_link.html @@ -1,9 +1,10 @@ @@ -14,11 +15,6 @@

{{appName}}

Invalid verification link!

-
- - - -
diff --git a/public/de/choose_password.html b/public/de/choose_password.html index 53c4eed65c..49cb65b1aa 100644 --- a/public/de/choose_password.html +++ b/public/de/choose_password.html @@ -11,6 +11,7 @@ add an 'error' query parameter. --> + Password Reset @@ -21,7 +22,7 @@

Reset Your Password

You can set a new Password for your account: {{username}}


-

{{error}}

+

{{error}}

diff --git a/public/de/invalid_link.html b/public/de/expired_verification_link.html similarity index 55% rename from public/de/invalid_link.html rename to public/de/expired_verification_link.html index 07a204a8a7..88a8f06452 100644 --- a/public/de/invalid_link.html +++ b/public/de/expired_verification_link.html @@ -8,12 +8,17 @@ - Invalid Link + Email Verification

{{appName}}

-

Invalid Link

+

Expired verification link!

+ + + + +
diff --git a/public/de/invalid_verification_link.html b/public/de/invalid_verification_link.html index 0f45d6b6b1..ccdbf12a56 100644 --- a/public/de/invalid_verification_link.html +++ b/public/de/invalid_verification_link.html @@ -1,9 +1,10 @@ @@ -14,11 +15,6 @@

{{appName}}

Invalid verification link!

-
- - - -
diff --git a/public/expired_verification_link.html b/public/expired_verification_link.html new file mode 100644 index 0000000000..88a8f06452 --- /dev/null +++ b/public/expired_verification_link.html @@ -0,0 +1,24 @@ + + + + + + Email Verification + + + +

{{appName}}

+

Expired verification link!

+
+ + + +
+ + + diff --git a/public/invalid_link.html b/public/invalid_link.html deleted file mode 100644 index f949f30f89..0000000000 --- a/public/invalid_link.html +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - Invalid Link - - - -

{{appName}}

-

Invalid Link

- - - diff --git a/public/invalid_verification_link.html b/public/invalid_verification_link.html index 0f45d6b6b1..ccdbf12a56 100644 --- a/public/invalid_verification_link.html +++ b/public/invalid_verification_link.html @@ -1,9 +1,10 @@ @@ -14,11 +15,6 @@

{{appName}}

Invalid verification link!

-
- - - -
diff --git a/spec/PagesRouter.spec.js b/spec/PagesRouter.spec.js index e3d740cc46..43ecacad55 100644 --- a/spec/PagesRouter.spec.js +++ b/spec/PagesRouter.spec.js @@ -23,7 +23,7 @@ describe('Pages Router', () => { it('responds with file content on direct page request', async () => { const urls = [ - 'http://localhost:8378/1/apps/invalid_link.html', + 'http://localhost:8378/1/apps/invalid_verification_link.html', 'http://localhost:8378/1/apps/choose_password?appId=test', 'http://localhost:8378/1/apps/verify_email_success.html', 'http://localhost:8378/1/apps/password_reset_success.html', @@ -40,7 +40,7 @@ describe('Pages Router', () => { await reconfigureServer(_config); const response = await request({ - url: 'http://localhost:8378/1/apps/invalid_link.html' + url: 'http://localhost:8378/1/apps/invalid_verification_link.html' }).catch(e => e); expect(response.status).toBe(200); }); @@ -51,7 +51,7 @@ describe('Pages Router', () => { await reconfigureServer(_config); const response = await request({ - url: `http://localhost:8378/1/pages/invalid_link.html` + url: `http://localhost:8378/1/pages/invalid_verification_link.html` }).catch(e => e); expect(response.status).toBe(200); }); @@ -278,7 +278,7 @@ describe('Pages Router', () => { describe('placeholders', () => { it('replaces placeholder in response content', async () => { - await expectAsync(router.goToPage(req, pages.invalidLink)).toBeResolved(); + await expectAsync(router.goToPage(req, pages.invalidPasswordResetLink)).toBeResolved(); expect(readFile.calls.all()[0].returnValue).toBeDefined(); const originalContent = await readFile.calls.all()[0].returnValue; @@ -309,81 +309,81 @@ describe('Pages Router', () => { it('returns default file if localization is disabled', async () => { delete req.config.pages.enableLocalization; - await expectAsync(router.goToPage(req, pages.invalidLink)).toBeResolved(); + await expectAsync(router.goToPage(req, pages.invalidPasswordResetLink)).toBeResolved(); expect(pageResponse.calls.all()[0].args[0]).toBeDefined(); expect(pageResponse.calls.all()[0].args[0]).not.toMatch( - new RegExp(`\/de(-AT)?\/${pages.invalidLink.defaultFile}`) + new RegExp(`\/de(-AT)?\/${pages.invalidPasswordResetLink.defaultFile}`) ); }); it('returns default file if no locale is specified', async () => { delete req.query.locale; - await expectAsync(router.goToPage(req, pages.invalidLink)).toBeResolved(); + await expectAsync(router.goToPage(req, pages.invalidPasswordResetLink)).toBeResolved(); expect(pageResponse.calls.all()[0].args[0]).toBeDefined(); expect(pageResponse.calls.all()[0].args[0]).not.toMatch( - new RegExp(`\/de(-AT)?\/${pages.invalidLink.defaultFile}`) + new RegExp(`\/de(-AT)?\/${pages.invalidPasswordResetLink.defaultFile}`) ); }); it('returns custom page regardless of localization enabled', async () => { - req.config.customPages = { invalidLink: 'http://invalid-link.example.com' }; + req.config.customPages = { invalidPasswordResetLink: 'http://invalid-link.example.com' }; - await expectAsync(router.goToPage(req, pages.invalidLink)).toBeResolved(); + await expectAsync(router.goToPage(req, pages.invalidPasswordResetLink)).toBeResolved(); expect(pageResponse).not.toHaveBeenCalled(); - expect(redirectResponse.calls.all()[0].args[0]).toBe(req.config.customPages.invalidLink); + expect(redirectResponse.calls.all()[0].args[0]).toBe(req.config.customPages.invalidPasswordResetLink); }); it('returns file for locale match', async () => { - await expectAsync(router.goToPage(req, pages.invalidLink)).toBeResolved(); + await expectAsync(router.goToPage(req, pages.invalidPasswordResetLink)).toBeResolved(); expect(pageResponse.calls.all()[0].args[0]).toBeDefined(); expect(pageResponse.calls.all()[0].args[0]).toMatch( - new RegExp(`\/${req.query.locale}\/${pages.invalidLink.defaultFile}`) + new RegExp(`\/${req.query.locale}\/${pages.invalidPasswordResetLink.defaultFile}`) ); }); it('returns file for language match', async () => { // Pretend no locale matching file exists spyOn(Utils, 'fileExists').and.callFake(async path => { - return !path.includes(`/${req.query.locale}/${pages.invalidLink.defaultFile}`); + return !path.includes(`/${req.query.locale}/${pages.invalidPasswordResetLink.defaultFile}`); }); - await expectAsync(router.goToPage(req, pages.invalidLink)).toBeResolved(); + await expectAsync(router.goToPage(req, pages.invalidPasswordResetLink)).toBeResolved(); expect(pageResponse.calls.all()[0].args[0]).toBeDefined(); expect(pageResponse.calls.all()[0].args[0]).toMatch( - new RegExp(`\/de\/${pages.invalidLink.defaultFile}`) + new RegExp(`\/de\/${pages.invalidPasswordResetLink.defaultFile}`) ); }); it('returns default file for neither locale nor language match', async () => { req.query.locale = 'yo-LO'; - await expectAsync(router.goToPage(req, pages.invalidLink)).toBeResolved(); + await expectAsync(router.goToPage(req, pages.invalidPasswordResetLink)).toBeResolved(); expect(pageResponse.calls.all()[0].args[0]).toBeDefined(); expect(pageResponse.calls.all()[0].args[0]).not.toMatch( - new RegExp(`\/yo(-LO)?\/${pages.invalidLink.defaultFile}`) + new RegExp(`\/yo(-LO)?\/${pages.invalidPasswordResetLink.defaultFile}`) ); }); it('returns a file for GET request', async () => { - await expectAsync(router.goToPage(req, pages.invalidLink)).toBeResolved(); + await expectAsync(router.goToPage(req, pages.invalidPasswordResetLink)).toBeResolved(); expect(pageResponse).toHaveBeenCalled(); expect(redirectResponse).not.toHaveBeenCalled(); }); it('returns a redirect for POST request', async () => { req.method = 'POST'; - await expectAsync(router.goToPage(req, pages.invalidLink)).toBeResolved(); + await expectAsync(router.goToPage(req, pages.invalidPasswordResetLink)).toBeResolved(); expect(pageResponse).not.toHaveBeenCalled(); expect(redirectResponse).toHaveBeenCalled(); }); it('returns a redirect for custom pages for GET and POST request', async () => { - req.config.customPages = { invalidLink: 'http://invalid-link.example.com' }; + req.config.customPages = { invalidPasswordResetLink: 'http://invalid-link.example.com' }; for (const method of ['GET', 'POST']) { req.method = method; - await expectAsync(router.goToPage(req, pages.invalidLink)).toBeResolved(); + await expectAsync(router.goToPage(req, pages.invalidPasswordResetLink)).toBeResolved(); expect(pageResponse).not.toHaveBeenCalled(); expect(redirectResponse).toHaveBeenCalled(); } @@ -550,7 +550,7 @@ describe('Pages Router', () => { expect(username).toBeDefined(); expect(publicServerUrl).toBeDefined(); expect(invalidVerificationPagePath).toMatch( - new RegExp(`\/${exampleLocale}\/${pages.invalidVerificationLink.defaultFile}`) + new RegExp(`\/${exampleLocale}\/${pages.expiredVerificationLink.defaultFile}`) ); const formUrl = `${publicServerUrl}/apps/${appId}/resend_verification_email`; @@ -598,7 +598,7 @@ describe('Pages Router', () => { expect(username).toBeDefined(); expect(publicServerUrl).toBeDefined(); expect(invalidVerificationPagePath).toMatch( - new RegExp(`\/${exampleLocale}\/${pages.invalidVerificationLink.defaultFile}`) + new RegExp(`\/${exampleLocale}\/${pages.expiredVerificationLink.defaultFile}`) ); spyOn(UserController.prototype, 'resendVerificationEmail').and.callFake(() => Promise.reject( @@ -620,7 +620,7 @@ describe('Pages Router', () => { expect(formResponse.text).toContain(`/${locale}/${pages.linkSendFail.defaultFile}`); }); - it('localizes end-to-end for verify email: invalid link', async () => { + it('localizes end-to-end for resend verification email: invalid link', async () => { await reconfigureServer(config); const formUrl = `${config.publicServerURL}/apps/${config.appId}/resend_verification_email`; const formResponse = await request({ @@ -662,9 +662,8 @@ describe('Pages Router', () => { expect(resetPassword(req)).toThrow(); }); - it('verifyEmail: responds with invalid link on missing appId', async () => { + it('verifyEmail: responds with invalid link on missing username', async () => { req.query.token = 'exampleToken'; - req.query.username = 'exampleUsername'; req.params = {}; req.config.userController = { verifyEmail: () => Promise.reject() }; const verifyEmail = (req) => new PagesRouter().verifyEmail(req); diff --git a/src/Options/index.js b/src/Options/index.js index ac276cb3bd..d3c2084787 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -262,6 +262,8 @@ export interface CustomPagesOptions { passwordResetSuccess: ?string; /* invalid verification link page path */ invalidVerificationLink: ?string; + /* expired verification link page path */ + expiredVerificationLink: ?string; /* invalid password reset link page path */ invalidPasswordResetLink: ?string; /* for masking user-facing pages */ diff --git a/src/Routers/PagesRouter.js b/src/Routers/PagesRouter.js index 8264532d33..4843e41d1d 100644 --- a/src/Routers/PagesRouter.js +++ b/src/Routers/PagesRouter.js @@ -10,13 +10,13 @@ import Page from '../Page'; // All pages with custom page key for reference and file name const pages = Object.freeze({ - invalidLink: new Page({ id: 'invalidLink', defaultFile: 'invalid_link.html' }), linkSendFail: new Page({ id: 'linkSendFail', defaultFile: 'link_send_fail.html' }), choosePassword: new Page({ id: 'choosePassword', defaultFile: 'choose_password.html' }), linkSendSuccess: new Page({ id: 'linkSendSuccess', defaultFile: 'link_send_success.html' }), verifyEmailSuccess: new Page({ id: 'verifyEmailSuccess', defaultFile: 'verify_email_success.html' }), passwordResetSuccess: new Page({ id: 'passwordResetSuccess', defaultFile: 'password_reset_success.html' }), invalidVerificationLink: new Page({ id: 'invalidVerificationLink', defaultFile: 'invalid_verification_link.html' }), + expiredVerificationLink: new Page({ id: 'expiredVerificationLink', defaultFile: 'expired_verification_link.html' }), invalidPasswordResetLink: new Page({ id: 'invalidPasswordResetLink', defaultFile: 'invalid_password_reset_link.html' }), }); // All page parameters for reference to be used as template placeholders or query params @@ -74,7 +74,7 @@ export class PagesRouter extends PromiseRouter { const params = { [pageParams.username]: username, }; - return this.goToPage(req, pages.invalidVerificationLink, params); + return this.goToPage(req, pages.expiredVerificationLink, params); } ); } From 373b6dfb25b0f147f17ec7f149a2c04c3d8d0e2f Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Wed, 20 Jan 2021 20:54:56 +0100 Subject: [PATCH 28/48] improved HTML files documentation --- public/expired_verification_link.html | 10 +++++----- public/invalid_password_reset_link.html | 10 +++++----- public/invalid_verification_link.html | 13 +++++++------ public/link_send_fail.html | 11 ++++++----- public/link_send_success.html | 9 ++++----- public/password_reset_success.html | 6 ++---- public/verify_email_success.html | 5 ++--- 7 files changed, 31 insertions(+), 33 deletions(-) diff --git a/public/expired_verification_link.html b/public/expired_verification_link.html index 88a8f06452..bea8d949fb 100644 --- a/public/expired_verification_link.html +++ b/public/expired_verification_link.html @@ -1,10 +1,10 @@ + This page is displayed when a user opens a verify email link with a security + token that is expired or incorrect. This can either mean the user has clicked + on a stale link (i.e. re-clicked on the link) or this could be a sign of a + malicious user trying to tamper with your app. +--> diff --git a/public/invalid_password_reset_link.html b/public/invalid_password_reset_link.html index 98653a1e40..5db34de15e 100644 --- a/public/invalid_password_reset_link.html +++ b/public/invalid_password_reset_link.html @@ -1,10 +1,10 @@ + This page is displayed when a user opens a password reset link with parameters + that are missing or incorrect. This can either mean the user has incorrectly + entered a link or this could be a sign of a malicious user trying to tamper + with your app. +--> diff --git a/public/invalid_verification_link.html b/public/invalid_verification_link.html index ccdbf12a56..3a99265a66 100644 --- a/public/invalid_verification_link.html +++ b/public/invalid_verification_link.html @@ -1,11 +1,12 @@ + This page is displayed when a user opens a verify email link with parameters + that are missing or incorrect. This can either mean the user has incorrectly + entered a link or this could be a sign of a malicious user trying to tamper + with your app. + If the link contains an expired security token (or the email has already + been verified), this page is not displayed, there is another page for that. +--> diff --git a/public/link_send_fail.html b/public/link_send_fail.html index 8e1ba0ca06..afd59407b8 100644 --- a/public/link_send_fail.html +++ b/public/link_send_fail.html @@ -1,10 +1,11 @@ + This page is displayed when a user opens a verify email link with a security + token that is expired or incorrect, then requests to receive another link, + but it fails because the username is invalid or the email has already been + verified. This can either mean the user has previously verified the email + or this could be a sign of a malicious user trying to tamper with your app. +--> diff --git a/public/link_send_success.html b/public/link_send_success.html index fa2d7f2ee5..192a33142b 100644 --- a/public/link_send_success.html +++ b/public/link_send_success.html @@ -1,10 +1,9 @@ + This page is displayed when a user opens a verify email link with a + security token that is expired, then requests to receive another email + with a new link and the email is sent successfully. +--> diff --git a/public/password_reset_success.html b/public/password_reset_success.html index 7a90901f87..4b4e4c7104 100644 --- a/public/password_reset_success.html +++ b/public/password_reset_success.html @@ -1,9 +1,7 @@ diff --git a/public/verify_email_success.html b/public/verify_email_success.html index 3d643e4f42..e8db182551 100644 --- a/public/verify_email_success.html +++ b/public/verify_email_success.html @@ -1,8 +1,7 @@ From ff3c877ce6958ccdf4bd24fd51cd871187a4b93e Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Wed, 20 Jan 2021 21:07:00 +0100 Subject: [PATCH 29/48] improved HTML files documentation --- public/de-AT/expired_verification_link.html | 10 +++++----- public/de-AT/invalid_password_reset_link.html | 10 +++++----- public/de-AT/invalid_verification_link.html | 13 +++++++------ public/de-AT/link_send_fail.html | 11 ++++++----- public/de-AT/link_send_success.html | 9 ++++----- public/de-AT/password_reset_success.html | 6 ++---- public/de-AT/verify_email_success.html | 5 ++--- public/de/expired_verification_link.html | 10 +++++----- public/de/invalid_password_reset_link.html | 10 +++++----- public/de/invalid_verification_link.html | 13 +++++++------ public/de/link_send_fail.html | 11 ++++++----- public/de/link_send_success.html | 9 ++++----- public/de/password_reset_success.html | 6 ++---- public/de/verify_email_success.html | 5 ++--- 14 files changed, 62 insertions(+), 66 deletions(-) diff --git a/public/de-AT/expired_verification_link.html b/public/de-AT/expired_verification_link.html index 88a8f06452..bea8d949fb 100644 --- a/public/de-AT/expired_verification_link.html +++ b/public/de-AT/expired_verification_link.html @@ -1,10 +1,10 @@ + This page is displayed when a user opens a verify email link with a security + token that is expired or incorrect. This can either mean the user has clicked + on a stale link (i.e. re-clicked on the link) or this could be a sign of a + malicious user trying to tamper with your app. +--> diff --git a/public/de-AT/invalid_password_reset_link.html b/public/de-AT/invalid_password_reset_link.html index 98653a1e40..5db34de15e 100644 --- a/public/de-AT/invalid_password_reset_link.html +++ b/public/de-AT/invalid_password_reset_link.html @@ -1,10 +1,10 @@ + This page is displayed when a user opens a password reset link with parameters + that are missing or incorrect. This can either mean the user has incorrectly + entered a link or this could be a sign of a malicious user trying to tamper + with your app. +--> diff --git a/public/de-AT/invalid_verification_link.html b/public/de-AT/invalid_verification_link.html index ccdbf12a56..3a99265a66 100644 --- a/public/de-AT/invalid_verification_link.html +++ b/public/de-AT/invalid_verification_link.html @@ -1,11 +1,12 @@ + This page is displayed when a user opens a verify email link with parameters + that are missing or incorrect. This can either mean the user has incorrectly + entered a link or this could be a sign of a malicious user trying to tamper + with your app. + If the link contains an expired security token (or the email has already + been verified), this page is not displayed, there is another page for that. +--> diff --git a/public/de-AT/link_send_fail.html b/public/de-AT/link_send_fail.html index 8e1ba0ca06..afd59407b8 100644 --- a/public/de-AT/link_send_fail.html +++ b/public/de-AT/link_send_fail.html @@ -1,10 +1,11 @@ + This page is displayed when a user opens a verify email link with a security + token that is expired or incorrect, then requests to receive another link, + but it fails because the username is invalid or the email has already been + verified. This can either mean the user has previously verified the email + or this could be a sign of a malicious user trying to tamper with your app. +--> diff --git a/public/de-AT/link_send_success.html b/public/de-AT/link_send_success.html index fa2d7f2ee5..192a33142b 100644 --- a/public/de-AT/link_send_success.html +++ b/public/de-AT/link_send_success.html @@ -1,10 +1,9 @@ + This page is displayed when a user opens a verify email link with a + security token that is expired, then requests to receive another email + with a new link and the email is sent successfully. +--> diff --git a/public/de-AT/password_reset_success.html b/public/de-AT/password_reset_success.html index 7a90901f87..4b4e4c7104 100644 --- a/public/de-AT/password_reset_success.html +++ b/public/de-AT/password_reset_success.html @@ -1,9 +1,7 @@ diff --git a/public/de-AT/verify_email_success.html b/public/de-AT/verify_email_success.html index 3d643e4f42..e8db182551 100644 --- a/public/de-AT/verify_email_success.html +++ b/public/de-AT/verify_email_success.html @@ -1,8 +1,7 @@ diff --git a/public/de/expired_verification_link.html b/public/de/expired_verification_link.html index 88a8f06452..bea8d949fb 100644 --- a/public/de/expired_verification_link.html +++ b/public/de/expired_verification_link.html @@ -1,10 +1,10 @@ + This page is displayed when a user opens a verify email link with a security + token that is expired or incorrect. This can either mean the user has clicked + on a stale link (i.e. re-clicked on the link) or this could be a sign of a + malicious user trying to tamper with your app. +--> diff --git a/public/de/invalid_password_reset_link.html b/public/de/invalid_password_reset_link.html index 98653a1e40..5db34de15e 100644 --- a/public/de/invalid_password_reset_link.html +++ b/public/de/invalid_password_reset_link.html @@ -1,10 +1,10 @@ + This page is displayed when a user opens a password reset link with parameters + that are missing or incorrect. This can either mean the user has incorrectly + entered a link or this could be a sign of a malicious user trying to tamper + with your app. +--> diff --git a/public/de/invalid_verification_link.html b/public/de/invalid_verification_link.html index ccdbf12a56..3a99265a66 100644 --- a/public/de/invalid_verification_link.html +++ b/public/de/invalid_verification_link.html @@ -1,11 +1,12 @@ + This page is displayed when a user opens a verify email link with parameters + that are missing or incorrect. This can either mean the user has incorrectly + entered a link or this could be a sign of a malicious user trying to tamper + with your app. + If the link contains an expired security token (or the email has already + been verified), this page is not displayed, there is another page for that. +--> diff --git a/public/de/link_send_fail.html b/public/de/link_send_fail.html index 8e1ba0ca06..afd59407b8 100644 --- a/public/de/link_send_fail.html +++ b/public/de/link_send_fail.html @@ -1,10 +1,11 @@ + This page is displayed when a user opens a verify email link with a security + token that is expired or incorrect, then requests to receive another link, + but it fails because the username is invalid or the email has already been + verified. This can either mean the user has previously verified the email + or this could be a sign of a malicious user trying to tamper with your app. +--> diff --git a/public/de/link_send_success.html b/public/de/link_send_success.html index fa2d7f2ee5..192a33142b 100644 --- a/public/de/link_send_success.html +++ b/public/de/link_send_success.html @@ -1,10 +1,9 @@ + This page is displayed when a user opens a verify email link with a + security token that is expired, then requests to receive another email + with a new link and the email is sent successfully. +--> diff --git a/public/de/password_reset_success.html b/public/de/password_reset_success.html index 7a90901f87..4b4e4c7104 100644 --- a/public/de/password_reset_success.html +++ b/public/de/password_reset_success.html @@ -1,9 +1,7 @@ diff --git a/public/de/verify_email_success.html b/public/de/verify_email_success.html index 3d643e4f42..e8db182551 100644 --- a/public/de/verify_email_success.html +++ b/public/de/verify_email_success.html @@ -1,8 +1,7 @@ From 04fbfb41732c12277111b888bb747e7cd3005bcf Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Wed, 20 Jan 2021 21:28:15 +0100 Subject: [PATCH 30/48] changed changelog entry for experimental feature --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1bd26f4fc5..39ad434585 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,9 +4,9 @@ [Full Changelog](https://github.com/parse-community/parse-server/compare/4.5.0...master) __BREAKING CHANGES:__ -- NEW: Added page localization. This PR contains breaking changes regarding password reset, email verification, page templating and the serving of pages in general. See PR for a detailed list of breaking changes [#6891](https://github.com/parse-community/parse-server/issues/6891). Thanks to [Manuel Trezza](https://github.com/mtrezza). - NEW: Added file upload restriction. File upload is now only allowed for authenticated users by default for improved security. To allow file upload also for Anonymous Users or Public, set the `fileUpload` parameter in the [Parse Server Options](https://parseplatform.org/parse-server/api/master/ParseServerOptions.html). [#7071](https://github.com/parse-community/parse-server/pull/7071). Thanks to [dblythy](https://github.com/dblythy). ___ +- NEW (EXPERIMENTAL): Added page localization for password reset and email verification. **Caution, this is an experimental feature that may not be appropriate for production.** [#6891](https://github.com/parse-community/parse-server/issues/6891). Thanks to [Manuel Trezza](https://github.com/mtrezza). - IMPROVE: Optimize queries on classes with pointer permissions. [#7061](https://github.com/parse-community/parse-server/pull/7061). Thanks to [Pedro Diaz](https://github.com/pdiaz) - FIX: request.context for afterFind triggers. [#7078](https://github.com/parse-community/parse-server/pull/7078). Thanks to [dblythy](https://github.com/dblythy) - NEW: Added convenience method Parse.Cloud.sendEmail(...) to send email via email adapter in Cloud Code. [#7089](https://github.com/parse-community/parse-server/pull/7089). Thanks to [dblythy](https://github.com/dblythy) From 211bb002578c7b043e2864601fa26edbcfe278c8 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Thu, 21 Jan 2021 01:31:04 +0100 Subject: [PATCH 31/48] improved file naming to make it more descriptive --- ...l => email_verification_link_expired.html} | 0 ...l => email_verification_link_invalid.html} | 0 ...html => email_verification_send_fail.html} | 0 ...l => email_verification_send_success.html} | 0 ...s.html => email_verification_success.html} | 0 .../password_reset.html} | 0 ....html => password_reset_link_invalid.html} | 0 ...l => email_verification_link_expired.html} | 0 ...l => email_verification_link_invalid.html} | 0 ...html => email_verification_send_fail.html} | 0 ...l => email_verification_send_success.html} | 0 ...s.html => email_verification_success.html} | 0 .../password_reset.html} | 0 ....html => password_reset_link_invalid.html} | 0 ...l => email_verification_link_expired.html} | 0 ...l => email_verification_link_invalid.html} | 0 ...html => email_verification_send_fail.html} | 0 ...l => email_verification_send_success.html} | 0 ...s.html => email_verification_success.html} | 0 ...oose_password.html => password_reset.html} | 0 ....html => password_reset_link_invalid.html} | 0 resources/buildConfigDefinitions.js | 3 +- spec/PagesRouter.spec.js | 85 ++++++++++--------- src/Config.js | 5 ++ src/Options/Definitions.js | 44 ++++++++++ src/Options/docs.js | 14 +++ src/Options/index.js | 22 +++++ src/Routers/PagesRouter.js | 52 ++++++------ 28 files changed, 159 insertions(+), 66 deletions(-) rename public/de-AT/{expired_verification_link.html => email_verification_link_expired.html} (100%) rename public/de-AT/{invalid_verification_link.html => email_verification_link_invalid.html} (100%) rename public/de-AT/{link_send_fail.html => email_verification_send_fail.html} (100%) rename public/de-AT/{link_send_success.html => email_verification_send_success.html} (100%) rename public/de-AT/{verify_email_success.html => email_verification_success.html} (100%) rename public/{choose_password.html => de-AT/password_reset.html} (100%) rename public/de-AT/{invalid_password_reset_link.html => password_reset_link_invalid.html} (100%) rename public/de/{expired_verification_link.html => email_verification_link_expired.html} (100%) rename public/de/{invalid_verification_link.html => email_verification_link_invalid.html} (100%) rename public/de/{link_send_fail.html => email_verification_send_fail.html} (100%) rename public/de/{link_send_success.html => email_verification_send_success.html} (100%) rename public/de/{verify_email_success.html => email_verification_success.html} (100%) rename public/{de-AT/choose_password.html => de/password_reset.html} (100%) rename public/de/{invalid_password_reset_link.html => password_reset_link_invalid.html} (100%) rename public/{expired_verification_link.html => email_verification_link_expired.html} (100%) rename public/{invalid_verification_link.html => email_verification_link_invalid.html} (100%) rename public/{link_send_fail.html => email_verification_send_fail.html} (100%) rename public/{link_send_success.html => email_verification_send_success.html} (100%) rename public/{verify_email_success.html => email_verification_success.html} (100%) rename public/{de/choose_password.html => password_reset.html} (100%) rename public/{invalid_password_reset_link.html => password_reset_link_invalid.html} (100%) diff --git a/public/de-AT/expired_verification_link.html b/public/de-AT/email_verification_link_expired.html similarity index 100% rename from public/de-AT/expired_verification_link.html rename to public/de-AT/email_verification_link_expired.html diff --git a/public/de-AT/invalid_verification_link.html b/public/de-AT/email_verification_link_invalid.html similarity index 100% rename from public/de-AT/invalid_verification_link.html rename to public/de-AT/email_verification_link_invalid.html diff --git a/public/de-AT/link_send_fail.html b/public/de-AT/email_verification_send_fail.html similarity index 100% rename from public/de-AT/link_send_fail.html rename to public/de-AT/email_verification_send_fail.html diff --git a/public/de-AT/link_send_success.html b/public/de-AT/email_verification_send_success.html similarity index 100% rename from public/de-AT/link_send_success.html rename to public/de-AT/email_verification_send_success.html diff --git a/public/de-AT/verify_email_success.html b/public/de-AT/email_verification_success.html similarity index 100% rename from public/de-AT/verify_email_success.html rename to public/de-AT/email_verification_success.html diff --git a/public/choose_password.html b/public/de-AT/password_reset.html similarity index 100% rename from public/choose_password.html rename to public/de-AT/password_reset.html diff --git a/public/de-AT/invalid_password_reset_link.html b/public/de-AT/password_reset_link_invalid.html similarity index 100% rename from public/de-AT/invalid_password_reset_link.html rename to public/de-AT/password_reset_link_invalid.html diff --git a/public/de/expired_verification_link.html b/public/de/email_verification_link_expired.html similarity index 100% rename from public/de/expired_verification_link.html rename to public/de/email_verification_link_expired.html diff --git a/public/de/invalid_verification_link.html b/public/de/email_verification_link_invalid.html similarity index 100% rename from public/de/invalid_verification_link.html rename to public/de/email_verification_link_invalid.html diff --git a/public/de/link_send_fail.html b/public/de/email_verification_send_fail.html similarity index 100% rename from public/de/link_send_fail.html rename to public/de/email_verification_send_fail.html diff --git a/public/de/link_send_success.html b/public/de/email_verification_send_success.html similarity index 100% rename from public/de/link_send_success.html rename to public/de/email_verification_send_success.html diff --git a/public/de/verify_email_success.html b/public/de/email_verification_success.html similarity index 100% rename from public/de/verify_email_success.html rename to public/de/email_verification_success.html diff --git a/public/de-AT/choose_password.html b/public/de/password_reset.html similarity index 100% rename from public/de-AT/choose_password.html rename to public/de/password_reset.html diff --git a/public/de/invalid_password_reset_link.html b/public/de/password_reset_link_invalid.html similarity index 100% rename from public/de/invalid_password_reset_link.html rename to public/de/password_reset_link_invalid.html diff --git a/public/expired_verification_link.html b/public/email_verification_link_expired.html similarity index 100% rename from public/expired_verification_link.html rename to public/email_verification_link_expired.html diff --git a/public/invalid_verification_link.html b/public/email_verification_link_invalid.html similarity index 100% rename from public/invalid_verification_link.html rename to public/email_verification_link_invalid.html diff --git a/public/link_send_fail.html b/public/email_verification_send_fail.html similarity index 100% rename from public/link_send_fail.html rename to public/email_verification_send_fail.html diff --git a/public/link_send_success.html b/public/email_verification_send_success.html similarity index 100% rename from public/link_send_success.html rename to public/email_verification_send_success.html diff --git a/public/verify_email_success.html b/public/email_verification_success.html similarity index 100% rename from public/verify_email_success.html rename to public/email_verification_success.html diff --git a/public/de/choose_password.html b/public/password_reset.html similarity index 100% rename from public/de/choose_password.html rename to public/password_reset.html diff --git a/public/invalid_password_reset_link.html b/public/password_reset_link_invalid.html similarity index 100% rename from public/invalid_password_reset_link.html rename to public/password_reset_link_invalid.html diff --git a/resources/buildConfigDefinitions.js b/resources/buildConfigDefinitions.js index 6807a0dbd7..f9cf5038a9 100644 --- a/resources/buildConfigDefinitions.js +++ b/resources/buildConfigDefinitions.js @@ -44,6 +44,7 @@ function getENVPrefix(iface) { 'ParseServerOptions' : 'PARSE_SERVER_', 'PagesOptions' : 'PARSE_SERVER_PAGES_', 'CustomPagesOptions' : 'PARSE_SERVER_CUSTOM_PAGES_', + 'CustomPageUrlsOptions' : 'PARSE_SERVER_PAGES_CUSTOM_URL_', 'LiveQueryServerOptions' : 'PARSE_LIVE_QUERY_SERVER_', 'LiveQueryOptions' : 'PARSE_SERVER_LIVEQUERY_', 'IdempotencyOptions' : 'PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_', @@ -165,7 +166,7 @@ function parseDefaultValue(elt, value, t) { if (type == 'NumberOrBoolean') { literalValue = t.numericLiteral(parsers.numberOrBoolParser('')(value)); } - const literalTypes = ['IdempotencyOptions','FileUploadOptions','CustomPagesOptions', 'PagesOptions']; + const literalTypes = ['IdempotencyOptions','FileUploadOptions','CustomPagesOptions', 'PagesCustomUrlsOptions', 'PagesOptions']; if (literalTypes.includes(type)) { const object = parsers.objectParser(value); const props = Object.keys(object).map((key) => { diff --git a/spec/PagesRouter.spec.js b/spec/PagesRouter.spec.js index 43ecacad55..73cd714c5e 100644 --- a/spec/PagesRouter.spec.js +++ b/spec/PagesRouter.spec.js @@ -23,9 +23,9 @@ describe('Pages Router', () => { it('responds with file content on direct page request', async () => { const urls = [ - 'http://localhost:8378/1/apps/invalid_verification_link.html', + 'http://localhost:8378/1/apps/email_verification_link_invalid.html', 'http://localhost:8378/1/apps/choose_password?appId=test', - 'http://localhost:8378/1/apps/verify_email_success.html', + 'http://localhost:8378/1/apps/email_verification_success.html', 'http://localhost:8378/1/apps/password_reset_success.html', ]; for (const url of urls) { @@ -40,7 +40,7 @@ describe('Pages Router', () => { await reconfigureServer(_config); const response = await request({ - url: 'http://localhost:8378/1/apps/invalid_verification_link.html' + url: 'http://localhost:8378/1/apps/email_verification_link_invalid.html' }).catch(e => e); expect(response.status).toBe(200); }); @@ -51,7 +51,7 @@ describe('Pages Router', () => { await reconfigureServer(_config); const response = await request({ - url: `http://localhost:8378/1/pages/invalid_verification_link.html` + url: `http://localhost:8378/1/pages/email_verification_link_invalid.html` }).catch(e => e); expect(response.status).toBe(200); }); @@ -185,10 +185,10 @@ describe('Pages Router', () => { sendMail: () => {}, }, publicServerURL: 'http://localhost:8378/1', - customPages: {}, pages: { enableRouter: true, enableLocalization: true, + customUrls: {}, }, }; async function reconfigureServerWithPageOptions(options) { @@ -212,9 +212,9 @@ describe('Pages Router', () => { appId: 'test', appName: 'ExampleAppName', publicServerURL: 'http://localhost:8378/1', - customPages: {}, pages: { enableLocalization: true, + customUrls: {}, }, }, query: { @@ -241,6 +241,9 @@ describe('Pages Router', () => { expect(Config.get(Parse.applicationId).pages.pagesEndpoint).toBe( Definitions.PagesOptions.pagesEndpoint.default ); + expect(Config.get(Parse.applicationId).pages.customUrls).toBe( + Definitions.PagesOptions.customUrls.default + ); }); it('throws on invalid configuration', async () => { @@ -269,6 +272,10 @@ describe('Pages Router', () => { { pagesEndpoint: 0 }, { pagesEndpoint: {} }, { pagesEndpoint: [] }, + { customUrls: true }, + { customUrls: 0 }, + { customUrls: 'a' }, + { customUrls: [] }, ]; for (const option of options) { await expectAsync(reconfigureServerWithPageOptions(option)).toBeRejected(); @@ -278,7 +285,7 @@ describe('Pages Router', () => { describe('placeholders', () => { it('replaces placeholder in response content', async () => { - await expectAsync(router.goToPage(req, pages.invalidPasswordResetLink)).toBeResolved(); + await expectAsync(router.goToPage(req, pages.passwordResetLinkInvalid)).toBeResolved(); expect(readFile.calls.all()[0].returnValue).toBeDefined(); const originalContent = await readFile.calls.all()[0].returnValue; @@ -291,7 +298,7 @@ describe('Pages Router', () => { }); it('removes undefined placeholder in response content', async () => { - await expectAsync(router.goToPage(req, pages.choosePassword)).toBeResolved(); + await expectAsync(router.goToPage(req, pages.passwordReset)).toBeResolved(); expect(readFile.calls.all()[0].returnValue).toBeDefined(); const originalContent = await readFile.calls.all()[0].returnValue; @@ -309,81 +316,81 @@ describe('Pages Router', () => { it('returns default file if localization is disabled', async () => { delete req.config.pages.enableLocalization; - await expectAsync(router.goToPage(req, pages.invalidPasswordResetLink)).toBeResolved(); + await expectAsync(router.goToPage(req, pages.passwordResetLinkInvalid)).toBeResolved(); expect(pageResponse.calls.all()[0].args[0]).toBeDefined(); expect(pageResponse.calls.all()[0].args[0]).not.toMatch( - new RegExp(`\/de(-AT)?\/${pages.invalidPasswordResetLink.defaultFile}`) + new RegExp(`\/de(-AT)?\/${pages.passwordResetLinkInvalid.defaultFile}`) ); }); it('returns default file if no locale is specified', async () => { delete req.query.locale; - await expectAsync(router.goToPage(req, pages.invalidPasswordResetLink)).toBeResolved(); + await expectAsync(router.goToPage(req, pages.passwordResetLinkInvalid)).toBeResolved(); expect(pageResponse.calls.all()[0].args[0]).toBeDefined(); expect(pageResponse.calls.all()[0].args[0]).not.toMatch( - new RegExp(`\/de(-AT)?\/${pages.invalidPasswordResetLink.defaultFile}`) + new RegExp(`\/de(-AT)?\/${pages.passwordResetLinkInvalid.defaultFile}`) ); }); it('returns custom page regardless of localization enabled', async () => { - req.config.customPages = { invalidPasswordResetLink: 'http://invalid-link.example.com' }; + req.config.pages.customUrls = { passwordResetLinkInvalid: 'http://invalid-link.example.com' }; - await expectAsync(router.goToPage(req, pages.invalidPasswordResetLink)).toBeResolved(); + await expectAsync(router.goToPage(req, pages.passwordResetLinkInvalid)).toBeResolved(); expect(pageResponse).not.toHaveBeenCalled(); - expect(redirectResponse.calls.all()[0].args[0]).toBe(req.config.customPages.invalidPasswordResetLink); + expect(redirectResponse.calls.all()[0].args[0]).toBe(req.config.pages.customUrls.passwordResetLinkInvalid); }); it('returns file for locale match', async () => { - await expectAsync(router.goToPage(req, pages.invalidPasswordResetLink)).toBeResolved(); + await expectAsync(router.goToPage(req, pages.passwordResetLinkInvalid)).toBeResolved(); expect(pageResponse.calls.all()[0].args[0]).toBeDefined(); expect(pageResponse.calls.all()[0].args[0]).toMatch( - new RegExp(`\/${req.query.locale}\/${pages.invalidPasswordResetLink.defaultFile}`) + new RegExp(`\/${req.query.locale}\/${pages.passwordResetLinkInvalid.defaultFile}`) ); }); it('returns file for language match', async () => { // Pretend no locale matching file exists spyOn(Utils, 'fileExists').and.callFake(async path => { - return !path.includes(`/${req.query.locale}/${pages.invalidPasswordResetLink.defaultFile}`); + return !path.includes(`/${req.query.locale}/${pages.passwordResetLinkInvalid.defaultFile}`); }); - await expectAsync(router.goToPage(req, pages.invalidPasswordResetLink)).toBeResolved(); + await expectAsync(router.goToPage(req, pages.passwordResetLinkInvalid)).toBeResolved(); expect(pageResponse.calls.all()[0].args[0]).toBeDefined(); expect(pageResponse.calls.all()[0].args[0]).toMatch( - new RegExp(`\/de\/${pages.invalidPasswordResetLink.defaultFile}`) + new RegExp(`\/de\/${pages.passwordResetLinkInvalid.defaultFile}`) ); }); it('returns default file for neither locale nor language match', async () => { req.query.locale = 'yo-LO'; - await expectAsync(router.goToPage(req, pages.invalidPasswordResetLink)).toBeResolved(); + await expectAsync(router.goToPage(req, pages.passwordResetLinkInvalid)).toBeResolved(); expect(pageResponse.calls.all()[0].args[0]).toBeDefined(); expect(pageResponse.calls.all()[0].args[0]).not.toMatch( - new RegExp(`\/yo(-LO)?\/${pages.invalidPasswordResetLink.defaultFile}`) + new RegExp(`\/yo(-LO)?\/${pages.passwordResetLinkInvalid.defaultFile}`) ); }); it('returns a file for GET request', async () => { - await expectAsync(router.goToPage(req, pages.invalidPasswordResetLink)).toBeResolved(); + await expectAsync(router.goToPage(req, pages.passwordResetLinkInvalid)).toBeResolved(); expect(pageResponse).toHaveBeenCalled(); expect(redirectResponse).not.toHaveBeenCalled(); }); it('returns a redirect for POST request', async () => { req.method = 'POST'; - await expectAsync(router.goToPage(req, pages.invalidPasswordResetLink)).toBeResolved(); + await expectAsync(router.goToPage(req, pages.passwordResetLinkInvalid)).toBeResolved(); expect(pageResponse).not.toHaveBeenCalled(); expect(redirectResponse).toHaveBeenCalled(); }); it('returns a redirect for custom pages for GET and POST request', async () => { - req.config.customPages = { invalidPasswordResetLink: 'http://invalid-link.example.com' }; + req.config.pages.customUrls = { passwordResetLinkInvalid: 'http://invalid-link.example.com' }; for (const method of ['GET', 'POST']) { req.method = method; - await expectAsync(router.goToPage(req, pages.invalidPasswordResetLink)).toBeResolved(); + await expectAsync(router.goToPage(req, pages.passwordResetLinkInvalid)).toBeResolved(); expect(pageResponse).not.toHaveBeenCalled(); expect(redirectResponse).toHaveBeenCalled(); } @@ -398,7 +405,7 @@ describe('Pages Router', () => { method: 'POST', }); expect(response.status).toEqual(303); - expect(response.headers.location).toContain('http://localhost:8378/1/apps/de-AT/invalid_password_reset_link.html'); + expect(response.headers.location).toContain('http://localhost:8378/1/apps/de-AT/password_reset_link_invalid.html'); }); it('responds to GET request with content response', async () => { @@ -440,14 +447,14 @@ describe('Pages Router', () => { const locale = linkResponse.headers['x-parse-page-param-locale']; const username = linkResponse.headers['x-parse-page-param-username']; const publicServerUrl = linkResponse.headers['x-parse-page-param-publicserverurl']; - const choosePasswordPagePath = pageResponse.calls.all()[0].args[0]; + const passwordResetPagePath = pageResponse.calls.all()[0].args[0]; expect(appId).toBeDefined(); expect(token).toBeDefined(); expect(locale).toBeDefined(); expect(username).toBeDefined(); expect(publicServerUrl).toBeDefined(); - expect(choosePasswordPagePath).toMatch( - new RegExp(`\/${exampleLocale}\/${pages.choosePassword.defaultFile}`) + expect(passwordResetPagePath).toMatch( + new RegExp(`\/${exampleLocale}\/${pages.passwordReset.defaultFile}`) ); pageResponse.calls.reset(); @@ -491,7 +498,7 @@ describe('Pages Router', () => { const pagePath = pageResponse.calls.all()[0].args[0]; expect(pagePath).toMatch( - new RegExp(`\/${exampleLocale}\/${pages.invalidPasswordResetLink.defaultFile}`) + new RegExp(`\/${exampleLocale}\/${pages.passwordResetLinkInvalid.defaultFile}`) ); }); @@ -516,7 +523,7 @@ describe('Pages Router', () => { const pagePath = pageResponse.calls.all()[0].args[0]; expect(pagePath).toMatch( - new RegExp(`\/${exampleLocale}\/${pages.verifyEmailSuccess.defaultFile}`) + new RegExp(`\/${exampleLocale}\/${pages.emailVerificationSuccess.defaultFile}`) ); }); @@ -550,7 +557,7 @@ describe('Pages Router', () => { expect(username).toBeDefined(); expect(publicServerUrl).toBeDefined(); expect(invalidVerificationPagePath).toMatch( - new RegExp(`\/${exampleLocale}\/${pages.expiredVerificationLink.defaultFile}`) + new RegExp(`\/${exampleLocale}\/${pages.emailVerificationLinkExpired.defaultFile}`) ); const formUrl = `${publicServerUrl}/apps/${appId}/resend_verification_email`; @@ -565,7 +572,7 @@ describe('Pages Router', () => { followRedirects: false, }); expect(formResponse.status).toEqual(303); - expect(formResponse.text).toContain(`/${locale}/${pages.linkSendSuccess.defaultFile}`); + expect(formResponse.text).toContain(`/${locale}/${pages.emailVerificationResendSuccess.defaultFile}`); }); it('localizes end-to-end for verify email: invalid verification link - link send fail', async () => { @@ -598,7 +605,7 @@ describe('Pages Router', () => { expect(username).toBeDefined(); expect(publicServerUrl).toBeDefined(); expect(invalidVerificationPagePath).toMatch( - new RegExp(`\/${exampleLocale}\/${pages.expiredVerificationLink.defaultFile}`) + new RegExp(`\/${exampleLocale}\/${pages.emailVerificationLinkExpired.defaultFile}`) ); spyOn(UserController.prototype, 'resendVerificationEmail').and.callFake(() => Promise.reject( @@ -617,7 +624,7 @@ describe('Pages Router', () => { followRedirects: false, }); expect(formResponse.status).toEqual(303); - expect(formResponse.text).toContain(`/${locale}/${pages.linkSendFail.defaultFile}`); + expect(formResponse.text).toContain(`/${locale}/${pages.emailVerificationSendFail.defaultFile}`); }); it('localizes end-to-end for resend verification email: invalid link', async () => { @@ -633,7 +640,7 @@ describe('Pages Router', () => { followRedirects: false, }); expect(formResponse.status).toEqual(303); - expect(formResponse.text).toContain(`/${exampleLocale}/${pages.invalidVerificationLink.defaultFile}`); + expect(formResponse.text).toContain(`/${exampleLocale}/${pages.emailVerificationLinkInvalid.defaultFile}`); }); }); @@ -669,7 +676,7 @@ describe('Pages Router', () => { const verifyEmail = (req) => new PagesRouter().verifyEmail(req); await verifyEmail(req); - expect(goToPage.calls.all()[0].args[1]).toBe(pages.invalidVerificationLink); + expect(goToPage.calls.all()[0].args[1]).toBe(pages.emailVerificationLinkInvalid); }); it('resetPassword: responds with page choose password with error message on failed password update', async () => { @@ -683,7 +690,7 @@ describe('Pages Router', () => { const resetPassword = (req) => new PagesRouter().resetPassword(req); await resetPassword(req); - expect(goToPage.calls.all()[0].args[1]).toBe(pages.choosePassword); + expect(goToPage.calls.all()[0].args[1]).toBe(pages.passwordReset); expect(goToPage.calls.all()[0].args[2].error).toBe(error); }); diff --git a/src/Config.js b/src/Config.js index f772ff3db6..6967d418fc 100644 --- a/src/Config.js +++ b/src/Config.js @@ -144,6 +144,11 @@ export class Config { } else if (!isString(pages.pagesEndpoint)) { throw 'Parse Server option pages.pagesEndpoint must be a string.'; } + if (pages.customUrls === undefined) { + pages.customUrls = PagesOptions.customUrls.default; + } else if (Object.prototype.toString.call(pages.customUrls) !== '[object Object]') { + throw 'Parse Server option pages.customUrls must be an object.'; + } } static validateIdempotencyOptions(idempotencyOptions) { diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index 4c051585a8..87fe558524 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -413,6 +413,12 @@ module.exports.ParseServerOptions = { } }; module.exports.PagesOptions = { + "customUrls": { + "env": "PARSE_SERVER_PAGES_CUSTOM_URLS", + "help": "The URLs to the custom pages.", + "action": parsers.objectParser, + "default": {} + }, "enableLocalization": { "env": "PARSE_SERVER_PAGES_ENABLE_LOCALIZATION", "help": "Is true if pages should be localized; this has no effect on custom page redirects.", @@ -442,11 +448,49 @@ module.exports.PagesOptions = { "default": "./public" } }; +module.exports.PagesCustomUrlsOptions = { + "choosePassword": { + "env": "undefinedCHOOSE_PASSWORD", + "help": "The URL to the custom page for password reset." + }, + "expiredVerificationLink": { + "env": "undefinedEXPIRED_VERIFICATION_LINK", + "help": "The URL to the custom page for email verification -> link expired." + }, + "invalidPasswordResetLink": { + "env": "undefinedINVALID_PASSWORD_RESET_LINK", + "help": "The URL to the custom page for password reset -> link invalid." + }, + "invalidVerificationLink": { + "env": "undefinedINVALID_VERIFICATION_LINK", + "help": "The URL to the custom page for email verification -> link invalid." + }, + "linkSendFail": { + "env": "undefinedLINK_SEND_FAIL", + "help": "The URL to the custom page for email verification -> link send fail." + }, + "linkSendSuccess": { + "env": "undefinedLINK_SEND_SUCCESS", + "help": "The URL to the custom page for email verification -> resend link -> success." + }, + "passwordResetSuccess": { + "env": "undefinedPASSWORD_RESET_SUCCESS", + "help": "The URL to the custom page for password reset -> success." + }, + "verifyEmailSuccess": { + "env": "undefinedVERIFY_EMAIL_SUCCESS", + "help": "The URL to the custom page for email verification -> success." + } +}; module.exports.CustomPagesOptions = { "choosePassword": { "env": "PARSE_SERVER_CUSTOM_PAGES_CHOOSE_PASSWORD", "help": "choose password page path" }, + "expiredVerificationLink": { + "env": "PARSE_SERVER_CUSTOM_PAGES_EXPIRED_VERIFICATION_LINK", + "help": "expired verification link page path" + }, "invalidLink": { "env": "PARSE_SERVER_CUSTOM_PAGES_INVALID_LINK", "help": "invalid link page path" diff --git a/src/Options/docs.js b/src/Options/docs.js index 533bf8c15b..d86b9d306d 100644 --- a/src/Options/docs.js +++ b/src/Options/docs.js @@ -82,6 +82,7 @@ /** * @interface PagesOptions + * @property {PagesCustomUrlsOptions} customUrls The URLs to the custom pages. * @property {Boolean} enableLocalization Is true if pages should be localized; this has no effect on custom page redirects. * @property {Boolean} enableRouter Is true if the pages router should be enabled; this is required for any of the pages options to take effect. Caution, this is an experimental feature that may not be appropriate for production. * @property {Boolean} forceRedirect Is true if responses should always be redirects and never content, false if the response type should depend on the request type (GET request -> content response; POST request -> redirect response). @@ -89,9 +90,22 @@ * @property {String} pagesPath The path to the pages directory; this also defines where the static endpoint '/apps' points to. Default is the './public/' directory. */ +/** + * @interface PagesCustomUrlsOptions + * @property {String} choosePassword The URL to the custom page for password reset. + * @property {String} expiredVerificationLink The URL to the custom page for email verification -> link expired. + * @property {String} invalidPasswordResetLink The URL to the custom page for password reset -> link invalid. + * @property {String} invalidVerificationLink The URL to the custom page for email verification -> link invalid. + * @property {String} linkSendFail The URL to the custom page for email verification -> link send fail. + * @property {String} linkSendSuccess The URL to the custom page for email verification -> resend link -> success. + * @property {String} passwordResetSuccess The URL to the custom page for password reset -> success. + * @property {String} verifyEmailSuccess The URL to the custom page for email verification -> success. + */ + /** * @interface CustomPagesOptions * @property {String} choosePassword choose password page path + * @property {String} expiredVerificationLink expired verification link page path * @property {String} invalidLink invalid link page path * @property {String} invalidPasswordResetLink invalid password reset link page path * @property {String} invalidVerificationLink invalid verification link page path diff --git a/src/Options/index.js b/src/Options/index.js index d3c2084787..81a823b43c 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -245,6 +245,28 @@ export interface PagesOptions { /* The API endoint for the pages. Default is the 'apps'. :DEFAULT: apps */ pagesEndpoint: ?string; + /* The URLs to the custom pages. + :DEFAULT: {} */ + customUrls: ?PagesCustomUrlsOptions +} + +export interface PagesCustomUrlsOptions { + /* The URL to the custom page for email verification -> link send fail. */ + linkSendFail: ?string; + /* The URL to the custom page for password reset. */ + choosePassword: ?string; + /* The URL to the custom page for email verification -> resend link -> success. */ + linkSendSuccess: ?string; + /* The URL to the custom page for email verification -> success. */ + verifyEmailSuccess: ?string; + /* The URL to the custom page for password reset -> success. */ + passwordResetSuccess: ?string; + /* The URL to the custom page for email verification -> link invalid. */ + invalidVerificationLink: ?string; + /* The URL to the custom page for email verification -> link expired. */ + expiredVerificationLink: ?string; + /* The URL to the custom page for password reset -> link invalid. */ + invalidPasswordResetLink: ?string; } export interface CustomPagesOptions { diff --git a/src/Routers/PagesRouter.js b/src/Routers/PagesRouter.js index 4843e41d1d..ef9bedbcac 100644 --- a/src/Routers/PagesRouter.js +++ b/src/Routers/PagesRouter.js @@ -10,14 +10,14 @@ import Page from '../Page'; // All pages with custom page key for reference and file name const pages = Object.freeze({ - linkSendFail: new Page({ id: 'linkSendFail', defaultFile: 'link_send_fail.html' }), - choosePassword: new Page({ id: 'choosePassword', defaultFile: 'choose_password.html' }), - linkSendSuccess: new Page({ id: 'linkSendSuccess', defaultFile: 'link_send_success.html' }), - verifyEmailSuccess: new Page({ id: 'verifyEmailSuccess', defaultFile: 'verify_email_success.html' }), + passwordReset: new Page({ id: 'passwordReset', defaultFile: 'password_reset.html' }), passwordResetSuccess: new Page({ id: 'passwordResetSuccess', defaultFile: 'password_reset_success.html' }), - invalidVerificationLink: new Page({ id: 'invalidVerificationLink', defaultFile: 'invalid_verification_link.html' }), - expiredVerificationLink: new Page({ id: 'expiredVerificationLink', defaultFile: 'expired_verification_link.html' }), - invalidPasswordResetLink: new Page({ id: 'invalidPasswordResetLink', defaultFile: 'invalid_password_reset_link.html' }), + passwordResetLinkInvalid: new Page({ id: 'passwordResetLinkInvalid', defaultFile: 'password_reset_link_invalid.html' }), + emailVerificationSuccess: new Page({ id: 'emailVerificationSuccess', defaultFile: 'email_verification_success.html' }), + emailVerificationSendFail: new Page({ id: 'emailVerificationSendFail', defaultFile: 'email_verification_send_fail.html' }), + emailVerificationResendSuccess: new Page({ id: 'emailVerificationResendSuccess', defaultFile: 'email_verification_send_success.html' }), + emailVerificationLinkInvalid: new Page({ id: 'emailVerificationLinkInvalid', defaultFile: 'email_verification_link_invalid.html' }), + emailVerificationLinkExpired: new Page({ id: 'emailVerificationLinkExpired', defaultFile: 'email_verification_link_expired.html' }), }); // All page parameters for reference to be used as template placeholders or query params const pageParams = Object.freeze({ @@ -59,7 +59,7 @@ export class PagesRouter extends PromiseRouter { } if (!token || !username) { - return this.goToPage(req, pages.invalidVerificationLink); + return this.goToPage(req, pages.emailVerificationLinkInvalid); } const userController = config.userController; @@ -68,13 +68,13 @@ export class PagesRouter extends PromiseRouter { const params = { [pageParams.username]: username, }; - return this.goToPage(req, pages.verifyEmailSuccess, params); + return this.goToPage(req, pages.emailVerificationSuccess, params); }, () => { const params = { [pageParams.username]: username, }; - return this.goToPage(req, pages.expiredVerificationLink, params); + return this.goToPage(req, pages.emailVerificationLinkExpired, params); } ); } @@ -88,22 +88,22 @@ export class PagesRouter extends PromiseRouter { } if (!username) { - return this.goToPage(req, pages.invalidVerificationLink); + return this.goToPage(req, pages.emailVerificationLinkInvalid); } const userController = config.userController; return userController.resendVerificationEmail(username).then( () => { - return this.goToPage(req, pages.linkSendSuccess); + return this.goToPage(req, pages.emailVerificationResendSuccess); }, () => { - return this.goToPage(req, pages.linkSendFail); + return this.goToPage(req, pages.emailVerificationSendFail); } ); } - choosePassword(req) { + passwordReset(req) { const config = req.config; const params = { [pageParams.appId]: req.params.appId, @@ -112,7 +112,7 @@ export class PagesRouter extends PromiseRouter { [pageParams.username]: req.query.username, [pageParams.publicServerUrl]: config.publicServerURL }; - return this.goToPage(req, pages.choosePassword, params); + return this.goToPage(req, pages.passwordReset, params); } requestResetPassword(req) { @@ -126,7 +126,7 @@ export class PagesRouter extends PromiseRouter { const token = rawToken && typeof rawToken !== 'string' ? rawToken.toString() : rawToken; if (!username || !token) { - return this.goToPage(req, pages.invalidPasswordResetLink); + return this.goToPage(req, pages.passwordResetLinkInvalid); } return config.userController.checkResetTokenValidity(username, token).then( @@ -137,13 +137,13 @@ export class PagesRouter extends PromiseRouter { [pageParams.appId]: config.applicationId, [pageParams.appName]: config.appName, }; - return this.goToPage(req, pages.choosePassword, params); + return this.goToPage(req, pages.passwordReset, params); }, () => { const params = { [pageParams.username]: username, }; - return this.goToPage(req, pages.invalidPasswordResetLink, params); + return this.goToPage(req, pages.passwordResetLinkInvalid, params); } ); } @@ -159,7 +159,7 @@ export class PagesRouter extends PromiseRouter { const token = rawToken && typeof rawToken !== 'string' ? rawToken.toString() : rawToken; if ((!username || !token || !new_password) && req.xhr === false) { - return this.goToPage(req, pages.invalidPasswordResetLink); + return this.goToPage(req, pages.passwordResetLinkInvalid); } if (!username) { @@ -213,7 +213,7 @@ export class PagesRouter extends PromiseRouter { [pageParams.error]: result.err, [pageParams.appName]: config.appName, }; - const page = result.success ? pages.passwordResetSuccess : pages.choosePassword; + const page = result.success ? pages.passwordResetSuccess : pages.passwordReset; return this.goToPage(req, page, query, false); }); @@ -257,7 +257,7 @@ export class PagesRouter extends PromiseRouter { // Add locale to params to ensure it is passed on with every request; // that means, once a locale is set, it is passed on to any follow-up page, - // e.g. request_password_reset -> choose_password -> passwort_reset_success + // e.g. request_password_reset -> password_reset -> passwort_reset_success const locale = (req.query || {})[pageParams.locale] || (req.body || {})[pageParams.locale] @@ -270,10 +270,10 @@ export class PagesRouter extends PromiseRouter { const defaultPath = this.defaultPagePath(defaultFile); const defaultUrl = this.composePageUrl(defaultFile, config.publicServerURL); - // If custom page URL is set redirect to it without localization - const customPageUrl = config.customPages[page.id]; - if (customPageUrl && !Utils.isPath(customPageUrl)) { - return this.redirectResponse(customPageUrl, params); + // If custom URL is set redirect to it without localization + const customUrl = config.pages.customUrls[page.id]; + if (customUrl && !Utils.isPath(customUrl)) { + return this.redirectResponse(customUrl, params); } // If localization is enabled @@ -423,7 +423,7 @@ export class PagesRouter extends PromiseRouter { this.setConfig(req); }, req => { - return this.choosePassword(req); + return this.passwordReset(req); } ); From b11e5b982d0dccc832a4f94cb80ae3543d3bbc48 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Thu, 21 Jan 2021 01:51:59 +0100 Subject: [PATCH 32/48] fixed file naming and env parameter naming --- resources/buildConfigDefinitions.js | 2 +- spec/PagesRouter.spec.js | 2 +- src/Options/Definitions.js | 42 ++++++++++++++--------------- src/Options/docs.js | 14 +++++----- src/Options/index.js | 22 +++++++-------- src/Routers/PagesRouter.js | 4 +-- 6 files changed, 43 insertions(+), 43 deletions(-) diff --git a/resources/buildConfigDefinitions.js b/resources/buildConfigDefinitions.js index f9cf5038a9..9c2ed48699 100644 --- a/resources/buildConfigDefinitions.js +++ b/resources/buildConfigDefinitions.js @@ -43,8 +43,8 @@ function getENVPrefix(iface) { const options = { 'ParseServerOptions' : 'PARSE_SERVER_', 'PagesOptions' : 'PARSE_SERVER_PAGES_', + 'PagesCustomUrlsOptions' : 'PARSE_SERVER_PAGES_CUSTOM_URL_', 'CustomPagesOptions' : 'PARSE_SERVER_CUSTOM_PAGES_', - 'CustomPageUrlsOptions' : 'PARSE_SERVER_PAGES_CUSTOM_URL_', 'LiveQueryServerOptions' : 'PARSE_LIVE_QUERY_SERVER_', 'LiveQueryOptions' : 'PARSE_SERVER_LIVEQUERY_', 'IdempotencyOptions' : 'PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_', diff --git a/spec/PagesRouter.spec.js b/spec/PagesRouter.spec.js index 73cd714c5e..86c529e109 100644 --- a/spec/PagesRouter.spec.js +++ b/spec/PagesRouter.spec.js @@ -572,7 +572,7 @@ describe('Pages Router', () => { followRedirects: false, }); expect(formResponse.status).toEqual(303); - expect(formResponse.text).toContain(`/${locale}/${pages.emailVerificationResendSuccess.defaultFile}`); + expect(formResponse.text).toContain(`/${locale}/${pages.emailVerificationSendSuccess.defaultFile}`); }); it('localizes end-to-end for verify email: invalid verification link - link send fail', async () => { diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index 87fe558524..8343b01646 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -449,37 +449,37 @@ module.exports.PagesOptions = { } }; module.exports.PagesCustomUrlsOptions = { - "choosePassword": { - "env": "undefinedCHOOSE_PASSWORD", - "help": "The URL to the custom page for password reset." - }, - "expiredVerificationLink": { - "env": "undefinedEXPIRED_VERIFICATION_LINK", + "emailVerificationLinkExpired": { + "env": "PARSE_SERVER_PAGES_CUSTOM_URL_EMAIL_VERIFICATION_LINK_EXPIRED", "help": "The URL to the custom page for email verification -> link expired." }, - "invalidPasswordResetLink": { - "env": "undefinedINVALID_PASSWORD_RESET_LINK", - "help": "The URL to the custom page for password reset -> link invalid." - }, - "invalidVerificationLink": { - "env": "undefinedINVALID_VERIFICATION_LINK", + "emailVerificationLinkInvalid": { + "env": "PARSE_SERVER_PAGES_CUSTOM_URL_EMAIL_VERIFICATION_LINK_INVALID", "help": "The URL to the custom page for email verification -> link invalid." }, - "linkSendFail": { - "env": "undefinedLINK_SEND_FAIL", + "emailVerificationSendFail": { + "env": "PARSE_SERVER_PAGES_CUSTOM_URL_EMAIL_VERIFICATION_SEND_FAIL", "help": "The URL to the custom page for email verification -> link send fail." }, - "linkSendSuccess": { - "env": "undefinedLINK_SEND_SUCCESS", + "emailVerificationSendSuccess": { + "env": "PARSE_SERVER_PAGES_CUSTOM_URL_EMAIL_VERIFICATION_SEND_SUCCESS", "help": "The URL to the custom page for email verification -> resend link -> success." }, + "emailVerificationSuccess": { + "env": "PARSE_SERVER_PAGES_CUSTOM_URL_EMAIL_VERIFICATION_SUCCESS", + "help": "The URL to the custom page for email verification -> success." + }, + "passwordReset": { + "env": "PARSE_SERVER_PAGES_CUSTOM_URL_PASSWORD_RESET", + "help": "The URL to the custom page for password reset." + }, + "passwordResetLinkInvalid": { + "env": "PARSE_SERVER_PAGES_CUSTOM_URL_PASSWORD_RESET_LINK_INVALID", + "help": "The URL to the custom page for password reset -> link invalid." + }, "passwordResetSuccess": { - "env": "undefinedPASSWORD_RESET_SUCCESS", + "env": "PARSE_SERVER_PAGES_CUSTOM_URL_PASSWORD_RESET_SUCCESS", "help": "The URL to the custom page for password reset -> success." - }, - "verifyEmailSuccess": { - "env": "undefinedVERIFY_EMAIL_SUCCESS", - "help": "The URL to the custom page for email verification -> success." } }; module.exports.CustomPagesOptions = { diff --git a/src/Options/docs.js b/src/Options/docs.js index d86b9d306d..cece87c084 100644 --- a/src/Options/docs.js +++ b/src/Options/docs.js @@ -92,14 +92,14 @@ /** * @interface PagesCustomUrlsOptions - * @property {String} choosePassword The URL to the custom page for password reset. - * @property {String} expiredVerificationLink The URL to the custom page for email verification -> link expired. - * @property {String} invalidPasswordResetLink The URL to the custom page for password reset -> link invalid. - * @property {String} invalidVerificationLink The URL to the custom page for email verification -> link invalid. - * @property {String} linkSendFail The URL to the custom page for email verification -> link send fail. - * @property {String} linkSendSuccess The URL to the custom page for email verification -> resend link -> success. + * @property {String} emailVerificationLinkExpired The URL to the custom page for email verification -> link expired. + * @property {String} emailVerificationLinkInvalid The URL to the custom page for email verification -> link invalid. + * @property {String} emailVerificationSendFail The URL to the custom page for email verification -> link send fail. + * @property {String} emailVerificationSendSuccess The URL to the custom page for email verification -> resend link -> success. + * @property {String} emailVerificationSuccess The URL to the custom page for email verification -> success. + * @property {String} passwordReset The URL to the custom page for password reset. + * @property {String} passwordResetLinkInvalid The URL to the custom page for password reset -> link invalid. * @property {String} passwordResetSuccess The URL to the custom page for password reset -> success. - * @property {String} verifyEmailSuccess The URL to the custom page for email verification -> success. */ /** diff --git a/src/Options/index.js b/src/Options/index.js index 81a823b43c..a0cb3f8082 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -251,22 +251,22 @@ export interface PagesOptions { } export interface PagesCustomUrlsOptions { - /* The URL to the custom page for email verification -> link send fail. */ - linkSendFail: ?string; /* The URL to the custom page for password reset. */ - choosePassword: ?string; - /* The URL to the custom page for email verification -> resend link -> success. */ - linkSendSuccess: ?string; - /* The URL to the custom page for email verification -> success. */ - verifyEmailSuccess: ?string; + passwordReset: ?string; + /* The URL to the custom page for password reset -> link invalid. */ + passwordResetLinkInvalid: ?string; /* The URL to the custom page for password reset -> success. */ passwordResetSuccess: ?string; + /* The URL to the custom page for email verification -> success. */ + emailVerificationSuccess: ?string; + /* The URL to the custom page for email verification -> link send fail. */ + emailVerificationSendFail: ?string; + /* The URL to the custom page for email verification -> resend link -> success. */ + emailVerificationSendSuccess: ?string; /* The URL to the custom page for email verification -> link invalid. */ - invalidVerificationLink: ?string; + emailVerificationLinkInvalid: ?string; /* The URL to the custom page for email verification -> link expired. */ - expiredVerificationLink: ?string; - /* The URL to the custom page for password reset -> link invalid. */ - invalidPasswordResetLink: ?string; + emailVerificationLinkExpired: ?string; } export interface CustomPagesOptions { diff --git a/src/Routers/PagesRouter.js b/src/Routers/PagesRouter.js index ef9bedbcac..6c2a8b59ff 100644 --- a/src/Routers/PagesRouter.js +++ b/src/Routers/PagesRouter.js @@ -15,7 +15,7 @@ const pages = Object.freeze({ passwordResetLinkInvalid: new Page({ id: 'passwordResetLinkInvalid', defaultFile: 'password_reset_link_invalid.html' }), emailVerificationSuccess: new Page({ id: 'emailVerificationSuccess', defaultFile: 'email_verification_success.html' }), emailVerificationSendFail: new Page({ id: 'emailVerificationSendFail', defaultFile: 'email_verification_send_fail.html' }), - emailVerificationResendSuccess: new Page({ id: 'emailVerificationResendSuccess', defaultFile: 'email_verification_send_success.html' }), + emailVerificationSendSuccess: new Page({ id: 'emailVerificationSendSuccess', defaultFile: 'email_verification_send_success.html' }), emailVerificationLinkInvalid: new Page({ id: 'emailVerificationLinkInvalid', defaultFile: 'email_verification_link_invalid.html' }), emailVerificationLinkExpired: new Page({ id: 'emailVerificationLinkExpired', defaultFile: 'email_verification_link_expired.html' }), }); @@ -95,7 +95,7 @@ export class PagesRouter extends PromiseRouter { return userController.resendVerificationEmail(username).then( () => { - return this.goToPage(req, pages.emailVerificationResendSuccess); + return this.goToPage(req, pages.emailVerificationSendSuccess); }, () => { return this.goToPage(req, pages.emailVerificationSendFail); From 617dbc97ea05bcdb3a8b5656286760a8b50b03b3 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Thu, 21 Jan 2021 02:26:05 +0100 Subject: [PATCH 33/48] added readme entry --- README.md | 155 +++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 131 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index c3720c4138..0cf9b18859 100644 --- a/README.md +++ b/README.md @@ -44,28 +44,49 @@ Parse Server works with the Express web application framework. It can be added t The full documentation for Parse Server is available in the [wiki](https://github.com/parse-community/parse-server/wiki). The [Parse Server guide](http://docs.parseplatform.org/parse-server/guide/) is a good place to get started. An [API reference](http://parseplatform.org/parse-server/api/) and [Cloud Code guide](https://docs.parseplatform.org/cloudcode/guide/) are also available. If you're interested in developing for Parse Server, the [Development guide](http://docs.parseplatform.org/parse-server/guide/#development-guide) will help you get set up. - [Getting Started](#getting-started) - - [Running Parse Server](#running-parse-server) - - [Locally](#locally) - - [Docker](#inside-a-docker-container) - - [Saving an Object](#saving-your-first-object) - - [Connect an SDK](#connect-your-app-to-parse-server) - - [Running elsewhere](#running-parse-server-elsewhere) - - [Sample Application](#parse-server-sample-application) - - [Parse Server + Express](#parse-server--express) - - [Configuration](#configuration) - - [Basic Options](#basic-options) - - [Client Key Options](#client-key-options) - - [Email Verification & Password Reset](#email-verification-and-password-reset) - - [Custom Pages](#custom-pages) - - [Using Environment Variables](#using-environment-variables-to-configure-parse-server) - - [Available Adapters](#available-adapters) - - [Configuring File Adapters](#configuring-file-adapters) - - [Logging](#logging) + - [Running Parse Server](#running-parse-server) + - [Locally](#locally) + - [Inside a Docker container](#inside-a-docker-container) + - [Running the Parse Server Image](#running-the-parse-server-image) + - [Saving your first object](#saving-your-first-object) + - [Connect your app to Parse Server](#connect-your-app-to-parse-server) + - [Running Parse Server elsewhere](#running-parse-server-elsewhere) + - [Parse Server Sample Application](#parse-server-sample-application) + - [Parse Server + Express](#parse-server--express) + - [Configuration](#configuration) + - [Basic options](#basic-options) + - [Client key options](#client-key-options) + - [Email verification and password reset](#email-verification-and-password-reset) + - [Custom Pages](#custom-pages) + - [Using environment variables to configure Parse Server](#using-environment-variables-to-configure-parse-server) + - [Available Adapters](#available-adapters) + - [Configuring File Adapters](#configuring-file-adapters) + - [Idempodency Enforcement](#idempodency-enforcement) + - [Configuration example](#configuration-example) + - [Parameters](#parameters) + - [Notes](#notes) + - [Localization](#localization) + - [Pages](#pages) + - [Configuration example](#configuration-example-1) + - [Parameters](#parameters-1) + - [Notes](#notes-1) + - [Logging](#logging) - [Live Queries](#live-queries) - [GraphQL](#graphql) + - [Running](#running) + - [Using the CLI](#using-the-cli) + - [Using Docker](#using-docker) + - [Running the Parse Server Image](#running-the-parse-server-image-1) + - [Using Express.js](#using-expressjs) + - [Checking the API health](#checking-the-api-health) + - [Creating your first class](#creating-your-first-class) + - [Using automatically generated operations](#using-automatically-generated-operations) + - [Customizing your GraphQL Schema](#customizing-your-graphql-schema) + - [Creating your first custom query](#creating-your-first-custom-query) + - [Learning more](#learning-more) - [Upgrading to 3.0.0](#upgrading-to-300) -- [Support](#support) -- [Ride the Bleeding Edge](#want-to-ride-the-bleeding-edge) +- [Want to ride the bleeding edge?](#want-to-ride-the-bleeding-edge) + - [Experimenting](#experimenting) - [Contributing](#contributing) - [Contributors](#contributors) - [Sponsors](#sponsors) @@ -421,16 +442,102 @@ let api = new ParseServer({ ``` #### Parameters -| Parameter | Optional | Type | Default value | Example values | Environment variable | Description | -|-----------|----------|--------|---------------|-----------|-----------|-------------| -| `idempotencyOptions` | yes | `Object` | `undefined` | | PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_OPTIONS | Setting this enables idempotency enforcement for the specified paths. | -| `idempotencyOptions.paths`| yes | `Array` | `[]` | `.*` (all paths, includes the examples below),
`functions/.*` (all functions),
`jobs/.*` (all jobs),
`classes/.*` (all classes),
`functions/.*` (all functions),
`users` (user creation / update),
`installations` (installation creation / update) | PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_PATHS | An array of path patterns that have to match the request path for request deduplication to be enabled. The mount path must not be included, for example to match the request path `/parse/functions/myFunction` specifiy the path pattern `functions/myFunction`. A trailing slash of the request path is ignored, for example the path pattern `functions/myFunction` matches both `/parse/functions/myFunction` and `/parse/functions/myFunction/`. | -| `idempotencyOptions.ttl` | yes | `Integer` | `300` | `60` (60 seconds) | PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_TTL | The duration in seconds after which a request record is discarded from the database. Duplicate requests due to network issues can be expected to arrive within milliseconds up to several seconds. This value must be greater than `0`. | +| Parameter | Optional | Type | Default value | Example values | Environment variable | Description | +|----------------------------|----------|-----------------|---------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `idempotencyOptions` | yes | `Object` | `undefined` | | PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_OPTIONS | Setting this enables idempotency enforcement for the specified paths. | +| `idempotencyOptions.paths` | yes | `Array` | `[]` | `.*` (all paths, includes the examples below),
`functions/.*` (all functions),
`jobs/.*` (all jobs),
`classes/.*` (all classes),
`functions/.*` (all functions),
`users` (user creation / update),
`installations` (installation creation / update) | PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_PATHS | An array of path patterns that have to match the request path for request deduplication to be enabled. The mount path must not be included, for example to match the request path `/parse/functions/myFunction` specifiy the path pattern `functions/myFunction`. A trailing slash of the request path is ignored, for example the path pattern `functions/myFunction` matches both `/parse/functions/myFunction` and `/parse/functions/myFunction/`. | +| `idempotencyOptions.ttl` | yes | `Integer` | `300` | `60` (60 seconds) | PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_TTL | The duration in seconds after which a request record is discarded from the database. Duplicate requests due to network issues can be expected to arrive within milliseconds up to several seconds. This value must be greater than `0`. | #### Notes - This feature is currently only available for MongoDB and not for Postgres. +### Localization + +#### Pages +**Caution, this is an experimental feature that may not be appropriate for production.** + +Pagse for password reset and email verification can be localized with the `pages` option in the Parse Server configuration: + +```js +const api = new ParseServer({ + ...otherOptions, + + pages: { + enableRouter: true, // Enables the experimental feature; required for localization + enableLocalization: true, + } +} +``` + +Localzation is achieved by matching a request-supplied `locale` parameter with a localized page. The locale can be supplied in either the request query, body or header with the following key: +- query: `locale` +- body: `locale` +- header: `x-parse-page-param-locale` + +For example, a password reset link with the locale parameter in the query could look like this: +``` +http://example.com/parse/apps/[appId]/request_password_reset?token=[token]&username=[username]&locale=de-AT +``` + +Localized pages are determined by the directory structure: +``` +root/ +├── base/ // base path to files +│ ├── example.html // default file +│ └── de/ // de language folder +│ │ └── example.html // de localized file +│ └── de-AT/ // de-AT locale folder +│ │ └── example.html // de-AT localized file + +Files are matched with the locale in the following order: +1. Locale match, e.g. locale `de-AT` matches file in folder `de-AT`. +2. Language match, e.g. locale `de-CH` matches file in folder `de`. +3. Default; file in base folder is returned. +``` + +Localization is only enabled for the default pages in the `public` directory; localization is disabled if `customUrls` are set (even if the custom URLs point to the default pages). +##### Configuration example +```js +const api = new ParseServer({ + ...otherOptions, + + pages: { + enableRouter: true, // Enables the experimental feature; required for localization + enableLocalization: true, + forceRedirect: false, + pagesPath: './public/pages', + pagesEndpoint: 'pages', + customUrls: { + passwordReset: 'https://example.com/page.html' + } + } +} +``` +##### Parameters + +| Parameter | Optional | Type | Default value | Example values | Environment variable | Description | +|-------------------------------------------------|----------|-----------|----------------------------------------|------------------------------------------------------|-----------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `pages` | yes | `Object` | `undefined` | - | `PARSE_SERVER_PAGES` | The options for pages such as password reset and email verification. | +| `pages.enableRouter` | yes | `Boolean` | `false` | - | `PARSE_SERVER_PAGES_ENABLE_ROUTER` | Is `true` if the pages router should be enabled; this is required for any of the pages options to take effect. **Caution, this is an experimental feature that may not be appropriate for production.** | +| `pages.forceRedirect` | yes | `Boolean` | `false` | - | `PARSE_SERVER_PAGES_FORCE_REDIRECT` | Is `true` if responses should always be redirects and never content, `false` if the response type should depend on the request type (`GET` request -> content response; `POST` request -> redirect response). | +| `pages.pagesPath` | yes | `String` | `./public` | `./files/pages`, `../../pages` | `PARSE_SERVER_PAGES_PAGES_PATH` | The path to the pages directory; this also defines where the static endpoint `/apps` points to. | +| `pages.pagesEndpoint` | yes | `String` | `apps` | - | `PARSE_SERVER_PAGES_PAGES_ENDPOINT` | The API endoint for the pages. | +| `pages.customUrls` | yes | `Object` | `{}` | `{ passwordReset: 'https://example.com/page.html' }` | `PARSE_SERVER_PAGES_CUSTOM_URLS` | The URLs to the custom pages | +| `pages.customUrls.passwordReset` | yes | `String` | `password_reset.html` | - | `PARSE_SERVER_PAGES_CUSTOM_URL_PASSWORD_RESET` | The URL to the custom page for password reset. | +| `pages.customUrls.passwordResetSuccess` | yes | `String` | `password_reset_success.html` | - | `PARSE_SERVER_PAGES_CUSTOM_URL_PASSWORD_RESET_SUCCESS` | The URL to the custom page for password reset -> success. | +| `pages.customUrls.passwordResetLinkInvalid` | yes | `String` | `password_reset_link_invalid.html` | - | `PARSE_SERVER_PAGES_CUSTOM_URL_PASSWORD_RESET_LINK_INVALID` | The URL to the custom page for password reset -> link invalid. | +| `pages.customUrls.emailVerificationSuccess` | yes | `String` | `email_verification_success.html` | - | `PARSE_SERVER_PAGES_CUSTOM_URL_EMAIL_VERIFICATION_SUCCESS` | The URL to the custom page for email verification -> success. | +| `pages.customUrls.emailVerificationSendFail` | yes | `String` | `email_verification_send_fail.html` | - | `PARSE_SERVER_PAGES_CUSTOM_URL_EMAIL_VERIFICATION_SEND_FAIL` | The URL to the custom page for email verification -> link send fail. | +| `pages.customUrls.emailVerificationSendSuccess` | yes | `String` | `email_verification_send_success.html` | - | `PARSE_SERVER_PAGES_CUSTOM_URL_EMAIL_VERIFICATION_SEND_SUCCESS` | The URL to the custom page for email verification -> resend link -> success. | +| `pages.customUrls.emailVerificationLinkInvalid` | yes | `String` | `email_verification_link_invalid.html` | - | `PARSE_SERVER_PAGES_CUSTOM_URL_EMAIL_VERIFICATION_LINK_INVALID` | The URL to the custom page for email verification -> link invalid. | +| `pages.customUrls.emailVerificationLinkExpired` | yes | `String` | `email_verification_link_expired.html` | - | `PARSE_SERVER_PAGES_CUSTOM_URL_EMAIL_VERIFICATION_LINK_EXPIRED` | The URL to the custom page for email verification -> link expired. | + +##### Notes + +- In combination with the [Parse Server API Mail Adapter](https://www.npmjs.com/package/parse-server-api-mail-adapter) Parse Server provides a fully localized flow (emails, pages) for the user. The email adapter sends out a localized email and adds a locale parameter to the password reset / email verification link, which is then used to respond with localized pages. + + ### Logging Parse Server will, by default, log: From 1573806bf572a98e60ce5feb09b2c0d58791b03d Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Thu, 21 Jan 2021 02:40:33 +0100 Subject: [PATCH 34/48] fixed readme TOC - hasn't been updated in a while --- README.md | 69 +++++++++++++++++++++++-------------------------------- 1 file changed, 29 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index 0cf9b18859..3e9ee52592 100644 --- a/README.md +++ b/README.md @@ -46,47 +46,36 @@ The full documentation for Parse Server is available in the [wiki](https://githu - [Getting Started](#getting-started) - [Running Parse Server](#running-parse-server) - [Locally](#locally) - - [Inside a Docker container](#inside-a-docker-container) - - [Running the Parse Server Image](#running-the-parse-server-image) - - [Saving your first object](#saving-your-first-object) - - [Connect your app to Parse Server](#connect-your-app-to-parse-server) + - [Docker Container](#docker-container) + - [Saving an Object](#saving-an-object) + - [Connect an SDK](#connect-an-sdk) - [Running Parse Server elsewhere](#running-parse-server-elsewhere) - - [Parse Server Sample Application](#parse-server-sample-application) + - [Sample Application](#sample-application) - [Parse Server + Express](#parse-server--express) - [Configuration](#configuration) - - [Basic options](#basic-options) - - [Client key options](#client-key-options) - - [Email verification and password reset](#email-verification-and-password-reset) + - [Basic Options](#basic-options) + - [Client Key Options](#client-key-options) + - [Email Verification and Password Reset](#email-verification-and-password-reset) - [Custom Pages](#custom-pages) - - [Using environment variables to configure Parse Server](#using-environment-variables-to-configure-parse-server) + - [Using Environment Variables](#using-environment-variables) - [Available Adapters](#available-adapters) - [Configuring File Adapters](#configuring-file-adapters) - [Idempodency Enforcement](#idempodency-enforcement) - - [Configuration example](#configuration-example) - - [Parameters](#parameters) - - [Notes](#notes) - [Localization](#localization) - - [Pages](#pages) - - [Configuration example](#configuration-example-1) - - [Parameters](#parameters-1) - - [Notes](#notes-1) - [Logging](#logging) -- [Live Queries](#live-queries) +- [Live Query](#live-query) - [GraphQL](#graphql) - [Running](#running) - [Using the CLI](#using-the-cli) - [Using Docker](#using-docker) - - [Running the Parse Server Image](#running-the-parse-server-image-1) - [Using Express.js](#using-expressjs) - [Checking the API health](#checking-the-api-health) - [Creating your first class](#creating-your-first-class) - [Using automatically generated operations](#using-automatically-generated-operations) - [Customizing your GraphQL Schema](#customizing-your-graphql-schema) - - [Creating your first custom query](#creating-your-first-custom-query) - [Learning more](#learning-more) - [Upgrading to 3.0.0](#upgrading-to-300) - [Want to ride the bleeding edge?](#want-to-ride-the-bleeding-edge) - - [Experimenting](#experimenting) - [Contributing](#contributing) - [Contributors](#contributors) - [Sponsors](#sponsors) @@ -114,7 +103,7 @@ $ parse-server --appId APPLICATION_ID --masterKey MASTER_KEY --databaseURI mongo ***Note:*** *If installation with* `-g` *fails due to permission problems* (`npm ERR! code 'EACCES'`), *please refer to [this link](https://docs.npmjs.com/getting-started/fixing-npm-permissions).* -### Inside a Docker container +### Docker Container ```bash $ git clone https://github.com/parse-community/parse-server @@ -123,7 +112,7 @@ $ docker build --tag parse-server . $ docker run --name my-mongo -d mongo ``` -#### Running the Parse Server Image +#### Running the Parse Server Image ```bash $ docker run --name my-parse-server -v config-vol:/parse-server/config -p 1337:1337 --link my-mongo:mongo -d parse-server --appId APPLICATION_ID --masterKey MASTER_KEY --databaseURI mongodb://mongo/test @@ -137,7 +126,7 @@ That's it! You are now running a standalone version of Parse Server on your mach **Using a remote MongoDB?** Pass the `--databaseURI DATABASE_URI` parameter when starting `parse-server`. Learn more about configuring Parse Server [here](#configuration). For a full list of available options, run `parse-server --help`. -### Saving your first object +### Saving an Object Now that you're running Parse Server, it is time to save your first object. We'll use the [REST API](http://docs.parseplatform.org/rest/guide), but you can easily do the same using any of the [Parse SDKs](http://parseplatform.org/#sdks). Run the following: @@ -203,7 +192,7 @@ $ curl -X GET \ To learn more about using saving and querying objects on Parse Server, check out the [Parse documentation](http://docs.parseplatform.org). -### Connect your app to Parse Server +### Connect an SDK Parse provides SDKs for all the major platforms. Refer to the Parse Server guide to [learn how to connect your app to Parse Server](https://docs.parseplatform.org/parse-server/guide/#using-parse-sdks-with-parse-server). @@ -211,7 +200,7 @@ Parse provides SDKs for all the major platforms. Refer to the Parse Server guide Once you have a better understanding of how the project works, please refer to the [Parse Server wiki](https://github.com/parse-community/parse-server/wiki) for in-depth guides to deploy Parse Server to major infrastructure providers. Read on to learn more about additional ways of running Parse Server. -### Parse Server Sample Application +### Sample Application We have provided a basic [Node.js application](https://github.com/parse-community/parse-server-example) that uses the Parse Server module on Express and can be easily deployed to various infrastructure providers: @@ -260,7 +249,7 @@ Parse Server can be configured using the following options. You may pass these a For the full list of available options, run `parse-server --help` or take a look at [Parse Server Configurations](http://parseplatform.org/parse-server/api/master/ParseServerOptions.html). -### Basic options +### Basic Options * `appId` **(required)** - The application id to host with this server instance. You can use any arbitrary string. For migrated apps, this should match your hosted Parse app. * `masterKey` **(required)** - The master key to use for overriding ACL security. You can use any arbitrary string. Keep it secret! For migrated apps, this should match your hosted Parse app. @@ -270,7 +259,7 @@ For the full list of available options, run `parse-server --help` or take a look * `cloud` - The absolute path to your cloud code `main.js` file. * `push` - Configuration options for APNS and GCM push. See the [Push Notifications quick start](http://docs.parseplatform.org/parse-server/guide/#push-notifications_push-notifications-quick-start). -### Client key options +### Client Key Options The client keys used with Parse are no longer necessary with Parse Server. If you wish to still require them, perhaps to be able to refuse access to older clients, you can set the keys at initialization time. Setting any of these keys will require all requests to provide one of the configured keys. @@ -279,7 +268,7 @@ The client keys used with Parse are no longer necessary with Parse Server. If yo * `restAPIKey` * `dotNetKey` -### Email verification and password reset +### Email Verification and Password Reset Verifying user email addresses and enabling password reset via email requires an email adapter. As part of the `parse-server` package we provide an adapter for sending email through Mailgun. To use it, sign up for Mailgun, and add this to your initialization code: @@ -380,7 +369,7 @@ var server = ParseServer({ }) ``` -### Using environment variables to configure Parse Server +### Using Environment Variables You may configure the Parse Server using environment variables: @@ -431,7 +420,7 @@ Identical requests are identified by their request header `X-Parse-Request-Id`. Deduplication is only done for object creation and update (`POST` and `PUT` requests). Deduplication is not done for object finding and deletion (`GET` and `DELETE` requests), as these operations are already idempotent by definition. -#### Configuration example +#### Configuration example ``` let api = new ParseServer({ idempotencyOptions: { @@ -440,7 +429,7 @@ let api = new ParseServer({ } } ``` -#### Parameters +#### Parameters | Parameter | Optional | Type | Default value | Example values | Environment variable | Description | |----------------------------|----------|-----------------|---------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| @@ -448,13 +437,13 @@ let api = new ParseServer({ | `idempotencyOptions.paths` | yes | `Array` | `[]` | `.*` (all paths, includes the examples below),
`functions/.*` (all functions),
`jobs/.*` (all jobs),
`classes/.*` (all classes),
`functions/.*` (all functions),
`users` (user creation / update),
`installations` (installation creation / update) | PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_PATHS | An array of path patterns that have to match the request path for request deduplication to be enabled. The mount path must not be included, for example to match the request path `/parse/functions/myFunction` specifiy the path pattern `functions/myFunction`. A trailing slash of the request path is ignored, for example the path pattern `functions/myFunction` matches both `/parse/functions/myFunction` and `/parse/functions/myFunction/`. | | `idempotencyOptions.ttl` | yes | `Integer` | `300` | `60` (60 seconds) | PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_TTL | The duration in seconds after which a request record is discarded from the database. Duplicate requests due to network issues can be expected to arrive within milliseconds up to several seconds. This value must be greater than `0`. | -#### Notes +#### Notes - This feature is currently only available for MongoDB and not for Postgres. ### Localization -#### Pages +#### Pages **Caution, this is an experimental feature that may not be appropriate for production.** Pagse for password reset and email verification can be localized with the `pages` option in the Parse Server configuration: @@ -497,7 +486,7 @@ Files are matched with the locale in the following order: ``` Localization is only enabled for the default pages in the `public` directory; localization is disabled if `customUrls` are set (even if the custom URLs point to the default pages). -##### Configuration example +##### Configuration example ```js const api = new ParseServer({ ...otherOptions, @@ -514,7 +503,7 @@ const api = new ParseServer({ } } ``` -##### Parameters +##### Parameters | Parameter | Optional | Type | Default value | Example values | Environment variable | Description | |-------------------------------------------------|----------|-----------|----------------------------------------|------------------------------------------------------|-----------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| @@ -533,7 +522,7 @@ const api = new ParseServer({ | `pages.customUrls.emailVerificationLinkInvalid` | yes | `String` | `email_verification_link_invalid.html` | - | `PARSE_SERVER_PAGES_CUSTOM_URL_EMAIL_VERIFICATION_LINK_INVALID` | The URL to the custom page for email verification -> link invalid. | | `pages.customUrls.emailVerificationLinkExpired` | yes | `String` | `email_verification_link_expired.html` | - | `PARSE_SERVER_PAGES_CUSTOM_URL_EMAIL_VERIFICATION_LINK_EXPIRED` | The URL to the custom page for email verification -> link expired. | -##### Notes +##### Notes - In combination with the [Parse Server API Mail Adapter](https://www.npmjs.com/package/parse-server-api-mail-adapter) Parse Server provides a fully localized flow (emails, pages) for the user. The email adapter sends out a localized email and adds a locale parameter to the password reset / email verification link, which is then used to respond with localized pages. @@ -554,7 +543,7 @@ Logs are also viewable in Parse Dashboard. **Want new line delimited JSON error logs (for consumption by CloudWatch, Google Cloud Logging, etc)?** Pass the `JSON_LOGS` environment variable when starting `parse-server`. Usage :- `JSON_LOGS='1' parse-server --appId APPLICATION_ID --masterKey MASTER_KEY` -# Live Queries +# Live Query Live queries are meant to be used in real-time reactive applications, where just using the traditional query paradigm could cause several problems, like increased response time and high network and server usage. Live queries should be used in cases where you need to continuously update a page with fresh data coming from the database, which often happens in (but is not limited to) online games, messaging clients and shared to-do lists. @@ -591,7 +580,7 @@ $ docker build --tag parse-server . $ docker run --name my-mongo -d mongo ``` -#### Running the Parse Server Image +#### Running the Parse Server Image ```bash $ docker run --name my-parse-server --link my-mongo:mongo -v config-vol:/parse-server/config -p 1337:1337 -d parse-server --appId APPLICATION_ID --masterKey MASTER_KEY --databaseURI mongodb://mongo/test --publicServerURL http://localhost:1337/parse --mountGraphQL --mountPlayground @@ -838,7 +827,7 @@ To start creating your custom schema, you need to code a `schema.graphql` file a $ parse-server --appId APPLICATION_ID --masterKey MASTER_KEY --databaseURI mongodb://localhost/test --publicServerURL http://localhost:1337/parse --cloud ./cloud/main.js --graphQLSchema ./cloud/schema.graphql --mountGraphQL --mountPlayground ``` -### Creating your first custom query +### Creating your first custom query Use the code below for your `schema.graphql` and `main.js` files. Then restart your Parse Server. @@ -900,7 +889,7 @@ directly on this branch: npm install parse-community/parse-server.git#master ``` -## Experimenting +## Experimenting You can also use your own forks, and work in progress branches by specifying them: From fa7a753e5903ac7a479bfac64fe5ce359512533c Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Sun, 31 Jan 2021 21:54:40 +0100 Subject: [PATCH 35/48] added localization with JSON resource --- package-lock.json | 4297 ++++++++++++------------------------ public/custom_json.html | 17 + public/custom_json.json | 23 + spec/PagesRouter.spec.js | 347 ++- src/Config.js | 16 +- src/Options/Definitions.js | 1366 ++++++------ src/Options/docs.js | 3 +- src/Options/index.js | 7 +- src/ParseServer.js | 9 +- src/Routers/PagesRouter.js | 243 +- src/Utils.js | 38 +- 11 files changed, 2680 insertions(+), 3686 deletions(-) create mode 100644 public/custom_json.html create mode 100644 public/custom_json.json diff --git a/package-lock.json b/package-lock.json index 9328ac5184..068bfb50e1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,39 +5,31 @@ "requires": true, "dependencies": { "@apollo/client": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/@apollo/client/-/client-3.2.5.tgz", - "integrity": "sha512-zpruxnFMz6K94gs2pqc3sidzFDbQpKT5D6P/J/I9s8ekHZ5eczgnRp6pqXC86Bh7+44j/btpmOT0kwiboyqTnA==", + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/@apollo/client/-/client-3.3.7.tgz", + "integrity": "sha512-Cb0OqqvlehlRHtHIXRIS/Pe5WYU4hHl1FznXTRSxBAN42WmBUM3zy/Unvw183RdWMyV6Kc2pFKOEuaG1K7JTAQ==", "requires": { "@graphql-typed-document-node/core": "^3.0.0", "@types/zen-observable": "^0.8.0", "@wry/context": "^0.5.2", - "@wry/equality": "^0.2.0", + "@wry/equality": "^0.3.0", "fast-json-stable-stringify": "^2.0.0", "graphql-tag": "^2.11.0", "hoist-non-react-statics": "^3.3.2", - "optimism": "^0.13.0", + "optimism": "^0.14.0", "prop-types": "^15.7.2", "symbol-observable": "^2.0.0", - "ts-invariant": "^0.4.4", + "ts-invariant": "^0.6.0", "tslib": "^1.10.0", "zen-observable": "^0.8.14" }, "dependencies": { - "@wry/context": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/@wry/context/-/context-0.5.2.tgz", - "integrity": "sha512-B/JLuRZ/vbEKHRUiGj6xiMojST1kHhu4WcreLfNN7q9DqQFrb97cWgf/kiYsPSUCAMVN0HzfFc8XjJdzgZzfjw==", - "requires": { - "tslib": "^1.9.3" - } - }, "@wry/equality": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@wry/equality/-/equality-0.2.0.tgz", - "integrity": "sha512-Y4d+WH6hs+KZJUC8YKLYGarjGekBrhslDbf/R20oV+AakHPINSitHfDRQz3EGcEWc1luXYNUvMhawWtZVWNGvQ==", + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@wry/equality/-/equality-0.3.1.tgz", + "integrity": "sha512-8/Ftr3jUZ4EXhACfSwPIfNsE8V6WKesdjp+Dxi78Bej6qlasAxiz0/F8j0miACRj9CL4vC5Y5FsfwwEYAuhWbg==", "requires": { - "tslib": "^1.9.3" + "tslib": "^1.14.1" } }, "graphql-tag": { @@ -45,18 +37,20 @@ "resolved": "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.11.0.tgz", "integrity": "sha512-VmsD5pJqWJnQZMUeRwrDhfgoyqcfwEkvtpANqcoUG8/tOLkwNgU9mzub/Mc78OJMhHjx7gfAMTxzdG43VGg3bA==" }, - "optimism": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/optimism/-/optimism-0.13.0.tgz", - "integrity": "sha512-6JAh3dH+YUE4QUdsgUw8nUQyrNeBKfAEKOHMlLkQ168KhIYFIxzPsHakWrRXDnTO+x61RJrS3/2uEt6W0xlocA==", + "ts-invariant": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/ts-invariant/-/ts-invariant-0.6.0.tgz", + "integrity": "sha512-caoafsfgb8QxdrKzFfjKt627m4i8KTtfAiji0DYJfWI4A/S9ORNNpzYuD9br64kyKFgxn9UNaLLbSupam84mCA==", "requires": { - "@wry/context": "^0.5.2" + "@types/ungap__global-this": "^0.3.1", + "@ungap/global-this": "^0.4.2", + "tslib": "^1.9.3" } }, - "symbol-observable": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-2.0.3.tgz", - "integrity": "sha512-sQV7phh2WCYAn81oAkakC5qjq2Ml0g8ozqz03wOGnx9dDlG1de6yrF+0RAzSJD8fPUow3PTSMf2SAbOGxb93BA==" + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" } } }, @@ -81,9 +75,9 @@ }, "dependencies": { "@types/node": { - "version": "10.17.50", - "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.50.tgz", - "integrity": "sha512-vwX+/ija9xKc/z9VqMCdbf4WYcMTGsI0I/L/6shIF3qXURxZOhPQlPRHtjTpiNhAwn0paMJzlOQqw6mAGEQnTA==" + "version": "10.17.51", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.51.tgz", + "integrity": "sha512-KANw+MkL626tq90l++hGelbl67irOJzGhUJk6a1Bt8QHOeh9tztJx+L0AqttraWKinmZn7Qi5lJZJzx45Gq0dg==" } } }, @@ -109,13 +103,6 @@ "integrity": "sha512-vyrkEHG1jrukmzTPtyWB4NLPauUw5bQeg4uhn8f+1SSynmrOcyvlb1GKQjjgoBzElLdfXCRYX8UnBlhklOHYRQ==", "requires": { "tslib": "~2.0.1" - }, - "dependencies": { - "tslib": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz", - "integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==" - } } }, "@babel/cli": { @@ -150,32 +137,19 @@ } }, "@babel/code-frame": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.5.5.tgz", - "integrity": "sha512-27d4lZoomVyo51VegxI20xZPuSHusqbQag/ztrBC7wegWoQ1nLREPVSKSW8byhTlzTKyNE4ifaTA6lCp7JjpFw==", + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", + "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", "dev": true, "requires": { - "@babel/highlight": "^7.0.0" + "@babel/highlight": "^7.10.4" } }, "@babel/compat-data": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.10.4.tgz", - "integrity": "sha512-t+rjExOrSVvjQQXNp5zAIYDp00KjdvGl/TpDX5REPr0S9IAIPQMTilcfG6q8c0QFmj9lSTVySV2VTsyggvtNIw==", - "dev": true, - "requires": { - "browserslist": "^4.12.0", - "invariant": "^2.2.4", - "semver": "^5.5.0" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } - } + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.12.7.tgz", + "integrity": "sha512-YaxPMGs/XIWtYqrdEOZOCPsVWfEoriXopnsz3/i7apYPXQ3698UFhS6dVT1KN5qOsWmVgw/FOrmQgpRaZayGsw==", + "dev": true }, "@babel/core": { "version": "7.10.0", @@ -201,121 +175,21 @@ "source-map": "^0.5.0" }, "dependencies": { - "@babel/code-frame": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", - "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", - "dev": true, - "requires": { - "@babel/highlight": "^7.10.4" - } - }, - "@babel/generator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.10.4.tgz", - "integrity": "sha512-toLIHUIAgcQygFZRAQcsLQV3CBuX6yOIru1kJk/qqqvcRmZrYe6WavZTSG+bB8MxhnL9YPf+pKQfuiP161q7ng==", - "dev": true, - "requires": { - "@babel/types": "^7.10.4", - "jsesc": "^2.5.1", - "lodash": "^4.17.13", - "source-map": "^0.5.0" - } - }, - "@babel/helper-function-name": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", - "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", - "dev": true, - "requires": { - "@babel/helper-get-function-arity": "^7.10.4", - "@babel/template": "^7.10.4", - "@babel/types": "^7.10.4" - } - }, - "@babel/helper-get-function-arity": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", - "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", - "dev": true, - "requires": { - "@babel/types": "^7.10.4" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.10.4.tgz", - "integrity": "sha512-pySBTeoUff56fL5CBU2hWm9TesA4r/rOkI9DyJLvvgz09MB9YtfIYe3iBriVaYNaPe+Alua0vBIOVOLs2buWhg==", - "dev": true, - "requires": { - "@babel/types": "^7.10.4" - } - }, - "@babel/highlight": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", - "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" + "ms": "2.1.2" } }, - "@babel/parser": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.10.4.tgz", - "integrity": "sha512-8jHII4hf+YVDsskTF6WuMB3X4Eh+PsUkC2ljq22so5rHvH+T8BzyL94VOdyFLNR8tBSVXOTbNHOKpR4TfRxVtA==", + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, - "@babel/template": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", - "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.10.4", - "@babel/parser": "^7.10.4", - "@babel/types": "^7.10.4" - } - }, - "@babel/traverse": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.10.4.tgz", - "integrity": "sha512-aSy7p5THgSYm4YyxNGz6jZpXf+Ok40QF3aA2LyIONkDHpAcJzDUqlCKXv6peqYUs2gmic849C/t2HKw2a2K20Q==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.10.4", - "@babel/generator": "^7.10.4", - "@babel/helper-function-name": "^7.10.4", - "@babel/helper-split-export-declaration": "^7.10.4", - "@babel/parser": "^7.10.4", - "@babel/types": "^7.10.4", - "debug": "^4.1.0", - "globals": "^11.1.0", - "lodash": "^4.17.13" - } - }, - "@babel/types": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.4.tgz", - "integrity": "sha512-UTCFOxC3FsFHb7lkRMVvgLzaRVamXuAs2Tz4wajva4WxtVY82eZeaUBtC2Zt95FU9TiznuC0Zk35tsim8jeVpg==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "lodash": "^4.17.13", - "to-fast-properties": "^2.0.0" - } - }, - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, "semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", @@ -331,14 +205,13 @@ } }, "@babel/generator": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.7.4.tgz", - "integrity": "sha512-m5qo2WgdOJeyYngKImbkyQrnUN1mPceaG5BV+G0E3gWsa4l/jCSryWJdM2x8OuGAOyh+3d5pVYfZWCiNFtynxg==", + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.12.11.tgz", + "integrity": "sha512-Ggg6WPOJtSi8yYQvLVjG8F/TlpWDlKx0OpS4Kt+xMQPs5OaGYWy+v1A+1TvxI6sAMGZpKWWoAQ1DaeQbImlItA==", "dev": true, "requires": { - "@babel/types": "^7.7.4", + "@babel/types": "^7.12.11", "jsesc": "^2.5.1", - "lodash": "^4.17.13", "source-map": "^0.5.0" }, "dependencies": { @@ -351,25 +224,12 @@ } }, "@babel/helper-annotate-as-pure": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.4.tgz", - "integrity": "sha512-XQlqKQP4vXFB7BN8fEEerrmYvHp3fK/rBkRFz9jaJbzK0B1DSfej9Kc7ZzE8Z/OnId1jpJdNAZ3BFQjWG68rcA==", + "version": "7.12.10", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.12.10.tgz", + "integrity": "sha512-XplmVbC1n+KY6jL8/fgLVXXUauDIB+lD5+GsQEh6F6GBF1dq1qy4DP4yXWzDKcoqXB3X58t61e85Fitoww4JVQ==", "dev": true, "requires": { - "@babel/types": "^7.10.4" - }, - "dependencies": { - "@babel/types": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.4.tgz", - "integrity": "sha512-UTCFOxC3FsFHb7lkRMVvgLzaRVamXuAs2Tz4wajva4WxtVY82eZeaUBtC2Zt95FU9TiznuC0Zk35tsim8jeVpg==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "lodash": "^4.17.13", - "to-fast-properties": "^2.0.0" - } - } + "@babel/types": "^7.12.10" } }, "@babel/helper-builder-binary-assignment-operator-visitor": { @@ -380,31 +240,17 @@ "requires": { "@babel/helper-explode-assignable-expression": "^7.10.4", "@babel/types": "^7.10.4" - }, - "dependencies": { - "@babel/types": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.4.tgz", - "integrity": "sha512-UTCFOxC3FsFHb7lkRMVvgLzaRVamXuAs2Tz4wajva4WxtVY82eZeaUBtC2Zt95FU9TiznuC0Zk35tsim8jeVpg==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "lodash": "^4.17.13", - "to-fast-properties": "^2.0.0" - } - } } }, "@babel/helper-compilation-targets": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.10.4.tgz", - "integrity": "sha512-a3rYhlsGV0UHNDvrtOXBg8/OpfV0OKTkxKPzIplS1zpx7CygDcWWxckxZeDd3gzPzC4kUT0A4nVFDK0wGMh4MQ==", + "version": "7.12.5", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.12.5.tgz", + "integrity": "sha512-+qH6NrscMolUlzOYngSBMIOQpKUGPPsc61Bu5W10mg84LxZ7cmvnBHzARKbDoFxVvqqAbj6Tg6N7bSrWSPXMyw==", "dev": true, "requires": { - "@babel/compat-data": "^7.10.4", - "browserslist": "^4.12.0", - "invariant": "^2.2.4", - "levenary": "^1.1.1", + "@babel/compat-data": "^7.12.5", + "@babel/helper-validator-option": "^7.12.1", + "browserslist": "^4.14.5", "semver": "^5.5.0" }, "dependencies": { @@ -417,347 +263,66 @@ } }, "@babel/helper-create-class-features-plugin": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.10.4.tgz", - "integrity": "sha512-9raUiOsXPxzzLjCXeosApJItoMnX3uyT4QdM2UldffuGApNrF8e938MwNpDCK9CPoyxrEoCgT+hObJc3mZa6lQ==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.12.1.tgz", + "integrity": "sha512-hkL++rWeta/OVOBTRJc9a5Azh5mt5WgZUGAKMD8JM141YsE08K//bp1unBBieO6rUKkIPyUE0USQ30jAy3Sk1w==", "dev": true, "requires": { "@babel/helper-function-name": "^7.10.4", - "@babel/helper-member-expression-to-functions": "^7.10.4", + "@babel/helper-member-expression-to-functions": "^7.12.1", "@babel/helper-optimise-call-expression": "^7.10.4", - "@babel/helper-plugin-utils": "^7.10.4", - "@babel/helper-replace-supers": "^7.10.4", + "@babel/helper-replace-supers": "^7.12.1", "@babel/helper-split-export-declaration": "^7.10.4" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", - "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", - "dev": true, - "requires": { - "@babel/highlight": "^7.10.4" - } - }, - "@babel/helper-function-name": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", - "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", - "dev": true, - "requires": { - "@babel/helper-get-function-arity": "^7.10.4", - "@babel/template": "^7.10.4", - "@babel/types": "^7.10.4" - } - }, - "@babel/helper-get-function-arity": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", - "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", - "dev": true, - "requires": { - "@babel/types": "^7.10.4" - } - }, - "@babel/helper-plugin-utils": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", - "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", - "dev": true - }, - "@babel/helper-split-export-declaration": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.10.4.tgz", - "integrity": "sha512-pySBTeoUff56fL5CBU2hWm9TesA4r/rOkI9DyJLvvgz09MB9YtfIYe3iBriVaYNaPe+Alua0vBIOVOLs2buWhg==", - "dev": true, - "requires": { - "@babel/types": "^7.10.4" - } - }, - "@babel/highlight": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", - "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, - "@babel/parser": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.10.4.tgz", - "integrity": "sha512-8jHII4hf+YVDsskTF6WuMB3X4Eh+PsUkC2ljq22so5rHvH+T8BzyL94VOdyFLNR8tBSVXOTbNHOKpR4TfRxVtA==", - "dev": true - }, - "@babel/template": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", - "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.10.4", - "@babel/parser": "^7.10.4", - "@babel/types": "^7.10.4" - } - }, - "@babel/types": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.4.tgz", - "integrity": "sha512-UTCFOxC3FsFHb7lkRMVvgLzaRVamXuAs2Tz4wajva4WxtVY82eZeaUBtC2Zt95FU9TiznuC0Zk35tsim8jeVpg==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "lodash": "^4.17.13", - "to-fast-properties": "^2.0.0" - } - } } }, "@babel/helper-create-regexp-features-plugin": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.10.4.tgz", - "integrity": "sha512-2/hu58IEPKeoLF45DBwx3XFqsbCXmkdAay4spVr2x0jYgRxrSNp+ePwvSsy9g6YSaNDcKIQVPXk1Ov8S2edk2g==", + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.12.7.tgz", + "integrity": "sha512-idnutvQPdpbduutvi3JVfEgcVIHooQnhvhx0Nk9isOINOIGYkZea1Pk2JlJRiUnMefrlvr0vkByATBY/mB4vjQ==", "dev": true, "requires": { "@babel/helper-annotate-as-pure": "^7.10.4", - "@babel/helper-regex": "^7.10.4", - "regexpu-core": "^4.7.0" + "regexpu-core": "^4.7.1" } }, "@babel/helper-define-map": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-define-map/-/helper-define-map-7.10.4.tgz", - "integrity": "sha512-nIij0oKErfCnLUCWaCaHW0Bmtl2RO9cN7+u2QT8yqTywgALKlyUVOvHDElh+b5DwVC6YB1FOYFOTWcN/+41EDA==", + "version": "7.10.5", + "resolved": "https://registry.npmjs.org/@babel/helper-define-map/-/helper-define-map-7.10.5.tgz", + "integrity": "sha512-fMw4kgFB720aQFXSVaXr79pjjcW5puTCM16+rECJ/plGS+zByelE8l9nCpV1GibxTnFVmUuYG9U8wYfQHdzOEQ==", "dev": true, "requires": { "@babel/helper-function-name": "^7.10.4", - "@babel/types": "^7.10.4", - "lodash": "^4.17.13" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", - "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", - "dev": true, - "requires": { - "@babel/highlight": "^7.10.4" - } - }, - "@babel/helper-function-name": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", - "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", - "dev": true, - "requires": { - "@babel/helper-get-function-arity": "^7.10.4", - "@babel/template": "^7.10.4", - "@babel/types": "^7.10.4" - } - }, - "@babel/helper-get-function-arity": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", - "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", - "dev": true, - "requires": { - "@babel/types": "^7.10.4" - } - }, - "@babel/highlight": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", - "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, - "@babel/parser": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.10.4.tgz", - "integrity": "sha512-8jHII4hf+YVDsskTF6WuMB3X4Eh+PsUkC2ljq22so5rHvH+T8BzyL94VOdyFLNR8tBSVXOTbNHOKpR4TfRxVtA==", - "dev": true - }, - "@babel/template": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", - "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.10.4", - "@babel/parser": "^7.10.4", - "@babel/types": "^7.10.4" - } - }, - "@babel/types": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.4.tgz", - "integrity": "sha512-UTCFOxC3FsFHb7lkRMVvgLzaRVamXuAs2Tz4wajva4WxtVY82eZeaUBtC2Zt95FU9TiznuC0Zk35tsim8jeVpg==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "lodash": "^4.17.13", - "to-fast-properties": "^2.0.0" - } - } + "@babel/types": "^7.10.5", + "lodash": "^4.17.19" } }, "@babel/helper-explode-assignable-expression": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.10.4.tgz", - "integrity": "sha512-4K71RyRQNPRrR85sr5QY4X3VwG4wtVoXZB9+L3r1Gp38DhELyHCtovqydRi7c1Ovb17eRGiQ/FD5s8JdU0Uy5A==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.12.1.tgz", + "integrity": "sha512-dmUwH8XmlrUpVqgtZ737tK88v07l840z9j3OEhCLwKTkjlvKpfqXVIZ0wpK3aeOxspwGrf/5AP5qLx4rO3w5rA==", "dev": true, "requires": { - "@babel/traverse": "^7.10.4", - "@babel/types": "^7.10.4" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", - "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", - "dev": true, - "requires": { - "@babel/highlight": "^7.10.4" - } - }, - "@babel/generator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.10.4.tgz", - "integrity": "sha512-toLIHUIAgcQygFZRAQcsLQV3CBuX6yOIru1kJk/qqqvcRmZrYe6WavZTSG+bB8MxhnL9YPf+pKQfuiP161q7ng==", - "dev": true, - "requires": { - "@babel/types": "^7.10.4", - "jsesc": "^2.5.1", - "lodash": "^4.17.13", - "source-map": "^0.5.0" - } - }, - "@babel/helper-function-name": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", - "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", - "dev": true, - "requires": { - "@babel/helper-get-function-arity": "^7.10.4", - "@babel/template": "^7.10.4", - "@babel/types": "^7.10.4" - } - }, - "@babel/helper-get-function-arity": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", - "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", - "dev": true, - "requires": { - "@babel/types": "^7.10.4" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.10.4.tgz", - "integrity": "sha512-pySBTeoUff56fL5CBU2hWm9TesA4r/rOkI9DyJLvvgz09MB9YtfIYe3iBriVaYNaPe+Alua0vBIOVOLs2buWhg==", - "dev": true, - "requires": { - "@babel/types": "^7.10.4" - } - }, - "@babel/highlight": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", - "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, - "@babel/parser": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.10.4.tgz", - "integrity": "sha512-8jHII4hf+YVDsskTF6WuMB3X4Eh+PsUkC2ljq22so5rHvH+T8BzyL94VOdyFLNR8tBSVXOTbNHOKpR4TfRxVtA==", - "dev": true - }, - "@babel/template": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", - "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.10.4", - "@babel/parser": "^7.10.4", - "@babel/types": "^7.10.4" - } - }, - "@babel/traverse": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.10.4.tgz", - "integrity": "sha512-aSy7p5THgSYm4YyxNGz6jZpXf+Ok40QF3aA2LyIONkDHpAcJzDUqlCKXv6peqYUs2gmic849C/t2HKw2a2K20Q==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.10.4", - "@babel/generator": "^7.10.4", - "@babel/helper-function-name": "^7.10.4", - "@babel/helper-split-export-declaration": "^7.10.4", - "@babel/parser": "^7.10.4", - "@babel/types": "^7.10.4", - "debug": "^4.1.0", - "globals": "^11.1.0", - "lodash": "^4.17.13" - } - }, - "@babel/types": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.4.tgz", - "integrity": "sha512-UTCFOxC3FsFHb7lkRMVvgLzaRVamXuAs2Tz4wajva4WxtVY82eZeaUBtC2Zt95FU9TiznuC0Zk35tsim8jeVpg==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "lodash": "^4.17.13", - "to-fast-properties": "^2.0.0" - } - }, - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - } + "@babel/types": "^7.12.1" } }, "@babel/helper-function-name": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.7.4.tgz", - "integrity": "sha512-AnkGIdiBhEuiwdoMnKm7jfPfqItZhgRaZfMg1XX3bS25INOnLPjPG1Ppnajh8eqgt5kPJnfqrRHqFqmjKDZLzQ==", + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.12.11.tgz", + "integrity": "sha512-AtQKjtYNolKNi6nNNVLQ27CP6D9oFR6bq/HPYSizlzbp7uC1M59XJe8L+0uXjbIaZaUJF99ruHqVGiKXU/7ybA==", "dev": true, "requires": { - "@babel/helper-get-function-arity": "^7.7.4", - "@babel/template": "^7.7.4", - "@babel/types": "^7.7.4" + "@babel/helper-get-function-arity": "^7.12.10", + "@babel/template": "^7.12.7", + "@babel/types": "^7.12.11" } }, "@babel/helper-get-function-arity": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.7.4.tgz", - "integrity": "sha512-QTGKEdCkjgzgfJ3bAyRwF4yyT3pg+vDgan8DSivq1eS0gwi+KGKE5x8kRcbeFTb/673mkO5SN1IZfmCfA5o+EA==", + "version": "7.12.10", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.10.tgz", + "integrity": "sha512-mm0n5BPjR06wh9mPQaDdXWDoll/j5UpCAPl1x8fS71GHm7HA6Ua2V4ylG1Ju8lvcTOietbPNNPaSilKj+pj+Ag==", "dev": true, "requires": { - "@babel/types": "^7.7.4" + "@babel/types": "^7.12.10" } }, "@babel/helper-hoist-variables": { @@ -767,915 +332,219 @@ "dev": true, "requires": { "@babel/types": "^7.10.4" - }, - "dependencies": { - "@babel/types": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.4.tgz", - "integrity": "sha512-UTCFOxC3FsFHb7lkRMVvgLzaRVamXuAs2Tz4wajva4WxtVY82eZeaUBtC2Zt95FU9TiznuC0Zk35tsim8jeVpg==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "lodash": "^4.17.13", - "to-fast-properties": "^2.0.0" - } - } } }, "@babel/helper-member-expression-to-functions": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.10.4.tgz", - "integrity": "sha512-m5j85pK/KZhuSdM/8cHUABQTAslV47OjfIB9Cc7P+PvlAoBzdb79BGNfw8RhT5Mq3p+xGd0ZfAKixbrUZx0C7A==", + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.12.7.tgz", + "integrity": "sha512-DCsuPyeWxeHgh1Dus7APn7iza42i/qXqiFPWyBDdOFtvS581JQePsc1F/nD+fHrcswhLlRc2UpYS1NwERxZhHw==", "dev": true, "requires": { - "@babel/types": "^7.10.4" - }, - "dependencies": { - "@babel/types": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.4.tgz", - "integrity": "sha512-UTCFOxC3FsFHb7lkRMVvgLzaRVamXuAs2Tz4wajva4WxtVY82eZeaUBtC2Zt95FU9TiznuC0Zk35tsim8jeVpg==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "lodash": "^4.17.13", - "to-fast-properties": "^2.0.0" - } - } + "@babel/types": "^7.12.7" } }, "@babel/helper-module-imports": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.10.4.tgz", - "integrity": "sha512-nEQJHqYavI217oD9+s5MUBzk6x1IlvoS9WTPfgG43CbMEeStE0v+r+TucWdx8KFGowPGvyOkDT9+7DHedIDnVw==", + "version": "7.12.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.12.5.tgz", + "integrity": "sha512-SR713Ogqg6++uexFRORf/+nPXMmWIn80TALu0uaFb+iQIUoR7bOC7zBWyzBs5b3tBBJXuyD0cRu1F15GyzjOWA==", "dev": true, "requires": { - "@babel/types": "^7.10.4" - }, - "dependencies": { - "@babel/types": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.4.tgz", - "integrity": "sha512-UTCFOxC3FsFHb7lkRMVvgLzaRVamXuAs2Tz4wajva4WxtVY82eZeaUBtC2Zt95FU9TiznuC0Zk35tsim8jeVpg==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "lodash": "^4.17.13", - "to-fast-properties": "^2.0.0" - } - } + "@babel/types": "^7.12.5" } }, "@babel/helper-module-transforms": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.10.4.tgz", - "integrity": "sha512-Er2FQX0oa3nV7eM1o0tNCTx7izmQtwAQsIiaLRWtavAAEcskb0XJ5OjJbVrYXWOTr8om921Scabn4/tzlx7j1Q==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.12.1.tgz", + "integrity": "sha512-QQzehgFAZ2bbISiCpmVGfiGux8YVFXQ0abBic2Envhej22DVXV9nCFaS5hIQbkyo1AdGb+gNME2TSh3hYJVV/w==", "dev": true, "requires": { - "@babel/helper-module-imports": "^7.10.4", - "@babel/helper-replace-supers": "^7.10.4", - "@babel/helper-simple-access": "^7.10.4", - "@babel/helper-split-export-declaration": "^7.10.4", + "@babel/helper-module-imports": "^7.12.1", + "@babel/helper-replace-supers": "^7.12.1", + "@babel/helper-simple-access": "^7.12.1", + "@babel/helper-split-export-declaration": "^7.11.0", + "@babel/helper-validator-identifier": "^7.10.4", "@babel/template": "^7.10.4", - "@babel/types": "^7.10.4", - "lodash": "^4.17.13" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", - "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", - "dev": true, - "requires": { - "@babel/highlight": "^7.10.4" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.10.4.tgz", - "integrity": "sha512-pySBTeoUff56fL5CBU2hWm9TesA4r/rOkI9DyJLvvgz09MB9YtfIYe3iBriVaYNaPe+Alua0vBIOVOLs2buWhg==", - "dev": true, - "requires": { - "@babel/types": "^7.10.4" - } - }, - "@babel/highlight": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", - "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, - "@babel/parser": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.10.4.tgz", - "integrity": "sha512-8jHII4hf+YVDsskTF6WuMB3X4Eh+PsUkC2ljq22so5rHvH+T8BzyL94VOdyFLNR8tBSVXOTbNHOKpR4TfRxVtA==", - "dev": true - }, - "@babel/template": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", - "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.10.4", - "@babel/parser": "^7.10.4", - "@babel/types": "^7.10.4" - } - }, - "@babel/types": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.4.tgz", - "integrity": "sha512-UTCFOxC3FsFHb7lkRMVvgLzaRVamXuAs2Tz4wajva4WxtVY82eZeaUBtC2Zt95FU9TiznuC0Zk35tsim8jeVpg==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "lodash": "^4.17.13", - "to-fast-properties": "^2.0.0" - } - } + "@babel/traverse": "^7.12.1", + "@babel/types": "^7.12.1", + "lodash": "^4.17.19" } }, "@babel/helper-optimise-call-expression": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.4.tgz", - "integrity": "sha512-n3UGKY4VXwXThEiKrgRAoVPBMqeoPgHVqiHZOanAJCG9nQUL2pLRQirUzl0ioKclHGpGqRgIOkgcIJaIWLpygg==", + "version": "7.12.10", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.12.10.tgz", + "integrity": "sha512-4tpbU0SrSTjjt65UMWSrUOPZTsgvPgGG4S8QSTNHacKzpS51IVWGDj0yCwyeZND/i+LSN2g/O63jEXEWm49sYQ==", "dev": true, "requires": { - "@babel/types": "^7.10.4" - }, - "dependencies": { - "@babel/types": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.4.tgz", - "integrity": "sha512-UTCFOxC3FsFHb7lkRMVvgLzaRVamXuAs2Tz4wajva4WxtVY82eZeaUBtC2Zt95FU9TiznuC0Zk35tsim8jeVpg==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "lodash": "^4.17.13", - "to-fast-properties": "^2.0.0" - } - } + "@babel/types": "^7.12.10" } }, "@babel/helper-plugin-utils": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", - "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==", - "dev": true - }, - "@babel/helper-regex": { "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-regex/-/helper-regex-7.10.4.tgz", - "integrity": "sha512-inWpnHGgtg5NOF0eyHlC0/74/VkdRITY9dtTpB2PrxKKn+AkVMRiZz/Adrx+Ssg+MLDesi2zohBW6MVq6b4pOQ==", - "dev": true, - "requires": { - "lodash": "^4.17.13" - } + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", + "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", + "dev": true }, "@babel/helper-remap-async-to-generator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.10.4.tgz", - "integrity": "sha512-86Lsr6NNw3qTNl+TBcF1oRZMaVzJtbWTyTko+CQL/tvNvcGYEFKbLXDPxtW0HKk3McNOk4KzY55itGWCAGK5tg==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.12.1.tgz", + "integrity": "sha512-9d0KQCRM8clMPcDwo8SevNs+/9a8yWVVmaE80FGJcEP8N1qToREmWEGnBn8BUlJhYRFz6fqxeRL1sl5Ogsed7A==", "dev": true, "requires": { "@babel/helper-annotate-as-pure": "^7.10.4", "@babel/helper-wrap-function": "^7.10.4", - "@babel/template": "^7.10.4", - "@babel/traverse": "^7.10.4", - "@babel/types": "^7.10.4" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", - "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", - "dev": true, - "requires": { - "@babel/highlight": "^7.10.4" - } - }, - "@babel/generator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.10.4.tgz", - "integrity": "sha512-toLIHUIAgcQygFZRAQcsLQV3CBuX6yOIru1kJk/qqqvcRmZrYe6WavZTSG+bB8MxhnL9YPf+pKQfuiP161q7ng==", - "dev": true, - "requires": { - "@babel/types": "^7.10.4", - "jsesc": "^2.5.1", - "lodash": "^4.17.13", - "source-map": "^0.5.0" - } - }, - "@babel/helper-function-name": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", - "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", - "dev": true, - "requires": { - "@babel/helper-get-function-arity": "^7.10.4", - "@babel/template": "^7.10.4", - "@babel/types": "^7.10.4" - } - }, - "@babel/helper-get-function-arity": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", - "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", - "dev": true, - "requires": { - "@babel/types": "^7.10.4" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.10.4.tgz", - "integrity": "sha512-pySBTeoUff56fL5CBU2hWm9TesA4r/rOkI9DyJLvvgz09MB9YtfIYe3iBriVaYNaPe+Alua0vBIOVOLs2buWhg==", - "dev": true, - "requires": { - "@babel/types": "^7.10.4" - } - }, - "@babel/highlight": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", - "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, - "@babel/parser": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.10.4.tgz", - "integrity": "sha512-8jHII4hf+YVDsskTF6WuMB3X4Eh+PsUkC2ljq22so5rHvH+T8BzyL94VOdyFLNR8tBSVXOTbNHOKpR4TfRxVtA==", - "dev": true - }, - "@babel/template": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", - "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.10.4", - "@babel/parser": "^7.10.4", - "@babel/types": "^7.10.4" - } - }, - "@babel/traverse": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.10.4.tgz", - "integrity": "sha512-aSy7p5THgSYm4YyxNGz6jZpXf+Ok40QF3aA2LyIONkDHpAcJzDUqlCKXv6peqYUs2gmic849C/t2HKw2a2K20Q==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.10.4", - "@babel/generator": "^7.10.4", - "@babel/helper-function-name": "^7.10.4", - "@babel/helper-split-export-declaration": "^7.10.4", - "@babel/parser": "^7.10.4", - "@babel/types": "^7.10.4", - "debug": "^4.1.0", - "globals": "^11.1.0", - "lodash": "^4.17.13" - } - }, - "@babel/types": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.4.tgz", - "integrity": "sha512-UTCFOxC3FsFHb7lkRMVvgLzaRVamXuAs2Tz4wajva4WxtVY82eZeaUBtC2Zt95FU9TiznuC0Zk35tsim8jeVpg==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "lodash": "^4.17.13", - "to-fast-properties": "^2.0.0" - } - }, - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - } + "@babel/types": "^7.12.1" } }, "@babel/helper-replace-supers": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.10.4.tgz", - "integrity": "sha512-sPxZfFXocEymYTdVK1UNmFPBN+Hv5mJkLPsYWwGBxZAxaWfFu+xqp7b6qWD0yjNuNL2VKc6L5M18tOXUP7NU0A==", + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.12.11.tgz", + "integrity": "sha512-q+w1cqmhL7R0FNzth/PLLp2N+scXEK/L2AHbXUyydxp828F4FEa5WcVoqui9vFRiHDQErj9Zof8azP32uGVTRA==", "dev": true, "requires": { - "@babel/helper-member-expression-to-functions": "^7.10.4", - "@babel/helper-optimise-call-expression": "^7.10.4", - "@babel/traverse": "^7.10.4", - "@babel/types": "^7.10.4" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", - "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", - "dev": true, - "requires": { - "@babel/highlight": "^7.10.4" - } - }, - "@babel/generator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.10.4.tgz", - "integrity": "sha512-toLIHUIAgcQygFZRAQcsLQV3CBuX6yOIru1kJk/qqqvcRmZrYe6WavZTSG+bB8MxhnL9YPf+pKQfuiP161q7ng==", - "dev": true, - "requires": { - "@babel/types": "^7.10.4", - "jsesc": "^2.5.1", - "lodash": "^4.17.13", - "source-map": "^0.5.0" - } - }, - "@babel/helper-function-name": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", - "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", - "dev": true, - "requires": { - "@babel/helper-get-function-arity": "^7.10.4", - "@babel/template": "^7.10.4", - "@babel/types": "^7.10.4" - } - }, - "@babel/helper-get-function-arity": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", - "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", - "dev": true, - "requires": { - "@babel/types": "^7.10.4" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.10.4.tgz", - "integrity": "sha512-pySBTeoUff56fL5CBU2hWm9TesA4r/rOkI9DyJLvvgz09MB9YtfIYe3iBriVaYNaPe+Alua0vBIOVOLs2buWhg==", - "dev": true, - "requires": { - "@babel/types": "^7.10.4" - } - }, - "@babel/highlight": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", - "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, - "@babel/parser": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.10.4.tgz", - "integrity": "sha512-8jHII4hf+YVDsskTF6WuMB3X4Eh+PsUkC2ljq22so5rHvH+T8BzyL94VOdyFLNR8tBSVXOTbNHOKpR4TfRxVtA==", - "dev": true - }, - "@babel/template": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", - "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.10.4", - "@babel/parser": "^7.10.4", - "@babel/types": "^7.10.4" - } - }, - "@babel/traverse": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.10.4.tgz", - "integrity": "sha512-aSy7p5THgSYm4YyxNGz6jZpXf+Ok40QF3aA2LyIONkDHpAcJzDUqlCKXv6peqYUs2gmic849C/t2HKw2a2K20Q==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.10.4", - "@babel/generator": "^7.10.4", - "@babel/helper-function-name": "^7.10.4", - "@babel/helper-split-export-declaration": "^7.10.4", - "@babel/parser": "^7.10.4", - "@babel/types": "^7.10.4", - "debug": "^4.1.0", - "globals": "^11.1.0", - "lodash": "^4.17.13" - } - }, - "@babel/types": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.4.tgz", - "integrity": "sha512-UTCFOxC3FsFHb7lkRMVvgLzaRVamXuAs2Tz4wajva4WxtVY82eZeaUBtC2Zt95FU9TiznuC0Zk35tsim8jeVpg==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "lodash": "^4.17.13", - "to-fast-properties": "^2.0.0" - } - }, - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - } + "@babel/helper-member-expression-to-functions": "^7.12.7", + "@babel/helper-optimise-call-expression": "^7.12.10", + "@babel/traverse": "^7.12.10", + "@babel/types": "^7.12.11" } }, "@babel/helper-simple-access": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.10.4.tgz", - "integrity": "sha512-0fMy72ej/VEvF8ULmX6yb5MtHG4uH4Dbd6I/aHDb/JVg0bbivwt9Wg+h3uMvX+QSFtwr5MeItvazbrc4jtRAXw==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.12.1.tgz", + "integrity": "sha512-OxBp7pMrjVewSSC8fXDFrHrBcJATOOFssZwv16F3/6Xtc138GHybBfPbm9kfiqQHKhYQrlamWILwlDCeyMFEaA==", "dev": true, "requires": { - "@babel/template": "^7.10.4", - "@babel/types": "^7.10.4" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", - "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", - "dev": true, - "requires": { - "@babel/highlight": "^7.10.4" - } - }, - "@babel/highlight": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", - "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, - "@babel/parser": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.10.4.tgz", - "integrity": "sha512-8jHII4hf+YVDsskTF6WuMB3X4Eh+PsUkC2ljq22so5rHvH+T8BzyL94VOdyFLNR8tBSVXOTbNHOKpR4TfRxVtA==", - "dev": true - }, - "@babel/template": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", - "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.10.4", - "@babel/parser": "^7.10.4", - "@babel/types": "^7.10.4" - } - }, - "@babel/types": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.4.tgz", - "integrity": "sha512-UTCFOxC3FsFHb7lkRMVvgLzaRVamXuAs2Tz4wajva4WxtVY82eZeaUBtC2Zt95FU9TiznuC0Zk35tsim8jeVpg==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "lodash": "^4.17.13", - "to-fast-properties": "^2.0.0" - } - } + "@babel/types": "^7.12.1" + } + }, + "@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.12.1.tgz", + "integrity": "sha512-Mf5AUuhG1/OCChOJ/HcADmvcHM42WJockombn8ATJG3OnyiSxBK/Mm5x78BQWvmtXZKHgbjdGL2kin/HOLlZGA==", + "dev": true, + "requires": { + "@babel/types": "^7.12.1" } }, "@babel/helper-split-export-declaration": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.7.4.tgz", - "integrity": "sha512-guAg1SXFcVr04Guk9eq0S4/rWS++sbmyqosJzVs8+1fH5NI+ZcmkaSkc7dmtAFbHFva6yRJnjW3yAcGxjueDug==", + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.12.11.tgz", + "integrity": "sha512-LsIVN8j48gHgwzfocYUSkO/hjYAOJqlpJEc7tGXcIm4cubjVUf8LGW6eWRyxEu7gA25q02p0rQUWoCI33HNS5g==", "dev": true, "requires": { - "@babel/types": "^7.7.4" + "@babel/types": "^7.12.11" } }, "@babel/helper-validator-identifier": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", - "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", + "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==", + "dev": true + }, + "@babel/helper-validator-option": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.12.11.tgz", + "integrity": "sha512-TBFCyj939mFSdeX7U7DDj32WtzYY7fDcalgq8v3fBZMNOJQNn7nOYzMaUCiPxPYfCup69mtIpqlKgMZLvQ8Xhw==", "dev": true }, "@babel/helper-wrap-function": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.10.4.tgz", - "integrity": "sha512-6py45WvEF0MhiLrdxtRjKjufwLL1/ob2qDJgg5JgNdojBAZSAKnAjkyOCNug6n+OBl4VW76XjvgSFTdaMcW0Ug==", + "version": "7.12.3", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.12.3.tgz", + "integrity": "sha512-Cvb8IuJDln3rs6tzjW3Y8UeelAOdnpB8xtQ4sme2MSZ9wOxrbThporC0y/EtE16VAtoyEfLM404Xr1e0OOp+ow==", "dev": true, "requires": { "@babel/helper-function-name": "^7.10.4", "@babel/template": "^7.10.4", "@babel/traverse": "^7.10.4", "@babel/types": "^7.10.4" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", - "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", - "dev": true, - "requires": { - "@babel/highlight": "^7.10.4" - } - }, - "@babel/generator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.10.4.tgz", - "integrity": "sha512-toLIHUIAgcQygFZRAQcsLQV3CBuX6yOIru1kJk/qqqvcRmZrYe6WavZTSG+bB8MxhnL9YPf+pKQfuiP161q7ng==", - "dev": true, - "requires": { - "@babel/types": "^7.10.4", - "jsesc": "^2.5.1", - "lodash": "^4.17.13", - "source-map": "^0.5.0" - } - }, - "@babel/helper-function-name": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", - "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", - "dev": true, - "requires": { - "@babel/helper-get-function-arity": "^7.10.4", - "@babel/template": "^7.10.4", - "@babel/types": "^7.10.4" - } - }, - "@babel/helper-get-function-arity": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", - "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", - "dev": true, - "requires": { - "@babel/types": "^7.10.4" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.10.4.tgz", - "integrity": "sha512-pySBTeoUff56fL5CBU2hWm9TesA4r/rOkI9DyJLvvgz09MB9YtfIYe3iBriVaYNaPe+Alua0vBIOVOLs2buWhg==", - "dev": true, - "requires": { - "@babel/types": "^7.10.4" - } - }, - "@babel/highlight": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", - "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, - "@babel/parser": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.10.4.tgz", - "integrity": "sha512-8jHII4hf+YVDsskTF6WuMB3X4Eh+PsUkC2ljq22so5rHvH+T8BzyL94VOdyFLNR8tBSVXOTbNHOKpR4TfRxVtA==", - "dev": true - }, - "@babel/template": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", - "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.10.4", - "@babel/parser": "^7.10.4", - "@babel/types": "^7.10.4" - } - }, - "@babel/traverse": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.10.4.tgz", - "integrity": "sha512-aSy7p5THgSYm4YyxNGz6jZpXf+Ok40QF3aA2LyIONkDHpAcJzDUqlCKXv6peqYUs2gmic849C/t2HKw2a2K20Q==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.10.4", - "@babel/generator": "^7.10.4", - "@babel/helper-function-name": "^7.10.4", - "@babel/helper-split-export-declaration": "^7.10.4", - "@babel/parser": "^7.10.4", - "@babel/types": "^7.10.4", - "debug": "^4.1.0", - "globals": "^11.1.0", - "lodash": "^4.17.13" - } - }, - "@babel/types": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.4.tgz", - "integrity": "sha512-UTCFOxC3FsFHb7lkRMVvgLzaRVamXuAs2Tz4wajva4WxtVY82eZeaUBtC2Zt95FU9TiznuC0Zk35tsim8jeVpg==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "lodash": "^4.17.13", - "to-fast-properties": "^2.0.0" - } - }, - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - } } }, "@babel/helpers": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.10.4.tgz", - "integrity": "sha512-L2gX/XeUONeEbI78dXSrJzGdz4GQ+ZTA/aazfUsFaWjSe95kiCuOZ5HsXvkiw3iwF+mFHSRUfJU8t6YavocdXA==", + "version": "7.12.5", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.12.5.tgz", + "integrity": "sha512-lgKGMQlKqA8meJqKsW6rUnc4MdUk35Ln0ATDqdM1a/UpARODdI4j5Y5lVfUScnSNkJcdCRAaWkspykNoFg9sJA==", "dev": true, "requires": { "@babel/template": "^7.10.4", - "@babel/traverse": "^7.10.4", - "@babel/types": "^7.10.4" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", - "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", - "dev": true, - "requires": { - "@babel/highlight": "^7.10.4" - } - }, - "@babel/generator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.10.4.tgz", - "integrity": "sha512-toLIHUIAgcQygFZRAQcsLQV3CBuX6yOIru1kJk/qqqvcRmZrYe6WavZTSG+bB8MxhnL9YPf+pKQfuiP161q7ng==", - "dev": true, - "requires": { - "@babel/types": "^7.10.4", - "jsesc": "^2.5.1", - "lodash": "^4.17.13", - "source-map": "^0.5.0" - } - }, - "@babel/helper-function-name": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", - "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", - "dev": true, - "requires": { - "@babel/helper-get-function-arity": "^7.10.4", - "@babel/template": "^7.10.4", - "@babel/types": "^7.10.4" - } - }, - "@babel/helper-get-function-arity": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", - "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", - "dev": true, - "requires": { - "@babel/types": "^7.10.4" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.10.4.tgz", - "integrity": "sha512-pySBTeoUff56fL5CBU2hWm9TesA4r/rOkI9DyJLvvgz09MB9YtfIYe3iBriVaYNaPe+Alua0vBIOVOLs2buWhg==", - "dev": true, - "requires": { - "@babel/types": "^7.10.4" - } - }, - "@babel/highlight": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", - "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, - "@babel/parser": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.10.4.tgz", - "integrity": "sha512-8jHII4hf+YVDsskTF6WuMB3X4Eh+PsUkC2ljq22so5rHvH+T8BzyL94VOdyFLNR8tBSVXOTbNHOKpR4TfRxVtA==", - "dev": true - }, - "@babel/template": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", - "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.10.4", - "@babel/parser": "^7.10.4", - "@babel/types": "^7.10.4" - } - }, - "@babel/traverse": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.10.4.tgz", - "integrity": "sha512-aSy7p5THgSYm4YyxNGz6jZpXf+Ok40QF3aA2LyIONkDHpAcJzDUqlCKXv6peqYUs2gmic849C/t2HKw2a2K20Q==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.10.4", - "@babel/generator": "^7.10.4", - "@babel/helper-function-name": "^7.10.4", - "@babel/helper-split-export-declaration": "^7.10.4", - "@babel/parser": "^7.10.4", - "@babel/types": "^7.10.4", - "debug": "^4.1.0", - "globals": "^11.1.0", - "lodash": "^4.17.13" - } - }, - "@babel/types": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.4.tgz", - "integrity": "sha512-UTCFOxC3FsFHb7lkRMVvgLzaRVamXuAs2Tz4wajva4WxtVY82eZeaUBtC2Zt95FU9TiznuC0Zk35tsim8jeVpg==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "lodash": "^4.17.13", - "to-fast-properties": "^2.0.0" - } - }, - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - } + "@babel/traverse": "^7.12.5", + "@babel/types": "^7.12.5" } }, "@babel/highlight": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.5.0.tgz", - "integrity": "sha512-7dV4eu9gBxoM0dAnj/BCFDW9LFU0zvTrkq0ugM7pnHEgguOEeOz1so2ZghEdzviYzQEED0r4EAgpsBChKy1TRQ==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", + "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", "dev": true, "requires": { + "@babel/helper-validator-identifier": "^7.10.4", "chalk": "^2.0.0", - "esutils": "^2.0.2", "js-tokens": "^4.0.0" } }, "@babel/parser": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.7.4.tgz", - "integrity": "sha512-jIwvLO0zCL+O/LmEJQjWA75MQTWwx3c3u2JOTDK5D3/9egrWRRA0/0hk9XXywYnXZVVpzrBYeIQTmhwUaePI9g==", + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.11.tgz", + "integrity": "sha512-N3UxG+uuF4CMYoNj8AhnbAcJF0PiuJ9KHuy1lQmkYsxTer/MAH9UBNHsBoAX/4s6NvlDD047No8mYVGGzLL4hg==", "dev": true }, "@babel/plugin-proposal-async-generator-functions": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.10.4.tgz", - "integrity": "sha512-MJbxGSmejEFVOANAezdO39SObkURO5o/8b6fSH6D1pi9RZQt+ldppKPXfqgUWpSQ9asM6xaSaSJIaeWMDRP0Zg==", + "version": "7.12.12", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.12.12.tgz", + "integrity": "sha512-nrz9y0a4xmUrRq51bYkWJIO5SBZyG2ys2qinHsN0zHDHVsUaModrkpyWWWXfGqYQmOL3x9sQIcTNN/pBGpo09A==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4", - "@babel/helper-remap-async-to-generator": "^7.10.4", + "@babel/helper-remap-async-to-generator": "^7.12.1", "@babel/plugin-syntax-async-generators": "^7.8.0" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", - "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", - "dev": true - } } }, "@babel/plugin-proposal-class-properties": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.10.4.tgz", - "integrity": "sha512-vhwkEROxzcHGNu2mzUC0OFFNXdZ4M23ib8aRRcJSsW8BZK9pQMD7QB7csl97NBbgGZO7ZyHUyKDnxzOaP4IrCg==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.12.1.tgz", + "integrity": "sha512-cKp3dlQsFsEs5CWKnN7BnSHOd0EOW8EKpEjkoz1pO2E5KzIDNV9Ros1b0CnmbVgAGXJubOYVBOGCT1OmJwOI7w==", "dev": true, "requires": { - "@babel/helper-create-class-features-plugin": "^7.10.4", + "@babel/helper-create-class-features-plugin": "^7.12.1", "@babel/helper-plugin-utils": "^7.10.4" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", - "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", - "dev": true - } } }, "@babel/plugin-proposal-dynamic-import": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.10.4.tgz", - "integrity": "sha512-up6oID1LeidOOASNXgv/CFbgBqTuKJ0cJjz6An5tWD+NVBNlp3VNSBxv2ZdU7SYl3NxJC7agAQDApZusV6uFwQ==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.12.1.tgz", + "integrity": "sha512-a4rhUSZFuq5W8/OO8H7BL5zspjnc1FLd9hlOxIK/f7qG4a0qsqk8uvF/ywgBA8/OmjsapjpvaEOYItfGG1qIvQ==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4", "@babel/plugin-syntax-dynamic-import": "^7.8.0" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", - "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", - "dev": true - } } }, "@babel/plugin-proposal-json-strings": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.10.4.tgz", - "integrity": "sha512-fCL7QF0Jo83uy1K0P2YXrfX11tj3lkpN7l4dMv9Y9VkowkhkQDwFHFd8IiwyK5MZjE8UpbgokkgtcReH88Abaw==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.12.1.tgz", + "integrity": "sha512-GoLDUi6U9ZLzlSda2Df++VSqDJg3CG+dR0+iWsv6XRw1rEq+zwt4DirM9yrxW6XWaTpmai1cWJLMfM8qQJf+yw==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4", "@babel/plugin-syntax-json-strings": "^7.8.0" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", - "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", - "dev": true - } } }, "@babel/plugin-proposal-nullish-coalescing-operator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.10.4.tgz", - "integrity": "sha512-wq5n1M3ZUlHl9sqT2ok1T2/MTt6AXE0e1Lz4WzWBr95LsAZ5qDXe4KnFuauYyEyLiohvXFMdbsOTMyLZs91Zlw==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.12.1.tgz", + "integrity": "sha512-nZY0ESiaQDI1y96+jk6VxMOaL4LPo/QDHBqL+SF3/vl6dHkTwHlOI8L4ZwuRBHgakRBw5zsVylel7QPbbGuYgg==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.0" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", - "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", - "dev": true - } } }, "@babel/plugin-proposal-numeric-separator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.10.4.tgz", - "integrity": "sha512-73/G7QoRoeNkLZFxsoCCvlg4ezE4eM+57PnOqgaPOozd5myfj7p0muD1mRVJvbUWbOzD+q3No2bWbaKy+DJ8DA==", + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.12.7.tgz", + "integrity": "sha512-8c+uy0qmnRTeukiGsjLGy6uVs/TFjJchGXUeBqlG4VWYOdJWkhhVPdQ3uHwbmalfJwv2JsV0qffXP4asRfL2SQ==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4", "@babel/plugin-syntax-numeric-separator": "^7.10.4" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", - "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", - "dev": true - } } }, "@babel/plugin-proposal-object-rest-spread": { @@ -1690,75 +559,44 @@ } }, "@babel/plugin-proposal-optional-catch-binding": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.10.4.tgz", - "integrity": "sha512-LflT6nPh+GK2MnFiKDyLiqSqVHkQnVf7hdoAvyTnnKj9xB3docGRsdPuxp6qqqW19ifK3xgc9U5/FwrSaCNX5g==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.12.1.tgz", + "integrity": "sha512-hFvIjgprh9mMw5v42sJWLI1lzU5L2sznP805zeT6rySVRA0Y18StRhDqhSxlap0oVgItRsB6WSROp4YnJTJz0g==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4", "@babel/plugin-syntax-optional-catch-binding": "^7.8.0" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", - "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", - "dev": true - } } }, "@babel/plugin-proposal-optional-chaining": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.10.4.tgz", - "integrity": "sha512-ZIhQIEeavTgouyMSdZRap4VPPHqJJ3NEs2cuHs5p0erH+iz6khB0qfgU8g7UuJkG88+fBMy23ZiU+nuHvekJeQ==", + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.12.7.tgz", + "integrity": "sha512-4ovylXZ0PWmwoOvhU2vhnzVNnm88/Sm9nx7V8BPgMvAzn5zDou3/Awy0EjglyubVHasJj+XCEkr/r1X3P5elCA==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-skip-transparent-expression-wrappers": "^7.12.1", "@babel/plugin-syntax-optional-chaining": "^7.8.0" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", - "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", - "dev": true - } } }, "@babel/plugin-proposal-private-methods": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.10.4.tgz", - "integrity": "sha512-wh5GJleuI8k3emgTg5KkJK6kHNsGEr0uBTDBuQUBJwckk9xs1ez79ioheEVVxMLyPscB0LfkbVHslQqIzWV6Bw==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.12.1.tgz", + "integrity": "sha512-mwZ1phvH7/NHK6Kf8LP7MYDogGV+DKB1mryFOEwx5EBNQrosvIczzZFTUmWaeujd5xT6G1ELYWUz3CutMhjE1w==", "dev": true, "requires": { - "@babel/helper-create-class-features-plugin": "^7.10.4", + "@babel/helper-create-class-features-plugin": "^7.12.1", "@babel/helper-plugin-utils": "^7.10.4" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", - "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", - "dev": true - } } }, "@babel/plugin-proposal-unicode-property-regex": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.10.4.tgz", - "integrity": "sha512-H+3fOgPnEXFL9zGYtKQe4IDOPKYlZdF1kqFDQRRb8PK4B8af1vAGK04tF5iQAAsui+mHNBQSAtd2/ndEDe9wuA==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.12.1.tgz", + "integrity": "sha512-MYq+l+PvHuw/rKUz1at/vb6nCnQ2gmJBNaM62z0OgH7B2W1D9pvkpYtlti9bGtizNIU1K3zm4bZF9F91efVY0w==", "dev": true, "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.10.4", + "@babel/helper-create-regexp-features-plugin": "^7.12.1", "@babel/helper-plugin-utils": "^7.10.4" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", - "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", - "dev": true - } } }, "@babel/plugin-syntax-async-generators": { @@ -1771,20 +609,12 @@ } }, "@babel/plugin-syntax-class-properties": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.10.4.tgz", - "integrity": "sha512-GCSBF7iUle6rNugfURwNmCGG3Z/2+opxAMLs1nND4bhEG5PuxTIggDBoeYYSujAlLtsupzOHYJQgPS3pivwXIA==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.1.tgz", + "integrity": "sha512-U40A76x5gTwmESz+qiqssqmeEsKvcSyvtgktrm0uzcARAmM9I1jR221f6Oq+GmHrcD+LvZDag1UTOTe2fL3TeA==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", - "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", - "dev": true - } } }, "@babel/plugin-syntax-dynamic-import": { @@ -1797,12 +627,12 @@ } }, "@babel/plugin-syntax-flow": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.8.3.tgz", - "integrity": "sha512-innAx3bUbA0KSYj2E2MNFSn9hiCeowOFLxlsuhXzw8hMQnzkDomUr9QCD7E9VF60NmnG1sNTuuv6Qf4f8INYsg==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.12.1.tgz", + "integrity": "sha512-1lBLLmtxrwpm4VKmtVFselI/P3pX+G63fAtUUt6b2Nzgao77KNDwyuRt90Mj2/9pKobtt68FdvjfqohZjg/FCA==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.8.3" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-syntax-json-strings": { @@ -1830,14 +660,6 @@ "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", - "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", - "dev": true - } } }, "@babel/plugin-syntax-object-rest-spread": { @@ -1868,97 +690,56 @@ } }, "@babel/plugin-syntax-top-level-await": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.10.4.tgz", - "integrity": "sha512-ni1brg4lXEmWyafKr0ccFWkJG0CeMt4WV1oyeBW6EFObF4oOHclbkj5cARxAPQyAQ2UTuplJyK4nfkXIMMFvsQ==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.12.1.tgz", + "integrity": "sha512-i7ooMZFS+a/Om0crxZodrTzNEPJHZrlMVGMTEpFAj6rYY/bKCddB0Dk/YxfPuYXOopuhKk/e1jV6h+WUU9XN3A==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", - "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", - "dev": true - } } }, "@babel/plugin-transform-arrow-functions": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.10.4.tgz", - "integrity": "sha512-9J/oD1jV0ZCBcgnoFWFq1vJd4msoKb/TCpGNFyyLt0zABdcvgK3aYikZ8HjzB14c26bc7E3Q1yugpwGy2aTPNA==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.12.1.tgz", + "integrity": "sha512-5QB50qyN44fzzz4/qxDPQMBCTHgxg3n0xRBLJUmBlLoU/sFvxVWGZF/ZUfMVDQuJUKXaBhbupxIzIfZ6Fwk/0A==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", - "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", - "dev": true - } } }, "@babel/plugin-transform-async-to-generator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.10.4.tgz", - "integrity": "sha512-F6nREOan7J5UXTLsDsZG3DXmZSVofr2tGNwfdrVwkDWHfQckbQXnXSPfD7iO+c/2HGqycwyLST3DnZ16n+cBJQ==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.12.1.tgz", + "integrity": "sha512-SDtqoEcarK1DFlRJ1hHRY5HvJUj5kX4qmtpMAm2QnhOlyuMC4TMdCRgW6WXpv93rZeYNeLP22y8Aq2dbcDRM1A==", "dev": true, "requires": { - "@babel/helper-module-imports": "^7.10.4", + "@babel/helper-module-imports": "^7.12.1", "@babel/helper-plugin-utils": "^7.10.4", - "@babel/helper-remap-async-to-generator": "^7.10.4" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", - "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", - "dev": true - } + "@babel/helper-remap-async-to-generator": "^7.12.1" } }, "@babel/plugin-transform-block-scoped-functions": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.10.4.tgz", - "integrity": "sha512-WzXDarQXYYfjaV1szJvN3AD7rZgZzC1JtjJZ8dMHUyiK8mxPRahynp14zzNjU3VkPqPsO38CzxiWO1c9ARZ8JA==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.12.1.tgz", + "integrity": "sha512-5OpxfuYnSgPalRpo8EWGPzIYf0lHBWORCkj5M0oLBwHdlux9Ri36QqGW3/LR13RSVOAoUUMzoPI/jpE4ABcHoA==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", - "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", - "dev": true - } + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-block-scoping": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.10.4.tgz", - "integrity": "sha512-J3b5CluMg3hPUii2onJDRiaVbPtKFPLEaV5dOPY5OeAbDi1iU/UbbFFTgwb7WnanaDy7bjU35kc26W3eM5Qa0A==", + "version": "7.12.12", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.12.12.tgz", + "integrity": "sha512-VOEPQ/ExOVqbukuP7BYJtI5ZxxsmegTwzZ04j1aF0dkSypGo9XpDHuOrABsJu+ie+penpSJheDJ11x1BEZNiyQ==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.4", - "lodash": "^4.17.13" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", - "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", - "dev": true - } + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-classes": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.10.4.tgz", - "integrity": "sha512-2oZ9qLjt161dn1ZE0Ms66xBncQH4In8Sqw1YWgBUZuGVJJS5c0OFZXL6dP2MRHrkU/eKhWg8CzFJhRQl50rQxA==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.12.1.tgz", + "integrity": "sha512-/74xkA7bVdzQTBeSUhLLJgYIcxw/dpEpCdRDiHgPJ3Mv6uC11UhjpOhl72CgqbBCmt1qtssCyB2xnJm1+PFjog==", "dev": true, "requires": { "@babel/helper-annotate-as-pure": "^7.10.4", @@ -1966,181 +747,56 @@ "@babel/helper-function-name": "^7.10.4", "@babel/helper-optimise-call-expression": "^7.10.4", "@babel/helper-plugin-utils": "^7.10.4", - "@babel/helper-replace-supers": "^7.10.4", + "@babel/helper-replace-supers": "^7.12.1", "@babel/helper-split-export-declaration": "^7.10.4", "globals": "^11.1.0" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", - "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", - "dev": true, - "requires": { - "@babel/highlight": "^7.10.4" - } - }, - "@babel/helper-function-name": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", - "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", - "dev": true, - "requires": { - "@babel/helper-get-function-arity": "^7.10.4", - "@babel/template": "^7.10.4", - "@babel/types": "^7.10.4" - } - }, - "@babel/helper-get-function-arity": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", - "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", - "dev": true, - "requires": { - "@babel/types": "^7.10.4" - } - }, - "@babel/helper-plugin-utils": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", - "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", - "dev": true - }, - "@babel/helper-split-export-declaration": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.10.4.tgz", - "integrity": "sha512-pySBTeoUff56fL5CBU2hWm9TesA4r/rOkI9DyJLvvgz09MB9YtfIYe3iBriVaYNaPe+Alua0vBIOVOLs2buWhg==", - "dev": true, - "requires": { - "@babel/types": "^7.10.4" - } - }, - "@babel/highlight": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", - "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, - "@babel/parser": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.10.4.tgz", - "integrity": "sha512-8jHII4hf+YVDsskTF6WuMB3X4Eh+PsUkC2ljq22so5rHvH+T8BzyL94VOdyFLNR8tBSVXOTbNHOKpR4TfRxVtA==", - "dev": true - }, - "@babel/template": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", - "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.10.4", - "@babel/parser": "^7.10.4", - "@babel/types": "^7.10.4" - } - }, - "@babel/types": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.4.tgz", - "integrity": "sha512-UTCFOxC3FsFHb7lkRMVvgLzaRVamXuAs2Tz4wajva4WxtVY82eZeaUBtC2Zt95FU9TiznuC0Zk35tsim8jeVpg==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "lodash": "^4.17.13", - "to-fast-properties": "^2.0.0" - } - } } }, "@babel/plugin-transform-computed-properties": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.10.4.tgz", - "integrity": "sha512-JFwVDXcP/hM/TbyzGq3l/XWGut7p46Z3QvqFMXTfk6/09m7xZHJUN9xHfsv7vqqD4YnfI5ueYdSJtXqqBLyjBw==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.12.1.tgz", + "integrity": "sha512-vVUOYpPWB7BkgUWPo4C44mUQHpTZXakEqFjbv8rQMg7TC6S6ZhGZ3otQcRH6u7+adSlE5i0sp63eMC/XGffrzg==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", - "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", - "dev": true - } } }, "@babel/plugin-transform-destructuring": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.10.4.tgz", - "integrity": "sha512-+WmfvyfsyF603iPa6825mq6Qrb7uLjTOsa3XOFzlYcYDHSS4QmpOWOL0NNBY5qMbvrcf3tq0Cw+v4lxswOBpgA==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.12.1.tgz", + "integrity": "sha512-fRMYFKuzi/rSiYb2uRLiUENJOKq4Gnl+6qOv5f8z0TZXg3llUwUhsNNwrwaT/6dUhJTzNpBr+CUvEWBtfNY1cw==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", - "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", - "dev": true - } } }, "@babel/plugin-transform-dotall-regex": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.10.4.tgz", - "integrity": "sha512-ZEAVvUTCMlMFAbASYSVQoxIbHm2OkG2MseW6bV2JjIygOjdVv8tuxrCTzj1+Rynh7ODb8GivUy7dzEXzEhuPaA==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.12.1.tgz", + "integrity": "sha512-B2pXeRKoLszfEW7J4Hg9LoFaWEbr/kzo3teWHmtFCszjRNa/b40f9mfeqZsIDLLt/FjwQ6pz/Gdlwy85xNckBA==", "dev": true, "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.10.4", + "@babel/helper-create-regexp-features-plugin": "^7.12.1", "@babel/helper-plugin-utils": "^7.10.4" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", - "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", - "dev": true - } } }, "@babel/plugin-transform-duplicate-keys": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.10.4.tgz", - "integrity": "sha512-GL0/fJnmgMclHiBTTWXNlYjYsA7rDrtsazHG6mglaGSTh0KsrW04qml+Bbz9FL0LcJIRwBWL5ZqlNHKTkU3xAA==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.12.1.tgz", + "integrity": "sha512-iRght0T0HztAb/CazveUpUQrZY+aGKKaWXMJ4uf9YJtqxSUe09j3wteztCUDRHs+SRAL7yMuFqUsLoAKKzgXjw==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", - "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", - "dev": true - } } }, "@babel/plugin-transform-exponentiation-operator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.10.4.tgz", - "integrity": "sha512-S5HgLVgkBcRdyQAHbKj+7KyuWx8C6t5oETmUuwz1pt3WTWJhsUV0WIIXuVvfXMxl/QQyHKlSCNNtaIamG8fysw==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.12.1.tgz", + "integrity": "sha512-7tqwy2bv48q+c1EHbXK0Zx3KXd2RVQp6OC7PbwFNt/dPTAV3Lu5sWtWuAj8owr5wqtWnqHfl2/mJlUmqkChKug==", "dev": true, "requires": { "@babel/helper-builder-binary-assignment-operator-visitor": "^7.10.4", "@babel/helper-plugin-utils": "^7.10.4" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", - "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", - "dev": true - } } }, "@babel/plugin-transform-flow-strip-types": { @@ -2154,464 +810,215 @@ } }, "@babel/plugin-transform-for-of": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.10.4.tgz", - "integrity": "sha512-ItdQfAzu9AlEqmusA/65TqJ79eRcgGmpPPFvBnGILXZH975G0LNjP1yjHvGgfuCxqrPPueXOPe+FsvxmxKiHHQ==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.12.1.tgz", + "integrity": "sha512-Zaeq10naAsuHo7heQvyV0ptj4dlZJwZgNAtBYBnu5nNKJoW62m0zKcIEyVECrUKErkUkg6ajMy4ZfnVZciSBhg==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", - "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", - "dev": true - } } }, "@babel/plugin-transform-function-name": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.10.4.tgz", - "integrity": "sha512-OcDCq2y5+E0dVD5MagT5X+yTRbcvFjDI2ZVAottGH6tzqjx/LKpgkUepu3hp/u4tZBzxxpNGwLsAvGBvQ2mJzg==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.12.1.tgz", + "integrity": "sha512-JF3UgJUILoFrFMEnOJLJkRHSk6LUSXLmEFsA23aR2O5CSLUxbeUX1IZ1YQ7Sn0aXb601Ncwjx73a+FVqgcljVw==", "dev": true, "requires": { "@babel/helper-function-name": "^7.10.4", "@babel/helper-plugin-utils": "^7.10.4" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", - "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", - "dev": true, - "requires": { - "@babel/highlight": "^7.10.4" - } - }, - "@babel/helper-function-name": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", - "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", - "dev": true, - "requires": { - "@babel/helper-get-function-arity": "^7.10.4", - "@babel/template": "^7.10.4", - "@babel/types": "^7.10.4" - } - }, - "@babel/helper-get-function-arity": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", - "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", - "dev": true, - "requires": { - "@babel/types": "^7.10.4" - } - }, - "@babel/helper-plugin-utils": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", - "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", - "dev": true - }, - "@babel/highlight": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", - "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, - "@babel/parser": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.10.4.tgz", - "integrity": "sha512-8jHII4hf+YVDsskTF6WuMB3X4Eh+PsUkC2ljq22so5rHvH+T8BzyL94VOdyFLNR8tBSVXOTbNHOKpR4TfRxVtA==", - "dev": true - }, - "@babel/template": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", - "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.10.4", - "@babel/parser": "^7.10.4", - "@babel/types": "^7.10.4" - } - }, - "@babel/types": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.4.tgz", - "integrity": "sha512-UTCFOxC3FsFHb7lkRMVvgLzaRVamXuAs2Tz4wajva4WxtVY82eZeaUBtC2Zt95FU9TiznuC0Zk35tsim8jeVpg==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "lodash": "^4.17.13", - "to-fast-properties": "^2.0.0" - } - } } }, "@babel/plugin-transform-literals": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.10.4.tgz", - "integrity": "sha512-Xd/dFSTEVuUWnyZiMu76/InZxLTYilOSr1UlHV+p115Z/Le2Fi1KXkJUYz0b42DfndostYlPub3m8ZTQlMaiqQ==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.12.1.tgz", + "integrity": "sha512-+PxVGA+2Ag6uGgL0A5f+9rklOnnMccwEBzwYFL3EUaKuiyVnUipyXncFcfjSkbimLrODoqki1U9XxZzTvfN7IQ==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", - "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", - "dev": true - } } }, "@babel/plugin-transform-member-expression-literals": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.10.4.tgz", - "integrity": "sha512-0bFOvPyAoTBhtcJLr9VcwZqKmSjFml1iVxvPL0ReomGU53CX53HsM4h2SzckNdkQcHox1bpAqzxBI1Y09LlBSw==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.12.1.tgz", + "integrity": "sha512-1sxePl6z9ad0gFMB9KqmYofk34flq62aqMt9NqliS/7hPEpURUCMbyHXrMPlo282iY7nAvUB1aQd5mg79UD9Jg==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", - "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", - "dev": true - } } }, "@babel/plugin-transform-modules-amd": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.10.4.tgz", - "integrity": "sha512-3Fw+H3WLUrTlzi3zMiZWp3AR4xadAEMv6XRCYnd5jAlLM61Rn+CRJaZMaNvIpcJpQ3vs1kyifYvEVPFfoSkKOA==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.12.1.tgz", + "integrity": "sha512-tDW8hMkzad5oDtzsB70HIQQRBiTKrhfgwC/KkJeGsaNFTdWhKNt/BiE8c5yj19XiGyrxpbkOfH87qkNg1YGlOQ==", "dev": true, "requires": { - "@babel/helper-module-transforms": "^7.10.4", + "@babel/helper-module-transforms": "^7.12.1", "@babel/helper-plugin-utils": "^7.10.4", "babel-plugin-dynamic-import-node": "^2.3.3" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", - "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", - "dev": true - } } }, "@babel/plugin-transform-modules-commonjs": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.10.4.tgz", - "integrity": "sha512-Xj7Uq5o80HDLlW64rVfDBhao6OX89HKUmb+9vWYaLXBZOma4gA6tw4Ni1O5qVDoZWUV0fxMYA0aYzOawz0l+1w==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.12.1.tgz", + "integrity": "sha512-dY789wq6l0uLY8py9c1B48V8mVL5gZh/+PQ5ZPrylPYsnAvnEMjqsUXkuoDVPeVK+0VyGar+D08107LzDQ6pag==", "dev": true, "requires": { - "@babel/helper-module-transforms": "^7.10.4", + "@babel/helper-module-transforms": "^7.12.1", "@babel/helper-plugin-utils": "^7.10.4", - "@babel/helper-simple-access": "^7.10.4", + "@babel/helper-simple-access": "^7.12.1", "babel-plugin-dynamic-import-node": "^2.3.3" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", - "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", - "dev": true - } } }, "@babel/plugin-transform-modules-systemjs": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.10.4.tgz", - "integrity": "sha512-Tb28LlfxrTiOTGtZFsvkjpyjCl9IoaRI52AEU/VIwOwvDQWtbNJsAqTXzh+5R7i74e/OZHH2c2w2fsOqAfnQYQ==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.12.1.tgz", + "integrity": "sha512-Hn7cVvOavVh8yvW6fLwveFqSnd7rbQN3zJvoPNyNaQSvgfKmDBO9U1YL9+PCXGRlZD9tNdWTy5ACKqMuzyn32Q==", "dev": true, "requires": { "@babel/helper-hoist-variables": "^7.10.4", - "@babel/helper-module-transforms": "^7.10.4", + "@babel/helper-module-transforms": "^7.12.1", "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-validator-identifier": "^7.10.4", "babel-plugin-dynamic-import-node": "^2.3.3" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", - "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", - "dev": true - } } }, "@babel/plugin-transform-modules-umd": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.10.4.tgz", - "integrity": "sha512-mohW5q3uAEt8T45YT7Qc5ws6mWgJAaL/8BfWD9Dodo1A3RKWli8wTS+WiQ/knF+tXlPirW/1/MqzzGfCExKECA==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.12.1.tgz", + "integrity": "sha512-aEIubCS0KHKM0zUos5fIoQm+AZUMt1ZvMpqz0/H5qAQ7vWylr9+PLYurT+Ic7ID/bKLd4q8hDovaG3Zch2uz5Q==", "dev": true, "requires": { - "@babel/helper-module-transforms": "^7.10.4", + "@babel/helper-module-transforms": "^7.12.1", "@babel/helper-plugin-utils": "^7.10.4" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", - "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", - "dev": true - } } }, "@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.10.4.tgz", - "integrity": "sha512-V6LuOnD31kTkxQPhKiVYzYC/Jgdq53irJC/xBSmqcNcqFGV+PER4l6rU5SH2Vl7bH9mLDHcc0+l9HUOe4RNGKA==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.12.1.tgz", + "integrity": "sha512-tB43uQ62RHcoDp9v2Nsf+dSM8sbNodbEicbQNA53zHz8pWUhsgHSJCGpt7daXxRydjb0KnfmB+ChXOv3oADp1Q==", "dev": true, "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.10.4" + "@babel/helper-create-regexp-features-plugin": "^7.12.1" } }, "@babel/plugin-transform-new-target": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.10.4.tgz", - "integrity": "sha512-YXwWUDAH/J6dlfwqlWsztI2Puz1NtUAubXhOPLQ5gjR/qmQ5U96DY4FQO8At33JN4XPBhrjB8I4eMmLROjjLjw==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.12.1.tgz", + "integrity": "sha512-+eW/VLcUL5L9IvJH7rT1sT0CzkdUTvPrXC2PXTn/7z7tXLBuKvezYbGdxD5WMRoyvyaujOq2fWoKl869heKjhw==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", - "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", - "dev": true - } } }, "@babel/plugin-transform-object-super": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.10.4.tgz", - "integrity": "sha512-5iTw0JkdRdJvr7sY0vHqTpnruUpTea32JHmq/atIWqsnNussbRzjEDyWep8UNztt1B5IusBYg8Irb0bLbiEBCQ==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.12.1.tgz", + "integrity": "sha512-AvypiGJH9hsquNUn+RXVcBdeE3KHPZexWRdimhuV59cSoOt5kFBmqlByorAeUlGG2CJWd0U+4ZtNKga/TB0cAw==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4", - "@babel/helper-replace-supers": "^7.10.4" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", - "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", - "dev": true - } + "@babel/helper-replace-supers": "^7.12.1" } }, "@babel/plugin-transform-parameters": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.10.4.tgz", - "integrity": "sha512-RurVtZ/D5nYfEg0iVERXYKEgDFeesHrHfx8RT05Sq57ucj2eOYAP6eu5fynL4Adju4I/mP/I6SO0DqNWAXjfLQ==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.12.1.tgz", + "integrity": "sha512-xq9C5EQhdPK23ZeCdMxl8bbRnAgHFrw5EOC3KJUsSylZqdkCaFEXxGSBuTSObOpiiHHNyb82es8M1QYgfQGfNg==", "dev": true, "requires": { - "@babel/helper-get-function-arity": "^7.10.4", "@babel/helper-plugin-utils": "^7.10.4" - }, - "dependencies": { - "@babel/helper-get-function-arity": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", - "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", - "dev": true, - "requires": { - "@babel/types": "^7.10.4" - } - }, - "@babel/helper-plugin-utils": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", - "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", - "dev": true - }, - "@babel/types": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.4.tgz", - "integrity": "sha512-UTCFOxC3FsFHb7lkRMVvgLzaRVamXuAs2Tz4wajva4WxtVY82eZeaUBtC2Zt95FU9TiznuC0Zk35tsim8jeVpg==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "lodash": "^4.17.13", - "to-fast-properties": "^2.0.0" - } - } } }, "@babel/plugin-transform-property-literals": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.10.4.tgz", - "integrity": "sha512-ofsAcKiUxQ8TY4sScgsGeR2vJIsfrzqvFb9GvJ5UdXDzl+MyYCaBj/FGzXuv7qE0aJcjWMILny1epqelnFlz8g==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.12.1.tgz", + "integrity": "sha512-6MTCR/mZ1MQS+AwZLplX4cEySjCpnIF26ToWo942nqn8hXSm7McaHQNeGx/pt7suI1TWOWMfa/NgBhiqSnX0cQ==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", - "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", - "dev": true - } } }, "@babel/plugin-transform-regenerator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.10.4.tgz", - "integrity": "sha512-3thAHwtor39A7C04XucbMg17RcZ3Qppfxr22wYzZNcVIkPHfpM9J0SO8zuCV6SZa265kxBJSrfKTvDCYqBFXGw==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.12.1.tgz", + "integrity": "sha512-gYrHqs5itw6i4PflFX3OdBPMQdPbF4bj2REIUxlMRUFk0/ZOAIpDFuViuxPjUL7YC8UPnf+XG7/utJvqXdPKng==", "dev": true, "requires": { "regenerator-transform": "^0.14.2" } }, "@babel/plugin-transform-reserved-words": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.10.4.tgz", - "integrity": "sha512-hGsw1O6Rew1fkFbDImZIEqA8GoidwTAilwCyWqLBM9f+e/u/sQMQu7uX6dyokfOayRuuVfKOW4O7HvaBWM+JlQ==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.12.1.tgz", + "integrity": "sha512-pOnUfhyPKvZpVyBHhSBoX8vfA09b7r00Pmm1sH+29ae2hMTKVmSp4Ztsr8KBKjLjx17H0eJqaRC3bR2iThM54A==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", - "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", - "dev": true - } } }, "@babel/plugin-transform-shorthand-properties": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.10.4.tgz", - "integrity": "sha512-AC2K/t7o07KeTIxMoHneyX90v3zkm5cjHJEokrPEAGEy3UCp8sLKfnfOIGdZ194fyN4wfX/zZUWT9trJZ0qc+Q==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.12.1.tgz", + "integrity": "sha512-GFZS3c/MhX1OusqB1MZ1ct2xRzX5ppQh2JU1h2Pnfk88HtFTM+TWQqJNfwkmxtPQtb/s1tk87oENfXJlx7rSDw==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", - "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", - "dev": true - } } }, "@babel/plugin-transform-spread": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.10.4.tgz", - "integrity": "sha512-1e/51G/Ni+7uH5gktbWv+eCED9pP8ZpRhZB3jOaI3mmzfvJTWHkuyYTv0Z5PYtyM+Tr2Ccr9kUdQxn60fI5WuQ==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.12.1.tgz", + "integrity": "sha512-vuLp8CP0BE18zVYjsEBZ5xoCecMK6LBMMxYzJnh01rxQRvhNhH1csMMmBfNo5tGpGO+NhdSNW2mzIvBu3K1fng==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", - "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", - "dev": true - } + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-skip-transparent-expression-wrappers": "^7.12.1" } }, "@babel/plugin-transform-sticky-regex": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.10.4.tgz", - "integrity": "sha512-Ddy3QZfIbEV0VYcVtFDCjeE4xwVTJWTmUtorAJkn6u/92Z/nWJNV+mILyqHKrUxXYKA2EoCilgoPePymKL4DvQ==", + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.12.7.tgz", + "integrity": "sha512-VEiqZL5N/QvDbdjfYQBhruN0HYjSPjC4XkeqW4ny/jNtH9gcbgaqBIXYEZCNnESMAGs0/K/R7oFGMhOyu/eIxg==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.4", - "@babel/helper-regex": "^7.10.4" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", - "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", - "dev": true - } + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-template-literals": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.10.4.tgz", - "integrity": "sha512-4NErciJkAYe+xI5cqfS8pV/0ntlY5N5Ske/4ImxAVX7mk9Rxt2bwDTGv1Msc2BRJvWQcmYEC+yoMLdX22aE4VQ==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.12.1.tgz", + "integrity": "sha512-b4Zx3KHi+taXB1dVRBhVJtEPi9h1THCeKmae2qP0YdUHIFhVjtpqqNfxeVAa1xeHVhAy4SbHxEwx5cltAu5apw==", "dev": true, "requires": { - "@babel/helper-annotate-as-pure": "^7.10.4", "@babel/helper-plugin-utils": "^7.10.4" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", - "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", - "dev": true - } } }, "@babel/plugin-transform-typeof-symbol": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.10.4.tgz", - "integrity": "sha512-QqNgYwuuW0y0H+kUE/GWSR45t/ccRhe14Fs/4ZRouNNQsyd4o3PG4OtHiIrepbM2WKUBDAXKCAK/Lk4VhzTaGA==", + "version": "7.12.10", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.12.10.tgz", + "integrity": "sha512-JQ6H8Rnsogh//ijxspCjc21YPd3VLVoYtAwv3zQmqAt8YGYUtdo5usNhdl4b9/Vir2kPFZl6n1h0PfUz4hJhaA==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", - "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", - "dev": true - } } }, "@babel/plugin-transform-unicode-escapes": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.10.4.tgz", - "integrity": "sha512-y5XJ9waMti2J+e7ij20e+aH+fho7Wb7W8rNuu72aKRwCHFqQdhkdU2lo3uZ9tQuboEJcUFayXdARhcxLQ3+6Fg==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.12.1.tgz", + "integrity": "sha512-I8gNHJLIc7GdApm7wkVnStWssPNbSRMPtgHdmH3sRM1zopz09UWPS4x5V4n1yz/MIWTVnJ9sp6IkuXdWM4w+2Q==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", - "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", - "dev": true - } } }, "@babel/plugin-transform-unicode-regex": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.10.4.tgz", - "integrity": "sha512-wNfsc4s8N2qnIwpO/WP2ZiSyjfpTamT2C9V9FDH/Ljub9zw6P3SjkXcFmc0RQUt96k2fmIvtla2MMjgTwIAC+A==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.12.1.tgz", + "integrity": "sha512-SqH4ClNngh/zGwHZOOQMTD+e8FGWexILV+ePMyiDJttAWRh5dhDL8rcl5lSgU3Huiq6Zn6pWTMvdPAb21Dwdyg==", "dev": true, "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.10.4", + "@babel/helper-create-regexp-features-plugin": "^7.12.1", "@babel/helper-plugin-utils": "^7.10.4" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", - "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", - "dev": true - } } }, "@babel/preset-env": { @@ -2686,17 +1093,6 @@ "semver": "^5.5.0" }, "dependencies": { - "@babel/types": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.4.tgz", - "integrity": "sha512-UTCFOxC3FsFHb7lkRMVvgLzaRVamXuAs2Tz4wajva4WxtVY82eZeaUBtC2Zt95FU9TiznuC0Zk35tsim8jeVpg==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "lodash": "^4.17.13", - "to-fast-properties": "^2.0.0" - } - }, "semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", @@ -2706,9 +1102,9 @@ } }, "@babel/preset-modules": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.3.tgz", - "integrity": "sha512-Ra3JXOHBq2xd56xSF7lMKXdjBn3T772Y1Wet3yWnkDly9zHvJki029tAFzvAAK5cf4YV3yoxuP61crYRol6SVg==", + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.4.tgz", + "integrity": "sha512-J36NhwnfdzpmH41M1DrnkkgAqhZaqr/NBdPfQ677mLzlaXo+oDiv1deyCDtgAhz8p328otdob0Du7+xgHGZbKg==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.0.0", @@ -2719,76 +1115,75 @@ } }, "@babel/runtime": { - "version": "7.11.2", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.11.2.tgz", - "integrity": "sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw==", + "version": "7.12.5", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.12.5.tgz", + "integrity": "sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg==", "requires": { "regenerator-runtime": "^0.13.4" - }, - "dependencies": { - "regenerator-runtime": { - "version": "0.13.7", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz", - "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==" - } } }, "@babel/runtime-corejs3": { - "version": "7.12.5", - "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.12.5.tgz", - "integrity": "sha512-roGr54CsTmNPPzZoCP1AmDXuBoNao7tnSA83TXTwt+UK5QVyh1DIJnrgYRPWKCF2flqZQXwa7Yr8v7VmLzF0YQ==", + "version": "7.11.2", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.11.2.tgz", + "integrity": "sha512-qh5IR+8VgFz83VBa6OkaET6uN/mJOhHONuy3m1sgF0CV6mXdPSEBdA7e1eUbVvyNtANjMbg22JUv71BaDXLY6A==", "requires": { "core-js-pure": "^3.0.0", "regenerator-runtime": "^0.13.4" } }, "@babel/template": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.7.4.tgz", - "integrity": "sha512-qUzihgVPguAzXCK7WXw8pqs6cEwi54s3E+HrejlkuWO6ivMKx9hZl3Y2fSXp9i5HgyWmj7RKP+ulaYnKM4yYxw==", + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.12.7.tgz", + "integrity": "sha512-GkDzmHS6GV7ZeXfJZ0tLRBhZcMcY0/Lnb+eEbXDBfCAcZCjrZKe6p3J4we/D24O9Y8enxWAg1cWwof59yLh2ow==", "dev": true, "requires": { - "@babel/code-frame": "^7.0.0", - "@babel/parser": "^7.7.4", - "@babel/types": "^7.7.4" + "@babel/code-frame": "^7.10.4", + "@babel/parser": "^7.12.7", + "@babel/types": "^7.12.7" } }, "@babel/traverse": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.7.4.tgz", - "integrity": "sha512-P1L58hQyupn8+ezVA2z5KBm4/Zr4lCC8dwKCMYzsa5jFMDMQAzaBNy9W5VjB+KAmBjb40U7a/H6ao+Xo+9saIw==", + "version": "7.12.12", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.12.tgz", + "integrity": "sha512-s88i0X0lPy45RrLM8b9mz8RPH5FqO9G9p7ti59cToE44xFm1Q+Pjh5Gq4SXBbtb88X7Uy7pexeqRIQDDMNkL0w==", "dev": true, "requires": { - "@babel/code-frame": "^7.5.5", - "@babel/generator": "^7.7.4", - "@babel/helper-function-name": "^7.7.4", - "@babel/helper-split-export-declaration": "^7.7.4", - "@babel/parser": "^7.7.4", - "@babel/types": "^7.7.4", + "@babel/code-frame": "^7.12.11", + "@babel/generator": "^7.12.11", + "@babel/helper-function-name": "^7.12.11", + "@babel/helper-split-export-declaration": "^7.12.11", + "@babel/parser": "^7.12.11", + "@babel/types": "^7.12.12", "debug": "^4.1.0", "globals": "^11.1.0", - "lodash": "^4.17.13" + "lodash": "^4.17.19" }, "dependencies": { "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", "dev": true, "requires": { - "ms": "^2.1.1" + "ms": "2.1.2" } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true } } }, "@babel/types": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.7.4.tgz", - "integrity": "sha512-cz5Ji23KCi4T+YIE/BolWosrJuSmoZeN1EFnRtBwF+KKLi8GG/Z2c2hOJJeCXPk4mwk4QFvTmwIodJowXgttRA==", + "version": "7.12.12", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.12.tgz", + "integrity": "sha512-lnIX7piTxOH22xE7fDXDbSHg9MM1/6ORnafpJmov5rs0kX5g4BZxeXNJLXsMRiO0U5Rb8/FvMS6xlTnTHvxonQ==", "dev": true, "requires": { - "esutils": "^2.0.2", - "lodash": "^4.17.13", + "@babel/helper-validator-identifier": "^7.12.11", + "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } }, @@ -2803,20 +1198,13 @@ } }, "@graphql-tools/batch-delegate": { - "version": "6.2.4", - "resolved": "https://registry.npmjs.org/@graphql-tools/batch-delegate/-/batch-delegate-6.2.4.tgz", - "integrity": "sha512-sDWHMuTVGB2ShAnMAF1fLgaPMDgvweUYBXKuef9qHtCE0YtSO8xhP72CtQvHDOIJf30emWTmFFIsw6zbRe1ZWA==", + "version": "6.2.6", + "resolved": "https://registry.npmjs.org/@graphql-tools/batch-delegate/-/batch-delegate-6.2.6.tgz", + "integrity": "sha512-QUoE9pQtkdNPFdJHSnBhZtUfr3M7pIRoXoMR+TG7DK2Y62ISKbT/bKtZEUU1/2v5uqd5WVIvw9dF8gHDSJAsSA==", "requires": { "@graphql-tools/delegate": "^6.2.4", "dataloader": "2.0.0", "tslib": "~2.0.1" - }, - "dependencies": { - "tslib": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz", - "integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==" - } } }, "@graphql-tools/delegate": { @@ -2830,28 +1218,6 @@ "dataloader": "2.0.0", "is-promise": "4.0.0", "tslib": "~2.0.1" - }, - "dependencies": { - "@graphql-tools/utils": { - "version": "6.2.4", - "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-6.2.4.tgz", - "integrity": "sha512-ybgZ9EIJE3JMOtTrTd2VcIpTXtDrn2q6eiYkeYMKRVh3K41+LZa6YnR2zKERTXqTWqhobROwLt4BZbw2O3Aeeg==", - "requires": { - "@ardatan/aggregate-error": "0.0.6", - "camel-case": "4.1.1", - "tslib": "~2.0.1" - } - }, - "is-promise": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==" - }, - "tslib": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz", - "integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==" - } } }, "@graphql-tools/links": { @@ -2869,13 +1235,20 @@ }, "dependencies": { "@graphql-tools/utils": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-7.0.2.tgz", - "integrity": "sha512-VQQ7krHeoXO0FS3qbWsb/vZb8c8oyiCYPIH4RSgeK9SKOUpatWYt3DW4jmLmyHZLVVMk0yjUbsOhKTBEMejKSA==", + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-7.2.5.tgz", + "integrity": "sha512-S9RUkPimq+5eEDohDjiq/JCPUsiZblKRG8ve+diUwF1f8+r6FV2xGXrOt0qhQJiMxIO+BOK3DU9c+U3tX9Jo0w==", "requires": { "@ardatan/aggregate-error": "0.0.6", - "camel-case": "4.1.1", - "tslib": "~2.0.1" + "camel-case": "4.1.2", + "tslib": "~2.1.0" + }, + "dependencies": { + "tslib": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz", + "integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==" + } } }, "apollo-upload-client": { @@ -2887,48 +1260,49 @@ "@babel/runtime": "^7.11.2", "extract-files": "^9.0.0" } - }, - "extract-files": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/extract-files/-/extract-files-9.0.0.tgz", - "integrity": "sha512-CvdFfHkC95B4bBBk36hcEmvdR2awOdhhVUYH6S/zrVj3477zven/fJMYg7121h4T1xHZC+tetUpubpAhxwI7hQ==" - }, - "is-promise": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==" - }, - "tslib": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz", - "integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==" } } }, "@graphql-tools/merge": { - "version": "6.2.4", - "resolved": "https://registry.npmjs.org/@graphql-tools/merge/-/merge-6.2.4.tgz", - "integrity": "sha512-hQbiSzCJgzUYG1Aspj5EAUY9DsbTI2OK30GLBOjUI16DWkoLVXLXy4ljQYJxq6wDc4fqixMOmvxwf8FoJ9okmw==", + "version": "6.2.7", + "resolved": "https://registry.npmjs.org/@graphql-tools/merge/-/merge-6.2.7.tgz", + "integrity": "sha512-9acgDkkYeAHpuqhOa3E63NZPCX/iWo819Q320sCCMkydF1xgx0qCRYz/V03xPdpQETKRqBG2i2N2csneeEYYig==", "requires": { - "@graphql-tools/schema": "^6.2.4", - "@graphql-tools/utils": "^6.2.4", - "tslib": "~2.0.1" + "@graphql-tools/schema": "^7.0.0", + "@graphql-tools/utils": "^7.0.0", + "tslib": "~2.1.0" }, "dependencies": { + "@graphql-tools/schema": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@graphql-tools/schema/-/schema-7.1.2.tgz", + "integrity": "sha512-GabNT51ErVHE2riDH4EQdRusUsI+nMElT8LdFHyuP53v8gwtleAj+LePQ9jif4NYUe/JQVqO8V28vPcHrA7gfQ==", + "requires": { + "@graphql-tools/utils": "^7.1.2", + "tslib": "~2.0.1" + }, + "dependencies": { + "tslib": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz", + "integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==" + } + } + }, "@graphql-tools/utils": { - "version": "6.2.4", - "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-6.2.4.tgz", - "integrity": "sha512-ybgZ9EIJE3JMOtTrTd2VcIpTXtDrn2q6eiYkeYMKRVh3K41+LZa6YnR2zKERTXqTWqhobROwLt4BZbw2O3Aeeg==", + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-7.2.5.tgz", + "integrity": "sha512-S9RUkPimq+5eEDohDjiq/JCPUsiZblKRG8ve+diUwF1f8+r6FV2xGXrOt0qhQJiMxIO+BOK3DU9c+U3tX9Jo0w==", "requires": { "@ardatan/aggregate-error": "0.0.6", - "camel-case": "4.1.1", - "tslib": "~2.0.1" + "camel-case": "4.1.2", + "tslib": "~2.1.0" } }, "tslib": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz", - "integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz", + "integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==" } } }, @@ -2939,23 +1313,6 @@ "requires": { "@graphql-tools/utils": "^6.2.4", "tslib": "~2.0.1" - }, - "dependencies": { - "@graphql-tools/utils": { - "version": "6.2.4", - "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-6.2.4.tgz", - "integrity": "sha512-ybgZ9EIJE3JMOtTrTd2VcIpTXtDrn2q6eiYkeYMKRVh3K41+LZa6YnR2zKERTXqTWqhobROwLt4BZbw2O3Aeeg==", - "requires": { - "@ardatan/aggregate-error": "0.0.6", - "camel-case": "4.1.1", - "tslib": "~2.0.1" - } - }, - "tslib": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz", - "integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==" - } } }, "@graphql-tools/stitch": { @@ -2971,28 +1328,6 @@ "@graphql-tools/wrap": "^6.2.4", "is-promise": "4.0.0", "tslib": "~2.0.1" - }, - "dependencies": { - "@graphql-tools/utils": { - "version": "6.2.4", - "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-6.2.4.tgz", - "integrity": "sha512-ybgZ9EIJE3JMOtTrTd2VcIpTXtDrn2q6eiYkeYMKRVh3K41+LZa6YnR2zKERTXqTWqhobROwLt4BZbw2O3Aeeg==", - "requires": { - "@ardatan/aggregate-error": "0.0.6", - "camel-case": "4.1.1", - "tslib": "~2.0.1" - } - }, - "is-promise": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==" - }, - "tslib": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz", - "integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==" - } } }, "@graphql-tools/utils": { @@ -3005,10 +1340,21 @@ "tslib": "~2.0.1" }, "dependencies": { - "tslib": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz", - "integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==" + "camel-case": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.1.tgz", + "integrity": "sha512-7fa2WcG4fYFkclIvEmxBbTvmibwF2/agfEBc6q3lOpVu0A13ltLsA+Hr/8Hp6kp5f+G7hKi6t8lys6XxP+1K6Q==", + "requires": { + "pascal-case": "^3.1.1", + "tslib": "^1.10.0" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + } + } } } }, @@ -3022,28 +1368,6 @@ "@graphql-tools/utils": "^6.2.4", "is-promise": "4.0.0", "tslib": "~2.0.1" - }, - "dependencies": { - "@graphql-tools/utils": { - "version": "6.2.4", - "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-6.2.4.tgz", - "integrity": "sha512-ybgZ9EIJE3JMOtTrTd2VcIpTXtDrn2q6eiYkeYMKRVh3K41+LZa6YnR2zKERTXqTWqhobROwLt4BZbw2O3Aeeg==", - "requires": { - "@ardatan/aggregate-error": "0.0.6", - "camel-case": "4.1.1", - "tslib": "~2.0.1" - } - }, - "is-promise": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==" - }, - "tslib": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz", - "integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==" - } } }, "@graphql-typed-document-node/core": { @@ -3094,14 +1418,6 @@ "optional": true, "requires": { "tslib": "^2.0.1" - }, - "dependencies": { - "tslib": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz", - "integrity": "sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ==", - "optional": true - } } }, "@parse/fs-files-adapter": { @@ -3126,14 +1442,6 @@ "verror": "1.10.0" }, "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "requires": { - "ms": "2.0.0" - } - }, "jsonwebtoken": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.1.0.tgz", @@ -3150,11 +1458,6 @@ "ms": "^2.0.0", "xtend": "^4.0.1" } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" } } }, @@ -3179,12 +1482,11 @@ "parse": "2.17.0" }, "dependencies": { - "@babel/runtime-corejs3": { + "@babel/runtime": { "version": "7.11.2", - "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.11.2.tgz", - "integrity": "sha512-qh5IR+8VgFz83VBa6OkaET6uN/mJOhHONuy3m1sgF0CV6mXdPSEBdA7e1eUbVvyNtANjMbg22JUv71BaDXLY6A==", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.11.2.tgz", + "integrity": "sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw==", "requires": { - "core-js-pure": "^3.0.0", "regenerator-runtime": "^0.13.4" } }, @@ -3223,12 +1525,11 @@ "parse": "2.17.0" }, "dependencies": { - "@babel/runtime-corejs3": { + "@babel/runtime": { "version": "7.11.2", - "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.11.2.tgz", - "integrity": "sha512-qh5IR+8VgFz83VBa6OkaET6uN/mJOhHONuy3m1sgF0CV6mXdPSEBdA7e1eUbVvyNtANjMbg22JUv71BaDXLY6A==", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.11.2.tgz", + "integrity": "sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw==", "requires": { - "core-js-pure": "^3.0.0", "regenerator-runtime": "^0.13.4" } }, @@ -3321,9 +1622,9 @@ "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=" }, "@samverschueren/stream-to-observable": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.0.tgz", - "integrity": "sha512-MI4Xx6LHs4Webyvi6EbspgyAb4D2Q2VtnCQ1blOJcoLS6mVa8lNN2rkIy1CVxfTUpoyIbCTkXES1rLXztFD1lg==", + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.1.tgz", + "integrity": "sha512-c/qwwcHyafOQuVQJj0IlBjf5yYgBI7YPJ77k4fOJYesb41jio65eaJODRUmfYKhTOFBrIZ66kgvGPlNbjuoRdQ==", "dev": true, "requires": { "any-observable": "^0.3.0" @@ -3351,16 +1652,10 @@ "@types/node": "*" } }, - "@types/color-name": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", - "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", - "dev": true - }, "@types/connect": { - "version": "3.4.33", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.33.tgz", - "integrity": "sha512-2+FrkXY4zllzTNfJth7jOqEHC+enpLeGslEhpnTAkg21GkRrWV4SsAtqchtT4YS9/nODBU2/ZfsBY2X4J/dX7A==", + "version": "3.4.34", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.34.tgz", + "integrity": "sha512-ePPA/JuI+X0vb+gSWlPKOY0NdNAie/rPUqX2GUPpbZwiKTkSPhjXWuee47E4MtE54QVzGCQMQkAL6JhV2E1+cQ==", "requires": { "@types/node": "*" } @@ -3390,9 +1685,9 @@ } }, "@types/express": { - "version": "4.17.4", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.4.tgz", - "integrity": "sha512-DO1L53rGqIDUEvOjJKmbMEQ5Z+BM2cIEPy/eV3En+s166Gz+FeuzRerxcab757u/U4v4XF4RYrZPmqKa+aY/2w==", + "version": "4.17.7", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.7.tgz", + "integrity": "sha512-dCOT5lcmV/uC2J9k0rPafATeeyz+99xTt54ReX11/LObZgfzJqZNcW27zGhYyX+9iSEGXGt5qLPwRSvBZcLvtQ==", "requires": { "@types/body-parser": "*", "@types/express-serve-static-core": "*", @@ -3410,9 +1705,9 @@ } }, "@types/express-serve-static-core": { - "version": "4.17.8", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.8.tgz", - "integrity": "sha512-1SJZ+R3Q/7mLkOD9ewCBDYD2k0WyZQtWYqF/2VvoNN2/uhI49J9CDN4OAm+wGMA0DbArA4ef27xl4+JwMtGggw==", + "version": "4.17.17", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.17.tgz", + "integrity": "sha512-YYlVaCni5dnHc+bLZfY908IG1+x5xuibKZMGv8srKkvtul3wUuanYvpIj9GXXoWkQbaAdR+kgX46IETKUALWNQ==", "requires": { "@types/node": "*", "@types/qs": "*", @@ -3462,9 +1757,9 @@ "integrity": "sha512-GJhpTepz2udxGexqos8wgaBx4I/zWIDPh/KOGEwAqtuGDkOUJu5eFvwmdBX4AmB8Odsr+9pHCQqiAqDL/yKMKw==" }, "@types/koa": { - "version": "2.11.6", - "resolved": "https://registry.npmjs.org/@types/koa/-/koa-2.11.6.tgz", - "integrity": "sha512-BhyrMj06eQkk04C97fovEDQMpLpd2IxCB4ecitaXwOKGq78Wi2tooaDOWOFGajPk8IkQOAtMppApgSVkYe1F/A==", + "version": "2.11.7", + "resolved": "https://registry.npmjs.org/@types/koa/-/koa-2.11.7.tgz", + "integrity": "sha512-1iXJZZWCePoMe9LGSIPWsu5k5RI4ooXijW78c+nljMn3YbUts8PXoEESu1OeFmrazLPl1l97vTxzwvmH32TWVQ==", "requires": { "@types/accepts": "*", "@types/content-disposition": "*", @@ -3490,14 +1785,14 @@ "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==" }, "@types/mime": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.2.tgz", - "integrity": "sha512-4kPlzbljFcsttWEq6aBW0OZe6BDajAmyvr2xknBG92tejQnvdGtT9+kXSZ580DqpxY9qG2xeQVF9Dq0ymUTo5Q==" + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", + "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==" }, "@types/node": { - "version": "14.0.23", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.0.23.tgz", - "integrity": "sha512-Z4U8yDAl5TFkmYsZdFPdjeMa57NOvnaf1tljHzhouaPEp7LCj2JKkejpI1ODviIAQuW4CcQmxkQ77rnLsOOoKw==" + "version": "14.14.22", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.22.tgz", + "integrity": "sha512-g+f/qj/cNcqKkc3tFqlXOYjrmZA+jNBiDzbP3kH+B+otKFqAdPgVTGP1IeKRdMml/aE69as5S4FqtxAbl+LaMw==" }, "@types/node-fetch": { "version": "2.5.7", @@ -3515,9 +1810,9 @@ "dev": true }, "@types/qs": { - "version": "6.9.3", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.3.tgz", - "integrity": "sha512-7s9EQWupR1fTc2pSMtXRQ9w9gLOcrJn+h7HOXw4evxyvVqMi4f+q7d2tnFe3ng3SNHjtK+0EzGMGFUQX4/AQRA==" + "version": "6.9.5", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.5.tgz", + "integrity": "sha512-/JHkVHtx/REVG0VVToGRGH2+23hsYLHdyG+GrvoUGlGAd0ErauXDyvHtRI/7H7mzLm+tBCKA7pfcpkQ1lf58iQ==" }, "@types/range-parser": { "version": "1.2.3", @@ -3525,14 +1820,19 @@ "integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==" }, "@types/serve-static": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.4.tgz", - "integrity": "sha512-jTDt0o/YbpNwZbQmE/+2e+lfjJEJJR0I3OFaKQKPWkASkCoW3i6fsUnqudSMcNAfbtmADGu8f4MV4q+GqULmug==", + "version": "1.13.9", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.9.tgz", + "integrity": "sha512-ZFqF6qa48XsPdjXV5Gsz0Zqmux2PerNd3a/ktL45mHpa19cuMi/cL8tcxdAx497yRh+QtYPuofjT9oWw9P7nkA==", "requires": { - "@types/express-serve-static-core": "*", - "@types/mime": "*" + "@types/mime": "^1", + "@types/node": "*" } }, + "@types/ungap__global-this": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@types/ungap__global-this/-/ungap__global-this-0.3.1.tgz", + "integrity": "sha512-+/DsiV4CxXl6ZWefwHZDXSe1Slitz21tom38qPCaG0DYCS1NnDPIQDTKcmQ/tvK/edJUKkmuIDBJbmKDiB0r/g==" + }, "@types/ws": { "version": "7.4.0", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-7.4.0.tgz", @@ -3542,18 +1842,28 @@ } }, "@types/zen-observable": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@types/zen-observable/-/zen-observable-0.8.0.tgz", - "integrity": "sha512-te5lMAWii1uEJ4FwLjzdlbw3+n0FZNOvFXHxQDKeT0dilh7HOzdMzV2TrJVUzq8ep7J4Na8OUYPRLSQkJHAlrg==" + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/@types/zen-observable/-/zen-observable-0.8.2.tgz", + "integrity": "sha512-HrCIVMLjE1MOozVoD86622S7aunluLb2PJdPfb3nYiEtohm8mIB/vyv0Fd37AdeMFrTUQXEunw78YloMA3Qilg==" }, - "@wry/context": { + "@ungap/global-this": { "version": "0.4.4", - "resolved": "https://registry.npmjs.org/@wry/context/-/context-0.4.4.tgz", - "integrity": "sha512-LrKVLove/zw6h2Md/KZyWxIkFM6AoyKp71OqpH9Hiip1csjPVoD3tPxlbQUNxEnHENks3UGgNpSBCAfq9KWuag==", - "dev": true, + "resolved": "https://registry.npmjs.org/@ungap/global-this/-/global-this-0.4.4.tgz", + "integrity": "sha512-mHkm6FvepJECMNthFuIgpAEFmPOk71UyXuIxYfjytvFTnSDBIz7jmViO+LfHI/AjrazWije0PnSP3+/NlwzqtA==" + }, + "@wry/context": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@wry/context/-/context-0.5.3.tgz", + "integrity": "sha512-n0uKHiWpf2ArHhmcHcUsKA+Dj0gtye/h56VmsDcoMRuK/ZPFeHKi8ck5L/ftqtF12ZbQR9l8xMPV7y+xybaRDA==", "requires": { - "@types/node": ">=6", - "tslib": "^1.9.3" + "tslib": "^1.14.1" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + } } }, "@wry/equality": { @@ -3562,6 +1872,28 @@ "integrity": "sha512-mwEVBDUVODlsQQ5dfuLUS5/Tf7jqUKyhKYHmVi4fPB6bDMOfWvUPJmKgS1Z7Za/sOI3vzWt4+O7yCiL/70MogA==", "requires": { "tslib": "^1.9.3" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + } + } + }, + "@wry/trie": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@wry/trie/-/trie-0.2.1.tgz", + "integrity": "sha512-sYkuXZqArky2MLQCv4tLW6hX3N8AfTZ5ZMBc8jC6Yy35WYr82UYLLtjS7k/uRGHOA0yTSjuNadG6QQ6a5CS5hQ==", + "requires": { + "tslib": "^1.14.1" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + } } }, "abstract-logging": { @@ -3579,15 +1911,15 @@ } }, "acorn": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.1.tgz", - "integrity": "sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg==", + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", "dev": true }, "acorn-jsx": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.1.0.tgz", - "integrity": "sha512-tMUqwBWfLFbJbizRmEcWSLw6HnFzfdJs2sOJEOwwtVPMoH/0Ay+E703oZz78VSXZiiDcZrQ5XKjPIUQixhmgVw==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.1.tgz", + "integrity": "sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==", "dev": true }, "agent-base": { @@ -3599,9 +1931,9 @@ } }, "aggregate-error": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.0.1.tgz", - "integrity": "sha512-quoaXsZ9/BLNae5yiNoUz+Nhkwz83GhWwtYFglcjEQB2NDHCIpApbqXxIFnm4Pq/Nvhrsq5sYJFyohrrxnTGAA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", "dev": true, "requires": { "clean-stack": "^2.0.0", @@ -3609,11 +1941,11 @@ } }, "ajv": { - "version": "6.10.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz", - "integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==", + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "requires": { - "fast-deep-equal": "^2.0.1", + "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" @@ -3665,12 +1997,20 @@ "dev": true }, "ansi-escapes": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.0.tgz", - "integrity": "sha512-EiYhwo0v255HUL6eDyuLrXEkTi7WwVCLAw+SeOQ7M7qdun1z1pum4DEm/nuqIVbPvi9RPPc9k9LbyBv6H0DwVg==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz", + "integrity": "sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA==", "dev": true, "requires": { - "type-fest": "^0.8.1" + "type-fest": "^0.11.0" + }, + "dependencies": { + "type-fest": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz", + "integrity": "sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==", + "dev": true + } } }, "ansi-regex": { @@ -3724,15 +2064,23 @@ "requires": { "apollo-utilities": "^1.3.4", "tslib": "^1.10.0" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + } } }, "apollo-cache-control": { - "version": "0.11.5", - "resolved": "https://registry.npmjs.org/apollo-cache-control/-/apollo-cache-control-0.11.5.tgz", - "integrity": "sha512-jvarfQhwDRazpOsmkt5Pd7qGFrtbL70zMbUZGqDhJSYpfqI672f7bXXc7O3vtpbD3qnS3XTBvK2kspX/Bdo0IA==", + "version": "0.11.6", + "resolved": "https://registry.npmjs.org/apollo-cache-control/-/apollo-cache-control-0.11.6.tgz", + "integrity": "sha512-YZ+uuIG+fPy+mkpBS2qKF0v1qlzZ3PW6xZVaDukeK3ed3iAs4L/2YnkTqau3OmoF/VPzX2FmSkocX/OVd59YSw==", "requires": { - "apollo-server-env": "^2.4.5", - "apollo-server-plugin-base": "^0.10.3" + "apollo-server-env": "^3.0.0", + "apollo-server-plugin-base": "^0.10.4" } }, "apollo-cache-inmemory": { @@ -3746,6 +2094,33 @@ "optimism": "^0.10.0", "ts-invariant": "^0.4.0", "tslib": "^1.10.0" + }, + "dependencies": { + "@wry/context": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@wry/context/-/context-0.4.4.tgz", + "integrity": "sha512-LrKVLove/zw6h2Md/KZyWxIkFM6AoyKp71OqpH9Hiip1csjPVoD3tPxlbQUNxEnHENks3UGgNpSBCAfq9KWuag==", + "dev": true, + "requires": { + "@types/node": ">=6", + "tslib": "^1.9.3" + } + }, + "optimism": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/optimism/-/optimism-0.10.3.tgz", + "integrity": "sha512-9A5pqGoQk49H6Vhjb9kPgAeeECfUDF6aIICbMDL23kDLStBn1MWk3YvcZ4xWF9CsSf6XEgvRLkXy4xof/56vVw==", + "dev": true, + "requires": { + "@wry/context": "^0.4.0" + } + }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + } } }, "apollo-client": { @@ -3762,15 +2137,29 @@ "ts-invariant": "^0.4.0", "tslib": "^1.10.0", "zen-observable": "^0.8.0" + }, + "dependencies": { + "symbol-observable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", + "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==", + "dev": true + }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + } } }, "apollo-datasource": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/apollo-datasource/-/apollo-datasource-0.7.2.tgz", - "integrity": "sha512-ibnW+s4BMp4K2AgzLEtvzkjg7dJgCaw9M5b5N0YKNmeRZRnl/I/qBTQae648FsRKgMwTbRQIvBhQ0URUFAqFOw==", + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/apollo-datasource/-/apollo-datasource-0.7.3.tgz", + "integrity": "sha512-PE0ucdZYjHjUyXrFWRwT02yLcx2DACsZ0jm1Mp/0m/I9nZu/fEkvJxfsryXB6JndpmQO77gQHixf/xGCN976kA==", "requires": { - "apollo-server-caching": "^0.5.2", - "apollo-server-env": "^2.4.5" + "apollo-server-caching": "^0.5.3", + "apollo-server-env": "^3.0.0" } }, "apollo-env": { @@ -3802,6 +2191,13 @@ "ts-invariant": "^0.4.0", "tslib": "^1.9.3", "zen-observable-ts": "^0.8.21" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + } } }, "apollo-link-http": { @@ -3815,28 +2211,31 @@ "tslib": "^1.9.3" }, "dependencies": { - "apollo-link-http-common": { - "version": "0.2.16", - "resolved": "https://registry.npmjs.org/apollo-link-http-common/-/apollo-link-http-common-0.2.16.tgz", - "integrity": "sha512-2tIhOIrnaF4UbQHf7kjeQA/EmSorB7+HyJIIrUjJOKBgnXwuexi8aMecRlqTIDWcyVXCeqLhUnztMa6bOH/jTg==", - "dev": true, - "requires": { - "apollo-link": "^1.2.14", - "ts-invariant": "^0.4.0", - "tslib": "^1.9.3" - } + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true } } }, "apollo-link-http-common": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/apollo-link-http-common/-/apollo-link-http-common-0.2.15.tgz", - "integrity": "sha512-+Heey4S2IPsPyTf8Ag3PugUupASJMW894iVps6hXbvwtg1aHSNMXUYO5VG7iRHkPzqpuzT4HMBanCTXPjtGzxg==", + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/apollo-link-http-common/-/apollo-link-http-common-0.2.16.tgz", + "integrity": "sha512-2tIhOIrnaF4UbQHf7kjeQA/EmSorB7+HyJIIrUjJOKBgnXwuexi8aMecRlqTIDWcyVXCeqLhUnztMa6bOH/jTg==", "dev": true, "requires": { - "apollo-link": "^1.2.13", + "apollo-link": "^1.2.14", "ts-invariant": "^0.4.0", "tslib": "^1.9.3" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + } } }, "apollo-link-ws": { @@ -3847,6 +2246,14 @@ "requires": { "apollo-link": "^1.2.14", "tslib": "^1.9.3" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + } } }, "apollo-reporting-protobuf": { @@ -3858,46 +2265,66 @@ } }, "apollo-server-caching": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/apollo-server-caching/-/apollo-server-caching-0.5.2.tgz", - "integrity": "sha512-HUcP3TlgRsuGgeTOn8QMbkdx0hLPXyEJehZIPrcof0ATz7j7aTPA4at7gaiFHCo8gk07DaWYGB3PFgjboXRcWQ==", + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/apollo-server-caching/-/apollo-server-caching-0.5.3.tgz", + "integrity": "sha512-iMi3087iphDAI0U2iSBE9qtx9kQoMMEWr6w+LwXruBD95ek9DWyj7OeC2U/ngLjRsXM43DoBDXlu7R+uMjahrQ==", "requires": { - "lru-cache": "^5.0.0" + "lru-cache": "^6.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "requires": { + "yallist": "^4.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } } }, "apollo-server-core": { - "version": "2.19.1", - "resolved": "https://registry.npmjs.org/apollo-server-core/-/apollo-server-core-2.19.1.tgz", - "integrity": "sha512-5EVmcY8Ij7Ywwu+Ze4VaUhZBcxl8t5ztcSatJrKMd4HYlEHyaNGBV2itfpyqAthxfdMbGKqlpeCHmTGSqDcNpA==", + "version": "2.19.2", + "resolved": "https://registry.npmjs.org/apollo-server-core/-/apollo-server-core-2.19.2.tgz", + "integrity": "sha512-liLgLhTIGWZtdQbxuxo3/Yv8j+faKQcI60kOL+uwfByGhoKLZEQp5nqi2IdMK6JXt1VuyKwKu7lTzj02a9S3jA==", "requires": { "@apollographql/apollo-tools": "^0.4.3", "@apollographql/graphql-playground-html": "1.6.26", "@types/graphql-upload": "^8.0.0", "@types/ws": "^7.0.0", - "apollo-cache-control": "^0.11.5", - "apollo-datasource": "^0.7.2", + "apollo-cache-control": "^0.11.6", + "apollo-datasource": "^0.7.3", "apollo-graphql": "^0.6.0", "apollo-reporting-protobuf": "^0.6.2", - "apollo-server-caching": "^0.5.2", - "apollo-server-env": "^2.4.5", + "apollo-server-caching": "^0.5.3", + "apollo-server-env": "^3.0.0", "apollo-server-errors": "^2.4.2", - "apollo-server-plugin-base": "^0.10.3", - "apollo-server-types": "^0.6.2", - "apollo-tracing": "^0.12.1", + "apollo-server-plugin-base": "^0.10.4", + "apollo-server-types": "^0.6.3", + "apollo-tracing": "^0.12.2", "async-retry": "^1.2.1", "fast-json-stable-stringify": "^2.0.0", - "graphql-extensions": "^0.12.7", - "graphql-tag": "^2.9.2", + "graphql-extensions": "^0.12.8", + "graphql-tag": "^2.11.0", "graphql-tools": "^4.0.0", "graphql-upload": "^8.0.2", "loglevel": "^1.6.7", - "lru-cache": "^5.0.0", + "lru-cache": "^6.0.0", "sha.js": "^2.4.11", "subscriptions-transport-ws": "^0.9.11", "uuid": "^8.0.0", "ws": "^6.0.0" }, "dependencies": { + "graphql-tag": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.11.0.tgz", + "integrity": "sha512-VmsD5pJqWJnQZMUeRwrDhfgoyqcfwEkvtpANqcoUG8/tOLkwNgU9mzub/Mc78OJMhHjx7gfAMTxzdG43VGg3bA==" + }, "graphql-upload": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/graphql-upload/-/graphql-upload-8.1.0.tgz", @@ -3909,6 +2336,14 @@ "object-path": "^0.11.4" } }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "requires": { + "yallist": "^4.0.0" + } + }, "ws": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.1.tgz", @@ -3916,13 +2351,18 @@ "requires": { "async-limiter": "~1.0.0" } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" } } }, "apollo-server-env": { - "version": "2.4.5", - "resolved": "https://registry.npmjs.org/apollo-server-env/-/apollo-server-env-2.4.5.tgz", - "integrity": "sha512-nfNhmGPzbq3xCEWT8eRpoHXIPNcNy3QcEoBlzVMjeglrBGryLG2LXwBSPnVmTRRrzUYugX0ULBtgE3rBFNoUgA==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/apollo-server-env/-/apollo-server-env-3.0.0.tgz", + "integrity": "sha512-tPSN+VttnPsoQAl/SBVUpGbLA97MXG990XIwq6YUnJyAixrrsjW1xYG7RlaOqetxm80y5mBZKLrRDiiSsW/vog==", "requires": { "node-fetch": "^2.1.2", "util.promisify": "^1.0.0" @@ -3955,56 +2395,33 @@ "parseurl": "^1.3.2", "subscriptions-transport-ws": "^0.9.16", "type-is": "^1.6.16" - }, - "dependencies": { - "@types/express": { - "version": "4.17.7", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.7.tgz", - "integrity": "sha512-dCOT5lcmV/uC2J9k0rPafATeeyz+99xTt54ReX11/LObZgfzJqZNcW27zGhYyX+9iSEGXGt5qLPwRSvBZcLvtQ==", - "requires": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "*", - "@types/qs": "*", - "@types/serve-static": "*" - } - }, - "@types/express-serve-static-core": { - "version": "4.17.17", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.17.tgz", - "integrity": "sha512-YYlVaCni5dnHc+bLZfY908IG1+x5xuibKZMGv8srKkvtul3wUuanYvpIj9GXXoWkQbaAdR+kgX46IETKUALWNQ==", - "requires": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*" - } - } } }, "apollo-server-plugin-base": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/apollo-server-plugin-base/-/apollo-server-plugin-base-0.10.3.tgz", - "integrity": "sha512-NCLOsk9Jsd8oLvefkQvROdMDQvnHnzbzz3MPCqEYjCOEv0YBM8T77D0wCwbcViDS2M5p0W6un2ub9s/vU71f7Q==", + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/apollo-server-plugin-base/-/apollo-server-plugin-base-0.10.4.tgz", + "integrity": "sha512-HRhbyHgHFTLP0ImubQObYhSgpmVH4Rk1BinnceZmwudIVLKrqayIVOELdyext/QnSmmzg5W7vF3NLGBcVGMqDg==", "requires": { - "apollo-server-types": "^0.6.2" + "apollo-server-types": "^0.6.3" } }, "apollo-server-types": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/apollo-server-types/-/apollo-server-types-0.6.2.tgz", - "integrity": "sha512-LgSKgAStiDzpUSLYwJoAmy0W8nkxx/ExMmgEPgEYVi6dKPkUmtu561J970PhGdYH+D79ke3g87D+plkUkgfnlQ==", + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/apollo-server-types/-/apollo-server-types-0.6.3.tgz", + "integrity": "sha512-aVR7SlSGGY41E1f11YYz5bvwA89uGmkVUtzMiklDhZ7IgRJhysT5Dflt5IuwDxp+NdQkIhVCErUXakopocFLAg==", "requires": { "apollo-reporting-protobuf": "^0.6.2", - "apollo-server-caching": "^0.5.2", - "apollo-server-env": "^2.4.5" + "apollo-server-caching": "^0.5.3", + "apollo-server-env": "^3.0.0" } }, "apollo-tracing": { - "version": "0.12.1", - "resolved": "https://registry.npmjs.org/apollo-tracing/-/apollo-tracing-0.12.1.tgz", - "integrity": "sha512-qdkUjW+pOaidGOSITypeYE288y28HkPmGNpUtyQSOeTxgqXHtQX3TDWiOJ2SmrLH08xdSwfvz9o5KrTq4PdISg==", + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/apollo-tracing/-/apollo-tracing-0.12.2.tgz", + "integrity": "sha512-SYN4o0C0wR1fyS3+P0FthyvsQVHFopdmN3IU64IaspR/RZScPxZ3Ae8uu++fTvkQflAkglnFM0aX6DkZERBp6w==", "requires": { - "apollo-server-env": "^2.4.5", - "apollo-server-plugin-base": "^0.10.3" + "apollo-server-env": "^3.0.0", + "apollo-server-plugin-base": "^0.10.4" } }, "apollo-upload-client": { @@ -4019,19 +2436,10 @@ "extract-files": "^8.0.0" }, "dependencies": { - "@babel/runtime": { - "version": "7.9.2", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.9.2.tgz", - "integrity": "sha512-NE2DtOdufG7R5vnfQUTehdTfNycfUANEtCa9PssN9O/xmTzP4E08UI797ixaei6hBEVL9BI/PsdJS5x7mWoB9Q==", - "dev": true, - "requires": { - "regenerator-runtime": "^0.13.4" - } - }, - "regenerator-runtime": { - "version": "0.13.5", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz", - "integrity": "sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA==", + "extract-files": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/extract-files/-/extract-files-8.1.0.tgz", + "integrity": "sha512-PTGtfthZK79WUMk+avLmwx3NGdU8+iVFXC2NMGxKsn0MnihOG2lvumj+AZo8CTwTrwjXDgZ5tztbRlEdRjBonQ==", "dev": true } } @@ -4045,6 +2453,13 @@ "fast-json-stable-stringify": "^2.0.0", "ts-invariant": "^0.4.0", "tslib": "^1.10.0" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + } } }, "append-transform": { @@ -4151,17 +2566,10 @@ }, "ast-types": { "version": "0.14.2", - "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.14.2.tgz", - "integrity": "sha512-O0yuUDnZeQDL+ncNGlJ78BiO4jnYI3bvMsD5prT0/nsgijG/LpNBIr63gTjVTNsiGkgQhiyCShTgxt8oXOrklA==", - "requires": { - "tslib": "^2.0.1" - }, - "dependencies": { - "tslib": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz", - "integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==" - } + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.14.2.tgz", + "integrity": "sha512-O0yuUDnZeQDL+ncNGlJ78BiO4jnYI3bvMsD5prT0/nsgijG/LpNBIr63gTjVTNsiGkgQhiyCShTgxt8oXOrklA==", + "requires": { + "tslib": "^2.0.1" } }, "astral-regex": { @@ -4239,9 +2647,9 @@ "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" }, "aws4": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.9.0.tgz", - "integrity": "sha512-Uvq6hVe90D0B2WEnUqtdgY1bATGz3mw33nH9Y+dmA+w5DHvUmBgkr5rM/KCHpCsiFNRUfokW/szpPPgMK2hm4A==" + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", + "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==" }, "axios": { "version": "0.21.1", @@ -4350,13 +2758,20 @@ "is-data-descriptor": "^1.0.0", "kind-of": "^6.0.2" } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true, + "optional": true } } }, "base64-js": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", - "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==" + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" }, "bcrypt-nodejs": { "version": "0.0.3", @@ -4395,10 +2810,9 @@ } }, "bl": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.3.tgz", - "integrity": "sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww==", - "dev": true, + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/bl/-/bl-2.2.1.tgz", + "integrity": "sha512-6Pesp1w0DEX1N550i/uGV/TqucVL4AM/pgThFSN/Qq9si1/DF9aIHs1BxD8V/QU0HoeHO6cQRTAuYnLPKq1e4g==", "requires": { "readable-stream": "^2.3.5", "safe-buffer": "^5.1.1" @@ -4452,11 +2866,6 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - }, "qs": { "version": "6.7.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", @@ -4519,26 +2928,33 @@ "requires": { "is-extendable": "^0.1.0" } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true, + "optional": true } } }, "browserslist": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.13.0.tgz", - "integrity": "sha512-MINatJ5ZNrLnQ6blGvePd/QOz9Xtu+Ne+x29iQSCHfkU5BugKVJwZKn/iiL8UbpIpa3JhviKjz+XxMo0m2caFQ==", + "version": "4.16.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.1.tgz", + "integrity": "sha512-UXhDrwqsNcpTYJBTZsbGATDxZbiVDsx6UjpmRUmtnP10pr8wAYr5LgFoEFw9ixriQH2mv/NX2SfGzE/o8GndLA==", "dev": true, "requires": { - "caniuse-lite": "^1.0.30001093", - "electron-to-chromium": "^1.3.488", - "escalade": "^3.0.1", - "node-releases": "^1.1.58" + "caniuse-lite": "^1.0.30001173", + "colorette": "^1.2.1", + "electron-to-chromium": "^1.3.634", + "escalade": "^3.1.1", + "node-releases": "^1.1.69" } }, "bson": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/bson/-/bson-1.1.5.tgz", - "integrity": "sha512-kDuEzldR21lHciPQAIulLs1LZlCXdLziXI6Mb/TDkwXhb//UORJNPXgcRs2CuO4H0DcMkpfT3/ySsP3unoZjBg==", - "dev": true + "integrity": "sha512-kDuEzldR21lHciPQAIulLs1LZlCXdLziXI6Mb/TDkwXhb//UORJNPXgcRs2CuO4H0DcMkpfT3/ySsP3unoZjBg==" }, "buffer": { "version": "4.9.2", @@ -4617,6 +3033,15 @@ "to-object-path": "^0.3.0", "union-value": "^1.0.0", "unset-value": "^1.0.0" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true, + "optional": true + } } }, "caching-transform": { @@ -4664,12 +3089,12 @@ "dev": true }, "camel-case": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.1.tgz", - "integrity": "sha512-7fa2WcG4fYFkclIvEmxBbTvmibwF2/agfEBc6q3lOpVu0A13ltLsA+Hr/8Hp6kp5f+G7hKi6t8lys6XxP+1K6Q==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", + "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", "requires": { - "pascal-case": "^3.1.1", - "tslib": "^1.10.0" + "pascal-case": "^3.1.2", + "tslib": "^2.0.3" } }, "camelcase": { @@ -4679,9 +3104,9 @@ "dev": true }, "caniuse-lite": { - "version": "1.0.30001099", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001099.tgz", - "integrity": "sha512-sdS9A+sQTk7wKoeuZBN/YMAHVztUfVnjDi4/UV3sDE8xoh7YR12hKW+pIdB3oqKGwr9XaFL2ovfzt9w8eUI5CA==", + "version": "1.0.30001181", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001181.tgz", + "integrity": "sha512-m5ul/ARCX50JB8BSNM+oiPmQrR5UmngaQ3QThTTp5HcIIQGP/nPBs82BYLE+tigzm3VW+F4BJIhUyaVtEweelQ==", "dev": true }, "caseless": { @@ -4800,6 +3225,13 @@ "requires": { "is-descriptor": "^0.1.0" } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true, + "optional": true } } }, @@ -4847,12 +3279,11 @@ "dev": true }, "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "requires": { - "@types/color-name": "^1.1.1", "color-convert": "^2.0.1" } }, @@ -4917,9 +3348,9 @@ } }, "cli-width": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", - "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", "dev": true }, "cliui": { @@ -5026,14 +3457,20 @@ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" }, "color-string": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.3.tgz", - "integrity": "sha512-dC2C5qeWoYkxki5UAXapdjqO672AM4vZuPGRQfO8b5HKuKGBbKWpITyDYN7TOFKvRW7kOgAn3746clDBMDJyQw==", + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.4.tgz", + "integrity": "sha512-57yF5yt8Xa3czSEW1jfQDE79Idk0+AkN/4KWad6tbdxUmAs3MvjxlWSWD4deYytcRfoZ9nhKyFl1kj5tBvidbw==", "requires": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, + "colorette": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.1.tgz", + "integrity": "sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw==", + "dev": true + }, "colors": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", @@ -5162,17 +3599,17 @@ "optional": true }, "core-js": { - "version": "3.8.2", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.8.2.tgz", - "integrity": "sha512-FfApuSRgrR6G5s58casCBd9M2k+4ikuu4wbW6pJyYU7bd9zvFc9qf7vr5xmrZOhT9nn+8uwlH1oRR9jTnFoA3A==" + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.8.3.tgz", + "integrity": "sha512-KPYXeVZYemC2TkNEkX/01I+7yd+nX3KddKwZ1Ww7SKWdI2wQprSgLmrTddT8nw92AjEklTsPBoSdQBhbI1bQ6Q==" }, "core-js-compat": { - "version": "3.6.5", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.6.5.tgz", - "integrity": "sha512-7ItTKOhOZbznhXAQ2g/slGg1PJV5zDO/WdkTwi7UEOJmkvsE32PWvx6mKtDjiMpjnR2CNf6BAD6sSxIlv7ptng==", + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.8.3.tgz", + "integrity": "sha512-1sCb0wBXnBIL16pfFG1Gkvei6UzvKyTNYpiC41yrdjEv0UoJoq9E/abTMzyYJ6JpTkAj15dLjbqifIzEBDVvog==", "dev": true, "requires": { - "browserslist": "^4.8.5", + "browserslist": "^4.16.1", "semver": "7.0.0" }, "dependencies": { @@ -5185,9 +3622,9 @@ } }, "core-js-pure": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.8.0.tgz", - "integrity": "sha512-fRjhg3NeouotRoIV0L1FdchA6CK7ZD+lyINyMoz19SyV+ROpC4noS1xItWHFtwZdlqfMfVPJEyEGdfri2bD1pA==" + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.8.3.tgz", + "integrity": "sha512-V5qQZVAr9K0xu7jXg1M7qTEwuxUgqr7dUOezGaNa7i+Xn9oXAU/d1fzqD9ObuwpVQOaorO5s70ckyi1woP9lVA==" }, "core-util-is": { "version": "1.0.2", @@ -5234,9 +3671,9 @@ } }, "cross-spawn": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.1.tgz", - "integrity": "sha512-u7v4o84SwFpD32Z8IIcPZ6z1/ie24O6RU3RbtL5Y316l3KuHVPx9ItBgWQ6VlfAFnRnTtMUrsQ9MUUTuEZjogg==", + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", "dev": true, "requires": { "path-key": "^3.1.0", @@ -5301,11 +3738,11 @@ "integrity": "sha512-YzhyDAwA4TaQIhM5go+vCLmU0UikghC/t9DTQYZR2M/UvZ1MdOhPezSDZcjj9uqQJOMqjLcpWtyW2iNINdlatQ==" }, "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", "requires": { - "ms": "^2.1.1" + "ms": "2.0.0" } }, "decamelize": { @@ -5543,6 +3980,13 @@ "is-data-descriptor": "^1.0.0", "kind-of": "^6.0.2" } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true, + "optional": true } } }, @@ -5554,13 +3998,6 @@ "ast-types": "0.x.x", "escodegen": "1.x.x", "esprima": "3.x.x" - }, - "dependencies": { - "esprima": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", - "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=" - } } }, "delayed-stream": { @@ -5574,9 +4011,9 @@ "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" }, "denque": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/denque/-/denque-1.4.1.tgz", - "integrity": "sha512-OfzPuSZKGcgr96rf1oODnfjqBFmr1DVoc/TrItj3Ohe0Ah1C5WX5Baquw/9U9KovnQ88EqmJbD66rKYUQYN1tQ==" + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-1.5.0.tgz", + "integrity": "sha512-CYiCSgIF1p6EUByQPlGkKnP1M9g0ZV3qMIrqMqZqdwazygIA/YP2vrbcyl1h/WppKJTdl1F85cXIle+394iDAQ==" }, "depd": { "version": "1.1.2", @@ -5689,15 +4126,6 @@ "are-we-there-yet": "~1.1.2", "gauge": "~1.2.5" } - }, - "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } } } }, @@ -5772,9 +4200,9 @@ "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" }, "electron-to-chromium": { - "version": "1.3.497", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.497.tgz", - "integrity": "sha512-sPdW5bUDZwiFtoonuZCUwRGzsZmKzcLM0bMVhp6SMCfUG+B3faENLx3cE+o+K0Jl+MPuNA9s9cScyFjOlixZpQ==", + "version": "1.3.649", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.649.tgz", + "integrity": "sha512-ojGDupQ3UMkvPWcTICe4JYe17+o9OLiFMPoduoR72Zp2ILt1mRVeqnxBEd6s/ptekrnsFU+0A4lStfBe/wyG/A==", "dev": true }, "elegant-spinner": { @@ -5841,35 +4269,24 @@ } }, "es-abstract": { - "version": "1.18.0-next.1", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz", - "integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==", + "version": "1.18.0-next.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.2.tgz", + "integrity": "sha512-Ih4ZMFHEtZupnUh6497zEL4y2+w8+1ljnCyaTa+adcoafI1GOvMwFlDjBLfWR7y9VLfrjRJe9ocuHY1PSR9jjw==", "requires": { + "call-bind": "^1.0.2", "es-to-primitive": "^1.2.1", "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2", "has": "^1.0.3", "has-symbols": "^1.0.1", "is-callable": "^1.2.2", - "is-negative-zero": "^2.0.0", + "is-negative-zero": "^2.0.1", "is-regex": "^1.1.1", - "object-inspect": "^1.8.0", + "object-inspect": "^1.9.0", "object-keys": "^1.1.1", - "object.assign": "^4.1.1", - "string.prototype.trimend": "^1.0.1", - "string.prototype.trimstart": "^1.0.1" - }, - "dependencies": { - "object.assign": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", - "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", - "requires": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3", - "has-symbols": "^1.0.1", - "object-keys": "^1.1.1" - } - } + "object.assign": "^4.1.2", + "string.prototype.trimend": "^1.0.3", + "string.prototype.trimstart": "^1.0.3" } }, "es-to-primitive": { @@ -5993,9 +4410,9 @@ } }, "escalade": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.0.1.tgz", - "integrity": "sha512-DR6NO3h9niOT+MZs7bjxlj2a1k+POu5RN8CLTPX2+i78bRi9eLe7+0zXgUHMnGXWybYcL61E9hGhPKqedy8tQA==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", "dev": true }, "escape-html": { @@ -6019,6 +4436,13 @@ "esutils": "^2.0.2", "optionator": "^0.8.1", "source-map": "~0.6.1" + }, + "dependencies": { + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" + } } }, "eslint": { @@ -6094,32 +4518,38 @@ } }, "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", "dev": true, "requires": { - "ms": "^2.1.1" + "ms": "2.1.2" } }, "glob-parent": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.0.tgz", - "integrity": "sha512-qjtRgnIVmOfnKUE3NJAQEdk+lKrxfw8t5ke7SXtfMTHcjsBfOfWXCQfdb30zfDoZQ2IRSIiidmjtbHZPZ++Ihw==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", + "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", "dev": true, "requires": { "is-glob": "^4.0.1" } }, "globals": { - "version": "12.3.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-12.3.0.tgz", - "integrity": "sha512-wAfjdLgFsPZsklLJvOBUBmzYE8/CwhEqSBEMRXA3qxIiNtyqvjYurAtIfDh6chlEPUfmTY3MnZh5Hfh4q0UlIw==", + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", + "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", "dev": true, "requires": { "type-fest": "^0.8.1" } }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, "path-key": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", @@ -6156,12 +4586,6 @@ "ansi-regex": "^4.1.0" } }, - "strip-json-comments": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.0.1.tgz", - "integrity": "sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw==", - "dev": true - }, "which": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", @@ -6184,12 +4608,12 @@ } }, "eslint-scope": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.0.0.tgz", - "integrity": "sha512-oYrhJW7S0bxAFDvWqzvMPRm6pcgcnWc4QnofCAqRTRfQC0JcwenzGglTtsLyIuuWFfkqDG9vz67cnttSd53djw==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "dev": true, "requires": { - "esrecurse": "^4.1.0", + "esrecurse": "^4.3.0", "estraverse": "^4.1.1" } }, @@ -6203,43 +4627,59 @@ } }, "eslint-visitor-keys": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz", - "integrity": "sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", "dev": true }, "espree": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/espree/-/espree-6.1.2.tgz", - "integrity": "sha512-2iUPuuPP+yW1PZaMSDM9eyVf8D5P0Hi8h83YtZ5bPc/zHYjII5khoixIUTMO794NOY8F/ThF1Bo8ncZILarUTA==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-6.2.1.tgz", + "integrity": "sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==", "dev": true, "requires": { - "acorn": "^7.1.0", - "acorn-jsx": "^5.1.0", + "acorn": "^7.1.1", + "acorn-jsx": "^5.2.0", "eslint-visitor-keys": "^1.1.0" } }, "esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", + "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=" }, "esquery": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.1.tgz", - "integrity": "sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.3.1.tgz", + "integrity": "sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ==", "dev": true, "requires": { - "estraverse": "^4.0.0" + "estraverse": "^5.1.0" + }, + "dependencies": { + "estraverse": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "dev": true + } } }, "esrecurse": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", - "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, "requires": { - "estraverse": "^4.1.0" + "estraverse": "^5.2.0" + }, + "dependencies": { + "estraverse": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "dev": true + } } }, "estraverse": { @@ -6290,9 +4730,9 @@ "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=" }, "execa": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/execa/-/execa-4.0.3.tgz", - "integrity": "sha512-WFDXGHckXPWZX19t1kCsXzOpqX9LWYNqn4C+HqZlk/V0imTkzJZqf87ZBhvpHaftERYknpk0fjSylnXVlVgI0A==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz", + "integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==", "dev": true, "requires": { "cross-spawn": "^7.0.0", @@ -6359,13 +4799,6 @@ "requires": { "is-extendable": "^0.1.0" } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true, - "optional": true } } }, @@ -6414,11 +4847,6 @@ "ms": "2.0.0" } }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - }, "qs": { "version": "6.7.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", @@ -6578,25 +5006,24 @@ } }, "extract-files": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/extract-files/-/extract-files-8.0.0.tgz", - "integrity": "sha512-TsYR7plS+8iZGaB01A6IH91stHJzWu8K3Kr1jEjFy7YDHpDXhgMepjEQ/qGeNOlTUXor0MTL9Rudwg85ElOnNw==", - "dev": true + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/extract-files/-/extract-files-9.0.0.tgz", + "integrity": "sha512-CvdFfHkC95B4bBBk36hcEmvdR2awOdhhVUYH6S/zrVj3477zven/fJMYg7121h4T1xHZC+tetUpubpAhxwI7hQ==" }, "extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.0.tgz", + "integrity": "sha1-4mifjzVvrWLMplo6kcXfX5VRaS8=" }, "fast-deep-equal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", - "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "fast-json-stable-stringify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", - "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" }, "fast-levenshtein": { "version": "2.0.6", @@ -6623,9 +5050,9 @@ "integrity": "sha512-aN3pcx/DSmtyoovUudctc8+6Hl4T+hI9GBBHLjA76jdZl7+b1sgh5g4k+u/GL3dTy1/pnYzKp69FpJ0OicE3Wg==" }, "figures": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-3.1.0.tgz", - "integrity": "sha512-ravh8VRXqHuMvZt/d8GblBeqDMkdJMBdv/2KntFH+ra5MXkO7nxNKpzQ3n6QD/2da1kH0aWmNISdvhM7gl2gVg==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", "dev": true, "requires": { "escape-string-regexp": "^1.0.5" @@ -6722,11 +5149,6 @@ "requires": { "ms": "2.0.0" } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" } } }, @@ -6792,23 +5214,12 @@ "flatted": "^2.0.0", "rimraf": "2.6.3", "write": "1.0.3" - }, - "dependencies": { - "rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - } } }, "flatted": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.1.tgz", - "integrity": "sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", + "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==", "dev": true }, "flow-bin": { @@ -6888,9 +5299,9 @@ "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" }, "fromentries": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/fromentries/-/fromentries-1.2.0.tgz", - "integrity": "sha512-33X7H/wdfO99GdRLLgkjUrD4geAFdq/Uv0kl3HD4da6HDixd2GUg8Mw7dahLCV9r/EARkmtYBB6Tch4EEokFTQ==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fromentries/-/fromentries-1.3.2.tgz", + "integrity": "sha512-cHEpEQHUg0f8XdtZCc2ZAhrHzKzT0MrFUTcvx+hfxYu7rGMDc5SKoXFh+n4YigxsHXRzc6OrCshdR1bWH6HHyg==", "dev": true }, "fs-capacitor": { @@ -6997,9 +5408,9 @@ } }, "gensync": { - "version": "1.0.0-beta.1", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.1.tgz", - "integrity": "sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg==", + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", "dev": true }, "get-caller-file": { @@ -7009,9 +5420,9 @@ "dev": true }, "get-intrinsic": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.0.2.tgz", - "integrity": "sha512-aeX0vrFm21ILl3+JpFFRNe9aUvp6VFZb2/CTbgLb8j75kOhvoNYjt9d8KA/tJG4gSo8nzEDedRl0h7vDmBYRVg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.0.tgz", + "integrity": "sha512-M11rgtQp5GZMZzDL7jLTNxbDfurpzuau5uqRWDPvlHjfvg3TdScAZo96GLvhMjImrmR8uAt0FS2RLoMrfWGKlg==", "requires": { "function-bind": "^1.1.1", "has": "^1.0.3", @@ -7063,9 +5474,9 @@ } }, "get-stream": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.1.0.tgz", - "integrity": "sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", "dev": true, "requires": { "pump": "^3.0.0" @@ -7091,11 +5502,6 @@ "requires": { "ms": "2.0.0" } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" } } }, @@ -7188,9 +5594,9 @@ } }, "graceful-fs": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz", - "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", "dev": true }, "graphql": { @@ -7199,13 +5605,13 @@ "integrity": "sha512-EB3zgGchcabbsU9cFe1j+yxdzKQKAbGUWRb13DsrsMN1yyfmmIq+2+L5MqVWcDCE4V89R5AyUOi7sMOGxdsYtA==" }, "graphql-extensions": { - "version": "0.12.7", - "resolved": "https://registry.npmjs.org/graphql-extensions/-/graphql-extensions-0.12.7.tgz", - "integrity": "sha512-yc9qOmEmWVZNkux9m0eCiHdtYSwNZRHkFhgfKfDn4u/gpsJolft1iyMUADnG/eytiRW0CGZFBpZjHkJhpginuQ==", + "version": "0.12.8", + "resolved": "https://registry.npmjs.org/graphql-extensions/-/graphql-extensions-0.12.8.tgz", + "integrity": "sha512-xjsSaB6yKt9jarFNNdivl2VOx52WySYhxPgf8Y16g6GKZyAzBoIFiwyGw5PJDlOSUa6cpmzn6o7z8fVMbSAbkg==", "requires": { "@apollographql/apollo-tools": "^0.4.3", - "apollo-server-env": "^2.4.5", - "apollo-server-types": "^0.6.2" + "apollo-server-env": "^3.0.0", + "apollo-server-types": "^0.6.3" } }, "graphql-list-fields": { @@ -7239,7 +5645,8 @@ "graphql-tag": { "version": "2.10.1", "resolved": "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.10.1.tgz", - "integrity": "sha512-jApXqWBzNXQ8jYa/HLkZJaVw9jgwNqZkywa2zfFn16Iv1Zb7ELNHkJaXHR7Quvd5SIGsy6Ny7SUKATgnu05uEg==" + "integrity": "sha512-jApXqWBzNXQ8jYa/HLkZJaVw9jgwNqZkywa2zfFn16Iv1Zb7ELNHkJaXHR7Quvd5SIGsy6Ny7SUKATgnu05uEg==", + "dev": true }, "graphql-tools": { "version": "4.0.8", @@ -7276,11 +5683,6 @@ "version": "6.2.0", "resolved": "https://registry.npmjs.org/fs-capacitor/-/fs-capacitor-6.2.0.tgz", "integrity": "sha512-nKcE1UduoSKX27NSZlg879LdQc94OtbOsEmKMN2MBNudXREvijRKx2GEBsTMTfws+BrbkJoEuynbGSVRSpauvw==" - }, - "isobject": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-4.0.0.tgz", - "integrity": "sha512-S/2fF5wH8SJA/kmwr6HYhK/RI/OkhD84k8ntalo0iJjZikgq1XFvR5M8NPT1x5F7fBwCG3qHfnzeP/Vh/ZxCUA==" } } }, @@ -7290,11 +5692,11 @@ "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" }, "har-validator": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", - "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", "requires": { - "ajv": "^6.5.5", + "ajv": "^6.12.3", "har-schema": "^2.0.0" } }, @@ -7347,6 +5749,15 @@ "get-value": "^2.0.6", "has-values": "^1.0.0", "isobject": "^3.0.0" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true, + "optional": true + } } }, "has-values": { @@ -7373,9 +5784,9 @@ } }, "hasha": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.0.tgz", - "integrity": "sha512-2W+jKdQbAdSIrggA8Q35Br8qKadTrqCTC8+XZvBWepKDK6m9XkX6Iz1a2yh2KP01kzAR/dpuMeUnocoLYDcskw==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.2.tgz", + "integrity": "sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ==", "dev": true, "requires": { "is-stream": "^2.0.0", @@ -7450,21 +5861,6 @@ "requires": { "agent-base": "4", "debug": "3.1.0" - }, - "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - } } }, "http-signature": { @@ -7511,12 +5907,11 @@ }, "dependencies": { "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "requires": { - "@types/color-name": "^1.1.1", "color-convert": "^2.0.1" } }, @@ -7558,9 +5953,9 @@ "dev": true }, "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "requires": { "has-flag": "^4.0.0" @@ -7588,9 +5983,9 @@ "dev": true }, "import-fresh": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz", - "integrity": "sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", "dev": true, "requires": { "parent-module": "^1.0.0", @@ -7638,29 +6033,29 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "ini": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.7.tgz", - "integrity": "sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ==", + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "dev": true }, "inquirer": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.0.1.tgz", - "integrity": "sha512-V1FFQ3TIO15det8PijPLFR9M9baSlnRs9nL7zWu1MNVA2T9YVl9ZbrHJhYs7e9X8jeMZ3lr2JH/rdHFgNCBdYw==", + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz", + "integrity": "sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==", "dev": true, "requires": { "ansi-escapes": "^4.2.1", - "chalk": "^2.4.2", + "chalk": "^4.1.0", "cli-cursor": "^3.1.0", - "cli-width": "^2.0.0", + "cli-width": "^3.0.0", "external-editor": "^3.0.3", "figures": "^3.0.0", - "lodash": "^4.17.15", + "lodash": "^4.17.19", "mute-stream": "0.0.8", - "run-async": "^2.2.0", - "rxjs": "^6.5.3", + "run-async": "^2.4.0", + "rxjs": "^6.6.0", "string-width": "^4.1.0", - "strip-ansi": "^5.1.0", + "strip-ansi": "^6.0.0", "through": "^2.3.6" }, "dependencies": { @@ -7670,6 +6065,46 @@ "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", "dev": true }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, "is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -7685,34 +6120,24 @@ "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.0" - }, - "dependencies": { - "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.0" - } - } } }, "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", "dev": true, "requires": { - "ansi-regex": "^4.1.0" - }, - "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", - "dev": true - } + "ansi-regex": "^5.0.0" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" } } } @@ -7737,9 +6162,9 @@ "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=" }, "ipaddr.js": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.0.tgz", - "integrity": "sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA==" + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" }, "is-accessor-descriptor": { "version": "0.1.6", @@ -7790,6 +6215,15 @@ "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz", "integrity": "sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==" }, + "is-core-module": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.2.0.tgz", + "integrity": "sha512-XRAfAdyyY5F5cOXn7hYQDqh2Xmii+DEfIcQGxK/uNwMHhIkPWO0g8msXcbzLe+MpGoR951MlqM/2iIlU4vKDdQ==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, "is-data-descriptor": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", @@ -7954,13 +6388,21 @@ "optional": true, "requires": { "isobject": "^3.0.1" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true, + "optional": true + } } }, "is-promise": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", - "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=", - "dev": true + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==" }, "is-regex": { "version": "1.1.1", @@ -8024,11 +6466,9 @@ "dev": true }, "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true, - "optional": true + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-4.0.0.tgz", + "integrity": "sha512-S/2fF5wH8SJA/kmwr6HYhK/RI/OkhD84k8ntalo0iJjZikgq1XFvR5M8NPT1x5F7fBwCG3qHfnzeP/Vh/ZxCUA==" }, "isstream": { "version": "0.1.2", @@ -8159,9 +6599,9 @@ "dev": true }, "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "requires": { "has-flag": "^4.0.0" @@ -8181,13 +6621,19 @@ }, "dependencies": { "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", "dev": true, "requires": { - "ms": "^2.1.1" + "ms": "2.1.2" } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true } } }, @@ -8212,9 +6658,9 @@ } }, "iterall": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/iterall/-/iterall-1.2.2.tgz", - "integrity": "sha512-yynBb1g+RFUPY64fTrFv7nsjRrENBQJaX2UL+2Szc9REFrSNm1rpSXHGzhmAy7a9uv3vlvgBlXnf9RqmPH1/DA==" + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/iterall/-/iterall-1.3.0.tgz", + "integrity": "sha512-QZ9qOMdF+QLHxy1QIpUHUU1D5pS2CG2P69LF6L6CPjPYA/XMOmKV3PZpawHoAjHNyB0swdVTRxdYT4tbBbxqwg==" }, "jasmine": { "version": "3.5.0", @@ -8243,22 +6689,30 @@ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "js-yaml": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", - "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", "dev": true, "requires": { "argparse": "^1.0.7", "esprima": "^4.0.0" + }, + "dependencies": { + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + } } }, "js2xmlparser": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.0.tgz", - "integrity": "sha512-WuNgdZOXVmBk5kUPMcTcVUpbGRzLfNkv7+7APq7WiDihpXVKrgxo6wwRpRl9OQeEBgKCVk9mR7RbzrnNWC8oBw==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.1.tgz", + "integrity": "sha512-KrPTolcw6RocpYjdC7pL7v62e55q7qOMHvLX1UCLc5AAS8qeJ6nukarEJAF2KL2PZxlbGueEbINqZR2bDe/gUw==", "dev": true, "requires": { - "xmlcreate": "^2.0.0" + "xmlcreate": "^2.0.3" } }, "jsbn": { @@ -8293,12 +6747,6 @@ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", "dev": true - }, - "strip-json-comments": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.0.1.tgz", - "integrity": "sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw==", - "dev": true } } }, @@ -8324,10 +6772,10 @@ "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", "dev": true }, - "json-parse-better-errors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", - "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", "dev": true }, "json-schema": { @@ -8386,6 +6834,11 @@ "semver": "^5.6.0" }, "dependencies": { + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, "semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", @@ -8402,6 +6855,13 @@ "extsprintf": "1.3.0", "json-schema": "0.2.3", "verror": "1.10.0" + }, + "dependencies": { + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + } } }, "jwa": { @@ -8445,6 +6905,13 @@ "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", "requires": { "ms": "2.1.2" + }, + "dependencies": { + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } } }, "http-proxy-agent": { @@ -8465,6 +6932,11 @@ "agent-base": "6", "debug": "4" } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" } } }, @@ -8593,12 +7065,11 @@ }, "dependencies": { "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "requires": { - "@types/color-name": "^1.1.1", "color-convert": "^2.0.1" } }, @@ -8637,12 +7108,12 @@ "dev": true }, "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", "dev": true, "requires": { - "ms": "^2.1.1" + "ms": "2.1.2" } }, "fill-range": { @@ -8676,10 +7147,16 @@ "picomatch": "^2.0.5" } }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "requires": { "has-flag": "^4.0.0" @@ -8719,12 +7196,11 @@ }, "dependencies": { "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "requires": { - "@types/color-name": "^1.1.1", "color-convert": "^2.0.1" } }, @@ -8753,34 +7229,16 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "figures": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", - "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", - "dev": true, - "requires": { - "escape-string-regexp": "^1.0.5" - } - }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, - "rxjs": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.0.tgz", - "integrity": "sha512-3HMA8z/Oz61DUHe+SdOiQyzIf4tOx5oQHmMir7IZEu6TMqCLHT4LRcmNaUS0NwOz8VLvmmBduMsoaUvMaIiqzg==", - "dev": true, - "requires": { - "tslib": "^1.9.0" - } - }, "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "requires": { "has-flag": "^4.0.0" @@ -8965,12 +7423,11 @@ }, "dependencies": { "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "requires": { - "@types/color-name": "^1.1.1", "color-convert": "^2.0.1" } }, @@ -9006,9 +7463,9 @@ "dev": true }, "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "requires": { "has-flag": "^4.0.0" @@ -9029,12 +7486,11 @@ }, "dependencies": { "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "requires": { - "@types/color-name": "^1.1.1", "color-convert": "^2.0.1" } }, @@ -9088,6 +7544,13 @@ "fecha": "^4.2.0", "ms": "^2.1.1", "triple-beam": "^1.3.0" + }, + "dependencies": { + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + } } }, "loglevel": { @@ -9109,11 +7572,11 @@ } }, "lower-case": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.1.tgz", - "integrity": "sha512-LiWgfDLLb1dwbFQZsSglpRj+1ctGnayXz3Uv0/WO8n558JycT5fg6zkNcnW0G68Nn0aEldTFeEfmjCfmqry/rQ==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", "requires": { - "tslib": "^1.10.0" + "tslib": "^2.0.3" } }, "lowercase-keys": { @@ -9128,13 +7591,6 @@ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", "requires": { "yallist": "^3.0.2" - }, - "dependencies": { - "yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" - } } }, "lru-memoizer": { @@ -9154,6 +7610,11 @@ "pseudomap": "^1.0.1", "yallist": "^2.0.0" } + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" } } }, @@ -9205,6 +7666,11 @@ "combined-stream": "^1.0.6", "mime-types": "^2.1.12" } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" } } }, @@ -9257,9 +7723,9 @@ } }, "markdown-it-anchor": { - "version": "5.2.5", - "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-5.2.5.tgz", - "integrity": "sha512-xLIjLQmtym3QpoY9llBgApknl7pxAcN3WDRc2d3rwpl+/YvDZHPmKscGs+L6E05xf2KrCXPBvosWt7MZukwSpQ==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-5.3.0.tgz", + "integrity": "sha512-/V1MnLL/rgJ3jkMWo84UR+K+jF1cxNG1a+KwqeXqTIJ+jtA8aWSHuigx8lTzauiIjBDbwF3NcWQMotd0Dm39jA==", "dev": true }, "marked": { @@ -9352,16 +7818,16 @@ "integrity": "sha512-dhNd1uA2u397uQk3Nv5LM4lm93WYDUXFn3Fu291FJerns4jyTudqhIWe4W04YLy7Uk1tm1Ore04NpjRvQp/NPA==" }, "mime-db": { - "version": "1.42.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.42.0.tgz", - "integrity": "sha512-UbfJCR4UAVRNgMpfImz05smAXK7+c+ZntjaA26ANtkXLlOe947Aag5zdIcKQULAiF9Cq4WxBi9jUs5zkA84bYQ==" + "version": "1.45.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.45.0.tgz", + "integrity": "sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w==" }, "mime-types": { - "version": "2.1.25", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.25.tgz", - "integrity": "sha512-5KhStqB5xpTAeGqKBAMgwaYMnQik7teQN4IAzC7npDv6kzeU6prfkR67bc87J1kWMPGkoaZSq1npmexMgkmEVg==", + "version": "2.1.28", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.28.tgz", + "integrity": "sha512-0TO2yJ5YHYr7M2zzT7gDU1tbwHxEUWBCLt0lscSNpcdAfFyJOVEpRYNS7EXVcTLNj/25QO8gulHC5JtTzSE2UQ==", "requires": { - "mime-db": "1.42.0" + "mime-db": "1.45.0" } }, "mimic-fn": { @@ -9415,18 +7881,18 @@ } }, "mkdirp": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.4.tgz", - "integrity": "sha512-iG9AK/dJLtJ0XNgTuDbSyNS3zECqDlAhnQW4CsNxBG3LQJBbHmRX1egw39DmtOdCAqY+dKXV+sgPgilNWUKMVw==", + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", "dev": true, "requires": { "minimist": "^1.2.5" } }, "moment": { - "version": "2.27.0", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.27.0.tgz", - "integrity": "sha512-al0MUK7cpIcglMv3YF13qSgdAIqxHTO7brRtaz3DlSULbqfazqkc5kEjNrLDOM7fsjshoFIihnU8snrP7zUvhQ==" + "version": "2.29.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz", + "integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==" }, "mongodb": { "version": "3.6.3", @@ -9439,22 +7905,6 @@ "require_optional": "^1.0.1", "safe-buffer": "^5.1.2", "saslprep": "^1.0.0" - }, - "dependencies": { - "bl": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/bl/-/bl-2.2.1.tgz", - "integrity": "sha512-6Pesp1w0DEX1N550i/uGV/TqucVL4AM/pgThFSN/Qq9si1/DF9aIHs1BxD8V/QU0HoeHO6cQRTAuYnLPKq1e4g==", - "requires": { - "readable-stream": "^2.3.5", - "safe-buffer": "^5.1.1" - } - }, - "bson": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/bson/-/bson-1.1.5.tgz", - "integrity": "sha512-kDuEzldR21lHciPQAIulLs1LZlCXdLziXI6Mb/TDkwXhb//UORJNPXgcRs2CuO4H0DcMkpfT3/ySsP3unoZjBg==" - } } }, "mongodb-core": { @@ -9497,12 +7947,6 @@ "ms": "2.0.0" } }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - }, "untildify": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/untildify/-/untildify-1.0.0.tgz", @@ -9538,12 +7982,6 @@ "ms": "2.0.0" } }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - }, "semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", @@ -9553,7 +7991,7 @@ } }, "mongodb-runner": { - "version": "github:mongodb-js/runner#dfb9a520147de6e2537f7c1c21a5e7005d1905f8", + "version": "github:mongodb-js/runner#f4e920d0ae8a2c1de2148343e062d3744b434fb8", "from": "github:mongodb-js/runner", "dev": true, "requires": { @@ -9587,6 +8025,12 @@ "requires": { "ms": "2.1.2" } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true } } }, @@ -9626,10 +8070,10 @@ "minimist": "0.0.8" } }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "rimraf": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.6.tgz", + "integrity": "sha1-xZWXVpsU2VatKcrMQr3d9fDqT0w=", "dev": true } } @@ -9686,12 +8130,6 @@ "graceful-fs": "^4.1.9" } }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - }, "semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", @@ -9759,9 +8197,9 @@ } }, "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, "mustache": { "version": "4.1.0", @@ -9775,9 +8213,9 @@ "dev": true }, "nan": { - "version": "2.14.1", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.1.tgz", - "integrity": "sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw==", + "version": "2.14.2", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz", + "integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==", "dev": true, "optional": true }, @@ -9830,12 +8268,12 @@ "dev": true }, "no-case": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.3.tgz", - "integrity": "sha512-ehY/mVQCf9BL0gKfsJBvFJen+1V//U+0HQMPrWct40ixE4jnv0bfvxDbWtAHL9EcaPEOJHVVYKoQn1TlZUB8Tw==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", "requires": { - "lower-case": "^2.0.1", - "tslib": "^1.10.0" + "lower-case": "^2.0.2", + "tslib": "^2.0.3" } }, "node-fetch": { @@ -9867,9 +8305,9 @@ } }, "node-releases": { - "version": "1.1.59", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.59.tgz", - "integrity": "sha512-H3JrdUczbdiwxN5FuJPyCHnGHIFqQ0wWxo+9j1kAXAzqNMAHlo+4I/sYYxpyK0irQ73HgdiyzD32oqQDcU2Osw==", + "version": "1.1.70", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.70.tgz", + "integrity": "sha512-Slf2s69+2/uAD79pVVQo8uSiC34+g8GWY8UH2Qtqv34ZfhYrxpYpfzs9Js9d6O0mbDmALuxaTlplnBTnSELcrw==", "dev": true }, "normalize-path": { @@ -10051,9 +8489,9 @@ } }, "object-hash": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.0.3.tgz", - "integrity": "sha512-JPKn0GMu+Fa3zt3Bmr66JhokJU5BaNBIh4ZeTlaCBzrBsOeXzwcKKAK1tbLiPKgvwmPXsDvvLHoWh5Bm7ofIYg==" + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.1.1.tgz", + "integrity": "sha512-VOJmgmS+7wvXf8CjbQmimtCnEx3IAoLxI3fp2fbWehxrWBcAQFbk+vcwb6vzR0VZv/eNCJ/27j151ZTwqW/JeQ==" }, "object-inspect": { "version": "1.9.0", @@ -10078,18 +8516,26 @@ "optional": true, "requires": { "isobject": "^3.0.0" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true, + "optional": true + } } }, "object.assign": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", - "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", - "dev": true, + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", + "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", "requires": { - "define-properties": "^1.1.2", - "function-bind": "^1.1.1", - "has-symbols": "^1.0.0", - "object-keys": "^1.0.11" + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" } }, "object.getownpropertydescriptors": { @@ -10110,6 +8556,15 @@ "optional": true, "requires": { "isobject": "^3.0.1" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true, + "optional": true + } } }, "on-finished": { @@ -10137,9 +8592,9 @@ } }, "onetime": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz", - "integrity": "sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", "dev": true, "requires": { "mimic-fn": "^2.1.0" @@ -10152,12 +8607,12 @@ "dev": true }, "optimism": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/optimism/-/optimism-0.10.3.tgz", - "integrity": "sha512-9A5pqGoQk49H6Vhjb9kPgAeeECfUDF6aIICbMDL23kDLStBn1MWk3YvcZ4xWF9CsSf6XEgvRLkXy4xof/56vVw==", - "dev": true, + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/optimism/-/optimism-0.14.0.tgz", + "integrity": "sha512-ygbNt8n4DOCVpkwiLF+IrKKeNHOjtr9aXLWGP9HNJGoblSGsnVbJLstcH6/nE9Xy5ZQtlkSioFQNnthmENW6FQ==", "requires": { - "@wry/context": "^0.4.0" + "@wry/context": "^0.5.2", + "@wry/trie": "^0.2.1" } }, "optionator": { @@ -10270,6 +8725,11 @@ "requires": { "ms": "2.1.2" } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" } } }, @@ -10334,11 +8794,12 @@ "xmlhttprequest": "1.8.0" }, "dependencies": { - "@babel/runtime": { + "@babel/runtime-corejs3": { "version": "7.12.5", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.12.5.tgz", - "integrity": "sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg==", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.12.5.tgz", + "integrity": "sha512-roGr54CsTmNPPzZoCP1AmDXuBoNao7tnSA83TXTwt+UK5QVyh1DIJnrgYRPWKCF2flqZQXwa7Yr8v7VmLzF0YQ==", "requires": { + "core-js-pure": "^3.0.0", "regenerator-runtime": "^0.13.4" } }, @@ -10355,14 +8816,14 @@ } }, "parse-json": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.0.0.tgz", - "integrity": "sha512-OOY5b7PAEFV0E2Fir1KOkxchnZNCdowAJgQ5NuxjpBKTRP3pQhwkrkxqQjeoKJ+fO7bCpmIZaogI4eZGDMEGOw==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", "dev": true, "requires": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1", + "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, @@ -10372,12 +8833,12 @@ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" }, "pascal-case": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.1.tgz", - "integrity": "sha512-XIeHKqIrsquVTQL2crjq3NfJUxmdLasn3TYOU0VBM+UX2a6ztAWBlJQBePLGY7VHW8+2dRadeIPK5+KImwTxQA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", "requires": { - "no-case": "^3.0.3", - "tslib": "^1.10.0" + "no-case": "^3.0.4", + "tslib": "^2.0.3" } }, "pascalcase": { @@ -10672,12 +9133,12 @@ "dev": true }, "proxy-addr": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.5.tgz", - "integrity": "sha512-t/7RxHXPH6cJtP0pRG6smSr9QJidhB+3kXu0KgXnbGYMgzEnUxRQ4/LDdfOwZEMyIh3/xHb8PX3t+lfL9z+YVQ==", + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz", + "integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==", "requires": { "forwarded": "~0.1.2", - "ipaddr.js": "1.9.0" + "ipaddr.js": "1.9.1" } }, "proxy-agent": { @@ -10702,6 +9163,11 @@ "requires": { "ms": "2.1.2" } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" } } }, @@ -10725,9 +9191,9 @@ "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" }, "psl": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.5.0.tgz", - "integrity": "sha512-4vqUjKi2huMu1OJiLhi3jN6jeeKvMZdI1tYgi/njW5zV52jNLgSAZSdN16m9bJFe61/cT8ulmw4qFitV9QRsEA==" + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", + "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==" }, "pump": { "version": "3.0.0", @@ -10781,9 +9247,9 @@ "integrity": "sha512-FNbLuG/HAdapQoybeZSoes1PWdOj0w242gb+e1R0hicf3Gyj/Mf8M9NaED2AnXVOX01b2FXomwUiw1xP1K+8sA==" }, "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -10825,9 +9291,9 @@ } }, "redis-commands": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.5.0.tgz", - "integrity": "sha512-6KxamqpZ468MeQC3bkWmCB1fp56XL64D4Kf0zJSwDZbVLLm7KFkoIcHrgRvQ+sk8dnhySs7+yBg94yIkAK7aJg==" + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.6.0.tgz", + "integrity": "sha512-2jnZ0IkjZxvguITjFTrGiLyzQZcTvaw8DAaCXxZq/dsHXz7KfMQ3OUJy7Tz9vnRtZRVz6VRCPDvruvU8Ts44wQ==" }, "redis-errors": { "version": "1.2.0", @@ -10843,9 +9309,9 @@ } }, "regenerate": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.1.tgz", - "integrity": "sha512-j2+C8+NtXQgEKWk49MMP5P/u2GhnahTtVkRIHr5R5lVRlbKvmQ+oS+A5aLKWp2ma5VkT8sh6v+v4hbH0YHR66A==", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", "dev": true }, "regenerate-unicode-properties": { @@ -10869,23 +9335,6 @@ "dev": true, "requires": { "@babel/runtime": "^7.8.4" - }, - "dependencies": { - "@babel/runtime": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.10.4.tgz", - "integrity": "sha512-UpTN5yUJr9b4EX2CnGNWIvER7Ab83ibv0pcvvHc4UOdrBI5jb8bj+32cCwPX6xu0mt2daFNjYhoi+X7beH0RSw==", - "dev": true, - "requires": { - "regenerator-runtime": "^0.13.4" - } - }, - "regenerator-runtime": { - "version": "0.13.5", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz", - "integrity": "sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA==", - "dev": true - } } }, "regex-not": { @@ -10906,9 +9355,9 @@ "dev": true }, "regexpu-core": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.7.0.tgz", - "integrity": "sha512-TQ4KXRnIn6tz6tjnrXEkD/sshygKH/j5KzK86X8MkeHyZ8qst/LZ89j3X4/8HEIfHANTFIP/AbXakeRhWIl5YQ==", + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.7.1.tgz", + "integrity": "sha512-ywH2VUraA44DZQuRKzARmw6S66mr48pQVva4LBeRhcOltJ6hExvWly5ZjFLYo67xbIxb6W1q4bAGtgfEl20zfQ==", "dev": true, "requires": { "regenerate": "^1.4.0", @@ -10926,9 +9375,9 @@ "dev": true }, "regjsparser": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.6.4.tgz", - "integrity": "sha512-64O87/dPDgfk8/RQqC4gkZoGyyWFIEUTTh80CU6CWuK5vkCGyekIx+oKcEIYtP/RAxSQltCZHCNu/mdd7fqlJw==", + "version": "0.6.7", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.6.7.tgz", + "integrity": "sha512-ib77G0uxsA2ovgiYbCVGx4Pv3PSttAx2vIwidqQzbL2U5S4Q+j00HdSAneSBuyVcMvEnTXMjiGgB+DlXozVhpQ==", "dev": true, "requires": { "jsesc": "~0.5.0" @@ -11054,11 +9503,12 @@ } }, "resolve": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.13.1.tgz", - "integrity": "sha512-CxqObCX8K8YtAhOBRg+lrcdn+LK+WYOS8tSjqSFbjtrI5PnS63QPhZl4+yKfrU9tdsbMu9Anr/amegT87M9Z6w==", + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.19.0.tgz", + "integrity": "sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==", "dev": true, "requires": { + "is-core-module": "^2.1.0", "path-parse": "^1.0.6" } }, @@ -11097,33 +9547,41 @@ "integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=" }, "rimraf": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.6.tgz", - "integrity": "sha1-xZWXVpsU2VatKcrMQr3d9fDqT0w=", - "dev": true - }, - "run-async": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", - "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=", + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", "dev": true, "requires": { - "is-promise": "^2.1.0" + "glob": "^7.1.3" } }, + "run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "dev": true + }, "rxjs": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.3.tgz", - "integrity": "sha512-wuYsAYYFdWTAnAaPoKGNhfpWwKZbJW+HgAJ+mImp+Epl7BG8oNWBCTyRM8gba9k4lk8BgWdoYm21Mo/RYhhbgA==", + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.3.tgz", + "integrity": "sha512-trsQc+xYYXZ3urjOiJOuCOa5N3jAZ3eiSpQB5hIT8zGlL2QfnHLJ2r7GMkBGuIausdJN1OneaI6gQlsqNHHmZQ==", "dev": true, "requires": { "tslib": "^1.9.0" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + } } }, "safe-buffer": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", - "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==" + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" }, "safe-regex": { "version": "1.1.0", @@ -11324,9 +9782,9 @@ "dev": true }, "signal-exit": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", + "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==" }, "simple-swizzle": { "version": "0.2.2", @@ -11413,13 +9871,6 @@ "is-extendable": "^0.1.0" } }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true, - "optional": true - }, "source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -11482,6 +9933,13 @@ "is-data-descriptor": "^1.0.0", "kind-of": "^6.0.2" } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true, + "optional": true } } }, @@ -11818,6 +10276,12 @@ "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", "dev": true }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, "strip-outer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz", @@ -11839,6 +10303,11 @@ "ws": "^5.2.0" }, "dependencies": { + "symbol-observable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", + "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==" + }, "ws": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/ws/-/ws-5.2.2.tgz", @@ -11859,9 +10328,9 @@ } }, "symbol-observable": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", - "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==" + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-2.0.3.tgz", + "integrity": "sha512-sQV7phh2WCYAn81oAkakC5qjq2Ml0g8ozqz03wOGnx9dDlG1de6yrF+0RAzSJD8fPUow3PTSMf2SAbOGxb93BA==" }, "table": { "version": "5.4.6", @@ -11943,6 +10412,18 @@ "readable-stream": "^2.3.0", "to-buffer": "^1.1.1", "xtend": "^4.0.0" + }, + "dependencies": { + "bl": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.3.tgz", + "integrity": "sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww==", + "dev": true, + "requires": { + "readable-stream": "^2.3.5", + "safe-buffer": "^5.1.1" + } + } } }, "test-exclude": { @@ -12147,12 +10628,19 @@ "integrity": "sha512-uEtWkFM/sdZvRNNDL3Ehu4WVpwaulhwQszV8mrtcdeE8nN00BV9mAmQ88RkrBhFgl9gMgvjJLAQcZbnPXI9mlA==", "requires": { "tslib": "^1.9.3" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + } } }, "tslib": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", - "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==" + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz", + "integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==" }, "tsscmp": { "version": "1.0.6", @@ -12249,9 +10737,9 @@ } }, "underscore": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.9.1.tgz", - "integrity": "sha512-5/4etnCkd9c8gwgowi5/om/mYO5ajCaOgdzj/oW+0eQV9WxKBDZw5+ycmKmeaTXjInS/W0BzpGLo2xR2aBwZdg==", + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.9.2.tgz", + "integrity": "sha512-D39qtimx0c1fI3ya1Lnhk3E9nONswSKhnffBI0gME9C99fYOkNi04xs8K6pePLhvl1frbDemkaBQ5ikWllR2HQ==", "dev": true }, "unicode-canonical-property-names-ecmascript": { @@ -12347,6 +10835,13 @@ "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", "dev": true, "optional": true + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true, + "optional": true } } }, @@ -12364,9 +10859,9 @@ "optional": true }, "uri-js": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", - "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "requires": { "punycode": "^2.1.0" } @@ -12450,9 +10945,9 @@ "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" }, "v8-compile-cache": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz", - "integrity": "sha512-usZBT3PW+LOjM25wbqIlZwPeJV+3OSz3M1k1Ws8snlW39dZyYL9lOGC5FgPVHfk0jKmjiDV8Z0mIbVQPiwFs7g==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.2.0.tgz", + "integrity": "sha512-gTpR5XQNKFwOd4clxfnhaqvfqMpqEwr4tOtCyz4MtYZX2JYhfr1JvBFKdS+7K/9rfpZR3VLX+YWBbKoxCgS43Q==", "dev": true }, "vary": { @@ -12551,36 +11046,6 @@ "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "winston-transport": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.4.0.tgz", - "integrity": "sha512-Lc7/p3GtqtqPBYYtS6KCN3c77/2QCev51DvcJKbkFPQNoj1sinkGwLGFDxkXY9J6p9+EPnYs+D90uwbnaiURTw==", - "requires": { - "readable-stream": "^2.3.7", - "triple-beam": "^1.2.0" - }, - "dependencies": { - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - } - } } } }, @@ -12596,11 +11061,11 @@ } }, "winston-transport": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.3.0.tgz", - "integrity": "sha512-B2wPuwUi3vhzn/51Uukcao4dIduEiPOcOt9HJ3QeaXgkJ5Z7UwpBzxS4ZGNHtrxrUvTwemsQiSys0ihOf8Mp1A==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.4.0.tgz", + "integrity": "sha512-Lc7/p3GtqtqPBYYtS6KCN3c77/2QCev51DvcJKbkFPQNoj1sinkGwLGFDxkXY9J6p9+EPnYs+D90uwbnaiURTw==", "requires": { - "readable-stream": "^2.3.6", + "readable-stream": "^2.3.7", "triple-beam": "^1.2.0" } }, @@ -12632,12 +11097,11 @@ "dev": true }, "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "requires": { - "@types/color-name": "^1.1.1", "color-convert": "^2.0.1" } }, @@ -12730,9 +11194,9 @@ "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=" }, "xmlcreate": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.1.tgz", - "integrity": "sha512-MjGsXhKG8YjTKrDCXseFo3ClbMGvUD4en29H2Cev1dv4P/chlpw6KdYmlCWDkhosBVKRDjM836+3e3pm1cBNJA==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.3.tgz", + "integrity": "sha512-HgS+X6zAztGa9zIK3Y3LXuJes33Lz9x+YyTxgrkIdabu2vqcGOWwdfCpf1hWLRrd553wd4QCDf6BBO6FfdsRiQ==", "dev": true }, "xmlhttprequest": { @@ -12746,9 +11210,9 @@ "integrity": "sha1-UqY+VsoLhKfzpfPWGHLxJq16WUM=" }, "xss": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/xss/-/xss-1.0.7.tgz", - "integrity": "sha512-A9v7tblGvxu8TWXQC9rlpW96a+LN1lyw6wyhpTmmGW+FwRMactchBR3ROKSi33UPCUcUHSu8s9YP6F+K3Mw//w==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/xss/-/xss-1.0.8.tgz", + "integrity": "sha512-3MgPdaXV8rfQ/pNn16Eio6VXYPTkqwa0vc7GkiymmY/DqR1SE/7VPAAVZz1GJsJFrllMYO3RHfEaiUGjab6TNw==", "requires": { "commander": "^2.20.3", "cssfilter": "0.0.10" @@ -12767,15 +11231,15 @@ "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" }, "y18n": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", - "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz", + "integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==", "dev": true }, "yallist": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" }, "yaml": { "version": "1.10.0", @@ -12868,6 +11332,13 @@ "requires": { "tslib": "^1.9.3", "zen-observable": "^0.8.0" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + } } } } diff --git a/public/custom_json.html b/public/custom_json.html new file mode 100644 index 0000000000..7e280bfc05 --- /dev/null +++ b/public/custom_json.html @@ -0,0 +1,17 @@ + + + + + + {{title}} + + + +

{{heading}}

+

{{body}}

+ + + diff --git a/public/custom_json.json b/public/custom_json.json new file mode 100644 index 0000000000..06d78f1d9d --- /dev/null +++ b/public/custom_json.json @@ -0,0 +1,23 @@ +{ + "en": { + "translation": { + "title": "Hello!", + "heading": "Welcome to {{appName}}!", + "body": "We are delighted to welcome you on board." + } + }, + "de": { + "translation": { + "title": "Hallo!", + "heading": "Willkommen bei {{appName}}!", + "body": "Wir freuen uns, dich begrüßen zu dürfen." + } + }, + "de-AT": { + "translation": { + "title": "Servus!", + "heading": "Willkommen bei {{appName}}!", + "body": "Wir freuen uns, dich begrüßen zu dürfen." + } + } +} \ No newline at end of file diff --git a/spec/PagesRouter.spec.js b/spec/PagesRouter.spec.js index 86c529e109..c2e1b85498 100644 --- a/spec/PagesRouter.spec.js +++ b/spec/PagesRouter.spec.js @@ -2,22 +2,29 @@ const request = require('../lib/request'); const fs = require('fs').promises; +const mustache = require('mustache'); const Utils = require('../lib/Utils'); const Config = require('../lib/Config'); const Definitions = require('../lib/Options/Definitions'); const UserController = require('../lib/Controllers/UserController').UserController; -const { PagesRouter, pages, pageParams } = require('../lib/Routers/PagesRouter'); +const { + PagesRouter, + pages, + pageParams, + pageParamHeaderPrefix, +} = require('../lib/Routers/PagesRouter'); describe('Pages Router', () => { describe('basic request', () => { - const config = { - appId: 'test', - appName: 'exampleAppname', - publicServerURL: 'http://localhost:8378/1', - pages: { enableRouter: true }, - }; + let config; beforeEach(async () => { + config = { + appId: 'test', + appName: 'exampleAppname', + publicServerURL: 'http://localhost:8378/1', + pages: { enableRouter: true }, + }; await reconfigureServer(config); }); @@ -40,7 +47,7 @@ describe('Pages Router', () => { await reconfigureServer(_config); const response = await request({ - url: 'http://localhost:8378/1/apps/email_verification_link_invalid.html' + url: 'http://localhost:8378/1/apps/email_verification_link_invalid.html', }).catch(e => e); expect(response.status).toBe(200); }); @@ -51,7 +58,7 @@ describe('Pages Router', () => { await reconfigureServer(_config); const response = await request({ - url: `http://localhost:8378/1/pages/email_verification_link_invalid.html` + url: `http://localhost:8378/1/pages/email_verification_link_invalid.html`, }).catch(e => e); expect(response.status).toBe(200); }); @@ -59,7 +66,7 @@ describe('Pages Router', () => { it('responds with 404 if publicServerURL is not confgured', async () => { await reconfigureServer({ appName: 'unused', - pages: { enableRouter: true } + pages: { enableRouter: true }, }); const urls = [ 'http://localhost:8378/1/apps/test/verify_email', @@ -78,7 +85,7 @@ describe('Pages Router', () => { { url: 'http://localhost:8378/1/apps/choose_password?id=invalid', method: 'GET' }, { url: 'http://localhost:8378/1/apps/invalid/request_password_reset', method: 'GET' }, { url: 'http://localhost:8378/1/apps/invalid/request_password_reset', method: 'POST' }, - { url: 'http://localhost:8378/1/apps/invalid/resend_verification_email', method: 'GET' }, + { url: 'http://localhost:8378/1/apps/invalid/resend_verification_email', method: 'POST' }, ]; for (const req of reqs) { const response = await request(req).catch(e => e); @@ -170,53 +177,45 @@ describe('Pages Router', () => { describe('pages', () => { let router = new PagesRouter(); let req; + let config; let goToPage; let pageResponse; let redirectResponse; let readFile; - const exampleLocale = 'de-AT'; - const config = { - appId: 'test', - appName: 'ExampleAppName', - verifyUserEmails: true, - emailAdapter: { - sendVerificationEmail: () => Promise.resolve(), - sendPasswordResetEmail: () => Promise.resolve(), - sendMail: () => {}, - }, - publicServerURL: 'http://localhost:8378/1', - pages: { - enableRouter: true, - enableLocalization: true, - customUrls: {}, - }, - }; - async function reconfigureServerWithPageOptions(options) { - await reconfigureServer({ - appId: Parse.applicationId, - masterKey: Parse.masterKey, - serverURL: Parse.serverURL, - pages: options, - }); + let exampleLocale; + + const fillPlaceholders = (text, fill) => text.replace(/({{2,3}.*?}{2,3})/g, fill); + async function reconfigureServerWithPagesConfig(pagesConfig) { + config.pages = pagesConfig; + await reconfigureServer(config); } beforeEach(async () => { router = new PagesRouter(); readFile = spyOn(fs, 'readFile').and.callThrough(); - goToPage = spyOn(PagesRouter.prototype, 'goToPage').and.callThrough() - pageResponse = spyOn(PagesRouter.prototype, 'pageResponse').and.callThrough() + goToPage = spyOn(PagesRouter.prototype, 'goToPage').and.callThrough(); + pageResponse = spyOn(PagesRouter.prototype, 'pageResponse').and.callThrough(); redirectResponse = spyOn(PagesRouter.prototype, 'redirectResponse').and.callThrough(); + exampleLocale = 'de-AT'; + config = { + appId: 'test', + appName: 'ExampleAppName', + verifyUserEmails: true, + emailAdapter: { + sendVerificationEmail: () => Promise.resolve(), + sendPasswordResetEmail: () => Promise.resolve(), + sendMail: () => {}, + }, + publicServerURL: 'http://localhost:8378/1', + pages: { + enableRouter: true, + enableLocalization: true, + customUrls: {}, + }, + }; req = { method: 'GET', - config: { - appId: 'test', - appName: 'ExampleAppName', - publicServerURL: 'http://localhost:8378/1', - pages: { - enableLocalization: true, - customUrls: {}, - }, - }, + config, query: { locale: exampleLocale, }, @@ -225,13 +224,19 @@ describe('Pages Router', () => { describe('server options', () => { it('uses default configuration when none is set', async () => { - await reconfigureServerWithPageOptions({}); + await reconfigureServerWithPagesConfig({}); expect(Config.get(Parse.applicationId).pages.enableRouter).toBe( Definitions.PagesOptions.enableRouter.default ); expect(Config.get(Parse.applicationId).pages.enableLocalization).toBe( Definitions.PagesOptions.enableLocalization.default ); + expect(Config.get(Parse.applicationId).pages.localizationJsonPath).toBe( + Definitions.PagesOptions.localizationJsonPath.default + ); + expect(Config.get(Parse.applicationId).pages.localizationFallbackLocale).toBe( + Definitions.PagesOptions.localizationFallbackLocale.default + ); expect(Config.get(Parse.applicationId).pages.forceRedirect).toBe( Definitions.PagesOptions.forceRedirect.default ); @@ -276,9 +281,17 @@ describe('Pages Router', () => { { customUrls: 0 }, { customUrls: 'a' }, { customUrls: [] }, + { localizationJsonPath: true }, + { localizationJsonPath: 0 }, + { localizationJsonPath: {} }, + { localizationJsonPath: [] }, + { localizationFallbackLocale: true }, + { localizationFallbackLocale: 0 }, + { localizationFallbackLocale: {} }, + { localizationFallbackLocale: [] }, ]; for (const option of options) { - await expectAsync(reconfigureServerWithPageOptions(option)).toBeRejected(); + await expectAsync(reconfigureServerWithPagesConfig(option)).toBeRejected(); } }); }); @@ -334,11 +347,15 @@ describe('Pages Router', () => { }); it('returns custom page regardless of localization enabled', async () => { - req.config.pages.customUrls = { passwordResetLinkInvalid: 'http://invalid-link.example.com' }; + req.config.pages.customUrls = { + passwordResetLinkInvalid: 'http://invalid-link.example.com', + }; await expectAsync(router.goToPage(req, pages.passwordResetLinkInvalid)).toBeResolved(); expect(pageResponse).not.toHaveBeenCalled(); - expect(redirectResponse.calls.all()[0].args[0]).toBe(req.config.pages.customUrls.passwordResetLinkInvalid); + expect(redirectResponse.calls.all()[0].args[0]).toBe( + req.config.pages.customUrls.passwordResetLinkInvalid + ); }); it('returns file for locale match', async () => { @@ -352,7 +369,9 @@ describe('Pages Router', () => { it('returns file for language match', async () => { // Pretend no locale matching file exists spyOn(Utils, 'fileExists').and.callFake(async path => { - return !path.includes(`/${req.query.locale}/${pages.passwordResetLinkInvalid.defaultFile}`); + return !path.includes( + `/${req.query.locale}/${pages.passwordResetLinkInvalid.defaultFile}` + ); }); await expectAsync(router.goToPage(req, pages.passwordResetLinkInvalid)).toBeResolved(); @@ -371,7 +390,146 @@ describe('Pages Router', () => { new RegExp(`\/yo(-LO)?\/${pages.passwordResetLinkInvalid.defaultFile}`) ); }); + }); + + describe('localization with JSON resource', () => { + let jsonPageFile; + let jsonPageUrl; + let jsonResource; + + beforeEach(async () => { + jsonPageFile = 'custom_json.html'; + jsonPageUrl = new URL(`${config.publicServerURL}/apps/${jsonPageFile}`); + jsonResource = require('../public/custom_json.json'); + }); + + it('does not localize with JSON resource if localization is disabled', async () => { + config.pages.enableLocalization = false; + config.pages.localizationJsonPath = './public/custom_json.json'; + config.pages.localizationFallbackLocale = 'en'; + await reconfigureServer(config); + + const response = await request({ + url: jsonPageUrl.toString(), + followRedirects: false, + }); + expect(response.status).toBe(200); + expect(pageResponse.calls.all()[0].args[1]).toEqual({}); + expect(pageResponse.calls.all()[0].args[2]).toEqual({}); + + // Ensure header contains no page params + const pageParamHeaders = Object.keys(response.headers).filter(header => + header.startsWith(pageParamHeaderPrefix) + ); + expect(pageParamHeaders.length).toBe(0); + + // Ensure page response does not contain any translation + const flattenedJson = Utils.flattenObject(jsonResource); + for (const value of Object.values(flattenedJson)) { + const valueWithoutPlaceholder = fillPlaceholders(value, ''); + expect(response.text).not.toContain(valueWithoutPlaceholder); + } + }); + + it('localizes with JSON resource and fallback locale', async () => { + config.pages.enableLocalization = true; + config.pages.localizationJsonPath = './public/custom_json.json'; + config.pages.localizationFallbackLocale = 'en'; + await reconfigureServer(config); + + const response = await request({ + url: jsonPageUrl.toString(), + followRedirects: false, + }); + expect(response.status).toBe(200); + + // Ensure page response contains translation of fallback locale + const translation = jsonResource[config.pages.localizationFallbackLocale].translation; + for (const value of Object.values(translation)) { + const valueWithoutPlaceholder = fillPlaceholders(value, ''); + expect(response.text).toContain(valueWithoutPlaceholder); + } + }); + + it('localizes with JSON resource and request locale', async () => { + config.pages.enableLocalization = true; + config.pages.localizationJsonPath = './public/custom_json.json'; + config.pages.localizationFallbackLocale = 'en'; + await reconfigureServer(config); + + // Add locale to request URL + jsonPageUrl.searchParams.set('locale', exampleLocale); + const response = await request({ + url: jsonPageUrl.toString(), + followRedirects: false, + }); + expect(response.status).toBe(200); + + // Ensure page response contains translations of request locale + const translation = jsonResource[exampleLocale].translation; + for (const value of Object.values(translation)) { + const valueWithoutPlaceholder = fillPlaceholders(value, ''); + expect(response.text).toContain(valueWithoutPlaceholder); + } + }); + + it('localizes with JSON resource and language matching request locale', async () => { + config.pages.enableLocalization = true; + config.pages.localizationJsonPath = './public/custom_json.json'; + config.pages.localizationFallbackLocale = 'en'; + await reconfigureServer(config); + + // Add locale to request URL that has no locale match but only a language + // match in the JSON resource + jsonPageUrl.searchParams.set('locale', 'de-CH'); + + const response = await request({ + url: jsonPageUrl.toString(), + followRedirects: false, + }); + expect(response.status).toBe(200); + + // Ensure page response contains translations of requst language + const translation = jsonResource['de'].translation; + for (const value of Object.values(translation)) { + const valueWithoutPlaceholder = fillPlaceholders(value, ''); + expect(response.text).toContain(valueWithoutPlaceholder); + } + }); + + it('localizes with JSON resource and fills placeholders in JSON values', async () => { + config.pages.enableLocalization = true; + config.pages.localizationJsonPath = './public/custom_json.json'; + config.pages.localizationFallbackLocale = 'en'; + await reconfigureServer(config); + + // Add app ID to request URL so that the request is assigned to a Parse Server app + // and placeholders within translations strings can be replaced with default page + // parameters such as `appId` + jsonPageUrl.searchParams.set('appId', config.appId); + jsonPageUrl.searchParams.set('locale', exampleLocale); + + const response = await request({ + url: jsonPageUrl.toString(), + followRedirects: false, + }); + expect(response.status).toBe(200); + + // Fill placeholders in transation + let translation = jsonResource[exampleLocale].translation; + translation = JSON.stringify(translation); + translation = mustache.render(translation, { appName: config.appName }); + translation = JSON.parse(translation); + + // Ensure page response contains translation of request locale + for (const value of Object.values(translation)) { + expect(response.text).toContain(value); + } + }); + }); + + describe('response type', () => { it('returns a file for GET request', async () => { await expectAsync(router.goToPage(req, pages.passwordResetLinkInvalid)).toBeResolved(); expect(pageResponse).toHaveBeenCalled(); @@ -386,7 +544,9 @@ describe('Pages Router', () => { }); it('returns a redirect for custom pages for GET and POST request', async () => { - req.config.pages.customUrls = { passwordResetLinkInvalid: 'http://invalid-link.example.com' }; + req.config.pages.customUrls = { + passwordResetLinkInvalid: 'http://invalid-link.example.com', + }; for (const method of ['GET', 'POST']) { req.method = method; @@ -405,7 +565,9 @@ describe('Pages Router', () => { method: 'POST', }); expect(response.status).toEqual(303); - expect(response.headers.location).toContain('http://localhost:8378/1/apps/de-AT/password_reset_link_invalid.html'); + expect(response.headers.location).toContain( + 'http://localhost:8378/1/apps/de-AT/password_reset_link_invalid.html' + ); }); it('responds to GET request with content response', async () => { @@ -424,7 +586,10 @@ describe('Pages Router', () => { describe('end-to-end tests', () => { it('localizes end-to-end for password reset: success', async () => { await reconfigureServer(config); - const sendPasswordResetEmail = spyOn(config.emailAdapter, 'sendPasswordResetEmail').and.callThrough(); + const sendPasswordResetEmail = spyOn( + config.emailAdapter, + 'sendPasswordResetEmail' + ).and.callThrough(); const user = new Parse.User(); user.setUsername('exampleUsername'); user.setPassword('examplePassword'); @@ -472,12 +637,17 @@ describe('Pages Router', () => { followRedirects: false, }); expect(formResponse.status).toEqual(200); - expect(pageResponse.calls.all()[0].args[0]).toContain(`/${locale}/${pages.passwordResetSuccess.defaultFile}`); + expect(pageResponse.calls.all()[0].args[0]).toContain( + `/${locale}/${pages.passwordResetSuccess.defaultFile}` + ); }); it('localizes end-to-end for password reset: invalid link', async () => { await reconfigureServer(config); - const sendPasswordResetEmail = spyOn(config.emailAdapter, 'sendPasswordResetEmail').and.callThrough(); + const sendPasswordResetEmail = spyOn( + config.emailAdapter, + 'sendPasswordResetEmail' + ).and.callThrough(); const user = new Parse.User(); user.setUsername('exampleUsername'); user.setPassword('examplePassword'); @@ -504,7 +674,10 @@ describe('Pages Router', () => { it('localizes end-to-end for verify email: success', async () => { await reconfigureServer(config); - const sendVerificationEmail = spyOn(config.emailAdapter, 'sendVerificationEmail').and.callThrough(); + const sendVerificationEmail = spyOn( + config.emailAdapter, + 'sendVerificationEmail' + ).and.callThrough(); const user = new Parse.User(); user.setUsername('exampleUsername'); user.setPassword('examplePassword'); @@ -529,7 +702,10 @@ describe('Pages Router', () => { it('localizes end-to-end for verify email: invalid verification link - link send success', async () => { await reconfigureServer(config); - const sendVerificationEmail = spyOn(config.emailAdapter, 'sendVerificationEmail').and.callThrough(); + const sendVerificationEmail = spyOn( + config.emailAdapter, + 'sendVerificationEmail' + ).and.callThrough(); const user = new Parse.User(); user.setUsername('exampleUsername'); user.setPassword('examplePassword'); @@ -572,12 +748,17 @@ describe('Pages Router', () => { followRedirects: false, }); expect(formResponse.status).toEqual(303); - expect(formResponse.text).toContain(`/${locale}/${pages.emailVerificationSendSuccess.defaultFile}`); + expect(formResponse.text).toContain( + `/${locale}/${pages.emailVerificationSendSuccess.defaultFile}` + ); }); it('localizes end-to-end for verify email: invalid verification link - link send fail', async () => { await reconfigureServer(config); - const sendVerificationEmail = spyOn(config.emailAdapter, 'sendVerificationEmail').and.callThrough(); + const sendVerificationEmail = spyOn( + config.emailAdapter, + 'sendVerificationEmail' + ).and.callThrough(); const user = new Parse.User(); user.setUsername('exampleUsername'); user.setPassword('examplePassword'); @@ -608,9 +789,9 @@ describe('Pages Router', () => { new RegExp(`\/${exampleLocale}\/${pages.emailVerificationLinkExpired.defaultFile}`) ); - spyOn(UserController.prototype, 'resendVerificationEmail').and.callFake(() => Promise.reject( - 'failed to resend verification email' - )); + spyOn(UserController.prototype, 'resendVerificationEmail').and.callFake(() => + Promise.reject('failed to resend verification email') + ); const formUrl = `${publicServerUrl}/apps/${appId}/resend_verification_email`; const formResponse = await request({ @@ -624,7 +805,9 @@ describe('Pages Router', () => { followRedirects: false, }); expect(formResponse.status).toEqual(303); - expect(formResponse.text).toContain(`/${locale}/${pages.emailVerificationSendFail.defaultFile}`); + expect(formResponse.text).toContain( + `/${locale}/${pages.emailVerificationSendFail.defaultFile}` + ); }); it('localizes end-to-end for resend verification email: invalid link', async () => { @@ -640,32 +823,36 @@ describe('Pages Router', () => { followRedirects: false, }); expect(formResponse.status).toEqual(303); - expect(formResponse.text).toContain(`/${exampleLocale}/${pages.emailVerificationLinkInvalid.defaultFile}`); + expect(formResponse.text).toContain( + `/${exampleLocale}/${pages.emailVerificationLinkInvalid.defaultFile}` + ); }); }); describe('failing with missing parameters', () => { it('verifyEmail: throws on missing server configuration', async () => { delete req.config; - const verifyEmail = (req) => (() => new PagesRouter().verifyEmail(req)).bind(null); + const verifyEmail = req => (() => new PagesRouter().verifyEmail(req)).bind(null); expect(verifyEmail(req)).toThrow(); }); it('resendVerificationEmail: throws on missing server configuration', async () => { delete req.config; - const resendVerificationEmail = (req) => (() => new PagesRouter().resendVerificationEmail(req)).bind(null); + const resendVerificationEmail = req => + (() => new PagesRouter().resendVerificationEmail(req)).bind(null); expect(resendVerificationEmail(req)).toThrow(); }); it('requestResetPassword: throws on missing server configuration', async () => { delete req.config; - const requestResetPassword = (req) => (() => new PagesRouter().requestResetPassword(req)).bind(null); + const requestResetPassword = req => + (() => new PagesRouter().requestResetPassword(req)).bind(null); expect(requestResetPassword(req)).toThrow(); }); it('resetPassword: throws on missing server configuration', async () => { delete req.config; - const resetPassword = (req) => (() => new PagesRouter().resetPassword(req)).bind(null); + const resetPassword = req => (() => new PagesRouter().resetPassword(req)).bind(null); expect(resetPassword(req)).toThrow(); }); @@ -673,7 +860,7 @@ describe('Pages Router', () => { req.query.token = 'exampleToken'; req.params = {}; req.config.userController = { verifyEmail: () => Promise.reject() }; - const verifyEmail = (req) => new PagesRouter().verifyEmail(req); + const verifyEmail = req => new PagesRouter().verifyEmail(req); await verifyEmail(req); expect(goToPage.calls.all()[0].args[1]).toBe(pages.emailVerificationLinkInvalid); @@ -687,7 +874,7 @@ describe('Pages Router', () => { }; const error = 'exampleError'; req.config.userController = { updatePassword: () => Promise.reject(error) }; - const resetPassword = (req) => new PagesRouter().resetPassword(req); + const resetPassword = req => new PagesRouter().resetPassword(req); await resetPassword(req); expect(goToPage.calls.all()[0].args[1]).toBe(pages.passwordReset); @@ -703,11 +890,27 @@ describe('Pages Router', () => { }; const error = 'exampleError'; req.config.userController = { updatePassword: () => Promise.reject(error) }; - const resetPassword = (req) => new PagesRouter().resetPassword(req).catch(e => e); + const resetPassword = req => new PagesRouter().resetPassword(req).catch(e => e); const response = await resetPassword(req); expect(response.code).toBe(Parse.Error.OTHER_CAUSE); }); }); + + describe('exploits', () => { + it('rejects requesting file outside of pages scope with UNIX path patterns', async () => { + await reconfigureServer(config); + + // Do not compose this URL with `new URL(...)` because that would normalize + // the URL and remove path patterns; the path patterns must reach the router + const url = `${config.publicServerURL}/apps/../.gitignore`; + const response = await request({ + url: url, + followRedirects: false, + }).catch(e => e); + expect(response.status).toBe(404); + expect(response.text).toBe('Not found.'); + }); + }); }); }); diff --git a/src/Config.js b/src/Config.js index 6967d418fc..960be4ee57 100644 --- a/src/Config.js +++ b/src/Config.js @@ -6,11 +6,7 @@ import AppCache from './cache'; import SchemaCache from './Controllers/SchemaCache'; import DatabaseController from './Controllers/DatabaseController'; import net from 'net'; -import { - IdempotencyOptions, - FileUploadOptions, - PagesOptions, -} from './Options/Definitions'; +import { IdempotencyOptions, FileUploadOptions, PagesOptions } from './Options/Definitions'; import { isBoolean, isString } from 'lodash'; function removeTrailingSlash(str) { @@ -129,6 +125,16 @@ export class Config { } else if (!isBoolean(pages.enableLocalization)) { throw 'Parse Server option pages.enableLocalization must be a boolean.'; } + if (pages.localizationJsonPath === undefined) { + pages.localizationJsonPath = PagesOptions.localizationJsonPath.default; + } else if (!isString(pages.localizationJsonPath)) { + throw 'Parse Server option pages.localizationJsonPath must be a string.'; + } + if (pages.localizationFallbackLocale === undefined) { + pages.localizationFallbackLocale = PagesOptions.localizationFallbackLocale.default; + } else if (!isString(pages.localizationFallbackLocale)) { + throw 'Parse Server option pages.localizationFallbackLocale must be a string.'; + } if (pages.forceRedirect === undefined) { pages.forceRedirect = PagesOptions.forceRedirect.default; } else if (!isBoolean(pages.forceRedirect)) { diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index 8343b01646..cfd8742535 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -3,689 +3,725 @@ This code has been generated by resources/buildConfigDefinitions.js Do not edit manually, but update Options/index.js */ -var parsers = require("./parsers"); +var parsers = require('./parsers'); module.exports.ParseServerOptions = { - "accountLockout": { - "env": "PARSE_SERVER_ACCOUNT_LOCKOUT", - "help": "account lockout policy for failed login attempts", - "action": parsers.objectParser - }, - "allowClientClassCreation": { - "env": "PARSE_SERVER_ALLOW_CLIENT_CLASS_CREATION", - "help": "Enable (or disable) client class creation, defaults to true", - "action": parsers.booleanParser, - "default": true - }, - "allowCustomObjectId": { - "env": "PARSE_SERVER_ALLOW_CUSTOM_OBJECT_ID", - "help": "Enable (or disable) custom objectId", - "action": parsers.booleanParser, - "default": false - }, - "allowHeaders": { - "env": "PARSE_SERVER_ALLOW_HEADERS", - "help": "Add headers to Access-Control-Allow-Headers", - "action": parsers.arrayParser - }, - "allowOrigin": { - "env": "PARSE_SERVER_ALLOW_ORIGIN", - "help": "Sets the origin to Access-Control-Allow-Origin" - }, - "analyticsAdapter": { - "env": "PARSE_SERVER_ANALYTICS_ADAPTER", - "help": "Adapter module for the analytics", - "action": parsers.moduleOrObjectParser - }, - "appId": { - "env": "PARSE_SERVER_APPLICATION_ID", - "help": "Your Parse Application ID", - "required": true - }, - "appName": { - "env": "PARSE_SERVER_APP_NAME", - "help": "Sets the app name" - }, - "auth": { - "env": "PARSE_SERVER_AUTH_PROVIDERS", - "help": "Configuration for your authentication providers, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#oauth-and-3rd-party-authentication", - "action": parsers.objectParser - }, - "cacheAdapter": { - "env": "PARSE_SERVER_CACHE_ADAPTER", - "help": "Adapter module for the cache", - "action": parsers.moduleOrObjectParser - }, - "cacheMaxSize": { - "env": "PARSE_SERVER_CACHE_MAX_SIZE", - "help": "Sets the maximum size for the in memory cache, defaults to 10000", - "action": parsers.numberParser("cacheMaxSize"), - "default": 10000 - }, - "cacheTTL": { - "env": "PARSE_SERVER_CACHE_TTL", - "help": "Sets the TTL for the in memory cache (in ms), defaults to 5000 (5 seconds)", - "action": parsers.numberParser("cacheTTL"), - "default": 5000 - }, - "clientKey": { - "env": "PARSE_SERVER_CLIENT_KEY", - "help": "Key for iOS, MacOS, tvOS clients" - }, - "cloud": { - "env": "PARSE_SERVER_CLOUD", - "help": "Full path to your cloud code main.js" - }, - "cluster": { - "env": "PARSE_SERVER_CLUSTER", - "help": "Run with cluster, optionally set the number of processes default to os.cpus().length", - "action": parsers.numberOrBooleanParser - }, - "collectionPrefix": { - "env": "PARSE_SERVER_COLLECTION_PREFIX", - "help": "A collection prefix for the classes", - "default": "" - }, - "customPages": { - "env": "PARSE_SERVER_CUSTOM_PAGES", - "help": "custom pages for password validation and reset", - "action": parsers.objectParser, - "default": {} - }, - "databaseAdapter": { - "env": "PARSE_SERVER_DATABASE_ADAPTER", - "help": "Adapter module for the database", - "action": parsers.moduleOrObjectParser - }, - "databaseOptions": { - "env": "PARSE_SERVER_DATABASE_OPTIONS", - "help": "Options to pass to the mongodb client", - "action": parsers.objectParser - }, - "databaseURI": { - "env": "PARSE_SERVER_DATABASE_URI", - "help": "The full URI to your database. Supported databases are mongodb or postgres.", - "required": true, - "default": "mongodb://localhost:27017/parse" - }, - "directAccess": { - "env": "PARSE_SERVER_ENABLE_EXPERIMENTAL_DIRECT_ACCESS", - "help": "Replace HTTP Interface when using JS SDK in current node runtime, defaults to false. Caution, this is an experimental feature that may not be appropriate for production.", - "action": parsers.booleanParser, - "default": false - }, - "dotNetKey": { - "env": "PARSE_SERVER_DOT_NET_KEY", - "help": "Key for Unity and .Net SDK" - }, - "emailAdapter": { - "env": "PARSE_SERVER_EMAIL_ADAPTER", - "help": "Adapter module for email sending", - "action": parsers.moduleOrObjectParser - }, - "emailVerifyTokenReuseIfValid": { - "env": "PARSE_SERVER_EMAIL_VERIFY_TOKEN_REUSE_IF_VALID", - "help": "an existing email verify token should be reused when resend verification email is requested", - "action": parsers.booleanParser, - "default": false - }, - "emailVerifyTokenValidityDuration": { - "env": "PARSE_SERVER_EMAIL_VERIFY_TOKEN_VALIDITY_DURATION", - "help": "Email verification token validity duration, in seconds", - "action": parsers.numberParser("emailVerifyTokenValidityDuration") - }, - "enableAnonymousUsers": { - "env": "PARSE_SERVER_ENABLE_ANON_USERS", - "help": "Enable (or disable) anonymous users, defaults to true", - "action": parsers.booleanParser, - "default": true - }, - "enableExpressErrorHandler": { - "env": "PARSE_SERVER_ENABLE_EXPRESS_ERROR_HANDLER", - "help": "Enables the default express error handler for all errors", - "action": parsers.booleanParser, - "default": false - }, - "enableSingleSchemaCache": { - "env": "PARSE_SERVER_ENABLE_SINGLE_SCHEMA_CACHE", - "help": "Use a single schema cache shared across requests. Reduces number of queries made to _SCHEMA, defaults to false, i.e. unique schema cache per request.", - "action": parsers.booleanParser, - "default": false - }, - "encryptionKey": { - "env": "PARSE_SERVER_ENCRYPTION_KEY", - "help": "Key for encrypting your files" - }, - "expireInactiveSessions": { - "env": "PARSE_SERVER_EXPIRE_INACTIVE_SESSIONS", - "help": "Sets wether we should expire the inactive sessions, defaults to true", - "action": parsers.booleanParser, - "default": true - }, - "fileKey": { - "env": "PARSE_SERVER_FILE_KEY", - "help": "Key for your files" - }, - "filesAdapter": { - "env": "PARSE_SERVER_FILES_ADAPTER", - "help": "Adapter module for the files sub-system", - "action": parsers.moduleOrObjectParser - }, - "fileUpload": { - "env": "PARSE_SERVER_FILE_UPLOAD_OPTIONS", - "help": "Options for file uploads", - "action": parsers.objectParser, - "default": {} - }, - "graphQLPath": { - "env": "PARSE_SERVER_GRAPHQL_PATH", - "help": "Mount path for the GraphQL endpoint, defaults to /graphql", - "default": "/graphql" - }, - "graphQLSchema": { - "env": "PARSE_SERVER_GRAPH_QLSCHEMA", - "help": "Full path to your GraphQL custom schema.graphql file" - }, - "host": { - "env": "PARSE_SERVER_HOST", - "help": "The host to serve ParseServer on, defaults to 0.0.0.0", - "default": "0.0.0.0" - }, - "idempotencyOptions": { - "env": "PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_OPTIONS", - "help": "Options for request idempotency to deduplicate identical requests that may be caused by network issues. Caution, this is an experimental feature that may not be appropriate for production.", - "action": parsers.objectParser, - "default": {} - }, - "javascriptKey": { - "env": "PARSE_SERVER_JAVASCRIPT_KEY", - "help": "Key for the Javascript SDK" - }, - "jsonLogs": { - "env": "JSON_LOGS", - "help": "Log as structured JSON objects", - "action": parsers.booleanParser - }, - "liveQuery": { - "env": "PARSE_SERVER_LIVE_QUERY", - "help": "parse-server's LiveQuery configuration object", - "action": parsers.objectParser - }, - "liveQueryServerOptions": { - "env": "PARSE_SERVER_LIVE_QUERY_SERVER_OPTIONS", - "help": "Live query server configuration options (will start the liveQuery server)", - "action": parsers.objectParser - }, - "loggerAdapter": { - "env": "PARSE_SERVER_LOGGER_ADAPTER", - "help": "Adapter module for the logging sub-system", - "action": parsers.moduleOrObjectParser - }, - "logLevel": { - "env": "PARSE_SERVER_LOG_LEVEL", - "help": "Sets the level for logs" - }, - "logsFolder": { - "env": "PARSE_SERVER_LOGS_FOLDER", - "help": "Folder for the logs (defaults to './logs'); set to null to disable file based logging", - "default": "./logs" - }, - "masterKey": { - "env": "PARSE_SERVER_MASTER_KEY", - "help": "Your Parse Master Key", - "required": true - }, - "masterKeyIps": { - "env": "PARSE_SERVER_MASTER_KEY_IPS", - "help": "Restrict masterKey to be used by only these ips, defaults to [] (allow all ips)", - "action": parsers.arrayParser, - "default": [] - }, - "maxLimit": { - "env": "PARSE_SERVER_MAX_LIMIT", - "help": "Max value for limit option on queries, defaults to unlimited", - "action": parsers.numberParser("maxLimit") - }, - "maxLogFiles": { - "env": "PARSE_SERVER_MAX_LOG_FILES", - "help": "Maximum number of logs to keep. If not set, no logs will be removed. This can be a number of files or number of days. If using days, add 'd' as the suffix. (default: null)", - "action": parsers.objectParser - }, - "maxUploadSize": { - "env": "PARSE_SERVER_MAX_UPLOAD_SIZE", - "help": "Max file size for uploads, defaults to 20mb", - "default": "20mb" - }, - "middleware": { - "env": "PARSE_SERVER_MIDDLEWARE", - "help": "middleware for express server, can be string or function" - }, - "mountGraphQL": { - "env": "PARSE_SERVER_MOUNT_GRAPHQL", - "help": "Mounts the GraphQL endpoint", - "action": parsers.booleanParser, - "default": false - }, - "mountPath": { - "env": "PARSE_SERVER_MOUNT_PATH", - "help": "Mount path for the server, defaults to /parse", - "default": "/parse" - }, - "mountPlayground": { - "env": "PARSE_SERVER_MOUNT_PLAYGROUND", - "help": "Mounts the GraphQL Playground - never use this option in production", - "action": parsers.booleanParser, - "default": false - }, - "objectIdSize": { - "env": "PARSE_SERVER_OBJECT_ID_SIZE", - "help": "Sets the number of characters in generated object id's, default 10", - "action": parsers.numberParser("objectIdSize"), - "default": 10 - }, - "pages": { - "env": "PARSE_SERVER_PAGES", - "help": "The options for pages such as password reset and email verification. Caution, this is an experimental feature that may not be appropriate for production.", - "action": parsers.objectParser, - "default": {} - }, - "passwordPolicy": { - "env": "PARSE_SERVER_PASSWORD_POLICY", - "help": "Password policy for enforcing password related rules", - "action": parsers.objectParser - }, - "playgroundPath": { - "env": "PARSE_SERVER_PLAYGROUND_PATH", - "help": "Mount path for the GraphQL Playground, defaults to /playground", - "default": "/playground" - }, - "port": { - "env": "PORT", - "help": "The port to run the ParseServer, defaults to 1337.", - "action": parsers.numberParser("port"), - "default": 1337 - }, - "preserveFileName": { - "env": "PARSE_SERVER_PRESERVE_FILE_NAME", - "help": "Enable (or disable) the addition of a unique hash to the file names", - "action": parsers.booleanParser, - "default": false - }, - "preventLoginWithUnverifiedEmail": { - "env": "PARSE_SERVER_PREVENT_LOGIN_WITH_UNVERIFIED_EMAIL", - "help": "Prevent user from login if email is not verified and PARSE_SERVER_VERIFY_USER_EMAILS is true, defaults to false", - "action": parsers.booleanParser, - "default": false - }, - "protectedFields": { - "env": "PARSE_SERVER_PROTECTED_FIELDS", - "help": "Protected fields that should be treated with extra security when fetching details.", - "action": parsers.objectParser, - "default": { - "_User": { - "*": ["email"] - } - } - }, - "publicServerURL": { - "env": "PARSE_PUBLIC_SERVER_URL", - "help": "Public URL to your parse server with http:// or https://." - }, - "push": { - "env": "PARSE_SERVER_PUSH", - "help": "Configuration for push, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#push-notifications", - "action": parsers.objectParser - }, - "readOnlyMasterKey": { - "env": "PARSE_SERVER_READ_ONLY_MASTER_KEY", - "help": "Read-only key, which has the same capabilities as MasterKey without writes" - }, - "restAPIKey": { - "env": "PARSE_SERVER_REST_API_KEY", - "help": "Key for REST calls" - }, - "revokeSessionOnPasswordReset": { - "env": "PARSE_SERVER_REVOKE_SESSION_ON_PASSWORD_RESET", - "help": "When a user changes their password, either through the reset password email or while logged in, all sessions are revoked if this is true. Set to false if you don't want to revoke sessions.", - "action": parsers.booleanParser, - "default": true - }, - "scheduledPush": { - "env": "PARSE_SERVER_SCHEDULED_PUSH", - "help": "Configuration for push scheduling, defaults to false.", - "action": parsers.booleanParser, - "default": false - }, - "schemaCacheTTL": { - "env": "PARSE_SERVER_SCHEMA_CACHE_TTL", - "help": "The TTL for caching the schema for optimizing read/write operations. You should put a long TTL when your DB is in production. default to 5000; set 0 to disable.", - "action": parsers.numberParser("schemaCacheTTL"), - "default": 5000 - }, - "serverCloseComplete": { - "env": "PARSE_SERVER_SERVER_CLOSE_COMPLETE", - "help": "Callback when server has closed" - }, - "serverStartComplete": { - "env": "PARSE_SERVER_SERVER_START_COMPLETE", - "help": "Callback when server has started" - }, - "serverURL": { - "env": "PARSE_SERVER_URL", - "help": "URL to your parse server with http:// or https://.", - "required": true - }, - "sessionLength": { - "env": "PARSE_SERVER_SESSION_LENGTH", - "help": "Session duration, in seconds, defaults to 1 year", - "action": parsers.numberParser("sessionLength"), - "default": 31536000 - }, - "silent": { - "env": "SILENT", - "help": "Disables console output", - "action": parsers.booleanParser - }, - "startLiveQueryServer": { - "env": "PARSE_SERVER_START_LIVE_QUERY_SERVER", - "help": "Starts the liveQuery server", - "action": parsers.booleanParser - }, - "userSensitiveFields": { - "env": "PARSE_SERVER_USER_SENSITIVE_FIELDS", - "help": "Personally identifiable information fields in the user table the should be removed for non-authorized users. Deprecated @see protectedFields", - "action": parsers.arrayParser - }, - "verbose": { - "env": "VERBOSE", - "help": "Set the logging to verbose", - "action": parsers.booleanParser - }, - "verifyUserEmails": { - "env": "PARSE_SERVER_VERIFY_USER_EMAILS", - "help": "Enable (or disable) user email validation, defaults to false", - "action": parsers.booleanParser, - "default": false - }, - "webhookKey": { - "env": "PARSE_SERVER_WEBHOOK_KEY", - "help": "Key sent with outgoing webhook calls" - } + accountLockout: { + env: 'PARSE_SERVER_ACCOUNT_LOCKOUT', + help: 'account lockout policy for failed login attempts', + action: parsers.objectParser, + }, + allowClientClassCreation: { + env: 'PARSE_SERVER_ALLOW_CLIENT_CLASS_CREATION', + help: 'Enable (or disable) client class creation, defaults to true', + action: parsers.booleanParser, + default: true, + }, + allowCustomObjectId: { + env: 'PARSE_SERVER_ALLOW_CUSTOM_OBJECT_ID', + help: 'Enable (or disable) custom objectId', + action: parsers.booleanParser, + default: false, + }, + allowHeaders: { + env: 'PARSE_SERVER_ALLOW_HEADERS', + help: 'Add headers to Access-Control-Allow-Headers', + action: parsers.arrayParser, + }, + allowOrigin: { + env: 'PARSE_SERVER_ALLOW_ORIGIN', + help: 'Sets the origin to Access-Control-Allow-Origin', + }, + analyticsAdapter: { + env: 'PARSE_SERVER_ANALYTICS_ADAPTER', + help: 'Adapter module for the analytics', + action: parsers.moduleOrObjectParser, + }, + appId: { + env: 'PARSE_SERVER_APPLICATION_ID', + help: 'Your Parse Application ID', + required: true, + }, + appName: { + env: 'PARSE_SERVER_APP_NAME', + help: 'Sets the app name', + }, + auth: { + env: 'PARSE_SERVER_AUTH_PROVIDERS', + help: + 'Configuration for your authentication providers, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#oauth-and-3rd-party-authentication', + action: parsers.objectParser, + }, + cacheAdapter: { + env: 'PARSE_SERVER_CACHE_ADAPTER', + help: 'Adapter module for the cache', + action: parsers.moduleOrObjectParser, + }, + cacheMaxSize: { + env: 'PARSE_SERVER_CACHE_MAX_SIZE', + help: 'Sets the maximum size for the in memory cache, defaults to 10000', + action: parsers.numberParser('cacheMaxSize'), + default: 10000, + }, + cacheTTL: { + env: 'PARSE_SERVER_CACHE_TTL', + help: 'Sets the TTL for the in memory cache (in ms), defaults to 5000 (5 seconds)', + action: parsers.numberParser('cacheTTL'), + default: 5000, + }, + clientKey: { + env: 'PARSE_SERVER_CLIENT_KEY', + help: 'Key for iOS, MacOS, tvOS clients', + }, + cloud: { + env: 'PARSE_SERVER_CLOUD', + help: 'Full path to your cloud code main.js', + }, + cluster: { + env: 'PARSE_SERVER_CLUSTER', + help: 'Run with cluster, optionally set the number of processes default to os.cpus().length', + action: parsers.numberOrBooleanParser, + }, + collectionPrefix: { + env: 'PARSE_SERVER_COLLECTION_PREFIX', + help: 'A collection prefix for the classes', + default: '', + }, + customPages: { + env: 'PARSE_SERVER_CUSTOM_PAGES', + help: 'custom pages for password validation and reset', + action: parsers.objectParser, + default: {}, + }, + databaseAdapter: { + env: 'PARSE_SERVER_DATABASE_ADAPTER', + help: 'Adapter module for the database', + action: parsers.moduleOrObjectParser, + }, + databaseOptions: { + env: 'PARSE_SERVER_DATABASE_OPTIONS', + help: 'Options to pass to the mongodb client', + action: parsers.objectParser, + }, + databaseURI: { + env: 'PARSE_SERVER_DATABASE_URI', + help: 'The full URI to your database. Supported databases are mongodb or postgres.', + required: true, + default: 'mongodb://localhost:27017/parse', + }, + directAccess: { + env: 'PARSE_SERVER_ENABLE_EXPERIMENTAL_DIRECT_ACCESS', + help: + 'Replace HTTP Interface when using JS SDK in current node runtime, defaults to false. Caution, this is an experimental feature that may not be appropriate for production.', + action: parsers.booleanParser, + default: false, + }, + dotNetKey: { + env: 'PARSE_SERVER_DOT_NET_KEY', + help: 'Key for Unity and .Net SDK', + }, + emailAdapter: { + env: 'PARSE_SERVER_EMAIL_ADAPTER', + help: 'Adapter module for email sending', + action: parsers.moduleOrObjectParser, + }, + emailVerifyTokenReuseIfValid: { + env: 'PARSE_SERVER_EMAIL_VERIFY_TOKEN_REUSE_IF_VALID', + help: + 'an existing email verify token should be reused when resend verification email is requested', + action: parsers.booleanParser, + default: false, + }, + emailVerifyTokenValidityDuration: { + env: 'PARSE_SERVER_EMAIL_VERIFY_TOKEN_VALIDITY_DURATION', + help: 'Email verification token validity duration, in seconds', + action: parsers.numberParser('emailVerifyTokenValidityDuration'), + }, + enableAnonymousUsers: { + env: 'PARSE_SERVER_ENABLE_ANON_USERS', + help: 'Enable (or disable) anonymous users, defaults to true', + action: parsers.booleanParser, + default: true, + }, + enableExpressErrorHandler: { + env: 'PARSE_SERVER_ENABLE_EXPRESS_ERROR_HANDLER', + help: 'Enables the default express error handler for all errors', + action: parsers.booleanParser, + default: false, + }, + enableSingleSchemaCache: { + env: 'PARSE_SERVER_ENABLE_SINGLE_SCHEMA_CACHE', + help: + 'Use a single schema cache shared across requests. Reduces number of queries made to _SCHEMA, defaults to false, i.e. unique schema cache per request.', + action: parsers.booleanParser, + default: false, + }, + encryptionKey: { + env: 'PARSE_SERVER_ENCRYPTION_KEY', + help: 'Key for encrypting your files', + }, + expireInactiveSessions: { + env: 'PARSE_SERVER_EXPIRE_INACTIVE_SESSIONS', + help: 'Sets wether we should expire the inactive sessions, defaults to true', + action: parsers.booleanParser, + default: true, + }, + fileKey: { + env: 'PARSE_SERVER_FILE_KEY', + help: 'Key for your files', + }, + filesAdapter: { + env: 'PARSE_SERVER_FILES_ADAPTER', + help: 'Adapter module for the files sub-system', + action: parsers.moduleOrObjectParser, + }, + fileUpload: { + env: 'PARSE_SERVER_FILE_UPLOAD_OPTIONS', + help: 'Options for file uploads', + action: parsers.objectParser, + default: {}, + }, + graphQLPath: { + env: 'PARSE_SERVER_GRAPHQL_PATH', + help: 'Mount path for the GraphQL endpoint, defaults to /graphql', + default: '/graphql', + }, + graphQLSchema: { + env: 'PARSE_SERVER_GRAPH_QLSCHEMA', + help: 'Full path to your GraphQL custom schema.graphql file', + }, + host: { + env: 'PARSE_SERVER_HOST', + help: 'The host to serve ParseServer on, defaults to 0.0.0.0', + default: '0.0.0.0', + }, + idempotencyOptions: { + env: 'PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_OPTIONS', + help: + 'Options for request idempotency to deduplicate identical requests that may be caused by network issues. Caution, this is an experimental feature that may not be appropriate for production.', + action: parsers.objectParser, + default: {}, + }, + javascriptKey: { + env: 'PARSE_SERVER_JAVASCRIPT_KEY', + help: 'Key for the Javascript SDK', + }, + jsonLogs: { + env: 'JSON_LOGS', + help: 'Log as structured JSON objects', + action: parsers.booleanParser, + }, + liveQuery: { + env: 'PARSE_SERVER_LIVE_QUERY', + help: "parse-server's LiveQuery configuration object", + action: parsers.objectParser, + }, + liveQueryServerOptions: { + env: 'PARSE_SERVER_LIVE_QUERY_SERVER_OPTIONS', + help: 'Live query server configuration options (will start the liveQuery server)', + action: parsers.objectParser, + }, + loggerAdapter: { + env: 'PARSE_SERVER_LOGGER_ADAPTER', + help: 'Adapter module for the logging sub-system', + action: parsers.moduleOrObjectParser, + }, + logLevel: { + env: 'PARSE_SERVER_LOG_LEVEL', + help: 'Sets the level for logs', + }, + logsFolder: { + env: 'PARSE_SERVER_LOGS_FOLDER', + help: "Folder for the logs (defaults to './logs'); set to null to disable file based logging", + default: './logs', + }, + masterKey: { + env: 'PARSE_SERVER_MASTER_KEY', + help: 'Your Parse Master Key', + required: true, + }, + masterKeyIps: { + env: 'PARSE_SERVER_MASTER_KEY_IPS', + help: 'Restrict masterKey to be used by only these ips, defaults to [] (allow all ips)', + action: parsers.arrayParser, + default: [], + }, + maxLimit: { + env: 'PARSE_SERVER_MAX_LIMIT', + help: 'Max value for limit option on queries, defaults to unlimited', + action: parsers.numberParser('maxLimit'), + }, + maxLogFiles: { + env: 'PARSE_SERVER_MAX_LOG_FILES', + help: + "Maximum number of logs to keep. If not set, no logs will be removed. This can be a number of files or number of days. If using days, add 'd' as the suffix. (default: null)", + action: parsers.objectParser, + }, + maxUploadSize: { + env: 'PARSE_SERVER_MAX_UPLOAD_SIZE', + help: 'Max file size for uploads, defaults to 20mb', + default: '20mb', + }, + middleware: { + env: 'PARSE_SERVER_MIDDLEWARE', + help: 'middleware for express server, can be string or function', + }, + mountGraphQL: { + env: 'PARSE_SERVER_MOUNT_GRAPHQL', + help: 'Mounts the GraphQL endpoint', + action: parsers.booleanParser, + default: false, + }, + mountPath: { + env: 'PARSE_SERVER_MOUNT_PATH', + help: 'Mount path for the server, defaults to /parse', + default: '/parse', + }, + mountPlayground: { + env: 'PARSE_SERVER_MOUNT_PLAYGROUND', + help: 'Mounts the GraphQL Playground - never use this option in production', + action: parsers.booleanParser, + default: false, + }, + objectIdSize: { + env: 'PARSE_SERVER_OBJECT_ID_SIZE', + help: "Sets the number of characters in generated object id's, default 10", + action: parsers.numberParser('objectIdSize'), + default: 10, + }, + pages: { + env: 'PARSE_SERVER_PAGES', + help: + 'The options for pages such as password reset and email verification. Caution, this is an experimental feature that may not be appropriate for production.', + action: parsers.objectParser, + default: {}, + }, + passwordPolicy: { + env: 'PARSE_SERVER_PASSWORD_POLICY', + help: 'Password policy for enforcing password related rules', + action: parsers.objectParser, + }, + playgroundPath: { + env: 'PARSE_SERVER_PLAYGROUND_PATH', + help: 'Mount path for the GraphQL Playground, defaults to /playground', + default: '/playground', + }, + port: { + env: 'PORT', + help: 'The port to run the ParseServer, defaults to 1337.', + action: parsers.numberParser('port'), + default: 1337, + }, + preserveFileName: { + env: 'PARSE_SERVER_PRESERVE_FILE_NAME', + help: 'Enable (or disable) the addition of a unique hash to the file names', + action: parsers.booleanParser, + default: false, + }, + preventLoginWithUnverifiedEmail: { + env: 'PARSE_SERVER_PREVENT_LOGIN_WITH_UNVERIFIED_EMAIL', + help: + 'Prevent user from login if email is not verified and PARSE_SERVER_VERIFY_USER_EMAILS is true, defaults to false', + action: parsers.booleanParser, + default: false, + }, + protectedFields: { + env: 'PARSE_SERVER_PROTECTED_FIELDS', + help: 'Protected fields that should be treated with extra security when fetching details.', + action: parsers.objectParser, + default: { + _User: { + '*': ['email'], + }, + }, + }, + publicServerURL: { + env: 'PARSE_PUBLIC_SERVER_URL', + help: 'Public URL to your parse server with http:// or https://.', + }, + push: { + env: 'PARSE_SERVER_PUSH', + help: + 'Configuration for push, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#push-notifications', + action: parsers.objectParser, + }, + readOnlyMasterKey: { + env: 'PARSE_SERVER_READ_ONLY_MASTER_KEY', + help: 'Read-only key, which has the same capabilities as MasterKey without writes', + }, + restAPIKey: { + env: 'PARSE_SERVER_REST_API_KEY', + help: 'Key for REST calls', + }, + revokeSessionOnPasswordReset: { + env: 'PARSE_SERVER_REVOKE_SESSION_ON_PASSWORD_RESET', + help: + "When a user changes their password, either through the reset password email or while logged in, all sessions are revoked if this is true. Set to false if you don't want to revoke sessions.", + action: parsers.booleanParser, + default: true, + }, + scheduledPush: { + env: 'PARSE_SERVER_SCHEDULED_PUSH', + help: 'Configuration for push scheduling, defaults to false.', + action: parsers.booleanParser, + default: false, + }, + schemaCacheTTL: { + env: 'PARSE_SERVER_SCHEMA_CACHE_TTL', + help: + 'The TTL for caching the schema for optimizing read/write operations. You should put a long TTL when your DB is in production. default to 5000; set 0 to disable.', + action: parsers.numberParser('schemaCacheTTL'), + default: 5000, + }, + serverCloseComplete: { + env: 'PARSE_SERVER_SERVER_CLOSE_COMPLETE', + help: 'Callback when server has closed', + }, + serverStartComplete: { + env: 'PARSE_SERVER_SERVER_START_COMPLETE', + help: 'Callback when server has started', + }, + serverURL: { + env: 'PARSE_SERVER_URL', + help: 'URL to your parse server with http:// or https://.', + required: true, + }, + sessionLength: { + env: 'PARSE_SERVER_SESSION_LENGTH', + help: 'Session duration, in seconds, defaults to 1 year', + action: parsers.numberParser('sessionLength'), + default: 31536000, + }, + silent: { + env: 'SILENT', + help: 'Disables console output', + action: parsers.booleanParser, + }, + startLiveQueryServer: { + env: 'PARSE_SERVER_START_LIVE_QUERY_SERVER', + help: 'Starts the liveQuery server', + action: parsers.booleanParser, + }, + userSensitiveFields: { + env: 'PARSE_SERVER_USER_SENSITIVE_FIELDS', + help: + 'Personally identifiable information fields in the user table the should be removed for non-authorized users. Deprecated @see protectedFields', + action: parsers.arrayParser, + }, + verbose: { + env: 'VERBOSE', + help: 'Set the logging to verbose', + action: parsers.booleanParser, + }, + verifyUserEmails: { + env: 'PARSE_SERVER_VERIFY_USER_EMAILS', + help: 'Enable (or disable) user email validation, defaults to false', + action: parsers.booleanParser, + default: false, + }, + webhookKey: { + env: 'PARSE_SERVER_WEBHOOK_KEY', + help: 'Key sent with outgoing webhook calls', + }, }; module.exports.PagesOptions = { - "customUrls": { - "env": "PARSE_SERVER_PAGES_CUSTOM_URLS", - "help": "The URLs to the custom pages.", - "action": parsers.objectParser, - "default": {} - }, - "enableLocalization": { - "env": "PARSE_SERVER_PAGES_ENABLE_LOCALIZATION", - "help": "Is true if pages should be localized; this has no effect on custom page redirects.", - "action": parsers.booleanParser, - "default": false - }, - "enableRouter": { - "env": "PARSE_SERVER_PAGES_ENABLE_ROUTER", - "help": "Is true if the pages router should be enabled; this is required for any of the pages options to take effect. Caution, this is an experimental feature that may not be appropriate for production.", - "action": parsers.booleanParser, - "default": false - }, - "forceRedirect": { - "env": "PARSE_SERVER_PAGES_FORCE_REDIRECT", - "help": "Is true if responses should always be redirects and never content, false if the response type should depend on the request type (GET request -> content response; POST request -> redirect response).", - "action": parsers.booleanParser, - "default": false - }, - "pagesEndpoint": { - "env": "PARSE_SERVER_PAGES_PAGES_ENDPOINT", - "help": "The API endoint for the pages. Default is the 'apps'.", - "default": "apps" - }, - "pagesPath": { - "env": "PARSE_SERVER_PAGES_PAGES_PATH", - "help": "The path to the pages directory; this also defines where the static endpoint '/apps' points to. Default is the './public/' directory.", - "default": "./public" - } + customUrls: { + env: 'PARSE_SERVER_PAGES_CUSTOM_URLS', + help: 'The URLs to the custom pages.', + action: parsers.objectParser, + default: {}, + }, + enableLocalization: { + env: 'PARSE_SERVER_PAGES_ENABLE_LOCALIZATION', + help: 'Is true if pages should be localized; this has no effect on custom page redirects.', + action: parsers.booleanParser, + default: false, + }, + enableRouter: { + env: 'PARSE_SERVER_PAGES_ENABLE_ROUTER', + help: + 'Is true if the pages router should be enabled; this is required for any of the pages options to take effect. Caution, this is an experimental feature that may not be appropriate for production.', + action: parsers.booleanParser, + default: false, + }, + forceRedirect: { + env: 'PARSE_SERVER_PAGES_FORCE_REDIRECT', + help: + 'Is true if responses should always be redirects and never content, false if the response type should depend on the request type (GET request -> content response; POST request -> redirect response).', + action: parsers.booleanParser, + default: false, + }, + localizationFallbackLocale: { + env: 'PARSE_SERVER_PAGES_LOCALIZATION_FALLBACK_LOCALE', + help: + 'The fallback locale for localization if no matching translation is provided for the given locale. This is only relevant when providing translation resources via JSON file.', + default: 'en', + }, + localizationJsonPath: { + env: 'PARSE_SERVER_PAGES_LOCALIZATION_JSON_PATH', + help: + 'The path to the JSON file for localization; the translations will be used to fill template placeholders according to the locale.', + }, + pagesEndpoint: { + env: 'PARSE_SERVER_PAGES_PAGES_ENDPOINT', + help: "The API endoint for the pages. Default is the 'apps'.", + default: 'apps', + }, + pagesPath: { + env: 'PARSE_SERVER_PAGES_PAGES_PATH', + help: + "The path to the pages directory; this also defines where the static endpoint '/apps' points to. Default is the './public/' directory.", + default: './public', + }, }; module.exports.PagesCustomUrlsOptions = { - "emailVerificationLinkExpired": { - "env": "PARSE_SERVER_PAGES_CUSTOM_URL_EMAIL_VERIFICATION_LINK_EXPIRED", - "help": "The URL to the custom page for email verification -> link expired." - }, - "emailVerificationLinkInvalid": { - "env": "PARSE_SERVER_PAGES_CUSTOM_URL_EMAIL_VERIFICATION_LINK_INVALID", - "help": "The URL to the custom page for email verification -> link invalid." - }, - "emailVerificationSendFail": { - "env": "PARSE_SERVER_PAGES_CUSTOM_URL_EMAIL_VERIFICATION_SEND_FAIL", - "help": "The URL to the custom page for email verification -> link send fail." - }, - "emailVerificationSendSuccess": { - "env": "PARSE_SERVER_PAGES_CUSTOM_URL_EMAIL_VERIFICATION_SEND_SUCCESS", - "help": "The URL to the custom page for email verification -> resend link -> success." - }, - "emailVerificationSuccess": { - "env": "PARSE_SERVER_PAGES_CUSTOM_URL_EMAIL_VERIFICATION_SUCCESS", - "help": "The URL to the custom page for email verification -> success." - }, - "passwordReset": { - "env": "PARSE_SERVER_PAGES_CUSTOM_URL_PASSWORD_RESET", - "help": "The URL to the custom page for password reset." - }, - "passwordResetLinkInvalid": { - "env": "PARSE_SERVER_PAGES_CUSTOM_URL_PASSWORD_RESET_LINK_INVALID", - "help": "The URL to the custom page for password reset -> link invalid." - }, - "passwordResetSuccess": { - "env": "PARSE_SERVER_PAGES_CUSTOM_URL_PASSWORD_RESET_SUCCESS", - "help": "The URL to the custom page for password reset -> success." - } + emailVerificationLinkExpired: { + env: 'PARSE_SERVER_PAGES_CUSTOM_URL_EMAIL_VERIFICATION_LINK_EXPIRED', + help: 'The URL to the custom page for email verification -> link expired.', + }, + emailVerificationLinkInvalid: { + env: 'PARSE_SERVER_PAGES_CUSTOM_URL_EMAIL_VERIFICATION_LINK_INVALID', + help: 'The URL to the custom page for email verification -> link invalid.', + }, + emailVerificationSendFail: { + env: 'PARSE_SERVER_PAGES_CUSTOM_URL_EMAIL_VERIFICATION_SEND_FAIL', + help: 'The URL to the custom page for email verification -> link send fail.', + }, + emailVerificationSendSuccess: { + env: 'PARSE_SERVER_PAGES_CUSTOM_URL_EMAIL_VERIFICATION_SEND_SUCCESS', + help: 'The URL to the custom page for email verification -> resend link -> success.', + }, + emailVerificationSuccess: { + env: 'PARSE_SERVER_PAGES_CUSTOM_URL_EMAIL_VERIFICATION_SUCCESS', + help: 'The URL to the custom page for email verification -> success.', + }, + passwordReset: { + env: 'PARSE_SERVER_PAGES_CUSTOM_URL_PASSWORD_RESET', + help: 'The URL to the custom page for password reset.', + }, + passwordResetLinkInvalid: { + env: 'PARSE_SERVER_PAGES_CUSTOM_URL_PASSWORD_RESET_LINK_INVALID', + help: 'The URL to the custom page for password reset -> link invalid.', + }, + passwordResetSuccess: { + env: 'PARSE_SERVER_PAGES_CUSTOM_URL_PASSWORD_RESET_SUCCESS', + help: 'The URL to the custom page for password reset -> success.', + }, }; module.exports.CustomPagesOptions = { - "choosePassword": { - "env": "PARSE_SERVER_CUSTOM_PAGES_CHOOSE_PASSWORD", - "help": "choose password page path" - }, - "expiredVerificationLink": { - "env": "PARSE_SERVER_CUSTOM_PAGES_EXPIRED_VERIFICATION_LINK", - "help": "expired verification link page path" - }, - "invalidLink": { - "env": "PARSE_SERVER_CUSTOM_PAGES_INVALID_LINK", - "help": "invalid link page path" - }, - "invalidPasswordResetLink": { - "env": "PARSE_SERVER_CUSTOM_PAGES_INVALID_PASSWORD_RESET_LINK", - "help": "invalid password reset link page path" - }, - "invalidVerificationLink": { - "env": "PARSE_SERVER_CUSTOM_PAGES_INVALID_VERIFICATION_LINK", - "help": "invalid verification link page path" - }, - "linkSendFail": { - "env": "PARSE_SERVER_CUSTOM_PAGES_LINK_SEND_FAIL", - "help": "verification link send fail page path" - }, - "linkSendSuccess": { - "env": "PARSE_SERVER_CUSTOM_PAGES_LINK_SEND_SUCCESS", - "help": "verification link send success page path" - }, - "parseFrameURL": { - "env": "PARSE_SERVER_CUSTOM_PAGES_PARSE_FRAME_URL", - "help": "for masking user-facing pages" - }, - "passwordResetSuccess": { - "env": "PARSE_SERVER_CUSTOM_PAGES_PASSWORD_RESET_SUCCESS", - "help": "password reset success page path" - }, - "verifyEmailSuccess": { - "env": "PARSE_SERVER_CUSTOM_PAGES_VERIFY_EMAIL_SUCCESS", - "help": "verify email success page path" - } + choosePassword: { + env: 'PARSE_SERVER_CUSTOM_PAGES_CHOOSE_PASSWORD', + help: 'choose password page path', + }, + expiredVerificationLink: { + env: 'PARSE_SERVER_CUSTOM_PAGES_EXPIRED_VERIFICATION_LINK', + help: 'expired verification link page path', + }, + invalidLink: { + env: 'PARSE_SERVER_CUSTOM_PAGES_INVALID_LINK', + help: 'invalid link page path', + }, + invalidPasswordResetLink: { + env: 'PARSE_SERVER_CUSTOM_PAGES_INVALID_PASSWORD_RESET_LINK', + help: 'invalid password reset link page path', + }, + invalidVerificationLink: { + env: 'PARSE_SERVER_CUSTOM_PAGES_INVALID_VERIFICATION_LINK', + help: 'invalid verification link page path', + }, + linkSendFail: { + env: 'PARSE_SERVER_CUSTOM_PAGES_LINK_SEND_FAIL', + help: 'verification link send fail page path', + }, + linkSendSuccess: { + env: 'PARSE_SERVER_CUSTOM_PAGES_LINK_SEND_SUCCESS', + help: 'verification link send success page path', + }, + parseFrameURL: { + env: 'PARSE_SERVER_CUSTOM_PAGES_PARSE_FRAME_URL', + help: 'for masking user-facing pages', + }, + passwordResetSuccess: { + env: 'PARSE_SERVER_CUSTOM_PAGES_PASSWORD_RESET_SUCCESS', + help: 'password reset success page path', + }, + verifyEmailSuccess: { + env: 'PARSE_SERVER_CUSTOM_PAGES_VERIFY_EMAIL_SUCCESS', + help: 'verify email success page path', + }, }; module.exports.LiveQueryOptions = { - "classNames": { - "env": "PARSE_SERVER_LIVEQUERY_CLASSNAMES", - "help": "parse-server's LiveQuery classNames", - "action": parsers.arrayParser - }, - "pubSubAdapter": { - "env": "PARSE_SERVER_LIVEQUERY_PUB_SUB_ADAPTER", - "help": "LiveQuery pubsub adapter", - "action": parsers.moduleOrObjectParser - }, - "redisOptions": { - "env": "PARSE_SERVER_LIVEQUERY_REDIS_OPTIONS", - "help": "parse-server's LiveQuery redisOptions", - "action": parsers.objectParser - }, - "redisURL": { - "env": "PARSE_SERVER_LIVEQUERY_REDIS_URL", - "help": "parse-server's LiveQuery redisURL" - }, - "wssAdapter": { - "env": "PARSE_SERVER_LIVEQUERY_WSS_ADAPTER", - "help": "Adapter module for the WebSocketServer", - "action": parsers.moduleOrObjectParser - } + classNames: { + env: 'PARSE_SERVER_LIVEQUERY_CLASSNAMES', + help: "parse-server's LiveQuery classNames", + action: parsers.arrayParser, + }, + pubSubAdapter: { + env: 'PARSE_SERVER_LIVEQUERY_PUB_SUB_ADAPTER', + help: 'LiveQuery pubsub adapter', + action: parsers.moduleOrObjectParser, + }, + redisOptions: { + env: 'PARSE_SERVER_LIVEQUERY_REDIS_OPTIONS', + help: "parse-server's LiveQuery redisOptions", + action: parsers.objectParser, + }, + redisURL: { + env: 'PARSE_SERVER_LIVEQUERY_REDIS_URL', + help: "parse-server's LiveQuery redisURL", + }, + wssAdapter: { + env: 'PARSE_SERVER_LIVEQUERY_WSS_ADAPTER', + help: 'Adapter module for the WebSocketServer', + action: parsers.moduleOrObjectParser, + }, }; module.exports.LiveQueryServerOptions = { - "appId": { - "env": "PARSE_LIVE_QUERY_SERVER_APP_ID", - "help": "This string should match the appId in use by your Parse Server. If you deploy the LiveQuery server alongside Parse Server, the LiveQuery server will try to use the same appId." - }, - "cacheTimeout": { - "env": "PARSE_LIVE_QUERY_SERVER_CACHE_TIMEOUT", - "help": "Number in milliseconds. When clients provide the sessionToken to the LiveQuery server, the LiveQuery server will try to fetch its ParseUser's objectId from parse server and store it in the cache. The value defines the duration of the cache. Check the following Security section and our protocol specification for details, defaults to 5 * 1000 ms (5 seconds).", - "action": parsers.numberParser("cacheTimeout") - }, - "keyPairs": { - "env": "PARSE_LIVE_QUERY_SERVER_KEY_PAIRS", - "help": "A JSON object that serves as a whitelist of keys. It is used for validating clients when they try to connect to the LiveQuery server. Check the following Security section and our protocol specification for details.", - "action": parsers.objectParser - }, - "logLevel": { - "env": "PARSE_LIVE_QUERY_SERVER_LOG_LEVEL", - "help": "This string defines the log level of the LiveQuery server. We support VERBOSE, INFO, ERROR, NONE, defaults to INFO." - }, - "masterKey": { - "env": "PARSE_LIVE_QUERY_SERVER_MASTER_KEY", - "help": "This string should match the masterKey in use by your Parse Server. If you deploy the LiveQuery server alongside Parse Server, the LiveQuery server will try to use the same masterKey." - }, - "port": { - "env": "PARSE_LIVE_QUERY_SERVER_PORT", - "help": "The port to run the LiveQuery server, defaults to 1337.", - "action": parsers.numberParser("port"), - "default": 1337 - }, - "pubSubAdapter": { - "env": "PARSE_LIVE_QUERY_SERVER_PUB_SUB_ADAPTER", - "help": "LiveQuery pubsub adapter", - "action": parsers.moduleOrObjectParser - }, - "redisOptions": { - "env": "PARSE_LIVE_QUERY_SERVER_REDIS_OPTIONS", - "help": "parse-server's LiveQuery redisOptions", - "action": parsers.objectParser - }, - "redisURL": { - "env": "PARSE_LIVE_QUERY_SERVER_REDIS_URL", - "help": "parse-server's LiveQuery redisURL" - }, - "serverURL": { - "env": "PARSE_LIVE_QUERY_SERVER_SERVER_URL", - "help": "This string should match the serverURL in use by your Parse Server. If you deploy the LiveQuery server alongside Parse Server, the LiveQuery server will try to use the same serverURL." - }, - "websocketTimeout": { - "env": "PARSE_LIVE_QUERY_SERVER_WEBSOCKET_TIMEOUT", - "help": "Number of milliseconds between ping/pong frames. The WebSocket server sends ping/pong frames to the clients to keep the WebSocket alive. This value defines the interval of the ping/pong frame from the server to clients, defaults to 10 * 1000 ms (10 s).", - "action": parsers.numberParser("websocketTimeout") - }, - "wssAdapter": { - "env": "PARSE_LIVE_QUERY_SERVER_WSS_ADAPTER", - "help": "Adapter module for the WebSocketServer", - "action": parsers.moduleOrObjectParser - } + appId: { + env: 'PARSE_LIVE_QUERY_SERVER_APP_ID', + help: + 'This string should match the appId in use by your Parse Server. If you deploy the LiveQuery server alongside Parse Server, the LiveQuery server will try to use the same appId.', + }, + cacheTimeout: { + env: 'PARSE_LIVE_QUERY_SERVER_CACHE_TIMEOUT', + help: + "Number in milliseconds. When clients provide the sessionToken to the LiveQuery server, the LiveQuery server will try to fetch its ParseUser's objectId from parse server and store it in the cache. The value defines the duration of the cache. Check the following Security section and our protocol specification for details, defaults to 5 * 1000 ms (5 seconds).", + action: parsers.numberParser('cacheTimeout'), + }, + keyPairs: { + env: 'PARSE_LIVE_QUERY_SERVER_KEY_PAIRS', + help: + 'A JSON object that serves as a whitelist of keys. It is used for validating clients when they try to connect to the LiveQuery server. Check the following Security section and our protocol specification for details.', + action: parsers.objectParser, + }, + logLevel: { + env: 'PARSE_LIVE_QUERY_SERVER_LOG_LEVEL', + help: + 'This string defines the log level of the LiveQuery server. We support VERBOSE, INFO, ERROR, NONE, defaults to INFO.', + }, + masterKey: { + env: 'PARSE_LIVE_QUERY_SERVER_MASTER_KEY', + help: + 'This string should match the masterKey in use by your Parse Server. If you deploy the LiveQuery server alongside Parse Server, the LiveQuery server will try to use the same masterKey.', + }, + port: { + env: 'PARSE_LIVE_QUERY_SERVER_PORT', + help: 'The port to run the LiveQuery server, defaults to 1337.', + action: parsers.numberParser('port'), + default: 1337, + }, + pubSubAdapter: { + env: 'PARSE_LIVE_QUERY_SERVER_PUB_SUB_ADAPTER', + help: 'LiveQuery pubsub adapter', + action: parsers.moduleOrObjectParser, + }, + redisOptions: { + env: 'PARSE_LIVE_QUERY_SERVER_REDIS_OPTIONS', + help: "parse-server's LiveQuery redisOptions", + action: parsers.objectParser, + }, + redisURL: { + env: 'PARSE_LIVE_QUERY_SERVER_REDIS_URL', + help: "parse-server's LiveQuery redisURL", + }, + serverURL: { + env: 'PARSE_LIVE_QUERY_SERVER_SERVER_URL', + help: + 'This string should match the serverURL in use by your Parse Server. If you deploy the LiveQuery server alongside Parse Server, the LiveQuery server will try to use the same serverURL.', + }, + websocketTimeout: { + env: 'PARSE_LIVE_QUERY_SERVER_WEBSOCKET_TIMEOUT', + help: + 'Number of milliseconds between ping/pong frames. The WebSocket server sends ping/pong frames to the clients to keep the WebSocket alive. This value defines the interval of the ping/pong frame from the server to clients, defaults to 10 * 1000 ms (10 s).', + action: parsers.numberParser('websocketTimeout'), + }, + wssAdapter: { + env: 'PARSE_LIVE_QUERY_SERVER_WSS_ADAPTER', + help: 'Adapter module for the WebSocketServer', + action: parsers.moduleOrObjectParser, + }, }; module.exports.IdempotencyOptions = { - "paths": { - "env": "PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_PATHS", - "help": "An array of paths for which the feature should be enabled. The mount path must not be included, for example instead of `/parse/functions/myFunction` specifiy `functions/myFunction`. The entries are interpreted as regular expression, for example `functions/.*` matches all functions, `jobs/.*` matches all jobs, `classes/.*` matches all classes, `.*` matches all paths.", - "action": parsers.arrayParser, - "default": [] - }, - "ttl": { - "env": "PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_TTL", - "help": "The duration in seconds after which a request record is discarded from the database, defaults to 300s.", - "action": parsers.numberParser("ttl"), - "default": 300 - } + paths: { + env: 'PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_PATHS', + help: + 'An array of paths for which the feature should be enabled. The mount path must not be included, for example instead of `/parse/functions/myFunction` specifiy `functions/myFunction`. The entries are interpreted as regular expression, for example `functions/.*` matches all functions, `jobs/.*` matches all jobs, `classes/.*` matches all classes, `.*` matches all paths.', + action: parsers.arrayParser, + default: [], + }, + ttl: { + env: 'PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_TTL', + help: + 'The duration in seconds after which a request record is discarded from the database, defaults to 300s.', + action: parsers.numberParser('ttl'), + default: 300, + }, }; module.exports.AccountLockoutOptions = { - "duration": { - "env": "PARSE_SERVER_ACCOUNT_LOCKOUT_DURATION", - "help": "number of minutes that a locked-out account remains locked out before automatically becoming unlocked.", - "action": parsers.numberParser("duration") - }, - "threshold": { - "env": "PARSE_SERVER_ACCOUNT_LOCKOUT_THRESHOLD", - "help": "number of failed sign-in attempts that will cause a user account to be locked", - "action": parsers.numberParser("threshold") - } + duration: { + env: 'PARSE_SERVER_ACCOUNT_LOCKOUT_DURATION', + help: + 'number of minutes that a locked-out account remains locked out before automatically becoming unlocked.', + action: parsers.numberParser('duration'), + }, + threshold: { + env: 'PARSE_SERVER_ACCOUNT_LOCKOUT_THRESHOLD', + help: 'number of failed sign-in attempts that will cause a user account to be locked', + action: parsers.numberParser('threshold'), + }, }; module.exports.PasswordPolicyOptions = { - "doNotAllowUsername": { - "env": "PARSE_SERVER_PASSWORD_POLICY_DO_NOT_ALLOW_USERNAME", - "help": "disallow username in passwords", - "action": parsers.booleanParser - }, - "maxPasswordAge": { - "env": "PARSE_SERVER_PASSWORD_POLICY_MAX_PASSWORD_AGE", - "help": "days for password expiry", - "action": parsers.numberParser("maxPasswordAge") - }, - "maxPasswordHistory": { - "env": "PARSE_SERVER_PASSWORD_POLICY_MAX_PASSWORD_HISTORY", - "help": "setting to prevent reuse of previous n passwords", - "action": parsers.numberParser("maxPasswordHistory") - }, - "resetTokenReuseIfValid": { - "env": "PARSE_SERVER_PASSWORD_POLICY_RESET_TOKEN_REUSE_IF_VALID", - "help": "resend token if it's still valid", - "action": parsers.booleanParser - }, - "resetTokenValidityDuration": { - "env": "PARSE_SERVER_PASSWORD_POLICY_RESET_TOKEN_VALIDITY_DURATION", - "help": "time for token to expire", - "action": parsers.numberParser("resetTokenValidityDuration") - }, - "validatorCallback": { - "env": "PARSE_SERVER_PASSWORD_POLICY_VALIDATOR_CALLBACK", - "help": "a callback function to be invoked to validate the password" - }, - "validatorPattern": { - "env": "PARSE_SERVER_PASSWORD_POLICY_VALIDATOR_PATTERN", - "help": "a RegExp object or a regex string representing the pattern to enforce" - } + doNotAllowUsername: { + env: 'PARSE_SERVER_PASSWORD_POLICY_DO_NOT_ALLOW_USERNAME', + help: 'disallow username in passwords', + action: parsers.booleanParser, + }, + maxPasswordAge: { + env: 'PARSE_SERVER_PASSWORD_POLICY_MAX_PASSWORD_AGE', + help: 'days for password expiry', + action: parsers.numberParser('maxPasswordAge'), + }, + maxPasswordHistory: { + env: 'PARSE_SERVER_PASSWORD_POLICY_MAX_PASSWORD_HISTORY', + help: 'setting to prevent reuse of previous n passwords', + action: parsers.numberParser('maxPasswordHistory'), + }, + resetTokenReuseIfValid: { + env: 'PARSE_SERVER_PASSWORD_POLICY_RESET_TOKEN_REUSE_IF_VALID', + help: "resend token if it's still valid", + action: parsers.booleanParser, + }, + resetTokenValidityDuration: { + env: 'PARSE_SERVER_PASSWORD_POLICY_RESET_TOKEN_VALIDITY_DURATION', + help: 'time for token to expire', + action: parsers.numberParser('resetTokenValidityDuration'), + }, + validatorCallback: { + env: 'PARSE_SERVER_PASSWORD_POLICY_VALIDATOR_CALLBACK', + help: 'a callback function to be invoked to validate the password', + }, + validatorPattern: { + env: 'PARSE_SERVER_PASSWORD_POLICY_VALIDATOR_PATTERN', + help: 'a RegExp object or a regex string representing the pattern to enforce', + }, }; module.exports.FileUploadOptions = { - "enableForAnonymousUser": { - "env": "PARSE_SERVER_FILE_UPLOAD_ENABLE_FOR_ANONYMOUS_USER", - "help": "Is true if file upload should be allowed for anonymous users.", - "action": parsers.booleanParser, - "default": false - }, - "enableForAuthenticatedUser": { - "env": "PARSE_SERVER_FILE_UPLOAD_ENABLE_FOR_AUTHENTICATED_USER", - "help": "Is true if file upload should be allowed for authenticated users.", - "action": parsers.booleanParser, - "default": true - }, - "enableForPublic": { - "env": "PARSE_SERVER_FILE_UPLOAD_ENABLE_FOR_PUBLIC", - "help": "Is true if file upload should be allowed for anyone, regardless of user authentication.", - "action": parsers.booleanParser, - "default": false - } + enableForAnonymousUser: { + env: 'PARSE_SERVER_FILE_UPLOAD_ENABLE_FOR_ANONYMOUS_USER', + help: 'Is true if file upload should be allowed for anonymous users.', + action: parsers.booleanParser, + default: false, + }, + enableForAuthenticatedUser: { + env: 'PARSE_SERVER_FILE_UPLOAD_ENABLE_FOR_AUTHENTICATED_USER', + help: 'Is true if file upload should be allowed for authenticated users.', + action: parsers.booleanParser, + default: true, + }, + enableForPublic: { + env: 'PARSE_SERVER_FILE_UPLOAD_ENABLE_FOR_PUBLIC', + help: 'Is true if file upload should be allowed for anyone, regardless of user authentication.', + action: parsers.booleanParser, + default: false, + }, }; diff --git a/src/Options/docs.js b/src/Options/docs.js index cece87c084..b781f3cae7 100644 --- a/src/Options/docs.js +++ b/src/Options/docs.js @@ -86,6 +86,8 @@ * @property {Boolean} enableLocalization Is true if pages should be localized; this has no effect on custom page redirects. * @property {Boolean} enableRouter Is true if the pages router should be enabled; this is required for any of the pages options to take effect. Caution, this is an experimental feature that may not be appropriate for production. * @property {Boolean} forceRedirect Is true if responses should always be redirects and never content, false if the response type should depend on the request type (GET request -> content response; POST request -> redirect response). + * @property {String} localizationFallbackLocale The fallback locale for localization if no matching translation is provided for the given locale. This is only relevant when providing translation resources via JSON file. + * @property {String} localizationJsonPath The path to the JSON file for localization; the translations will be used to fill template placeholders according to the locale. * @property {String} pagesEndpoint The API endoint for the pages. Default is the 'apps'. * @property {String} pagesPath The path to the pages directory; this also defines where the static endpoint '/apps' points to. Default is the './public/' directory. */ @@ -170,4 +172,3 @@ * @property {Boolean} enableForAuthenticatedUser Is true if file upload should be allowed for authenticated users. * @property {Boolean} enableForPublic Is true if file upload should be allowed for anyone, regardless of user authentication. */ - diff --git a/src/Options/index.js b/src/Options/index.js index a0cb3f8082..801c15c21d 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -236,6 +236,11 @@ export interface PagesOptions { /* Is true if pages should be localized; this has no effect on custom page redirects. :DEFAULT: false */ enableLocalization: ?boolean; + /* The path to the JSON file for localization; the translations will be used to fill template placeholders according to the locale. */ + localizationJsonPath: ?string; + /* The fallback locale for localization if no matching translation is provided for the given locale. This is only relevant when providing translation resources via JSON file. + :DEFAULT: en */ + localizationFallbackLocale: ?string; /* Is true if responses should always be redirects and never content, false if the response type should depend on the request type (GET request -> content response; POST request -> redirect response). :DEFAULT: false */ forceRedirect: ?boolean; @@ -247,7 +252,7 @@ export interface PagesOptions { pagesEndpoint: ?string; /* The URLs to the custom pages. :DEFAULT: {} */ - customUrls: ?PagesCustomUrlsOptions + customUrls: ?PagesCustomUrlsOptions; } export interface PagesCustomUrlsOptions { diff --git a/src/ParseServer.js b/src/ParseServer.js index a79baad163..a81e97fc60 100644 --- a/src/ParseServer.js +++ b/src/ParseServer.js @@ -156,9 +156,12 @@ class ParseServer { }); }); - api.use('/', bodyParser.urlencoded({ extended: false }), pages.enableRouter - ? new PagesRouter(pages).expressRouter() - : new PublicAPIRouter().expressRouter() + api.use( + '/', + bodyParser.urlencoded({ extended: false }), + pages.enableRouter + ? new PagesRouter(pages).expressRouter() + : new PublicAPIRouter().expressRouter() ); api.use(bodyParser.json({ type: '*/*', limit: maxUploadSize })); diff --git a/src/Routers/PagesRouter.js b/src/Routers/PagesRouter.js index 6c2a8b59ff..f181854baa 100644 --- a/src/Routers/PagesRouter.js +++ b/src/Routers/PagesRouter.js @@ -19,6 +19,7 @@ const pages = Object.freeze({ emailVerificationLinkInvalid: new Page({ id: 'emailVerificationLinkInvalid', defaultFile: 'email_verification_link_invalid.html' }), emailVerificationLinkExpired: new Page({ id: 'emailVerificationLinkExpired', defaultFile: 'email_verification_link_expired.html' }), }); + // All page parameters for reference to be used as template placeholders or query params const pageParams = Object.freeze({ appName: 'appName', @@ -29,9 +30,16 @@ const pageParams = Object.freeze({ locale: 'locale', publicServerUrl: 'publicServerUrl', }); + // The header prefix to add page params as response headers const pageParamHeaderPrefix = 'x-parse-page-param-'; +// The errors being thrown +const errors = Object.freeze({ + jsonFailedFileLoading: "failed to load JSON file", + fileOutsideAllowedScope: "not allowed to read file outside of pages directory" +}); + export class PagesRouter extends PromiseRouter { /** * Constructs a PagesRouter. @@ -40,12 +48,15 @@ export class PagesRouter extends PromiseRouter { constructor(pages = {}) { super(); + // Set instance properties + this.pagesConfig = pages; this.pagesEndpoint = pages.pagesEndpoint ? pages.pagesEndpoint : 'apps'; this.pagesPath = pages.pagesPath ? path.resolve('./', pages.pagesPath) : path.resolve(__dirname, '../../public'); + this.loadJsonResource(); this.mountPagesRoutes(); } @@ -232,7 +243,7 @@ export class PagesRouter extends PromiseRouter { * should depend on the request type by default: * - GET request -> content response * - POST request -> redirect response (PRG pattern) - * @returns {Promise} The express response. + * @returns {Promise} The PromiseRouter response. */ goToPage(req, page, params = {}, responseType) { const config = req.config; @@ -245,11 +256,7 @@ export class PagesRouter extends PromiseRouter { : req.method == 'POST'; // Include default parameters - const defaultParams = { - [pageParams.appId]: config.appId, - [pageParams.appName]: config.appName, - [pageParams.publicServerUrl]: config.publicServerURL, - }; + const defaultParams = this.getDefaultParams(config); if (Object.values(defaultParams).includes(undefined)) { return this.notFound(); } @@ -258,11 +265,7 @@ export class PagesRouter extends PromiseRouter { // Add locale to params to ensure it is passed on with every request; // that means, once a locale is set, it is passed on to any follow-up page, // e.g. request_password_reset -> password_reset -> passwort_reset_success - const locale = - (req.query || {})[pageParams.locale] - || (req.body || {})[pageParams.locale] - || (req.params || {})[pageParams.locale] - || (req.headers || {})[pageParamHeaderPrefix + pageParams.locale]; + const locale = this.getLocale(req); params[pageParams.locale] = locale; // Compose paths and URLs @@ -290,28 +293,120 @@ export class PagesRouter extends PromiseRouter { } } + /** + * Serves a request to a static resource and localizes the resource if it + * is a HTML file. + * @param {Object} req The request object. + * @returns {Promise} The response. + */ + staticRoute(req) { + // Get requested path + const relativePath = req.params[0]; + + // Resolve requested path to absolute path + const absolutePath = path.resolve(this.pagesPath, relativePath); + + // If the requested file is not a HTML file send its raw content + if (!absolutePath || !absolutePath.endsWith('.html')) { + return this.fileResponse(absolutePath); + } + + // Get default parameters + const params = this.getDefaultParams(req.config); + + // Get locale + const locale = this.getLocale(req); + let placeholders = {}; + + // If localization is enabled and there is JSON resource is set + if (this.pagesConfig.enableLocalization && this.pagesConfig.localizationJsonPath) { + // Get JSON placeholders for locale + placeholders = this.getJsonPlaceholders(locale); + + // Fill any placeholders within the localized params; + // this allows a translation string to contain default + // placeholders like {{appName}} which are filled here + placeholders = JSON.stringify(placeholders); + placeholders = mustache.render(placeholders, params); + placeholders = JSON.parse(placeholders); + } + + return this.pageResponse(absolutePath, params, placeholders); + } + + /** + * Returns a translation from the JSON resource for a given locale. The JSON + * resource is parsed according to i18next syntax. + * + * Example JSON content: + * ```js + * { + * "en": { // resource for language `en` (English) + * "translation": { + * "greeting": "Hello!" + * } + * }, + * "de": { // resource for language `de` (German) + * "translation": { + * "greeting": "Hallo!" + * } + * } + * "de-CH": { // resource for locale `de-CH` (Swiss German) + * "translation": { + * "greeting": "Grüezi!" + * } + * } + * } + * ``` + * @param {String} locale The locale to translate to. + * @returns {Object} The translation keys or an empty object if no matching + * translation was found. + */ + getJsonPlaceholders(locale) { + + // If there is no JSON resource + if (this.jsonParameters === undefined) { + return {}; + } + + // If locale is not set use the fallback locale + locale = locale || this.pagesConfig.localizationFallbackLocale; + + // Get matching translation by locale, language or fallback locale + const language = locale.split("-")[0]; + const resource = this.jsonParameters[locale] + || this.jsonParameters[language] + || this.jsonParameters[this.pagesConfig.localizationFallbackLocale] + || {}; + const translation = resource.translation || {}; + return translation; + } + /** * Creates a response with file content. * @param {String} path The path of the file to return. - * @param {Object} placeholders The placeholders to fill in the - * content. + * @param {Object} [params={}] The parameters to be included in the response + * header. These will also be used to fill placeholders. + * @param {Object} [placeholders={}] The placeholders to fill in the content. + * These will not be included in the response header. * @returns {Object} The Promise Router response. */ - async pageResponse(path, placeholders) { + async pageResponse(path, params = {}, placeholders = {}) { // Get file content let data; try { - data = await fs.readFile(path, 'utf-8'); + data = await this.readFile(path); } catch (e) { return this.notFound(); } // Fill placeholders - data = mustache.render(data, placeholders); + const allPlaceholders = Object.assign({}, params, placeholders); + data = mustache.render(data, allPlaceholders); // Add placeholers in header to allow parsing for programmatic use // of response, instead of having to parse the HTML content. - const headers = Object.entries(placeholders).reduce((m, p) => { + const headers = Object.entries(params).reduce((m, p) => { if (p[1] !== undefined) { m[`${pageParamHeaderPrefix}${p[0].toLowerCase()}`] = p[1]; } @@ -321,6 +416,96 @@ export class PagesRouter extends PromiseRouter { return { text: data, headers: headers }; } + /** + * Creates a response with file content. + * @param {String} path The path of the file to return. + * @returns {Object} The PromiseRouter response. + */ + async fileResponse(path) { + // Get file content + let data; + try { + data = await this.readFile(path); + } catch (e) { + return this.notFound(); + } + + return { text: data }; + } + + /** + * Reads and returns the contet of a file at a given path. File reading to + * serve content on the static route is only allowed from the pages + * directory on downwards. + * ----------------------------------------------------------------------- + * **WARNING:** All file reads in the PagesRouter must be executed by this + * wrapper because it also detects and prevents common exploits. + * ----------------------------------------------------------------------- + * @param {String} filePath The path to the file to read. + * @returns {Promise} The file content. + */ + async readFile(filePath) { + + // Normalize path to prevent it from containing any directory changing + // UNIX patterns which could expose the whole file system, e.g. + // `http://example.com/parse/apps/../file.txt` requests a file outside + // of the pages directory scope. + const normalizedPath = path.normalize(filePath); + + // Abort if the path is outside of the path directory scope + if (!normalizedPath.startsWith(this.pagesPath)) { + throw(errors.fileOutsideAllowedScope); + } + + return await fs.readFile(normalizedPath, 'utf-8'); + } + + /** + * Loads a language resource JSON file that is used for translations. + */ + loadJsonResource() { + if (this.pagesConfig.localizationJsonPath === undefined) { + return; + } + try { + const json = require(path.resolve('./', this.pagesConfig.localizationJsonPath)); + this.jsonParameters = json; + } catch (e) { + throw(errors.jsonFailedFileLoading); + } + } + + /** + * Extracts and returns the page default parameters from the Parse Server + * configuration. These parameters are made accessible in every page served + * by this router. + * @param {Object} config The Parse Server configuration. + * @returns {Object} The default parameters. + */ + getDefaultParams(config) { + return config + ? { + [pageParams.appId]: config.appId, + [pageParams.appName]: config.appName, + [pageParams.publicServerUrl]: config.publicServerURL, + } + : {}; + } + + /** + * Extracts and returns the locale from an express request. + * @param {Object} req The express request. + * @returns {String|undefined} The locale, or undefined if no locale was set. + */ + getLocale(req) { + const locale = + (req.query || {})[pageParams.locale] + || (req.body || {})[pageParams.locale] + || (req.params || {})[pageParams.locale] + || (req.headers || {})[pageParamHeaderPrefix + pageParams.locale]; + return locale; + } + /** * Creates a response with http rediret. * @param {Object} req The express request. @@ -385,9 +570,16 @@ export class PagesRouter extends PromiseRouter { throw error; } - setConfig(req) { + /** + * Sets the Parse Server configuration in the request object to make it + * easily accessible throughtout request processing. + * @param {Object} req The request. + * @param {Boolean} failGracefully Is true if failing to set the config should + * not result in an invalid request response. Default is `false`. + */ + setConfig(req, failGracefully = false) { req.config = Config.get(req.params.appId || req.query.appId); - if (!req.config) { + if (!req.config && !failGracefully) { this.invalidRequest(); } return Promise.resolve(); @@ -448,11 +640,21 @@ export class PagesRouter extends PromiseRouter { return this.requestResetPassword(req); } ); + + this.route( + 'GET', + `/${this.pagesEndpoint}/(*)?`, + req => { + this.setConfig(req, true); + }, + req => { + return this.staticRoute(req); + } + ); } expressRouter() { const router = express.Router(); - router.use(`/${this.pagesEndpoint}`, express.static(this.pagesPath)); router.use('/', super.expressRouter()); return router; } @@ -461,6 +663,7 @@ export class PagesRouter extends PromiseRouter { export default PagesRouter; module.exports = { PagesRouter, + pageParamHeaderPrefix, pageParams, pages, }; diff --git a/src/Utils.js b/src/Utils.js index 89753f5cac..f4912e3736 100644 --- a/src/Utils.js +++ b/src/Utils.js @@ -11,7 +11,6 @@ const fs = require('fs').promises; * The general purpose utilities. */ class Utils { - /** * @function getLocalizedPath * @description Returns a localized file path accoring to the locale. @@ -41,28 +40,33 @@ class Utils { * there is no matching localized file. */ static async getLocalizedPath(defaultPath, locale) { - // Get file name and paths const file = path.basename(defaultPath); const basePath = path.dirname(defaultPath); // If locale is not set return default file - if (!locale) { return { path: defaultPath }; } + if (!locale) { + return { path: defaultPath }; + } // Check file for locale exists const localePath = path.join(basePath, locale, file); const localeFileExists = await Utils.fileExists(localePath); // If file for locale exists return file - if (localeFileExists) { return { path: localePath, subdir: locale }; } + if (localeFileExists) { + return { path: localePath, subdir: locale }; + } // Check file for language exists - const language = locale.split("-")[0]; + const language = locale.split('-')[0]; const languagePath = path.join(basePath, language, file); const languageFileExists = await Utils.fileExists(languagePath); // If file for language exists return file - if (languageFileExists) { return { path: languagePath, subdir: language }; } + if (languageFileExists) { + return { path: languagePath, subdir: language }; + } // Return default file return { path: defaultPath }; @@ -92,6 +96,28 @@ class Utils { static isPath(s) { return /(^\/)|(^\.\/)|(^\.\.\/)/.test(s); } + + /** + * Flattens an object and crates new keys with custom delimiters. + * @param {Object} obj The object to flatten. + * @param {String} [delimiter='.'] The delimiter of the newly generated keys. + * @param {Object} result + * @returns {Object} The flattened object. + **/ + static flattenObject(obj, parentKey, delimiter = '.', result = {}) { + for (const key in obj) { + if (Object.prototype.hasOwnProperty.call(obj, key)) { + const newKey = parentKey ? parentKey + delimiter + key : key; + + if (typeof obj[key] === 'object' && obj[key] !== null) { + this.flattenObject(obj[key], newKey, delimiter, result); + } else { + result[newKey] = obj[key]; + } + } + } + return result; + } } module.exports = Utils; From 03bba6a771a848710c6a3a4f483db7c243e4739d Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Sun, 31 Jan 2021 23:19:47 +0100 Subject: [PATCH 36/48] added JSON localization to feature pages (password reset, email verification) --- spec/PagesRouter.spec.js | 10 +++---- src/Routers/PagesRouter.js | 56 +++++++++++++++++++++++++------------- 2 files changed, 42 insertions(+), 24 deletions(-) diff --git a/spec/PagesRouter.spec.js b/spec/PagesRouter.spec.js index c2e1b85498..a561d8de8a 100644 --- a/spec/PagesRouter.spec.js +++ b/spec/PagesRouter.spec.js @@ -412,7 +412,7 @@ describe('Pages Router', () => { const response = await request({ url: jsonPageUrl.toString(), followRedirects: false, - }); + }).catch(e => e); expect(response.status).toBe(200); expect(pageResponse.calls.all()[0].args[1]).toEqual({}); expect(pageResponse.calls.all()[0].args[2]).toEqual({}); @@ -440,7 +440,7 @@ describe('Pages Router', () => { const response = await request({ url: jsonPageUrl.toString(), followRedirects: false, - }); + }).catch(e => e); expect(response.status).toBe(200); // Ensure page response contains translation of fallback locale @@ -463,7 +463,7 @@ describe('Pages Router', () => { const response = await request({ url: jsonPageUrl.toString(), followRedirects: false, - }); + }).catch(e => e); expect(response.status).toBe(200); // Ensure page response contains translations of request locale @@ -487,7 +487,7 @@ describe('Pages Router', () => { const response = await request({ url: jsonPageUrl.toString(), followRedirects: false, - }); + }).catch(e => e); expect(response.status).toBe(200); // Ensure page response contains translations of requst language @@ -513,7 +513,7 @@ describe('Pages Router', () => { const response = await request({ url: jsonPageUrl.toString(), followRedirects: false, - }); + }).catch(e => e); expect(response.status).toBe(200); // Fill placeholders in transation diff --git a/src/Routers/PagesRouter.js b/src/Routers/PagesRouter.js index f181854baa..031f535904 100644 --- a/src/Routers/PagesRouter.js +++ b/src/Routers/PagesRouter.js @@ -279,17 +279,20 @@ export class PagesRouter extends PromiseRouter { return this.redirectResponse(customUrl, params); } + // Get JSON placeholders + const placeholders = this.getJsonPlaceholders(locale, params); + // If localization is enabled if (config.pages.enableLocalization && locale) { return Utils.getLocalizedPath(defaultPath, locale).then(({ path, subdir }) => redirect ? this.redirectResponse(this.composePageUrl(defaultFile, config.publicServerURL, subdir), params) - : this.pageResponse(path, params) + : this.pageResponse(path, params, placeholders) ); } else { return redirect ? this.redirectResponse(defaultUrl, params) - : this.pageResponse(defaultPath, params); + : this.pageResponse(defaultPath, params, placeholders); } } @@ -314,22 +317,9 @@ export class PagesRouter extends PromiseRouter { // Get default parameters const params = this.getDefaultParams(req.config); - // Get locale + // Get JSON placeholders const locale = this.getLocale(req); - let placeholders = {}; - - // If localization is enabled and there is JSON resource is set - if (this.pagesConfig.enableLocalization && this.pagesConfig.localizationJsonPath) { - // Get JSON placeholders for locale - placeholders = this.getJsonPlaceholders(locale); - - // Fill any placeholders within the localized params; - // this allows a translation string to contain default - // placeholders like {{appName}} which are filled here - placeholders = JSON.stringify(placeholders); - placeholders = mustache.render(placeholders, params); - placeholders = JSON.parse(placeholders); - } + const placeholders = this.getJsonPlaceholders(locale, params); return this.pageResponse(absolutePath, params, placeholders); } @@ -359,10 +349,10 @@ export class PagesRouter extends PromiseRouter { * } * ``` * @param {String} locale The locale to translate to. - * @returns {Object} The translation keys or an empty object if no matching + * @returns {Object} The translation or an empty object if no matching * translation was found. */ - getJsonPlaceholders(locale) { + getJsonTranslation(locale) { // If there is no JSON resource if (this.jsonParameters === undefined) { @@ -382,6 +372,34 @@ export class PagesRouter extends PromiseRouter { return translation; } + /** + * Returns a translation from the JSON resource for a given locale with + * placeholders filled in by given parameters. + * @param {String} locale The locale to translate to. + * @param {Object} params The parameters to fill into any placeholders + * within the translations. + * @returns {Object} The translation or an empty object if no matching + * translation was found. + */ + getJsonPlaceholders(locale, params = {}) { + + // If localization is disabled or there is no JSON resource + if (!this.pagesConfig.enableLocalization || !this.pagesConfig.localizationJsonPath) { + return {}; + } + + // Get JSON placeholders + let placeholders = this.getJsonTranslation(locale); + + // Fill in any placeholders in the translation; this allows a translation + // to contain default placeholders like {{appName}} which are filled here + placeholders = JSON.stringify(placeholders); + placeholders = mustache.render(placeholders, params); + placeholders = JSON.parse(placeholders); + + return placeholders; + } + /** * Creates a response with file content. * @param {String} path The path of the file to return. From a54c4c7aed7de603d05e4ed8cf4b3142cc96dd41 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Sun, 31 Jan 2021 23:19:54 +0100 Subject: [PATCH 37/48] updated readme --- README.md | 160 +++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 117 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index 3e9ee52592..cedd12db15 100644 --- a/README.md +++ b/README.md @@ -52,17 +52,21 @@ The full documentation for Parse Server is available in the [wiki](https://githu - [Running Parse Server elsewhere](#running-parse-server-elsewhere) - [Sample Application](#sample-application) - [Parse Server + Express](#parse-server--express) - - [Configuration](#configuration) - - [Basic Options](#basic-options) - - [Client Key Options](#client-key-options) - - [Email Verification and Password Reset](#email-verification-and-password-reset) - - [Custom Pages](#custom-pages) - - [Using Environment Variables](#using-environment-variables) - - [Available Adapters](#available-adapters) - - [Configuring File Adapters](#configuring-file-adapters) - - [Idempodency Enforcement](#idempodency-enforcement) - - [Localization](#localization) - - [Logging](#logging) +- [Configuration](#configuration) + - [Basic Options](#basic-options) + - [Client Key Options](#client-key-options) + - [Email Verification and Password Reset](#email-verification-and-password-reset) + - [Custom Pages](#custom-pages) + - [Using Environment Variables](#using-environment-variables) + - [Available Adapters](#available-adapters) + - [Configuring File Adapters](#configuring-file-adapters) + - [Idempodency Enforcement](#idempodency-enforcement) + - [Localization](#localization) + - [Pages](#pages) + - [Localization with Directory Structure](#localization-with-directory-structure) + - [Localization with JSON Resource](#localization-with-json-resource) + - [Parameters](#parameters) + - [Logging](#logging) - [Live Query](#live-query) - [GraphQL](#graphql) - [Running](#running) @@ -243,13 +247,13 @@ app.listen(1337, function() { For a full list of available options, run `parse-server --help` or take a look at [Parse Server Configurations](http://parseplatform.org/parse-server/api/master/ParseServerOptions.html). -## Configuration +# Configuration Parse Server can be configured using the following options. You may pass these as parameters when running a standalone `parse-server`, or by loading a configuration file in JSON format using `parse-server path/to/configuration.json`. If you're using Parse Server on Express, you may also pass these to the `ParseServer` object as options. For the full list of available options, run `parse-server --help` or take a look at [Parse Server Configurations](http://parseplatform.org/parse-server/api/master/ParseServerOptions.html). -### Basic Options +## Basic Options * `appId` **(required)** - The application id to host with this server instance. You can use any arbitrary string. For migrated apps, this should match your hosted Parse app. * `masterKey` **(required)** - The master key to use for overriding ACL security. You can use any arbitrary string. Keep it secret! For migrated apps, this should match your hosted Parse app. @@ -259,7 +263,7 @@ For the full list of available options, run `parse-server --help` or take a look * `cloud` - The absolute path to your cloud code `main.js` file. * `push` - Configuration options for APNS and GCM push. See the [Push Notifications quick start](http://docs.parseplatform.org/parse-server/guide/#push-notifications_push-notifications-quick-start). -### Client Key Options +## Client Key Options The client keys used with Parse are no longer necessary with Parse Server. If you wish to still require them, perhaps to be able to refuse access to older clients, you can set the keys at initialization time. Setting any of these keys will require all requests to provide one of the configured keys. @@ -268,7 +272,7 @@ The client keys used with Parse are no longer necessary with Parse Server. If yo * `restAPIKey` * `dotNetKey` -### Email Verification and Password Reset +## Email Verification and Password Reset Verifying user email addresses and enabling password reset via email requires an email adapter. As part of the `parse-server` package we provide an adapter for sending email through Mailgun. To use it, sign up for Mailgun, and add this to your initialization code: @@ -348,7 +352,7 @@ You can also use other email adapters contributed by the community such as: - [simple-parse-smtp-adapter](https://www.npmjs.com/package/simple-parse-smtp-adapter) - [parse-server-generic-email-adapter](https://www.npmjs.com/package/parse-server-generic-email-adapter) -### Custom Pages +## Custom Pages It’s possible to change the default pages of the app and redirect the user to another path or domain. @@ -369,7 +373,7 @@ var server = ParseServer({ }) ``` -### Using Environment Variables +## Using Environment Variables You may configure the Parse Server using environment variables: @@ -390,7 +394,7 @@ $ PORT=8080 parse-server --appId APPLICATION_ID --masterKey MASTER_KEY For the full list of configurable environment variables, run `parse-server --help` or take a look at [Parse Server Configuration](https://github.com/parse-community/parse-server/blob/master/src/Options/Definitions.js). -### Available Adapters +## Available Adapters All official adapters are distributed as scoped pacakges on [npm (@parse)](https://www.npmjs.com/search?q=scope%3Aparse). @@ -398,7 +402,7 @@ Some well maintained adapters are also available on the [Parse Server Modules](h You can also find more adapters maintained by the community by searching on [npm](https://www.npmjs.com/search?q=parse-server%20adapter&page=1&ranking=optimal). -### Configuring File Adapters +## Configuring File Adapters Parse Server allows developers to choose from several options when hosting files: @@ -408,7 +412,7 @@ Parse Server allows developers to choose from several options when hosting files `GridFSBucketAdapter` is used by default and requires no setup, but if you're interested in using S3 or Google Cloud Storage, additional configuration information is available in the [Parse Server guide](http://docs.parseplatform.org/parse-server/guide/#configuring-file-adapters). -### Idempodency Enforcement +## Idempodency Enforcement **Caution, this is an experimental feature that may not be appropriate for production.** @@ -420,7 +424,7 @@ Identical requests are identified by their request header `X-Parse-Request-Id`. Deduplication is only done for object creation and update (`POST` and `PUT` requests). Deduplication is not done for object finding and deletion (`GET` and `DELETE` requests), as these operations are already idempotent by definition. -#### Configuration example +### Configuration example ``` let api = new ParseServer({ idempotencyOptions: { @@ -429,7 +433,7 @@ let api = new ParseServer({ } } ``` -#### Parameters +### Parameters | Parameter | Optional | Type | Default value | Example values | Environment variable | Description | |----------------------------|----------|-----------------|---------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| @@ -437,16 +441,16 @@ let api = new ParseServer({ | `idempotencyOptions.paths` | yes | `Array` | `[]` | `.*` (all paths, includes the examples below),
`functions/.*` (all functions),
`jobs/.*` (all jobs),
`classes/.*` (all classes),
`functions/.*` (all functions),
`users` (user creation / update),
`installations` (installation creation / update) | PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_PATHS | An array of path patterns that have to match the request path for request deduplication to be enabled. The mount path must not be included, for example to match the request path `/parse/functions/myFunction` specifiy the path pattern `functions/myFunction`. A trailing slash of the request path is ignored, for example the path pattern `functions/myFunction` matches both `/parse/functions/myFunction` and `/parse/functions/myFunction/`. | | `idempotencyOptions.ttl` | yes | `Integer` | `300` | `60` (60 seconds) | PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_TTL | The duration in seconds after which a request record is discarded from the database. Duplicate requests due to network issues can be expected to arrive within milliseconds up to several seconds. This value must be greater than `0`. | -#### Notes +### Notes - This feature is currently only available for MongoDB and not for Postgres. -### Localization +## Localization -#### Pages +### Pages **Caution, this is an experimental feature that may not be appropriate for production.** -Pagse for password reset and email verification can be localized with the `pages` option in the Parse Server configuration: +Custom pages as well as feature pages (e.g. password reset, email verification) can be localized with the `pages` option in the Parse Server configuration: ```js const api = new ParseServer({ @@ -459,7 +463,7 @@ const api = new ParseServer({ } ``` -Localzation is achieved by matching a request-supplied `locale` parameter with a localized page. The locale can be supplied in either the request query, body or header with the following key: +Localization is achieved by matching a request-supplied `locale` parameter with localized page content. The locale can be supplied in either the request query, body or header with the following keys: - query: `locale` - body: `locale` - header: `x-parse-page-param-locale` @@ -469,24 +473,33 @@ For example, a password reset link with the locale parameter in the query could http://example.com/parse/apps/[appId]/request_password_reset?token=[token]&username=[username]&locale=de-AT ``` -Localized pages are determined by the directory structure: -``` +- Localization is only available for pages in the pages directory as set with `pages.pagesPath`. +- Localization for feature pages (e.g. password reset, email verification) is disabled if `pages.customUrls` are set, even if the custom URLs point to the pages within the pages path. +- Only `.html` files are considered for localization when localizing custom pages. + +Pages can be localized in two ways: + +#### Localization with Directory Structure + +Pages are localized by using the corresponding file in the directory structure where the files are placed in subdirectories named after the locale or language. The file in the base directory is the default file. + +**Example Directory Structure:** +```js root/ -├── base/ // base path to files +├── public/ // pages base path │ ├── example.html // default file │ └── de/ // de language folder │ │ └── example.html // de localized file │ └── de-AT/ // de-AT locale folder │ │ └── example.html // de-AT localized file +``` Files are matched with the locale in the following order: 1. Locale match, e.g. locale `de-AT` matches file in folder `de-AT`. -2. Language match, e.g. locale `de-CH` matches file in folder `de`. -3. Default; file in base folder is returned. -``` +1. Language match, e.g. locale `de-CH` matches file in folder `de`. +1. Default; file in base folder is returned. -Localization is only enabled for the default pages in the `public` directory; localization is disabled if `customUrls` are set (even if the custom URLs point to the default pages). -##### Configuration example +**Configuration Example:** ```js const api = new ParseServer({ ...otherOptions, @@ -494,21 +507,83 @@ const api = new ParseServer({ pages: { enableRouter: true, // Enables the experimental feature; required for localization enableLocalization: true, - forceRedirect: false, - pagesPath: './public/pages', - pagesEndpoint: 'pages', customUrls: { passwordReset: 'https://example.com/page.html' } } } ``` -##### Parameters + +Pros: +- All files are complete in their content and can be easily opened and previewed by viewing the file in a browser. +Cons: +- In most cases, a localized page differs only slighly from the default page, which could cause a lot of duplicate code that is difficult to maintain. + +#### Localization with JSON Resource + +Pages are localized by adding placeholders in the HTML files and providing a JSON resource that contains the translations to fill into the placeholders. + +**Example Directory Structure:** +```js +root/ +├── public/ // pages base path +│ ├── example.html // the page containg placeholders +├── private/ // folder outside of public scope +│ └── translations.json // JSON resource file +``` + +The JSON resource file loosely follows the i18next syntax, which is a popular syntax that is often supported by translation platforms, making it easy to manage translations, exporting them for use in Parse Server, and even to automate this workflow. + +**Example JSON Content:** +```json +{ + "en": { // resource for language `en` (English) + "translation": { + "greeting": "Hello!" + } + }, + "de": { // resource for language `de` (German) + "translation": { + "greeting": "Hallo!" + } + } + "de-AT": { // resource for locale `de-AT` (Austrian German) + "translation": { + "greeting": "Servus!" + } + } +} +``` + +**Configuration Example:** +```js +const api = new ParseServer({ + ...otherOptions, + + pages: { + enableRouter: true, // Enables the experimental feature; required for localization + enableLocalization: true, + localizationJsonPath: './private/localization.json', + localizationFallbackLocale: 'en' + } +} +``` + +Pros: +- There is only one HTML file to maintain that contains the placeholders that are filled with the translations according to the locale. +Cons: +- Files cannot be easily previewed by viewing the file in a browser because the content contains only placeholders and even HTML or CSS changes may be dynamically applied, e.g. when a localization requires a Right-To-Left layout direction. +- Style and other fundamental layout changes may be more difficult to apply. +- +#### Parameters | Parameter | Optional | Type | Default value | Example values | Environment variable | Description | |-------------------------------------------------|----------|-----------|----------------------------------------|------------------------------------------------------|-----------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `pages` | yes | `Object` | `undefined` | - | `PARSE_SERVER_PAGES` | The options for pages such as password reset and email verification. | | `pages.enableRouter` | yes | `Boolean` | `false` | - | `PARSE_SERVER_PAGES_ENABLE_ROUTER` | Is `true` if the pages router should be enabled; this is required for any of the pages options to take effect. **Caution, this is an experimental feature that may not be appropriate for production.** | +| `pages.enableLocalization` | yes | `Boolean` | `false` | - | `PARSE_SERVER_PAGES_ENABLE_LOCALIZATION` | Is true if pages should be localized; this has no effect on custom page redirects. | +| `pages.localizationJsonPath` | yes | `String` | `undefined` | `./private/translations.json` | `PARSE_SERVER_PAGES_LOCALIZATION_JSON_PATH` | The path to the JSON file for localization; the translations will be used to fill template placeholders according to the locale. | +| `pages.localizationFallbackLocale` | yes | `String` | `en` | `en`, `en-GB`, `default` | `PARSE_SERVER_PAGES_LOCALIZATION_FALLBACK_LOCALE` | The fallback locale for localization if no matching translation is provided for the given locale. This is only relevant when providing translation resources via JSON file. | | `pages.forceRedirect` | yes | `Boolean` | `false` | - | `PARSE_SERVER_PAGES_FORCE_REDIRECT` | Is `true` if responses should always be redirects and never content, `false` if the response type should depend on the request type (`GET` request -> content response; `POST` request -> redirect response). | | `pages.pagesPath` | yes | `String` | `./public` | `./files/pages`, `../../pages` | `PARSE_SERVER_PAGES_PAGES_PATH` | The path to the pages directory; this also defines where the static endpoint `/apps` points to. | | `pages.pagesEndpoint` | yes | `String` | `apps` | - | `PARSE_SERVER_PAGES_PAGES_ENDPOINT` | The API endoint for the pages. | @@ -522,12 +597,11 @@ const api = new ParseServer({ | `pages.customUrls.emailVerificationLinkInvalid` | yes | `String` | `email_verification_link_invalid.html` | - | `PARSE_SERVER_PAGES_CUSTOM_URL_EMAIL_VERIFICATION_LINK_INVALID` | The URL to the custom page for email verification -> link invalid. | | `pages.customUrls.emailVerificationLinkExpired` | yes | `String` | `email_verification_link_expired.html` | - | `PARSE_SERVER_PAGES_CUSTOM_URL_EMAIL_VERIFICATION_LINK_EXPIRED` | The URL to the custom page for email verification -> link expired. | -##### Notes - -- In combination with the [Parse Server API Mail Adapter](https://www.npmjs.com/package/parse-server-api-mail-adapter) Parse Server provides a fully localized flow (emails, pages) for the user. The email adapter sends out a localized email and adds a locale parameter to the password reset / email verification link, which is then used to respond with localized pages. +### Notes +- In combination with the [Parse Server API Mail Adapter](https://www.npmjs.com/package/parse-server-api-mail-adapter) Parse Server provides a fully localized flow (emails -> pages) for the user. The email adapter sends a localized email and adds a locale parameter to the password reset or email verification link, which is then used to respond with localized pages. -### Logging +## Logging Parse Server will, by default, log: * to the console From 3d01cf117d9f9986f9a5fb356c215a02e0acb7b7 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Sun, 31 Jan 2021 23:26:55 +0100 Subject: [PATCH 38/48] updated readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cedd12db15..f084002450 100644 --- a/README.md +++ b/README.md @@ -532,7 +532,7 @@ root/ │ └── translations.json // JSON resource file ``` -The JSON resource file loosely follows the i18next syntax, which is a popular syntax that is often supported by translation platforms, making it easy to manage translations, exporting them for use in Parse Server, and even to automate this workflow. +The JSON resource file loosely follows the [i18next](https://github.com/i18next/i18next) syntax, which is a syntax that is often supported by translation platforms, making it easy to manage translations, exporting them for use in Parse Server, and even to automate this workflow. **Example JSON Content:** ```json From 19ed17bd4abcf4a0dc0c83f26c5f8770a2e3b9c4 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Mon, 1 Feb 2021 01:06:23 +0100 Subject: [PATCH 39/48] optimized JSON localization for feature pages; added e2e test case --- spec/PagesRouter.spec.js | 57 ++++++++++++++++++++++---------------- src/Page.js | 17 ++++++++++-- src/Routers/PagesRouter.js | 7 +++-- 3 files changed, 53 insertions(+), 28 deletions(-) diff --git a/spec/PagesRouter.spec.js b/spec/PagesRouter.spec.js index a561d8de8a..760f6374e4 100644 --- a/spec/PagesRouter.spec.js +++ b/spec/PagesRouter.spec.js @@ -4,6 +4,7 @@ const request = require('../lib/request'); const fs = require('fs').promises; const mustache = require('mustache'); const Utils = require('../lib/Utils'); +const { Page } = require('../lib/Page'); const Config = require('../lib/Config'); const Definitions = require('../lib/Options/Definitions'); const UserController = require('../lib/Controllers/UserController').UserController; @@ -401,6 +402,11 @@ describe('Pages Router', () => { jsonPageFile = 'custom_json.html'; jsonPageUrl = new URL(`${config.publicServerURL}/apps/${jsonPageFile}`); jsonResource = require('../public/custom_json.json'); + + config.pages.enableLocalization = true; + config.pages.localizationJsonPath = './public/custom_json.json'; + config.pages.localizationFallbackLocale = 'en'; + await reconfigureServer(config); }); it('does not localize with JSON resource if localization is disabled', async () => { @@ -431,12 +437,7 @@ describe('Pages Router', () => { } }); - it('localizes with JSON resource and fallback locale', async () => { - config.pages.enableLocalization = true; - config.pages.localizationJsonPath = './public/custom_json.json'; - config.pages.localizationFallbackLocale = 'en'; - await reconfigureServer(config); - + it('localizes static page with JSON resource and fallback locale', async () => { const response = await request({ url: jsonPageUrl.toString(), followRedirects: false, @@ -451,12 +452,7 @@ describe('Pages Router', () => { } }); - it('localizes with JSON resource and request locale', async () => { - config.pages.enableLocalization = true; - config.pages.localizationJsonPath = './public/custom_json.json'; - config.pages.localizationFallbackLocale = 'en'; - await reconfigureServer(config); - + it('localizes static page with JSON resource and request locale', async () => { // Add locale to request URL jsonPageUrl.searchParams.set('locale', exampleLocale); @@ -474,12 +470,7 @@ describe('Pages Router', () => { } }); - it('localizes with JSON resource and language matching request locale', async () => { - config.pages.enableLocalization = true; - config.pages.localizationJsonPath = './public/custom_json.json'; - config.pages.localizationFallbackLocale = 'en'; - await reconfigureServer(config); - + it('localizes static page with JSON resource and language matching request locale', async () => { // Add locale to request URL that has no locale match but only a language // match in the JSON resource jsonPageUrl.searchParams.set('locale', 'de-CH'); @@ -498,12 +489,7 @@ describe('Pages Router', () => { } }); - it('localizes with JSON resource and fills placeholders in JSON values', async () => { - config.pages.enableLocalization = true; - config.pages.localizationJsonPath = './public/custom_json.json'; - config.pages.localizationFallbackLocale = 'en'; - await reconfigureServer(config); - + it('localizes static page with JSON resource and fills placeholders in JSON values', async () => { // Add app ID to request URL so that the request is assigned to a Parse Server app // and placeholders within translations strings can be replaced with default page // parameters such as `appId` @@ -527,6 +513,29 @@ describe('Pages Router', () => { expect(response.text).toContain(value); } }); + + it('localizes feature page with JSON resource and fills placeholders in JSON values', async () => { + // Fake any page to load the JSON page file + spyOnProperty(Page.prototype, 'defaultFile').and.returnValue(jsonPageFile); + + const response = await request({ + url: + `http://localhost:8378/1/apps/test/request_password_reset?token=exampleToken&username=exampleUsername&locale=${exampleLocale}`, + followRedirects: false, + }).catch(e => e); + expect(response.status).toEqual(200); + + // Fill placeholders in transation + let translation = jsonResource[exampleLocale].translation; + translation = JSON.stringify(translation); + translation = mustache.render(translation, { appName: config.appName }); + translation = JSON.parse(translation); + + // Ensure page response contains translation of request locale + for (const value of Object.values(translation)) { + expect(response.text).toContain(value); + } + }); }); describe('response type', () => { diff --git a/src/Page.js b/src/Page.js index 006e72044b..27a5237197 100644 --- a/src/Page.js +++ b/src/Page.js @@ -15,8 +15,21 @@ export class Page { constructor(params = {}) { const { id, defaultFile } = params; - this.id = id; - this.defaultFile = defaultFile; + this._id = id; + this._defaultFile = defaultFile; + } + + get id() { + return this._id; + } + get defaultFile() { + return this._defaultFile; + } + set id(v) { + this._id = v; + } + set defaultFile(v) { + this._defaultFile = v; } } diff --git a/src/Routers/PagesRouter.js b/src/Routers/PagesRouter.js index 031f535904..f0c6a0dda4 100644 --- a/src/Routers/PagesRouter.js +++ b/src/Routers/PagesRouter.js @@ -280,9 +280,12 @@ export class PagesRouter extends PromiseRouter { } // Get JSON placeholders - const placeholders = this.getJsonPlaceholders(locale, params); + let placeholders = {}; + if (config.pages.enableLocalization && config.pages.localizationJsonPath) { + placeholders = this.getJsonPlaceholders(locale, params); + } - // If localization is enabled + // Send response if (config.pages.enableLocalization && locale) { return Utils.getLocalizedPath(defaultPath, locale).then(({ path, subdir }) => redirect From 33b53d4738c8419b49a7c1cffe4a566320331a78 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Mon, 1 Feb 2021 01:30:00 +0100 Subject: [PATCH 40/48] fixed readme typo --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index bf30e304e0..389a123c93 100644 --- a/README.md +++ b/README.md @@ -519,6 +519,7 @@ const api = new ParseServer({ Pros: - All files are complete in their content and can be easily opened and previewed by viewing the file in a browser. + Cons: - In most cases, a localized page differs only slighly from the default page, which could cause a lot of duplicate code that is difficult to maintain. @@ -574,10 +575,11 @@ const api = new ParseServer({ Pros: - There is only one HTML file to maintain that contains the placeholders that are filled with the translations according to the locale. + Cons: - Files cannot be easily previewed by viewing the file in a browser because the content contains only placeholders and even HTML or CSS changes may be dynamically applied, e.g. when a localization requires a Right-To-Left layout direction. - Style and other fundamental layout changes may be more difficult to apply. -- + #### Parameters | Parameter | Optional | Type | Default value | Example values | Environment variable | Description | From 4ac55ef90fb03ae42b835a6be1058caf89b3a52d Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Sun, 7 Feb 2021 21:56:07 +0100 Subject: [PATCH 41/48] minor refactoring of existing tests --- spec/PagesRouter.spec.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/spec/PagesRouter.spec.js b/spec/PagesRouter.spec.js index 760f6374e4..8174e78671 100644 --- a/spec/PagesRouter.spec.js +++ b/spec/PagesRouter.spec.js @@ -35,6 +35,7 @@ describe('Pages Router', () => { 'http://localhost:8378/1/apps/choose_password?appId=test', 'http://localhost:8378/1/apps/email_verification_success.html', 'http://localhost:8378/1/apps/password_reset_success.html', + 'http://localhost:8378/1/apps/custom_json.html', ]; for (const url of urls) { const response = await request({ url }).catch(e => e); @@ -43,9 +44,8 @@ describe('Pages Router', () => { }); it('can load file from custom pages path', async () => { - const _config = Object.assign({}, config); - _config.pages.pagesPath = './public'; - await reconfigureServer(_config); + config.pages.pagesPath = './public'; + await reconfigureServer(config); const response = await request({ url: 'http://localhost:8378/1/apps/email_verification_link_invalid.html', @@ -54,9 +54,8 @@ describe('Pages Router', () => { }); it('can load file from custom pages endpoint', async () => { - const _config = Object.assign({}, config); - _config.pages.pagesEndpoint = 'pages'; - await reconfigureServer(_config); + config.pages.pagesEndpoint = 'pages'; + await reconfigureServer(config); const response = await request({ url: `http://localhost:8378/1/pages/email_verification_link_invalid.html`, From 11a2c814de7d4fd88e9d059a75976bb9b76d6e21 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Sun, 7 Feb 2021 21:56:43 +0100 Subject: [PATCH 42/48] fixed bug where Object type was not recognized as config key type --- resources/buildConfigDefinitions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/buildConfigDefinitions.js b/resources/buildConfigDefinitions.js index 9c2ed48699..81492a75d4 100644 --- a/resources/buildConfigDefinitions.js +++ b/resources/buildConfigDefinitions.js @@ -166,7 +166,7 @@ function parseDefaultValue(elt, value, t) { if (type == 'NumberOrBoolean') { literalValue = t.numericLiteral(parsers.numberOrBoolParser('')(value)); } - const literalTypes = ['IdempotencyOptions','FileUploadOptions','CustomPagesOptions', 'PagesCustomUrlsOptions', 'PagesOptions']; + const literalTypes = ['Object', 'IdempotencyOptions','FileUploadOptions','CustomPagesOptions', 'PagesCustomUrlsOptions', 'PagesOptions']; if (literalTypes.includes(type)) { const object = parsers.objectParser(value); const props = Object.keys(object).map((key) => { From 2b9f2dcc94a9a75673f8816453792f5a7a63b39d Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Sun, 7 Feb 2021 22:00:40 +0100 Subject: [PATCH 43/48] added feature config placeholders --- spec/PagesRouter.spec.js | 55 ++++++++++++++++++++++++++++++++++++-- src/Config.js | 10 ++++++- src/Options/Definitions.js | 9 ++++++- src/Options/docs.js | 3 ++- src/Options/index.js | 5 +++- src/Routers/PagesRouter.js | 19 ++++++++++--- 6 files changed, 91 insertions(+), 10 deletions(-) diff --git a/spec/PagesRouter.spec.js b/spec/PagesRouter.spec.js index 8174e78671..6a22657ba7 100644 --- a/spec/PagesRouter.spec.js +++ b/spec/PagesRouter.spec.js @@ -237,6 +237,9 @@ describe('Pages Router', () => { expect(Config.get(Parse.applicationId).pages.localizationFallbackLocale).toBe( Definitions.PagesOptions.localizationFallbackLocale.default ); + expect(Config.get(Parse.applicationId).pages.placeholders).toBe( + Definitions.PagesOptions.placeholders.default + ); expect(Config.get(Parse.applicationId).pages.forceRedirect).toBe( Definitions.PagesOptions.forceRedirect.default ); @@ -269,6 +272,10 @@ describe('Pages Router', () => { { forceRedirect: 0 }, { forceRedirect: {} }, { forceRedirect: [] }, + { placeholders: true }, + { placeholders: 'a' }, + { placeholders: 0 }, + { placeholders: [] }, { pagesPath: true }, { pagesPath: 0 }, { pagesPath: {} }, @@ -323,6 +330,51 @@ describe('Pages Router', () => { const replacedContent = await pageResponse.calls.all()[0].returnValue; expect(replacedContent.text).not.toContain('{{error}}'); }); + + it('fills placeholders from config object', async () => { + config.pages.enableLocalization = false; + config.pages.placeholders = { + title: 'setViaConfig', + }; + await reconfigureServer(config); + const response = await request({ + url: 'http://localhost:8378/1/apps/custom_json.html', + followRedirects: false, + method: 'GET', + }); + expect(response.status).toEqual(200); + expect(response.text).toContain(config.pages.placeholders.title); + }); + + it('fills placeholders from config function', async () => { + config.pages.enableLocalization = false; + config.pages.placeholders = () => { + return { title: 'setViaConfig' }; + }; + await reconfigureServer(config); + const response = await request({ + url: 'http://localhost:8378/1/apps/custom_json.html', + followRedirects: false, + method: 'GET', + }); + expect(response.status).toEqual(200); + expect(response.text).toContain(config.pages.placeholders().title); + }); + + it('fills placeholders from config promise', async () => { + config.pages.enableLocalization = false; + config.pages.placeholders = async () => { + return { title: 'setViaConfig' }; + }; + await reconfigureServer(config); + const response = await request({ + url: 'http://localhost:8378/1/apps/custom_json.html', + followRedirects: false, + method: 'GET', + }); + expect(response.status).toEqual(200); + expect(response.text).toContain((await config.pages.placeholders()).title); + }); }); describe('localization', () => { @@ -518,8 +570,7 @@ describe('Pages Router', () => { spyOnProperty(Page.prototype, 'defaultFile').and.returnValue(jsonPageFile); const response = await request({ - url: - `http://localhost:8378/1/apps/test/request_password_reset?token=exampleToken&username=exampleUsername&locale=${exampleLocale}`, + url: `http://localhost:8378/1/apps/test/request_password_reset?token=exampleToken&username=exampleUsername&locale=${exampleLocale}`, followRedirects: false, }).catch(e => e); expect(response.status).toEqual(200); diff --git a/src/Config.js b/src/Config.js index efaba9bc24..0dacc5cbe0 100644 --- a/src/Config.js +++ b/src/Config.js @@ -10,7 +10,7 @@ import { IdempotencyOptions, FileUploadOptions, AccountLockoutOptions, - PagesOptions + PagesOptions, } from './Options/Definitions'; import { isBoolean, isString } from 'lodash'; @@ -140,6 +140,14 @@ export class Config { } else if (!isString(pages.localizationFallbackLocale)) { throw 'Parse Server option pages.localizationFallbackLocale must be a string.'; } + if (pages.placeholders === undefined) { + pages.placeholders = PagesOptions.placeholders.default; + } else if ( + Object.prototype.toString.call(pages.placeholders) !== '[object Object]' && + typeof pages.placeholders !== 'function' + ) { + throw 'Parse Server option pages.placeholders must be an object or a function.'; + } if (pages.forceRedirect === undefined) { pages.forceRedirect = PagesOptions.forceRedirect.default; } else if (!isBoolean(pages.forceRedirect)) { diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index fbf976b438..0ac239a136 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -464,7 +464,7 @@ module.exports.PagesOptions = { }, pagesEndpoint: { env: 'PARSE_SERVER_PAGES_PAGES_ENDPOINT', - help: "The API endoint for the pages. Default is the 'apps'.", + help: "The API endpoint for the pages. Default is 'apps'.", default: 'apps', }, pagesPath: { @@ -473,6 +473,13 @@ module.exports.PagesOptions = { "The path to the pages directory; this also defines where the static endpoint '/apps' points to. Default is the './public/' directory.", default: './public', }, + placeholders: { + env: 'PARSE_SERVER_PAGES_PLACEHOLDERS', + help: + 'The placeholder keys and values which will be filled in pages; this can be an simple object or a callback function.', + action: parsers.objectParser, + default: {}, + }, }; module.exports.PagesCustomUrlsOptions = { emailVerificationLinkExpired: { diff --git a/src/Options/docs.js b/src/Options/docs.js index de1e9fe6b1..b4163dcc77 100644 --- a/src/Options/docs.js +++ b/src/Options/docs.js @@ -88,8 +88,9 @@ * @property {Boolean} forceRedirect Is true if responses should always be redirects and never content, false if the response type should depend on the request type (GET request -> content response; POST request -> redirect response). * @property {String} localizationFallbackLocale The fallback locale for localization if no matching translation is provided for the given locale. This is only relevant when providing translation resources via JSON file. * @property {String} localizationJsonPath The path to the JSON file for localization; the translations will be used to fill template placeholders according to the locale. - * @property {String} pagesEndpoint The API endoint for the pages. Default is the 'apps'. + * @property {String} pagesEndpoint The API endpoint for the pages. Default is 'apps'. * @property {String} pagesPath The path to the pages directory; this also defines where the static endpoint '/apps' points to. Default is the './public/' directory. + * @property {Object} placeholders The placeholder keys and values which will be filled in pages; this can be an simple object or a callback function. */ /** diff --git a/src/Options/index.js b/src/Options/index.js index d6b62aee3a..38205c1875 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -241,13 +241,16 @@ export interface PagesOptions { /* The fallback locale for localization if no matching translation is provided for the given locale. This is only relevant when providing translation resources via JSON file. :DEFAULT: en */ localizationFallbackLocale: ?string; + /* The placeholder keys and values which will be filled in pages; this can be an simple object or a callback function. + :DEFAULT: {} */ + placeholders: ?Object; /* Is true if responses should always be redirects and never content, false if the response type should depend on the request type (GET request -> content response; POST request -> redirect response). :DEFAULT: false */ forceRedirect: ?boolean; /* The path to the pages directory; this also defines where the static endpoint '/apps' points to. Default is the './public/' directory. :DEFAULT: ./public */ pagesPath: ?string; - /* The API endoint for the pages. Default is the 'apps'. + /* The API endpoint for the pages. Default is 'apps'. :DEFAULT: apps */ pagesEndpoint: ?string; /* The URLs to the custom pages. diff --git a/src/Routers/PagesRouter.js b/src/Routers/PagesRouter.js index f0c6a0dda4..113a09d6e0 100644 --- a/src/Routers/PagesRouter.js +++ b/src/Routers/PagesRouter.js @@ -421,11 +421,22 @@ export class PagesRouter extends PromiseRouter { return this.notFound(); } + // Get config placeholders; can be an object, a function or an async function + let configPlaceholders = typeof this.pagesConfig.placeholders === 'function' + ? this.pagesConfig.placeholders() + : Object.prototype.toString.call(this.pagesConfig.placeholders) === '[object Object]' + ? this.pagesConfig.placeholders + : {}; + if (configPlaceholders instanceof Promise) { + configPlaceholders = await configPlaceholders; + } + // Fill placeholders - const allPlaceholders = Object.assign({}, params, placeholders); - data = mustache.render(data, allPlaceholders); + const allPlaceholders = Object.assign({}, configPlaceholders, placeholders); + const paramsAndPlaceholders = Object.assign({}, params, allPlaceholders); + data = mustache.render(data, paramsAndPlaceholders); - // Add placeholers in header to allow parsing for programmatic use + // Add placeholders in header to allow parsing for programmatic use // of response, instead of having to parse the HTML content. const headers = Object.entries(params).reduce((m, p) => { if (p[1] !== undefined) { @@ -455,7 +466,7 @@ export class PagesRouter extends PromiseRouter { } /** - * Reads and returns the contet of a file at a given path. File reading to + * Reads and returns the content of a file at a given path. File reading to * serve content on the static route is only allowed from the pages * directory on downwards. * ----------------------------------------------------------------------- From 3ba14db262c218c1f2e35e1d1484f813b2b90dd1 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Sun, 7 Feb 2021 22:07:58 +0100 Subject: [PATCH 44/48] prettier --- src/Routers/PagesRouter.js | 87 +++++++++++++++++++++++--------------- 1 file changed, 54 insertions(+), 33 deletions(-) diff --git a/src/Routers/PagesRouter.js b/src/Routers/PagesRouter.js index 113a09d6e0..c1de2f95d1 100644 --- a/src/Routers/PagesRouter.js +++ b/src/Routers/PagesRouter.js @@ -11,13 +11,34 @@ import Page from '../Page'; // All pages with custom page key for reference and file name const pages = Object.freeze({ passwordReset: new Page({ id: 'passwordReset', defaultFile: 'password_reset.html' }), - passwordResetSuccess: new Page({ id: 'passwordResetSuccess', defaultFile: 'password_reset_success.html' }), - passwordResetLinkInvalid: new Page({ id: 'passwordResetLinkInvalid', defaultFile: 'password_reset_link_invalid.html' }), - emailVerificationSuccess: new Page({ id: 'emailVerificationSuccess', defaultFile: 'email_verification_success.html' }), - emailVerificationSendFail: new Page({ id: 'emailVerificationSendFail', defaultFile: 'email_verification_send_fail.html' }), - emailVerificationSendSuccess: new Page({ id: 'emailVerificationSendSuccess', defaultFile: 'email_verification_send_success.html' }), - emailVerificationLinkInvalid: new Page({ id: 'emailVerificationLinkInvalid', defaultFile: 'email_verification_link_invalid.html' }), - emailVerificationLinkExpired: new Page({ id: 'emailVerificationLinkExpired', defaultFile: 'email_verification_link_expired.html' }), + passwordResetSuccess: new Page({ + id: 'passwordResetSuccess', + defaultFile: 'password_reset_success.html', + }), + passwordResetLinkInvalid: new Page({ + id: 'passwordResetLinkInvalid', + defaultFile: 'password_reset_link_invalid.html', + }), + emailVerificationSuccess: new Page({ + id: 'emailVerificationSuccess', + defaultFile: 'email_verification_success.html', + }), + emailVerificationSendFail: new Page({ + id: 'emailVerificationSendFail', + defaultFile: 'email_verification_send_fail.html', + }), + emailVerificationSendSuccess: new Page({ + id: 'emailVerificationSendSuccess', + defaultFile: 'email_verification_send_success.html', + }), + emailVerificationLinkInvalid: new Page({ + id: 'emailVerificationLinkInvalid', + defaultFile: 'email_verification_link_invalid.html', + }), + emailVerificationLinkExpired: new Page({ + id: 'emailVerificationLinkExpired', + defaultFile: 'email_verification_link_expired.html', + }), }); // All page parameters for reference to be used as template placeholders or query params @@ -36,8 +57,8 @@ const pageParamHeaderPrefix = 'x-parse-page-param-'; // The errors being thrown const errors = Object.freeze({ - jsonFailedFileLoading: "failed to load JSON file", - fileOutsideAllowedScope: "not allowed to read file outside of pages directory" + jsonFailedFileLoading: 'failed to load JSON file', + fileOutsideAllowedScope: 'not allowed to read file outside of pages directory', }); export class PagesRouter extends PromiseRouter { @@ -50,9 +71,7 @@ export class PagesRouter extends PromiseRouter { // Set instance properties this.pagesConfig = pages; - this.pagesEndpoint = pages.pagesEndpoint - ? pages.pagesEndpoint - : 'apps'; + this.pagesEndpoint = pages.pagesEndpoint ? pages.pagesEndpoint : 'apps'; this.pagesPath = pages.pagesPath ? path.resolve('./', pages.pagesPath) : path.resolve(__dirname, '../../public'); @@ -121,7 +140,7 @@ export class PagesRouter extends PromiseRouter { [pageParams.appName]: config.appName, [pageParams.token]: req.query.token, [pageParams.username]: req.query.username, - [pageParams.publicServerUrl]: config.publicServerURL + [pageParams.publicServerUrl]: config.publicServerURL, }; return this.goToPage(req, pages.passwordReset, params); } @@ -289,7 +308,10 @@ export class PagesRouter extends PromiseRouter { if (config.pages.enableLocalization && locale) { return Utils.getLocalizedPath(defaultPath, locale).then(({ path, subdir }) => redirect - ? this.redirectResponse(this.composePageUrl(defaultFile, config.publicServerURL, subdir), params) + ? this.redirectResponse( + this.composePageUrl(defaultFile, config.publicServerURL, subdir), + params + ) : this.pageResponse(path, params, placeholders) ); } else { @@ -356,7 +378,6 @@ export class PagesRouter extends PromiseRouter { * translation was found. */ getJsonTranslation(locale) { - // If there is no JSON resource if (this.jsonParameters === undefined) { return {}; @@ -366,11 +387,12 @@ export class PagesRouter extends PromiseRouter { locale = locale || this.pagesConfig.localizationFallbackLocale; // Get matching translation by locale, language or fallback locale - const language = locale.split("-")[0]; - const resource = this.jsonParameters[locale] - || this.jsonParameters[language] - || this.jsonParameters[this.pagesConfig.localizationFallbackLocale] - || {}; + const language = locale.split('-')[0]; + const resource = + this.jsonParameters[locale] || + this.jsonParameters[language] || + this.jsonParameters[this.pagesConfig.localizationFallbackLocale] || + {}; const translation = resource.translation || {}; return translation; } @@ -385,7 +407,6 @@ export class PagesRouter extends PromiseRouter { * translation was found. */ getJsonPlaceholders(locale, params = {}) { - // If localization is disabled or there is no JSON resource if (!this.pagesConfig.enableLocalization || !this.pagesConfig.localizationJsonPath) { return {}; @@ -422,11 +443,12 @@ export class PagesRouter extends PromiseRouter { } // Get config placeholders; can be an object, a function or an async function - let configPlaceholders = typeof this.pagesConfig.placeholders === 'function' - ? this.pagesConfig.placeholders() - : Object.prototype.toString.call(this.pagesConfig.placeholders) === '[object Object]' - ? this.pagesConfig.placeholders - : {}; + let configPlaceholders = + typeof this.pagesConfig.placeholders === 'function' + ? this.pagesConfig.placeholders() + : Object.prototype.toString.call(this.pagesConfig.placeholders) === '[object Object]' + ? this.pagesConfig.placeholders + : {}; if (configPlaceholders instanceof Promise) { configPlaceholders = await configPlaceholders; } @@ -477,7 +499,6 @@ export class PagesRouter extends PromiseRouter { * @returns {Promise} The file content. */ async readFile(filePath) { - // Normalize path to prevent it from containing any directory changing // UNIX patterns which could expose the whole file system, e.g. // `http://example.com/parse/apps/../file.txt` requests a file outside @@ -486,7 +507,7 @@ export class PagesRouter extends PromiseRouter { // Abort if the path is outside of the path directory scope if (!normalizedPath.startsWith(this.pagesPath)) { - throw(errors.fileOutsideAllowedScope); + throw errors.fileOutsideAllowedScope; } return await fs.readFile(normalizedPath, 'utf-8'); @@ -503,7 +524,7 @@ export class PagesRouter extends PromiseRouter { const json = require(path.resolve('./', this.pagesConfig.localizationJsonPath)); this.jsonParameters = json; } catch (e) { - throw(errors.jsonFailedFileLoading); + throw errors.jsonFailedFileLoading; } } @@ -531,10 +552,10 @@ export class PagesRouter extends PromiseRouter { */ getLocale(req) { const locale = - (req.query || {})[pageParams.locale] - || (req.body || {})[pageParams.locale] - || (req.params || {})[pageParams.locale] - || (req.headers || {})[pageParamHeaderPrefix + pageParams.locale]; + (req.query || {})[pageParams.locale] || + (req.body || {})[pageParams.locale] || + (req.params || {})[pageParams.locale] || + (req.headers || {})[pageParamHeaderPrefix + pageParams.locale]; return locale; } From 3e31aae75488f45bd81887c990acdba92bce8d35 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Mon, 8 Feb 2021 02:23:08 +0100 Subject: [PATCH 45/48] added passing locale to page config placeholder callback --- src/Routers/PagesRouter.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Routers/PagesRouter.js b/src/Routers/PagesRouter.js index c1de2f95d1..e213b3c24a 100644 --- a/src/Routers/PagesRouter.js +++ b/src/Routers/PagesRouter.js @@ -339,12 +339,12 @@ export class PagesRouter extends PromiseRouter { return this.fileResponse(absolutePath); } - // Get default parameters + // Get parameters const params = this.getDefaultParams(req.config); + params.locale = this.getLocale(req); // Get JSON placeholders - const locale = this.getLocale(req); - const placeholders = this.getJsonPlaceholders(locale, params); + const placeholders = this.getJsonPlaceholders(params.locale, params); return this.pageResponse(absolutePath, params, placeholders); } @@ -445,7 +445,7 @@ export class PagesRouter extends PromiseRouter { // Get config placeholders; can be an object, a function or an async function let configPlaceholders = typeof this.pagesConfig.placeholders === 'function' - ? this.pagesConfig.placeholders() + ? this.pagesConfig.placeholders(params) : Object.prototype.toString.call(this.pagesConfig.placeholders) === '[object Object]' ? this.pagesConfig.placeholders : {}; From 76e983fc842dead1ebed7758b05a881c67409a18 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Mon, 8 Feb 2021 02:39:39 +0100 Subject: [PATCH 46/48] refactored passing locale to placeholder to pass test --- src/Routers/PagesRouter.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Routers/PagesRouter.js b/src/Routers/PagesRouter.js index e213b3c24a..3243d5a58c 100644 --- a/src/Routers/PagesRouter.js +++ b/src/Routers/PagesRouter.js @@ -341,10 +341,13 @@ export class PagesRouter extends PromiseRouter { // Get parameters const params = this.getDefaultParams(req.config); - params.locale = this.getLocale(req); + const locale = this.getLocale(req); + if (locale) { + params.locale = locale; + } // Get JSON placeholders - const placeholders = this.getJsonPlaceholders(params.locale, params); + const placeholders = this.getJsonPlaceholders(locale, params); return this.pageResponse(absolutePath, params, placeholders); } From aeaac52d9b685c23da6cd7009d0bda854a3f9d36 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Mon, 8 Feb 2021 03:18:40 +0100 Subject: [PATCH 47/48] added config placeholder feature to README --- README.md | 78 +++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 59 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index e40f342b59..12e6754f3d 100644 --- a/README.md +++ b/README.md @@ -595,27 +595,67 @@ Cons: - Files cannot be easily previewed by viewing the file in a browser because the content contains only placeholders and even HTML or CSS changes may be dynamically applied, e.g. when a localization requires a Right-To-Left layout direction. - Style and other fundamental layout changes may be more difficult to apply. +#### Dynamic placeholders + +In addition to feature related default parameters such as `appId` and the translations provided via JSON resource, it is possible to define custom dynamic placeholders as part of the router configuration. This works independently of localization and, also if `enableLocalization` is disabled. + +**Configuration Example:** +```js +const api = new ParseServer({ + ...otherOptions, + + pages: { + enableRouter: true, // Enables the experimental feature; required for localization + placeholders: { + exampleKey: 'exampleValue' + } + } +} +``` +The placeholders can also be provided as function or as async function, with the `locale` and other feature related parameters passed through, to allow for dynamic placeholder values: + +```js +const api = new ParseServer({ + ...otherOptions, + + pages: { + enableRouter: true, // Enables the experimental feature; required for localization + placeholders: async (params) => { + const value = await doSomething(params.locale); + return { + exampleKey: value + }; + } + } +} +``` + +#### Reserved Keys + +The following parameter and placeholder keys are reserved because they are used related to features such as password reset or email verification. They should not be used as translation keys in the JSON resource or as manually defined placeholder keys in the configuration: `appId`, `appName`, `email`, `error`, `locale`, `publicServerUrl`, `token`, `username`. + #### Parameters -| Parameter | Optional | Type | Default value | Example values | Environment variable | Description | -|-------------------------------------------------|----------|-----------|----------------------------------------|------------------------------------------------------|-----------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `pages` | yes | `Object` | `undefined` | - | `PARSE_SERVER_PAGES` | The options for pages such as password reset and email verification. | -| `pages.enableRouter` | yes | `Boolean` | `false` | - | `PARSE_SERVER_PAGES_ENABLE_ROUTER` | Is `true` if the pages router should be enabled; this is required for any of the pages options to take effect. **Caution, this is an experimental feature that may not be appropriate for production.** | -| `pages.enableLocalization` | yes | `Boolean` | `false` | - | `PARSE_SERVER_PAGES_ENABLE_LOCALIZATION` | Is true if pages should be localized; this has no effect on custom page redirects. | -| `pages.localizationJsonPath` | yes | `String` | `undefined` | `./private/translations.json` | `PARSE_SERVER_PAGES_LOCALIZATION_JSON_PATH` | The path to the JSON file for localization; the translations will be used to fill template placeholders according to the locale. | -| `pages.localizationFallbackLocale` | yes | `String` | `en` | `en`, `en-GB`, `default` | `PARSE_SERVER_PAGES_LOCALIZATION_FALLBACK_LOCALE` | The fallback locale for localization if no matching translation is provided for the given locale. This is only relevant when providing translation resources via JSON file. | -| `pages.forceRedirect` | yes | `Boolean` | `false` | - | `PARSE_SERVER_PAGES_FORCE_REDIRECT` | Is `true` if responses should always be redirects and never content, `false` if the response type should depend on the request type (`GET` request -> content response; `POST` request -> redirect response). | -| `pages.pagesPath` | yes | `String` | `./public` | `./files/pages`, `../../pages` | `PARSE_SERVER_PAGES_PAGES_PATH` | The path to the pages directory; this also defines where the static endpoint `/apps` points to. | -| `pages.pagesEndpoint` | yes | `String` | `apps` | - | `PARSE_SERVER_PAGES_PAGES_ENDPOINT` | The API endoint for the pages. | -| `pages.customUrls` | yes | `Object` | `{}` | `{ passwordReset: 'https://example.com/page.html' }` | `PARSE_SERVER_PAGES_CUSTOM_URLS` | The URLs to the custom pages | -| `pages.customUrls.passwordReset` | yes | `String` | `password_reset.html` | - | `PARSE_SERVER_PAGES_CUSTOM_URL_PASSWORD_RESET` | The URL to the custom page for password reset. | -| `pages.customUrls.passwordResetSuccess` | yes | `String` | `password_reset_success.html` | - | `PARSE_SERVER_PAGES_CUSTOM_URL_PASSWORD_RESET_SUCCESS` | The URL to the custom page for password reset -> success. | -| `pages.customUrls.passwordResetLinkInvalid` | yes | `String` | `password_reset_link_invalid.html` | - | `PARSE_SERVER_PAGES_CUSTOM_URL_PASSWORD_RESET_LINK_INVALID` | The URL to the custom page for password reset -> link invalid. | -| `pages.customUrls.emailVerificationSuccess` | yes | `String` | `email_verification_success.html` | - | `PARSE_SERVER_PAGES_CUSTOM_URL_EMAIL_VERIFICATION_SUCCESS` | The URL to the custom page for email verification -> success. | -| `pages.customUrls.emailVerificationSendFail` | yes | `String` | `email_verification_send_fail.html` | - | `PARSE_SERVER_PAGES_CUSTOM_URL_EMAIL_VERIFICATION_SEND_FAIL` | The URL to the custom page for email verification -> link send fail. | -| `pages.customUrls.emailVerificationSendSuccess` | yes | `String` | `email_verification_send_success.html` | - | `PARSE_SERVER_PAGES_CUSTOM_URL_EMAIL_VERIFICATION_SEND_SUCCESS` | The URL to the custom page for email verification -> resend link -> success. | -| `pages.customUrls.emailVerificationLinkInvalid` | yes | `String` | `email_verification_link_invalid.html` | - | `PARSE_SERVER_PAGES_CUSTOM_URL_EMAIL_VERIFICATION_LINK_INVALID` | The URL to the custom page for email verification -> link invalid. | -| `pages.customUrls.emailVerificationLinkExpired` | yes | `String` | `email_verification_link_expired.html` | - | `PARSE_SERVER_PAGES_CUSTOM_URL_EMAIL_VERIFICATION_LINK_EXPIRED` | The URL to the custom page for email verification -> link expired. | +| Parameter | Optional | Type | Default value | Example values | Environment variable | Description | +|-------------------------------------------------|----------|---------------------------------------|----------------------------------------|------------------------------------------------------|-----------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `pages` | yes | `Object` | `undefined` | - | `PARSE_SERVER_PAGES` | The options for pages such as password reset and email verification. | +| `pages.enableRouter` | yes | `Boolean` | `false` | - | `PARSE_SERVER_PAGES_ENABLE_ROUTER` | Is `true` if the pages router should be enabled; this is required for any of the pages options to take effect. **Caution, this is an experimental feature that may not be appropriate for production.** | +| `pages.enableLocalization` | yes | `Boolean` | `false` | - | `PARSE_SERVER_PAGES_ENABLE_LOCALIZATION` | Is true if pages should be localized; this has no effect on custom page redirects. | +| `pages.localizationJsonPath` | yes | `String` | `undefined` | `./private/translations.json` | `PARSE_SERVER_PAGES_LOCALIZATION_JSON_PATH` | The path to the JSON file for localization; the translations will be used to fill template placeholders according to the locale. | +| `pages.localizationFallbackLocale` | yes | `String` | `en` | `en`, `en-GB`, `default` | `PARSE_SERVER_PAGES_LOCALIZATION_FALLBACK_LOCALE` | The fallback locale for localization if no matching translation is provided for the given locale. This is only relevant when providing translation resources via JSON file. | +| `pages.placeholders` | yes | `Object`, `Function`, `AsyncFunction` | `undefined` | `{ exampleKey: 'exampleValue' }` | `PARSE_SERVER_PAGES_PLACEHOLDERS` | The placeholder keys and values which will be filled in pages; this can be an simple object or a callback function. | +| `pages.forceRedirect` | yes | `Boolean` | `false` | - | `PARSE_SERVER_PAGES_FORCE_REDIRECT` | Is `true` if responses should always be redirects and never content, `false` if the response type should depend on the request type (`GET` request -> content response; `POST` request -> redirect response). | +| `pages.pagesPath` | yes | `String` | `./public` | `./files/pages`, `../../pages` | `PARSE_SERVER_PAGES_PAGES_PATH` | The path to the pages directory; this also defines where the static endpoint `/apps` points to. | +| `pages.pagesEndpoint` | yes | `String` | `apps` | - | `PARSE_SERVER_PAGES_PAGES_ENDPOINT` | The API endpoint for the pages. | +| `pages.customUrls` | yes | `Object` | `{}` | `{ passwordReset: 'https://example.com/page.html' }` | `PARSE_SERVER_PAGES_CUSTOM_URLS` | The URLs to the custom pages | +| `pages.customUrls.passwordReset` | yes | `String` | `password_reset.html` | - | `PARSE_SERVER_PAGES_CUSTOM_URL_PASSWORD_RESET` | The URL to the custom page for password reset. | +| `pages.customUrls.passwordResetSuccess` | yes | `String` | `password_reset_success.html` | - | `PARSE_SERVER_PAGES_CUSTOM_URL_PASSWORD_RESET_SUCCESS` | The URL to the custom page for password reset -> success. | +| `pages.customUrls.passwordResetLinkInvalid` | yes | `String` | `password_reset_link_invalid.html` | - | `PARSE_SERVER_PAGES_CUSTOM_URL_PASSWORD_RESET_LINK_INVALID` | The URL to the custom page for password reset -> link invalid. | +| `pages.customUrls.emailVerificationSuccess` | yes | `String` | `email_verification_success.html` | - | `PARSE_SERVER_PAGES_CUSTOM_URL_EMAIL_VERIFICATION_SUCCESS` | The URL to the custom page for email verification -> success. | +| `pages.customUrls.emailVerificationSendFail` | yes | `String` | `email_verification_send_fail.html` | - | `PARSE_SERVER_PAGES_CUSTOM_URL_EMAIL_VERIFICATION_SEND_FAIL` | The URL to the custom page for email verification -> link send fail. | +| `pages.customUrls.emailVerificationSendSuccess` | yes | `String` | `email_verification_send_success.html` | - | `PARSE_SERVER_PAGES_CUSTOM_URL_EMAIL_VERIFICATION_SEND_SUCCESS` | The URL to the custom page for email verification -> resend link -> success. | +| `pages.customUrls.emailVerificationLinkInvalid` | yes | `String` | `email_verification_link_invalid.html` | - | `PARSE_SERVER_PAGES_CUSTOM_URL_EMAIL_VERIFICATION_LINK_INVALID` | The URL to the custom page for email verification -> link invalid. | +| `pages.customUrls.emailVerificationLinkExpired` | yes | `String` | `email_verification_link_expired.html` | - | `PARSE_SERVER_PAGES_CUSTOM_URL_EMAIL_VERIFICATION_LINK_EXPIRED` | The URL to the custom page for email verification -> link expired. | ### Notes From 79058b03a1dbf5740aa941d701dc34eb5f5a1df2 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Mon, 8 Feb 2021 03:22:44 +0100 Subject: [PATCH 48/48] fixed typo in README --- README.md | 2 +- src/Options/Definitions.js | 2 +- src/Options/docs.js | 2 +- src/Options/index.js | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 12e6754f3d..3310db734e 100644 --- a/README.md +++ b/README.md @@ -643,7 +643,7 @@ The following parameter and placeholder keys are reserved because they are used | `pages.enableLocalization` | yes | `Boolean` | `false` | - | `PARSE_SERVER_PAGES_ENABLE_LOCALIZATION` | Is true if pages should be localized; this has no effect on custom page redirects. | | `pages.localizationJsonPath` | yes | `String` | `undefined` | `./private/translations.json` | `PARSE_SERVER_PAGES_LOCALIZATION_JSON_PATH` | The path to the JSON file for localization; the translations will be used to fill template placeholders according to the locale. | | `pages.localizationFallbackLocale` | yes | `String` | `en` | `en`, `en-GB`, `default` | `PARSE_SERVER_PAGES_LOCALIZATION_FALLBACK_LOCALE` | The fallback locale for localization if no matching translation is provided for the given locale. This is only relevant when providing translation resources via JSON file. | -| `pages.placeholders` | yes | `Object`, `Function`, `AsyncFunction` | `undefined` | `{ exampleKey: 'exampleValue' }` | `PARSE_SERVER_PAGES_PLACEHOLDERS` | The placeholder keys and values which will be filled in pages; this can be an simple object or a callback function. | +| `pages.placeholders` | yes | `Object`, `Function`, `AsyncFunction` | `undefined` | `{ exampleKey: 'exampleValue' }` | `PARSE_SERVER_PAGES_PLACEHOLDERS` | The placeholder keys and values which will be filled in pages; this can be a simple object or a callback function. | | `pages.forceRedirect` | yes | `Boolean` | `false` | - | `PARSE_SERVER_PAGES_FORCE_REDIRECT` | Is `true` if responses should always be redirects and never content, `false` if the response type should depend on the request type (`GET` request -> content response; `POST` request -> redirect response). | | `pages.pagesPath` | yes | `String` | `./public` | `./files/pages`, `../../pages` | `PARSE_SERVER_PAGES_PAGES_PATH` | The path to the pages directory; this also defines where the static endpoint `/apps` points to. | | `pages.pagesEndpoint` | yes | `String` | `apps` | - | `PARSE_SERVER_PAGES_PAGES_ENDPOINT` | The API endpoint for the pages. | diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index 0ac239a136..c67017a585 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -476,7 +476,7 @@ module.exports.PagesOptions = { placeholders: { env: 'PARSE_SERVER_PAGES_PLACEHOLDERS', help: - 'The placeholder keys and values which will be filled in pages; this can be an simple object or a callback function.', + 'The placeholder keys and values which will be filled in pages; this can be a simple object or a callback function.', action: parsers.objectParser, default: {}, }, diff --git a/src/Options/docs.js b/src/Options/docs.js index b4163dcc77..da90760389 100644 --- a/src/Options/docs.js +++ b/src/Options/docs.js @@ -90,7 +90,7 @@ * @property {String} localizationJsonPath The path to the JSON file for localization; the translations will be used to fill template placeholders according to the locale. * @property {String} pagesEndpoint The API endpoint for the pages. Default is 'apps'. * @property {String} pagesPath The path to the pages directory; this also defines where the static endpoint '/apps' points to. Default is the './public/' directory. - * @property {Object} placeholders The placeholder keys and values which will be filled in pages; this can be an simple object or a callback function. + * @property {Object} placeholders The placeholder keys and values which will be filled in pages; this can be a simple object or a callback function. */ /** diff --git a/src/Options/index.js b/src/Options/index.js index 38205c1875..e333b53694 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -241,7 +241,7 @@ export interface PagesOptions { /* The fallback locale for localization if no matching translation is provided for the given locale. This is only relevant when providing translation resources via JSON file. :DEFAULT: en */ localizationFallbackLocale: ?string; - /* The placeholder keys and values which will be filled in pages; this can be an simple object or a callback function. + /* The placeholder keys and values which will be filled in pages; this can be a simple object or a callback function. :DEFAULT: {} */ placeholders: ?Object; /* Is true if responses should always be redirects and never content, false if the response type should depend on the request type (GET request -> content response; POST request -> redirect response).