From 2bf074d85a94304aa51b5fe6e7e9bcfd7c007f3b Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Wed, 9 Oct 2024 13:00:16 -0400 Subject: [PATCH] feature/IO-2979-DST-Handling - Add LocalStack and Adjust local Emailing Signed-off-by: Dave Richer --- .dockerignore | 1 - Dockerfile | 5 +- _reference/dockerreadme.md | 5 ++ client/src/redux/user/user.sagas.js | 4 +- docker-compose.yml | 106 +++++++++++++++++++++------- server/email/mailer.js | 49 +++++++++++++ server/email/sendemail.js | 29 ++------ server/email/tasksEmails.js | 23 +----- 8 files changed, 151 insertions(+), 71 deletions(-) create mode 100644 server/email/mailer.js diff --git a/.dockerignore b/.dockerignore index da9a0d123..b6ef4a047 100644 --- a/.dockerignore +++ b/.dockerignore @@ -7,7 +7,6 @@ _reference client redis/dockerdata hasura -node_modules # Files to exclude .ebignore diff --git a/Dockerfile b/Dockerfile index dab748c1b..01a1522cd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -25,6 +25,9 @@ RUN dnf install -y \ # Set the working directory WORKDIR /app +# This is because our test route uses a git commit hash +RUN git config --global --add safe.directory /app + # Copy package.json and package-lock.json COPY package*.json ./ @@ -32,7 +35,7 @@ COPY package*.json ./ RUN npm install -g nodemon # Install dependencies -RUN npm install --omit=dev +RUN npm ci # Copy the rest of your application code COPY . . diff --git a/_reference/dockerreadme.md b/_reference/dockerreadme.md index b65c6b23d..9ae7c140e 100644 --- a/_reference/dockerreadme.md +++ b/_reference/dockerreadme.md @@ -138,6 +138,10 @@ sudo sysctl -p # Install Docker and Docker Compose in WSL2 - https://docs.docker.com/desktop/wsl/ +# Local Stack +- LocalStack Front end (Optional) - https://apps.microsoft.com/detail/9ntrnft9zws2?hl=en-us&gl=US +- http://localhost:4566/_aws/ses will allow you to see emails sent + # Docker Commands ## General `docker-compose` Commands: @@ -150,6 +154,7 @@ sudo sysctl -p 7. View running Containers: `docker-compose ps` 8. View a specific containers logs: `docker-compose logs ` 9. Scale services (multiple instances of a service): `docker-compose up --scale = -d` +10. Watch a specific containers logs in realtime with timestamps: `docker-compose logs -f --timestamps ` ## Volume Management Commands 1. List Docker volumes: `docker volume ls` diff --git a/client/src/redux/user/user.sagas.js b/client/src/redux/user/user.sagas.js index b399c0052..f565ea544 100644 --- a/client/src/redux/user/user.sagas.js +++ b/client/src/redux/user/user.sagas.js @@ -314,8 +314,8 @@ export function* SetAuthLevelFromShopDetails({ payload }) { try { const userEmail = yield select((state) => state.user.currentUser.email); try { - //console.log("Setting shop timezone."); - // dayjs.tz.setDefault(payload.timezone); + console.log("Setting shop timezone."); + day.tz.setDefault(payload.timezone); } catch (error) { console.log(error); } diff --git a/docker-compose.yml b/docker-compose.yml index 4e8ecf9d4..1bd0bc423 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,11 +1,14 @@ ############################# # Ports Exposed -# 4000 - Imex Node API -# 3333 - SocketIO Admin-UI -# 3334 - Redis-Insights +# 4000 - Imex Node API +# 4556 - LocalStack (Local AWS) +# 3333 - SocketIO Admin-UI (Optional) +# 3334 - Redis-Insights (Optional) ############################# services: + + # Redis Node 1 redis-node-1: build: context: ./redis @@ -22,7 +25,8 @@ services: interval: 10s timeout: 5s retries: 10 - + + # Redis Node 2 redis-node-2: build: context: ./redis @@ -40,6 +44,7 @@ services: timeout: 5s retries: 10 + # Redis Node 3 redis-node-3: build: context: ./redis @@ -57,6 +62,52 @@ services: timeout: 5s retries: 10 + # LocalStack: Used to emulate AWS services locally, currently setup for SES + localstack: + image: localstack/localstack + container_name: localstack + hostname: localstack + networks: + - redis-cluster-net + restart: always + volumes: + - /var/run/docker.sock:/var/run/docker.sock + environment: + - SERVICES=ses + - DEBUG=1 + - AWS_ACCESS_KEY_ID=test + - AWS_SECRET_ACCESS_KEY=test + - AWS_DEFAULT_REGION=ca-central-1 + ports: + - "4566:4566" + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:4566/_localstack/health"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 20s + + # AWS-CLI - Used in conjunction with LocalStack to set required permission to send emails + aws-cli: + image: amazon/aws-cli + container_name: aws-cli + hostname: aws-cli + networks: + - redis-cluster-net + depends_on: + localstack: + condition: service_healthy + environment: + - AWS_ACCESS_KEY_ID=test + - AWS_SECRET_ACCESS_KEY=test + - AWS_DEFAULT_REGION=ca-central-1 + entrypoint: /bin/sh -c + command: > + " + aws --endpoint-url=http://localstack:4566 ses verify-domain-identity --domain imex.online --region ca-central-1 + aws --endpoint-url=http://localstack:4566 ses verify-email-identity --email-address noreply@imex.online --region ca-central-1 + " + # Node App: The Main IMEX API node-app: build: context: . @@ -73,37 +124,44 @@ services: condition: service_healthy redis-node-3: condition: service_healthy + localstack: + condition: service_healthy + aws-cli: + condition: service_completed_successfully ports: - "4000:4000" volumes: - .:/app - - /app/node_modules + - node-app-npm-cache:/app/node_modules - socketio-admin-ui: - image: maitrungduc1410/socket.io-admin-ui - container_name: socketio-admin-ui - networks: - - redis-cluster-net - ports: - - "3333:80" - - redis-insight: - image: redislabs/redisinsight:latest - container_name: redis-insight - hostname: redis-insight - restart: always - ports: - - "3334:5540" - networks: - - redis-cluster-net - volumes: - - redis-insight-data:/db +# ## Optional Container to Observe SocketIO data +# socketio-admin-ui: +# image: maitrungduc1410/socket.io-admin-ui +# container_name: socketio-admin-ui +# networks: +# - redis-cluster-net +# ports: +# - "3333:80" + +# ##Optional Container to Observe Redis Cluster Data +# redis-insight: +# image: redislabs/redisinsight:latest +# container_name: redis-insight +# hostname: redis-insight +# restart: always +# ports: +# - "3334:5540" +# networks: +# - redis-cluster-net +# volumes: +# - redis-insight-data:/db networks: redis-cluster-net: driver: bridge volumes: + node-app-npm-cache: redis-node-1-data: redis-node-2-data: redis-node-3-data: diff --git a/server/email/mailer.js b/server/email/mailer.js new file mode 100644 index 000000000..78e02037e --- /dev/null +++ b/server/email/mailer.js @@ -0,0 +1,49 @@ +const { isString, isEmpty } = require("lodash"); +const { defaultProvider } = require("@aws-sdk/credential-provider-node"); +const { default: InstanceManager } = require("../utils/instanceMgr"); +const aws = require("@aws-sdk/client-ses"); +const nodemailer = require("nodemailer"); + +const isLocal = isString(process.env?.LOCALSTACK_HOSTNAME) && !isEmpty(process.env?.LOCALSTACK_HOSTNAME); + +const sesConfig = { + apiVersion: "latest", + credentials: defaultProvider(), + region: isLocal + ? "ca-central-1" + : InstanceManager({ + imex: "ca-central-1", + rome: "us-east-2" + }) +}; + +if (isLocal) { + sesConfig.endpoint = `http://${process.env.LOCALSTACK_HOSTNAME}:4566`; + console.log(`SES Mailer set to LocalStack end point: ${sesConfig.endpoint}`); +} + +const ses = new aws.SES(sesConfig); + +let transporter = nodemailer.createTransport({ + SES: { ses, aws } +}); + +if (isLocal) { + // Wrap the sendMail function to log the email contents to the console in local environment + const originalSendMail = transporter.sendMail.bind(transporter); + transporter.sendMail = async (mailOptions) => { + try { + const result = await originalSendMail(mailOptions); + console.log( + `Email sent successfully - From: ${result?.envelope?.from} - To: ${result?.envelope?.to} - MessageID ${result.messageId} - Check LocalStack to see message` + ); + + return result; + } catch (error) { + console.error("Failed to send email:", error); + throw error; + } + }; +} + +module.exports = transporter; diff --git a/server/email/sendemail.js b/server/email/sendemail.js index f1586dbec..97e8f74ad 100644 --- a/server/email/sendemail.js +++ b/server/email/sendemail.js @@ -3,30 +3,13 @@ require("dotenv").config({ path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`) }); const axios = require("axios"); -let nodemailer = require("nodemailer"); -let aws = require("@aws-sdk/client-ses"); -let { defaultProvider } = require("@aws-sdk/credential-provider-node"); const InstanceManager = require("../utils/instanceMgr").default; const logger = require("../utils/logger"); const client = require("../graphql-client/graphql-client").client; const queries = require("../graphql-client/queries"); const { isObject } = require("lodash"); const generateEmailTemplate = require("./generateTemplate"); -const moment = require("moment"); - -const ses = new aws.SES({ - // The key apiVersion is no longer supported in v3, and can be removed. - // @deprecated The client uses the "latest" apiVersion. - apiVersion: "latest", - credentials: defaultProvider(), - region: InstanceManager({ - imex: "ca-central-1", - rome: "us-east-2" - }) -}); -let transporter = nodemailer.createTransport({ - SES: { ses, aws } -}); +const mailer = require("./mailer"); // Get the image from the URL and return it as a base64 string const getImage = async (imageUrl) => { @@ -66,7 +49,7 @@ const logEmail = async (req, email) => { const sendServerEmail = async ({ subject, text }) => { if (process.env.NODE_ENV === undefined) return; try { - transporter.sendMail( + mailer.sendMail( { from: InstanceManager({ imex: `ImEX Online API - ${process.env.NODE_ENV} `, @@ -98,7 +81,7 @@ const sendServerEmail = async ({ subject, text }) => { const sendProManagerWelcomeEmail = async ({ to, subject, html }) => { try { - await transporter.sendMail({ + await mailer.sendMail({ from: `ProManager `, to, subject, @@ -112,7 +95,7 @@ const sendProManagerWelcomeEmail = async ({ to, subject, html }) => { const sendTaskEmail = async ({ to, subject, type = "text", html, text, attachments }) => { try { - transporter.sendMail( + mailer.sendMail( { from: InstanceManager({ imex: `ImEX Online `, @@ -166,7 +149,7 @@ const sendEmail = async (req, res) => { ); } - transporter.sendMail( + mailer.sendMail( { from: `${req.body.from.name} <${req.body.from.address}>`, replyTo: req.body.ReplyTo.Email, @@ -280,7 +263,7 @@ const emailBounce = async (req, res) => { status: "Bounced", context: message.bounce?.bouncedRecipients }); - transporter.sendMail( + mailer.sendMail( { from: InstanceMgr({ imex: `ImEX Online `, diff --git a/server/email/tasksEmails.js b/server/email/tasksEmails.js index 088c36a66..ea99a2d23 100644 --- a/server/email/tasksEmails.js +++ b/server/email/tasksEmails.js @@ -2,9 +2,6 @@ const path = require("path"); require("dotenv").config({ path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`) }); -let nodemailer = require("nodemailer"); -let aws = require("@aws-sdk/client-ses"); -let { defaultProvider } = require("@aws-sdk/credential-provider-node"); const InstanceManager = require("../utils/instanceMgr").default; const logger = require("../utils/logger"); const client = require("../graphql-client/graphql-client").client; @@ -12,20 +9,7 @@ const queries = require("../graphql-client/queries"); const generateEmailTemplate = require("./generateTemplate"); const moment = require("moment-timezone"); const { taskEmailQueue } = require("./tasksEmailsQueue"); - -const ses = new aws.SES({ - apiVersion: "latest", - credentials: defaultProvider(), - region: InstanceManager({ - imex: "ca-central-1", - rome: "us-east-2" - }) -}); - -const transporter = nodemailer.createTransport({ - SES: { ses, aws }, - sendingRate: InstanceManager({ imex: 40, rome: 10 }) -}); +const mailer = require("./mailer"); // Initialize the Tasks Email Queue const tasksEmailQueue = taskEmailQueue(); @@ -124,6 +108,7 @@ const generateTemplateArgs = (title, priority, description, dueDate, bodyshop, j * @param html * @param taskIds * @param successCallback + * @param requestInstance */ const sendMail = (type, to, subject, html, taskIds, successCallback, requestInstance) => { const fromEmails = InstanceManager({ @@ -134,7 +119,7 @@ const sendMail = (type, to, subject, html, taskIds, successCallback, requestInst : "Rome Online " }); - transporter.sendMail( + mailer.sendMail( { from: fromEmails, to, @@ -151,8 +136,6 @@ const sendMail = (type, to, subject, html, taskIds, successCallback, requestInst } } ); - // } - // }); }; /**