From 72d220d6ad71a55461e787d00c2720dbddfb8c77 Mon Sep 17 00:00:00 2001 From: Moshe Feuchtwanger Date: Thu, 28 Apr 2022 21:45:00 +0300 Subject: [PATCH 01/10] feat: verify email --- src/middlewares/auth.middleware.ts | 6 ++++++ src/modules/common/interfaces/user.interface.ts | 1 + 2 files changed, 7 insertions(+) diff --git a/src/middlewares/auth.middleware.ts b/src/middlewares/auth.middleware.ts index fc77304..1e82ce8 100644 --- a/src/middlewares/auth.middleware.ts +++ b/src/middlewares/auth.middleware.ts @@ -42,6 +42,12 @@ export class AuthMiddleware implements NestMiddleware { next(); return; } + if (!req.user.email_verified) { + return res.status(401).send({ + success: false, + errors: ['Email not verified'], + }); + } if (error) { const status = error.status || 401; const message = diff --git a/src/modules/common/interfaces/user.interface.ts b/src/modules/common/interfaces/user.interface.ts index a064b39..f3bdaee 100644 --- a/src/modules/common/interfaces/user.interface.ts +++ b/src/modules/common/interfaces/user.interface.ts @@ -26,6 +26,7 @@ export interface User extends Document { readonly _id: ObjectID; readonly auth0Id: string; readonly email: string; + readonly email_verified: boolean; readonly available: boolean; readonly name: string; readonly avatar: string; From 7fb4cfcdba3a8c56aefe3069a083f6718de6fea4 Mon Sep 17 00:00:00 2001 From: Moshe Feuchtwanger Date: Thu, 28 Apr 2022 23:39:03 +0300 Subject: [PATCH 02/10] return email_verified with current user --- docs/cc-api-spec.json | 2 +- src/modules/common/dto/user.dto.ts | 15 ++++++++++++++- src/modules/common/interfaces/user.interface.ts | 2 ++ src/modules/common/users.service.ts | 2 +- .../users/__tests__/users.controller.spec.ts | 17 ++++++++++++++++- src/modules/users/users.controller.ts | 16 ++++++++++++---- 6 files changed, 46 insertions(+), 8 deletions(-) diff --git a/docs/cc-api-spec.json b/docs/cc-api-spec.json index 990d7db..a247024 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/{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/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 f3bdaee..0f5b0c6 100644 --- a/src/modules/common/interfaces/user.interface.ts +++ b/src/modules/common/interfaces/user.interface.ts @@ -26,6 +26,8 @@ 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; diff --git a/src/modules/common/users.service.ts b/src/modules/common/users.service.ts index 007e9ab..9c455fb 100644 --- a/src/modules/common/users.service.ts +++ b/src/modules/common/users.service.ts @@ -27,7 +27,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/users/__tests__/users.controller.spec.ts b/src/modules/users/__tests__/users.controller.spec.ts index 5852d87..9455e23 100644 --- a/src/modules/users/__tests__/users.controller.spec.ts +++ b/src/modules/users/__tests__/users.controller.spec.ts @@ -117,7 +117,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 +127,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)); diff --git a/src/modules/users/users.controller.ts b/src/modules/users/users.controller.ts index ae042f6..02ea302 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, @@ -76,18 +77,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 user: User = await this.auth0Service.getUserProfile( data.access_token, userId, ); @@ -137,7 +138,7 @@ export class UsersController { }, }); - response.data = newUser; + response.data = this.enrichUserResponse(newUser, request.user); } } catch (error) { return { @@ -158,6 +159,13 @@ export class UsersController { return response; } + private enrichUserResponse(user: User, auth0User: User): User { + return { + ...user, + email_verified: Boolean(auth0User.email_verified), + }; + } + private async shouldIncludeChannels( currentUser?: User, requestedUser?: User, From 298ab9fe080b2df5ce70c615baa4e80e4fd62233 Mon Sep 17 00:00:00 2001 From: Moshe Feuchtwanger Date: Mon, 2 May 2022 01:06:27 +0300 Subject: [PATCH 03/10] complete send verification email --- CONTRIBUTING.md | 1 + docs/cc-api-spec.json | 2 +- package.json | 1 + .../common/{ => auth0}/auth0.service.ts | 47 +++++++++++++++++-- src/modules/common/auth0/auth0.types.ts | 32 +++++++++++++ src/modules/common/common.module.ts | 2 +- .../__tests__/mentorships.controller.spec.ts | 2 +- src/modules/users/users.controller.ts | 30 ++++++++++-- src/utils/request.d.ts | 14 +++++- yarn.lock | 19 +++++++- 10 files changed, 137 insertions(+), 13 deletions(-) rename src/modules/common/{ => auth0}/auth0.service.ts (52%) create mode 100644 src/modules/common/auth0/auth0.types.ts diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 269cc2d..32dc651 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` +- `update:users` (Sending email verification) These are the only scopes we need, but you can select all if you want. diff --git a/docs/cc-api-spec.json b/docs/cc-api-spec.json index a247024..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"},"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 +{"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/modules/common/auth0.service.ts b/src/modules/common/auth0/auth0.service.ts similarity index 52% rename from src/modules/common/auth0.service.ts rename to src/modules/common/auth0/auth0.service.ts index 8e730a1..002826c 100644 --- a/src/modules/common/auth0.service.ts +++ b/src/modules/common/auth0/auth0.service.ts @@ -1,11 +1,13 @@ -import { Injectable } from '@nestjs/common'; +import { HttpException, Injectable } from '@nestjs/common'; import fetch from 'node-fetch'; -import Config from '../../config'; +import * as Sentry from '@sentry/node'; +import Config from '../../../config'; +import type { Auth0Response } from './auth0.types'; @Injectable() export class Auth0Service { // Get an access token for the Auth0 Admin API - async getAdminAccessToken() { + async getAdminAccessToken(): Promise<{ access_token: string }> { const options = { method: 'POST', headers: { 'content-type': 'application/json' }, @@ -27,7 +29,7 @@ export class Auth0Service { } // Get the user's profile from auth0 - async getUserProfile(accessToken, userID) { + async getUserProfile(accessToken: string, userID: string) { const options = { headers: { Authorization: `Bearer ${accessToken}`, @@ -59,4 +61,41 @@ export class Auth0Service { return response; } + + async sendVerificationEmail( + accessToken: string, + auth0UserId: string, + ): Promise { + try { + const [provider, userId] = auth0UserId.split('|'); + const payload = { + user_id: auth0UserId, + identity: { user_id: userId, provider }, + }; + const options = { + method: 'POST', + headers: { + Authorization: `Bearer ${accessToken}`, + 'content-type': 'application/json', + }, + body: JSON.stringify(payload), + }; + + const response: Auth0Response = await ( + await fetch( + `https://${Config.auth0.backend.DOMAIN}/api/v2/jobs/verification-email`, + 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..fe08e39 --- /dev/null +++ b/src/modules/common/auth0/auth0.types.ts @@ -0,0 +1,32 @@ +interface Auth0ResponseSuccess {} // tslint:disable-line + +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/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/users.controller.ts b/src/modules/users/users.controller.ts index 02ea302..25fa1dd 100644 --- a/src/modules/users/users.controller.ts +++ b/src/modules/users/users.controller.ts @@ -28,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'; @@ -87,7 +87,7 @@ export class UsersController { if (!currentUser) { try { - const data: any = await this.auth0Service.getAdminAccessToken(); + const data = await this.auth0Service.getAdminAccessToken(); const user: User = await this.auth0Service.getUserProfile( data.access_token, userId, @@ -159,7 +159,7 @@ export class UsersController { return response; } - private enrichUserResponse(user: User, auth0User: User): User { + private enrichUserResponse(user: User, auth0User: AccessTokenUser): User { return { ...user, email_verified: Boolean(auth0User.email_verified), @@ -388,6 +388,30 @@ 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.sendVerificationEmail( + data.access_token, + user.auth0Id, + ); + + 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/utils/request.d.ts b/src/utils/request.d.ts index e6715a2..6415b7b 100644 --- a/src/utils/request.d.ts +++ b/src/utils/request.d.ts @@ -1,7 +1,17 @@ -import { User } from '../modules/common/interfaces/user.interface'; +interface AccessTokenUser { + iss: string; + sub: string; + aud: string; + iat: number; + exp: number; + at_hash: string; + nonce: string; + auth0Id: string; + email_verified: boolean; +} declare module 'express-serve-static-core' { interface Request { - user?: User; + user?: AccessTokenUser; } } 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" From 9203287444ecc0d6d3316dc1718cc6ba8cf51baf Mon Sep 17 00:00:00 2001 From: Moshe Feuchtwanger Date: Mon, 2 May 2022 02:09:51 +0300 Subject: [PATCH 04/10] auth0 job -> ticket to control redirect url todo: create verification email template --- CONTRIBUTING.md | 2 +- src/config/index.ts | 3 +++ src/modules/common/auth0/auth0.service.ts | 12 +++++++----- src/modules/common/auth0/auth0.types.ts | 10 ++++++++-- src/modules/email/interfaces/email.interface.ts | 8 ++++++++ src/modules/users/users.controller.ts | 10 +++++++++- 6 files changed, 36 insertions(+), 9 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 32dc651..bb32bf6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -84,7 +84,7 @@ After that you will need to select the following scopes: - `read:users` - `read:roles` - `delete:users` -- `update:users` (Sending email verification) +- `create:user_tickets` (Sending email verification) These are the only scopes we need, but you can select all if you want. 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/modules/common/auth0/auth0.service.ts b/src/modules/common/auth0/auth0.service.ts index 002826c..460072e 100644 --- a/src/modules/common/auth0/auth0.service.ts +++ b/src/modules/common/auth0/auth0.service.ts @@ -2,7 +2,7 @@ import { HttpException, Injectable } from '@nestjs/common'; import fetch from 'node-fetch'; import * as Sentry from '@sentry/node'; import Config from '../../../config'; -import type { Auth0Response } from './auth0.types'; +import type { Auth0Response, EmailVerificationTicket } from './auth0.types'; @Injectable() export class Auth0Service { @@ -62,28 +62,30 @@ export class Auth0Service { return response; } - async sendVerificationEmail( + async createVerificationEmailTicket( accessToken: string, auth0UserId: string, ): Promise { 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: { - Authorization: `Bearer ${accessToken}`, + 'Authorization': `Bearer ${accessToken}`, 'content-type': 'application/json', }, body: JSON.stringify(payload), }; - const response: Auth0Response = await ( + const response: Auth0Response = await ( await fetch( - `https://${Config.auth0.backend.DOMAIN}/api/v2/jobs/verification-email`, + `https://${Config.auth0.backend.DOMAIN}/api/v2/tickets/email-verification`, options, ) ).json(); diff --git a/src/modules/common/auth0/auth0.types.ts b/src/modules/common/auth0/auth0.types.ts index fe08e39..3f2b68d 100644 --- a/src/modules/common/auth0/auth0.types.ts +++ b/src/modules/common/auth0/auth0.types.ts @@ -1,4 +1,8 @@ -interface Auth0ResponseSuccess {} // tslint:disable-line +type Auth0ResponseSuccess = T; + +export interface EmailVerificationTicket { + ticket: string; +} interface Auth0ResponseError { statusCode: number; @@ -7,7 +11,9 @@ interface Auth0ResponseError { errorCode: string; } -export type Auth0Response = Auth0ResponseSuccess | Auth0ResponseError; +export type Auth0Response = + | Auth0ResponseSuccess + | Auth0ResponseError; interface Auth0UserIdentity { connection: string; diff --git a/src/modules/email/interfaces/email.interface.ts b/src/modules/email/interfaces/email.interface.ts index 1a29f46..f4a3d07 100644 --- a/src/modules/email/interfaces/email.interface.ts +++ b/src/modules/email/interfaces/email.interface.ts @@ -19,6 +19,13 @@ interface WelcomePayload { }; } +interface VerificationEmailPayload { + name: 'verification-email'; + data: { + ticket: string; + }; +} + interface MentorshipAccepted { name: 'mentorship-accepted'; data: { @@ -109,6 +116,7 @@ interface MentorFreeze { export type EmailParams = Required> & ( | WelcomePayload + | VerificationEmailPayload | MentorshipAccepted | MentorshipCancelled | MentorshipDeclined diff --git a/src/modules/users/users.controller.ts b/src/modules/users/users.controller.ts index 25fa1dd..6ee63dd 100644 --- a/src/modules/users/users.controller.ts +++ b/src/modules/users/users.controller.ts @@ -401,11 +401,19 @@ export class UsersController { } const data = await this.auth0Service.getAdminAccessToken(); - const response = await this.auth0Service.sendVerificationEmail( + const response = await this.auth0Service.createVerificationEmailTicket( data.access_token, user.auth0Id, ); + // TODO - create email verification template + this.emailService.sendLocalTemplate({ + name: 'verification-email', + data: response, + to: user.email, + subject: 'Verify your email', + }); + return { success: true, data: response, From 2e51633dddba9dd5c7cff62741baff5a7fa48095 Mon Sep 17 00:00:00 2001 From: Moshe Feuchtwanger Date: Tue, 3 May 2022 10:12:19 +0300 Subject: [PATCH 05/10] email verification tempalte todo: check it works using the client --- content/email_templates/README.md | 4 +- .../email_templates/email-verification.html | 55 +++++++++++++++++++ content/email_templates/show.js | 2 +- 3 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 content/email_templates/email-verification.html 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 @@ +
+ + + + + + +
+ Illustration +
+

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`); }); From d309b96d773434a30936cec93dde85b2518b3de0 Mon Sep 17 00:00:00 2001 From: Moshe Feuchtwanger Date: Wed, 4 May 2022 11:42:05 +0300 Subject: [PATCH 06/10] complete email verification template --- src/modules/common/auth0/auth0.service.ts | 5 +++-- src/modules/common/users.service.ts | 3 ++- src/modules/email/interfaces/email.interface.ts | 5 +++-- src/modules/users/users.controller.ts | 8 +++++--- 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/modules/common/auth0/auth0.service.ts b/src/modules/common/auth0/auth0.service.ts index 460072e..d4a2079 100644 --- a/src/modules/common/auth0/auth0.service.ts +++ b/src/modules/common/auth0/auth0.service.ts @@ -65,7 +65,7 @@ export class Auth0Service { async createVerificationEmailTicket( accessToken: string, auth0UserId: string, - ): Promise { + ) { try { const [provider, userId] = auth0UserId.split('|'); const payload = { @@ -77,7 +77,8 @@ export class Auth0Service { const options = { method: 'POST', headers: { - 'Authorization': `Bearer ${accessToken}`, + /* tslint:disable-next-line */ + Authorization: `Bearer ${accessToken}`, 'content-type': 'application/json', }, body: JSON.stringify(payload), diff --git a/src/modules/common/users.service.ts b/src/modules/common/users.service.ts index 9c455fb..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 { diff --git a/src/modules/email/interfaces/email.interface.ts b/src/modules/email/interfaces/email.interface.ts index f4a3d07..248490e 100644 --- a/src/modules/email/interfaces/email.interface.ts +++ b/src/modules/email/interfaces/email.interface.ts @@ -20,9 +20,10 @@ interface WelcomePayload { } interface VerificationEmailPayload { - name: 'verification-email'; + name: 'email-verification'; data: { - ticket: string; + name: string; + link: string; }; } diff --git a/src/modules/users/users.controller.ts b/src/modules/users/users.controller.ts index 6ee63dd..b480fa3 100644 --- a/src/modules/users/users.controller.ts +++ b/src/modules/users/users.controller.ts @@ -406,10 +406,12 @@ export class UsersController { user.auth0Id, ); - // TODO - create email verification template this.emailService.sendLocalTemplate({ - name: 'verification-email', - data: response, + name: 'email-verification', + data: { + name: user.name, + link: response.ticket, + }, to: user.email, subject: 'Verify your email', }); From 3d94d62a5d76b6ae241f726bd76fc0cebd74827c Mon Sep 17 00:00:00 2001 From: Moshe Feuchtwanger Date: Wed, 4 May 2022 12:30:24 +0300 Subject: [PATCH 07/10] fix build --- src/middlewares/auth.middleware.ts | 3 ++- .../admin/__tests__/mentors.controller.spec.ts | 2 +- src/modules/admin/admin.controller.ts | 2 +- .../lists/__tests__/favorites.controller.spec.ts | 2 +- src/modules/lists/__tests__/lists.controller.spec.ts | 2 +- src/modules/lists/favorites.controller.ts | 6 +++--- src/modules/lists/lists.controller.ts | 2 +- src/modules/users/users.controller.ts | 1 + src/{utils => types}/request.d.ts | 8 ++++---- tsconfig.json | 11 +++++++++-- 10 files changed, 24 insertions(+), 15 deletions(-) rename src/{utils => types}/request.d.ts (59%) diff --git a/src/middlewares/auth.middleware.ts b/src/middlewares/auth.middleware.ts index 1e82ce8..831aee8 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({ 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/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/users/users.controller.ts b/src/modules/users/users.controller.ts index b480fa3..3239cbe 100644 --- a/src/modules/users/users.controller.ts +++ b/src/modules/users/users.controller.ts @@ -39,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() diff --git a/src/utils/request.d.ts b/src/types/request.d.ts similarity index 59% rename from src/utils/request.d.ts rename to src/types/request.d.ts index 6415b7b..e059f57 100644 --- a/src/utils/request.d.ts +++ b/src/types/request.d.ts @@ -1,3 +1,5 @@ +import { Request as ExpressReqeust } from 'express'; + interface AccessTokenUser { iss: string; sub: string; @@ -10,8 +12,6 @@ interface AccessTokenUser { email_verified: boolean; } -declare module 'express-serve-static-core' { - interface Request { - user?: AccessTokenUser; - } +export interface Request extends ExpressReqeust { + user?: AccessTokenUser; } 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"] } From bc67dedc4a6095d926094b3c78a84309aa6ade2a Mon Sep 17 00:00:00 2001 From: Moshe Feuchtwanger Date: Fri, 6 May 2022 09:13:06 +0300 Subject: [PATCH 08/10] be nice --- src/middlewares/auth.middleware.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/middlewares/auth.middleware.ts b/src/middlewares/auth.middleware.ts index 831aee8..ffa813c 100644 --- a/src/middlewares/auth.middleware.ts +++ b/src/middlewares/auth.middleware.ts @@ -46,7 +46,7 @@ export class AuthMiddleware implements NestMiddleware { if (!req.user.email_verified) { return res.status(401).send({ success: false, - errors: ['Email not verified'], + errors: ['Please verify your email address'], }); } if (error) { From 01513cc95a88e90fd3e2437a9de6c00eec1c2362 Mon Sep 17 00:00:00 2001 From: Moshe Feuchtwanger Date: Fri, 6 May 2022 09:24:37 +0300 Subject: [PATCH 09/10] fix tests --- .../users/__tests__/users.controller.spec.ts | 29 ++++++++++++------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/src/modules/users/__tests__/users.controller.spec.ts b/src/modules/users/__tests__/users.controller.spec.ts index 9455e23..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 {} @@ -422,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); }); @@ -477,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); }); @@ -497,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); @@ -524,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); @@ -553,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); From a400f43563ebfd70af547ea1edb3da86582f669c Mon Sep 17 00:00:00 2001 From: Moshe Feuchtwanger Date: Sun, 8 May 2022 08:50:02 +0300 Subject: [PATCH 10/10] add + fix e2e tests --- src/middlewares/auth.middleware.ts | 13 ++++++------ src/types/request.d.ts | 2 +- test/api/mentorships.e2e-spec.ts | 33 ++++++++++++++++++++++-------- test/utils/jwt.ts | 13 ++++++++---- 4 files changed, 41 insertions(+), 20 deletions(-) diff --git a/src/middlewares/auth.middleware.ts b/src/middlewares/auth.middleware.ts index ffa813c..c2d9afa 100644 --- a/src/middlewares/auth.middleware.ts +++ b/src/middlewares/auth.middleware.ts @@ -43,12 +43,7 @@ export class AuthMiddleware implements NestMiddleware { next(); return; } - if (!req.user.email_verified) { - return res.status(401).send({ - success: false, - errors: ['Please verify your email address'], - }); - } + if (error) { const status = error.status || 401; const message = @@ -60,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/types/request.d.ts b/src/types/request.d.ts index e059f57..2943963 100644 --- a/src/types/request.d.ts +++ b/src/types/request.d.ts @@ -1,6 +1,6 @@ import { Request as ExpressReqeust } from 'express'; -interface AccessTokenUser { +export interface AccessTokenUser { iss: string; sub: string; aud: string; 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 = {