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.
743 lines
22 KiB
JavaScript
743 lines
22 KiB
JavaScript
import PromiseRouter from '../PromiseRouter';
|
|
import Config from '../Config';
|
|
import express from 'express';
|
|
import path from 'path';
|
|
import { promises as fs } from 'fs';
|
|
import { Parse } from 'parse/node';
|
|
import Utils from '../Utils';
|
|
import mustache from 'mustache';
|
|
import Page from '../Page';
|
|
|
|
// All pages with custom page key for reference and file name
|
|
const pages = Object.freeze({
|
|
passwordReset: new Page({ id: 'passwordReset', defaultFile: 'password_reset.html' }),
|
|
passwordResetSuccess: new Page({
|
|
id: 'passwordResetSuccess',
|
|
defaultFile: 'password_reset_success.html',
|
|
}),
|
|
passwordResetLinkInvalid: new Page({
|
|
id: 'passwordResetLinkInvalid',
|
|
defaultFile: 'password_reset_link_invalid.html',
|
|
}),
|
|
emailVerificationSuccess: new Page({
|
|
id: 'emailVerificationSuccess',
|
|
defaultFile: 'email_verification_success.html',
|
|
}),
|
|
emailVerificationSendFail: new Page({
|
|
id: 'emailVerificationSendFail',
|
|
defaultFile: 'email_verification_send_fail.html',
|
|
}),
|
|
emailVerificationSendSuccess: new Page({
|
|
id: 'emailVerificationSendSuccess',
|
|
defaultFile: 'email_verification_send_success.html',
|
|
}),
|
|
emailVerificationLinkInvalid: new Page({
|
|
id: 'emailVerificationLinkInvalid',
|
|
defaultFile: 'email_verification_link_invalid.html',
|
|
}),
|
|
emailVerificationLinkExpired: new Page({
|
|
id: 'emailVerificationLinkExpired',
|
|
defaultFile: 'email_verification_link_expired.html',
|
|
}),
|
|
});
|
|
|
|
// All page parameters for reference to be used as template placeholders or query params
|
|
const pageParams = Object.freeze({
|
|
appName: 'appName',
|
|
appId: 'appId',
|
|
token: 'token',
|
|
username: 'username',
|
|
error: 'error',
|
|
locale: 'locale',
|
|
publicServerUrl: 'publicServerUrl',
|
|
});
|
|
|
|
// The header prefix to add page params as response headers
|
|
const pageParamHeaderPrefix = 'x-parse-page-param-';
|
|
|
|
// The errors being thrown
|
|
const errors = Object.freeze({
|
|
jsonFailedFileLoading: 'failed to load JSON file',
|
|
fileOutsideAllowedScope: 'not allowed to read file outside of pages directory',
|
|
});
|
|
|
|
export class PagesRouter extends PromiseRouter {
|
|
/**
|
|
* Constructs a PagesRouter.
|
|
* @param {Object} pages The pages options from the Parse Server configuration.
|
|
*/
|
|
constructor(pages = {}) {
|
|
super();
|
|
|
|
// Set instance properties
|
|
this.pagesConfig = pages;
|
|
this.pagesEndpoint = pages.pagesEndpoint ? pages.pagesEndpoint : 'apps';
|
|
this.pagesPath = pages.pagesPath
|
|
? path.resolve('./', pages.pagesPath)
|
|
: path.resolve(__dirname, '../../public');
|
|
this.loadJsonResource();
|
|
this.mountPagesRoutes();
|
|
this.mountCustomRoutes();
|
|
this.mountStaticRoute();
|
|
}
|
|
|
|
verifyEmail(req) {
|
|
const config = req.config;
|
|
const { token: rawToken } = req.query;
|
|
const token = rawToken && typeof rawToken !== 'string' ? rawToken.toString() : rawToken;
|
|
|
|
if (!config) {
|
|
this.invalidRequest();
|
|
}
|
|
|
|
if (!token) {
|
|
return this.goToPage(req, pages.emailVerificationLinkInvalid);
|
|
}
|
|
|
|
const userController = config.userController;
|
|
return userController.verifyEmail(token).then(
|
|
() => {
|
|
return this.goToPage(req, pages.emailVerificationSuccess);
|
|
},
|
|
() => {
|
|
return this.goToPage(req, pages.emailVerificationLinkInvalid);
|
|
}
|
|
);
|
|
}
|
|
|
|
resendVerificationEmail(req) {
|
|
const config = req.config;
|
|
const username = req.body?.username;
|
|
const token = req.body?.token;
|
|
|
|
if (!config) {
|
|
this.invalidRequest();
|
|
}
|
|
|
|
if (!username && !token) {
|
|
return this.goToPage(req, pages.emailVerificationLinkInvalid);
|
|
}
|
|
|
|
const userController = config.userController;
|
|
|
|
return userController.resendVerificationEmail(username, req, token).then(
|
|
() => {
|
|
return this.goToPage(req, pages.emailVerificationSendSuccess);
|
|
},
|
|
() => {
|
|
return this.goToPage(req, pages.emailVerificationSendFail);
|
|
}
|
|
);
|
|
}
|
|
|
|
passwordReset(req) {
|
|
const config = req.config;
|
|
const params = {
|
|
[pageParams.appId]: req.params.appId,
|
|
[pageParams.appName]: config.appName,
|
|
[pageParams.token]: req.query.token,
|
|
[pageParams.username]: req.query.username,
|
|
[pageParams.publicServerUrl]: config.publicServerURL,
|
|
};
|
|
return this.goToPage(req, pages.passwordReset, params);
|
|
}
|
|
|
|
requestResetPassword(req) {
|
|
const config = req.config;
|
|
|
|
if (!config) {
|
|
this.invalidRequest();
|
|
}
|
|
|
|
const { token: rawToken } = req.query;
|
|
const token = rawToken && typeof rawToken !== 'string' ? rawToken.toString() : rawToken;
|
|
|
|
if (!token) {
|
|
return this.goToPage(req, pages.passwordResetLinkInvalid);
|
|
}
|
|
|
|
return config.userController.checkResetTokenValidity(token).then(
|
|
() => {
|
|
const params = {
|
|
[pageParams.token]: token,
|
|
[pageParams.appId]: config.applicationId,
|
|
[pageParams.appName]: config.appName,
|
|
};
|
|
return this.goToPage(req, pages.passwordReset, params);
|
|
},
|
|
() => {
|
|
return this.goToPage(req, pages.passwordResetLinkInvalid);
|
|
}
|
|
);
|
|
}
|
|
|
|
resetPassword(req) {
|
|
const config = req.config;
|
|
|
|
if (!config) {
|
|
this.invalidRequest();
|
|
}
|
|
|
|
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.goToPage(req, pages.passwordResetLinkInvalid);
|
|
}
|
|
|
|
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 => {
|
|
if (req.xhr) {
|
|
if (result.success) {
|
|
return Promise.resolve({
|
|
status: 200,
|
|
response: 'Password successfully reset',
|
|
});
|
|
}
|
|
if (result.err) {
|
|
throw new Parse.Error(Parse.Error.OTHER_CAUSE, `${result.err}`);
|
|
}
|
|
}
|
|
|
|
const query = result.success
|
|
? {}
|
|
: {
|
|
[pageParams.token]: token,
|
|
[pageParams.appId]: config.applicationId,
|
|
[pageParams.error]: result.err,
|
|
[pageParams.appName]: config.appName,
|
|
};
|
|
|
|
if (result?.err === 'The password reset link has expired') {
|
|
delete query[pageParams.token];
|
|
query[pageParams.token] = token;
|
|
}
|
|
const page = result.success ? pages.passwordResetSuccess : pages.passwordReset;
|
|
|
|
return this.goToPage(req, page, query, false);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Returns page content if the page is a local file or returns a
|
|
* redirect to a custom page.
|
|
* @param {Object} req The express request.
|
|
* @param {Page} page The page to go to.
|
|
* @param {Object} [params={}] The query parameters to attach to the URL in case of
|
|
* HTTP redirect responses for POST requests, or the placeholders to fill into
|
|
* the response content in case of HTTP content responses for GET requests.
|
|
* @param {Boolean} [responseType] Is true if a redirect response should be forced,
|
|
* false if a content response should be forced, undefined if the response type
|
|
* should depend on the request type by default:
|
|
* - GET request -> content response
|
|
* - POST request -> redirect response (PRG pattern)
|
|
* @returns {Promise<Object>} The PromiseRouter response.
|
|
*/
|
|
goToPage(req, page, params = {}, responseType) {
|
|
const config = req.config;
|
|
|
|
// Determine redirect either by force, response setting or request method
|
|
const redirect = config.pages.forceRedirect
|
|
? true
|
|
: responseType !== undefined
|
|
? responseType
|
|
: req.method == 'POST';
|
|
|
|
// Include default parameters
|
|
const defaultParams = this.getDefaultParams(config);
|
|
if (Object.values(defaultParams).includes(undefined)) {
|
|
return this.notFound();
|
|
}
|
|
params = Object.assign(params, defaultParams);
|
|
|
|
// Add locale to params to ensure it is passed on with every request;
|
|
// that means, once a locale is set, it is passed on to any follow-up page,
|
|
// e.g. request_password_reset -> password_reset -> password_reset_success
|
|
const locale = this.getLocale(req);
|
|
params[pageParams.locale] = locale;
|
|
|
|
// Compose paths and URLs
|
|
const defaultFile = page.defaultFile;
|
|
const defaultPath = this.defaultPagePath(defaultFile);
|
|
const defaultUrl = this.composePageUrl(defaultFile, config.publicServerURL);
|
|
|
|
// If custom URL is set redirect to it without localization
|
|
const customUrl = config.pages.customUrls[page.id];
|
|
if (customUrl && !Utils.isPath(customUrl)) {
|
|
return this.redirectResponse(customUrl, params);
|
|
}
|
|
|
|
// Get JSON placeholders
|
|
let placeholders = {};
|
|
if (config.pages.enableLocalization && config.pages.localizationJsonPath) {
|
|
placeholders = this.getJsonPlaceholders(locale, params);
|
|
}
|
|
|
|
// Send response
|
|
if (config.pages.enableLocalization && locale) {
|
|
return Utils.getLocalizedPath(defaultPath, locale).then(({ path, subdir }) =>
|
|
redirect
|
|
? this.redirectResponse(
|
|
this.composePageUrl(defaultFile, config.publicServerURL, subdir),
|
|
params
|
|
)
|
|
: this.pageResponse(path, params, placeholders)
|
|
);
|
|
} else {
|
|
return redirect
|
|
? this.redirectResponse(defaultUrl, params)
|
|
: this.pageResponse(defaultPath, params, placeholders);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Serves a request to a static resource and localizes the resource if it
|
|
* is a HTML file.
|
|
* @param {Object} req The request object.
|
|
* @returns {Promise<Object>} The response.
|
|
*/
|
|
staticRoute(req) {
|
|
// Get requested path
|
|
const relativePath = req.params['resource'][0];
|
|
|
|
// Resolve requested path to absolute path
|
|
const absolutePath = path.resolve(this.pagesPath, relativePath);
|
|
|
|
// If the requested file is not a HTML file send its raw content
|
|
if (!absolutePath || !absolutePath.endsWith('.html')) {
|
|
return this.fileResponse(absolutePath);
|
|
}
|
|
|
|
// Get parameters
|
|
const params = this.getDefaultParams(req.config);
|
|
const locale = this.getLocale(req);
|
|
if (locale) {
|
|
params.locale = locale;
|
|
}
|
|
|
|
// Get JSON placeholders
|
|
const placeholders = this.getJsonPlaceholders(locale, params);
|
|
|
|
return this.pageResponse(absolutePath, params, placeholders);
|
|
}
|
|
|
|
/**
|
|
* Returns a translation from the JSON resource for a given locale. The JSON
|
|
* resource is parsed according to i18next syntax.
|
|
*
|
|
* Example JSON content:
|
|
* ```js
|
|
* {
|
|
* "en": { // resource for language `en` (English)
|
|
* "translation": {
|
|
* "greeting": "Hello!"
|
|
* }
|
|
* },
|
|
* "de": { // resource for language `de` (German)
|
|
* "translation": {
|
|
* "greeting": "Hallo!"
|
|
* }
|
|
* }
|
|
* "de-CH": { // resource for locale `de-CH` (Swiss German)
|
|
* "translation": {
|
|
* "greeting": "Grüezi!"
|
|
* }
|
|
* }
|
|
* }
|
|
* ```
|
|
* @param {String} locale The locale to translate to.
|
|
* @returns {Object} The translation or an empty object if no matching
|
|
* translation was found.
|
|
*/
|
|
getJsonTranslation(locale) {
|
|
// If there is no JSON resource
|
|
if (this.jsonParameters === undefined) {
|
|
return {};
|
|
}
|
|
|
|
// If locale is not set use the fallback locale
|
|
locale = locale || this.pagesConfig.localizationFallbackLocale;
|
|
|
|
// Get matching translation by locale, language or fallback locale
|
|
const language = locale.split('-')[0];
|
|
const resource =
|
|
this.jsonParameters[locale] ||
|
|
this.jsonParameters[language] ||
|
|
this.jsonParameters[this.pagesConfig.localizationFallbackLocale] ||
|
|
{};
|
|
const translation = resource.translation || {};
|
|
return translation;
|
|
}
|
|
|
|
/**
|
|
* Returns a translation from the JSON resource for a given locale with
|
|
* placeholders filled in by given parameters.
|
|
* @param {String} locale The locale to translate to.
|
|
* @param {Object} params The parameters to fill into any placeholders
|
|
* within the translations.
|
|
* @returns {Object} The translation or an empty object if no matching
|
|
* translation was found.
|
|
*/
|
|
getJsonPlaceholders(locale, params = {}) {
|
|
// If localization is disabled or there is no JSON resource
|
|
if (!this.pagesConfig.enableLocalization || !this.pagesConfig.localizationJsonPath) {
|
|
return {};
|
|
}
|
|
|
|
// Get JSON placeholders
|
|
let placeholders = this.getJsonTranslation(locale);
|
|
|
|
// Fill in any placeholders in the translation; this allows a translation
|
|
// to contain default placeholders like {{appName}} which are filled here
|
|
placeholders = JSON.stringify(placeholders);
|
|
placeholders = mustache.render(placeholders, params);
|
|
placeholders = JSON.parse(placeholders);
|
|
|
|
return placeholders;
|
|
}
|
|
|
|
/**
|
|
* Creates a response with file content.
|
|
* @param {String} path The path of the file to return.
|
|
* @param {Object} [params={}] The parameters to be included in the response
|
|
* header. These will also be used to fill placeholders.
|
|
* @param {Object} [placeholders={}] The placeholders to fill in the content.
|
|
* These will not be included in the response header.
|
|
* @returns {Object} The Promise Router response.
|
|
*/
|
|
async pageResponse(path, params = {}, placeholders = {}) {
|
|
// Get file content
|
|
let data;
|
|
try {
|
|
data = await this.readFile(path);
|
|
} catch (e) {
|
|
return this.notFound();
|
|
}
|
|
|
|
// Get config placeholders; can be an object, a function or an async function
|
|
let configPlaceholders =
|
|
typeof this.pagesConfig.placeholders === 'function'
|
|
? this.pagesConfig.placeholders(params)
|
|
: Object.prototype.toString.call(this.pagesConfig.placeholders) === '[object Object]'
|
|
? this.pagesConfig.placeholders
|
|
: {};
|
|
if (configPlaceholders instanceof Promise) {
|
|
configPlaceholders = await configPlaceholders;
|
|
}
|
|
|
|
// Fill placeholders
|
|
const allPlaceholders = Object.assign({}, configPlaceholders, placeholders);
|
|
const paramsAndPlaceholders = Object.assign({}, params, allPlaceholders);
|
|
data = mustache.render(data, paramsAndPlaceholders);
|
|
|
|
// Add placeholders in header to allow parsing for programmatic use
|
|
// of response, instead of having to parse the HTML content.
|
|
const headers = Object.entries(params).reduce((m, p) => {
|
|
if (p[1] !== undefined) {
|
|
m[`${pageParamHeaderPrefix}${p[0].toLowerCase()}`] = p[1];
|
|
}
|
|
return m;
|
|
}, {});
|
|
|
|
return { text: data, headers: headers };
|
|
}
|
|
|
|
/**
|
|
* Creates a response with file content.
|
|
* @param {String} path The path of the file to return.
|
|
* @returns {Object} The PromiseRouter response.
|
|
*/
|
|
async fileResponse(path) {
|
|
// Get file content
|
|
let data;
|
|
try {
|
|
data = await this.readFile(path);
|
|
} catch (e) {
|
|
return this.notFound();
|
|
}
|
|
|
|
return { text: data };
|
|
}
|
|
|
|
/**
|
|
* Reads and returns the content of a file at a given path. File reading to
|
|
* serve content on the static route is only allowed from the pages
|
|
* directory on downwards.
|
|
* -----------------------------------------------------------------------
|
|
* **WARNING:** All file reads in the PagesRouter must be executed by this
|
|
* wrapper because it also detects and prevents common exploits.
|
|
* -----------------------------------------------------------------------
|
|
* @param {String} filePath The path to the file to read.
|
|
* @returns {Promise<String>} The file content.
|
|
*/
|
|
async readFile(filePath) {
|
|
// Normalize path to prevent it from containing any directory changing
|
|
// UNIX patterns which could expose the whole file system, e.g.
|
|
// `http://example.com/parse/apps/../file.txt` requests a file outside
|
|
// of the pages directory scope.
|
|
const normalizedPath = path.normalize(filePath);
|
|
|
|
// Abort if the path is outside of the path directory scope
|
|
if (!normalizedPath.startsWith(this.pagesPath)) {
|
|
throw errors.fileOutsideAllowedScope;
|
|
}
|
|
|
|
return await fs.readFile(normalizedPath, 'utf-8');
|
|
}
|
|
|
|
/**
|
|
* Loads a language resource JSON file that is used for translations.
|
|
*/
|
|
loadJsonResource() {
|
|
if (this.pagesConfig.localizationJsonPath === undefined) {
|
|
return;
|
|
}
|
|
try {
|
|
const json = require(path.resolve('./', this.pagesConfig.localizationJsonPath));
|
|
this.jsonParameters = json;
|
|
} catch (e) {
|
|
throw errors.jsonFailedFileLoading;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Extracts and returns the page default parameters from the Parse Server
|
|
* configuration. These parameters are made accessible in every page served
|
|
* by this router.
|
|
* @param {Object} config The Parse Server configuration.
|
|
* @returns {Object} The default parameters.
|
|
*/
|
|
getDefaultParams(config) {
|
|
return config
|
|
? {
|
|
[pageParams.appId]: config.appId,
|
|
[pageParams.appName]: config.appName,
|
|
[pageParams.publicServerUrl]: config.publicServerURL,
|
|
}
|
|
: {};
|
|
}
|
|
|
|
/**
|
|
* Extracts and returns the locale from an express request.
|
|
* @param {Object} req The express request.
|
|
* @returns {String|undefined} The locale, or undefined if no locale was set.
|
|
*/
|
|
getLocale(req) {
|
|
const locale =
|
|
(req.query || {})[pageParams.locale] ||
|
|
(req.body || {})[pageParams.locale] ||
|
|
(req.params || {})[pageParams.locale] ||
|
|
(req.headers || {})[pageParamHeaderPrefix + pageParams.locale];
|
|
return locale;
|
|
}
|
|
|
|
/**
|
|
* Creates a response with http redirect.
|
|
* @param {Object} req The express request.
|
|
* @param {String} path The path of the file to return.
|
|
* @param {Object} params The query parameters to include.
|
|
* @returns {Object} The Promise Router response.
|
|
*/
|
|
async redirectResponse(url, params) {
|
|
// Remove any parameters with undefined value
|
|
params = Object.entries(params).reduce((m, p) => {
|
|
if (p[1] !== undefined) {
|
|
m[p[0]] = p[1];
|
|
}
|
|
return m;
|
|
}, {});
|
|
|
|
// Compose URL with parameters in query
|
|
const location = new URL(url);
|
|
Object.entries(params).forEach(p => location.searchParams.set(p[0], p[1]));
|
|
const locationString = location.toString();
|
|
|
|
// Add parameters to header to allow parsing for programmatic use
|
|
// of response, instead of having to parse the HTML content.
|
|
const headers = Object.entries(params).reduce((m, p) => {
|
|
if (p[1] !== undefined) {
|
|
m[`${pageParamHeaderPrefix}${p[0].toLowerCase()}`] = p[1];
|
|
}
|
|
return m;
|
|
}, {});
|
|
|
|
return {
|
|
status: 303,
|
|
location: locationString,
|
|
headers: headers,
|
|
};
|
|
}
|
|
|
|
defaultPagePath(file) {
|
|
return path.join(this.pagesPath, file);
|
|
}
|
|
|
|
composePageUrl(file, publicServerUrl, locale) {
|
|
let url = publicServerUrl;
|
|
url += url.endsWith('/') ? '' : '/';
|
|
url += this.pagesEndpoint + '/';
|
|
url += locale === undefined ? '' : locale + '/';
|
|
url += file;
|
|
return url;
|
|
}
|
|
|
|
notFound() {
|
|
return {
|
|
text: 'Not found.',
|
|
status: 404,
|
|
};
|
|
}
|
|
|
|
invalidRequest() {
|
|
const error = new Error();
|
|
error.status = 403;
|
|
error.message = 'unauthorized';
|
|
throw error;
|
|
}
|
|
|
|
/**
|
|
* Sets the Parse Server configuration in the request object to make it
|
|
* easily accessible throughtout request processing.
|
|
* @param {Object} req The request.
|
|
* @param {Boolean} failGracefully Is true if failing to set the config should
|
|
* not result in an invalid request response. Default is `false`.
|
|
*/
|
|
setConfig(req, failGracefully = false) {
|
|
req.config = Config.get(req.params.appId || req.query.appId);
|
|
if (!req.config && !failGracefully) {
|
|
this.invalidRequest();
|
|
}
|
|
return Promise.resolve();
|
|
}
|
|
|
|
mountPagesRoutes() {
|
|
this.route(
|
|
'GET',
|
|
`/${this.pagesEndpoint}/:appId/verify_email`,
|
|
req => {
|
|
this.setConfig(req);
|
|
},
|
|
req => {
|
|
return this.verifyEmail(req);
|
|
}
|
|
);
|
|
|
|
this.route(
|
|
'POST',
|
|
`/${this.pagesEndpoint}/:appId/resend_verification_email`,
|
|
req => {
|
|
this.setConfig(req);
|
|
},
|
|
req => {
|
|
return this.resendVerificationEmail(req);
|
|
}
|
|
);
|
|
|
|
this.route(
|
|
'GET',
|
|
`/${this.pagesEndpoint}/choose_password`,
|
|
req => {
|
|
this.setConfig(req);
|
|
},
|
|
req => {
|
|
return this.passwordReset(req);
|
|
}
|
|
);
|
|
|
|
this.route(
|
|
'POST',
|
|
`/${this.pagesEndpoint}/:appId/request_password_reset`,
|
|
req => {
|
|
this.setConfig(req);
|
|
},
|
|
req => {
|
|
return this.resetPassword(req);
|
|
}
|
|
);
|
|
|
|
this.route(
|
|
'GET',
|
|
`/${this.pagesEndpoint}/:appId/request_password_reset`,
|
|
req => {
|
|
this.setConfig(req);
|
|
},
|
|
req => {
|
|
return this.requestResetPassword(req);
|
|
}
|
|
);
|
|
}
|
|
|
|
mountCustomRoutes() {
|
|
for (const route of this.pagesConfig.customRoutes || []) {
|
|
this.route(
|
|
route.method,
|
|
`/${this.pagesEndpoint}/:appId/${route.path}`,
|
|
req => {
|
|
this.setConfig(req);
|
|
},
|
|
async req => {
|
|
const { file, query = {} } = (await route.handler(req)) || {};
|
|
|
|
// If route handler did not return a page send 404 response
|
|
if (!file) {
|
|
return this.notFound();
|
|
}
|
|
|
|
// Send page response
|
|
const page = new Page({ id: file, defaultFile: file });
|
|
return this.goToPage(req, page, query, false);
|
|
}
|
|
);
|
|
}
|
|
}
|
|
|
|
mountStaticRoute() {
|
|
this.route(
|
|
'GET',
|
|
`/${this.pagesEndpoint}/*resource`,
|
|
req => {
|
|
this.setConfig(req, true);
|
|
},
|
|
req => {
|
|
return this.staticRoute(req);
|
|
}
|
|
);
|
|
}
|
|
|
|
expressRouter() {
|
|
const router = express.Router();
|
|
router.use('/', super.expressRouter());
|
|
return router;
|
|
}
|
|
}
|
|
|
|
export default PagesRouter;
|
|
module.exports = {
|
|
PagesRouter,
|
|
pageParamHeaderPrefix,
|
|
pageParams,
|
|
pages,
|
|
};
|