Skip to content

Commit 412877b

Browse files
committed
feat(): basic features added for users, auth and basic validations.
1 parent be3aa16 commit 412877b

File tree

12 files changed

+445
-1
lines changed

12 files changed

+445
-1
lines changed

README.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,10 @@
1-
# rest-api-tutorial
1+
# rest-api-tutorial
2+
3+
This project was created to help the toptal article called *Creating secure REST API Using Node.js*
4+
5+
## Usage
6+
7+
Make sure you have mongodb installed into your own machine and running;
8+
Get the project and run: `npm install`
9+
10+
Run `npm start`. It will initialize the server at port 3600.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
const jwtSecret = require('../../common/config/env.config.js').jwt_secret,
2+
jwt = require('jsonwebtoken');
3+
const crypto = require('crypto');
4+
const uuid = require('node-uuid');
5+
6+
exports.login = (req, res) => {
7+
try {
8+
let refreshId = req.body.userId + jwtSecret;
9+
let salt = crypto.randomBytes(16).toString('base64');
10+
let hash = crypto.createHmac('sha512', salt).update(refreshId).digest("base64");
11+
req.body.refreshKey = salt;
12+
let token = jwt.sign(req.body, jwtSecret);
13+
let b = new Buffer(hash);
14+
let refresh_token = b.toString('base64');
15+
res.status(201).send({accessToken: token, refreshToken: refresh_token});
16+
} catch (err) {
17+
res.status(500).send({errors: err});
18+
}
19+
};
20+
21+
exports.refresh_token = (req, res) => {
22+
try {
23+
req.body = req.jwt;
24+
let token = jwt.sign(req.body, jwtSecret);
25+
res.status(201).send({id: token});
26+
} catch (err) {
27+
res.status(500).send({errors: err});
28+
}
29+
};
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
const UserModel = require('../../users/models/users.model');
2+
const crypto = require('crypto');
3+
4+
exports.hasAuthValidFields = (req, res, next) => {
5+
let errors = [];
6+
7+
if (req.body) {
8+
if (!req.body.email) {
9+
errors.push('Missing email field');
10+
}
11+
if (!req.body.password) {
12+
errors.push('Missing password field');
13+
}
14+
15+
if (errors.length) {
16+
return res.status(400).send({errors: errors.join(',')});
17+
} else {
18+
return next();
19+
}
20+
} else {
21+
return res.status(400).send({errors: 'Missing email and password fields'});
22+
}
23+
};
24+
25+
exports.isPasswordAndUserMatch = (req, res, next) => {
26+
UserModel.findByEmail(req.body.email)
27+
.then((user)=>{
28+
if(!user[0]){
29+
res.status(404).send({});
30+
}else{
31+
let passwordFields = user[0].password.split('$');
32+
let salt = passwordFields[0];
33+
let hash = crypto.createHmac('sha512', salt).update(req.body.password).digest("base64");
34+
if (hash === passwordFields[1]) {
35+
req.body = {
36+
userId: user[0]._id,
37+
email: user[0].email,
38+
permissionLevel: user[0].permissionLevel,
39+
provider: 'email',
40+
name: user[0].firstName + ' ' + user[0].lastName,
41+
};
42+
return next();
43+
} else {
44+
return res.status(400).send({errors: ['Invalid e-mail or password']});
45+
}
46+
}
47+
});
48+
};

authorization/routes.config.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
const VerifyUserMiddleware = require('./middlewares/verify.user.middleware');
2+
const AuthorizationController = require('./controllers/authorization.controller');
3+
const AuthValidationMiddleware = require('../common/middlewares/auth.validation.middleware');
4+
exports.routesConfig = function (app) {
5+
6+
app.post('/auth', [
7+
VerifyUserMiddleware.hasAuthValidFields,
8+
VerifyUserMiddleware.isPasswordAndUserMatch,
9+
AuthorizationController.login
10+
]);
11+
12+
app.post('/auth/refresh', [
13+
AuthValidationMiddleware.validJWTNeeded,
14+
AuthValidationMiddleware.verifyRefreshBodyField,
15+
AuthValidationMiddleware.validRefreshNeeded,
16+
AuthorizationController.login
17+
]);
18+
};

