Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: verify email #249

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ After that you will need to select the following scopes:
- `read:users`
- `read:roles`
- `delete:users`
- `create:user_tickets` (Sending email verification)

These are the only scopes we need, but you can select all if you want.

Expand Down
4 changes: 3 additions & 1 deletion content/email_templates/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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}|
|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}|
55 changes: 55 additions & 0 deletions content/email_templates/email-verification.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<div style="text-align: center; font-size: 18px">
<table width="100%">
<tbody>
<tr>
<td align="center">
<img
style="width: 39%; max-width: 234px"
src="http://cdn.mcauto-images-production.sendgrid.net/83a8af126d5ca8ff/70fa68eb-a954-47c6-9798-306089b6a4e3/600x370.jpg"
alt="Illustration"
/>
</td>
</tr>
</tbody>
</table>
<h2 style="font-size: 26px; font-weight: normal">Hey <%= name %></h2>
<h1
style="
font-size: 32px;
line-height: 42px;
font-weight: normal;
color: #69d5b1;
"
>
You're almost there!
</h1>
<p>Please click the link below to verify your email</p>
<p style="margin-top: 10px">
<a
href="https://mentors.codingcoach.io/me"
style="
background-color: #00bc89;
border: 1px solid #333333;
border-color: #00bc89;
border-radius: 6px;
border-width: 1px;
color: #ffffff;
display: inline-block;
font-size: 16px;
font-weight: normal;
letter-spacing: 0px;
line-height: 16px;
padding: 12px 18px 12px 18px;
text-align: center;
text-decoration: none;
"
target="_blank"
>Verify</a
>
</p>
<p style="margin-top: 10px">
<small>
(Or copy and paste this url
<a herf="<%= link %>" target="_blank"><%= link %></a> into your browser)
</p>
</div>
2 changes: 1 addition & 1 deletion content/email_templates/show.js
Original file line number Diff line number Diff line change
Expand Up @@ -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`);
});
2 changes: 1 addition & 1 deletion docs/cc-api-spec.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 3 additions & 0 deletions src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ const config = {
pagination: {
limit: 20,
},
urls: {
CLIENT_BASE_URL: process.env.CLIENT_BASE_URL,
},
};

export default config;
9 changes: 8 additions & 1 deletion src/middlewares/auth.middleware.ts
Original file line number Diff line number Diff line change
@@ -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({
Expand Down Expand Up @@ -42,6 +43,12 @@ 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 =
Expand Down
2 changes: 1 addition & 1 deletion src/modules/admin/__tests__/mentors.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { UnauthorizedException, BadRequestException } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import { Request } from '🧙‍♂️/types/request';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lol, what's with the 🧙‍♂️ in the filepath?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was exciting to use tsconfig/paths for the first time :)

"paths": {
"🧙‍♂️/types/*": ["src/types/*"]
}

🧙‍♂️ is our paths root

import { MentorsController } from '../../mentors/mentors.controller';
import { UsersService } from '../../common/users.service';
import { EmailService } from '../../email/email.service';
Expand All @@ -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 {}

Expand Down
2 changes: 1 addition & 1 deletion src/modules/admin/admin.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
62 changes: 0 additions & 62 deletions src/modules/common/auth0.service.ts

This file was deleted.

104 changes: 104 additions & 0 deletions src/modules/common/auth0/auth0.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { HttpException, Injectable } from '@nestjs/common';
import fetch from 'node-fetch';
import * as Sentry from '@sentry/node';
import Config from '../../../config';
import type { Auth0Response, EmailVerificationTicket } from './auth0.types';

@Injectable()
export class Auth0Service {
// Get an access token for the Auth0 Admin API
async getAdminAccessToken(): Promise<{ access_token: string }> {
const options = {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
client_id: Config.auth0.backend.CLIENT_ID,
client_secret: Config.auth0.backend.CLIENT_SECRET,
audience: `https://${Config.auth0.backend.DOMAIN}/api/v2/`,
grant_type: 'client_credentials',
}),
};

