Skip to content

Commit

Permalink
fixup! apply feedback
Browse files Browse the repository at this point in the history
Signed-off-by: jannyHou <[email protected]>
  • Loading branch information
nabdelgadir authored and jannyHou committed Jan 14, 2019
1 parent 8b2d6f7 commit 0001fe6
Show file tree
Hide file tree
Showing 9 changed files with 7,151 additions and 80 deletions.
7,072 changes: 7,072 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

13 changes: 5 additions & 8 deletions src/authentication-strategies/JWT.strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,22 @@ const SECRET = 'secretforjwt';
import {Request, HttpErrors} from '@loopback/rest';
import {UserProfile} from '@loopback/authentication';
import * as _ from 'lodash';
import {AuthenticationStrategy} from './authentication.strategy';

export class JWTStrategy {
// tslint:disable-next-line:no-any
export class JWTStrategy implements AuthenticationStrategy {
async authenticate(request: Request): Promise<UserProfile | undefined> {
// there is a discussion regarding how to retrieve the token,
// see comment https://github.com/strongloop/loopback-next/issues/1997#issuecomment-451054806
const token = request.query.token || request.headers['authorization'];
if (token) {
try {
const decoded = await verifyAsync(token, SECRET);
return Promise.resolve(_.pick(decoded, ['id', 'email']));
return _.pick(decoded, ['id', 'email']);
} catch (err) {
if (err)
return Promise.reject(
new HttpErrors.Unauthorized('Could not decode the JWT token!'),
);
throw new HttpErrors.Unauthorized('Could not decode the JWT token!');
}
} else {
return Promise.reject(new HttpErrors.Unauthorized('Token not found!'));
throw new HttpErrors.Unauthorized('Token not found!');
}
}
}
6 changes: 6 additions & 0 deletions src/authentication-strategies/authentication.strategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import {UserProfile} from '@loopback/authentication';
import {Request} from '@loopback/rest';

export interface AuthenticationStrategy {
authenticate(request: Request): Promise<UserProfile | undefined>;
}
51 changes: 12 additions & 39 deletions src/controllers/user.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,8 @@ import {
UserProfile,
AuthenticationBindings,
} from '@loopback/authentication';
import {Credentials} from '../types';
import {Credentials} from '../repositories/user.repository';
import * as _ from 'lodash';
const jwt = require('jsonwebtoken');
const signAsync = promisify(jwt.sign);

const hashAsync = promisify(hash);

Expand Down Expand Up @@ -166,9 +164,14 @@ export class UserController {
'200': {
description: 'Token',
content: {
'text/plain': {
'application/json': {
schema: {
type: 'string',
type: 'object',
properties: {
token: {
type: 'string',
},
},
},
},
},
Expand All @@ -177,39 +180,9 @@ export class UserController {
})
async login(
@requestBody() credentials: Credentials,
): Promise<String | undefined> {
// Validate Email
if (!isemail.validate(credentials.email)) {
throw new HttpErrors.UnprocessableEntity('invalid email');
}

// Validate Password Length
if (credentials.password.length < 8) {
throw new HttpErrors.UnprocessableEntity(
'password must be minimum 8 characters',
);
}

// Check if user exists
try {
const foundUser = await this.userRepository.findOne({
where: {email: credentials.email, password: credentials.password},
});
let token = undefined;
if (foundUser) {
const currentUser = _.pick(foundUser.toJSON(), ['id', 'email']);
// Generate user token using JWT
token = await signAsync(currentUser, 'secretforjwt', {
expiresIn: 300,
});
} else {
return Promise.reject(
new HttpErrors.Unauthorized('Wrong credentials!'),
);
}
return Promise.resolve(token);
} catch (err) {
return Promise.reject(new Error('Wrong Credentials!'));
}
): Promise<{token: string}> {
let res = {token: ''};
res.token = await this.userRepository.login(credentials);
return res;
}
}
4 changes: 2 additions & 2 deletions src/providers/custom.authentication.provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {Getter, Provider, Setter, inject} from '@loopback/context';
import {Request} from '@loopback/rest';
import {AuthenticationBindings} from '@loopback/authentication';
import {AuthenticateFn, UserProfile} from '@loopback/authentication';
import {JWTStrategy} from '../authentication-strategies/JWT.strategy';
import {AuthenticationStrategy} from '../authentication-strategies/authentication.strategy';

/**
* @description Provider of a function which authenticates
Expand All @@ -24,7 +24,7 @@ export class MyAuthenticateActionProvider implements Provider<AuthenticateFn> {
// defer resolution of the strategy until authenticate() action
// is executed.
@inject.getter(AuthenticationBindings.STRATEGY)
readonly getStrategy: Getter<JWTStrategy>,
readonly getStrategy: Getter<AuthenticationStrategy>,
@inject.setter(AuthenticationBindings.CURRENT_USER)
readonly setCurrentUser: Setter<UserProfile>,
) {}
Expand Down
43 changes: 42 additions & 1 deletion src/repositories/user.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,24 @@ import {
import {User, Order} from '../models';
import {inject} from '@loopback/core';
import {OrderRepository} from './order.repository';
import * as isemail from 'isemail';
import {HttpErrors} from '@loopback/rest';
import _ = require('lodash');
import {promisify} from 'util';
import {toJSON} from '@loopback/testlab';
const jwt = require('jsonwebtoken');
const signAsync = promisify(jwt.sign);

export type Credentials = {
email: string;
password: string;
};

export class UserRepository extends DefaultCrudRepository<
User,
typeof User.prototype.id
> {
public orders: HasManyRepositoryFactory<Order, typeof User.prototype.id>;
// From @jannyhou: should the relation here be hasOne or hasMany?

constructor(
@inject('datasources.mongo') protected datasource: juggler.DataSource,
Expand All @@ -30,4 +41,34 @@ export class UserRepository extends DefaultCrudRepository<
async () => orderRepository,
);
}

async login(credentials: Credentials) {
// Validate Email
if (!isemail.validate(credentials.email)) {
throw new HttpErrors.UnprocessableEntity('invalid email');
}

// Validate Password Length
if (credentials.password.length < 8) {
throw new HttpErrors.UnprocessableEntity(
'password must be minimum 8 characters',
);
}

const foundUser = await this.findOne({
where: {email: credentials.email, password: credentials.password},
});

if (!foundUser) {
throw new HttpErrors.Unauthorized('Wrong credentials!');
}

const currentUser = _.pick(toJSON(foundUser), ['id', 'email']);

// Generate user token using JWT
const token = await signAsync(currentUser, 'secretforjwt', {
expiresIn: 300,
});
return token;
}
}
4 changes: 0 additions & 4 deletions src/types/custom-types.ts

This file was deleted.

1 change: 0 additions & 1 deletion src/types/index.ts

This file was deleted.

37 changes: 12 additions & 25 deletions test/acceptance/user.controller.acceptance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {Client, expect} from '@loopback/testlab';
import {Client, expect, toJSON} from '@loopback/testlab';
import {Response} from 'supertest';
import {ShoppingApplication} from '../..';
import {UserRepository, OrderRepository} from '../../src/repositories';
Expand Down Expand Up @@ -143,8 +143,7 @@ describe('UserController', () => {
.then(getToken);

function getToken(res: Response) {
// is it good to return the token as text?
const token = res.text;
const token = res.body.token;
expect(token).to.not.be.empty();
}
});
Expand All @@ -160,21 +159,15 @@ describe('UserController', () => {

it('/me returns the current user', async () => {
const newUser = await userRepo.create(user);
let token = '';
await client
.post('/users/login')
.send({email: newUser.email, password: newUser.password})
.expect(200)
.then(getToken);
const token = await userRepo.login({
email: newUser.email,
password: newUser.password,
});

function getToken(res: Response) {
// is it good to return the token as text?
token = res.text;
}
// MongoDB returns an id object we need to convert to string
// since the REST API returns a string for the id property.
newUser.id = newUser.id.toString();
const me = _.pick(newUser.toJSON(), ['id', 'email']);
const me = _.pick(toJSON(newUser), ['id', 'email']);

await client
.get('/users/me')
Expand All @@ -184,18 +177,12 @@ describe('UserController', () => {

it('/me returns 401 when the token is not provided', async () => {
const newUser = await userRepo.create(user);
// tslint:disable-next-line:no-unused-variable
let token = '';
await client
.post('/users/login')
.send({email: newUser.email, password: newUser.password})
.expect(200)
.then(getToken);

function getToken(res: Response) {
// is it good to return the token as text?
token = res.text;
}
const token = await userRepo.login({
email: newUser.email,
password: newUser.password,
});
expect(token).to.be.not.empty();

await client.get('/users/me').expect(401);
});
Expand Down

0 comments on commit 0001fe6

Please sign in to comment.