From 64a6a76994283dd5335679b0c4dcaec0c0083081 Mon Sep 17 00:00:00 2001 From: Lisa DeSpain Date: Wed, 7 Sep 2022 13:17:29 -0500 Subject: [PATCH 01/13] initial commit with working email service on profile get by id --- .gitignore | 3 ++ README.md | 45 ++++++++++++------- api/email/emailHelper.js | 16 +++++++ api/profile/profileRouter.js | 9 ++++ package-lock.json | 84 ++++++++++++++++++++++++++++++++++-- package.json | 2 + 6 files changed, 141 insertions(+), 18 deletions(-) create mode 100644 api/email/emailHelper.js diff --git a/.gitignore b/.gitignore index a48bd98..4bd8dc6 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,6 @@ jspm_packages/ .env .env.test .env.local +sendgrid.env +sendgrid.env +sendgrid.env diff --git a/README.md b/README.md index 56f693a..2e3389a 100644 --- a/README.md +++ b/README.md @@ -72,18 +72,17 @@ avatarUr(not required): string, } ``` -| Method | URL | Description | -| -------- | ------------------------- | --------------------------------------------------------------------------------- | -| [GET] | /children | Returns an array containing all existing children.| -| [POST] | /children | Requires a username, name, and age. Returns the name, profile_id, and parent_id.| -| [GET] | /children/:child_id | Returns the child with the given 'id'.| -| [PUT] | /children/:child_id | Returns the updated child object| -| [DELETE] | /children/:child_id | Returns the name of the child deleted| -| [GET] | /children/:child_id/enrollments | Returns an array filled with event objects with the specified `id`. | -| [POST] | /children/:child_id/enrollments | Returns the event object with the specified `id`. Enrolls a student. | -| [PUT] | /children/enrollments/ | Returns the event object with the specified `id`. Updates a student's enrollments. (Not Implemented)| -| [DELETE] | /children/enrollments/:id | Returns the event object with the specified `id`. Unenrolls student from course. (Not Implemented)| - +| Method | URL | Description | +| -------- | ------------------------------- | ----------------------------------------------------------------------------------------------------------- | +| [GET] | /children | Returns an array containing all existing children. | +| [POST] | /children | Requires a username, name, and age. Returns the name, profile_id, and parent_id. | +| [GET] | /children/:child_id | Returns the child with the given 'id'. | +| [PUT] | /children/:child_id | Returns the updated child object | +| [DELETE] | /children/:child_id | Returns the name of the child deleted | +| [GET] | /children/:child_id/enrollments | Returns an array filled with event objects with the specified `id`. | +| [POST] | /children/:child_id/enrollments | Returns the event object with the specified `id`. Enrolls a student. | +| [PUT] | /children/enrollments/ | Returns the event object with the specified `id`. Updates a student's enrollments. (Not Implemented) | +| [DELETE] | /children/enrollments/:id | Returns the event object with the specified `id`. Unenrolls student from course. (Not Implemented) |

Instructors

@@ -147,7 +146,7 @@ avatarUr(not required): string, | -------- | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------- | | [GET] | /course | Returns an array containing all course objects | | [GET] | /course/:course_id | Returns the course object with the specified `course_id`. | -| [POST] | /course | --needs to be fleshed out-- | +| [POST] | /course | --needs to be fleshed out-- | | [PUT] | /course/:course_id | Updates and returns the updated course object with the specified `course_id`. | | [DELETE] | /course/:course_id | Deletes the course object with the specified `course_id` and returns a message containing the deleted course_id on successful deletion | @@ -180,8 +179,8 @@ avatarUr(not required): string, } ``` -| Method | URL | Description | -| -------- | ------------------- | -------------------------------------------------------------------------------------------------------------------------------- | +| Method | URL | Description | +| -------- | ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | | [GET] | /conversation_id/ | Returns an array filled with inbox event objects. | | [GET] | /conversation_id/:profile_id/ | Retrieves an inbox with the specified inbox_id BUG(?): incorrectly labeled as profile_id in codebase rather than inbox_id | | [POST] | /conversation_id/ | Creates an inbox and returns the newly created inbox. | @@ -300,3 +299,19 @@ Visual Database Schema: https://dbdesigner.page.link/WTZRbVeTR7EzLvs86 \*Curr [Loom Video PT4](https://www.loom.com/share/7da5fc043d3149afb05876c28df9bd3d)
+ +

Email Service

