-
Notifications
You must be signed in to change notification settings - Fork 37
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
BL-751-BE-Build an Email Service #124
base: main
Are you sure you want to change the base?
Changes from 3 commits
64a6a76
07cdbb3
7da3efe
c5b3b6c
9cc2082
d2ba469
735ea56
e1ff24c
ded3275
113ba69
fb3a011
d9119e9
bcf8484
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -39,3 +39,6 @@ jspm_packages/ | |
.env | ||
.env.test | ||
.env.local | ||
sendgrid.env | ||
sendgrid.env | ||
sendgrid.env | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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. <b>(Not Implemented)</b>| | ||
| [DELETE] | /children/enrollments/:id | Returns the event object with the specified `id`. Unenrolls student from course. <b>(Not Implemented)</b>| | ||
|
||
| 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. <b>(Not Implemented)</b> | | ||
| [DELETE] | /children/enrollments/:id | Returns the event object with the specified `id`. Unenrolls student from course. <b>(Not Implemented)</b> | | ||
|
||
<h1>Instructors</h1> | ||
|
||
|
@@ -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 <b>BUG(?): incorrectly labeled as profile_id in codebase rather than inbox_id</b> | | ||
| [POST] | /conversation_id/ | Creates an inbox and returns the newly created inbox. | | ||
|
@@ -300,3 +299,47 @@ Visual Database Schema: https://dbdesigner.page.link/WTZRbVeTR7EzLvs86 <b>\*Curr | |
[Loom Video PT4](https://www.loom.com/share/7da5fc043d3149afb05876c28df9bd3d) | ||
|
||
<br /> | ||
|
||
<h2>Email Service</h2> | ||
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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Slight typo in the second sentence of this paragraph "SendEmail sends an API request to SendGrid to send a templated email to the to email address it's given." Just have an extra to in there. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Got it - thanks! |
||
|
||
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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Awesome! This is great documentation for people working on this in the future. I am sure they will appreciate how thorough you are. |
||
|
||
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) | ||
- 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. 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. | ||
|
||
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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is fantastic. You've done an excellent job explaining how the process works. I feel like I could set up SendGrid after reading these instructions. Fantastic work. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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: '[email protected]', | ||
from: '[email protected]', // verified sender in SendGrid account | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am not sure what the comment here is trying to tell me. It may be beneficial to flesh it out a little to make things more clear, or remove it all together if it isn't necessary There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Made more clear, also explained domain validation. |
||
template_id: 'd-a6dacc6241f9484a96554a13bbdcd971', | ||
}; | ||
|
||
const newParent = { | ||
dynamic_template_data: { | ||
name: 'New Parent Here', | ||
}, | ||
to: '[email protected]', | ||
from: '[email protected]', | ||
template_id: 'd-19b895416ae74cea97e285c4401fcc1f', | ||
}; | ||
|
||
const newInstructor = { | ||
dynamic_template_data: { | ||
name: 'New Parent Here', | ||
}, | ||
to: '[email protected]', | ||
from: '[email protected]', | ||
template_id: 'd-a4de80911362438bb35d481efa068398', | ||
}; | ||
|
||
const newStudentContact = { | ||
list_ids: ['e7b598d9-23ca-48df-a62b-53470b5d1d86'], | ||
email: '[email protected]', | ||
name: 'new student firstname', | ||
}; | ||
|
||
const newParentContact = { | ||
list_ids: ['e7b598d9-23ca-48df-a62b-53470b5d1d86'], | ||
email: '[email protected]', | ||
name: 'new parent firstname', | ||
}; | ||
|
||
const newInstructorContact = { | ||
list_ids: ['e7b598d9-23ca-48df-a62b-53470b5d1d86'], | ||
email: '[email protected]', | ||
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); | ||
}); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
const sgMail = require('@sendgrid/mail'); | ||
sgMail.setApiKey(process.env.SENDGRID_API_KEY); | ||
|
||
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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think I would remove the commented out console.logs. If you feel the stringify response is better, then that's what should be used. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That makes sense. I removed them. |
||
// console.log(response[0].statusCode); | ||
// console.log(response[0].headers); | ||
console.log(JSON.stringify(response)); | ||
}) | ||
.catch((error) => { | ||
console.error(error); | ||
}); | ||
}; | ||
|
||
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 }; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,6 +2,7 @@ const express = require('express'); | |
const authRequired = require('../middleware/authRequired'); | ||
const ownerAuthorization = require('../middleware/ownerAuthorization'); | ||
const Profiles = require('./profileModel'); | ||
const { sendEmail, addToList } = require('../email/emailHelper'); | ||
const router = express.Router(); | ||
const { | ||
checkProfileObject, | ||
|
@@ -159,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: | ||
|
@@ -193,23 +194,85 @@ 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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would it be better to put this check for whether the profile exists in the checkProfileObject middleware? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Probably yes, but I didn't want to break existing functionality. |
||
res.status(400).json({ message: 'profile already exists' }); | ||
} else { | ||
const prof = await Profiles.create(profile); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To me, the variable name "prof" seems a little non-descript and a bit too similar to "profile" used above. I might consider renaming this to "newProfile". There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I like that - will do. |
||
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') { | ||
const instructorWelcomeMessage = { | ||
dynamic_template_data: { | ||
name: prof[0].name, | ||
}, | ||
to: prof[0].email, | ||
from: '[email protected]', // 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: '[email protected]', | ||
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: '[email protected]', | ||
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: | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should there be three lines with sedgrid.env here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nope, removed 2 of them.