Лекция 10
{
"name": "sample",
"version": "0.1.0",
"dependencies": {
"bluebird": "^3.5.1",
"body-parser": "^1.18.2",
"cookie-parser": "^1.4.3",
"express": "^4.16.2",
"moment": "^2.19.3",
"mysql2": "^1.5.1",
"sequelize": "^4.23.4"
}
}
const Sequelize = require('sequelize');
const config = require('./config.json');
const db = require('./context')(Sequelize, config);
const server = require('./server')(db, config);
...
...
(async function () {
await db.sequelize.sync();
await db.roles.findOrCreate({
where: { name: 'adminstrator' }
});
await db.roles.findOrCreate({
where: { name: 'user' }
});
server.listen(3000, () => console.log('Running'));
})();
{
"db": {
"host": "127.0.0.1",
"name": "my_db",
"user": "myuser",
"password": "123#qwe"
},
"cookie": {
"key": "secret key",
"auth": "__auth_id"
}
}
const express = require('express');
const cookieParser = require('cookie-parser');
const bodyParser = require('body-parser');
const errors = require('./helpers/errors');
const PostsService = require('./services/posts');
const UsersService = require('./services/users');
const RolesService = require('./services/roles');
const CacheService = require('./services/cache');
const AuthenticationService
= require('./services/authentication');
const AuthorizationService
= require('./services/authorization');
module.exports = (db, config) => {
...
};
const app = express();
// Services
const postsService = new PostsService(
db.posts,
errors
);
const usersService = new UsersService(
db.users,
db.roles,
errors
);
const rolesService = new RolesService(
db.roles,
errors
);
const authenticationService = new AuthenticationService(
db.users,
db.roles,
errors
);
const authorizationService = new AuthorizationService(
db.roles,
errors
);
const cacheService = new CacheService();
// Controllers
const logger
= require('./global-controllers/logger');
const authenticator
= require('./global-controllers/authenticator')(
usersService,
config
);
const authorizator
= require('./global-controllers/authorizator')
(authorizationService);
const cache
= require('./global-controllers/cache')
(cacheService);
const error
= require('./global-controllers/error');
const apiController = require('./controllers/api')(
postsService,
usersService,
rolesService,
authenticationService,
cacheService,
config,
);
// Mounting
app.use(express.static('public'));
app.use(cookieParser(config.cookie.key));
app.use(bodyParser.json());
app.use('/api', logger);
app.use('/api', authenticator);
app.use('/api', authorizator);
app.use('/api', cache);
app.use('/api', apiController);
app.use('/api', error);
return app;
// TODO: user hash algorithm e.g. bcrypt
module.exports = {
get: plain => {
return plain;
},
isValid: (plain, hash) => {
return plain === hash;
},
};
module.exports =
fn =>
(req, res, next) =>
fn(req, res, next).catch(next);
const express = require('express');
express.response.error = function(error) {
if (!error.code) {
error = {
message: error.toString(),
code: 'server_error',
status: 500
};
}
this.status(error.status).json(error);
};
...
...
module.exports = {
invalidId: {
message: 'Invalid id',
code: 'invalid_id', status: 400 },
notFound: {
message: 'Entity not found',
code: 'entity_not_found', status: 404 },
wrongCredentials: {
message: 'Email or password are wrong',
code: 'wrong_credentials', status: 404 },
accessDenied: {
message: 'Access denied',
code: 'access_denied', status: 403 }
};
module.exports = (Sequelize, sequelize) => {
return sequelize.define('posts', {
id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true
},
title: { type: Sequelize.STRING },
content: Sequelize.STRING,
date: Sequelize.DATEONLY,
rating: Sequelize.INTEGER,
draft: Sequelize.BOOLEAN
};
module.exports = (Sequelize, sequelize) => {
return sequelize.define('users', {
id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true
},
email: Sequelize.STRING,
password: Sequelize.STRING,
firstname: Sequelize.STRING,
lastname: Sequelize.STRING
});
};
module.exports = (Sequelize, sequelize) => {
return sequelize.define('roles', {
id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true
},
name: Sequelize.STRING
});
};
module.exports = (Sequelize, config) => {
const options = {
host: config.db.host,
dialect: 'mysql',
logging: false,
define: {
timestamps: true,
paranoid: true,
defaultScope: {
where: { deletedAt: { $eq: null } }
}
}
};
...
};
module.exports = (Sequelize, config) => {
...
const sequelize = new Sequelize(config.db.name,
config.db.user, config.db.password, options);
const User = require('../models/user')
(Sequelize, sequelize);
const Role = require('../models/role')
(Sequelize, sequelize);
const Post = require('../models/post')
(Sequelize, sequelize);
...
};
module.exports = (Sequelize, config) => {
...
// User <-> Role
User.belongsToMany(Role,
{ through: 'userRoles' });
Role.belongsToMany(User,
{ through: 'userRoles' });
// Post -> User
Post.belongsTo(User);
User.hasMany(Post);
...
};
module.exports = (Sequelize, config) => {
...
return {
users: User,
roles: Role,
posts: Post,
sequelize,
Sequelize,
};
};
class CrudService {
constructor(repository, errors) { ... }
async readChunk(options) { ... }
async read(id) { ... }
async create(data) { ... }
async update(id, data) { ... }
async delete(id) { ... }
}
module.exports = CrudService;
this.repository = repository;
this.errors = errors;
this.defaults = {
readChunk: {
limit: 10,
page: 1,
order: 'asc',
orderField: 'id'
}
};
options = Object.assign(
{}, this.defaults.readChunk, options
);
let limit = options.limit;
let offset = (options.page - 1) * options.limit;
return await this.repository.findAll({
limit: limit,
offset: offset,
order: [[
options.orderField,
options.order.toUpperCase()
]],
raw: true
});
id = parseInt(id);
if (isNaN(id)) {
throw this.errors.invalidId;
}
const item = await this.repository.findById(
id,
{ raw: true }
);
if (!item) {
throw this.errors.notFound;
}
return item;
const item = await this.repository.create(data);
return item.get({ plain: true });
await this.repository.update(data, {
where: { id: id },
limit: 1
});
return this.read(id);
return this.repository.destroy({
where: { id: id }
});
const CrudService = require('./crud');
class PostsService extends CrudService {
async create(data) { ... }
async update(data) { ... }
async upvote(id) { ... }
async downvote(id) { ... }
}
module.exports = PostsService;
let post = {
title: data.title,
content: data.content,
date: data.date,
draft: data.draft,
userId: data.userId,
rating: 0
};
return super.create(post);
let post = {
title: data.title,
content: data.content,
date: data.date,
draft: data.draft
};
return super.update(data.id, post);
const post = await this.repository.findById(id);
if (!post) {
throw this.errors.notFound;
}
return post.increment({ rating: 1 });
const post = await this.repository.findById(id);
if (!post) {
throw this.errors.notFound;
}
return post.increment({ rating: -1 });
const Promise = require('bluebird');
const CrudService = require('./crud');
const hash = require('../helpers/hash');
class UsersService extends CrudService {
constructor(usersRepository, rolesRepository,
errors) { ... }
async update(data) { ... }
async grant(userId, roleId) { ... }
async revoke(userId, roleId) { ... }
}
module.exports = UsersService;
let user = {
password: data.password
&& hash.get(data.password),
firstname: data.firstname,
lastname: data.lastname
};
return super.update(data.id, user);
const [ user, role ] = await Promise.all([
this.repository.findById(userId),
this.rolesRepository.findById(roleId),
]);
if (!user || !role) {
throw this.errors.invalidId;
}
await user.addRole(role);
const [ user, role ] = await Promise.all([
this.repository.findById(userId),
this.rolesRepository.findById(roleId),
]);
if (!user || !role) {
throw this.errors.invalidId;
}
await user.removeRole(role);
const CrudService = require('./crud');
class RolesService extends CrudService {
async create(data) { ... }
}
module.exports = RolesService;
let role = {
name: data.name
};
return super.create(role);
const Promise = require('bluebird');
const hash = require('../helpers/hash');
class AuthenticationService {
constructor(usersRepository, rolesRepository,
errors) { ... }
async login(data) { ... }
async register(data) { ... }
}
module.exports = AuthenticationService;
this.usersRepository = usersRepository;
this.rolesRepository = rolesRepository;
this.errors = errors;
const user = await this.usersRepository.findOne({
where: { email: data.email },
attributes: ['id', 'password']
});
if (!user
|| !hash.isValid(data.password, user.password)) {
throw this.errors.wrongCredentials;
}
return user;
const user = this.usersRepository.build({
email: data.email,
password: hash.get(data.password),
firstname: data.firstname,
lastname: data.lastname
});
const [, role] = await Promise.all([
user.save(),
this.rolesRepository.findOne({
where: { name: 'user' }
})
]);
await user.addRole(role);
return user;
const permissions = {
'/posts/create': 'user',
'/posts/update': 'user',
'/posts/delete': 'user',
'/users': 'administrator',
'/users/update': 'administrator',
'/users/delete': 'administrator',
'/users/grant': 'administrator',
'/users/revoke': 'administrator',
'/roles': 'administrator',
'/roles/create': 'administrator',
'/roles/delete': 'administrator'
};
class AuthorizationService {
constructor(rolesRepository, errors) { ... }
async checkPermissions(user, route) { ... }
}
module.exports = AuthorizationService;
this.rolesRepository = rolesRepository;
this.errors = errors;
if (!permissions[route]) return;
if(!user) throw this.errors.accessDenied;
const role = await this.rolesRepository.findOne({
where: { name: permissions[route] }
});
const hasRole = await user.hasRole(role);
if (!hasRole) {
throw this.errors.accessDenied;
}
// TODO: implement cache, in memory or external
class CacheService {
async set(req, data) {}
async get(req) {}
async invalidate(req) {}
}
module.exports = CacheService;
const moment = require('moment');
module.exports = (req, res, next) => {
res.locals.trace = { ... };
console.log(moment().format('HH:mm:ss'));
console.log(`${req.method} ${req.path}`);
console.log(JSON.stringify(req.query));
console.log(JSON.stringify(req.body));
console.log();
next();
};
const wrap = require('../helpers/wrap');
module.exports = (usersService, config) =>
wrap(async (req, res, next) => {
let userId = req.signedCookies[
config.cookie.auth
];
if (userId) {
req.user = await usersService.get(userId);
}
next();
});
const wrap = require('../helpers/wrap');
module.exports = (authorizationService) =>
wrap(async (req, res, next) => {
await authorizationService
.checkPermissions(req.user, req.path);
next();
});
module.exports = (cacheService) => async (req, res, next) => {
const cached = await cacheService.get(req);
if (cached) {
res.json(cached);
return;
}
next();
};
module.exports = (error, req, res, next) => {
// TODO: log error + 'res.locals.trace'
res.error(error);
};
const express = require('express');
module.exports = (
postsService,
usersService,
rolesService,
authenticationService,
cacheService,
config,
) => {
...
};
const router = express.Router();
const postsController = require('./posts')(
postsService,
cacheService
);
const usersController = require('./users')(
usersService
);
const rolesController = require('./roles')(
rolesService
);
const authController = require('./auth')(
authenticationService,
config
);
router.use('/posts', postsController);
router.use('/users', usersController);
router.use('/roles', rolesController);
router.use('/auth', authController);
return router;
const express = require('express');
const wrap = require('../helpers/wrap');
class CrudController {
constructor(service) {...}
async readAll(req, res) {...}
async read(req, res) {...}
async create(req, res) {...}
async update(req, res) {...}
async delete(req, res) {...}
registerRoutes() {...}
}
module.exports = CrudController;
this.service = service;
this.readAll = this.readAll.bind(this);
this.read = this.read.bind(this);
this.create = this.create.bind(this);
this.update = this.update.bind(this);
this.delete = this.delete.bind(this);
this.router = express.Router();
this.routes = {
'/': [{ method: 'get', cb: this.readAll }],
'/:id': [{ method: 'get', cb: this.read }],
'/create': [{ method: 'post', cb: this.create }],
'/update': [{ method: 'post', cb: this.update }],
'/delete': [{ method: 'post', cb: this.delete }]
};
res.json(
await this.service.readChunk(req.params)
);
res.json(
await this.service.read(req.params.id)
);
res.json(
await this.service.create(req.body)
);
res.json(
await this.service.update(req.body)
);
res.json(
await this.service.delete(req.body.id)
);
Object.keys(this.routes).forEach(route => {
let handlers = this.routes[route];
if (!handlers || !Array.isArray(handlers)) {
return;
}
for (let handler of handlers) {
this.router[handler.method](
route,
wrap(handler.cb)
);
}
});
const CrudController = require('./crud');
class PostsController extends CrudController {
constructor(postsService, cahceService) {...}
async readAll(req, res) {...}
async upvote(req, res) {...}
async downvote(req, res) {...}
}
module.exports = (postsService, cacheService) => {
const controller = new PostsController(
postsService,
cacheService
);
return controller.router;
};
super(postsService);
this.cacheService = cahceService;
this.readAll = this.readAll.bind(this);
this.upvote = this.upvote.bind(this);
this.downvote = this.downvote.bind(this);
this.routes['/'] = [{ method: 'get',
cb: this.readAll }];
this.routes['/upvote'] = [{ method: 'post',
cb: this.upvote }];
this.routes['/downvote'] = [{ method: 'post',
cb: this.downvote }];
this.registerRoutes();
const posts = await this.service.readChunk(
req.params
);
this.cacheService.set(req, posts);
res.json(posts);
await this.service.upvote(req.body.id);
res.json({ success: true });
await this.service.downvote(req.body.id);
res.json({ success: true });
const CrudController = require('./crud');
class UsersController extends CrudController {
constructor(usersService) {...}
async grant(req, res) {...}
async revoke(req, res) {...}
}
module.exports = (usersService) => {
const controller = new UsersController(
usersService
);
return controller.router;
};
super(usersService);
this.grant = this.grant.bind(this);
this.revoke = this.revoke.bind(this);
this.routes['/create'] = undefined;
this.routes['/grant'] = [{ method: 'post',
cb: this.grant
}];
this.routes['/revoke'] = [{ method: 'post',
cb: this.revoke
}];
this.registerRoutes();
await this.service.grant(
req.body.userId,
req.body.roleId
);
res.json({ success: true });
await this.service.revoke(
req.body.userId,
req.body.roleId
);
res.json({ success: true });
const CrudController = require('./crud');
class RolesController extends CrudController {
constructor(rolesService) {...}
}
module.exports = (rolesService) => {
const controller = new RolesController(
rolesService
);
return controller.router;
};
super(rolesService);
this.routes['/update'] = undefined;
this.registerRoutes();
const CrudController = require('./crud');
class AuthController extends CrudController {
constructor(authenticationService, config) {...}
async login(req, res) {...}
async register(req, res) {...}
async logout(req, res) {...}
}
module.exports = (authenticationService, config) => {
const controller = new AuthController(
authenticationService,
config
);
return controller.router;
};
super(authenticationService);
this.config = config;
this.login = this.login.bind(this);
this.register = this.register.bind(this);
this.logout = this.logout.bind(this);
this.routes = { ... };
this.registerRoutes();
this.routes = {
'/login': [{
method: 'post',
cb: this.login
}],
'/register': [{
method: 'post',
cb: this.register
}],
'/logut': [{
method: 'post',
cb: this.logout
}],
};
const user = await this.service.login(req.body);
res.cookie(
this.config.cookie.auth,
user.id,
{ signed: true }
);
res.json({ success: true });
const user = await this.service.register(req.body);
res.cookie(
this.config.cookie.auth,
user.id,
{ signed: true }
);
res.json({ success: true });
res.cookie(this.config.cookie.auth, '');
res.json({ success: true });