feat: Deprecation DEPPS11: Replace PublicAPIRouter with PagesRouter (#9974)

BREAKING CHANGE: This release replaces `PublicAPIRouter` with `PagesRouter` (Deprecation DEPPS11).
This commit is contained in:
Manuel
2025-12-12 20:55:39 +01:00
committed by GitHub
parent 1d31406233
commit 8f877d42c0
16 changed files with 84 additions and 895 deletions

View File

@@ -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 | - |
| 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 | - |
| 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 | - |
[i_deprecation]: ## "The version and date of the deprecation."

View File

@@ -40,7 +40,7 @@ COPY --from=build /tmp/lib lib
COPY package*.json ./
COPY bin bin
COPY public_html public_html
COPY public public
COPY views views
RUN mkdir -p logs && chown -R node: logs

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -40,12 +40,8 @@ describe('Email Verification Token Expiration:', () => {
url: sendEmailOptions.link,
followRedirects: false,
});
expect(response.status).toEqual(302);
const url = new URL(sendEmailOptions.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}`
);
expect(response.status).toEqual(200);
expect(response.text).toContain('Invalid verification link!');
});
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,
followRedirects: false,
});
expect(response.status).toEqual(302);
expect(response.status).toEqual(200);
await user.fetch();
expect(user.get('emailVerified')).toEqual(false);
});
@@ -114,10 +110,8 @@ describe('Email Verification Token Expiration:', () => {
url: sendEmailOptions.link,
followRedirects: false,
});
expect(response.status).toEqual(302);
expect(response.text).toEqual(
'Found. Redirecting to http://localhost:8378/1/apps/verify_email_success.html'
);
expect(response.status).toEqual(200);
expect(response.text).toContain('Email verified!');
});
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,
followRedirects: false,
});
expect(response.status).toEqual(302);
expect(response.status).toEqual(200);
await user.fetch();
expect(user.get('emailVerified')).toEqual(true);
});
@@ -181,7 +175,7 @@ describe('Email Verification Token Expiration:', () => {
url: sendEmailOptions.link,
followRedirects: false,
});
expect(response.status).toEqual(302);
expect(response.status).toEqual(200);
const verifiedUser = await Parse.User.logIn('testEmailVerifyTokenValidity', 'expiringToken');
expect(typeof verifiedUser).toBe('object');
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}`,
method: 'GET',
});
expect(res.text).toEqual(
`Found. Redirecting to http://localhost:8378/1/apps/invalid_verification_link.html?appId=test&token=${token}`
);
expect(res.text).toContain('Invalid verification link!');
const formUrl = `http://localhost:8378/1/apps/test/resend_verification_email`;
const formResponse = await request({
@@ -282,9 +274,7 @@ describe('Email Verification Token Expiration:', () => {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
followRedirects: false,
});
expect(formResponse.text).toEqual(
`Found. Redirecting to http://localhost:8378/1/apps/link_send_success.html`
);
expect(formResponse.text).toContain('email_verification_send_success.html');
});
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,
followRedirects: false,
});
expect(response.status).toEqual(302);
expect(response.status).toEqual(200);
const config = Config.get('test');
const results = await config.database.find('_User', {
username: 'unsets_email_verify_token_expires_at',
@@ -536,7 +526,7 @@ describe('Email Verification Token Expiration:', () => {
url: sendEmailOptions.link,
followRedirects: false,
});
expect(response.status).toEqual(302);
expect(response.status).toEqual(200);
const config = Config.get('test');
const results = await config.database.find('_User', {
username: 'unsets_email_verify_token_expires_at',
@@ -580,7 +570,7 @@ describe('Email Verification Token Expiration:', () => {
url: sendEmailOptions.link,
followRedirects: false,
});
expect(response.status).toEqual(302);
expect(response.status).toEqual(200);
await user.fetch();
expect(user.get('emailVerified')).toEqual(true);
// RECONFIGURE the server i.e., ENABLE the expire email verify token flag
@@ -591,12 +581,8 @@ describe('Email Verification Token Expiration:', () => {
url: sendEmailOptions.link,
followRedirects: false,
});
expect(response.status).toEqual(302);
const url = new URL(sendEmailOptions.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}`
);
expect(response.status).toEqual(200);
expect(response.text).toContain('Invalid verification link!');
});
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,
followRedirects: false,
});
expect(response.status).toEqual(302);
const url = new URL(sendEmailOptions.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}`
);
expect(response.status).toEqual(200);
expect(response.text).toContain('Invalid verification link!');
});
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,
followRedirects: false,
});
expect(response.status).toEqual(302);
expect(response.status).toEqual(200);
expect(sendVerificationEmailCallCount).toBe(1);
response = await request({
@@ -1143,7 +1125,7 @@ describe('Email Verification Token Expiration:', () => {
url: sendEmailOptions.link,
followRedirects: false,
});
expect(response.status).toEqual(302);
expect(response.status).toEqual(200);
user = await Parse.User.logIn('testEmailVerifyTokenValidity', 'expiringToken');
expect(typeof user).toBe('object');
expect(user.get('emailVerified')).toBe(true);
@@ -1159,6 +1141,6 @@ describe('Email Verification Token Expiration:', () => {
url: sendEmailOptions.link,
followRedirects: false,
});
expect(response.status).toEqual(302);
expect(response.status).toEqual(200);
});
});

