Skip to content

Commit c2b400f

Browse files
authored
feat(users): Add user roles handling (#41)
Closes: #36 Added a field `roles` in users and handle it in /users routes. See updates to the documentation for full explanation.
1 parent 384c7f1 commit c2b400f

8 files changed

Lines changed: 101 additions & 16 deletions

File tree

cli/index.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,13 @@ mongoose.connect(`mongodb://${userPart}${mongodb.host}:${mongodb.port}/${mongodb
1313
lastName: 'admin',
1414
email: 'admin@link-value.fr',
1515
fallbackEmail: 'admin@link-value.fr',
16+
roles: [
17+
'tech',
18+
'business',
19+
'hr',
20+
'staff',
21+
'board',
22+
],
1623
});
1724

1825
return user.hashPassword('admin');

docs/endpoint-users.md

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,16 @@ Retrieve the whole collection of users.
1616
"lastName": String,
1717
"email": String,
1818
"fallbackEmail": String,
19-
"createdAt": Date
19+
"createdAt": Date,
20+
"roles": [String]
2021
}
2122
]
2223
```
2324

2425
## `POST /users`
2526

26-
Creates a new user.
27+
Creates a new user. Requires to specify at least one valid role in: tech, hr, staff, business and board.
28+
Also requires to have either hr or staff in logged user roles to perform request.
2729

2830
#### Request payload
2931

@@ -34,6 +36,7 @@ Creates a new user.
3436
"email": String, // required
3537
"fallbackEmail": String,
3638
"plainPassword": String // required
39+
"roles": [String] // required
3740
}
3841
```
3942

@@ -46,7 +49,8 @@ Creates a new user.
4649
"lastName": String,
4750
"email": String,
4851
"fallbackEmail": String,
49-
"createdAt": Date
52+
"createdAt": Date,
53+
"roles": [String]
5054
}
5155
```
5256

@@ -63,13 +67,15 @@ Retrieve an user.
6367
"lastName": String,
6468
"email": String,
6569
"fallbackEmail": String,
66-
"createdAt": Date
70+
"createdAt": Date,
71+
"roles": [String]
6772
}
6873
```
6974

7075
## `PUT /users/{id}`
7176

72-
Updates an user.
77+
Updates an user. Connected user can edit himself.
78+
To edit user roles, connected user requires either hr or staff in his roles.
7379

7480
#### Request payload
7581

@@ -78,14 +84,23 @@ Updates an user.
7884
"firstName": String,
7985
"lastName": String,
8086
"email": String,
81-
"fallbackEmail": String
87+
"fallbackEmail": String,
88+
"roles": [String] // requires hr/staff role
8289
}
8390
```
8491

8592
## `DELETE /users/{id}`
8693

8794
Deletes an user.
95+
To delete a user, connected user requires either hr or staff in his roles.
96+
97+
#### Request payload
8898

99+
```js
100+
{
101+
"deleted": Boolean,
102+
}
103+
```
89104

90105
## `GET /users/me`
91106