common/config/env.config.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
module.exports = {
2+
"port": 3600,
3+
"appEndpoint": "http://localhost:3600",
4+
"apiEndpoint": "http;//localhost:3600",
5+
"jwt_secret": "myS33!!creeeT",
6+
"jwt_expiration_in_seconds": 36000,
7+
"environment": "dev",
8+
"permissionLevels": {
9+
"NORMAL_USER": 1,
10+
"PAID_USER": 4,
11+
"ADMIN": 2048
12+
}
13+
};
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
const jwt = require('jsonwebtoken'),
2+
secret = require('../config/env.config')['jwt_secret'];
3+
4+
const ADMIN_PERMISSION = 4096;
5+
6+
exports.minimumPermissionLevelRequired = (required_permission_level) => {
7+
return (req, res, next) => {
8+
let user_permission_level = parseInt(req.jwt.permissionLevel);
9+
let userId = req.jwt.userId;
10+
if (user_permission_level & required_permission_level) {
11+
return next();
12+
} else {
13+
return res.status(403).send();
14+
}
15+
};
16+
};
17+
18+
exports.onlySameUserOrAdminCanDoThisAction = (req, res, next) => {
19+
20+
let user_permission_level = parseInt(req.jwt.permissionLevel);
21+
let userId = req.jwt.userId;
22+
if (req.params && req.params.userId && userId === req.params.userId) {
23+
return next();
24+
} else {
25+
if (user_permission_level & ADMIN_PERMISSION) {
26+
return next();
27+
} else {
28+
return res.status(403).send();
29+
}
30+
}
31+
32+
};
33+
34+
exports.sameUserCantDoThisAction = (req, res, next) => {
35+
let userId = req.jwt.userId;
36+
37+
if (req.params.userId !== userId) {
38+
return next();
39+
} else {
40+
return res.status(400).send();
41+
}
42+
43+
};
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
const jwt = require('jsonwebtoken'),
2+
secret = require('../config/env.config.js').jwt_secret,
3+
crypto = require('crypto');
4+
5+
exports.verifyRefreshBodyField = (req, res, next) => {
6+
if (req.body && req.body.refresh_token) {
7+
return next();
8+
} else {
9+
return res.status(400).send({error: 'need to pass refresh_token field'});
10+
}
11+
};
12+
13+
exports.validRefreshNeeded = (req, res, next) => {
14+
let b = new Buffer(req.body.refresh_token, 'base64');
15+
let refresh_token = b.toString();
16+
let hash = crypto.createHmac('sha512', req.jwt.refresh_key).update(req.jwt.user_id + secret).digest("base64");
17+
if (hash === refresh_token) {
18+
req.body = req.jwt;
19+
return next();
20+
} else {
21+
return res.status(400).send({error: 'Invalid refresh token'});
22+
}
23+
};
24+
25+
26+
exports.validJWTNeeded = (req, res, next) => {
27+
if (req.headers['authorization']) {
28+
try {
29+
let authorization = req.headers['authorization'].split(' ');
30+
if (authorization[0] !== 'Bearer') {
31+
return res.status(401).send();
32+
} else {
33+
req.jwt = jwt.verify(authorization[1], secret);
34+
return next();
35+
}
36+
37+
} catch (err) {
38+
return res.status(403).send();
39+
}
40+
} else {
41+
return res.status(401).send();
42+
}
43+
};