const response = await fetch(
`https://${Config.auth0.backend.DOMAIN}/oauth/token`,
options,
);
const json = await response.json();

return json;
}

// Get the user's profile from auth0
async getUserProfile(accessToken: string, userID: string) {
const options = {
headers: {
Authorization: `Bearer ${accessToken}`,
},
};

const response = await fetch(
`https://${Config.auth0.backend.DOMAIN}/api/v2/users/${userID}`,
options,
);
const json = await response.json();

return json;
}

// Deletes a user from auth0
async deleteUser(accessToken: string, userID: string) {
const options = {
method: 'DELETE',
headers: {
Authorization: `Bearer ${accessToken}`,
},
};

const response = await fetch(
`https://${Config.auth0.backend.DOMAIN}/api/v2/users/${userID}`,
options,
);

return response;
}

async createVerificationEmailTicket(
accessToken: string,
auth0UserId: string,
) {
try {
const [provider, userId] = auth0UserId.split('|');
const payload = {
result_url: Config.urls.CLIENT_BASE_URL,
user_id: auth0UserId,
identity: { user_id: userId, provider },

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of curiousity, why are you stripping of the first part of the userId?

It looks like you refer to userId as the part behind the |, while at Auth0 we refer to a userId including what's in front of the |.

Looking at https://auth0.com/docs/manage-users/user-accounts/identify-users, it seems to be that we guarantee the userId to be unique in a tenant (which means, including the part before the |). I am not sure there is any guarantee the part behind the | is unique across multiple providers.

That said, I am not entirely sure here. But if u don't need to strip it, I wouldn't strip it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First, this is what works for me :)
The payload's user_id is indeed the full userId.
The identify is the combination of stripped userId and the provider.
Isn't that combination guarantee unique userId?
I can try passing the full userId in identify if you think it ahould work.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just wanted to call it out that you are aware. Not asking to change anything. The most important part is that it works for u, just as I mentioned ... If u don't need the stripping, I wouldn't do it. But if u need it, u need it.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't that combination guarantee unique userId?

Yes but the prefix is part of the userId for us. So seeing it stripped of while still referring to it as a userId can be confusing if you are used to Auth0 UserId's.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you saying I should use the full userid or should I name it differently? If 2, any suggestion?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you saying I should use the full userid or should I name it differently? If 2, any suggestion?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think as long as you are aware of the importance of the provider, you should be fine either way.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you!

};

const options = {
method: 'POST',
headers: {
/* tslint:disable-next-line */
Authorization: `Bearer ${accessToken}`,
'content-type': 'application/json',
},
body: JSON.stringify(payload),
};

const response: Auth0Response<EmailVerificationTicket> = await (
await fetch(
`https://${Config.auth0.backend.DOMAIN}/api/v2/tickets/email-verification`,
options,
)
).json();

if ('statusCode' in response) {
throw new HttpException(response, response.statusCode);
}

return response;
} catch (error) {
Sentry.captureException(error);
throw error;
}
}
}
38 changes: 38 additions & 0 deletions src/modules/common/auth0/auth0.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
type Auth0ResponseSuccess<T> = T;

export interface EmailVerificationTicket {
ticket: string;
}

interface Auth0ResponseError {
statusCode: number;
error: string;
message: string;
errorCode: string;
}

export type Auth0Response<T = any> =
| Auth0ResponseSuccess<T>
| 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;
}
2 changes: 1 addition & 1 deletion src/modules/common/common.module.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
15 changes: 14 additions & 1 deletion src/modules/common/dto/user.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ export class UserDto {
@ApiModelProperty()
readonly _id: string;

// for mentorship mentor / mentee
@ApiModelProperty()
readonly id: string;

@ApiModelProperty()
@IsEmail()
@IsString()
Expand All @@ -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()
Expand Down Expand Up @@ -94,7 +107,7 @@ export class UserDto {
@ArrayMaxSize(3)
readonly channels: Channel[];

constructor(values) {
constructor(values: Partial<UserDto>) {
Object.assign(this, values);
}
}
Loading