View File

@@ -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 password reset link!');
done();
})
.catch(error => {
@@ -106,9 +104,8 @@ 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\&/;
expect(response.text.match(re)).not.toBe(null);
expect(response.status).toEqual(200);
expect(response.text).toContain('password');
done();
})
.catch(error => {
@@ -621,8 +618,8 @@ 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\&/;
expect(response.status).toEqual(200);
const re = /name="token"[^>]*value="([^"]+)"/;
const match = response.text.match(re);
if (!match) {
fail('should have a token');
@@ -643,10 +640,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'
);
expect(response.status).toEqual(200);
expect(response.text).toContain('Success!');
Parse.User.logIn('user1', 'has2init')
.then(function () {
@@ -713,8 +708,8 @@ 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\&/;
expect(response.status).toEqual(200);
const re = /name="token"[^>]*value="([^"]+)"/;
const match = response.text.match(re);
if (!match) {
fail('should have a token');
@@ -735,10 +730,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?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 +892,8 @@ 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\&/;
expect(response.status).toEqual(200);
const re = /name="token"[^>]*value="([^"]+)"/;
const match = response.text.match(re);
if (!match) {
fail('should have a token');
@@ -921,10 +914,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?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 +981,8 @@ 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\&/;
expect(response.status).toEqual(200);
const re = /name="token"[^>]*value="([^"]+)"/;
const match = response.text.match(re);
if (!match) {
fail('should have a token');
@@ -1050,8 +1041,8 @@ 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\&/;
expect(response.status).toEqual(200);
const re = /name="token"[^>]*value="([^"]+)"/;
const match = response.text.match(re);
if (!match) {
fail('should have a token');
@@ -1072,10 +1063,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'
);
expect(response.status).toEqual(200);
expect(response.text).toContain('Success!');
Parse.User.logIn('user1', 'uuser11')
.then(function () {
@@ -1316,8 +1305,8 @@ 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\&/;
expect(response.status).toEqual(200);
const re = /name="token"[^>]*value="([^"]+)"/;
const match = response.text.match(re);
if (!match) {
fail('should have a token');
@@ -1338,10 +1327,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'
);
expect(response.status).toEqual(200);
expect(response.text).toContain('Success!');
Parse.User.logIn('user1', 'uuser11')
.then(function () {
@@ -1471,8 +1458,8 @@ 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\&/;
expect(response.status).toEqual(200);
const re = /name="token"[^>]*value="([^"]+)"/;
const match = response.text.match(re);
if (!match) {
fail('should have a token');
@@ -1498,10 +1485,8 @@ 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?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('New password should not be the same as last 1 passwords.');
done();
return Promise.resolve();
})

View File

@@ -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();
}
);
});
});

View File

