Add page localization (#7128)
* added localized pages; added refactored page templates; adapted test cases; introduced localization test cases * added changelog entry * fixed test description typo * fixed bug in PromiseRouter where headers are not added for text reponse * added page parameters in page headers for programmatic use * refactored tests for PublicAPIRouter * added mustache lib for template rendering * fixed fs.promises module reference * fixed template placeholder typo * changed redirect response to provide headers instead of query parameters * fix lint * fixed syntax errors and typos in html templates * removed obsolete URI encoding * added locale inferring from request body and header * added end-to-end localizaton test * added server option validation; refactored pages server option * fixed invalid redirect URL for no locale matching file * added end-to-end localizaton tests * adapted tests to new response content * re-added PublicAPIRouter; added PagesRouter as experimental feature * refactored PagesRouter test structure * added configuration option for custom path to pages * added configuration option for custom endpoint to pages * fixed lint * added tests * added a distinct page for invalid password reset link * renamed generic page invalidLink to expiredVerificationLink * improved HTML files documentation * improved HTML files documentation * changed changelog entry for experimental feature * improved file naming to make it more descriptive * fixed file naming and env parameter naming * added readme entry * fixed readme TOC - hasn't been updated in a while * added localization with JSON resource * added JSON localization to feature pages (password reset, email verification) * updated readme * updated readme * optimized JSON localization for feature pages; added e2e test case * fixed readme typo * minor refactoring of existing tests * fixed bug where Object type was not recognized as config key type * added feature config placeholders * prettier * added passing locale to page config placeholder callback * refactored passing locale to placeholder to pass test * added config placeholder feature to README * fixed typo in README
This commit is contained in:
725
src/Routers/PagesRouter.js
Normal file
725
src/Routers/PagesRouter.js
Normal file
@@ -0,0 +1,725 @@
|
||||
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();
|
||||
}
|
||||
|
||||
verifyEmail(req) {
|
||||
const config = req.config;
|
||||
const { username, token: rawToken } = req.query;
|
||||
const token = rawToken && typeof rawToken !== 'string' ? rawToken.toString() : rawToken;
|
||||
|
||||
if (!config) {
|
||||
this.invalidRequest();
|
||||
}
|
||||
|
||||
if (!token || !username) {
|
||||
return this.goToPage(req, pages.emailVerificationLinkInvalid);
|
||||
}
|
||||
|
||||
const userController = config.userController;
|
||||
return userController.verifyEmail(username, token).then(
|
||||
() => {
|
||||
const params = {
|
||||
[pageParams.username]: username,
|
||||
};
|
||||
return this.goToPage(req, pages.emailVerificationSuccess, params);
|
||||
},
|
||||
() => {
|
||||
const params = {
|
||||
[pageParams.username]: username,
|
||||
};
|
||||
return this.goToPage(req, pages.emailVerificationLinkExpired, params);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
resendVerificationEmail(req) {
|
||||
const config = req.config;
|
||||
const username = req.body.username;
|
||||
|
||||
if (!config) {
|
||||
this.invalidRequest();
|
||||
}
|
||||
|
||||
if (!username) {
|
||||
return this.goToPage(req, pages.emailVerificationLinkInvalid);
|
||||
}
|
||||
|
||||
const userController = config.userController;
|
||||
|
||||
return userController.resendVerificationEmail(username).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 { username, token: rawToken } = req.query;
|
||||
const token = rawToken && typeof rawToken !== 'string' ? rawToken.toString() : rawToken;
|
||||
|
||||
if (!username || !token) {
|
||||
return this.goToPage(req, pages.passwordResetLinkInvalid);
|
||||
}
|
||||
|
||||
return config.userController.checkResetTokenValidity(username, token).then(
|
||||
() => {
|
||||
const params = {
|
||||
[pageParams.token]: token,
|
||||
[pageParams.username]: username,
|
||||
[pageParams.appId]: config.applicationId,
|
||||
[pageParams.appName]: config.appName,
|
||||
};
|
||||
return this.goToPage(req, pages.passwordReset, params);
|
||||
},
|
||||
() => {
|
||||
const params = {
|
||||
[pageParams.username]: username,
|
||||
};
|
||||
return this.goToPage(req, pages.passwordResetLinkInvalid, params);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
resetPassword(req) {
|
||||
const config = req.config;
|
||||
|
||||
if (!config) {
|
||||
this.invalidRequest();
|
||||
}
|
||||
|
||||
const { username, new_password, token: rawToken } = req.body;
|
||||
const token = rawToken && typeof rawToken !== 'string' ? rawToken.toString() : rawToken;
|
||||
|
||||
if ((!username || !token || !new_password) && req.xhr === false) {
|
||||
return this.goToPage(req, pages.passwordResetLinkInvalid);
|
||||
}
|
||||
|
||||
if (!username) {
|
||||
throw new Parse.Error(Parse.Error.USERNAME_MISSING, 'Missing username');
|
||||
}
|
||||
|
||||
if (!token) {
|
||||
throw new Parse.Error(Parse.Error.OTHER_CAUSE, 'Missing token');
|
||||
}
|
||||
|
||||
if (!new_password) {
|
||||
throw new Parse.Error(Parse.Error.PASSWORD_MISSING, 'Missing password');
|
||||
}
|
||||
|
||||
return config.userController
|
||||
.updatePassword(username, token, new_password)
|
||||
.then(
|
||||
() => {
|
||||
return Promise.resolve({
|
||||
success: true,
|
||||
});
|
||||
},
|
||||
err => {
|
||||
return Promise.resolve({
|
||||
success: false,
|
||||
err,
|
||||
});
|
||||
}
|
||||
)
|
||||
.then(result => {
|
||||
if (req.xhr) {
|
||||
if (result.success) {
|
||||
return Promise.resolve({
|
||||
status: 200,
|
||||
response: 'Password successfully reset',
|
||||
});
|
||||
}
|
||||
if (result.err) {
|
||||
throw new Parse.Error(Parse.Error.OTHER_CAUSE, `${result.err}`);
|
||||
}
|
||||
}
|
||||
|
||||
const query = result.success
|
||||
? {
|
||||
[pageParams.username]: username,
|
||||
}
|
||||
: {
|
||||
[pageParams.username]: username,
|
||||
[pageParams.token]: token,
|
||||
[pageParams.appId]: config.applicationId,
|
||||
[pageParams.error]: result.err,
|
||||
[pageParams.appName]: config.appName,
|
||||
};
|
||||
const page = result.success ? pages.passwordResetSuccess : pages.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 -> passwort_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[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 rediret.
|
||||
* @param {Object} req The express request.
|
||||
* @param {String} path The path of the file to return.
|
||||
* @param {Object} params The query parameters to include.
|
||||
* @returns {Object} The Promise Router response.
|
||||
*/
|
||||
async redirectResponse(url, params) {
|
||||
// Remove any parameters with undefined value
|
||||
params = Object.entries(params).reduce((m, p) => {
|
||||
if (p[1] !== undefined) {
|
||||
m[p[0]] = p[1];
|
||||
}
|
||||
return m;
|
||||
}, {});
|
||||
|
||||
// Compose URL with parameters in query
|
||||
const location = new URL(url);
|
||||
Object.entries(params).forEach(p => location.searchParams.set(p[0], p[1]));
|
||||
const locationString = location.toString();
|
||||
|
||||
// Add parameters to header to allow parsing for programmatic use
|
||||
// of response, instead of having to parse the HTML content.
|
||||
const headers = Object.entries(params).reduce((m, p) => {
|
||||
if (p[1] !== undefined) {
|
||||
m[`${pageParamHeaderPrefix}${p[0].toLowerCase()}`] = p[1];
|
||||
}
|
||||
return m;
|
||||
}, {});
|
||||
|
||||
return {
|
||||
status: 303,
|
||||
location: locationString,
|
||||
headers: headers,
|
||||
};
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
);
|
||||
|
||||
this.route(
|
||||
'GET',
|
||||
`/${this.pagesEndpoint}/(*)?`,
|
||||
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,
|
||||
};
|
||||
Reference in New Issue
Block a user