Skip to content

Commit

Permalink
Merge 70d275e into de697b9
Browse files Browse the repository at this point in the history
  • Loading branch information
CEOehis committed Sep 20, 2018
2 parents de697b9 + 70d275e commit 2796516
Show file tree
Hide file tree
Showing 14 changed files with 247 additions and 14 deletions.
1 change: 1 addition & 0 deletions .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ PROD_DB_DIALECT=postgres

USER_PASSWORD=
ADMIN_PASSWORD=
SUPER_ADMIN_PASSWORD=

JWT_KEY=

Expand Down
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@
"method-override": "^2.3.10",
"methods": "^1.1.2",
"morgan": "^1.9.0",
"nodemon": "^1.18.4",
"passport": "^0.4.0",
"passport-facebook": "^2.1.1",
"passport-google-oauth": "^1.0.0",
Expand Down
22 changes: 19 additions & 3 deletions server/controllers/users.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import UserValidation from '../utils/validation';
import trimInput from '../utils/trim_input';
import TokenHelper from '../utils/TokenHelper';
import { User } from '../models';
import { isAdmin } from '../utils/verifyRoles';

sendgrid.setApiKey(process.env.SENDGRID_API_KEY);
env.config();
Expand Down Expand Up @@ -201,17 +202,31 @@ class UsersController {
*/
static updateUserProfile(req, res, next) {
const { userId } = req.params;
const { userRole } = req;
const {
firstName, lastName, username, bio, twitter, linkedin, facebook, image
} = req.body;

const { error, isValid } = UserValidation.validateProfileInput(req.body);

// assign role field if user in session is an admin
const { role } = isAdmin(userRole) ? req.body : '';

if (role === 'admin') {
const err = new Error('only Super Admin can update role to admin');
err.status = 401;
return next(err);
}

isValidNumber(req, res);

// check if user in current session is same with user being updated
// prevent user from updating another users' profile
if (Number(userId) !== Number(req.userId)) {
/*
* prevent user from updating another users ' profile
* check if user in current session is same with user being updated
* An admin can update a users ' profile as well, so check if user
* accessing this route is an admin
*/
if (!isAdmin(userRole) && (Number(userId) !== Number(req.userId))) {
const err = new Error('you are not allowed to update another user\'s profile');
err.status = 401;
return next(err);
Expand Down Expand Up @@ -249,6 +264,7 @@ class UsersController {
lastName: trimInput(lastName) || user.lastName,
username: trimInput(username) || user.username,
bio: trimInput(bio) || user.bio,
role: role || user.role,
twitter: twitter || user.twitter,
linkedin: linkedin || user.linkedin,
facebook: facebook || user.facebook,
Expand Down
2 changes: 1 addition & 1 deletion server/docs/swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -1830,7 +1830,7 @@
}
}
}
},
},
"securityDefinitions": {
"Bearer": {
"description": "For accessing the API a valid JWT token must be passed in all the queries in\nthe 'Authorization' header.\n\n\nA valid JWT token is generated by the API and returned as answer of a call\nto the route /api/users/login giving a valid user & password.\n\n\nThe following syntax must be used in the 'Authorization' header :\n\n Bearer xxxxxx.yyyyyyy.zzzzzz\n",
Expand Down
25 changes: 24 additions & 1 deletion server/middleware/auth.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import TokenHelper from '../utils/TokenHelper';
import { isAdmin, isAuthor } from '../utils/verifyRoles';
import { isAdmin, isAuthor, isSuperAdmin } from '../utils/verifyRoles';

/**
*
Expand Down Expand Up @@ -91,4 +91,27 @@ export default class auth {
}
return next();
}

/**
* authorizes a user whose role is set to "superAdmin"
*
* @static
* @param {*} req
* @param {*} res
* @param {*} next
* @returns {next} next middleware
* @memberof auth
*/
static authorizeSuperAdmin(req, res, next) {
// before code got here user is clearly authed with token
// obtain userRole as previously set in req object
const { userRole } = req;

if (!isSuperAdmin(userRole)) {
const error = new Error('you are not a Super Admin');
error.status = 401;
return next(error);
}
return next();
}
}
19 changes: 19 additions & 0 deletions server/migrations/20180916093338-user-add-super-admin-role.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
module.exports = {
up: queryInterface => queryInterface.sequelize
.query(`
ALTER TYPE "enum_Users_role" ADD VALUE 'superAdmin';
`),

down: queryInterface => queryInterface.sequelize
.query(`
CREATE TYPE "enum_Users_role_new"
AS ENUM('admin', 'user', 'author');
ALTER TABLE "Users" ALTER COLUMN "role" DROP DEFAULT;
DELETE FROM "Users" WHERE "role" = 'superAdmin';
ALTER TABLE "Users"
ALTER COLUMN "role" TYPE "enum_Users_role_new"
USING ("role"::text::"enum_Users_role_new");
DROP TYPE "enum_Users_role";
ALTER TYPE "enum_Users_role_new" RENAME TO "enum_Users_role";
`),
};
2 changes: 1 addition & 1 deletion server/models/User.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ module.exports = (sequelize, DataTypes) => {
type: DataTypes.STRING,
},
role: {
type: DataTypes.ENUM('admin', 'user', 'author'),
type: DataTypes.ENUM('admin', 'user', 'author', 'superAdmin'),
allowNull: false,
defaultValue: 'user',
},
Expand Down
2 changes: 2 additions & 0 deletions server/routes/api/admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ import auth from '../../middleware/auth';
import authorRequestsController from '../../controllers/authorRequests';
import HandleReports from '../../controllers/adminHandleReports';

// get authenticateUser method
const { authenticateUser, authorizeAdmin } = auth;
const adminRoutes = Router();


adminRoutes.get('/articles/reports', authenticateUser, authorizeAdmin, HandleReports.getReportedArticles);
adminRoutes.get('/articles/reports/:reportId', authenticateUser, authorizeAdmin, HandleReports.getAReportedArticle);
adminRoutes.get('/reports/articles/:article_slug', authenticateUser, authorizeAdmin, HandleReports.getReportsForAnArticle);
Expand Down
10 changes: 10 additions & 0 deletions server/seeders/20180904121828-user.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@ require('dotenv').config();
module.exports = {
up: queryInterface => queryInterface.bulkInsert('Users',
[
{
firstName: 'Sudo',
lastName: 'Admin',
username: 'superadmin',
email: 'superadmin@mail.com',
role: 'superAdmin',
hash: bcrypt.hashSync(process.env.SUPER_ADMIN_PASSWORD, 10),
createdAt: new Date(),
updatedAt: new Date(),
},
{
firstName: 'author1',
lastName: 'Author',
Expand Down
57 changes: 52 additions & 5 deletions server/tests/controllers/authorRequests.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,64 @@ import chai, { expect } from 'chai';
import chaiHttp from 'chai-http';
import app from '../../../index';
import TokenHelper from '../../utils/TokenHelper';
import db from '../../models';

chai.use(chaiHttp);

describe('author Request', () => {
const tokens = {};

tokens.admin = TokenHelper.generateToken({ id: 5, username: 'Admin', role: 'admin' });
tokens.author = TokenHelper.generateToken({ id: 2, username: 'randomAuthor2', role: 'author' });
tokens.userOne = TokenHelper.generateToken({ id: 3, username: 'randomUser', role: 'user' });
tokens.userTwo = TokenHelper.generateToken({ id: 4, username: 'romeo', role: 'user' });
tokens.userThree = TokenHelper.generateToken({ id: 6, username: 'andelan', role: 'user' });
before((done) => {
db.User.bulkCreate([
{
firstName: 'Michael',
lastName: 'Angelo',
username: 'admin',
email: 'mangelo@mail.com',
role: 'admin',
hash: 'password'
},
{
firstName: 'Femi',
lastName: 'Kuti',
username: 'author',
email: 'fkuti@mail.com',
role: 'author',
hash: 'password'
},
{
firstName: 'Fela',
lastName: 'Kuti',
username: 'userOne',
email: 'fekuti@mail.com',
hash: 'password'
},
{
firstName: 'Debby',
lastName: 'Grace',
username: 'userTwo',
email: 'dgrace@mail.com',
hash: 'password'
},
{
firstName: 'Bola',
lastName: 'Salami',
username: 'userThree',
email: 'bsalami@mail.com',
hash: 'password'
},
])
.then(() => db.User.findAll({
order: [['id', 'DESC']],
limit: 5
}))
.then((users) => {
users.forEach((user) => {
tokens[user.username] = TokenHelper.generateToken(user);
});
done();
});
});

describe('Authenticated Users', () => {
describe('Make a request', () => {
Expand Down
70 changes: 70 additions & 0 deletions server/tests/controllers/users_profile.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,76 @@ describe('Users Controllers', () => {
});

describe('updateUserProfile', () => {
it('an admin can update any users profile', (done) => {
const adminToken = `Bearer ${TokenHelper.generateToken({ id: 10, role: 'admin' })}`;
chai.request(app)
.put('/api/users/1')
.set('Authorization', adminToken)
.send({
firstName: 'Samuel',
linkedin: 'www.linkedin.com',
bio: 'A very good and prolific Author'
})
.end((error, res) => {
const { dataValues } = res.body;
expect(res).to.have.status(200);
expect(res.body).to.be.an('object');
dataValues.should.have.property('lastName');
dataValues.should.have.property('image');
done();
});
});
it('an admin can update a user role', (done) => {
const adminToken = `Bearer ${TokenHelper.generateToken({ id: 10, role: 'admin' })}`;
chai.request(app)
.put('/api/users/1')
.set('Authorization', adminToken)
.send({
role: 'author'
})
.end((error, res) => {
const { dataValues } = res.body;
expect(res).to.have.status(200);
expect(res.body).to.be.an('object');
dataValues.should.have.property('lastName');
dataValues.should.have.property('image');
dataValues.role.should.equal('author');
done();
});
});
it('only a super admin can assign "admin" role', (done) => {
const adminToken = `Bearer ${TokenHelper.generateToken({ id: 10, role: 'admin' })}`;
chai.request(app)
.put('/api/users/1')
.set('Authorization', adminToken)
.send({
firstName: 'Samuel',
linkedin: 'www.linkedin.com',
bio: 'A very good and prolific Author',
role: 'admin'
})
.end((error, res) => {
expect(res).to.have.status(401);
expect(res.body).to.be.an('object');
expect(res.body.error.message).to.equal('only Super Admin can update role to admin');
done();
});
});
it('should return an error if wrong role is supplied', (done) => {
const superAdminToken = `Bearer ${TokenHelper.generateToken({ id: 1, role: 'superAdmin' })}`;
chai.request(app)
.put('/api/users/1')
.set('Authorization', superAdminToken)
.send({
role: 'non existing role'
})
.end((error, res) => {
expect(res).to.have.status(400);
expect(res.body).to.be.an('object');
expect(res.body.error.role).to.equal('role type is not valid');
done();
});
});
it('should prevent user from updating other users profiles', (done) => {
const unauthorizedUserToken = `Bearer ${TokenHelper.generateToken({ id: 10 })}`;
chai.request(app)
Expand Down
40 changes: 39 additions & 1 deletion server/tests/middleware/auth.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@ import { spy } from 'sinon';
import auth from '../../middleware/auth';
import TokenHelper from '../../utils/TokenHelper';

const { authenticateUser, authorizeAdmin, authorizeAuthor } = auth;
const {
authenticateUser,
authorizeAdmin,
authorizeAuthor,
authorizeSuperAdmin
} = auth;

describe('auth middleware', () => {
it('should exist', () => {
Expand Down Expand Up @@ -143,4 +148,37 @@ describe('auth middleware', () => {
expect(args[0]).to.equal(undefined);
});
});

describe('authorizeSuperAdmin', () => {
const req = {
headers: {}
};
const res = {};

it('should always call next()', () => {
const nextSpy = spy();
authorizeSuperAdmin(req, res, nextSpy);
expect(nextSpy.called).to.equal(true);
});

it('should call next with an error if user is not a super admin', () => {
req.userRole = 'user';
const nextSpy = spy();
authorizeSuperAdmin(req, res, nextSpy);
expect(nextSpy.called).to.equal(true);

const { args } = nextSpy.getCalls()[0];
expect(nextSpy.called).to.equal(true);
expect(args[0].message).to.equal('you are not a Super Admin');
});

it('should call next middleware if user is a super admin', () => {
req.userRole = 'superAdmin';
const nextSpy = spy();
authorizeSuperAdmin(req, res, nextSpy);
const { args } = nextSpy.getCalls()[0];
expect(nextSpy.called).to.equal(true);
expect(args[0]).to.equal(undefined);
});
});
});
7 changes: 7 additions & 0 deletions server/utils/validation.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ class UserValidation {
static validateProfileInput(data) {
const error = {};

const { role } = data;
data.firstName = !isEmpty(data.firstName) ? data.firstName : '';
data.lastName = !isEmpty(data.lastName) ? data.lastName : '';
data.username = !isEmpty(data.username) ? data.username : '';
Expand Down Expand Up @@ -115,6 +116,12 @@ class UserValidation {
}
}

if (!isEmpty(role)) {
if (role !== 'user' && role !== 'author' && role !== 'admin') {
error.role = 'role type is not valid';
}
}


return {
error,
Expand Down
3 changes: 2 additions & 1 deletion server/utils/verifyRoles.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export const isAdmin = data => data === 'admin';
export const isAdmin = data => data === 'admin' || data === 'superAdmin';
export const isAuthor = data => data === 'author';
export const isSuperAdmin = data => data === 'superAdmin';

0 comments on commit 2796516

Please sign in to comment.