+In api/email, the emailHelper.js file contains the function to send a message, called sendEmail. Any function that wants to use sendEmail will need to import it into their file (see profileRouter.js). Parameters can be passed into it, like toEmail, name, template_id or other parameters. The parameters to use are created in the file that's calling the sendEmail function. + +To set up SendGrid: + +- Create an account with SendGrid +- Create an API (replace YOUR_API_KEY with the API you receive from SendGrid into the below terminal command) +- In your terminal, do these commands: + echo "export SENDGRID_API_KEY='YOUR_API_KEY'" > sendgrid.env + echo "sendgrid.env" >> .gitignore + source ./sendgrid.env + +SendGrid will look at its own .env file for the API key and any other environment variables you wish to send it, such as the SENDGRID_FROM_EMAIL. (Send new keys via the first and third commands listed above. The second one only adds sendgrid.env to .gitignore so you don't share secrets.) When accessing SendGrid's env file, use process.env.NAME_OF_VARIABLE. + +Templates exist on SendGrid's site, under "Dynamic Templates." When requesting an email be sent, you'll use the template_id as one of the parameters you pass to SendEmail. diff --git a/api/email/emailHelper.js b/api/email/emailHelper.js new file mode 100644 index 0000000..eb1fe0c --- /dev/null +++ b/api/email/emailHelper.js @@ -0,0 +1,16 @@ +const sgMail = require('@sendgrid/mail'); +sgMail.setApiKey(process.env.SENDGRID_API_KEY); + +const sendEmail = (msg) => { + sgMail + .send(msg) + .then((response) => { + console.log(response[0].statusCode); + console.log(response[0].headers); + }) + .catch((error) => { + console.error(error); + }); +}; + +module.exports = { sendEmail }; diff --git a/api/profile/profileRouter.js b/api/profile/profileRouter.js index 403b499..c326b79 100644 --- a/api/profile/profileRouter.js +++ b/api/profile/profileRouter.js @@ -2,6 +2,7 @@ const express = require('express'); const authRequired = require('../middleware/authRequired'); const ownerAuthorization = require('../middleware/ownerAuthorization'); const Profiles = require('./profileModel'); +const { sendEmail } = require('../email/emailHelper'); const router = express.Router(); const { checkProfileObject, @@ -36,6 +37,14 @@ router.get( }); } else { res.status(200).json(foundProfile); + const instructorWelcomeMessage = { + to: 'lisamdespain@gmail.com', + // to: foundProfile.email, // Change to your recipient + from: process.env.SENDGRID_FROM_EMAIL, // Change to your verified sender + template_id: 'd-a4de80911362438bb35d481efa068398', + name: foundProfile.name, + }; + sendEmail(instructorWelcomeMessage); } } ); diff --git a/package-lock.json b/package-lock.json index 5c8f40d..5030950 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@okta/jwt-verifier": "^2.0.0", "@okta/okta-sdk-nodejs": "^6.4.0", + "@sendgrid/mail": "^7.7.0", "axios": "^0.21.1", "cookie-parser": "~1.4.4", "cors": "^2.8.5", @@ -1355,6 +1356,49 @@ "node": ">=12.0" } }, + "node_modules/@sendgrid/client": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@sendgrid/client/-/client-7.7.0.tgz", + "integrity": "sha512-SxH+y8jeAQSnDavrTD0uGDXYIIkFylCo+eDofVmZLQ0f862nnqbC3Vd1ej6b7Le7lboyzQF6F7Fodv02rYspuA==", + "dependencies": { + "@sendgrid/helpers": "^7.7.0", + "axios": "^0.26.0" + }, + "engines": { + "node": "6.* || 8.* || >=10.*" + } + }, + "node_modules/@sendgrid/client/node_modules/axios": { + "version": "0.26.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz", + "integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==", + "dependencies": { + "follow-redirects": "^1.14.8" + } + }, + "node_modules/@sendgrid/helpers": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@sendgrid/helpers/-/helpers-7.7.0.tgz", + "integrity": "sha512-3AsAxfN3GDBcXoZ/y1mzAAbKzTtUZ5+ZrHOmWQ279AuaFXUNCh9bPnRpN504bgveTqoW+11IzPg3I0WVgDINpw==", + "dependencies": { + "deepmerge": "^4.2.2" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/@sendgrid/mail": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@sendgrid/mail/-/mail-7.7.0.tgz", + "integrity": "sha512-5+nApPE9wINBvHSUxwOxkkQqM/IAAaBYoP9hw7WwgDNQPxraruVqHizeTitVtKGiqWCKm2mnjh4XGN3fvFLqaw==", + "dependencies": { + "@sendgrid/client": "^7.7.0", + "@sendgrid/helpers": "^7.7.0" + }, + "engines": { + "node": "6.* || 8.* || >=10.*" + } + }, "node_modules/@sindresorhus/is": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", @@ -3157,7 +3201,6 @@ "version": "4.2.2", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -13274,6 +13317,42 @@ "safe-flat": "^2.0.2" } }, + "@sendgrid/client": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@sendgrid/client/-/client-7.7.0.tgz", + "integrity": "sha512-SxH+y8jeAQSnDavrTD0uGDXYIIkFylCo+eDofVmZLQ0f862nnqbC3Vd1ej6b7Le7lboyzQF6F7Fodv02rYspuA==", + "requires": { + "@sendgrid/helpers": "^7.7.0", + "axios": "^0.26.0" + }, + "dependencies": { + "axios": { + "version": "0.26.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz", + "integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==", + "requires": { + "follow-redirects": "^1.14.8" + } + } + } + }, + "@sendgrid/helpers": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@sendgrid/helpers/-/helpers-7.7.0.tgz", + "integrity": "sha512-3AsAxfN3GDBcXoZ/y1mzAAbKzTtUZ5+ZrHOmWQ279AuaFXUNCh9bPnRpN504bgveTqoW+11IzPg3I0WVgDINpw==", + "requires": { + "deepmerge": "^4.2.2" + } + }, + "@sendgrid/mail": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@sendgrid/mail/-/mail-7.7.0.tgz", + "integrity": "sha512-5+nApPE9wINBvHSUxwOxkkQqM/IAAaBYoP9hw7WwgDNQPxraruVqHizeTitVtKGiqWCKm2mnjh4XGN3fvFLqaw==", + "requires": { + "@sendgrid/client": "^7.7.0", + "@sendgrid/helpers": "^7.7.0" + } + }, "@sindresorhus/is": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", @@ -14696,8 +14775,7 @@ "deepmerge": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", - "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", - "dev": true + "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==" }, "defer-to-connect": { "version": "1.1.3", diff --git a/package.json b/package.json index df17cf4..2905147 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "migrate": "knex migrate:latest --knexfile config/knexfile.js", "rollback": "knex migrate:rollback --knexfile config/knexfile.js", "watch:dev": "nodemon", + "email": "node api/email/emailTest.js", "lint": "npx eslint .", "lint:fix": "npx eslint --fix .", "format": "npx prettier --write \"**/*.+(js|jsx|json|yml|yaml|css|md)\"", @@ -64,6 +65,7 @@ "dependencies": { "@okta/jwt-verifier": "^2.0.0", "@okta/okta-sdk-nodejs": "^6.4.0", + "@sendgrid/mail": "^7.7.0", "axios": "^0.21.1", "cookie-parser": "~1.4.4", "cors": "^2.8.5", From 07cdbb303e348296bb5274e5aa51f56e1e36c4ce Mon Sep 17 00:00:00 2001 From: Lisa DeSpain Date: Tue, 13 Sep 2022 11:17:30 -0500 Subject: [PATCH 02/13] profileRouter is sending conditional requests to emailHelper, README is updated, SendGrid installed --- README.md | 36 ++++++++- __tests__/routes/emailHelper.test.js | 104 ++++++++++++++++++++++++++ api/email/emailHelper.js | 38 ++++++++-- api/profile/profileRouter.js | 105 ++++++++++++++++++++------- package.json | 2 +- 5 files changed, 250 insertions(+), 35 deletions(-) create mode 100644 __tests__/routes/emailHelper.test.js diff --git a/README.md b/README.md index 2e3389a..bb5f2f7 100644 --- a/README.md +++ b/README.md @@ -301,9 +301,14 @@ Visual Database Schema: https://dbdesigner.page.link/WTZRbVeTR7EzLvs86 \*Curr

