middleware.js 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387
  1. import express from 'express';
  2. import cookieSession from 'cookie-session';
  3. import nodemailer from 'nodemailer';
  4. import schedule from 'node-schedule';
  5. import { throwError } from './error.js';
  6. import log from './log.js';
  7. import store from './store/index.js';
  8. import site from './site.js';
  9. import origin from './origin.js';
  10. import { getStoreBackend, wrapBackend } from './store/backends/index.js';
  11. import {
  12. getFileStoreBackend,
  13. wrapBackend as wrapFileBackend,
  14. } from './fileStore/backends/index.js';
  15. import remote from './remote.js';
  16. import execute from './execute.js';
  17. import auth from './authentication.js';
  18. import { decrypt } from './crypt.js';
  19. export const ricochetMiddleware = ({
  20. secret,
  21. fakeEmail = false,
  22. storeBackend,
  23. fileStoreBackend,
  24. storePrefix,
  25. disableCache = false,
  26. setupPath,
  27. getTransporter,
  28. } = {}) => {
  29. const router = express.Router();
  30. // Remote Function map
  31. const functionsBySite = {};
  32. // Schedule map
  33. const schedulesBySite = {};
  34. // Hooks map
  35. const hooksBySite = {};
  36. const decryptPayload = (script, { siteConfig, siteId }) => {
  37. const data = JSON.parse(script);
  38. if (!siteConfig[siteId]) {
  39. throwError(`Site ${siteId} not registered on ricochet.js`, 404);
  40. }
  41. const { key } = siteConfig[siteId];
  42. try {
  43. const decrypted = decrypt(data, key);
  44. return decrypted;
  45. } catch (e) {
  46. log.warn(
  47. { error: e },
  48. `Fails to decrypt Ricochet setup file from ${remote}. Please check your encryption key.`
  49. );
  50. throwError(
  51. `Fails to decrypt Ricochet setup file from ${remote}. Please check your encryption key.`,
  52. 500
  53. );
  54. }
  55. };
  56. // Remote code
  57. router.use(
  58. remote({
  59. context: (req) => {
  60. const { siteId, authenticatedUser } = req;
  61. const wrappedBackend = wrapBackend(
  62. storeBackend,
  63. siteId,
  64. authenticatedUser
  65. );
  66. const wrappedFileBackend = wrapFileBackend(
  67. fileStoreBackend,
  68. siteId,
  69. authenticatedUser
  70. );
  71. if (!functionsBySite[siteId]) {
  72. functionsBySite[siteId] = {};
  73. }
  74. if (!schedulesBySite[siteId]) {
  75. schedulesBySite[siteId] = { hourly: [], daily: [] };
  76. }
  77. if (!hooksBySite[siteId]) {
  78. hooksBySite[siteId] = {};
  79. }
  80. return {
  81. store: wrappedBackend,
  82. fileStore: wrappedFileBackend,
  83. functions: functionsBySite[siteId],
  84. schedules: schedulesBySite[siteId],
  85. hooks: hooksBySite[siteId],
  86. };
  87. },
  88. disableCache,
  89. setupPath,
  90. preProcess: decryptPayload,
  91. })
  92. );
  93. const onSendToken = async ({ remote, userEmail, userId, token, req }) => {
  94. const { siteConfig, siteId, t } = req;
  95. if (!siteConfig[siteId]) {
  96. throwError(`Site ${siteId} not registered on ricochet.js`, 404);
  97. }
  98. const { name: siteName, emailFrom } = siteConfig[siteId];
  99. log.debug(`Link to connect: ${remote}/login/${userId}/${token}`);
  100. // if fake host, link is only loggued
  101. if (fakeEmail) {
  102. log.info(
  103. t('Auth mail text message', {
  104. url: `${remote}/login/${userId}/${token}`,
  105. siteName: siteName,
  106. interpolation: { escapeValue: false },
  107. })
  108. );
  109. }
  110. await getTransporter().sendMail({
  111. from: emailFrom,
  112. to: userEmail,
  113. subject: t('Your authentication link', {
  114. siteName,
  115. interpolation: { escapeValue: false },
  116. }),
  117. text: t('Auth mail text message', {
  118. url: `${remote}/login/${userId}/${token}`,
  119. siteName,
  120. }),
  121. html: t('Auth mail html message', {
  122. url: `${remote}/login/${userId}/${token}`,
  123. siteName,
  124. }),
  125. });
  126. log.info('Auth mail sent');
  127. };
  128. const onLogin = (userId, req) => {
  129. req.session.userId = userId;
  130. };
  131. const onLogout = (req) => {
  132. req.session = null;
  133. };
  134. // Session middleware
  135. router.use(
  136. cookieSession({
  137. name: 'session',
  138. keys: [secret],
  139. httpOnly: true,
  140. // Cookie Options
  141. maxAge: 10 * 24 * 60 * 60 * 1000, // 10 days
  142. sameSite: 'Lax',
  143. })
  144. );
  145. // Re-set cookie on activity
  146. router.use((req, res, next) => {
  147. req.session.nowInMinutes = Math.floor(Date.now() / (60 * 1000));
  148. next();
  149. });
  150. // authenticate middleware
  151. router.use((req, res, next) => {
  152. if (req.session.userId) {
  153. req.authenticatedUser = req.session.userId;
  154. } else {
  155. req.authenticatedUser = null;
  156. }
  157. next();
  158. });
  159. // Auth middleware
  160. router.use(auth({ onSendToken, onLogin, onLogout, secret: secret }));
  161. // JSON store
  162. router.use(
  163. store({
  164. prefix: storePrefix,
  165. backend: storeBackend,
  166. fileBackend: fileStoreBackend,
  167. hooks: (req) => {
  168. const { siteId } = req;
  169. return hooksBySite[siteId];
  170. },
  171. })
  172. );
  173. // Execute middleware
  174. router.use(
  175. execute({
  176. context: (req) => {
  177. const { siteId, authenticatedUser } = req;
  178. const wrappedBackend = wrapBackend(
  179. storeBackend,
  180. siteId,
  181. authenticatedUser
  182. );
  183. const wrappedFileBackend = wrapFileBackend(
  184. fileStoreBackend,
  185. siteId,
  186. authenticatedUser
  187. );
  188. return { store: wrappedBackend, fileStore: wrappedFileBackend };
  189. },
  190. functions: (req) => {
  191. const { siteId } = req;
  192. return functionsBySite[siteId];
  193. },
  194. })
  195. );
  196. // Schedule daily and hourly actions
  197. schedule.scheduleJob('22 * * * *', () => {
  198. log.info('Execute hourly actions');
  199. for (const key in schedulesBySite) {
  200. const { hourly } = schedulesBySite[key];
  201. hourly.forEach((callback) => {
  202. callback();
  203. });
  204. }
  205. });
  206. schedule.scheduleJob('42 3 * * *', () => {
  207. log.info('Execute daily actions');
  208. for (const key in schedulesBySite) {
  209. const { daily } = schedulesBySite[key];
  210. daily.forEach((callback) => {
  211. callback();
  212. });
  213. }
  214. });
  215. return router;
  216. };
  217. export const mainMiddleware = ({
  218. serverUrl,
  219. serverName,
  220. siteRegistrationEnabled,
  221. fileStoreConfig = {},
  222. storeConfig = {},
  223. configFile = './site.json',
  224. emailConfig = { host: 'fake' },
  225. ...rest
  226. } = {}) => {
  227. const router = express.Router();
  228. const fakeEmail = emailConfig.host === 'fake';
  229. let _transporter = null;
  230. const getTransporter = () => {
  231. const transportConfig =
  232. emailConfig.host === 'fake'
  233. ? {
  234. streamTransport: true,
  235. newline: 'unix',
  236. buffer: true,
  237. }
  238. : emailConfig;
  239. if (_transporter === null) {
  240. _transporter = nodemailer.createTransport({
  241. ...transportConfig,
  242. });
  243. }
  244. return _transporter;
  245. };
  246. // Store backends
  247. const storeBackend = getStoreBackend(storeConfig.type, storeConfig);
  248. const fileStoreBackend = getFileStoreBackend(fileStoreConfig.type, {
  249. url: fileStoreConfig.apiUrl,
  250. destination: fileStoreConfig.diskDestination,
  251. bucket: fileStoreConfig.s3Bucket,
  252. endpoint: fileStoreConfig.s3Endpoint,
  253. accessKey: fileStoreConfig.s3AccessKey,
  254. secretKey: fileStoreConfig.s3SecretKey,
  255. region: fileStoreConfig.s3Region,
  256. proxy: fileStoreConfig.s3Proxy,
  257. cdn: fileStoreConfig.s3Cdn,
  258. signedUrl: fileStoreConfig.s3SignedUrl,
  259. });
  260. const onSiteCreation = async ({ req, site, confirmPath }) => {
  261. const { t } = req;
  262. const confirmURL = `${serverUrl}${confirmPath}`;
  263. if (fakeEmail) {
  264. log.info(
  265. t('Site creation text message', {
  266. url: confirmURL,
  267. siteId: site._id,
  268. siteName: serverName,
  269. interpolation: { escapeValue: false },
  270. })
  271. );
  272. }
  273. await getTransporter().sendMail({
  274. from: emailConfig.from,
  275. to: site.owner,
  276. subject: t('Please confirm site creation'),
  277. text: t('Site creation text message', {
  278. url: confirmURL,
  279. siteId: site._id,
  280. siteName: serverName,
  281. }),
  282. html: t('Site creation html message', {
  283. url: confirmURL,
  284. siteId: site._id,
  285. siteName: serverName,
  286. }),
  287. });
  288. };
  289. const onSiteUpdate = async ({ req, previous, confirmPath }) => {
  290. const { t } = req;
  291. const confirmURL = `${serverUrl}${confirmPath}`;
  292. if (fakeEmail) {
  293. log.info(
  294. t('Site update text message', {
  295. url: confirmURL,
  296. siteId: previous._id,
  297. siteName: serverName,
  298. interpolation: { escapeValue: false },
  299. })
  300. );
  301. }
  302. await getTransporter().sendMail({
  303. from: emailConfig.from,
  304. to: previous.owner,
  305. subject: t('Please confirm site update'),
  306. text: t('Site update text message', {
  307. url: confirmURL,
  308. siteId: previous._id,
  309. siteName: serverName,
  310. }),
  311. html: t('Site update html message', {
  312. url: confirmURL,
  313. siteId: previous._id,
  314. siteName: serverName,
  315. }),
  316. });
  317. };
  318. router.use(
  319. site({
  320. configFile,
  321. storeBackend,
  322. siteRegistrationEnabled,
  323. onSiteCreation,
  324. onSiteUpdate,
  325. })
  326. );
  327. router.use(
  328. '/:siteId',
  329. (req, res, next) => {
  330. req.siteId = req.params.siteId;
  331. next();
  332. },
  333. origin(),
  334. ricochetMiddleware({
  335. fakeEmail,
  336. storePrefix: storeConfig.prefix,
  337. storeBackend,
  338. fileStoreBackend,
  339. getTransporter,
  340. ...rest,
  341. })
  342. );
  343. return router;
  344. };
  345. export default mainMiddleware;