Skip to content

Commit b0659b0

Browse files
committed
Authentication , Authorization, Error Handlers and redis improvements
1 parent 42f1314 commit b0659b0

28 files changed

+612
-123
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ Node / Express / Docker API Boilerplate
1010
## API Side:
1111
* docker-compose up
1212
* http://localhost:8080/api/v1/example to test the api
13+
* http://localhost:8080/api/v1/docs/ to swagger interface
1314
* Database: Postgres. Use localhost:5433 to connect to the database from your local.
1415

1516
## Client Side:

docker-compose.yml

+6
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ services:
88
volumes:
99
- ./volumes/postgresql:/var/lib/postgresql/data
1010

11+
redis:
12+
image: redis:5.0.3-alpine
13+
ports:
14+
- '6380:6379'
15+
1116
api:
1217
build: .
1318
volumes:
@@ -20,4 +25,5 @@ services:
2025
- CHOKIDAR_USEPOLLING=true
2126
depends_on:
2227
- "db"
28+
- "redis"
2329
command: ["./scripts/wait-for-it.sh", "db:5432", "--", "npm", "start"] # Wait for postgres

packages/api/controllers/AuthController.js

+12
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11

22
const Joi = require('../utils/joi');
3+
const redis = require('../redis');
34
const { User } = require('../models');
45
const { LogicError } = require('../utils/errors');
56
const hash = require('../utils/hash');
@@ -44,6 +45,17 @@ async function login(req, res, next) {
4445
if (!isPasswordMatches) throw new LogicError('username_password_wrong');
4546

4647
const token = auth.generateToken(user.id);
48+
const tokens = await redis.getAccessTokens(user.id) || {};
49+
50+
// if mobile device and web can access same time send Platform in header
51+
const platform = req.get('Platform');
52+
53+
if (platform && platform === 'mobile') {
54+
tokens.mobile = token;
55+
} else {
56+
tokens.web = token;
57+
}
58+
await redis.setAccessToken({ userId: user.id, tokens });
4759

4860
return res.json({ token });
4961
} catch (e) {

packages/api/controllers/TestController.js

+3
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
async function root(req, res, next) {
44
try {
55
// throw new LogicError('test_error')
6+
// setTimeout(function() {
7+
// return res.json({ result: Date.now() });
8+
// }, 10 * 1000);
69
return res.json({ result: Date.now() });
710
} catch (e) {
811
// if you throw an error in the try block, it'll catch in this catch block
+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
const Joi = require('../utils/joi');
2+
const { User } = require('../models');
3+
4+
5+
/*
6+
*
7+
* USER OPERATIONS
8+
*
9+
*/
10+
async function userList(req, res, next) {
11+
try {
12+
const { page, pageSize, search, column, order } = await Joi.validate(req.query, User.filters);
13+
14+
const data = await User
15+
.query()
16+
.modify((queryBuilder) => {
17+
if (search) {
18+
const term = `%${search}%`;
19+
queryBuilder.where('firstname', 'like', term);
20+
queryBuilder.orWhere('lastname', 'like', term);
21+
}
22+
})
23+
.page(page, pageSize)
24+
.orderBy(column, order);
25+
26+
return res.json(data);
27+
} catch (e) {
28+
console.log(e);
29+
return next(e);
30+
}
31+
}
32+
33+
module.exports = {
34+
/* USER OPERATIONS */
35+
userList,
36+
};

packages/api/controllers/index.js

+2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
const ExampleController = require('./ExampleController');
22
const AuthController = require('./AuthController');
3+
const UserController = require('./UserController');
34
const TestController = require('./TestController');
45

56
module.exports = {
67
ExampleController,
78
AuthController,
9+
UserController,
810
TestController,
911
};

packages/api/middlewares/auth.js

+26-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
const jwt = require('jsonwebtoken');
2+
const redis = require('../redis');
3+
const { AuthorizationError, ForbiddenError } = require('../utils/errors');
24
const { constants } = require('../resources');
3-
const { AuthorizationError } = require('../utils/errors');
45
// const errorHandler = require('./errorHandler');
56

67
function isAuthenticated(req, res, next) {
@@ -14,18 +15,39 @@ function isAuthenticated(req, res, next) {
1415
if (err) return next(new AuthorizationError('token_not_valid'));
1516

1617
req.userId = decoded.userId;
18+
if (decoded.roles) req.roles = decoded.roles;
1719

18-
return next();
20+
// if mobile device and web can access same time send Platform in header
21+
const platform = req.get('Platform');
22+
return redis.getAccessToken(decoded.userId, (platform || 'web'))
23+
.then((redisToken) => {
24+
if (token === redisToken) return next();
25+
return next(new AuthorizationError('token_not_valid'));
26+
})
27+
.catch(() => next(new AuthorizationError('token_not_valid')));
1928
});
2029
}
2130
return next(new AuthorizationError('token_should_bearer'));
2231
}
2332

24-
function generateToken(userId) {
25-
return jwt.sign({ userId }, constants.AUTH_SECRET);
33+
async function isAuthorized(req, res, next) {
34+
if (!req.roles) return next(new ForbiddenError('user_not_authenticated'));
35+
const url = req.url.split('?').shift();
36+
const routeName = url.replace(/\/\d+$/gi, '').replace(/\/([a-z])/gi, '$1');
37+
const roles = await redis.getRoles(req.roles);
38+
// console.log(req.originalUrl, req.url, url, routeName, roles);
39+
if (roles.map(c => c.toLowerCase()).indexOf(`${routeName.toLowerCase()}:${constants.PERMISSION[req.method]}`) === -1) {
40+
return next(new ForbiddenError('user_not_authenticated'));
41+
}
42+
return next();
43+
}
44+
45+
function generateToken(userId, roles) {
46+
return jwt.sign({ userId, roles }, constants.AUTH_SECRET);
2647
}
2748

2849
module.exports = {
2950
isAuthenticated,
51+
isAuthorized,
3052
generateToken,
3153
};
+54-102
Original file line numberDiff line numberDiff line change
@@ -1,109 +1,61 @@
1+
const { ValidationError: ObjectionValidationError } = require('../utils/objection');
2+
const { UniqueViolationError, NotNullViolationError, ForeignKeyViolationError, DataError } = require('objection-db-errors');
13
const { i18n } = require('../resources');
24
const {
5+
LogicError,
6+
BadRequestError,
37
ValidationError,
4-
NotFoundError
5-
} = require('objection');
8+
ServerError,
9+
AuthorizationError,
10+
ForbiddenError,
11+
NotFoundError,
12+
TimeoutError } = require('../utils/errors');
613

7-
const {
8-
DBError,
9-
ConstraintViolationError,
10-
UniqueViolationError,
11-
NotNullViolationError,
12-
ForeignKeyViolationError,
13-
CheckViolationError,
14-
DataError
15-
} = require('objection-db-errors');
14+
function generateResponse(e) {
15+
switch (e.constructor) {
16+
case ObjectionValidationError:
17+
return new ValidationError('orm_validation', { ...e });
1618

17-
module.exports = function errorHandler(err, req, res, next) {
18-
// TODO: REFACTOR
19-
// If error thrown by us
20-
if (err.isJoi) {
21-
return res.status(400).send({
22-
message: err.message,
23-
type: 'ModelValidation',
24-
data: err.details
25-
});
26-
}
27-
return handler(err, res)
28-
};
19+
case UniqueViolationError:
20+
return new LogicError('unique_violation', { message: e.detail, field: e.columns[0], constraint: e.constraint });
21+
22+
case NotNullViolationError:
23+
return new ValidationError('field_null', { message: `${e.column} can't be null.`, field: e.column, constraint: e.constraint }); // TODO: i18n message
24+
25+
case ForeignKeyViolationError:
26+
return new LogicError('foreign_key_violation', { message: e.detail, constraint: e.constraint });
27+
28+
case DataError:
29+
return new BadRequestError('invalid_data', e);
30+
31+
case SyntaxError:
32+
return new BadRequestError('json_syntax');
2933

30-
function handler(err, res) {
31-
if (err instanceof ValidationError) {
32-
res.status(400).send({
33-
message: err.message,
34-
type: err.type,
35-
data: {}
36-
});
37-
} else if (err instanceof NotFoundError) {
38-
res.status(404).send({
39-
message: err.message,
40-
type: 'NotFound',
41-
data: {}
42-
});
43-
} else if (err instanceof UniqueViolationError) {
44-
res.status(409).send({
45-
message: err.message,
46-
type: 'UniqueViolation',
47-
data: {
48-
columns: err.columns,
49-
table: err.table,
50-
constraint: err.constraint
51-
}
52-
});
53-
} else if (err instanceof NotNullViolationError) {
54-
res.status(400).send({
55-
message: err.message,
56-
type: 'NotNullViolation',
57-
data: {
58-
column: err.column,
59-
table: err.table,
60-
}
61-
});
62-
} else if (err instanceof ForeignKeyViolationError) {
63-
res.status(409).send({
64-
message: err.message,
65-
type: 'ForeignKeyViolation',
66-
data: {
67-
table: err.table,
68-
constraint: err.constraint
69-
}
70-
});
71-
} else if (err instanceof CheckViolationError) {
72-
res.status(400).send({
73-
message: err.message,
74-
type: 'CheckViolation',
75-
data: {
76-
table: err.table,
77-
constraint: err.constraint
78-
}
79-
});
80-
} else if (err instanceof DataError) {
81-
res.status(400).send({
82-
message: err.message,
83-
type: 'InvalidData',
84-
data: {}
85-
});
86-
} else if (err instanceof DBError) {
87-
res.status(500).send({
88-
message: err.message,
89-
type: 'UnknownDatabaseError',
90-
data: {}
91-
});
92-
} else {
93-
if (err.type) {
94-
return res.status(err.status).send({
95-
error: {
96-
type: err.type,
97-
code: err.code,
98-
message: i18n.t(err.code),
99-
},
100-
});
101-
}
102-
console.log(err);// eslint-disable-line no-console
103-
res.status(500).send({
104-
message: err.message,
105-
type: 'UnknownError',
106-
data: {}
107-
});
34+
case NotFoundError:
35+
case BadRequestError:
36+
case LogicError:
37+
case ValidationError:
38+
case AuthorizationError:
39+
case ForbiddenError:
40+
case ServerError:
41+
return e;
42+
43+
case Error:
44+
if (e.isJoi) return new ValidationError('field_validation', { message: e.details[0].message, field: e.details[0].path });
45+
return new ServerError('error', { message: e.message });
46+
47+
default:
48+
if (e.code === 'ETIMEDOUT') return new TimeoutError('timeout_error', { ...e });
49+
return new ServerError('unhandled_error', { errorClass: e.constructor.name, ...e });
10850
}
109-
}
51+
}
52+
53+
54+
function errorHandler(err, req, res, next) {
55+
const { status, ...response } = generateResponse(err);
56+
return res
57+
.status(status)
58+
.json(response);
59+
}
60+
61+
module.exports = errorHandler;

packages/api/middlewares/index.js

+3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
const auth = require('./auth');
22
const errorHandler = require('./errorHandler');
3+
const routeChecker = require('./routeChecker');
4+
35

46
module.exports = {
57
auth,
68
errorHandler,
9+
routeChecker,
710
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
module.exports = function requestHandler(req, res, next) { // eslint-disable-line no-unused-vars
2+
if (req.body && typeof req.body === 'object') {
3+
// email must be lowercase
4+
if (req.body.email && typeof req.body.email === 'string') {
5+
req.body.email = req.body.email.toLowerCase();
6+
}
7+
}
8+
next();
9+
};
+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
const redis = require('../redis');
2+
const { NotFoundError } = require('../utils/errors');
3+
4+
async function isExist(req, res, next) {
5+
const url = `${req.originalUrl.split('?').shift()}/`;
6+
const routeList = await redis.getRoutes();
7+
var routes = routeList.filter((w) => w.path !== '*').filter((w) => {
8+
const path = `${w.path}/`;
9+
const routeMatcher = new RegExp(path.replace(/:[^\s/]+/g, '([\\d-]+)'));
10+
return url.match(routeMatcher) && w.methods.includes(req.method);
11+
});
12+
if(routes.length == 0 ) return next(new NotFoundError('request_method_not_found'));
13+
return next();
14+
}
15+
16+
17+
module.exports = {
18+
isExist,
19+
};

0 commit comments

Comments
 (0)