Email Service

-In api/email, the emailHelper.js file contains the function to send a message, called sendEmail. Any function that wants to use sendEmail will need to import it into their file (see profileRouter.js). Parameters can be passed into it, like toEmail, name, template_id or other parameters. The parameters to use are created in the file that's calling the sendEmail function. +In api/email, the emailHelper.js file contains the following functions: sendEmail and addToList. SendEmail sends an API request to SendGrid to send a templated email to the to email address its given. The other function: addToList adds the email and name to a specified SendGrid contact list (they're added to all no matter what, then also to the specified list id). Any function that wants to use sendEmail and addToList will need to import it into their file (see profileRouter.js). Parameters can be passed into it, like the toEmail, name, template_id or list_ids (which is an array so it could have more than 1 list_ids there). The parameters to use are created in the file that's calling the sendEmail function. -To set up SendGrid: +SendGrid's npm package info and how to set up the API key (also listed below): https://www.npmjs.com/package/@sendgrid/mail. The npm package is @sendgrid/mail. Very lightweight, highly supported, and heavily downloaded. (SendGrid also supports Java) + +Loom walkthrough on backend: https://www.loom.com/share/a7d867ee6f9f4bca9a4a3e911bca3de4 +Loom how to see if it's working: https://www.loom.com/share/7acaa082c8454ac4bf17e0670aec18d7 + +To set up SendGrid at https://sendgrid.com/: - Create an account with SendGrid - Create an API (replace YOUR_API_KEY with the API you receive from SendGrid into the below terminal command) @@ -312,6 +317,29 @@ To set up SendGrid: echo "sendgrid.env" >> .gitignore source ./sendgrid.env -SendGrid will look at its own .env file for the API key and any other environment variables you wish to send it, such as the SENDGRID_FROM_EMAIL. (Send new keys via the first and third commands listed above. The second one only adds sendgrid.env to .gitignore so you don't share secrets.) When accessing SendGrid's env file, use process.env.NAME_OF_VARIABLE. +SendGrid will look at its own .env file for the API key. Unclear as to whether it's possible to put other environment variables there - it's been inconsistent in testing. (Send new keys via the first and third commands listed above. The second one only adds sendgrid.env to .gitignore so you don't share secrets.) When accessing SendGrid's .env file, use process.env.SENDGRID_API_KEY, just as if you had put the key in the .env file. + +Templates exist on SendGrid's site, under "Dynamic Templates." When requesting an email be sent, you'll use the template_id as one of the parameters you pass on to sendEmail. If you want a starting spot for what your emails could say (pending stakeholder approval), here's some copy: https://docs.google.com/document/d/1WZQ6Njj0Xt_eXLAEWm0nYA7eqACByFinpyrh7EjcU8U/edit?usp=sharing + +Dynamic template data: the template will be looking for something from the json request that matches the field you put in the template. For example, "name": "Some Name" coming from your email request will match {{ name }} in the template. Double curly braces make that connection. + +Potential templates to be created plus an idea for what the template_id will look like (could update this list as the templates are created on SendGrid - the template_id is generated by SendGrid when you create a Dynamic Template): + +EmailType = { +WELCOME_EMAIL_PARENT: 'd-026a2f461bdd480098be08a2cb949eea', (All 3 welcome emails go out automatically upon post from the profileRouter. Change template id based on role_id.) +WELCOME_EMAIL_INSTRUCTOR: 'd-a4de80911362438bb35d481efa068398', +WELCOME_EMAIL_STUDENT: 'd-026a2f461bdd480098be08a2cb949eea', +INSTRUCTOR_SUBMITTED_APPLICATION: 'd-026a2f461bdd480098be08bxhsyeyyy', (Goes out when an instructor submits an application and lets them know about the next step in the approval process.) +INSTRUCTOR_APPLICATION_REVIEW: 'd-026a2f461bdd480098be08bxhsyeyyy', email to admin or staff that an application is ready to be approved or denied +CHANGE_PASSWORD: 'd-026a2f4hsyw20098be08a2cb949eea', +PURCHASED_COURSE_PARENT: 'd-026a2f461bdd480098be08bxhsyeyyy', (Upon enrollment of a child student, to include the start date, time, and Zoom link) +PURCHASED_COURSE_STUDENT: 'd-026a2f461bdd480098be08bxhsyeyyy', (Upon enrollment of a child student, email sent to student to include the start date, time, and Zoom link) +INSTRUCTOR_IS_APPROVED: 'd-026a2f461bdd480098be08bxhsyeyyy', (Congrats, you've been approved, with link to create a course) +INSTRUCTOR_IS_REJECTED: 'd-026a2f461bdd480098be08bxhsyeyyy', (Sorry, friend! We'll keep your info on file for future needs) +STUDENT_CLASS_REMINDER: 'd-026a2f461bdd480098be08bxhsyeyyy', (Could be 2 emails to land 1 hour, or 5 minutes before class starts, with the class start time and Zoom link) +PARENT_CLASS_REMINDER: 'd-026a2f461bdd480098be08bxhsyeyyy', (Same as above with different wording) +}; + +AddToList: First up , add list(s) to the SendGrid account. In addition to the default "all" list, we added instructors, parents, and students. From a marketing perspective, these are the major groups, each requiring a different kind of information, and will potentially need different mass email types. -Templates exist on SendGrid's site, under "Dynamic Templates." When requesting an email be sent, you'll use the template_id as one of the parameters you pass to SendEmail. +As part of the request in emailHelper.js, you'll send the id(s) of the contact list (list_ids, an array) you want to add the email to, plus any additional data you want to add to the request, like the email address (as 'email') and name (as 'first_name'). If you use custom fields, make sure the fields are already set up in SendGrid or SG won't know where to map them. diff --git a/__tests__/routes/emailHelper.test.js b/__tests__/routes/emailHelper.test.js new file mode 100644 index 0000000..ea9d8c2 --- /dev/null +++ b/__tests__/routes/emailHelper.test.js @@ -0,0 +1,104 @@ +const express = require('express'); + +const { sendEmail, addToList } = require('../../api/email/emailHelper'); +const server = express(); + +server.use(express.json()); + +jest.mock('../../api/email/emailHelper.js'); +jest.mock('../../api/email/emailHelper.js', () => + jest.fn((req, res, next) => next()) +); + +const newStudent = { + dynamic_template_data: { + name: 'New Student Here', + }, + to: 'someperson@somewhere.com', + from: 'someperson@somewhere.com', // verified sender in SendGrid account + template_id: 'd-a6dacc6241f9484a96554a13bbdcd971', +}; + +const newParent = { + dynamic_template_data: { + name: 'New Parent Here', + }, + to: 'someperson@somewhere.com', + from: 'someperson@somewhere.com', + template_id: 'd-19b895416ae74cea97e285c4401fcc1f', +}; + +const newInstructor = { + dynamic_template_data: { + name: 'New Parent Here', + }, + to: 'someperson@somewhere.com', + from: 'someperson@somewhere.com', + template_id: 'd-a4de80911362438bb35d481efa068398', +}; + +const newStudentContact = { + list_ids: ['e7b598d9-23ca-48df-a62b-53470b5d1d86'], + email: 'someperson@somewhere.com', + name: 'new student firstname', +}; + +const newParentContact = { + list_ids: ['e7b598d9-23ca-48df-a62b-53470b5d1d86'], + email: 'someperson@somewhere.com', + name: 'new parent firstname', +}; + +const newInstructorContact = { + list_ids: ['e7b598d9-23ca-48df-a62b-53470b5d1d86'], + email: 'someperson@somewhere.com', + name: 'new instructor firstname', +}; + +describe('Send different email types', () => { + describe('Send an email to a new student', () => { + it('Should return 202 when it successfully posts a new student email to SendGrid', async () => { + const res = await sendEmail(newStudent); + expect(res.status).toBe(202); + expect(res.headers.date.length).not.toBe(0); + }); + }); + describe('Send an email to a new parent', () => { + it('Should return 202 when it successfully posts a new parent email to SendGrid', async () => { + const res = await sendEmail(newParent); + expect(res.status).toBe(202); + expect(res.headers.date.length).not.toBe(0); + }); + }); + describe('Send an email to a new instructor', () => { + it('Should return 202 when it successfully posts a new instructor email to SendGrid', async () => { + const res = await sendEmail(newInstructor); + expect(res.status).toBe(202); + expect(res.headers.date.length).not.toBe(0); + }); + }); +}); + +describe('Add users to a contact list on SendGrid', () => { + describe('Add a new student to a contact list', () => { + it('Should return 202 when it successfully adds a new student to a contact list', async () => { + const res = await addToList(newStudentContact); + expect(res.status).toBe(202); + expect(res.headers.date.length).not.toBe(0); + }); + }); + describe('Add a new parent to a contact list', () => { + it('Should return 202 when it successfully adds a new parent to a contact list', async () => { + const res = await addToList(newParentContact); + expect(res.status).toBe(202); + expect(res.headers.date.length).not.toBe(0); + }); + }); + describe('Add a new instructor to a contact list', () => { + it('Should return 202 when it successfully adds a new instructor to a contact list', async () => { + const res = await addToList(newInstructorContact); + expect(res.status).toBe(202); + expect(res.headers.date.length).not.toBe(0); + }); + }); +}); diff --git a/api/email/emailHelper.js b/api/email/emailHelper.js index eb1fe0c..93612ab 100644 --- a/api/email/emailHelper.js +++ b/api/email/emailHelper.js @@ -1,16 +1,44 @@ const sgMail = require('@sendgrid/mail'); sgMail.setApiKey(process.env.SENDGRID_API_KEY); -const sendEmail = (msg) => { +const sendEmail = (data) => { sgMail - .send(msg) + .send(data) .then((response) => { - console.log(response[0].statusCode); - console.log(response[0].headers); + // note: the following 2 console logs are SendGrid out of the box. Keep them if you like them. We found the stringify response to be more descriptive. + // console.log(response[0].statusCode); + // console.log(response[0].headers); + console.log(JSON.stringify(response)); }) .catch((error) => { console.error(error); }); }; -module.exports = { sendEmail }; +const addToList = (data) => { + let request = require('request'); + let options = { + method: 'PUT', + url: 'https://api.sendgrid.com/v3/marketing/contacts', + headers: { + 'content-type': 'application/json', + authorization: 'Bearer ' + process.env.SENDGRID_API_KEY, + }, + body: { + list_ids: data.list_ids, + contacts: [ + { + email: data.email, + first_name: data.name, + }, + ], + }, + json: true, + }; + request(options, function (error, response, body) { + if (error) throw new Error(error); + console.log(body); + }); +}; + +module.exports = { sendEmail, addToList }; diff --git a/api/profile/profileRouter.js b/api/profile/profileRouter.js index c326b79..7dfc7cf 100644 --- a/api/profile/profileRouter.js +++ b/api/profile/profileRouter.js @@ -2,7 +2,7 @@ const express = require('express'); const authRequired = require('../middleware/authRequired'); const ownerAuthorization = require('../middleware/ownerAuthorization'); const Profiles = require('./profileModel'); -const { sendEmail } = require('../email/emailHelper'); +const { sendEmail, addToList } = require('../email/emailHelper'); const router = express.Router(); const { checkProfileObject, @@ -37,14 +37,6 @@ router.get( }); } else { res.status(200).json(foundProfile); - const instructorWelcomeMessage = { - to: 'lisamdespain@gmail.com', - // to: foundProfile.email, // Change to your recipient - from: process.env.SENDGRID_FROM_EMAIL, // Change to your verified sender - template_id: 'd-a4de80911362438bb35d481efa068398', - name: foundProfile.name, - }; - sendEmail(instructorWelcomeMessage); } } ); @@ -168,7 +160,7 @@ router.get( * @swagger * /profile: * post: - * summary: Add a profile + * summary: Add a profile, send a welcome email, add to contact list (all by default, then specified per role) * security: * - okta: [] * tags: @@ -202,23 +194,86 @@ router.get( */ router.post('/', checkProfileObject, async (req, res) => { const profile = req.body; - try { - await Profiles.findById(profile.okta_id).then(async (pf) => { - if (pf == undefined) { - await Profiles.create(profile).then((profile) => - res - .status(200) - .json({ message: 'profile created', profile: profile[0] }) - ); - } else { - res.status(400).json({ message: 'profile already exists' }); - } - }); - } catch (e) { - console.error(e); - res.status(500).json({ message: e.message }); + const profileExists = await Profiles.findById(profile.okta_id); + if (profileExists) { + res.status(400).json({ message: 'profile already exists' }); + } else { + const prof = await Profiles.create(profile); + if (!prof) { + res.status(404).json({ + message: 'There was an error saving the profile to the database.', + }); + } + if (prof[0].role_id === 3 || prof[0].role_id === '3') { + console.log(prof.profile_id); + const instructorWelcomeMessage = { + dynamic_template_data: { + name: prof[0].name, + }, + to: prof[0].email, + from: 'someone@somewhere.com', // verified sender in SendGrid account. Try to put this in env - hardcoded here because it wasn't working there. + template_id: 'd-a4de80911362438bb35d481efa068398', + }; + const instructorList = { + list_ids: ['e7b598d9-23ca-48df-a62b-53470b5d1d86'], + email: prof[0].email, + name: prof[0].name, + }; + sendEmail(instructorWelcomeMessage); + addToList(instructorList); + res.status(200).json({ + message: 'instructor profile created', + profile: prof[0], + }); + } else if (prof[0].role_id === 4 || prof[0].role_id === '4') { + const parentWelcomeMessage = { + dynamic_template_data: { + name: prof[0].name, + }, + to: prof[0].email, + from: 'someone@somewhere.com', + template_id: 'd-19b895416ae74cea97e285c4401fcc1f', + }; + const parentList = { + list: 'e7b598d9-23ca-48df-a62b-53470b5d1d86', + email: prof[0].email, + name: prof[0].name, + }; + sendEmail(parentWelcomeMessage); + addToList(parentList); + res.status(200).json({ + message: 'parent profile created', + profile: prof[0], + }); + } else if (prof[0].role_id === 5 || prof[0].role_id === '5') { + const studentWelcomeMessage = { + dynamic_template_data: { + name: prof[0].name, + }, + to: prof[0].email, + from: 'someone@somewhere.com', + template_id: 'd-a6dacc6241f9484a96554a13bbdcd971', + }; + const studentList = { + list: '4dd72555-266f-4f8e-b595-ecc1f7ff8f28', + email: prof[0].email, + name: prof[0].name, + }; + sendEmail(studentWelcomeMessage); + addToList(studentList); + res.status(200).json({ + message: 'parent profile created', + profile: prof[0], + }); + } else { + res.status(200).json({ + message: 'profile created', + profile: prof[0], + }); + } } }); + /** * @swagger * /profile: diff --git a/package.json b/package.json index 2905147..107f360 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "migrate": "knex migrate:latest --knexfile config/knexfile.js", "rollback": "knex migrate:rollback --knexfile config/knexfile.js", "watch:dev": "nodemon", - "email": "node api/email/emailTest.js", + "email": "node api/email/emailHelper.js", "lint": "npx eslint .", "lint:fix": "npx eslint --fix .", "format": "npx prettier --write \"**/*.+(js|jsx|json|yml|yaml|css|md)\"", From 7da3efe9f5ae0f326e46f8d8040ff339f875083b Mon Sep 17 00:00:00 2001 From: Lisa DeSpain Date: Tue, 13 Sep 2022 11:29:34 -0500 Subject: [PATCH 03/13] removed a stray console log --- api/profile/profileRouter.js | 1 - 1 file changed, 1 deletion(-) diff --git a/api/profile/profileRouter.js b/api/profile/profileRouter.js index 7dfc7cf..4a87235 100644 --- a/api/profile/profileRouter.js +++ b/api/profile/profileRouter.js @@ -205,7 +205,6 @@ router.post('/', checkProfileObject, async (req, res) => { }); } if (prof[0].role_id === 3 || prof[0].role_id === '3') { - console.log(prof.profile_id); const instructorWelcomeMessage = { dynamic_template_data: { name: prof[0].name, From c5b3b6c75040cd13f1f2dab0fb3c07250f20d188 Mon Sep 17 00:00:00 2001 From: Cassius-Cassity <80988165+Cassius-Cassity@users.noreply.github.com> Date: Fri, 12 Aug 2022 15:02:19 -0600 Subject: [PATCH 04/13] can now add child under the endpoint /parent --- api/parent/parentRouter.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/api/parent/parentRouter.js b/api/parent/parentRouter.js index 8218cf4..28ac80a 100644 --- a/api/parent/parentRouter.js +++ b/api/parent/parentRouter.js @@ -1,8 +1,19 @@ const express = require('express'); const authRequired = require('../middleware/authRequired'); const Parents = require('./parentModel'); +const Children = require('../children/childrenModel'); const router = express.Router(); +router.post('/', authRequired, async function (req, res, next) { + const { profile_id } = req.profile; + try { + let child = await Children.addChild(profile_id, req.body); + res.status(201).json(child); + } catch (error) { + next(error); + } +}); + router.get('/:profile_id/children', authRequired, function (req, res) { const { profile_id } = req.params; From 9cc20820e45e047453123b8675ce340bf79227cf Mon Sep 17 00:00:00 2001 From: Cassius-Cassity <80988165+Cassius-Cassity@users.noreply.github.com> Date: Mon, 15 Aug 2022 12:56:22 -0600 Subject: [PATCH 05/13] Ability for a Parent to update a child profile already built. --- api/parent/parentRouter.js | 45 +++++++++++++++++++++++++++++++------- 1 file changed, 37 insertions(+), 8 deletions(-) diff --git a/api/parent/parentRouter.js b/api/parent/parentRouter.js index 28ac80a..ebfa8b1 100644 --- a/api/parent/parentRouter.js +++ b/api/parent/parentRouter.js @@ -1,18 +1,47 @@ const express = require('express'); const authRequired = require('../middleware/authRequired'); +const { + roleAuthenticationParent, +} = require('../middleware/roleAuthentication.js'); +const { + checkChildObject, + checkChildExist, +} = require('../children/ChildrenMiddleware'); const Parents = require('./parentModel'); const Children = require('../children/childrenModel'); const router = express.Router(); -router.post('/', authRequired, async function (req, res, next) { - const { profile_id } = req.profile; - try { - let child = await Children.addChild(profile_id, req.body); - res.status(201).json(child); - } catch (error) { - next(error); +router.post( + '/', + authRequired, + roleAuthenticationParent, + checkChildObject, + async function (req, res, next) { + const { profile_id } = req.profile; + try { + let child = await Children.addChild(profile_id, req.body); + res.status(201).json(child); + } catch (error) { + next(error); + } } -}); +); + +router.put( + '/:child id', + authRequired, + roleAuthenticationParent, + checkChildExist, + async function (req, res, next) { + const { child_id } = req.params; + try { + let [child] = await Children.updateChild(child_id, req.body); + res.status(200).json(child); + } catch (error) { + next(error); + } + } +); router.get('/:profile_id/children', authRequired, function (req, res) { const { profile_id } = req.params; From d2ba469b00d6e44b68982b93e6d2ec5619b584e3 Mon Sep 17 00:00:00 2001 From: Cassius-Cassity <80988165+Cassius-Cassity@users.noreply.github.com> Date: Mon, 15 Aug 2022 16:06:17 -0600 Subject: [PATCH 06/13] testing parent endpoints --- api/parent/parentRouter.js | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/api/parent/parentRouter.js b/api/parent/parentRouter.js index ebfa8b1..487946e 100644 --- a/api/parent/parentRouter.js +++ b/api/parent/parentRouter.js @@ -28,7 +28,7 @@ router.post( ); router.put( - '/:child id', + '/:child_id', authRequired, roleAuthenticationParent, checkChildExist, @@ -37,6 +37,23 @@ router.put( try { let [child] = await Children.updateChild(child_id, req.body); res.status(200).json(child); + console.log(res); + } catch (error) { + next(error); + } + } +); + +router.delete( + '/:child_id', + authRequired, + roleAuthenticationParent, + checkChildExist, + async function (req, res, next) { + const { child_id } = req.params; + try { + let { name } = await Children.removeChild(child_id); + res.status(200).json({ name }); } catch (error) { next(error); } From 735ea56456d0063d4513360fba64d931f31562c6 Mon Sep 17 00:00:00 2001 From: Cassius-Cassity <80988165+Cassius-Cassity@users.noreply.github.com> Date: Wed, 17 Aug 2022 17:39:16 -0600 Subject: [PATCH 07/13] parents are now able to get a child based on child id --- api/parent/parentRouter.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/api/parent/parentRouter.js b/api/parent/parentRouter.js index 487946e..1964c33 100644 --- a/api/parent/parentRouter.js +++ b/api/parent/parentRouter.js @@ -37,7 +37,6 @@ router.put( try { let [child] = await Children.updateChild(child_id, req.body); res.status(200).json(child); - console.log(res); } catch (error) { next(error); } @@ -60,6 +59,19 @@ router.delete( } ); +router.get( + '/:child_id', + authRequired, + checkChildExist, + async function (req, res, next) { + try { + res.status(200).json(req.child); + } catch (error) { + next(error); + } + } +); + router.get('/:profile_id/children', authRequired, function (req, res) { const { profile_id } = req.params; From e1ff24c390dddb0cdb6aac13beb85b0135a7f29b Mon Sep 17 00:00:00 2001 From: Cassius-Cassity <80988165+Cassius-Cassity@users.noreply.github.com> Date: Wed, 24 Aug 2022 18:25:09 -0600 Subject: [PATCH 08/13] refactored parent model and parent router --- api/parent/parentRouter.js | 21 ++++----------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/api/parent/parentRouter.js b/api/parent/parentRouter.js index 1964c33..15c2e62 100644 --- a/api/parent/parentRouter.js +++ b/api/parent/parentRouter.js @@ -19,8 +19,8 @@ router.post( async function (req, res, next) { const { profile_id } = req.profile; try { - let child = await Children.addChild(profile_id, req.body); - res.status(201).json(child); + let newChild = await Children.addChild(profile_id, req.body); + res.status(201).json(newChild); } catch (error) { next(error); } @@ -35,8 +35,8 @@ router.put( async function (req, res, next) { const { child_id } = req.params; try { - let [child] = await Children.updateChild(child_id, req.body); - res.status(200).json(child); + let [updatedChild] = await Children.updateChild(child_id, req.body); + res.status(200).json(updatedChild); } catch (error) { next(error); } @@ -59,19 +59,6 @@ router.delete( } ); -router.get( - '/:child_id', - authRequired, - checkChildExist, - async function (req, res, next) { - try { - res.status(200).json(req.child); - } catch (error) { - next(error); - } - } -); - router.get('/:profile_id/children', authRequired, function (req, res) { const { profile_id } = req.params; From ded32757df3529f850067a36debfd46708c85db4 Mon Sep 17 00:00:00 2001 From: Colya Foxworth Date: Tue, 30 Aug 2022 14:38:16 -0500 Subject: [PATCH 09/13] refactored courses model, updated seed file and migration file --- api/courses/coursesModel.js | 2 +- data/migrations/20211102124801_profiles.js | 1 + data/seeds/004_instructors.js | 2 ++ 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/api/courses/coursesModel.js b/api/courses/coursesModel.js index b414949..5c6891c 100644 --- a/api/courses/coursesModel.js +++ b/api/courses/coursesModel.js @@ -2,7 +2,7 @@ const db = require('../../data/db-config'); const getAllCourses = async () => { return await db('courses as c') - .select('c.*', 'p.program_name', 'i.instructor_id') + .select('c.*', 'p.program_name', 'i.instructor_id', 'i.instructor_name') .leftJoin('programs as p', 'p.program_id', 'c.program_id') .leftJoin('instructors as i', 'c.instructor_id', 'i.instructor_id'); }; diff --git a/data/migrations/20211102124801_profiles.js b/data/migrations/20211102124801_profiles.js index f5bdff5..54794cd 100644 --- a/data/migrations/20211102124801_profiles.js +++ b/data/migrations/20211102124801_profiles.js @@ -56,6 +56,7 @@ exports.up = (knex) => { .createTable('instructors', function (table) { table.increments('instructor_id'); + table.string('instructor_name').notNullable() table.integer('rating').notNullable(); table.string('availability'); table.string('bio').notNullable(); diff --git a/data/seeds/004_instructors.js b/data/seeds/004_instructors.js index 777b489..dfd0af4 100644 --- a/data/seeds/004_instructors.js +++ b/data/seeds/004_instructors.js @@ -2,6 +2,7 @@ exports.seed = function (knex) { return knex('instructors').insert([ { profile_id: 3, + instructor_name: 'Brianne Caplan', rating: 2, bio: 'I love spaghetti and code, but not the two together.', status: false, @@ -9,6 +10,7 @@ exports.seed = function (knex) { }, { profile_id: 8, + instructor_name: 'Adam Smith', rating: 5, bio: 'Coding is life.', status: true, From 113ba6939200c3a6afc462cb2a21d5e39f363c06 Mon Sep 17 00:00:00 2001 From: Colya Foxworth Date: Tue, 30 Aug 2022 15:07:04 -0500 Subject: [PATCH 10/13] updated README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index bb5f2f7..e11b019 100644 --- a/README.md +++ b/README.md @@ -139,6 +139,7 @@ avatarUr(not required): string, end_date: DATE (required), location: STRING (required), number_of_sessions: INTEGER (required), + instructor_name: STRING (required) } ``` From fb3a011ec2ac87d57654c7db30d31385839d7dc3 Mon Sep 17 00:00:00 2001 From: Colya Foxworth Date: Tue, 30 Aug 2022 15:24:04 -0500 Subject: [PATCH 11/13] updated README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index e11b019..ac15a3b 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,7 @@ avatarUr(not required): string, ``` { instructor_id: INCREMENT (primary key, auto-increments, generated by database), + instructor_name: STRING (required), rating: INTEGER (required), availability: STRING (optional), bio: STRING (required), From d9119e93c258e15634390ae3416c18124efe20c1 Mon Sep 17 00:00:00 2001 From: Colya Foxworth Date: Tue, 30 Aug 2022 15:32:26 -0500 Subject: [PATCH 12/13] fixed lint error --- data/migrations/20211102124801_profiles.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/migrations/20211102124801_profiles.js b/data/migrations/20211102124801_profiles.js index 54794cd..0d97779 100644 --- a/data/migrations/20211102124801_profiles.js +++ b/data/migrations/20211102124801_profiles.js @@ -56,7 +56,7 @@ exports.up = (knex) => { .createTable('instructors', function (table) { table.increments('instructor_id'); - table.string('instructor_name').notNullable() + table.string('instructor_name').notNullable(); table.integer('rating').notNullable(); table.string('availability'); table.string('bio').notNullable(); From bcf8484aa91734f808da7b73ebb3472a81d533e3 Mon Sep 17 00:00:00 2001 From: Lisa DeSpain Date: Wed, 14 Sep 2022 09:01:20 -0500 Subject: [PATCH 13/13] made recommended changes --- .gitignore | 2 -- README.md | 2 +- __tests__/routes/emailHelper.test.js | 3 +- api/email/emailHelper.js | 3 -- api/profile/profileRouter.js | 42 ++++++++++++++-------------- 5 files changed, 24 insertions(+), 28 deletions(-) diff --git a/.gitignore b/.gitignore index 4bd8dc6..89f6ea7 100644 --- a/.gitignore +++ b/.gitignore @@ -40,5 +40,3 @@ jspm_packages/ .env.test .env.local sendgrid.env -sendgrid.env -sendgrid.env diff --git a/README.md b/README.md index ac15a3b..8403ae1 100644 --- a/README.md +++ b/README.md @@ -303,7 +303,7 @@ Visual Database Schema: https://dbdesigner.page.link/WTZRbVeTR7EzLvs86 \*Curr

