Skip to content

Commit

Permalink
add verify by email
Browse files Browse the repository at this point in the history
  • Loading branch information
ezzabuzaid committed Dec 6, 2019
1 parent f1d8613 commit ae3b8d1
Show file tree
Hide file tree
Showing 7 changed files with 86 additions and 40 deletions.
77 changes: 48 additions & 29 deletions src/app/api/portal/portal.routes.ts
Expand Up @@ -5,9 +5,11 @@ import { Request, Response } from 'express';
import usersService from '@api/users/users.service';
import { UsersSchema } from '@api/users';
import { Body } from '@lib/mongoose';
import { EmailService } from '@shared/email';
import { EmailService, fakeEmail } from '@shared/email';
import { AppUtils } from '@core/utils';
import { PortalHelper } from './portal.helper';
import { TokenExpiredError } from 'jsonwebtoken';
import { Auth } from './auth';

export interface IRefreshTokenBody {
token: string;
Expand Down Expand Up @@ -43,55 +45,72 @@ export class PortalRoutes {
// other instances will now it, (new column in users collection)
const { token, refreshToken } = req.body as IRefreshTokenBody;
// NOTE: if it's not valid it will implicity thrown an error
await tokenService.decodeToken(refreshToken);
const decodedRefreshToken = await tokenService.decodeToken(refreshToken);

const decodedToken = await tokenService.decodeToken(token);
// TODO: find a way to know that the token is really for this user (unique device id) generate one in
// login and save it in browser
if (AppUtils.not(tokenService.isTokenExpired(decodedToken))) {
throw new ErrorResponse(translate('not_allowed'), NetworkStatus.NOT_ACCEPTABLE);
try {
await tokenService.decodeToken(token);
} catch (error) {
if (error instanceof TokenExpiredError) {
// TODO: find a way to know that the token is really for this user (unique device id) generate one in
// login and save it in browser
const user = await throwIfNotExist({ _id: decodedRefreshToken.id });
// TODO: invalidate the refresh token directly after new one is generated
const response = new SuccessResponse<IRefreshTokenBody>({
token: PortalHelper.generateToken(user.id, user.role),
refreshToken: PortalHelper.generateRefreshToken(user.id)
});
return res.status(response.code).json(response);
} else {
throw error;
}
}
await throwIfNotExist({ _id: decodedToken.id });
const response = new SuccessResponse<IRefreshTokenBody>({
token: PortalHelper.generateToken(decodedToken.id, decodedToken.role),
refreshToken: PortalHelper.generateRefreshToken(decodedToken.id)
});
return res.status(response.code).json(response);
throw new ErrorResponse(translate('not_allowed'), NetworkStatus.NOT_ACCEPTABLE);

}

@Post(Constants.Endpoints.FORGET_PASSWORD)
public async forgotPassword(req: Request, res: Response) {
const { email } = req.body as Body<UsersSchema>;
const entity = await throwIfNotExist({ email });
const token = tokenService.generateToken({ id: entity.id, role: entity.role }, { expiresIn: '1h' });
const message = {
from: 'ezzabuzaid@gmail.com',
to: 'ezzabuzaid@hotmail.com',
subject: 'Nodemailer is unicode friendly ✔',
text: 'Hello to myself!',
html: `Click on this link to complete <a href="${token}">Reset Password</>`
};
const url = await EmailService.sendEmail(message);
const url = await EmailService.sendEmail(fakeEmail());
const response = new SuccessResponse({ url });
res.status(response.code).json(response);
}

@Post(Constants.Endpoints.RESET_PASSWORD)
public async resetPassword(req: Request, res: Response) {
// REVIEW if anyone get the token can change the user password,
// so we need to know that the user who really did that by answering a specifc questions, doing 2FA
// the attempts should be limited to 3 times, after that he need to re do the process again,
// if the procces faild 3 times, the account should be locked, and he need to call the support for that
const { password } = req.body as Body<UsersSchema>;
const decodedToken = await tokenService.decodeToken(req.headers.authorization);
await usersService.update({ id: decodedToken.id, body: { password } });
const response = new SuccessResponse(null);
const message = {
from: 'ezzabuzaid@gmail.com',
to: 'ezzabuzaid@hotmail.com',
subject: 'Nodemailer is unicode friendly ✔',
text: 'Hello to myself!',
html: `Password rest successfully`
};
await EmailService.sendEmail(message);
await EmailService.sendEmail(fakeEmail());
res.status(response.code).json(response);
}

// TODO: the user should verify his email and phonenumber
@Post(Constants.Endpoints.VERIFY_ACCOUNT)
public async verify(req: Request, res: Response) {
const { token } = req.query;
const decodedToken = await tokenService.decodeToken(token);
await usersService.update({ id: decodedToken.id, body: { verified: true } });
const response = new SuccessResponse(null);
res.status(response.code).json(response);
}

@Post(Constants.Endpoints.VERIFY_ACCOUNT, Auth.isAuthenticated)
public async sendVerificationEmail(req: Request, res: Response) {
const { token } = req.query;
const decodedToken = await tokenService.decodeToken(token);
await usersService.update({ id: decodedToken.id, body: { verified: true } });
const response = new SuccessResponse(null);
res.status(response.code).json(response);
}

}

async function throwIfNotExist(query: Partial<Body<UsersSchema> & { _id: string }>, message = 'not_exist') {
Expand Down
22 changes: 15 additions & 7 deletions src/app/api/portal/portal.spec.ts
Expand Up @@ -2,7 +2,7 @@ import { UsersSchema, ERoles } from '@api/users';
import { Body } from '@lib/mongoose';
import { superAgent } from '@test/index';
import { Constants, NetworkStatus, tokenService } from '@core/helpers';
import { getUri, generateExpiredToken, UserUtilityFixture } from '@test/fixture';
import { getUri, generateExpiredToken, UserUtilityFixture, sendRequest } from '@test/fixture';
import { AppUtils } from '@core/utils';
import { IRefreshTokenBody } from './portal.routes';
import { PortalHelper } from './portal.helper';
Expand Down Expand Up @@ -53,10 +53,10 @@ describe('Reset password ', () => {
});

describe('Refresh Token', () => {
async function getResponse(invalidToken, token = null) {
const req = (await superAgent).post(REFRESH_TOKEN);
return req.send({ refreshToken: invalidToken, token } as IRefreshTokenBody);
}
const getResponse = (invalidToken: string, token: string = null) => {
return sendRequest(REFRESH_TOKEN, { refreshToken: invalidToken, token } as IRefreshTokenBody);
};

const INVALID_TOKEN = 'invalid.token.refuesed';
it('should throw if the refresh token is expired', async () => {
const res = await getResponse(generateExpiredToken());
Expand All @@ -66,7 +66,7 @@ describe('Refresh Token', () => {
const res = await getResponse(INVALID_TOKEN);
expect(res.status).toBe(NetworkStatus.BAD_REQUEST);
});
it('should throw if the refresh token is invalid', async () => {
it('should throw if the token is invalid', async () => {
const res = await getResponse(
PortalHelper.generateRefreshToken(null),
INVALID_TOKEN
Expand All @@ -80,7 +80,15 @@ describe('Refresh Token', () => {
);
expect(res.status).toBe(NetworkStatus.NOT_ACCEPTABLE);
});

it('should not throw if the token is expired', async () => {
const userUtiltiy = new UserUtilityFixture();
await userUtiltiy.createUser();
const res = await getResponse(
PortalHelper.generateRefreshToken(userUtiltiy.user.id),
generateExpiredToken()
);
expect(res.status).toBe(NetworkStatus.OK);
});
// it.todo('should retrun with valid token and refresh token', async () => {
// const userUtility = new UserUtilityFixture();
// const user = await userUtility.createUser();
Expand Down
6 changes: 4 additions & 2 deletions src/app/api/users/users.model.ts
Expand Up @@ -46,11 +46,13 @@ export class UsersSchema {
],
unique: true,
}) public mobile: string;

@Field({
pure: true,
default: true
}) public verified: boolean;
public comparePassword(candidatePassword: string) {
return HashService.comparePassword(candidatePassword, this.password);
}
constructor(obj: Body<UsersSchema>) { }
}

export const UsersModel = BaseModel<UsersSchema>(UsersSchema);
8 changes: 7 additions & 1 deletion src/app/api/users/users.service.ts
@@ -1,14 +1,20 @@
import { UsersSchema } from './users.model';
import { CrudService } from '@shared/crud/crud.service';
import { usersRepo } from '.';
import { EmailService, fakeEmail } from '@shared/email';

// TODO: provide an option for strict schema checking to not allowed
// an additional attribute to come, morever check the types
// an additional attribute to come, moreover check the types
// TODO: Validate the body to meet the schema exactly using reflect metadata
class UserService extends CrudService<UsersSchema> {
constructor() {
super(usersRepo, {
unique: ['username', 'email'],
create: {
post(entity) {
EmailService.sendEmail(fakeEmail());
}
}
});
}
}
Expand Down
1 change: 1 addition & 0 deletions src/app/core/helpers/constants.ts
Expand Up @@ -12,6 +12,7 @@ export namespace Constants {
public static readonly FORGET_PASSWORD = 'forgetpassword';
public static readonly RESET_PASSWORD = 'resetpassword';
public static readonly REFRESH_TOKEN = 'refreshtoken';
public static readonly VERIFY_ACCOUNT = 'verifyaccount';
}
export class Schemas {
public static readonly favorites = 'favorites';
Expand Down
10 changes: 10 additions & 0 deletions src/app/shared/email/email.service.ts
Expand Up @@ -17,3 +17,13 @@ export class EmailService {
return nodemailer.getTestMessageUrl(info);
}
}

export function fakeEmail(): Mail.Options {
return {
from: 'ezzabuzaid@gmail.com',
to: 'ezzabuzaid@hotmail.com',
subject: 'Nodemailer is unicode friendly ✔',
text: 'Hello to myself!',
html: `Password rest successfully`
};
}
2 changes: 1 addition & 1 deletion src/test/fixture.ts
Expand Up @@ -17,7 +17,7 @@ export function generateExpiredToken() {
}

export class UserUtilityFixture {
private user = {
public user = {
id: null,
token: null
};
Expand Down

0 comments on commit ae3b8d1

Please sign in to comment.