index.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
const config = require('./common/config/env.config.js');
2+
3+
const express = require('express');
4+
const app = express();
5+
const bodyParser = require('body-parser');
6+
7+
const AuthorizationRouter = require('./authorization/routes.config');
8+
const UsersRouter = require('./users/routes.config');
9+
10+
app.use(function (req, res, next) {
11+
res.header('Access-Control-Allow-Origin', '*');
12+
res.header('Access-Control-Allow-Credentials', 'true');
13+
res.header('Access-Control-Allow-Methods', 'GET,HEAD,PUT,PATCH,POST,DEconstE');
14+
res.header('Access-Control-Expose-Headers', 'Content-Length');
15+
res.header('Access-Control-Allow-Headers', 'Accept, Authorization, Content-Type, X-Requested-With, Range');
16+
if (req.method === 'OPTIONS') {
17+
return res.send(200);
18+
} else {
19+
return next();
20+
}
21+
});
22+
23+
app.use(bodyParser.json());
24+
AuthorizationRouter.routesConfig(app);
25+
UsersRouter.routesConfig(app);
26+
27+
28+
app.listen(config.port, function () {
29+
console.log('app listening at port %s', config.port);
30+
});

package.json

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"name": "rest-api-tutorial",
3+
"version": "1.0.0",
4+
"description": "",
5+
"main": "index.js",
6+
"scripts": {
7+
"test": "echo \"Error: no test specified\" && exit 1",
8+
"start": "node index.js"
9+
},
10+
"repository": {
11+
"type": "git",
12+
"url": "git+https://github.com/makinhs/rest-api-tutorial.git"
13+
},
14+
"author": "",
15+
"license": "ISC",
16+
"bugs": {
17+
"url": "https://github.com/makinhs/rest-api-tutorial/issues"
18+
},
19+
"homepage": "https://github.com/makinhs/rest-api-tutorial#readme",
20+
"dependencies": {
21+
"body-parser": "1.7.0",
22+
"express": "^4.8.7",
23+
"jsonwebtoken": "^7.3.0",
24+
"moment": "^2.17.1",
25+
"moment-timezone": "^0.5.13",
26+
"mongoose": "^5.1.1",
27+
"node-uuid": "^1.4.8",
28+
"swagger-ui-express": "^2.0.13",
29+
"sync-request": "^4.0.2"
30+
}
31+
}

users/controllers/users.controller.js

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
const UserModel = require('../models/users.model');
2+
const crypto = require('crypto');
3+
4+
exports.insert = (req, res) => {
5+
let salt = crypto.randomBytes(16).toString('base64');
6+
let hash = crypto.createHmac('sha512', salt).update(req.body.password).digest("base64");
7+
req.body.password = salt + "$" + hash;
8+
req.body.permissionLevel = 1;
9+
UserModel.createUser(req.body)
10+
.then((result) => {
11+
res.status(201).send({id: result._id});
12+
});
13+
};
14+
15+
exports.list = (req, res) => {
16+
let limit = req.query.limit && req.query.limit <= 100 ? parseInt(req.query.limit) : 10;
17+
let page = 0;
18+
if (req.query) {
19+
if (req.query.page) {
20+
req.query.page = parseInt(req.query.page);
21+
page = Number.isInteger(req.query.page) ? req.query.page : 0;
22+
}
23+
}
24+
UserModel.list(limit, page)
25+
.then((result) => {
26+
res.status(200).send(result);
27+
})
28+
};
29+
30+
exports.getById = (req, res) => {
31+
UserModel.findById(req.params.userId)
32+
.then((result) => {
33+
res.status(200).send(result);
34+
});
35+
};
36+
exports.patchById = (req, res) => {
37+
if (req.body.password) {
38+
let salt = crypto.randomBytes(16).toString('base64');
39+
let hash = crypto.createHmac('sha512', salt).update(req.body.password).digest("base64");
40+
req.body.password = salt + "$" + hash;
41+
}
42+
43+
UserModel.patchUser(req.params.userId, req.body)
44+
.then((result) => {
45+
res.status(204).send({});
46+
});
47+
48+
};
49+
50+
exports.removeById = (req, res) => {
51+
UserModel.removeById(req.params.userId)
52+
.then((result)=>{
53+
res.status(204).send({});
54+
});
55+
};

0 commit comments

Comments
 (0)