Email Service

-In api/email, the emailHelper.js file contains the following functions: sendEmail and addToList. SendEmail sends an API request to SendGrid to send a templated email to the to email address its given. The other function: addToList adds the email and name to a specified SendGrid contact list (they're added to all no matter what, then also to the specified list id). Any function that wants to use sendEmail and addToList will need to import it into their file (see profileRouter.js). Parameters can be passed into it, like the toEmail, name, template_id or list_ids (which is an array so it could have more than 1 list_ids there). The parameters to use are created in the file that's calling the sendEmail function. +In api/email, the emailHelper.js file contains the following functions: sendEmail and addToList. SendEmail sends an API request to SendGrid to send a templated email to the email address its given. The other function: addToList adds the email and name to a specified SendGrid contact list (they're added to all no matter what, then also to the specified list id). Any function that wants to use sendEmail and addToList will need to import it into their file (see profileRouter.js). Parameters can be passed into it, like the toEmail, name, template_id or list_ids (which is an array so it could have more than 1 list_ids there). The parameters to use are created in the file that's calling the sendEmail function. SendGrid's npm package info and how to set up the API key (also listed below): https://www.npmjs.com/package/@sendgrid/mail. The npm package is @sendgrid/mail. Very lightweight, highly supported, and heavily downloaded. (SendGrid also supports Java) diff --git a/__tests__/routes/emailHelper.test.js b/__tests__/routes/emailHelper.test.js index ea9d8c2..5862a39 100644 --- a/__tests__/routes/emailHelper.test.js +++ b/__tests__/routes/emailHelper.test.js @@ -15,7 +15,8 @@ const newStudent = { name: 'New Student Here', }, to: 'someperson@somewhere.com', - from: 'someperson@somewhere.com', // verified sender in SendGrid account + // The from email must be the email address of a verified sender in SendGrid account. If/when you verify the domain, an email coming from the domain is likely good enough. + from: 'someperson@somewhere.com', template_id: 'd-a6dacc6241f9484a96554a13bbdcd971', }; diff --git a/api/email/emailHelper.js b/api/email/emailHelper.js index 93612ab..a9e342f 100644 --- a/api/email/emailHelper.js +++ b/api/email/emailHelper.js @@ -5,9 +5,6 @@ const sendEmail = (data) => { sgMail .send(data) .then((response) => { - // note: the following 2 console logs are SendGrid out of the box. Keep them if you like them. We found the stringify response to be more descriptive. - // console.log(response[0].statusCode); - // console.log(response[0].headers); console.log(JSON.stringify(response)); }) .catch((error) => { diff --git a/api/profile/profileRouter.js b/api/profile/profileRouter.js index 4a87235..b4dc980 100644 --- a/api/profile/profileRouter.js +++ b/api/profile/profileRouter.js @@ -198,76 +198,76 @@ router.post('/', checkProfileObject, async (req, res) => { if (profileExists) { res.status(400).json({ message: 'profile already exists' }); } else { - const prof = await Profiles.create(profile); - if (!prof) { + const newProfile = await Profiles.create(profile); + if (!newProfile) { res.status(404).json({ message: 'There was an error saving the profile to the database.', }); } - if (prof[0].role_id === 3 || prof[0].role_id === '3') { + if (newProfile[0].role_id === 3 || newProfile[0].role_id === '3') { const instructorWelcomeMessage = { dynamic_template_data: { - name: prof[0].name, + name: newProfile[0].name, }, - to: prof[0].email, + to: newProfile[0].email, from: 'someone@somewhere.com', // verified sender in SendGrid account. Try to put this in env - hardcoded here because it wasn't working there. template_id: 'd-a4de80911362438bb35d481efa068398', }; const instructorList = { list_ids: ['e7b598d9-23ca-48df-a62b-53470b5d1d86'], - email: prof[0].email, - name: prof[0].name, + email: newProfile[0].email, + name: newProfile[0].name, }; sendEmail(instructorWelcomeMessage); addToList(instructorList); res.status(200).json({ message: 'instructor profile created', - profile: prof[0], + profile: newProfile[0], }); - } else if (prof[0].role_id === 4 || prof[0].role_id === '4') { + } else if (newProfile[0].role_id === 4 || newProfile[0].role_id === '4') { const parentWelcomeMessage = { dynamic_template_data: { - name: prof[0].name, + name: newProfile[0].name, }, - to: prof[0].email, + to: newProfile[0].email, from: 'someone@somewhere.com', template_id: 'd-19b895416ae74cea97e285c4401fcc1f', }; const parentList = { list: 'e7b598d9-23ca-48df-a62b-53470b5d1d86', - email: prof[0].email, - name: prof[0].name, + email: newProfile[0].email, + name: newProfile[0].name, }; sendEmail(parentWelcomeMessage); addToList(parentList); res.status(200).json({ message: 'parent profile created', - profile: prof[0], + profile: newProfile[0], }); - } else if (prof[0].role_id === 5 || prof[0].role_id === '5') { + } else if (newProfile[0].role_id === 5 || newProfile[0].role_id === '5') { const studentWelcomeMessage = { dynamic_template_data: { - name: prof[0].name, + name: newProfile[0].name, }, - to: prof[0].email, + to: newProfile[0].email, from: 'someone@somewhere.com', template_id: 'd-a6dacc6241f9484a96554a13bbdcd971', }; const studentList = { list: '4dd72555-266f-4f8e-b595-ecc1f7ff8f28', - email: prof[0].email, - name: prof[0].name, + email: newProfile[0].email, + name: newProfile[0].name, }; sendEmail(studentWelcomeMessage); addToList(studentList); res.status(200).json({ message: 'parent profile created', - profile: prof[0], + profile: newProfile[0], }); } else { res.status(200).json({ message: 'profile created', - profile: prof[0], + profile: newProfile[0], }); } }