site.js 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  1. import fs from 'fs';
  2. import express from 'express';
  3. import log from './log.js';
  4. import { generateKey } from './crypt.js';
  5. import { errorGuard, errorMiddleware, throwError } from './error.js';
  6. import { longUid } from './uid.js';
  7. 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,}))$/;
  8. const validateEmail = (email) => {
  9. return emailRegex.test(email);
  10. };
  11. const writeConfigFile = (configFilePath, data) => {
  12. return new Promise((resolve, reject) => {
  13. fs.writeFile(configFilePath, JSON.stringify(data, null, 2), (err) => {
  14. if (err) {
  15. reject(err);
  16. } else {
  17. resolve(data);
  18. }
  19. });
  20. });
  21. };
  22. const loadConfigFile = (configFilePath, createIfMissing = false) => {
  23. return new Promise((resolve, reject) => {
  24. // Read config file
  25. fs.readFile(configFilePath, 'utf-8', (err, jsonString) => {
  26. if (err) {
  27. const { code } = err;
  28. if (code === 'ENOENT') {
  29. const data = {};
  30. if (createIfMissing) {
  31. log.info('No config file, create default');
  32. writeConfigFile(configFilePath, data).then(() => {
  33. resolve(data);
  34. });
  35. } else {
  36. reject(`File ${configFilePath} is missing`);
  37. }
  38. } else {
  39. reject(`Failed to load ${configFilePath} configuration file`);
  40. }
  41. } else {
  42. try {
  43. const data = JSON.parse(jsonString);
  44. resolve(data);
  45. } catch (e) {
  46. console.log('Fails to parse config file...\n', e);
  47. reject('Fails to parse config file...');
  48. }
  49. }
  50. });
  51. });
  52. };
  53. const siteMiddleware = ({
  54. storeBackend,
  55. configFile,
  56. onSiteCreation,
  57. onSiteUpdate,
  58. siteRegistrationEnabled = true,
  59. }) => {
  60. const router = express.Router();
  61. const siteConfig = {};
  62. let configLoaded = false;
  63. const getConfirmPath = (siteId, token) =>
  64. `/_register/${siteId}/confirm/${token}`;
  65. const loadSites = async () => {
  66. try {
  67. const sites = await storeBackend.list('_site');
  68. sites.forEach((site) => {
  69. siteConfig[site._id] = site;
  70. });
  71. configLoaded = true;
  72. } catch (e) {
  73. if (e.statusCode === 404 && e.message === 'Box not found') {
  74. await storeBackend.createOrUpdateBox('_site');
  75. await storeBackend.createOrUpdateBox('_pending');
  76. try {
  77. // Try to load deprecated config file if any
  78. const siteConfigFile = await loadConfigFile(configFile);
  79. Object.entries(siteConfigFile).forEach(async ([id, data]) => {
  80. await storeBackend.save('_site', id, data);
  81. });
  82. fs.renameSync(configFile, `${configFile}.bak`);
  83. console.log('Migrate deprecated config file data to store.');
  84. } catch (e) {
  85. if (!e.includes('missing')) {
  86. console.log('Deprecated config file appears to be invalid.');
  87. }
  88. }
  89. await loadSites();
  90. } else {
  91. console.log('Error while loading configuration', e);
  92. process.exit(-1);
  93. }
  94. }
  95. };
  96. loadSites();
  97. router.use((req, res, next) => {
  98. if (!configLoaded) {
  99. throwError('Server not ready, try again later.', 503);
  100. }
  101. req.siteConfig = siteConfig;
  102. next();
  103. });
  104. // Enable site registration
  105. if (siteRegistrationEnabled) {
  106. router.get(
  107. '/_register/:siteId/confirm/:token',
  108. errorGuard(async (req, res) => {
  109. const { siteId, token } = req.params;
  110. let pending;
  111. let previous;
  112. try {
  113. // Check if pending exists
  114. pending = await storeBackend.get('_pending', siteId);
  115. } catch (e) {
  116. if (e.statusCode === 404) {
  117. try {
  118. // Pending missing, check if site already exists
  119. await storeBackend.get('_site', siteId);
  120. // Yes, so token is already consumed
  121. throwError('Token already used.', 403);
  122. } catch (e) {
  123. if (e.statusCode === 404) {
  124. // If site not found so URL is wrong
  125. throwError('Bad site.', 404);
  126. } else {
  127. throw e;
  128. }
  129. }
  130. } else {
  131. throw e;
  132. }
  133. }
  134. try {
  135. // Get previous site if exists
  136. previous = await storeBackend.get('_site', siteId);
  137. } catch (e) {
  138. if (e.statusCode !== 404) {
  139. throw e;
  140. }
  141. }
  142. if (pending.token === token) {
  143. const toSave = { ...(previous || {}), ...pending };
  144. delete toSave.token;
  145. const saved = await storeBackend.save('_site', siteId, toSave);
  146. await storeBackend.delete('_pending', siteId);
  147. siteConfig[siteId] = { ...saved };
  148. } else {
  149. // Token can be invalid if another modification is sent in the meantime
  150. // or if the token is already consumed.
  151. throwError('Token invalid or already used.', 403);
  152. }
  153. if (previous) {
  154. // If previous, then we have just updated the site
  155. res.json({ message: 'Site updated' });
  156. } else {
  157. // otherwise we have created a new site
  158. res.json({ message: 'Site created' });
  159. }
  160. })
  161. );
  162. router.post(
  163. '/_register/',
  164. errorGuard(async (req, res) => {
  165. const { siteId, name, emailFrom, owner } = req.body;
  166. if (!siteId || !name || !emailFrom || !owner) {
  167. throwError(
  168. 'The following data are required for site creation: siteId, name, emailFrom, owner.',
  169. 400
  170. );
  171. }
  172. if (siteId.length < 3 || !siteId.match(/^[a-zA-Z0-9][a-zA-Z0-9_]*$/)) {
  173. throwError(
  174. "The siteId must contains at least 3 letters or '_' and can't start with '_'.",
  175. 400
  176. );
  177. }
  178. if (siteConfig[siteId]) {
  179. // The site already exists
  180. throwError('A site with the same name already exists.', 403);
  181. }
  182. if (!validateEmail(emailFrom)) {
  183. throwError('emailFrom must be a valid email.', 400);
  184. }
  185. if (!validateEmail(owner)) {
  186. throwError('emailFrom must be a valid email.', 400);
  187. }
  188. const key = generateKey();
  189. const token = longUid();
  190. const newSite = await storeBackend.save('_pending', siteId, {
  191. name,
  192. owner,
  193. emailFrom,
  194. key,
  195. token,
  196. });
  197. await onSiteCreation({
  198. req,
  199. site: newSite,
  200. confirmPath: getConfirmPath(siteId, token),
  201. });
  202. const response = { ...newSite };
  203. delete response.token;
  204. res.json(response);
  205. })
  206. );
  207. router.patch(
  208. '/_register/:siteId',
  209. errorGuard(async (req, res) => {
  210. const { siteId } = req.params;
  211. const { name, emailFrom } = req.body;
  212. if (!siteId || !siteConfig[siteId]) {
  213. // The site doesn't exist
  214. throwError(
  215. `Site '${siteId}' doesn't exist. You must create it before.`,
  216. 404
  217. );
  218. }
  219. if (!name || !emailFrom) {
  220. throwError(
  221. 'The following data are required for site update: name, emailFrom.',
  222. 400
  223. );
  224. }
  225. if (!validateEmail(emailFrom)) {
  226. throwError('emailFrom must be a valid email.', 400);
  227. }
  228. const previous = await storeBackend.get('_site', siteId);
  229. const token = longUid();
  230. const updated = await storeBackend.save('_pending', siteId, {
  231. name,
  232. emailFrom,
  233. token,
  234. });
  235. await onSiteUpdate({
  236. req,
  237. site: { ...updated },
  238. previous: { ...previous },
  239. confirmPath: getConfirmPath(siteId, token),
  240. });
  241. const response = { ...updated };
  242. delete response.key;
  243. delete response.token;
  244. res.json({ ...response });
  245. })
  246. );
  247. }
  248. router.get(
  249. '/site/settings',
  250. errorGuard(async (req, res) => {
  251. res.json({ registrationEnabled: siteRegistrationEnabled });
  252. })
  253. );
  254. router.use(errorMiddleware);
  255. return router;
  256. };
  257. export default siteMiddleware;