Александр Завьялов, Евгений Мокеев
app/ └── controllers/ └── notes.js └── pages.js └── models/ └── note.js └── views/ └── index.js └── routes.js
→ GET / → GET /notes → POST /notes → GET /notes/:name
controllers/
└── user.js
exports.login = (req, res) => {
res.render('login');
};
routes.js
const user = require('./controllers/user');
module.exports = function (app) {
app.get('/login', user.login);
}
→ GET /login
views/
└── login.hbs
<form action="/login" method="post">
<div>
<label>Логин:</label>
<input type="text" name="username">
</div>
<div>
<label>Пароль:</label>
<input type="password" name="password">
</div>
<div>
<input type="submit" value="Войти">
</div>
</form>
models/
└── user.js
const users = [
{id: 1, username: 'admin', password: 'admin'}
];
class User {
static find(username, password) {}
static findById(id) {}
}
module.exports = User;
package.json
"dependencies": {
"express-session": "^1.13.0",
}
index.js
app.use(require('express-session')({}));
lib/ └── passport/ └── index.js
const User = require('../../models/user');
exports.authenticate = (req, res) => {};
exports.onlyAuth = (req, res) => {};
exports.authenticate = (req, res) => {
const user = User.find(
req.body.username,
req.body.password
);
if (!user) {
req.session.authFailMessage =
'Пользователь не найден';
res.redirect('/login');
return;
}
req.session.auth = { id: user.id };
res.redirect('/');
};
exports.onlyAuth = (req, res, next) => {
const auth = req.session.auth;
if (auth && auth.id) {
if (User.findById(auth.id)) {
return next();
}
}
res.sendStatus(401);
};
controllers/
└── user.js
exports.login = (req, res) => {
res.render('login');
};
exports.login = (req, res) => {
const data = {};
if (req.session.authFailMessage) {
data.error = req.session.authFailMessage;
}
res.render('login', data);
};
exports.profile = (req, res) => {
res.send('Страница профиля');
};
routes.js
app.post('/login', passport.authenticate);
→ POST /login
app.get('/profile', passport.onlyAuth, user.profile);
→ GET /profile
S - Single responsibility principle
O - Open/closed principle
L - Liskov substitution principle
I - Interface segregation principle
D - Dependency inversion principle
На каждую сущность должна быть возложена одна единственная ответственность.
routes.js
app.get(
'/profile',
passport.onlyAuth,
user.profile
);
Делит ответственность за обработку между несколькими обработчиками
lib/ └── passport └── index.js
exports.onlyAuth = (req, res, next) => {
const auth = req.session.auth;
if (auth && auth.id) {
if (User.findById(auth.id)) {
return next();
}
}
res.sendStatus(401);
};
lib/ └── passport └── index.js
exports.initUser = (req, res, next) => {
const auth = req.session.auth;
if (auth && auth.id) {
req.user = User.findById(auth.id);
}
next();
};
exports.onlyAuth = (req, res, next) => {
if (!req.user) {
res.sendStatus(401);
return;
}
next();
};
index.js
const passport = require('./lib/passport');
app.use(passport.initUser);
controllers/
└── user.js
exports.profile = (req, res) => {
res.send('Страница профиля');
};
exports.profile = (req, res) => {
res.send(`Привет, ${req.user.username}!`);
};
Сущности (классы, модули, функции) должны быть открыты к раширению, но закрыты от модификаций.
lib/ └── passport └── index.js
exports.authenticate = (req, res) => {
const user = User.find(
req.body.username,
req.body.password
);
if (!user) {
req.session.authFailMessage =
'Пользователь не найден';
res.redirect('/login');
return;
}
req.session.auth = { id: user.id };
res.redirect('/');
};
routes.js
app.post('/login', passport.authenticate);
app.post('/login', passport.authenticate({
successRedirect: '/',
failureRedirect: '/login',
failureMessage: 'Неправильный логин или пароль'
}));
lib/ └── passport └── index.js
exports.authenticate = (req, res) => {};
exports.authenticate = options => {
return (req, res) => {};
};
exports.authenticate = options => {
return (req, res) => {
const user = User.find(
req.body.username,
req.body.password
);
if (!user) {
req.session.authFailMessage =
options.failureMessage'Пользователь не найден';
res.redirect(options.failureRedirect'/login');
return;
}
req.session.auth = { id: user.id };
res.redirect(options.successRedirect'/');
}
};
Создает однотипные объекты
exports.authenticate = options => {
return (req, res) => {
const user = User.find(
req.body.username,
req.body.password
);
if (!user) {
req.session.authFailMessage =
options.failureMessage ;
res.redirect( options.failureRedirect );
return;
}
req.session.auth = { id: user.id };
res.redirect(options.successRedirect);
}
};
lib/ └── passport └── index.js
exports.initUser = (req, res, next) => {
const auth = req.session.auth;
if (auth && auth.id) {
req.user = User.findById(auth.id);
}
next();
};
// Сериализация пользователя
auth = { id: user.id }
// Десериализация пользователя
user = User.findById(auth.id)
lib/ └── passport └── index.js
let serialize = () => {};
let deserialize = () => {};
exports.authenticate = options => {
return (req, res) => {
const user = User.find(
req.body.username,
req.body.password
);
if (!user) {
req.session.authFailMessage =
options.failureMessage ;
res.redirect( options.failureRedirect );
return;
}
req.session.auth = { id: serialize(user)user.id }
res.redirect(options.successRedirect);
}
};
lib/ └── passport └── index.js
exports.initUser = (req, res, next) => {
const auth = req.session.auth;
if (auth && auth.id) {
req.user = deserialize(auth.id)User.findById(auth.id)
}
next();
};
lib/ └── passport └── index.js
exports.registerSerializer = fn => {
serialize = fn;
};
exports.registerDeserializer = fn => {
deserialize = fn;
};
models/
└── user.js
class User {
static getSerializator() {
return user => user.id;
}
static getDeserializator() {
return id => User.findById(id);
}
}
index.js
const User = require('./models/user');
const passport = require('./lib/passport/index');
passport.registerSerializer(User.getSerializator());
passport.registerDeserializer(User.getDeserializator());
lib/ └── passport/ └── index.js passport.js
const User = require('../models/user');
const passport = require('./passport/index');
passport.registerSerializer(User.getSerializator());
passport.registerDeserializer(User.getDeserializator());
module.exports = passport;
Расширение базовой функциональности без наследования
function Universe() {
// имеется экземпляр, созданный ранее?
if (typeof Universe.instance === 'object') {
return Universe.instance;
}
// создать новый экземпляр
this.bang = "Big";
// сохранить его
Universe.instance = this;
}
// проверка
const uni = new Universe();
const uni2 = new Universe();
uni === uni2; // true
Создание уникальных объектов, существующих только в одном экземпляре
Сущности, которые используют базовый тип, должны иметь возможность использовать подтипы базового типа не зная об этом.
Подкласс не должен требовать от вызывающего кода больше, чем базовый класс, и не должен предоставлять вызывающему коду меньше, чем базовый класс.
exports.authenticate = options => {
return (req, res) => {
const user = User.find(
req.body.username,
req.body.password
);
if (!user) {
req.session.authFailMessage =
options.failureMessage ;
res.redirect( options.failureRedirect );
return;
}
req.session.auth = { id: serialize(user) }
res.redirect(options.successRedirect);
}
};
lib/ └── passport/ └── index.js
const strategies = {};
exports.registerStrategy = (name, strategy) => {
strategies[name] = strategy;
};
exports.authenticate = options => {
return (req, res) => {
/* поиск пользователя */const user = User.find(
req.body.username,
req.body.password
);
if (!user) { /* проверка пользователя */
/* обработка ошибки */req.session.authFailMessage =
options.failureMessage;
res.redirect( options.failureRedirect );
return;
}
/* обработка успешного результата */req.session.auth = { id: serialize(user) }
res.redirect(options.successRedirect);
}
};
exports.authenticate = (name, options) => {
return (req, res) => {
const strategy = strategies[name];
/* проверка пользователя */
strategy.authenticate(req, (err, user) => {
if (err) {
/* обработка ошибки */
}
/* обработка успешного результата */
});
}
};
lib/ └── passport └── strategies └── strategy.js
class Strategy {
constructor(verify) {
this._verify = verify;
}
authenticate(req, done) {
const username = req.body.username;
const password = req.body.password;
this._verify(username, password, done);
}
}
module.exports = Strategy;
lib/
└── passport.js
const Strategy = require('./passport/strategies/strategy');
const verify = (username, password, done) => {
const user = User.find(username, password);
const err = user ? null : new Error('...');
done(err, user);
}
passport.registerStrategy('local', new Strategy(verify));
routes.js
app.post('/login', passport.authenticate('local', {
successRedirect: '/',
failureRedirect: '/login'
}));
Выбор алгоритма поведения независимо от клиентов, которые его используют
lib/ └── passport └── strategies └── strategy.js └── anotherStrategy.js
var Strategy = require('./strategy');
class AnotherStrategy extends Strategy {
constructor(verify) {}
authenticate(req, done) {}
}
lib/ └── passport └── strategies └── BaseStrategy.js
class BaseStrategy {
constructor(verify) {
this._verify = verify;
}
authenticate(req, done) {
done(new Error('Такого пользователя нет'));
}
}
module.exports = BaseStrategy;
lib/ └── passport └── strategies └── BaseStrategy.js └── LocalStrategy.js
var BaseStrategy = require('./BaseStrategy');
class LocalStrategy extends BaseStrategy {
authenticate(req, done) {
const username = req.body.username;
const password = req.body.password;
this._verify(username, password, done);
}
}
module.exports = LocalStrategy;
lib/ └── passport └── strategies └── BaseStrategy.js └── CookieStrategy.js
var BaseStrategy = require('./BaseStrategy');
class CookieStrategy extends BaseStrategy {
authenticate(req, done) {
userId = req.cookies.userId;
this._verify(userId, done);
}
}
module.exports = CookieStrategy;
lib/ └── passport └── strategies └── BaseStrategy.js └── BadStrategy.js
var BaseStrategy = require('./BaseStrategy');
class BadStrategy extends BaseStrategy {
authenticate(req, done) {
userId = req.cookies.userId;
if (userId === 9) {
throw new Error('...');
}
this._verify(userId, done);
}
}
Маленькие интерфесы лучше больших
models
└── user.js
class User {
static findByName(username) {}
static checkPassword(password){}static find(username, password) {}
}
lib
└── passport.js
passport.registerStrategy('local',
new Strategy((username, password, done) => {
const user = User.findByName(username);
if (!user) {
done(new Error('Пользователя не существует'));
return;
}
if (!user.checkPassword(password)) {
done(new Error('Неправильный пароль'));
return;
}
done(null, user);
})
);
Модули верхнего уровня не должны зависеть от модулей нижнего уровня. Оба должны зависеть от абстракции.
lib/
└── passport.js
// Инъекция через метод
passport.registerSerializer(...);
passport.registerDeserializer(...);
passport.registerStrategy(...);
// Инъекция через конструктор
passport.registerStrategy('local', new LocalStartegy(...));
// Инъекция через свойство
passport.strategy = ...;