@@ -147,8 +147,8 @@ describe('Regex Vulnerabilities', () => {
url: `${serverURL}/apps/test/request_password_reset?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 password reset link!');
await request({
url: `${serverURL}/apps/test/request_password_reset`,
method: 'POST',
@@ -195,10 +195,8 @@ describe('Regex Vulnerabilities', () => {
url: `${serverURL}/apps/test/request_password_reset?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('Reset Your Password');
await request({
url: `${serverURL}/apps/test/request_password_reset`,
method: 'POST',

View File

@@ -333,10 +333,8 @@ describe('Custom Pages, Email Verification, Password Reset', () => {
url: sendEmailOptions.link,
followRedirects: false,
});
expect(response.status).toEqual(302);
expect(response.text).toEqual(
'Found. Redirecting to http://localhost:8378/1/apps/verify_email_success.html'
);
expect(response.status).toEqual(200);
expect(response.text).toContain('Email verified!');
user = await new Parse.Query(Parse.User).first({ useMasterKey: true });
expect(user.get('emailVerified')).toEqual(true);
user = await Parse.User.logIn('user', 'other-password');
@@ -674,10 +672,8 @@ 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'
);
expect(response.status).toEqual(200);
expect(response.text).toContain('Email verified!');
user
.fetch()
.then(
@@ -714,10 +710,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 verification link!');
done();
});
});
@@ -738,10 +732,8 @@ describe('Custom Pages, Email Verification, Password Reset', () => {
url: 'http://localhost:8378/1/apps/test/verify_email?token=asdfasdf',
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?appId=test&token=asdfasdf'
);
expect(response.status).toEqual(200);
expect(response.text).toContain('Invalid verification link!');
done();
});
});
@@ -766,10 +758,8 @@ describe('Custom Pages, Email Verification, Password Reset', () => {
username: 'sadfasga',
},
}).then(response => {
expect(response.status).toEqual(302);
expect(response.text).toEqual(
'Found. Redirecting to http://localhost:8378/1/apps/link_send_fail.html'
);
expect(response.status).toEqual(303);
expect(response.text).toContain('email_verification_send_fail.html');
done();
});
});
@@ -783,10 +773,8 @@ describe('Custom Pages, Email Verification, Password Reset', () => {
url: 'http://localhost:8378/1/apps/test/verify_email?token=invalid',
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?appId=test&token=invalid'
);
expect(response.status).toEqual(200);
expect(response.text).toContain('Invalid verification link!');
user.fetch().then(() => {
expect(user.get('emailVerified')).toEqual(false);
done();
@@ -824,8 +812,8 @@ 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\&/;
expect(response.status).toEqual(200);
const re = /name="token"[^>]*value="([^"]+)"/;
expect(response.text.match(re)).not.toBe(null);
done();
});
@@ -868,10 +856,8 @@ describe('Custom Pages, Email Verification, Password Reset', () => {
url: 'http://localhost:8378/1/apps/test/request_password_reset?token=asdfasdf',
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 password reset link!');
done();
});
});
@@ -886,8 +872,8 @@ 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\&/;
expect(response.status).toEqual(200);
const re = /name="token"[^>]*value="([^"]+)"/;
const match = response.text.match(re);
if (!match) {
fail('should have a token');
@@ -905,10 +891,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'
);
expect(response.status).toEqual(200);
expect(response.text).toContain('Success!');
Parse.User.logIn('zxcv', 'hello').then(
function () {
@@ -963,8 +947,8 @@ 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\&/;
expect(response.status).toEqual(200);
const re = /name="token"[^>]*value="([^"]+)"/;
const match = response.text.match(re);
if (!match) {
fail('should have a token');
@@ -982,10 +966,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'
);
expect(response.status).toEqual(200);
expect(response.text).toContain('Success!');
done();
});
});
@@ -1022,8 +1004,8 @@ 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\&/;
expect(response.status).toEqual(200);
const re = /name="token"[^>]*value="([^"]+)"/;
const match = response.text.match(re);
if (!match) {
fail('should have a token');
@@ -1197,9 +1179,7 @@ describe('Custom Pages, Email Verification, Password Reset', () => {
new_password: 'newpassword',
},
});
expect(res.text).toEqual(
`Found. Redirecting to http://localhost:8378/1/apps/choose_password?id=test&error=The%20password%20reset%20link%20has%20expired&app=emailVerifyToken&token=${token}`
);
expect(res.text).toContain('The password reset link has expired');
await request({
url: `http://localhost:8378/1/requestPasswordReset`,

View File

@@ -828,10 +828,8 @@ export class Config {
return this.masterKey;
}
// TODO: Remove this function once PagesRouter replaces the PublicAPIRouter;
// the (default) endpoint has to be defined in PagesRouter only.
get pagesEndpoint() {
return this.pages && this.pages.enableRouter && this.pages.pagesEndpoint
return this.pages && this.pages.pagesEndpoint
? this.pages.pagesEndpoint
: 'apps';
}

View File

@@ -27,7 +27,6 @@ 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';
import { RolesRouter } from './Routers/RolesRouter';
@@ -330,9 +329,7 @@ class ParseServer {
api.use(
'/',
express.urlencoded({ extended: false }),
pages.enableRouter
? new PagesRouter(pages).expressRouter()
: new PublicAPIRouter().expressRouter()
new PagesRouter(pages).expressRouter()
);
api.use(express.json({ type: '*/*', limit: maxUploadSize }));

View File

@@ -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;