123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299 |
- import fs from 'fs';
- import express from 'express';
- import log from './log.js';
- import { generateKey } from './crypt.js';
- import { errorGuard, errorMiddleware, throwError } from './error.js';
- import { longUid } from './uid.js';
- const emailRegex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
- const validateEmail = (email) => {
- return emailRegex.test(email);
- };
- const writeConfigFile = (configFilePath, data) => {
- return new Promise((resolve, reject) => {
- fs.writeFile(configFilePath, JSON.stringify(data, null, 2), (err) => {
- if (err) {
- reject(err);
- } else {
- resolve(data);
- }
- });
- });
- };
- const loadConfigFile = (configFilePath, createIfMissing = false) => {
- return new Promise((resolve, reject) => {
- // Read config file
- fs.readFile(configFilePath, 'utf-8', (err, jsonString) => {
- if (err) {
- const { code } = err;
- if (code === 'ENOENT') {
- const data = {};
- if (createIfMissing) {
- log.info('No config file, create default');
- writeConfigFile(configFilePath, data).then(() => {
- resolve(data);
- });
- } else {
- reject(`File ${configFilePath} is missing`);
- }
- } else {
- reject(`Failed to load ${configFilePath} configuration file`);
- }
- } else {
- try {
- const data = JSON.parse(jsonString);
- resolve(data);
- } catch (e) {
- console.log('Fails to parse config file...\n', e);
- reject('Fails to parse config file...');
- }
- }
- });
- });
- };
- const siteMiddleware = ({
- storeBackend,
- configFile,
- onSiteCreation,
- onSiteUpdate,
- siteRegistrationEnabled = true,
- }) => {
- const router = express.Router();
- const siteConfig = {};
- let configLoaded = false;
- const getConfirmPath = (siteId, token) =>
- `/_register/${siteId}/confirm/${token}`;
- const loadSites = async () => {
- try {
- const sites = await storeBackend.list('_site');
- sites.forEach((site) => {
- siteConfig[site._id] = site;
- });
- configLoaded = true;
- } catch (e) {
- if (e.statusCode === 404 && e.message === 'Box not found') {
- await storeBackend.createOrUpdateBox('_site');
- await storeBackend.createOrUpdateBox('_pending');
- try {
- // Try to load deprecated config file if any
- const siteConfigFile = await loadConfigFile(configFile);
- Object.entries(siteConfigFile).forEach(async ([id, data]) => {
- await storeBackend.save('_site', id, data);
- });
- fs.renameSync(configFile, `${configFile}.bak`);
- console.log('Migrate deprecated config file data to store.');
- } catch (e) {
- if (!e.includes('missing')) {
- console.log('Deprecated config file appears to be invalid.');
- }
- }
- await loadSites();
- } else {
- console.log('Error while loading configuration', e);
- process.exit(-1);
- }
- }
- };
- loadSites();
- router.use((req, res, next) => {
- if (!configLoaded) {
- throwError('Server not ready, try again later.', 503);
- }
- req.siteConfig = siteConfig;
- next();
- });
- // Enable site registration
- if (siteRegistrationEnabled) {
- router.get(
- '/_register/:siteId/confirm/:token',
- errorGuard(async (req, res) => {
- const { siteId, token } = req.params;
- let pending;
- let previous;
- try {
- // Check if pending exists
- pending = await storeBackend.get('_pending', siteId);
- } catch (e) {
- if (e.statusCode === 404) {
- try {
- // Pending missing, check if site already exists
- await storeBackend.get('_site', siteId);
- // Yes, so token is already consumed
- throwError('Token already used.', 403);
- } catch (e) {
- if (e.statusCode === 404) {
- // If site not found so URL is wrong
- throwError('Bad site.', 404);
- } else {
- throw e;
- }
- }
- } else {
- throw e;
- }
- }
- try {
- // Get previous site if exists
- previous = await storeBackend.get('_site', siteId);
- } catch (e) {
- if (e.statusCode !== 404) {
- throw e;
- }
- }
- if (pending.token === token) {
- const toSave = { ...(previous || {}), ...pending };
- delete toSave.token;
- const saved = await storeBackend.save('_site', siteId, toSave);
- await storeBackend.delete('_pending', siteId);
- siteConfig[siteId] = { ...saved };
- } else {
- // Token can be invalid if another modification is sent in the meantime
- // or if the token is already consumed.
- throwError('Token invalid or already used.', 403);
- }
- if (previous) {
- // If previous, then we have just updated the site
- res.json({ message: 'Site updated' });
- } else {
- // otherwise we have created a new site
- res.json({ message: 'Site created' });
- }
- })
- );
- router.post(
- '/_register/',
- errorGuard(async (req, res) => {
- const { siteId, name, emailFrom, owner } = req.body;
- if (!siteId || !name || !emailFrom || !owner) {
- throwError(
- 'The following data are required for site creation: siteId, name, emailFrom, owner.',
- 400
- );
- }
- if (siteId.length < 3 || !siteId.match(/^[a-zA-Z0-9][a-zA-Z0-9_]*$/)) {
- throwError(
- "The siteId must contains at least 3 letters or '_' and can't start with '_'.",
- 400
- );
- }
- if (siteConfig[siteId]) {
- // The site already exists
- throwError('A site with the same name already exists.', 403);
- }
- if (!validateEmail(emailFrom)) {
- throwError('emailFrom must be a valid email.', 400);
- }
- if (!validateEmail(owner)) {
- throwError('emailFrom must be a valid email.', 400);
- }
- const key = generateKey();
- const token = longUid();
- const newSite = await storeBackend.save('_pending', siteId, {
- name,
- owner,
- emailFrom,
- key,
- token,
- });
- await onSiteCreation({
- req,
- site: newSite,
- confirmPath: getConfirmPath(siteId, token),
- });
- const response = { ...newSite };
- delete response.token;
- res.json(response);
- })
- );
- router.patch(
- '/_register/:siteId',
- errorGuard(async (req, res) => {
- const { siteId } = req.params;
- const { name, emailFrom } = req.body;
- if (!siteId || !siteConfig[siteId]) {
- // The site doesn't exist
- throwError(
- `Site '${siteId}' doesn't exist. You must create it before.`,
- 404
- );
- }
- if (!name || !emailFrom) {
- throwError(
- 'The following data are required for site update: name, emailFrom.',
- 400
- );
- }
- if (!validateEmail(emailFrom)) {
- throwError('emailFrom must be a valid email.', 400);
- }
- const previous = await storeBackend.get('_site', siteId);
- const token = longUid();
- const updated = await storeBackend.save('_pending', siteId, {
- name,
- emailFrom,
- token,
- });
- await onSiteUpdate({
- req,
- site: { ...updated },
- previous: { ...previous },
- confirmPath: getConfirmPath(siteId, token),
- });
- const response = { ...updated };
- delete response.key;
- delete response.token;
- res.json({ ...response });
- })
- );
- }
- router.get(
- '/site/settings',
- errorGuard(async (req, res) => {
- res.json({ registrationEnabled: siteRegistrationEnabled });
- })
- );
- router.use(errorMiddleware);
- return router;
- };
- export default siteMiddleware;
|