BREAKING CHANGE: This upgrades the internally used Express framework from version 4 to 5, which may be a breaking change. If Parse Server is set up to be mounted on an Express application, we recommend to also use version 5 of the Express framework to avoid any compatibility issues. Note that even if there are no issues after upgrading, future releases of Parse Server may introduce issues if Parse Server internally relies on Express 5-specific features which are unsupported by the Express version on which it is mounted. See the Express [migration guide](https://expressjs.com/en/guide/migrating-5.html) and [release announcement](https://expressjs.com/2024/10/15/v5-release.html#breaking-changes) for more info.
333 lines
7.5 KiB
JavaScript
333 lines
7.5 KiB
JavaScript
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;
|