diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 269cc2d..bb32bf6 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -84,6 +84,7 @@ After that you will need to select the following scopes:
- `read:users`
- `read:roles`
- `delete:users`
+- `create:user_tickets` (Sending email verification)
These are the only scopes we need, but you can select all if you want.
diff --git a/content/email_templates/README.md b/content/email_templates/README.md
index bc5d128..8f53985 100644
--- a/content/email_templates/README.md
+++ b/content/email_templates/README.md
@@ -17,4 +17,6 @@ nodemon --config nodemon-emails.json
|Mentorship reminder|http://localhost:3003/mentorship-reminder?data={%22menteeName%22:%22Moshe%22,%22mentorName%22:%22Brent%22,%22message%22:%22because%22}|
|Mentor application received|http://localhost:3003/mentor-application-received?data={%22name%22:%22Brent%22}|
|Mentorship application denied|http://localhost:3003/mentor-application-declined?data={%22name%22:%22Moshe%22,%22reason%22:%22your%20avatar%20is%20not%20you%22}|
-|Mentorship application approved|http://localhost:3003/mentor-application-approved?data={%22name%22:%22Moshe%22}|
\ No newline at end of file
+|Mentorship application approved|http://localhost:3003/mentor-application-approved?data={%22name%22:%22Moshe%22}|
+|Mentor freeze|http://localhost:3003/mentor-freeze?data={%22mentorName%22:%22Brent%22}}|
+|Email verification|http://localhost:3003/email-verification?data=%7B%22name%22:%22Moshe%22,%22link%22:%22http://localhost:3003%22%7D}|
\ No newline at end of file
diff --git a/content/email_templates/email-verification.html b/content/email_templates/email-verification.html
new file mode 100644
index 0000000..8714946
--- /dev/null
+++ b/content/email_templates/email-verification.html
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+ |
+
+
+
+
Hey <%= name %>
+
+ You're almost there!
+
+
Please click the link below to verify your email
+
+ Verify
+
+
+
+ (Or copy and paste this url
+ <%= link %> into your browser)
+
+
diff --git a/content/email_templates/show.js b/content/email_templates/show.js
index 4c17e27..8513b55 100644
--- a/content/email_templates/show.js
+++ b/content/email_templates/show.js
@@ -31,5 +31,5 @@ app.get('/:templateName', function (req, res) {
});
app.listen(port, () => {
- console.log(`Example app listening at http://localhost:${port}`);
+ console.log(`Running on http://localhost:${port}. Grab a URL from the email_tempaltes/readme file`);
});
diff --git a/docs/cc-api-spec.json b/docs/cc-api-spec.json
index 990d7db..a2b862f 100644
--- a/docs/cc-api-spec.json
+++ b/docs/cc-api-spec.json
@@ -1 +1 @@
-{"swagger":"2.0","info":{"description":"A REST API for the coding coach platform","version":"1.0","title":"Coding Coach"},"basePath":"/","tags":[],"schemes":["https","http"],"securityDefinitions":{"bearer":{"type":"apiKey","name":"Authorization","in":"header"}},"paths":{"/mentors":{"get":{"summary":"Return all mentors in the platform by the given filters","parameters":[{"type":"number","name":"page","required":false,"in":"query"},{"type":"number","name":"limit","required":false,"in":"query"},{"type":"boolean","name":"available","required":false,"in":"query"},{"type":"string","name":"tags","required":false,"in":"query"},{"type":"string","name":"country","required":false,"in":"query"},{"type":"string","name":"spokenLanguages","required":false,"in":"query"},{"type":"string","name":"name","required":false,"in":"query"}],"responses":{"200":{"description":""}},"tags":["/mentors"],"produces":["application/json"],"consumes":["application/json"]}},"/mentors/featured":{"get":{"summary":"Retrieves a random mentor to be featured in the blog (or anywhere else)","responses":{"200":{"description":""}},"tags":["/mentors"],"produces":["application/json"],"consumes":["application/json"]}},"/mentors/applications":{"get":{"summary":"Retrieve applications filter by the given status","parameters":[{"type":"string","name":"status","required":true,"in":"query"}],"responses":{"200":{"description":""}},"tags":["/mentors"],"security":[{"bearer":[]}],"produces":["application/json"],"consumes":["application/json"]},"post":{"summary":"Creates a new request to become a mentor, pending for Admin to approve","parameters":[{"name":"ApplicationDto","required":true,"in":"body","schema":{"$ref":"#/definitions/ApplicationDto"}}],"responses":{"201":{"description":""}},"tags":["/mentors"],"security":[{"bearer":[]}],"produces":["application/json"],"consumes":["application/json"]}},"/mentors/{userId}/applications":{"get":{"summary":"Retrieve applications for the given user","parameters":[{"type":"string","name":"status","required":true,"in":"query"},{"type":"string","name":"userId","required":true,"in":"path"}],"responses":{"200":{"description":""}},"tags":["/mentors"],"security":[{"bearer":[]}],"produces":["application/json"],"consumes":["application/json"]}},"/mentors/applications/{id}":{"put":{"summary":"Approves or rejects an application after review","parameters":[{"name":"ApplicationDto","required":true,"in":"body","schema":{"$ref":"#/definitions/ApplicationDto"}},{"type":"string","name":"id","required":true,"in":"path"}],"responses":{"200":{"description":""}},"tags":["/mentors"],"security":[{"bearer":[]}],"produces":["application/json"],"consumes":["application/json"]}},"/mentorships/{mentorId}/apply":{"post":{"summary":"Creates a new mentorship request for the given mentor","parameters":[{"name":"MentorshipDto","required":true,"in":"body","schema":{"$ref":"#/definitions/MentorshipDto"}},{"type":"string","name":"mentorId","required":true,"in":"path"}],"responses":{"201":{"description":""}},"tags":["/mentorships"],"security":[{"bearer":[]}],"produces":["application/json"],"consumes":["application/json"]}},"/mentorships/{userId}/requests":{"get":{"summary":"Returns the mentorship requests for a mentor or a mentee.","parameters":[{"type":"string","name":"userId","required":true,"in":"path"}],"responses":{"200":{"description":""}},"tags":["/mentorships"],"security":[{"bearer":[]}],"produces":["application/json"],"consumes":["application/json"]}},"/mentorships/{userId}/requests/{id}":{"put":{"summary":"Updates the mentorship status by the mentor or mentee","parameters":[{"name":"MentorshipUpdatePayload","required":true,"in":"body","schema":{"$ref":"#/definitions/MentorshipUpdatePayload"}},{"name":"id","required":true,"in":"path","description":"Mentorship's id","type":""},{"name":"userId","required":true,"in":"path","description":"Mentor's id","type":""}],"responses":{"200":{"description":""}},"tags":["/mentorships"],"security":[{"bearer":[]}],"produces":["application/json"],"consumes":["application/json"]}},"/mentorships/requests":{"get":{"summary":"Returns all the mentorship requests","responses":{"200":{"description":""}},"tags":["/mentorships"],"security":[{"bearer":[]}],"produces":["application/json"],"consumes":["application/json"]}},"/mentorships/requests/{id}/reminder":{"put":{"summary":"Send mentor a reminder about an open mentorship","parameters":[],"responses":{"200":{"description":""}},"tags":["/mentorships"],"security":[{"bearer":[]}],"produces":["application/json"],"consumes":["application/json"]}},"/users/{userid}/favorites":{"get":{"summary":"Returns the favorite list for the given user","parameters":[{"type":"string","name":"userid","required":true,"in":"path"}],"responses":{"200":{"description":""}},"tags":["/users/:userid/favorites"],"security":[{"bearer":[]}],"produces":["application/json"],"consumes":["application/json"]}},"/users/{userid}/favorites/{mentorid}":{"post":{"summary":"Adds or removes a mentor from the favorite list","parameters":[{"type":"string","name":"mentorid","required":true,"in":"path"},{"type":"string","name":"userid","required":true,"in":"path"}],"responses":{"201":{"description":""}},"tags":["/users/:userid/favorites"],"security":[{"bearer":[]}],"produces":["application/json"],"consumes":["application/json"]}},"/users/{userid}/lists":{"post":{"summary":"Creates a new mentor's list for the given user","parameters":[{"name":"ListDto","required":true,"in":"body","schema":{"$ref":"#/definitions/ListDto"}},{"type":"string","name":"userid","required":true,"in":"path"}],"responses":{"201":{"description":""}},"tags":["/users/:userid/lists"],"security":[{"bearer":[]}],"produces":["application/json"],"consumes":["application/json"]},"get":{"summary":"Gets mentor's list for the given user","parameters":[{"type":"string","name":"userid","required":true,"in":"path"}],"responses":{"200":{"description":""}},"tags":["/users/:userid/lists"],"security":[{"bearer":[]}],"produces":["application/json"],"consumes":["application/json"]}},"/users/{userid}/lists/{listid}":{"put":{"summary":"Updates a given list","parameters":[{"name":"ListDto","required":true,"in":"body","schema":{"$ref":"#/definitions/ListDto"}},{"type":"string","name":"listid","required":true,"in":"path"},{"type":"string","name":"userid","required":true,"in":"path"}],"responses":{"200":{"description":""}},"tags":["/users/:userid/lists"],"security":[{"bearer":[]}],"produces":["application/json"],"consumes":["application/json"]}},"/users/{userid}/lists/{listId}":{"delete":{"summary":"Deletes the given mentor's list for the given user","parameters":[{"type":"string","name":"listId","required":true,"in":"path"},{"type":"string","name":"userid","required":true,"in":"path"}],"responses":{"200":{"description":""}},"tags":["/users/:userid/lists"],"security":[{"bearer":[]}],"produces":["application/json"],"consumes":["application/json"]}},"/users/{userid}/lists/{listid}/add":{"put":{"summary":"Add a new mentor to existing list","parameters":[{"name":"ListDto","required":true,"in":"body","schema":{"$ref":"#/definitions/ListDto"}},{"type":"string","name":"listid","required":true,"in":"path"},{"type":"string","name":"userid","required":true,"in":"path"}],"responses":{"200":{"description":""}},"tags":["/users/:userid/lists"],"security":[{"bearer":[]}],"produces":["application/json"],"consumes":["application/json"]}},"/users":{"get":{"summary":"Return all registered users","responses":{"200":{"description":""}},"tags":["/users"],"security":[{"bearer":[]}],"produces":["application/json"],"consumes":["application/json"]}},"/users/current":{"get":{"summary":"Returns the current user","responses":{"200":{"description":""}},"tags":["/users"],"security":[{"bearer":[]}],"produces":["application/json"],"consumes":["application/json"]}},"/users/{id}":{"get":{"summary":"Returns a single user by ID","parameters":[{"name":"id","required":true,"in":"path","description":"The user _id","type":""}],"responses":{"200":{"description":""}},"tags":["/users"],"security":[{"bearer":[]}],"produces":["application/json"],"consumes":["application/json"]},"put":{"summary":"Updates an existing user","parameters":[{"name":"UserDto","required":true,"in":"body","schema":{"$ref":"#/definitions/UserDto"}},{"name":"id","required":true,"in":"path","description":"The user _id","type":""}],"responses":{"200":{"description":""}},"tags":["/users"],"security":[{"bearer":[]}],"produces":["application/json"],"consumes":["application/json"]},"delete":{"summary":"Deletes the given user","parameters":[{"name":"id","required":true,"in":"path","description":"The user _id","type":""}],"responses":{"200":{"description":""}},"tags":["/users"],"security":[{"bearer":[]}],"produces":["application/json"],"consumes":["application/json"]}},"/users/{id}/avatar":{"post":{"summary":"Upload an avatar for the given user","parameters":[{"name":"id","required":true,"in":"path","description":"The user _id","type":""}],"responses":{"201":{"description":""}},"tags":["/users"],"security":[{"bearer":[]}],"produces":["application/json"],"consumes":["application/json"]}},"/users/{id}/records":{"post":{"summary":"Add a record to user","parameters":[{"name":"UserRecordDto","required":true,"in":"body","schema":{"$ref":"#/definitions/UserRecordDto"}},{"name":"id","required":true,"in":"path","description":"The user _id","type":""}],"responses":{"201":{"description":""}},"tags":["/users"],"security":[{"bearer":[]}],"produces":["application/json"],"consumes":["application/json"]},"get":{"summary":"Get user records","parameters":[{"name":"id","required":true,"in":"path","description":"The user _id","type":""}],"responses":{"200":{"description":""}},"tags":["/users"],"security":[{"bearer":[]}],"produces":["application/json"],"consumes":["application/json"]}},"/reports/users":{"get":{"summary":"Return total number of users by role for the given date range","parameters":[{"type":"string","name":"end","required":true,"in":"query"},{"type":"string","name":"start","required":true,"in":"query"}],"responses":{"200":{"description":""}},"tags":["/reports"],"security":[{"bearer":[]}],"produces":["application/json"],"consumes":["application/json"]}},"/admin/mentor/{id}/notActive":{"put":{"summary":"Send an email to not active mentor","responses":{"200":{"description":""}},"tags":["/admin"],"produces":["application/json"],"consumes":["application/json"]}},"/admin/mentor/{id}/freeze":{"put":{"summary":"Retrieves a random mentor to be featured in the blog (or anywhere else)","responses":{"200":{"description":""}},"tags":["/admin"],"produces":["application/json"],"consumes":["application/json"]}}},"definitions":{"ApplicationDto":{"type":"object","properties":{"status":{"type":"string"},"description":{"type":"string"},"reason":{"type":"string"}},"required":["status","description","reason"]},"MentorshipDto":{"type":"object","properties":{"status":{"type":"string"},"message":{"type":"string"},"goals":{"type":"array","items":{"type":"string"}},"expectation":{"type":"string"},"background":{"type":"string"},"reason":{"type":"string"}},"required":["message"]},"MentorshipUpdatePayload":{"type":"object","properties":{"status":{"type":"string"},"reason":{"type":"string"}},"required":["status"]},"ListDto":{"type":"object","properties":{"_id":{"type":"string"},"name":{"type":"string"},"public":{"type":"boolean"},"mentors":{"type":"array","items":{"type":"string"}}},"required":["_id","name"]},"UserDto":{"type":"object","properties":{"_id":{"type":"string"},"email":{"type":"string"},"name":{"type":"string"},"available":{"type":"boolean"},"avatar":{"type":"string"},"title":{"type":"string"},"description":{"type":"string"},"country":{"type":"string"},"timezone":{"type":"string"},"capacity":{"type":"number"},"spokenLanguages":{"type":"array","items":{"type":"string"}},"tags":{"type":"array","items":{"type":"string"}},"roles":{"type":"array","items":{"type":"string"}},"channels":{"type":"array","items":{"type":"string"}}},"required":["_id","email","name","available"]},"UserRecordDto":{"type":"object","properties":{"_id":{"type":"string"},"user":{"type":"string"},"type":{"type":"number"}},"required":["_id","user","type"]}}}
\ No newline at end of file
+{"swagger":"2.0","info":{"description":"A REST API for the coding coach platform","version":"1.0","title":"Coding Coach"},"basePath":"/","tags":[],"schemes":["https","http"],"securityDefinitions":{"bearer":{"type":"apiKey","name":"Authorization","in":"header"}},"paths":{"/mentors":{"get":{"summary":"Return all mentors in the platform by the given filters","parameters":[{"type":"number","name":"page","required":false,"in":"query"},{"type":"number","name":"limit","required":false,"in":"query"},{"type":"boolean","name":"available","required":false,"in":"query"},{"type":"string","name":"tags","required":false,"in":"query"},{"type":"string","name":"country","required":false,"in":"query"},{"type":"string","name":"spokenLanguages","required":false,"in":"query"},{"type":"string","name":"name","required":false,"in":"query"}],"responses":{"200":{"description":""}},"tags":["/mentors"],"produces":["application/json"],"consumes":["application/json"]}},"/mentors/featured":{"get":{"summary":"Retrieves a random mentor to be featured in the blog (or anywhere else)","responses":{"200":{"description":""}},"tags":["/mentors"],"produces":["application/json"],"consumes":["application/json"]}},"/mentors/applications":{"get":{"summary":"Retrieve applications filter by the given status","parameters":[{"type":"string","name":"status","required":true,"in":"query"}],"responses":{"200":{"description":""}},"tags":["/mentors"],"security":[{"bearer":[]}],"produces":["application/json"],"consumes":["application/json"]},"post":{"summary":"Creates a new request to become a mentor, pending for Admin to approve","parameters":[{"name":"ApplicationDto","required":true,"in":"body","schema":{"$ref":"#/definitions/ApplicationDto"}}],"responses":{"201":{"description":""}},"tags":["/mentors"],"security":[{"bearer":[]}],"produces":["application/json"],"consumes":["application/json"]}},"/mentors/{userId}/applications":{"get":{"summary":"Retrieve applications for the given user","parameters":[{"type":"string","name":"status","required":true,"in":"query"},{"type":"string","name":"userId","required":true,"in":"path"}],"responses":{"200":{"description":""}},"tags":["/mentors"],"security":[{"bearer":[]}],"produces":["application/json"],"consumes":["application/json"]}},"/mentors/applications/{id}":{"put":{"summary":"Approves or rejects an application after review","parameters":[{"name":"ApplicationDto","required":true,"in":"body","schema":{"$ref":"#/definitions/ApplicationDto"}},{"type":"string","name":"id","required":true,"in":"path"}],"responses":{"200":{"description":""}},"tags":["/mentors"],"security":[{"bearer":[]}],"produces":["application/json"],"consumes":["application/json"]}},"/mentorships/{mentorId}/apply":{"post":{"summary":"Creates a new mentorship request for the given mentor","parameters":[{"name":"MentorshipDto","required":true,"in":"body","schema":{"$ref":"#/definitions/MentorshipDto"}},{"type":"string","name":"mentorId","required":true,"in":"path"}],"responses":{"201":{"description":""}},"tags":["/mentorships"],"security":[{"bearer":[]}],"produces":["application/json"],"consumes":["application/json"]}},"/mentorships/{userId}/requests":{"get":{"summary":"Returns the mentorship requests for a mentor or a mentee.","parameters":[{"type":"string","name":"userId","required":true,"in":"path"}],"responses":{"200":{"description":""}},"tags":["/mentorships"],"security":[{"bearer":[]}],"produces":["application/json"],"consumes":["application/json"]}},"/mentorships/{userId}/requests/{id}":{"put":{"summary":"Updates the mentorship status by the mentor or mentee","parameters":[{"name":"MentorshipUpdatePayload","required":true,"in":"body","schema":{"$ref":"#/definitions/MentorshipUpdatePayload"}},{"name":"id","required":true,"in":"path","description":"Mentorship's id","type":""},{"name":"userId","required":true,"in":"path","description":"Mentor's id","type":""}],"responses":{"200":{"description":""}},"tags":["/mentorships"],"security":[{"bearer":[]}],"produces":["application/json"],"consumes":["application/json"]}},"/mentorships/requests":{"get":{"summary":"Returns all the mentorship requests","responses":{"200":{"description":""}},"tags":["/mentorships"],"security":[{"bearer":[]}],"produces":["application/json"],"consumes":["application/json"]}},"/mentorships/requests/{id}/reminder":{"put":{"summary":"Send mentor a reminder about an open mentorship","parameters":[],"responses":{"200":{"description":""}},"tags":["/mentorships"],"security":[{"bearer":[]}],"produces":["application/json"],"consumes":["application/json"]}},"/users/{userid}/favorites":{"get":{"summary":"Returns the favorite list for the given user","parameters":[{"type":"string","name":"userid","required":true,"in":"path"}],"responses":{"200":{"description":""}},"tags":["/users/:userid/favorites"],"security":[{"bearer":[]}],"produces":["application/json"],"consumes":["application/json"]}},"/users/{userid}/favorites/{mentorid}":{"post":{"summary":"Adds or removes a mentor from the favorite list","parameters":[{"type":"string","name":"mentorid","required":true,"in":"path"},{"type":"string","name":"userid","required":true,"in":"path"}],"responses":{"201":{"description":""}},"tags":["/users/:userid/favorites"],"security":[{"bearer":[]}],"produces":["application/json"],"consumes":["application/json"]}},"/users/{userid}/lists":{"post":{"summary":"Creates a new mentor's list for the given user","parameters":[{"name":"ListDto","required":true,"in":"body","schema":{"$ref":"#/definitions/ListDto"}},{"type":"string","name":"userid","required":true,"in":"path"}],"responses":{"201":{"description":""}},"tags":["/users/:userid/lists"],"security":[{"bearer":[]}],"produces":["application/json"],"consumes":["application/json"]},"get":{"summary":"Gets mentor's list for the given user","parameters":[{"type":"string","name":"userid","required":true,"in":"path"}],"responses":{"200":{"description":""}},"tags":["/users/:userid/lists"],"security":[{"bearer":[]}],"produces":["application/json"],"consumes":["application/json"]}},"/users/{userid}/lists/{listid}":{"put":{"summary":"Updates a given list","parameters":[{"name":"ListDto","required":true,"in":"body","schema":{"$ref":"#/definitions/ListDto"}},{"type":"string","name":"listid","required":true,"in":"path"},{"type":"string","name":"userid","required":true,"in":"path"}],"responses":{"200":{"description":""}},"tags":["/users/:userid/lists"],"security":[{"bearer":[]}],"produces":["application/json"],"consumes":["application/json"]}},"/users/{userid}/lists/{listId}":{"delete":{"summary":"Deletes the given mentor's list for the given user","parameters":[{"type":"string","name":"listId","required":true,"in":"path"},{"type":"string","name":"userid","required":true,"in":"path"}],"responses":{"200":{"description":""}},"tags":["/users/:userid/lists"],"security":[{"bearer":[]}],"produces":["application/json"],"consumes":["application/json"]}},"/users/{userid}/lists/{listid}/add":{"put":{"summary":"Add a new mentor to existing list","parameters":[{"name":"ListDto","required":true,"in":"body","schema":{"$ref":"#/definitions/ListDto"}},{"type":"string","name":"listid","required":true,"in":"path"},{"type":"string","name":"userid","required":true,"in":"path"}],"responses":{"200":{"description":""}},"tags":["/users/:userid/lists"],"security":[{"bearer":[]}],"produces":["application/json"],"consumes":["application/json"]}},"/users":{"get":{"summary":"Return all registered users","responses":{"200":{"description":""}},"tags":["/users"],"security":[{"bearer":[]}],"produces":["application/json"],"consumes":["application/json"]}},"/users/current":{"get":{"summary":"Returns the current user","responses":{"200":{"description":""}},"tags":["/users"],"security":[{"bearer":[]}],"produces":["application/json"],"consumes":["application/json"]}},"/users/{id}":{"get":{"summary":"Returns a single user by ID","parameters":[{"name":"id","required":true,"in":"path","description":"The user _id","type":""}],"responses":{"200":{"description":""}},"tags":["/users"],"security":[{"bearer":[]}],"produces":["application/json"],"consumes":["application/json"]},"put":{"summary":"Updates an existing user","parameters":[{"name":"UserDto","required":true,"in":"body","schema":{"$ref":"#/definitions/UserDto"}},{"name":"id","required":true,"in":"path","description":"The user _id","type":""}],"responses":{"200":{"description":""}},"tags":["/users"],"security":[{"bearer":[]}],"produces":["application/json"],"consumes":["application/json"]},"delete":{"summary":"Deletes the given user","parameters":[{"name":"id","required":true,"in":"path","description":"The user _id","type":""}],"responses":{"200":{"description":""}},"tags":["/users"],"security":[{"bearer":[]}],"produces":["application/json"],"consumes":["application/json"]}},"/users/{id}/avatar":{"post":{"summary":"Upload an avatar for the given user","parameters":[{"name":"id","required":true,"in":"path","description":"The user _id","type":""}],"responses":{"201":{"description":""}},"tags":["/users"],"security":[{"bearer":[]}],"produces":["application/json"],"consumes":["application/json"]}},"/users/verify":{"post":{"summary":"Send a verification email","parameters":[{"name":"id","required":true,"in":"path","description":"The user _id","type":""}],"responses":{"201":{"description":""}},"tags":["/users"],"security":[{"bearer":[]}],"produces":["application/json"],"consumes":["application/json"]}},"/users/{id}/records":{"post":{"summary":"Add a record to user","parameters":[{"name":"UserRecordDto","required":true,"in":"body","schema":{"$ref":"#/definitions/UserRecordDto"}},{"name":"id","required":true,"in":"path","description":"The user _id","type":""}],"responses":{"201":{"description":""}},"tags":["/users"],"security":[{"bearer":[]}],"produces":["application/json"],"consumes":["application/json"]},"get":{"summary":"Get user records","parameters":[{"name":"id","required":true,"in":"path","description":"The user _id","type":""}],"responses":{"200":{"description":""}},"tags":["/users"],"security":[{"bearer":[]}],"produces":["application/json"],"consumes":["application/json"]}},"/reports/users":{"get":{"summary":"Return total number of users by role for the given date range","parameters":[{"type":"string","name":"end","required":true,"in":"query"},{"type":"string","name":"start","required":true,"in":"query"}],"responses":{"200":{"description":""}},"tags":["/reports"],"security":[{"bearer":[]}],"produces":["application/json"],"consumes":["application/json"]}},"/admin/mentor/{id}/notActive":{"put":{"summary":"Send an email to not active mentor","responses":{"200":{"description":""}},"tags":["/admin"],"produces":["application/json"],"consumes":["application/json"]}},"/admin/mentor/{id}/freeze":{"put":{"summary":"Retrieves a random mentor to be featured in the blog (or anywhere else)","responses":{"200":{"description":""}},"tags":["/admin"],"produces":["application/json"],"consumes":["application/json"]}}},"definitions":{"ApplicationDto":{"type":"object","properties":{"status":{"type":"string"},"description":{"type":"string"},"reason":{"type":"string"}},"required":["status","description","reason"]},"MentorshipDto":{"type":"object","properties":{"status":{"type":"string"},"message":{"type":"string"},"goals":{"type":"array","items":{"type":"string"}},"expectation":{"type":"string"},"background":{"type":"string"},"reason":{"type":"string"}},"required":["message"]},"MentorshipUpdatePayload":{"type":"object","properties":{"status":{"type":"string"},"reason":{"type":"string"}},"required":["status"]},"ListDto":{"type":"object","properties":{"_id":{"type":"string"},"name":{"type":"string"},"public":{"type":"boolean"},"mentors":{"type":"array","items":{"type":"string"}}},"required":["_id","name"]},"UserDto":{"type":"object","properties":{"_id":{"type":"string"},"id":{"type":"string"},"email":{"type":"string"},"name":{"type":"string"},"available":{"type":"boolean"},"avatar":{"type":"string"},"image":{"type":"string"},"auth0Id":{"type":"string"},"title":{"type":"string"},"description":{"type":"string"},"country":{"type":"string"},"timezone":{"type":"string"},"capacity":{"type":"number"},"spokenLanguages":{"type":"array","items":{"type":"string"}},"tags":{"type":"array","items":{"type":"string"}},"roles":{"type":"array","items":{"type":"string"}},"channels":{"type":"array","items":{"type":"string"}}},"required":["_id","id","email","name","available"]},"UserRecordDto":{"type":"object","properties":{"_id":{"type":"string"},"user":{"type":"string"},"type":{"type":"number"}},"required":["_id","user","type"]}}}
\ No newline at end of file
diff --git a/package.json b/package.json
index b53c2a9..003ad3c 100644
--- a/package.json
+++ b/package.json
@@ -55,6 +55,7 @@
"@types/express-jwt": "^0.0.42",
"@types/jest": "^26.0.19",
"@types/node": "^10.12.18",
+ "@types/node-fetch": "^2.6.1",
"@types/supertest": "^2.0.7",
"concurrently": "^4.1.0",
"faker": "^5.1.0",
diff --git a/src/config/index.ts b/src/config/index.ts
index f8befc5..d95eb7a 100644
--- a/src/config/index.ts
+++ b/src/config/index.ts
@@ -32,6 +32,9 @@ const config = {
pagination: {
limit: 20,
},
+ urls: {
+ CLIENT_BASE_URL: process.env.CLIENT_BASE_URL,
+ },
};
export default config;
diff --git a/src/middlewares/auth.middleware.ts b/src/middlewares/auth.middleware.ts
index fc77304..c2d9afa 100644
--- a/src/middlewares/auth.middleware.ts
+++ b/src/middlewares/auth.middleware.ts
@@ -1,7 +1,8 @@
import { Injectable, NestMiddleware } from '@nestjs/common';
-import { Request, Response, NextFunction } from 'express';
+import { Response, NextFunction } from 'express';
import * as jwt from 'express-jwt';
import { expressJwtSecret } from 'jwks-rsa';
+import { Request } from '🧙♂️/types/request';
import Config from '../config';
const secret = expressJwtSecret({
@@ -42,6 +43,7 @@ export class AuthMiddleware implements NestMiddleware {
next();
return;
}
+
if (error) {
const status = error.status || 401;
const message =
@@ -53,6 +55,12 @@ export class AuthMiddleware implements NestMiddleware {
errors: [message],
});
}
+ if (!req.user.email_verified) {
+ return res.status(401).send({
+ success: false,
+ errors: ['Please verify your email address'],
+ });
+ }
next();
});
}
diff --git a/src/modules/admin/__tests__/mentors.controller.spec.ts b/src/modules/admin/__tests__/mentors.controller.spec.ts
index c0870eb..6f90a7c 100644
--- a/src/modules/admin/__tests__/mentors.controller.spec.ts
+++ b/src/modules/admin/__tests__/mentors.controller.spec.ts
@@ -1,5 +1,6 @@
import { UnauthorizedException, BadRequestException } from '@nestjs/common';
import { Test } from '@nestjs/testing';
+import { Request } from '🧙♂️/types/request';
import { MentorsController } from '../../mentors/mentors.controller';
import { UsersService } from '../../common/users.service';
import { EmailService } from '../../email/email.service';
@@ -8,7 +9,6 @@ import { User } from '../../common/interfaces/user.interface';
import { Application } from '../../common/interfaces/application.interface';
import { MentorFiltersDto } from '../../common/dto/mentorfilters.dto';
import { ApplicationDto } from '../../common/dto/application.dto';
-import { Request } from 'express';
class ServiceMock {}
diff --git a/src/modules/admin/admin.controller.ts b/src/modules/admin/admin.controller.ts
index 7de38bc..c35bc28 100644
--- a/src/modules/admin/admin.controller.ts
+++ b/src/modules/admin/admin.controller.ts
@@ -6,7 +6,7 @@ import {
Req,
} from '@nestjs/common';
import { ApiOperation, ApiUseTags } from '@nestjs/swagger';
-import { Request } from 'express';
+import { Request } from '🧙♂️/types/request';
import { MentorsService } from '../common/mentors.service';
import { MentorshipsService } from '../mentorships/mentorships.service';
import { EmailService } from '../email/email.service';
diff --git a/src/modules/common/auth0.service.ts b/src/modules/common/auth0.service.ts
deleted file mode 100644
index 8e730a1..0000000
--- a/src/modules/common/auth0.service.ts
+++ /dev/null
@@ -1,62 +0,0 @@
-import { Injectable } from '@nestjs/common';
-import fetch from 'node-fetch';
-import Config from '../../config';
-
-@Injectable()
-export class Auth0Service {
- // Get an access token for the Auth0 Admin API
- async getAdminAccessToken() {
- const options = {
- method: 'POST',
- headers: { 'content-type': 'application/json' },
- body: JSON.stringify({
- client_id: Config.auth0.backend.CLIENT_ID,
- client_secret: Config.auth0.backend.CLIENT_SECRET,
- audience: `https://${Config.auth0.backend.DOMAIN}/api/v2/`,
- grant_type: 'client_credentials',
- }),
- };
-
- const response = await fetch(
- `https://${Config.auth0.backend.DOMAIN}/oauth/token`,
- options,
- );
- const json = await response.json();
-
- return json;
- }
-
- // Get the user's profile from auth0
- async getUserProfile(accessToken, userID) {
- const options = {
- headers: {
- Authorization: `Bearer ${accessToken}`,
- },
- };
-
- const response = await fetch(
- `https://${Config.auth0.backend.DOMAIN}/api/v2/users/${userID}`,
- options,
- );
- const json = await response.json();
-
- return json;
- }
-
- // Deletes a user from auth0
- async deleteUser(accessToken: string, userID: string) {
- const options = {
- method: 'DELETE',
- headers: {
- Authorization: `Bearer ${accessToken}`,
- },
- };
-
- const response = await fetch(
- `https://${Config.auth0.backend.DOMAIN}/api/v2/users/${userID}`,
- options,
- );
-
- return response;
- }
-}
diff --git a/src/modules/common/auth0/auth0.service.ts b/src/modules/common/auth0/auth0.service.ts
new file mode 100644
index 0000000..d4a2079
--- /dev/null
+++ b/src/modules/common/auth0/auth0.service.ts
@@ -0,0 +1,104 @@
+import { HttpException, Injectable } from '@nestjs/common';
+import fetch from 'node-fetch';
+import * as Sentry from '@sentry/node';
+import Config from '../../../config';
+import type { Auth0Response, EmailVerificationTicket } from './auth0.types';
+
+@Injectable()
+export class Auth0Service {
+ // Get an access token for the Auth0 Admin API
+ async getAdminAccessToken(): Promise<{ access_token: string }> {
+ const options = {
+ method: 'POST',
+ headers: { 'content-type': 'application/json' },
+ body: JSON.stringify({
+ client_id: Config.auth0.backend.CLIENT_ID,
+ client_secret: Config.auth0.backend.CLIENT_SECRET,
+ audience: `https://${Config.auth0.backend.DOMAIN}/api/v2/`,
+ grant_type: 'client_credentials',
+ }),
+ };
+
+ const response = await fetch(
+ `https://${Config.auth0.backend.DOMAIN}/oauth/token`,
+ options,
+ );
+ const json = await response.json();
+
+ return json;
+ }
+
+ // Get the user's profile from auth0
+ async getUserProfile(accessToken: string, userID: string) {
+ const options = {
+ headers: {
+ Authorization: `Bearer ${accessToken}`,
+ },
+ };
+
+ const response = await fetch(
+ `https://${Config.auth0.backend.DOMAIN}/api/v2/users/${userID}`,
+ options,
+ );
+ const json = await response.json();
+
+ return json;
+ }
+
+ // Deletes a user from auth0
+ async deleteUser(accessToken: string, userID: string) {
+ const options = {
+ method: 'DELETE',
+ headers: {
+ Authorization: `Bearer ${accessToken}`,
+ },
+ };
+
+ const response = await fetch(
+ `https://${Config.auth0.backend.DOMAIN}/api/v2/users/${userID}`,
+ options,
+ );
+
+ return response;
+ }
+
+ async createVerificationEmailTicket(
+ accessToken: string,
+ auth0UserId: string,
+ ) {
+ try {
+ const [provider, userId] = auth0UserId.split('|');
+ const payload = {
+ result_url: Config.urls.CLIENT_BASE_URL,
+ user_id: auth0UserId,
+ identity: { user_id: userId, provider },
+ };
+
+ const options = {
+ method: 'POST',
+ headers: {
+ /* tslint:disable-next-line */
+ Authorization: `Bearer ${accessToken}`,
+ 'content-type': 'application/json',
+ },
+ body: JSON.stringify(payload),
+ };
+
+ const response: Auth0Response = await (
+ await fetch(
+ `https://${Config.auth0.backend.DOMAIN}/api/v2/tickets/email-verification`,
+ options,
+ )
+ ).json();
+
+ if ('statusCode' in response) {
+ throw new HttpException(response, response.statusCode);
+ }
+
+ return response;
+ } catch (error) {
+ Sentry.captureException(error);
+ throw error;
+ }
+ }
+}
diff --git a/src/modules/common/auth0/auth0.types.ts b/src/modules/common/auth0/auth0.types.ts
new file mode 100644
index 0000000..3f2b68d
--- /dev/null
+++ b/src/modules/common/auth0/auth0.types.ts
@@ -0,0 +1,38 @@
+type Auth0ResponseSuccess = T;
+
+export interface EmailVerificationTicket {
+ ticket: string;
+}
+
+interface Auth0ResponseError {
+ statusCode: number;
+ error: string;
+ message: string;
+ errorCode: string;
+}
+
+export type Auth0Response =
+ | Auth0ResponseSuccess
+ | Auth0ResponseError;
+
+interface Auth0UserIdentity {
+ connection: string;
+ provider: string;
+ user_id: string;
+ isSocial: boolean;
+}
+
+export interface Auth0User {
+ created_at: string;
+ email: string;
+ email_verified: boolean;
+ identities: Auth0UserIdentity[];
+ name: string;
+ nickname: string;
+ picture: string;
+ updated_at: string;
+ user_id: string;
+ last_ip: string;
+ last_login: string;
+ logins_count: number;
+}
diff --git a/src/modules/common/common.module.ts b/src/modules/common/common.module.ts
index c608898..82b5b1a 100644
--- a/src/modules/common/common.module.ts
+++ b/src/modules/common/common.module.ts
@@ -1,5 +1,5 @@
import { Module } from '@nestjs/common';
-import { Auth0Service } from './auth0.service';
+import { Auth0Service } from './auth0/auth0.service';
import { UsersService } from './users.service';
import { commonProviders } from './common.providers';
import { DatabaseModule } from '../../database/database.module';
diff --git a/src/modules/common/dto/user.dto.ts b/src/modules/common/dto/user.dto.ts
index f53f997..b13f74c 100644
--- a/src/modules/common/dto/user.dto.ts
+++ b/src/modules/common/dto/user.dto.ts
@@ -18,6 +18,10 @@ export class UserDto {
@ApiModelProperty()
readonly _id: string;
+ // for mentorship mentor / mentee
+ @ApiModelProperty()
+ readonly id: string;
+
@ApiModelProperty()
@IsEmail()
@IsString()
@@ -37,6 +41,15 @@ export class UserDto {
@IsUrl()
readonly avatar: string;
+ @ApiModelPropertyOptional()
+ @IsString()
+ @IsUrl()
+ readonly image: string;
+
+ @ApiModelPropertyOptional()
+ @IsString()
+ readonly auth0Id: string;
+
@ApiModelPropertyOptional()
@Length(3, 50)
@IsString()
@@ -94,7 +107,7 @@ export class UserDto {
@ArrayMaxSize(3)
readonly channels: Channel[];
- constructor(values) {
+ constructor(values: Partial) {
Object.assign(this, values);
}
}
diff --git a/src/modules/common/interfaces/user.interface.ts b/src/modules/common/interfaces/user.interface.ts
index a064b39..0f5b0c6 100644
--- a/src/modules/common/interfaces/user.interface.ts
+++ b/src/modules/common/interfaces/user.interface.ts
@@ -26,6 +26,9 @@ export interface User extends Document {
readonly _id: ObjectID;
readonly auth0Id: string;
readonly email: string;
+ readonly nickname: string;
+ readonly picture: string;
+ readonly email_verified: boolean;
readonly available: boolean;
readonly name: string;
readonly avatar: string;
diff --git a/src/modules/common/users.service.ts b/src/modules/common/users.service.ts
index 007e9ab..04dbc2f 100644
--- a/src/modules/common/users.service.ts
+++ b/src/modules/common/users.service.ts
@@ -15,7 +15,8 @@ export class UsersService {
async create(userDto: UserDto): Promise {
const user = new this.userModel(userDto);
- return await user.save();
+ const result = await user.save();
+ return result.toObject();
}
async findById(_id: string): Promise {
@@ -27,7 +28,7 @@ export class UsersService {
}
async findByAuth0Id(auth0Id: string): Promise {
- return await this.userModel.findOne({ auth0Id }).exec();
+ return await this.userModel.findOne({ auth0Id }).lean().exec();
}
async findByEmail(email: string): Promise {
diff --git a/src/modules/email/interfaces/email.interface.ts b/src/modules/email/interfaces/email.interface.ts
index 1a29f46..248490e 100644
--- a/src/modules/email/interfaces/email.interface.ts
+++ b/src/modules/email/interfaces/email.interface.ts
@@ -19,6 +19,14 @@ interface WelcomePayload {
};
}
+interface VerificationEmailPayload {
+ name: 'email-verification';
+ data: {
+ name: string;
+ link: string;
+ };
+}
+
interface MentorshipAccepted {
name: 'mentorship-accepted';
data: {
@@ -109,6 +117,7 @@ interface MentorFreeze {
export type EmailParams = Required> &
(
| WelcomePayload
+ | VerificationEmailPayload
| MentorshipAccepted
| MentorshipCancelled
| MentorshipDeclined
diff --git a/src/modules/lists/__tests__/favorites.controller.spec.ts b/src/modules/lists/__tests__/favorites.controller.spec.ts
index a4fa819..3d5d9b4 100644
--- a/src/modules/lists/__tests__/favorites.controller.spec.ts
+++ b/src/modules/lists/__tests__/favorites.controller.spec.ts
@@ -1,6 +1,6 @@
import { BadRequestException, UnauthorizedException } from '@nestjs/common';
import { Test } from '@nestjs/testing';
-import { Request } from 'express';
+import { Request } from '🧙♂️/types/request';
import { FavoritesController } from '../favorites.controller';
import { ListsService } from '../lists.service';
import { List } from '../interfaces/list.interface';
diff --git a/src/modules/lists/__tests__/lists.controller.spec.ts b/src/modules/lists/__tests__/lists.controller.spec.ts
index 3d06461..f2f19f6 100644
--- a/src/modules/lists/__tests__/lists.controller.spec.ts
+++ b/src/modules/lists/__tests__/lists.controller.spec.ts
@@ -1,6 +1,6 @@
import { BadRequestException, UnauthorizedException } from '@nestjs/common';
import { Test } from '@nestjs/testing';
-import { Request } from 'express';
+import { Request } from '🧙♂️/types/request';
import { ListsController } from '../lists.controller';
import { ListsService } from '../lists.service';
import { List } from '../interfaces/list.interface';
diff --git a/src/modules/lists/favorites.controller.ts b/src/modules/lists/favorites.controller.ts
index c23d7c9..620a153 100644
--- a/src/modules/lists/favorites.controller.ts
+++ b/src/modules/lists/favorites.controller.ts
@@ -10,7 +10,7 @@ import {
ValidationPipe,
} from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiUseTags } from '@nestjs/swagger';
-import { Request } from 'express';
+import { Request } from '🧙♂️/types/request';
import { Role, User } from '../common/interfaces/user.interface';
import { List } from './interfaces/list.interface';
import { UsersService } from '../common/users.service';
@@ -101,11 +101,11 @@ export class FavoritesController {
} else {
let listDto: ListDto;
- if (list.mentors.find(item => item._id.equals(mentor._id))) {
+ if (list.mentors.find((item) => item._id.equals(mentor._id))) {
// If the mentor exist in the list we need to remove it
listDto = new ListDto({
_id: list._id,
- mentors: list.mentors.filter(item => !item._id.equals(mentor._id)),
+ mentors: list.mentors.filter((item) => !item._id.equals(mentor._id)),
});
} else {
// If the mentor doesn't exist in the list we need to add it
diff --git a/src/modules/lists/lists.controller.ts b/src/modules/lists/lists.controller.ts
index 1f9bf09..c7c9cc7 100644
--- a/src/modules/lists/lists.controller.ts
+++ b/src/modules/lists/lists.controller.ts
@@ -13,7 +13,7 @@ import {
Put,
} from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiUseTags } from '@nestjs/swagger';
-import { Request } from 'express';
+import { Request } from '🧙♂️/types/request';
import { Role, User } from '../common/interfaces/user.interface';
import { List } from './interfaces/list.interface';
import { UsersService } from '../common/users.service';
diff --git a/src/modules/mentorships/__tests__/mentorships.controller.spec.ts b/src/modules/mentorships/__tests__/mentorships.controller.spec.ts
index 482a124..a676c7f 100644
--- a/src/modules/mentorships/__tests__/mentorships.controller.spec.ts
+++ b/src/modules/mentorships/__tests__/mentorships.controller.spec.ts
@@ -132,7 +132,7 @@ describe('modules/mentorships/MentorshipsController', () => {
await expect(
mentorshipsController.applyForMentorship(
- request,
+ request as Request,
mentorId,
mentorship,
),
diff --git a/src/modules/users/__tests__/users.controller.spec.ts b/src/modules/users/__tests__/users.controller.spec.ts
index 5852d87..1538213 100644
--- a/src/modules/users/__tests__/users.controller.spec.ts
+++ b/src/modules/users/__tests__/users.controller.spec.ts
@@ -5,11 +5,12 @@ import { UsersService } from '../../common/users.service';
import { EmailService } from '../../email/email.service';
import { MentorsService } from '../../common/mentors.service';
import { ListsService } from '../../lists/lists.service';
-import { Auth0Service } from '../../common/auth0.service';
+import { Auth0Service } from '../../common/auth0/auth0.service';
import { FileService } from '../../common/file.service';
import { UserDto } from '../../common/dto/user.dto';
import { User, Role } from '../../common/interfaces/user.interface';
import { MentorshipsService } from '../../mentorships/mentorships.service';
+import { Response } from 'node-fetch';
class ServiceMock {}
@@ -117,7 +118,7 @@ describe('modules/users/UsersController', () => {
beforeEach(() => {
request = { user: { auth0Id: '123' } };
- data = { _id: 123, name: 'Crysfel Villa' } as User;
+ data = { _id: 123, name: 'Crysfel Villa', email_verified: false } as User;
response = { success: true, data };
});
@@ -127,6 +128,21 @@ describe('modules/users/UsersController', () => {
expect(await usersController.currentUser(request)).toEqual(response);
});
+ it('should return if the user has verified email', async () => {
+ usersService.findByAuth0Id = jest.fn(() => Promise.resolve(data));
+
+ expect(
+ await usersController.currentUser({
+ ...request,
+ user: { ...request.user, email_verified: true },
+ }),
+ ).toMatchObject({
+ data: {
+ email_verified: true,
+ },
+ });
+ });
+
it('should create a new user', async () => {
usersService.findByAuth0Id = jest.fn(() => Promise.resolve(undefined));
usersService.findByEmail = jest.fn(() => Promise.resolve(undefined));
@@ -407,8 +423,10 @@ describe('modules/users/UsersController', () => {
mentorsService.removeAllApplicationsByUserId = jest.fn(() =>
Promise.resolve(),
);
- auth0Service.getAdminAccessToken = jest.fn(() => Promise.resolve({}));
- auth0Service.deleteUser = jest.fn(() => Promise.resolve());
+ auth0Service.getAdminAccessToken = jest.fn(() =>
+ Promise.resolve({ access_token: '1' }),
+ );
+ auth0Service.deleteUser = jest.fn(() => Promise.resolve(new Response()));
expect(await usersController.remove(request, params)).toEqual(response);
});
@@ -462,8 +480,10 @@ describe('modules/users/UsersController', () => {
mentorsService.removeAllApplicationsByUserId = jest.fn(() =>
Promise.resolve(),
);
- auth0Service.getAdminAccessToken = jest.fn(() => Promise.resolve({}));
- auth0Service.deleteUser = jest.fn(() => Promise.resolve());
+ auth0Service.getAdminAccessToken = jest.fn(() =>
+ Promise.resolve({ access_token: '1' }),
+ );
+ auth0Service.deleteUser = jest.fn(() => Promise.resolve(new Response()));
expect(await usersController.remove(request, params)).toEqual(response);
});
@@ -482,8 +502,10 @@ describe('modules/users/UsersController', () => {
mentorsService.removeAllApplicationsByUserId = jest.fn(() =>
Promise.resolve(),
);
- auth0Service.getAdminAccessToken = jest.fn(() => Promise.resolve({}));
- auth0Service.deleteUser = jest.fn(() => Promise.resolve());
+ auth0Service.getAdminAccessToken = jest.fn(() =>
+ Promise.resolve({ access_token: '1' }),
+ );
+ auth0Service.deleteUser = jest.fn(() => Promise.resolve(new Response()));
await usersController.remove(request, params);
@@ -509,8 +531,10 @@ describe('modules/users/UsersController', () => {
mentorsService.removeAllApplicationsByUserId = jest.fn(() =>
Promise.resolve(),
);
- auth0Service.getAdminAccessToken = jest.fn(() => Promise.resolve({}));
- auth0Service.deleteUser = jest.fn(() => Promise.resolve());
+ auth0Service.getAdminAccessToken = jest.fn(() =>
+ Promise.resolve({ access_token: '1' }),
+ );
+ auth0Service.deleteUser = jest.fn(() => Promise.resolve(new Response()));
await usersController.remove(request, params);
@@ -538,7 +562,7 @@ describe('modules/users/UsersController', () => {
auth0Service.getAdminAccessToken = jest.fn(() =>
Promise.resolve({ access_token: '159' }),
);
- auth0Service.deleteUser = jest.fn(() => Promise.resolve());
+ auth0Service.deleteUser = jest.fn(() => Promise.resolve(new Response()));
await usersController.remove(request, params);
diff --git a/src/modules/users/users.controller.ts b/src/modules/users/users.controller.ts
index ae042f6..3239cbe 100644
--- a/src/modules/users/users.controller.ts
+++ b/src/modules/users/users.controller.ts
@@ -1,4 +1,5 @@
import * as Sentry from '@sentry/node';
+import { Request } from 'express';
import {
Controller,
@@ -27,7 +28,7 @@ import Config from '../../config';
import { UserDto } from '../common/dto/user.dto';
import { UserRecordDto } from '../common/dto/user-record.dto';
import { UsersService } from '../common/users.service';
-import { Auth0Service } from '../common/auth0.service';
+import { Auth0Service } from '../common/auth0/auth0.service';
import { FileService } from '../common/file.service';
import { MentorsService } from '../common/mentors.service';
import { Role, User } from '../common/interfaces/user.interface';
@@ -38,6 +39,7 @@ import { ListsService } from '../lists/lists.service';
import { filterImages } from '../../utils/mimes';
import { MentorshipsService } from '../mentorships/mentorships.service';
import { Status } from '../mentorships/interfaces/mentorship.interface';
+import { AccessTokenUser } from '🧙♂️/types/request';
@ApiUseTags('/users')
@ApiBearerAuth()
@@ -76,18 +78,18 @@ export class UsersController {
@ApiOperation({ title: 'Returns the current user' })
@Get('current')
- async currentUser(@Req() request) {
+ async currentUser(@Req() request: Request) {
const userId: string = request.user.auth0Id;
const currentUser: User = await this.usersService.findByAuth0Id(userId);
const response = {
success: true,
- data: currentUser,
+ data: this.enrichUserResponse(currentUser, request.user),
};
if (!currentUser) {
try {
- const data: any = await this.auth0Service.getAdminAccessToken();
- const user: any = await this.auth0Service.getUserProfile(
+ const data = await this.auth0Service.getAdminAccessToken();
+ const user: User = await this.auth0Service.getUserProfile(
data.access_token,
userId,
);
@@ -137,7 +139,7 @@ export class UsersController {
},
});
- response.data = newUser;
+ response.data = this.enrichUserResponse(newUser, request.user);
}
} catch (error) {
return {
@@ -158,6 +160,13 @@ export class UsersController {
return response;
}
+ private enrichUserResponse(user: User, auth0User: AccessTokenUser): User {
+ return {
+ ...user,
+ email_verified: Boolean(auth0User.email_verified),
+ };
+ }
+
private async shouldIncludeChannels(
currentUser?: User,
requestedUser?: User,
@@ -380,6 +389,40 @@ export class UsersController {
};
}
+ @ApiOperation({ title: 'Send a verification email' })
+ @ApiImplicitParam({ name: 'id', description: 'The user _id' })
+ @Post('verify')
+ async resendVerificationEmail(@Req() request: Request) {
+ const user: User = await this.usersService.findByAuth0Id(
+ request.user.auth0Id,
+ );
+
+ if (!user) {
+ throw new BadRequestException('User not found');
+ }
+
+ const data = await this.auth0Service.getAdminAccessToken();
+ const response = await this.auth0Service.createVerificationEmailTicket(
+ data.access_token,
+ user.auth0Id,
+ );
+
+ this.emailService.sendLocalTemplate({
+ name: 'email-verification',
+ data: {
+ name: user.name,
+ link: response.ticket,
+ },
+ to: user.email,
+ subject: 'Verify your email',
+ });
+
+ return {
+ success: true,
+ data: response,
+ };
+ }
+
//#region admin
@ApiOperation({ title: 'Add a record to user' })
@ApiImplicitParam({ name: 'id', description: 'The user _id' })
diff --git a/src/types/request.d.ts b/src/types/request.d.ts
new file mode 100644
index 0000000..2943963
--- /dev/null
+++ b/src/types/request.d.ts
@@ -0,0 +1,17 @@
+import { Request as ExpressReqeust } from 'express';
+
+export interface AccessTokenUser {
+ iss: string;
+ sub: string;
+ aud: string;
+ iat: number;
+ exp: number;
+ at_hash: string;
+ nonce: string;
+ auth0Id: string;
+ email_verified: boolean;
+}
+
+export interface Request extends ExpressReqeust {
+ user?: AccessTokenUser;
+}
diff --git a/src/utils/request.d.ts b/src/utils/request.d.ts
deleted file mode 100644
index e6715a2..0000000
--- a/src/utils/request.d.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import { User } from '../modules/common/interfaces/user.interface';
-
-declare module 'express-serve-static-core' {
- interface Request {
- user?: User;
- }
-}
diff --git a/test/api/mentorships.e2e-spec.ts b/test/api/mentorships.e2e-spec.ts
index 220166e..f41fa1b 100644
--- a/test/api/mentorships.e2e-spec.ts
+++ b/test/api/mentorships.e2e-spec.ts
@@ -51,6 +51,7 @@ describe('Mentorships', () => {
it('returns a status code of 401', async () => {
return request(server)
.put('/mentorships/1234/requests/abc')
+ .send({ status: Status.APPROVED, reason: '' })
.expect(401);
});
});
@@ -66,8 +67,8 @@ describe('Mentorships', () => {
.expect(404);
});
- describe('bad requests', () => {
- it('returns a status code of 400 if the payload is invalid', () => {
+ describe('bad requests - returns a status code of 400', () => {
+ it('if the payload is invalid', () => {
const id = mongoose.Types.ObjectId();
const token = getToken();
return request(server)
@@ -76,7 +77,7 @@ describe('Mentorships', () => {
.expect(400);
});
- it('returns a status code of 400 if the mentorship id is invalid', () => {
+ it('if the mentorship id is invalid', () => {
const token = getToken();
return request(server)
.put(`/mentorships/${mentorId}/requests/abc`)
@@ -85,7 +86,7 @@ describe('Mentorships', () => {
.expect(400);
});
- it('returns a status code of 400 if the current user is a mentee in the mentorship and the status is not cancelled', async () => {
+ it('if the current user is a mentee in the mentorship and the status is not cancelled', async () => {
const [mentee, mentor] = await Promise.all([
createUser(),
createUser(),
@@ -103,7 +104,7 @@ describe('Mentorships', () => {
.expect(400);
});
- it('returns a status code of 400 if the current user is a mentor in the mentorship and the status is not allowed to be updated by a mentor', async () => {
+ it('if the current user is a mentor in the mentorship and the status is not allowed to be updated by a mentor', async () => {
const [mentee, mentor] = await Promise.all([
createUser(),
createUser(),
@@ -122,8 +123,20 @@ describe('Mentorships', () => {
});
});
- describe('unauthorized requests', () => {
- it('returns a status code of 401 if the current user is neither a mentor nor a mentee in the mentorship', async () => {
+ describe('unauthorized requests - returns a status code of 401', () => {
+ it('if the current user has not verified their email', () => {
+ const token = getToken({ auth0Id: '1234' }, false);
+ return request(server)
+ .put(`/mentorships/1234/requests/5678`)
+ .set('Authorization', `Bearer ${token}`)
+ .send({ status: Status.APPROVED })
+ .expect(401, {
+ success: false,
+ errors: ['Please verify your email address'],
+ });
+ });
+
+ it('if the current user is neither a mentor nor a mentee in the mentorship', async () => {
const [mentee1, mentee2, mentor] = await Promise.all([
createUser(),
createUser(),
@@ -146,7 +159,9 @@ describe('Mentorships', () => {
});
describe('successful requests', () => {
- let mentor: User, mentee: User, mentorship: Mentorship;
+ let mentor: User;
+ let mentee: User;
+ let mentorship: Mentorship;
beforeEach(async () => {
[mentee, mentor] = await Promise.all([createUser(), createUser()]);
@@ -242,7 +257,7 @@ describe('Mentorships', () => {
data: {
menteeName: mentee.name,
mentorName: mentor.name,
- reason: reason,
+ reason,
},
});
expect(body.mentorship.status).toBe(Status.CANCELLED);
diff --git a/test/utils/jwt.ts b/test/utils/jwt.ts
index 57f51dc..108c0d3 100644
--- a/test/utils/jwt.ts
+++ b/test/utils/jwt.ts
@@ -3,6 +3,7 @@
import * as jwt from 'jsonwebtoken';
import * as nock from 'nock';
import * as faker from 'faker';
+import { AccessTokenUser } from '../../src/types/request';
const testPrivateKey = `-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA3hcdO8Q55PC7zy9MLfpR2HrPmFf6GFMO/rQXUwtAI4JveIcT
@@ -38,8 +39,8 @@ const jwks = {
alg: 'RS256',
kty: 'RSA',
use: 'sig',
- n:
- '3hcdO8Q55PC7zy9MLfpR2HrPmFf6GFMO_rQXUwtAI4JveIcTSuJ4h3AaXWzmmEjz_0WG_-kYwqiMH_aVAW6dG2iQ_Wz902RHNyH44RTek-flDvs3lwiW_zvUutDfLRoXguSaIdYaJTDurqnMjNyMOXGn9FDA14ArC98nFVTXB8YN04N-1PDNsILyEXxFtG9QIHcZelMgKErPSW7qnofc0VE-1_eJ7ohJQam0-53nCM1mt3VwLWXW-h4mM-s8qeITu6YP6jrhkXIm_nlkyfIUOOOWX4PFiaRvTGoLUYp0K0PinCd1Txp-jERGvkPXv7fHJ7mN8qGAOh9QocxKEz-H6Q',
+ /* tslint:disable-next-line */
+ n: '3hcdO8Q55PC7zy9MLfpR2HrPmFf6GFMO_rQXUwtAI4JveIcTSuJ4h3AaXWzmmEjz_0WG_-kYwqiMH_aVAW6dG2iQ_Wz902RHNyH44RTek-flDvs3lwiW_zvUutDfLRoXguSaIdYaJTDurqnMjNyMOXGn9FDA14ArC98nFVTXB8YN04N-1PDNsILyEXxFtG9QIHcZelMgKErPSW7qnofc0VE-1_eJ7ohJQam0-53nCM1mt3VwLWXW-h4mM-s8qeITu6YP6jrhkXIm_nlkyfIUOOOWX4PFiaRvTGoLUYp0K0PinCd1Txp-jERGvkPXv7fHJ7mN8qGAOh9QocxKEz-H6Q',
e: 'AQAB',
kid: '0',
},
@@ -51,9 +52,13 @@ nock(`https://${process.env.AUTH0_DOMAIN}`)
.get('/.well-known/jwks.json')
.reply(200, jwks);
-export const getToken = (user = { auth0Id: faker.random.uuid() }) => {
- const payload = {
+export const getToken = (
+ user = { auth0Id: faker.random.uuid() },
+ emailVerified = true,
+) => {
+ const payload: Partial = {
sub: user.auth0Id,
+ email_verified: emailVerified,
};
const options = {
diff --git a/tsconfig.json b/tsconfig.json
index 3b4c9cb..5743bec 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -9,11 +9,18 @@
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",
- "resolveJsonModule": true
+ "resolveJsonModule": true,
+ "skipLibCheck": true,
+ "paths": {
+ "🧙♂️/types/*": ["src/types/*"]
+ }
},
+ "include": [
+ "src/**/*"
+ ],
"files": [
"src/main.ts",
- "src/utils/request.d.ts"
+ "src/types/request.d.ts"
],
"exclude": ["node_modules"]
}
diff --git a/yarn.lock b/yarn.lock
index 5fd973d..521d2c8 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -819,6 +819,14 @@
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==
+"@types/node-fetch@^2.6.1":
+ version "2.6.1"
+ resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.1.tgz#8f127c50481db65886800ef496f20bbf15518975"
+ integrity sha512-oMqjURCaxoSIsHSr1E47QHzbmzNR5rK8McHuNb11BOM9cHcIK3Avy0s/b2JlXHoQGTYS3NsvWzV1M0iK7l0wbA==
+ dependencies:
+ "@types/node" "*"
+ form-data "^3.0.0"
+
"@types/node@*":
version "12.0.2"
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.0.2.tgz#3452a24edf9fea138b48fad4a0a028a683da1e40"
@@ -1615,7 +1623,7 @@ color@^3.1.2:
color-convert "^1.9.1"
color-string "^1.5.2"
-combined-stream@^1.0.6, combined-stream@~1.0.6:
+combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6:
version "1.0.8"
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
dependencies:
@@ -2518,6 +2526,15 @@ form-data@^2.5.0:
combined-stream "^1.0.6"
mime-types "^2.1.12"
+form-data@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.1.tgz#ebd53791b78356a99af9a300d4282c4d5eb9755f"
+ integrity sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==
+ dependencies:
+ asynckit "^0.4.0"
+ combined-stream "^1.0.8"
+ mime-types "^2.1.12"
+
formidable@^1.2.0:
version "1.2.1"
resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.2.1.tgz#70fb7ca0290ee6ff961090415f4b3df3d2082659"