feat: Deprecation DEPPS11: Replace PublicAPIRouter with PagesRouter (#9974)
BREAKING CHANGE: This release replaces `PublicAPIRouter` with `PagesRouter` (Deprecation DEPPS11).
This commit is contained in:
@@ -14,7 +14,7 @@ The following is a list of deprecations, according to the [Deprecation Policy](h
|
|||||||
| DEPPS8 | Login with expired 3rd party authentication token defaults to `false` | [#7079](https://github.com/parse-community/parse-server/pull/7079) | 5.3.0 (2022) | 7.0.0 (2024) | removed | - |
|
| DEPPS8 | Login with expired 3rd party authentication token defaults to `false` | [#7079](https://github.com/parse-community/parse-server/pull/7079) | 5.3.0 (2022) | 7.0.0 (2024) | removed | - |
|
||||||
| DEPPS9 | Rename LiveQuery `fields` option to `keys` | [#8389](https://github.com/parse-community/parse-server/issues/8389) | 6.0.0 (2023) | 7.0.0 (2024) | removed | - |
|
| DEPPS9 | Rename LiveQuery `fields` option to `keys` | [#8389](https://github.com/parse-community/parse-server/issues/8389) | 6.0.0 (2023) | 7.0.0 (2024) | removed | - |
|
||||||
| DEPPS10 | Encode `Parse.Object` in Cloud Function and remove option `encodeParseObjectInCloudFunction` | [#8634](https://github.com/parse-community/parse-server/issues/8634) | 6.2.0 (2023) | 9.0.0 (2026) | removed | - |
|
| DEPPS10 | Encode `Parse.Object` in Cloud Function and remove option `encodeParseObjectInCloudFunction` | [#8634](https://github.com/parse-community/parse-server/issues/8634) | 6.2.0 (2023) | 9.0.0 (2026) | removed | - |
|
||||||
| DEPPS11 | Replace `PublicAPIRouter` with `PagesRouter` | [#7625](https://github.com/parse-community/parse-server/issues/7625) | 8.0.0 (2025) | 9.0.0 (2026) | deprecated | - |
|
| DEPPS11 | Replace `PublicAPIRouter` with `PagesRouter` | [#7625](https://github.com/parse-community/parse-server/issues/7625) | 8.0.0 (2025) | 9.0.0 (2026) | removed | - |
|
||||||
| DEPPS12 | Database option `allowPublicExplain` will default to `true` | [#7519](https://github.com/parse-community/parse-server/issues/7519) | 8.5.0 (2025) | 9.0.0 (2026) | deprecated | - |
|
| DEPPS12 | Database option `allowPublicExplain` will default to `true` | [#7519](https://github.com/parse-community/parse-server/issues/7519) | 8.5.0 (2025) | 9.0.0 (2026) | deprecated | - |
|
||||||
|
|
||||||
[i_deprecation]: ## "The version and date of the deprecation."
|
[i_deprecation]: ## "The version and date of the deprecation."
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ COPY --from=build /tmp/lib lib
|
|||||||
|
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
COPY bin bin
|
COPY bin bin
|
||||||
COPY public_html public_html
|
COPY public public
|
||||||
COPY views views
|
COPY views views
|
||||||
RUN mkdir -p logs && chown -R node: logs
|
RUN mkdir -p logs && chown -R node: logs
|
||||||
|
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<!-- This page is displayed when someone navigates to a verify email or reset password link
|
|
||||||
but their security token is wrong. This can either mean the user has clicked on a
|
|
||||||
stale link (i.e. re-click on a password reset link after resetting their password) or
|
|
||||||
(rarely) this could be a sign of a malicious user trying to tamper with your app.
|
|
||||||
-->
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>Invalid Link</title>
|
|
||||||
<style type='text/css'>
|
|
||||||
.container {
|
|
||||||
border-width: 0px;
|
|
||||||
display: block;
|
|
||||||
font: inherit;
|
|
||||||
font-family: 'Helvetica Neue', Helvetica;
|
|
||||||
font-size: 16px;
|
|
||||||
height: 30px;
|
|
||||||
line-height: 16px;
|
|
||||||
margin: 45px 0px 0px 45px;
|
|
||||||
padding: 0px 8px 0px 8px;
|
|
||||||
position: relative;
|
|
||||||
vertical-align: baseline;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1, h2, h3, h4, h5 {
|
|
||||||
color: #0067AB;
|
|
||||||
display: block;
|
|
||||||
font: inherit;
|
|
||||||
font-family: 'Open Sans', 'Helvetica Neue', Helvetica;
|
|
||||||
font-size: 30px;
|
|
||||||
font-weight: 600;
|
|
||||||
height: 30px;
|
|
||||||
line-height: 30px;
|
|
||||||
margin: 0 0 15px 0;
|
|
||||||
padding: 0 0 0 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
<h1>Invalid Link</h1>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<!-- This page is displayed when someone navigates to a verify email or reset password link
|
|
||||||
but their security token is wrong. This can either mean the user has clicked on a
|
|
||||||
stale link (i.e. re-click on a password reset link after resetting their password) or
|
|
||||||
(rarely) this could be a sign of a malicious user trying to tamper with your app.
|
|
||||||
-->
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>Invalid Link</title>
|
|
||||||
<style type='text/css'>
|
|
||||||
.container {
|
|
||||||
border-width: 0px;
|
|
||||||
display: block;
|
|
||||||
font: inherit;
|
|
||||||
font-family: 'Helvetica Neue', Helvetica;
|
|
||||||
font-size: 16px;
|
|
||||||
height: 30px;
|
|
||||||
line-height: 16px;
|
|
||||||
margin: 45px 0px 0px 45px;
|
|
||||||
padding: 0px 8px 0px 8px;
|
|
||||||
position: relative;
|
|
||||||
vertical-align: baseline;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1, h2, h3, h4, h5 {
|
|
||||||
color: #0067AB;
|
|
||||||
display: block;
|
|
||||||
font: inherit;
|
|
||||||
font-family: 'Open Sans', 'Helvetica Neue', Helvetica;
|
|
||||||
font-size: 30px;
|
|
||||||
font-weight: 600;
|
|
||||||
height: 30px;
|
|
||||||
line-height: 30px;
|
|
||||||
margin: 0 0 15px 0;
|
|
||||||
padding: 0 0 0 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<script type="text/javascript">
|
|
||||||
function getUrlParameter(name) {
|
|
||||||
name = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]');
|
|
||||||
var regex = new RegExp('[\\?&]' + name + '=([^&#]*)');
|
|
||||||
var results = regex.exec(location.search);
|
|
||||||
return results === null ? '' : decodeURIComponent(results[1].replace(/\+/g, ' '));
|
|
||||||
};
|
|
||||||
|
|
||||||
window.onload = addDataToForm;
|
|
||||||
|
|
||||||
function addDataToForm() {
|
|
||||||
const token = getUrlParameter("token");
|
|
||||||
document.getElementById("token").value = token;
|
|
||||||
|
|
||||||
var appId = getUrlParameter("appId");
|
|
||||||
document.getElementById("resendForm").action = '/apps/' + appId + '/resend_verification_email'
|
|
||||||
}
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
<h1>Invalid Verification Link</h1>
|
|
||||||
<form id="resendForm" method="POST" action="/resend_verification_email">
|
|
||||||
<input id="token" class="form-control" name="token" type="hidden" value="">
|
|
||||||
<button type="submit" class="btn btn-default">Resend Link</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<!-- This page is displayed when someone navigates to a verify email link with an invalid
|
|
||||||
security token and requests a link resend. This page is displayed when the username from
|
|
||||||
the original link is invalid or if the email of that user has already been verfieid when
|
|
||||||
the resend request is made
|
|
||||||
-->
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>Invalid Link</title>
|
|
||||||
<style type='text/css'>
|
|
||||||
.container {
|
|
||||||
border-width: 0px;
|
|
||||||
display: block;
|
|
||||||
font: inherit;
|
|
||||||
font-family: 'Helvetica Neue', Helvetica;
|
|
||||||
font-size: 16px;
|
|
||||||
height: 30px;
|
|
||||||
line-height: 16px;
|
|
||||||
margin: 45px 0px 0px 45px;
|
|
||||||
padding: 0px 8px 0px 8px;
|
|
||||||
position: relative;
|
|
||||||
vertical-align: baseline;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1, h2, h3, h4, h5 {
|
|
||||||
color: #0067AB;
|
|
||||||
display: block;
|
|
||||||
font: inherit;
|
|
||||||
font-family: 'Open Sans', 'Helvetica Neue', Helvetica;
|
|
||||||
font-size: 30px;
|
|
||||||
font-weight: 600;
|
|
||||||
height: 30px;
|
|
||||||
line-height: 30px;
|
|
||||||
margin: 0 0 15px 0;
|
|
||||||
padding: 0 0 0 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
<h1>No link sent. User not found or email already verified</h1>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<!-- This page is displayed when someone navigates to a verify email link with an invalid
|
|
||||||
security token and requests a link resend. This page is displayed when the username
|
|
||||||
from the original verification link has been found and a new verification link has
|
|
||||||
been successfully sent to the corresponding stored email
|
|
||||||
-->
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>Invalid Link</title>
|
|
||||||
<style type='text/css'>
|
|
||||||
.container {
|
|
||||||
border-width: 0px;
|
|
||||||
display: block;
|
|
||||||
font: inherit;
|
|
||||||
font-family: 'Helvetica Neue', Helvetica;
|
|
||||||
font-size: 16px;
|
|
||||||
height: 30px;
|
|
||||||
line-height: 16px;
|
|
||||||
margin: 45px 0px 0px 45px;
|
|
||||||
padding: 0px 8px 0px 8px;
|
|
||||||
position: relative;
|
|
||||||
vertical-align: baseline;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1, h2, h3, h4, h5 {
|
|
||||||
color: #0067AB;
|
|
||||||
display: block;
|
|
||||||
font: inherit;
|
|
||||||
font-family: 'Open Sans', 'Helvetica Neue', Helvetica;
|
|
||||||
font-size: 30px;
|
|
||||||
font-weight: 600;
|
|
||||||
height: 30px;
|
|
||||||
line-height: 30px;
|
|
||||||
margin: 0 0 15px 0;
|
|
||||||
padding: 0 0 0 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
<h1>Link Sent! Check your email.</h1>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<!-- 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'
|
|
||||||
-->
|
|
||||||
<head>
|
|
||||||
<title>Password Reset</title>
|
|
||||||
<style type='text/css'>
|
|
||||||
h1 {
|
|
||||||
color: #0067AB;
|
|
||||||
display: block;
|
|
||||||
font: inherit;
|
|
||||||
font-family: 'Open Sans', 'Helvetica Neue', Helvetica;
|
|
||||||
font-size: 30px;
|
|
||||||
font-weight: 600;
|
|
||||||
height: 30px;
|
|
||||||
line-height: 30px;
|
|
||||||
margin: 45px 0px 0px 45px;
|
|
||||||
padding: 0px 8px 0px 8px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<body>
|
|
||||||
<h1>Successfully updated your password!</h1>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<!-- 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'
|
|
||||||
-->
|
|
||||||
<head>
|
|
||||||
<title>Email Verification</title>
|
|
||||||
<style type='text/css'>
|
|
||||||
h1 {
|
|
||||||
color: #0067AB;
|
|
||||||
display: block;
|
|
||||||
font: inherit;
|
|
||||||
font-family: 'Open Sans', 'Helvetica Neue', Helvetica;
|
|
||||||
font-size: 30px;
|
|
||||||
font-weight: 600;
|
|
||||||
height: 30px;
|
|
||||||
line-height: 30px;
|
|
||||||
margin: 45px 0px 0px 45px;
|
|
||||||
padding: 0px 8px 0px 8px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<body>
|
|
||||||
<h1>Successfully verified your email!</h1>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -40,12 +40,8 @@ describe('Email Verification Token Expiration:', () => {
|
|||||||
url: sendEmailOptions.link,
|
url: sendEmailOptions.link,
|
||||||
followRedirects: false,
|
followRedirects: false,
|
||||||
});
|
});
|
||||||
expect(response.status).toEqual(302);
|
expect(response.status).toEqual(200);
|
||||||
const url = new URL(sendEmailOptions.link);
|
expect(response.text).toContain('Invalid verification link!');
|
||||||
const token = url.searchParams.get('token');
|
|
||||||
expect(response.text).toEqual(
|
|
||||||
`Found. Redirecting to http://localhost:8378/1/apps/invalid_verification_link.html?appId=test&token=${token}`
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('emailVerified should set to false, if the user does not verify their email before the email verify token expires', async () => {
|
it('emailVerified should set to false, if the user does not verify their email before the email verify token expires', async () => {
|
||||||
@@ -81,7 +77,7 @@ describe('Email Verification Token Expiration:', () => {
|
|||||||
url: sendEmailOptions.link,
|
url: sendEmailOptions.link,
|
||||||
followRedirects: false,
|
followRedirects: false,
|
||||||
});
|
});
|
||||||
expect(response.status).toEqual(302);
|
expect(response.status).toEqual(200);
|
||||||
await user.fetch();
|
await user.fetch();
|
||||||
expect(user.get('emailVerified')).toEqual(false);
|
expect(user.get('emailVerified')).toEqual(false);
|
||||||
});
|
});
|
||||||
@@ -114,10 +110,8 @@ describe('Email Verification Token Expiration:', () => {
|
|||||||
url: sendEmailOptions.link,
|
url: sendEmailOptions.link,
|
||||||
followRedirects: false,
|
followRedirects: false,
|
||||||
});
|
});
|
||||||
expect(response.status).toEqual(302);
|
expect(response.status).toEqual(200);
|
||||||
expect(response.text).toEqual(
|
expect(response.text).toContain('Email verified!');
|
||||||
'Found. Redirecting to http://localhost:8378/1/apps/verify_email_success.html'
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it_id('94956799-c85e-4297-b879-e2d1f985394c')(it)('if user clicks on the email verify link before email verification token expiration then emailVerified should be true', async () => {
|
it_id('94956799-c85e-4297-b879-e2d1f985394c')(it)('if user clicks on the email verify link before email verification token expiration then emailVerified should be true', async () => {
|
||||||
@@ -148,7 +142,7 @@ describe('Email Verification Token Expiration:', () => {
|
|||||||
url: sendEmailOptions.link,
|
url: sendEmailOptions.link,
|
||||||
followRedirects: false,
|
followRedirects: false,
|
||||||
});
|
});
|
||||||
expect(response.status).toEqual(302);
|
expect(response.status).toEqual(200);
|
||||||
await user.fetch();
|
await user.fetch();
|
||||||
expect(user.get('emailVerified')).toEqual(true);
|
expect(user.get('emailVerified')).toEqual(true);
|
||||||
});
|
});
|
||||||
@@ -181,7 +175,7 @@ describe('Email Verification Token Expiration:', () => {
|
|||||||
url: sendEmailOptions.link,
|
url: sendEmailOptions.link,
|
||||||
followRedirects: false,
|
followRedirects: false,
|
||||||
});
|
});
|
||||||
expect(response.status).toEqual(302);
|
expect(response.status).toEqual(200);
|
||||||
const verifiedUser = await Parse.User.logIn('testEmailVerifyTokenValidity', 'expiringToken');
|
const verifiedUser = await Parse.User.logIn('testEmailVerifyTokenValidity', 'expiringToken');
|
||||||
expect(typeof verifiedUser).toBe('object');
|
expect(typeof verifiedUser).toBe('object');
|
||||||
expect(verifiedUser.get('emailVerified')).toBe(true);
|
expect(verifiedUser.get('emailVerified')).toBe(true);
|
||||||
@@ -268,9 +262,7 @@ describe('Email Verification Token Expiration:', () => {
|
|||||||
url: `http://localhost:8378/1/apps/test/verify_email?token=${token}`,
|
url: `http://localhost:8378/1/apps/test/verify_email?token=${token}`,
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
});
|
});
|
||||||
expect(res.text).toEqual(
|
expect(res.text).toContain('Invalid verification link!');
|
||||||
`Found. Redirecting to http://localhost:8378/1/apps/invalid_verification_link.html?appId=test&token=${token}`
|
|
||||||
);
|
|
||||||
|
|
||||||
const formUrl = `http://localhost:8378/1/apps/test/resend_verification_email`;
|
const formUrl = `http://localhost:8378/1/apps/test/resend_verification_email`;
|
||||||
const formResponse = await request({
|
const formResponse = await request({
|
||||||
@@ -282,9 +274,7 @@ describe('Email Verification Token Expiration:', () => {
|
|||||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
followRedirects: false,
|
followRedirects: false,
|
||||||
});
|
});
|
||||||
expect(formResponse.text).toEqual(
|
expect(formResponse.text).toContain('email_verification_send_success.html');
|
||||||
`Found. Redirecting to http://localhost:8378/1/apps/link_send_success.html`
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it_id('9365c53c-b8b4-41f7-a3c1-77882f76a89c')(it)('can conditionally send emails', async () => {
|
it_id('9365c53c-b8b4-41f7-a3c1-77882f76a89c')(it)('can conditionally send emails', async () => {
|
||||||
@@ -493,7 +483,7 @@ describe('Email Verification Token Expiration:', () => {
|
|||||||
url: sendEmailOptions.link,
|
url: sendEmailOptions.link,
|
||||||
followRedirects: false,
|
followRedirects: false,
|
||||||
});
|
});
|
||||||
expect(response.status).toEqual(302);
|
expect(response.status).toEqual(200);
|
||||||
const config = Config.get('test');
|
const config = Config.get('test');
|
||||||
const results = await config.database.find('_User', {
|
const results = await config.database.find('_User', {
|
||||||
username: 'unsets_email_verify_token_expires_at',
|
username: 'unsets_email_verify_token_expires_at',
|
||||||
@@ -536,7 +526,7 @@ describe('Email Verification Token Expiration:', () => {
|
|||||||
url: sendEmailOptions.link,
|
url: sendEmailOptions.link,
|
||||||
followRedirects: false,
|
followRedirects: false,
|
||||||
});
|
});
|
||||||
expect(response.status).toEqual(302);
|
expect(response.status).toEqual(200);
|
||||||
const config = Config.get('test');
|
const config = Config.get('test');
|
||||||
const results = await config.database.find('_User', {
|
const results = await config.database.find('_User', {
|
||||||
username: 'unsets_email_verify_token_expires_at',
|
username: 'unsets_email_verify_token_expires_at',
|
||||||
@@ -580,7 +570,7 @@ describe('Email Verification Token Expiration:', () => {
|
|||||||
url: sendEmailOptions.link,
|
url: sendEmailOptions.link,
|
||||||
followRedirects: false,
|
followRedirects: false,
|
||||||
});
|
});
|
||||||
expect(response.status).toEqual(302);
|
expect(response.status).toEqual(200);
|
||||||
await user.fetch();
|
await user.fetch();
|
||||||
expect(user.get('emailVerified')).toEqual(true);
|
expect(user.get('emailVerified')).toEqual(true);
|
||||||
// RECONFIGURE the server i.e., ENABLE the expire email verify token flag
|
// RECONFIGURE the server i.e., ENABLE the expire email verify token flag
|
||||||
@@ -591,12 +581,8 @@ describe('Email Verification Token Expiration:', () => {
|
|||||||
url: sendEmailOptions.link,
|
url: sendEmailOptions.link,
|
||||||
followRedirects: false,
|
followRedirects: false,
|
||||||
});
|
});
|
||||||
expect(response.status).toEqual(302);
|
expect(response.status).toEqual(200);
|
||||||
const url = new URL(sendEmailOptions.link);
|
expect(response.text).toContain('Invalid verification link!');
|
||||||
const token = url.searchParams.get('token');
|
|
||||||
expect(response.text).toEqual(
|
|
||||||
`Found. Redirecting to http://localhost:8378/1/apps/invalid_verification_link.html?appId=test&token=${token}`
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('clicking on the email verify link by an email UNVERIFIED user that was setup before enabling the expire email verify token should show invalid verficiation link page', async () => {
|
it('clicking on the email verify link by an email UNVERIFIED user that was setup before enabling the expire email verify token should show invalid verficiation link page', async () => {
|
||||||
@@ -637,12 +623,8 @@ describe('Email Verification Token Expiration:', () => {
|
|||||||
url: sendEmailOptions.link,
|
url: sendEmailOptions.link,
|
||||||
followRedirects: false,
|
followRedirects: false,
|
||||||
});
|
});
|
||||||
expect(response.status).toEqual(302);
|
expect(response.status).toEqual(200);
|
||||||
const url = new URL(sendEmailOptions.link);
|
expect(response.text).toContain('Invalid verification link!');
|
||||||
const token = url.searchParams.get('token');
|
|
||||||
expect(response.text).toEqual(
|
|
||||||
`Found. Redirecting to http://localhost:8378/1/apps/invalid_verification_link.html?appId=test&token=${token}`
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it_id('b6c87f35-d887-477d-bc86-a9217a424f53')(it)('setting the email on the user should set a new email verification token and new expiration date for the token when expire email verify token flag is set', async () => {
|
it_id('b6c87f35-d887-477d-bc86-a9217a424f53')(it)('setting the email on the user should set a new email verification token and new expiration date for the token when expire email verify token flag is set', async () => {
|
||||||
@@ -958,7 +940,7 @@ describe('Email Verification Token Expiration:', () => {
|
|||||||
url: sendEmailOptions.link,
|
url: sendEmailOptions.link,
|
||||||
followRedirects: false,
|
followRedirects: false,
|
||||||
});
|
});
|
||||||
expect(response.status).toEqual(302);
|
expect(response.status).toEqual(200);
|
||||||
expect(sendVerificationEmailCallCount).toBe(1);
|
expect(sendVerificationEmailCallCount).toBe(1);
|
||||||
|
|
||||||
response = await request({
|
response = await request({
|
||||||
@@ -1143,7 +1125,7 @@ describe('Email Verification Token Expiration:', () => {
|
|||||||
url: sendEmailOptions.link,
|
url: sendEmailOptions.link,
|
||||||
followRedirects: false,
|
followRedirects: false,
|
||||||
});
|
});
|
||||||
expect(response.status).toEqual(302);
|
expect(response.status).toEqual(200);
|
||||||
user = await Parse.User.logIn('testEmailVerifyTokenValidity', 'expiringToken');
|
user = await Parse.User.logIn('testEmailVerifyTokenValidity', 'expiringToken');
|
||||||
expect(typeof user).toBe('object');
|
expect(typeof user).toBe('object');
|
||||||
expect(user.get('emailVerified')).toBe(true);
|
expect(user.get('emailVerified')).toBe(true);
|
||||||
@@ -1159,6 +1141,6 @@ describe('Email Verification Token Expiration:', () => {
|
|||||||
url: sendEmailOptions.link,
|
url: sendEmailOptions.link,
|
||||||
followRedirects: false,
|
followRedirects: false,
|
||||||
});
|
});
|
||||||
expect(response.status).toEqual(302);
|
expect(response.status).toEqual(200);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -46,10 +46,8 @@ describe('Password Policy: ', () => {
|
|||||||
resolveWithFullResponse: true,
|
resolveWithFullResponse: true,
|
||||||
})
|
})
|
||||||
.then(response => {
|
.then(response => {
|
||||||
expect(response.status).toEqual(302);
|
expect(response.status).toEqual(200);
|
||||||
expect(response.text).toEqual(
|
expect(response.text).toContain('Invalid password reset link!');
|
||||||
'Found. Redirecting to http://localhost:8378/1/apps/invalid_link.html'
|
|
||||||
);
|
|
||||||
done();
|
done();
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
@@ -106,9 +104,8 @@ describe('Password Policy: ', () => {
|
|||||||
followRedirects: false,
|
followRedirects: false,
|
||||||
})
|
})
|
||||||
.then(response => {
|
.then(response => {
|
||||||
expect(response.status).toEqual(302);
|
expect(response.status).toEqual(200);
|
||||||
const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=[a-zA-Z0-9]+\&id=test\&/;
|
expect(response.text).toContain('password');
|
||||||
expect(response.text.match(re)).not.toBe(null);
|
|
||||||
done();
|
done();
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
@@ -621,8 +618,8 @@ describe('Password Policy: ', () => {
|
|||||||
resolveWithFullResponse: true,
|
resolveWithFullResponse: true,
|
||||||
})
|
})
|
||||||
.then(response => {
|
.then(response => {
|
||||||
expect(response.status).toEqual(302);
|
expect(response.status).toEqual(200);
|
||||||
const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&/;
|
const re = /name="token"[^>]*value="([^"]+)"/;
|
||||||
const match = response.text.match(re);
|
const match = response.text.match(re);
|
||||||
if (!match) {
|
if (!match) {
|
||||||
fail('should have a token');
|
fail('should have a token');
|
||||||
@@ -643,10 +640,8 @@ describe('Password Policy: ', () => {
|
|||||||
resolveWithFullResponse: true,
|
resolveWithFullResponse: true,
|
||||||
})
|
})
|
||||||
.then(response => {
|
.then(response => {
|
||||||
expect(response.status).toEqual(302);
|
expect(response.status).toEqual(200);
|
||||||
expect(response.text).toEqual(
|
expect(response.text).toContain('Success!');
|
||||||
'Found. Redirecting to http://localhost:8378/1/apps/password_reset_success.html'
|
|
||||||
);
|
|
||||||
|
|
||||||
Parse.User.logIn('user1', 'has2init')
|
Parse.User.logIn('user1', 'has2init')
|
||||||
.then(function () {
|
.then(function () {
|
||||||
@@ -713,8 +708,8 @@ describe('Password Policy: ', () => {
|
|||||||
resolveWithFullResponse: true,
|
resolveWithFullResponse: true,
|
||||||
})
|
})
|
||||||
.then(response => {
|
.then(response => {
|
||||||
expect(response.status).toEqual(302);
|
expect(response.status).toEqual(200);
|
||||||
const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&/;
|
const re = /name="token"[^>]*value="([^"]+)"/;
|
||||||
const match = response.text.match(re);
|
const match = response.text.match(re);
|
||||||
if (!match) {
|
if (!match) {
|
||||||
fail('should have a token');
|
fail('should have a token');
|
||||||
@@ -735,10 +730,8 @@ describe('Password Policy: ', () => {
|
|||||||
resolveWithFullResponse: true,
|
resolveWithFullResponse: true,
|
||||||
})
|
})
|
||||||
.then(response => {
|
.then(response => {
|
||||||
expect(response.status).toEqual(302);
|
expect(response.status).toEqual(200);
|
||||||
expect(response.text).toEqual(
|
expect(response.text).toContain('Password should contain at least one digit.');
|
||||||
`Found. Redirecting to http://localhost:8378/1/apps/choose_password?token=${token}&id=test&error=Password%20should%20contain%20at%20least%20one%20digit.&app=passwordPolicy`
|
|
||||||
);
|
|
||||||
|
|
||||||
Parse.User.logIn('user1', 'has 1 digit')
|
Parse.User.logIn('user1', 'has 1 digit')
|
||||||
.then(function () {
|
.then(function () {
|
||||||
@@ -899,8 +892,8 @@ describe('Password Policy: ', () => {
|
|||||||
resolveWithFullResponse: true,
|
resolveWithFullResponse: true,
|
||||||
})
|
})
|
||||||
.then(response => {
|
.then(response => {
|
||||||
expect(response.status).toEqual(302);
|
expect(response.status).toEqual(200);
|
||||||
const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&/;
|
const re = /name="token"[^>]*value="([^"]+)"/;
|
||||||
const match = response.text.match(re);
|
const match = response.text.match(re);
|
||||||
if (!match) {
|
if (!match) {
|
||||||
fail('should have a token');
|
fail('should have a token');
|
||||||
@@ -921,10 +914,8 @@ describe('Password Policy: ', () => {
|
|||||||
resolveWithFullResponse: true,
|
resolveWithFullResponse: true,
|
||||||
})
|
})
|
||||||
.then(response => {
|
.then(response => {
|
||||||
expect(response.status).toEqual(302);
|
expect(response.status).toEqual(200);
|
||||||
expect(response.text).toEqual(
|
expect(response.text).toContain('Password cannot contain your username.');
|
||||||
`Found. Redirecting to http://localhost:8378/1/apps/choose_password?token=${token}&id=test&error=Password%20cannot%20contain%20your%20username.&app=passwordPolicy`
|
|
||||||
);
|
|
||||||
|
|
||||||
Parse.User.logIn('user1', 'r@nd0m')
|
Parse.User.logIn('user1', 'r@nd0m')
|
||||||
.then(function () {
|
.then(function () {
|
||||||
@@ -990,8 +981,8 @@ describe('Password Policy: ', () => {
|
|||||||
simple: false,
|
simple: false,
|
||||||
resolveWithFullResponse: true,
|
resolveWithFullResponse: true,
|
||||||
});
|
});
|
||||||
expect(response.status).toEqual(302);
|
expect(response.status).toEqual(200);
|
||||||
const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&/;
|
const re = /name="token"[^>]*value="([^"]+)"/;
|
||||||
const match = response.text.match(re);
|
const match = response.text.match(re);
|
||||||
if (!match) {
|
if (!match) {
|
||||||
fail('should have a token');
|
fail('should have a token');
|
||||||
@@ -1050,8 +1041,8 @@ describe('Password Policy: ', () => {
|
|||||||
resolveWithFullResponse: true,
|
resolveWithFullResponse: true,
|
||||||
})
|
})
|
||||||
.then(response => {
|
.then(response => {
|
||||||
expect(response.status).toEqual(302);
|
expect(response.status).toEqual(200);
|
||||||
const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&/;
|
const re = /name="token"[^>]*value="([^"]+)"/;
|
||||||
const match = response.text.match(re);
|
const match = response.text.match(re);
|
||||||
if (!match) {
|
if (!match) {
|
||||||
fail('should have a token');
|
fail('should have a token');
|
||||||
@@ -1072,10 +1063,8 @@ describe('Password Policy: ', () => {
|
|||||||
resolveWithFullResponse: true,
|
resolveWithFullResponse: true,
|
||||||
})
|
})
|
||||||
.then(response => {
|
.then(response => {
|
||||||
expect(response.status).toEqual(302);
|
expect(response.status).toEqual(200);
|
||||||
expect(response.text).toEqual(
|
expect(response.text).toContain('Success!');
|
||||||
'Found. Redirecting to http://localhost:8378/1/apps/password_reset_success.html'
|
|
||||||
);
|
|
||||||
|
|
||||||
Parse.User.logIn('user1', 'uuser11')
|
Parse.User.logIn('user1', 'uuser11')
|
||||||
.then(function () {
|
.then(function () {
|
||||||
@@ -1316,8 +1305,8 @@ describe('Password Policy: ', () => {
|
|||||||
resolveWithFullResponse: true,
|
resolveWithFullResponse: true,
|
||||||
})
|
})
|
||||||
.then(response => {
|
.then(response => {
|
||||||
expect(response.status).toEqual(302);
|
expect(response.status).toEqual(200);
|
||||||
const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&/;
|
const re = /name="token"[^>]*value="([^"]+)"/;
|
||||||
const match = response.text.match(re);
|
const match = response.text.match(re);
|
||||||
if (!match) {
|
if (!match) {
|
||||||
fail('should have a token');
|
fail('should have a token');
|
||||||
@@ -1338,10 +1327,8 @@ describe('Password Policy: ', () => {
|
|||||||
resolveWithFullResponse: true,
|
resolveWithFullResponse: true,
|
||||||
})
|
})
|
||||||
.then(response => {
|
.then(response => {
|
||||||
expect(response.status).toEqual(302);
|
expect(response.status).toEqual(200);
|
||||||
expect(response.text).toEqual(
|
expect(response.text).toContain('Success!');
|
||||||
'Found. Redirecting to http://localhost:8378/1/apps/password_reset_success.html'
|
|
||||||
);
|
|
||||||
|
|
||||||
Parse.User.logIn('user1', 'uuser11')
|
Parse.User.logIn('user1', 'uuser11')
|
||||||
.then(function () {
|
.then(function () {
|
||||||
@@ -1471,8 +1458,8 @@ describe('Password Policy: ', () => {
|
|||||||
followRedirects: false,
|
followRedirects: false,
|
||||||
})
|
})
|
||||||
.then(response => {
|
.then(response => {
|
||||||
expect(response.status).toEqual(302);
|
expect(response.status).toEqual(200);
|
||||||
const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&/;
|
const re = /name="token"[^>]*value="([^"]+)"/;
|
||||||
const match = response.text.match(re);
|
const match = response.text.match(re);
|
||||||
if (!match) {
|
if (!match) {
|
||||||
fail('should have a token');
|
fail('should have a token');
|
||||||
@@ -1498,10 +1485,8 @@ describe('Password Policy: ', () => {
|
|||||||
.then(data => {
|
.then(data => {
|
||||||
const response = data[0];
|
const response = data[0];
|
||||||
const token = data[1];
|
const token = data[1];
|
||||||
expect(response.status).toEqual(302);
|
expect(response.status).toEqual(200);
|
||||||
expect(response.text).toEqual(
|
expect(response.text).toContain('New password should not be the same as last 1 passwords.');
|
||||||
`Found. Redirecting to http://localhost:8378/1/apps/choose_password?token=${token}&id=test&error=New%20password%20should%20not%20be%20the%20same%20as%20last%201%20passwords.&app=passwordPolicy`
|
|
||||||
);
|
|
||||||
done();
|
done();
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,162 +0,0 @@
|
|||||||
const req = require('../lib/request');
|
|
||||||
|
|
||||||
const request = function (url, callback) {
|
|
||||||
return req({
|
|
||||||
url,
|
|
||||||
}).then(
|
|
||||||
response => callback(null, response),
|
|
||||||
err => callback(err, err)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('public API', () => {
|
|
||||||
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=`,
|
|
||||||
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 () => {
|
|
||||||
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=&token=132414`,
|
|
||||||
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 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 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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
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 password_reset_success.html', done => {
|
|
||||||
request('http://localhost:8378/1/apps/password_reset_success.html', (err, httpResponse) => {
|
|
||||||
expect(httpResponse.status).toBe(200);
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('public API without publicServerURL', () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
await reconfigureServer({ appName: 'unused' });
|
|
||||||
});
|
|
||||||
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', 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', 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', done => {
|
|
||||||
request('http://localhost:8378/1/apps/invalid/verify_email', (err, httpResponse) => {
|
|
||||||
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) => {
|
|
||||||
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) => {
|
|
||||||
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 => {
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -147,8 +147,8 @@ describe('Regex Vulnerabilities', () => {
|
|||||||
url: `${serverURL}/apps/test/request_password_reset?token[$regex]=`,
|
url: `${serverURL}/apps/test/request_password_reset?token[$regex]=`,
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
});
|
});
|
||||||
expect(passwordResetResponse.status).toEqual(302);
|
expect(passwordResetResponse.status).toEqual(200);
|
||||||
expect(passwordResetResponse.headers.location).toMatch(`\\/invalid\\_link\\.html`);
|
expect(passwordResetResponse.text).toContain('Invalid password reset link!');
|
||||||
await request({
|
await request({
|
||||||
url: `${serverURL}/apps/test/request_password_reset`,
|
url: `${serverURL}/apps/test/request_password_reset`,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -195,10 +195,8 @@ describe('Regex Vulnerabilities', () => {
|
|||||||
url: `${serverURL}/apps/test/request_password_reset?token=${token}`,
|
url: `${serverURL}/apps/test/request_password_reset?token=${token}`,
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
});
|
});
|
||||||
expect(passwordResetResponse.status).toEqual(302);
|
expect(passwordResetResponse.status).toEqual(200);
|
||||||
expect(passwordResetResponse.headers.location).toMatch(
|
expect(passwordResetResponse.text).toContain('Reset Your Password');
|
||||||
`\\/choose\\_password\\?token\\=${token}\\&`
|
|
||||||
);
|
|
||||||
await request({
|
await request({
|
||||||
url: `${serverURL}/apps/test/request_password_reset`,
|
url: `${serverURL}/apps/test/request_password_reset`,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|||||||
@@ -333,10 +333,8 @@ describe('Custom Pages, Email Verification, Password Reset', () => {
|
|||||||
url: sendEmailOptions.link,
|
url: sendEmailOptions.link,
|
||||||
followRedirects: false,
|
followRedirects: false,
|
||||||
});
|
});
|
||||||
expect(response.status).toEqual(302);
|
expect(response.status).toEqual(200);
|
||||||
expect(response.text).toEqual(
|
expect(response.text).toContain('Email verified!');
|
||||||
'Found. Redirecting to http://localhost:8378/1/apps/verify_email_success.html'
|
|
||||||
);
|
|
||||||
user = await new Parse.Query(Parse.User).first({ useMasterKey: true });
|
user = await new Parse.Query(Parse.User).first({ useMasterKey: true });
|
||||||
expect(user.get('emailVerified')).toEqual(true);
|
expect(user.get('emailVerified')).toEqual(true);
|
||||||
user = await Parse.User.logIn('user', 'other-password');
|
user = await Parse.User.logIn('user', 'other-password');
|
||||||
@@ -674,10 +672,8 @@ describe('Custom Pages, Email Verification, Password Reset', () => {
|
|||||||
url: sendEmailOptions.link,
|
url: sendEmailOptions.link,
|
||||||
followRedirects: false,
|
followRedirects: false,
|
||||||
}).then(response => {
|
}).then(response => {
|
||||||
expect(response.status).toEqual(302);
|
expect(response.status).toEqual(200);
|
||||||
expect(response.text).toEqual(
|
expect(response.text).toContain('Email verified!');
|
||||||
'Found. Redirecting to http://localhost:8378/1/apps/verify_email_success.html'
|
|
||||||
);
|
|
||||||
user
|
user
|
||||||
.fetch()
|
.fetch()
|
||||||
.then(
|
.then(
|
||||||
@@ -714,10 +710,8 @@ describe('Custom Pages, Email Verification, Password Reset', () => {
|
|||||||
url: 'http://localhost:8378/1/apps/test/verify_email',
|
url: 'http://localhost:8378/1/apps/test/verify_email',
|
||||||
followRedirects: false,
|
followRedirects: false,
|
||||||
}).then(response => {
|
}).then(response => {
|
||||||
expect(response.status).toEqual(302);
|
expect(response.status).toEqual(200);
|
||||||
expect(response.text).toEqual(
|
expect(response.text).toContain('Invalid verification link!');
|
||||||
'Found. Redirecting to http://localhost:8378/1/apps/invalid_link.html'
|
|
||||||
);
|
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -738,10 +732,8 @@ describe('Custom Pages, Email Verification, Password Reset', () => {
|
|||||||
url: 'http://localhost:8378/1/apps/test/verify_email?token=asdfasdf',
|
url: 'http://localhost:8378/1/apps/test/verify_email?token=asdfasdf',
|
||||||
followRedirects: false,
|
followRedirects: false,
|
||||||
}).then(response => {
|
}).then(response => {
|
||||||
expect(response.status).toEqual(302);
|
expect(response.status).toEqual(200);
|
||||||
expect(response.text).toEqual(
|
expect(response.text).toContain('Invalid verification link!');
|
||||||
'Found. Redirecting to http://localhost:8378/1/apps/invalid_verification_link.html?appId=test&token=asdfasdf'
|
|
||||||
);
|
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -766,10 +758,8 @@ describe('Custom Pages, Email Verification, Password Reset', () => {
|
|||||||
username: 'sadfasga',
|
username: 'sadfasga',
|
||||||
},
|
},
|
||||||
}).then(response => {
|
}).then(response => {
|
||||||
expect(response.status).toEqual(302);
|
expect(response.status).toEqual(303);
|
||||||
expect(response.text).toEqual(
|
expect(response.text).toContain('email_verification_send_fail.html');
|
||||||
'Found. Redirecting to http://localhost:8378/1/apps/link_send_fail.html'
|
|
||||||
);
|
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -783,10 +773,8 @@ describe('Custom Pages, Email Verification, Password Reset', () => {
|
|||||||
url: 'http://localhost:8378/1/apps/test/verify_email?token=invalid',
|
url: 'http://localhost:8378/1/apps/test/verify_email?token=invalid',
|
||||||
followRedirects: false,
|
followRedirects: false,
|
||||||
}).then(response => {
|
}).then(response => {
|
||||||
expect(response.status).toEqual(302);
|
expect(response.status).toEqual(200);
|
||||||
expect(response.text).toEqual(
|
expect(response.text).toContain('Invalid verification link!');
|
||||||
'Found. Redirecting to http://localhost:8378/1/apps/invalid_verification_link.html?appId=test&token=invalid'
|
|
||||||
);
|
|
||||||
user.fetch().then(() => {
|
user.fetch().then(() => {
|
||||||
expect(user.get('emailVerified')).toEqual(false);
|
expect(user.get('emailVerified')).toEqual(false);
|
||||||
done();
|
done();
|
||||||
@@ -824,8 +812,8 @@ describe('Custom Pages, Email Verification, Password Reset', () => {
|
|||||||
url: options.link,
|
url: options.link,
|
||||||
followRedirects: false,
|
followRedirects: false,
|
||||||
}).then(response => {
|
}).then(response => {
|
||||||
expect(response.status).toEqual(302);
|
expect(response.status).toEqual(200);
|
||||||
const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=[a-zA-Z0-9]+\&id=test\&/;
|
const re = /name="token"[^>]*value="([^"]+)"/;
|
||||||
expect(response.text.match(re)).not.toBe(null);
|
expect(response.text.match(re)).not.toBe(null);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
@@ -868,10 +856,8 @@ describe('Custom Pages, Email Verification, Password Reset', () => {
|
|||||||
url: 'http://localhost:8378/1/apps/test/request_password_reset?token=asdfasdf',
|
url: 'http://localhost:8378/1/apps/test/request_password_reset?token=asdfasdf',
|
||||||
followRedirects: false,
|
followRedirects: false,
|
||||||
}).then(response => {
|
}).then(response => {
|
||||||
expect(response.status).toEqual(302);
|
expect(response.status).toEqual(200);
|
||||||
expect(response.text).toEqual(
|
expect(response.text).toContain('Invalid password reset link!');
|
||||||
'Found. Redirecting to http://localhost:8378/1/apps/invalid_link.html'
|
|
||||||
);
|
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -886,8 +872,8 @@ describe('Custom Pages, Email Verification, Password Reset', () => {
|
|||||||
url: options.link,
|
url: options.link,
|
||||||
followRedirects: false,
|
followRedirects: false,
|
||||||
}).then(response => {
|
}).then(response => {
|
||||||
expect(response.status).toEqual(302);
|
expect(response.status).toEqual(200);
|
||||||
const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&/;
|
const re = /name="token"[^>]*value="([^"]+)"/;
|
||||||
const match = response.text.match(re);
|
const match = response.text.match(re);
|
||||||
if (!match) {
|
if (!match) {
|
||||||
fail('should have a token');
|
fail('should have a token');
|
||||||
@@ -905,10 +891,8 @@ describe('Custom Pages, Email Verification, Password Reset', () => {
|
|||||||
},
|
},
|
||||||
followRedirects: false,
|
followRedirects: false,
|
||||||
}).then(response => {
|
}).then(response => {
|
||||||
expect(response.status).toEqual(302);
|
expect(response.status).toEqual(200);
|
||||||
expect(response.text).toEqual(
|
expect(response.text).toContain('Success!');
|
||||||
'Found. Redirecting to http://localhost:8378/1/apps/password_reset_success.html'
|
|
||||||
);
|
|
||||||
|
|
||||||
Parse.User.logIn('zxcv', 'hello').then(
|
Parse.User.logIn('zxcv', 'hello').then(
|
||||||
function () {
|
function () {
|
||||||
@@ -963,8 +947,8 @@ describe('Custom Pages, Email Verification, Password Reset', () => {
|
|||||||
url: options.link,
|
url: options.link,
|
||||||
followRedirects: false,
|
followRedirects: false,
|
||||||
}).then(response => {
|
}).then(response => {
|
||||||
expect(response.status).toEqual(302);
|
expect(response.status).toEqual(200);
|
||||||
const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&/;
|
const re = /name="token"[^>]*value="([^"]+)"/;
|
||||||
const match = response.text.match(re);
|
const match = response.text.match(re);
|
||||||
if (!match) {
|
if (!match) {
|
||||||
fail('should have a token');
|
fail('should have a token');
|
||||||
@@ -982,10 +966,8 @@ describe('Custom Pages, Email Verification, Password Reset', () => {
|
|||||||
},
|
},
|
||||||
followRedirects: false,
|
followRedirects: false,
|
||||||
}).then(response => {
|
}).then(response => {
|
||||||
expect(response.status).toEqual(302);
|
expect(response.status).toEqual(200);
|
||||||
expect(response.text).toEqual(
|
expect(response.text).toContain('Success!');
|
||||||
'Found. Redirecting to http://localhost:8378/1/apps/password_reset_success.html'
|
|
||||||
);
|
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1022,8 +1004,8 @@ describe('Custom Pages, Email Verification, Password Reset', () => {
|
|||||||
url: options.link,
|
url: options.link,
|
||||||
followRedirects: false,
|
followRedirects: false,
|
||||||
});
|
});
|
||||||
expect(response.status).toEqual(302);
|
expect(response.status).toEqual(200);
|
||||||
const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&/;
|
const re = /name="token"[^>]*value="([^"]+)"/;
|
||||||
const match = response.text.match(re);
|
const match = response.text.match(re);
|
||||||
if (!match) {
|
if (!match) {
|
||||||
fail('should have a token');
|
fail('should have a token');
|
||||||
@@ -1197,9 +1179,7 @@ describe('Custom Pages, Email Verification, Password Reset', () => {
|
|||||||
new_password: 'newpassword',
|
new_password: 'newpassword',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(res.text).toEqual(
|
expect(res.text).toContain('The password reset link has expired');
|
||||||
`Found. Redirecting to http://localhost:8378/1/apps/choose_password?id=test&error=The%20password%20reset%20link%20has%20expired&app=emailVerifyToken&token=${token}`
|
|
||||||
);
|
|
||||||
|
|
||||||
await request({
|
await request({
|
||||||
url: `http://localhost:8378/1/requestPasswordReset`,
|
url: `http://localhost:8378/1/requestPasswordReset`,
|
||||||
|
|||||||
@@ -828,10 +828,8 @@ export class Config {
|
|||||||
return this.masterKey;
|
return this.masterKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Remove this function once PagesRouter replaces the PublicAPIRouter;
|
|
||||||
// the (default) endpoint has to be defined in PagesRouter only.
|
|
||||||
get pagesEndpoint() {
|
get pagesEndpoint() {
|
||||||
return this.pages && this.pages.enableRouter && this.pages.pagesEndpoint
|
return this.pages && this.pages.pagesEndpoint
|
||||||
? this.pages.pagesEndpoint
|
? this.pages.pagesEndpoint
|
||||||
: 'apps';
|
: 'apps';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ import { InstallationsRouter } from './Routers/InstallationsRouter';
|
|||||||
import { LogsRouter } from './Routers/LogsRouter';
|
import { LogsRouter } from './Routers/LogsRouter';
|
||||||
import { ParseLiveQueryServer } from './LiveQuery/ParseLiveQueryServer';
|
import { ParseLiveQueryServer } from './LiveQuery/ParseLiveQueryServer';
|
||||||
import { PagesRouter } from './Routers/PagesRouter';
|
import { PagesRouter } from './Routers/PagesRouter';
|
||||||
import { PublicAPIRouter } from './Routers/PublicAPIRouter';
|
|
||||||
import { PushRouter } from './Routers/PushRouter';
|
import { PushRouter } from './Routers/PushRouter';
|
||||||
import { CloudCodeRouter } from './Routers/CloudCodeRouter';
|
import { CloudCodeRouter } from './Routers/CloudCodeRouter';
|
||||||
import { RolesRouter } from './Routers/RolesRouter';
|
import { RolesRouter } from './Routers/RolesRouter';
|
||||||
@@ -330,9 +329,7 @@ class ParseServer {
|
|||||||
api.use(
|
api.use(
|
||||||
'/',
|
'/',
|
||||||
express.urlencoded({ extended: false }),
|
express.urlencoded({ extended: false }),
|
||||||
pages.enableRouter
|
new PagesRouter(pages).expressRouter()
|
||||||
? new PagesRouter(pages).expressRouter()
|
|
||||||
: new PublicAPIRouter().expressRouter()
|
|
||||||
);
|
);
|
||||||
|
|
||||||
api.use(express.json({ type: '*/*', limit: maxUploadSize }));
|
api.use(express.json({ type: '*/*', limit: maxUploadSize }));
|
||||||
|
|||||||
@@ -1,332 +0,0 @@
|
|||||||
import PromiseRouter from '../PromiseRouter';
|
|
||||||
import Config from '../Config';
|
|
||||||
import express from 'express';
|
|
||||||
import path from 'path';
|
|
||||||
import fs from 'fs';
|
|
||||||
import qs from 'querystring';
|
|
||||||
import { Parse } from 'parse/node';
|
|
||||||
import Deprecator from '../Deprecator/Deprecator';
|
|
||||||
|
|
||||||
const public_html = path.resolve(__dirname, '../../public_html');
|
|
||||||
const views = path.resolve(__dirname, '../../views');
|
|
||||||
|
|
||||||
export class PublicAPIRouter extends PromiseRouter {
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
Deprecator.logRuntimeDeprecation({
|
|
||||||
usage: 'PublicAPIRouter',
|
|
||||||
solution: 'pages.enableRouter'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
verifyEmail(req) {
|
|
||||||
const { 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) {
|
|
||||||
return this.invalidLink(req);
|
|
||||||
}
|
|
||||||
|
|
||||||
const userController = config.userController;
|
|
||||||
return userController.verifyEmail(token).then(
|
|
||||||
() => {
|
|
||||||
return Promise.resolve({
|
|
||||||
status: 302,
|
|
||||||
location: `${config.verifyEmailSuccessURL}`,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
return this.invalidVerificationLink(req, token);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
resendVerificationEmail(req) {
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
const token = req.body.token;
|
|
||||||
|
|
||||||
if (!username && !token) {
|
|
||||||
return this.invalidLink(req);
|
|
||||||
}
|
|
||||||
|
|
||||||
const userController = config.userController;
|
|
||||||
|
|
||||||
return userController.resendVerificationEmail(username, req, token).then(
|
|
||||||
() => {
|
|
||||||
return Promise.resolve({
|
|
||||||
status: 302,
|
|
||||||
location: `${config.linkSendSuccessURL}`,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
return Promise.resolve({
|
|
||||||
status: 302,
|
|
||||||
location: `${config.linkSendFailURL}`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
|
||||||
const config = req.config;
|
|
||||||
|
|
||||||
if (!config) {
|
|
||||||
this.invalidRequest();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!config.publicServerURL) {
|
|
||||||
return this.missingPublicServerURL();
|
|
||||||
}
|
|
||||||
|
|
||||||
const { token: rawToken } = req.query;
|
|
||||||
const token = rawToken && typeof rawToken !== 'string' ? rawToken.toString() : rawToken;
|
|
||||||
|
|
||||||
if (!token) {
|
|
||||||
return this.invalidLink(req);
|
|
||||||
}
|
|
||||||
|
|
||||||
return config.userController.checkResetTokenValidity(token).then(
|
|
||||||
() => {
|
|
||||||
const params = qs.stringify({
|
|
||||||
token,
|
|
||||||
id: config.applicationId,
|
|
||||||
app: config.appName,
|
|
||||||
});
|
|
||||||
return Promise.resolve({
|
|
||||||
status: 302,
|
|
||||||
location: `${config.choosePasswordURL}?${params}`,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
return this.invalidLink(req);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
resetPassword(req) {
|
|
||||||
const config = req.config;
|
|
||||||
|
|
||||||
if (!config) {
|
|
||||||
this.invalidRequest();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!config.publicServerURL) {
|
|
||||||
return this.missingPublicServerURL();
|
|
||||||
}
|
|
||||||
|
|
||||||
const { new_password, token: rawToken } = req.body || {};
|
|
||||||
const token = rawToken && typeof rawToken !== 'string' ? rawToken.toString() : rawToken;
|
|
||||||
|
|
||||||
if ((!token || !new_password) && req.xhr === false) {
|
|
||||||
return this.invalidLink(req);
|
|
||||||
}
|
|
||||||
|
|
||||||
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(token, new_password)
|
|
||||||
.then(
|
|
||||||
() => {
|
|
||||||
return Promise.resolve({
|
|
||||||
success: true,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
err => {
|
|
||||||
return Promise.resolve({
|
|
||||||
success: false,
|
|
||||||
err,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.then(result => {
|
|
||||||
const queryString = {
|
|
||||||
token: token,
|
|
||||||
id: config.applicationId,
|
|
||||||
error: result.err,
|
|
||||||
app: config.appName,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (result?.err === 'The password reset link has expired') {
|
|
||||||
delete queryString.token;
|
|
||||||
queryString.token = token;
|
|
||||||
}
|
|
||||||
const params = qs.stringify(queryString);
|
|
||||||
|
|
||||||
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 location = result.success
|
|
||||||
? `${config.passwordResetSuccessURL}`
|
|
||||||
: `${config.choosePasswordURL}?${params}`;
|
|
||||||
|
|
||||||
return Promise.resolve({
|
|
||||||
status: 302,
|
|
||||||
location,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
invalidLink(req) {
|
|
||||||
return Promise.resolve({
|
|
||||||
status: 302,
|
|
||||||
location: req.config.invalidLinkURL,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
invalidVerificationLink(req, token) {
|
|
||||||
const config = req.config;
|
|
||||||
if (req.params.appId) {
|
|
||||||
const params = qs.stringify({
|
|
||||||
appId: req.params.appId,
|
|
||||||
token,
|
|
||||||
});
|
|
||||||
return Promise.resolve({
|
|
||||||
status: 302,
|
|
||||||
location: `${config.invalidVerificationLinkURL}?${params}`,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
return this.invalidLink(req);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
missingPublicServerURL() {
|
|
||||||
return Promise.resolve({
|
|
||||||
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);
|
|
||||||
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 => {
|
|
||||||
return this.changePassword(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(public_html));
|
|
||||||
router.use('/', super.expressRouter());
|
|
||||||
return router;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default PublicAPIRouter;
|
|
||||||
Reference in New Issue
Block a user