@@ -100,6 +115,7 @@ Retrieve the user corresponding to given access token.
100115
"lastName": String,
101116
"email": String,
102117
"fallbackEmail": String,
103-
"createdAt": Date
118+
"createdAt": Date,
119+
"roles": [String]
104120
}
105121
```

server/users/middlewares/index.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
const Boom = require('boom');
2+
3+
const rightsError = Boom.forbidden('insufficient_rights');
4+
5+
// Role checking middleware
6+
function hasRoleInList(...roles) {
7+
return {
8+
method(request, reply) {
9+
const hasGivenRole = roles.some(role => request.auth.credentials.roles.some(r => r === role));
10+
return reply(hasGivenRole ? undefined : rightsError);
11+
},
12+
assign: 'hasRights',
13+
};
14+
}
15+
16+
// Check connected user is requested user
17+
const isConnectedUser = {
18+
method(request, reply) {
19+
const isSelf = request.params.user === request.auth.credentials._id.toString();
20+
return reply(isSelf || rightsError);
21+
},
22+
assign: 'isConnectedUser',
23+
failAction: 'ignore',
24+
};
25+
26+
module.exports = {
27+
rightsError,
28+
hasRoleInList,
29+
isConnectedUser,
30+
};

server/users/models/user.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const userSchema = new mongoose.Schema({
77
email: { type: String, index: true, unique: true },
88
fallbackEmail: String,
99
password: String,
10+
roles: [String],
1011
thirdParty: Object,
1112
createdAt: { type: Date, default: Date.now },
1213
});

server/users/routes/delete-users.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
const { hasRoleInList } = require('../middlewares');
12
const { params } = require('./user-validation');
23

34
module.exports = {
45
method: 'DELETE',
56
path: '/users/{user}',
67
config: {
8+
pre: [hasRoleInList('rh', 'staff')],
79
validate: {
810
params,
911
},

server/users/routes/post-users.js

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1+
const Boom = require('boom');
2+
const { hasRoleInList } = require('../middlewares');
13
const { payload } = require('./user-validation');
24

35
module.exports = {
46
method: 'POST',
57
path: '/users',
68
config: {
9+
pre: [hasRoleInList('rh', 'staff')],
710
validate: {
811
payload: payload.post,
912
},
@@ -19,7 +22,7 @@ module.exports = {
1922
fallbackEmail: req.payload.fallbackEmail,
2023
});
2124

22-
res.mongodb(user
25+
const userPromise = user
2326
.hashPassword(req.payload.plainPassword)
2427
.then(() => user.save())
2528
.then((savedUser) => {
@@ -29,6 +32,14 @@ module.exports = {
2932
plainPassword: req.payload.plainPassword,
3033
}).save();
3134
return savedUser;
32-
}), ['password']);
35+
})
36+
.catch((err) => {
37+
if (err.message.startsWith('E11000')) {
38+
return Promise.reject(Boom.badRequest('email_already_used'));
39+
}
40+
return Promise.reject(Boom.wrap(err));
41+
});
42+
43+
res.mongodb(userPromise, ['password']);
3344
},
3445
};

server/users/routes/put-users.js

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
const Boom = require('boom');
2+
const { hasRoleInList, isConnectedUser, rightsError } = require('../middlewares');
23
const { payload, params } = require('./user-validation');
34

45
module.exports = {
56
method: 'PUT',
67
path: '/users/{user}',
78
config: {
9+
pre: [isConnectedUser, hasRoleInList('rh', 'staff')],
810
validate: {
911
payload: payload.put,
1012
params,
@@ -13,23 +15,29 @@ module.exports = {
1315
handler(req, res) {
1416
const { User } = req.server.plugins.users.models;
1517

18+
// User can't edit his roles if doesn't have rights.
19+
if (req.pre.isOwner && !req.pre.hasRights && req.payload.roles) {
20+
return res(rightsError);
21+
}
22+
1623
const userPromise = User
1724
.findOne({ _id: req.params.user })
1825
.exec()
1926
.then((user) => {
2027
if (!user) {
21-
return Boom.notFound('User Not Found');
28+
return Promise.reject(Boom.notFound('User Not Found'));
2229
}
2330

2431
return Object
2532
.assign(user, {
26-
firstName: req.payload.firstName,
27-
lastName: req.payload.lastName,
28-
fallbackEmail: req.payload.fallbackEmail,
33+
firstName: req.payload.firstName || user.firstName,
34+
lastName: req.payload.lastName || user.lastName,
35+
fallbackEmail: req.payload.fallbackEmail || user.fallbackEmail,
36+
roles: req.payload.roles || user.roles,
2937
})
3038
.save();
3139
});
3240

33-
res.mongodb(userPromise, ['password']);
41+
return res.mongodb(userPromise, ['password']);
3442
},
3543
};

server/users/routes/user-validation.js

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,28 @@
11
const Joi = require('joi');
22
const { Types } = require('mongoose');
33

4+
const validRoles = [
5+
'tech',
6+
'business',
7+
'hr',
8+
'staff',
9+
'board',
10+
];
11+
412
exports.payload = {
513
post: Joi.object({
614
firstName: Joi.string().min(2).required(),
715
lastName: Joi.string().min(2).required(),
816
plainPassword: Joi.string().min(6).required(),
917
email: Joi.string().email().required(),
1018
fallbackEmail: Joi.string().email(),
19+
roles: Joi.array().items(Joi.string().valid(validRoles)).min(1).required(),
1120
}),
1221
put: Joi.object({
13-
firstName: Joi.string().min(2).required(),
14-
lastName: Joi.string().min(2).required(),
22+
firstName: Joi.string().min(2),
23+
lastName: Joi.string().min(2),
1524
fallbackEmail: Joi.string().email(),
25+
roles: Joi.array().items(Joi.string().valid(validRoles)).min(1),
1626
}),
1727
};
1828

0 commit comments

Comments
 (0)