dev-mailbox.ts 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
  1. import { LanguageCode } from '@vendure/common/lib/generated-types';
  2. import { Channel, RequestContext } from '@vendure/core';
  3. import { Request, Router } from 'express';
  4. import fs from 'fs-extra';
  5. import path from 'path';
  6. import { EmailEventHandler } from './handler/event-handler';
  7. import { EmailPluginDevModeOptions, EventWithContext } from './types';
  8. /**
  9. * An email inbox application that serves the contents of the dev mode `outputPath` directory.
  10. */
  11. export class DevMailbox {
  12. private handleMockEventFn: (
  13. handler: EmailEventHandler<string, any>,
  14. event: EventWithContext,
  15. ) => void | undefined;
  16. serve(options: EmailPluginDevModeOptions): Router {
  17. const { outputPath, handlers } = options;
  18. const server = Router();
  19. server.get('/', (req, res) => {
  20. res.sendFile('dev-mailbox.html', { root: path.join(__dirname, '../..') });
  21. });
  22. server.get('/list', async (req, res) => {
  23. const list = await fs.readdir(outputPath);
  24. const contents = await this.getEmailList(outputPath);
  25. res.send(contents);
  26. });
  27. server.get('/types', async (req, res) => {
  28. res.send(handlers.map(h => h.type));
  29. });
  30. server.get('/generate/:type/:languageCode', async (req, res) => {
  31. const { type, languageCode } = req.params;
  32. if (this.handleMockEventFn) {
  33. const handler = handlers.find(h => h.type === type);
  34. if (!handler || !handler.mockEvent) {
  35. res.statusCode = 404;
  36. res.send({ success: false, error: `No mock event registered for type "${type}"` });
  37. return;
  38. }
  39. try {
  40. this.handleMockEventFn(handler, {
  41. ...handler.mockEvent,
  42. ctx: this.createRequestContext(languageCode as LanguageCode, req),
  43. } as EventWithContext);
  44. res.send({ success: true });
  45. } catch (e: any) {
  46. res.statusCode = 500;
  47. res.send({ success: false, error: e.message });
  48. }
  49. return;
  50. } else {
  51. res.send({ success: false, error: 'Mock email generation not set up.' });
  52. }
  53. });
  54. server.get('/item/:id', async (req, res) => {
  55. const fileName = req.params.id;
  56. const content = await this.getEmail(outputPath, fileName);
  57. res.send(content);
  58. });
  59. server.get('/placeholder-image', async (req, res) => {
  60. const img = Buffer.from(
  61. // eslint-disable-next-line max-len
  62. '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iJnfaemDXGV6R400ttQ0vzI8mSE7to7jvXnFACUUUUDCu++H27+zp8/d8zj8q4NVLsFUZJOAK9R8M6a2l6TFFJ/rG+ZvbPOKBMb4r/5AN1/u15dXo3jm8Fvo/lZw0rYH0HWvOaAQUUUUDCvU/Cv/ACL9l/uf1NeWV6n4V/5F+y/3P6mgTNWiiigkKKKKACiiigAooooAKKKKACiiigDnfHn/ACAf+2q/1rzmvRvHn/IB/wC2q/1rzmgpBRRRQMK6XwXrQ0+8NvKwEM3dj0Nc1SgkHI4NAHs/DDB5FcVr3ghy0lxYndk7jD/hS+GfGQAW2v2wBwsx/ka7KORJlDIwdT0KnNBOx5BcWNxasVmheNh2YU+1026vGCwwPIf9kV69tHpRgelAXOS8N+DvsrLc3oDSD7sfYe9dazBVJJwB1qOe4jtoy8rrGo7sa4bxN4u+3BrazYrD0aTpu/8ArUBuZ/irWjq2oELxDF8qjPU9zWJRRQUFFFFABXqfhX/kX7L/AHP6mvLK9T8K/wDIv2X+5/U0CZq0UUUEhRRRQAUUUUAFFFFABRRRQAUUUUAYHjiMyaC2P4XVj+tebV7BqFmmoWcsD9HGK8o1Cxl026kgmXDKcZ7H3FBSK1FFFAwooooAKuWerXmnsDBcPHjoucj8jVOigDpY/HmoR7dyxyY67h1ps3jrUZFYL5cZPQqOn51zlFAizdajc3rEzzvJnsTx+VVqKKBhRRRQAUUUUAFeq+GY2i0GzVhhgnT8TXnWh6TJq9/HCo+TOXbsBXqsMSwQpGowqjAoEx9FFFBIUUUUAFFFFABRRRQAUUUUAFFFFABVHVtFttYhCTplh91x1FXqKAOIuPh5Lu/cXSbf+mgP9Ki/4V5d/wDP1D+R/wAK7yigdzg/+FeXf/P1D+R/wo/4V5d/8/UP5H/Cu8ooC5wf/CvLv/n6h/I/4Uf8K8u/+fqH8j/hXeUUBc4P/hXl3/z9Q/kf8KP+FeXf/P1D+R/wrvKKAucH/wAK8u/+fqH8j/hR/wAK8u/+fqH8j/hXeUUBc4P/AIV5d/8AP1D+R/wo/wCFeXf/AD9Q/kf8K7yigLnB/wDCvLv/AJ+ofyP+FWLX4ekMDc3KkZ6Rg12lFAXKmm6VbaVD5dvGEB6nuat0UUCCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAP/9k=',
  63. 'base64',
  64. );
  65. res.writeHead(200, {
  66. 'Content-Type': 'image/png',
  67. 'Content-Length': img.length,
  68. });
  69. res.end(img);
  70. });
  71. return server;
  72. }
  73. handleMockEvent(handler: (handler: EmailEventHandler<string, any>, event: EventWithContext) => void) {
  74. this.handleMockEventFn = handler;
  75. }
  76. private async getEmailList(outputPath: string) {
  77. const list = await fs.readdir(outputPath);
  78. const contents: Array<{
  79. fileName: string;
  80. date: string;
  81. subject: string;
  82. recipient: string;
  83. }> = [];
  84. for (const fileName of list.filter(name => name.endsWith('.json'))) {
  85. const json = await fs.readFile(path.join(outputPath, fileName), 'utf-8');
  86. const content = JSON.parse(json);
  87. contents.push({
  88. fileName,
  89. date: content.date,
  90. subject: content.subject,
  91. recipient: content.recipient,
  92. });
  93. }
  94. contents.sort((a, b) => {
  95. return a.fileName < b.fileName ? 1 : -1;
  96. });
  97. return contents;
  98. }
  99. private async getEmail(outputPath: string, fileName: string) {
  100. const safeSuffix = path.normalize(fileName).replace(/^(\.\.(\/|\\|$))+/, '');
  101. const safeFilePath = path.join(outputPath, safeSuffix);
  102. const json = await fs.readFile(safeFilePath, 'utf-8');
  103. const content = JSON.parse(json);
  104. return content;
  105. }
  106. private createRequestContext(languageCode: LanguageCode, req: Request): RequestContext {
  107. return new RequestContext({
  108. languageCode,
  109. req,
  110. apiType: 'admin',
  111. session: {} as any,
  112. isAuthorized: false,
  113. authorizedAsOwnerOnly: true,
  114. channel: new Channel(),
  115. });
  116. }
  117. }