Usuários e autenticação

Antes de entrarmos na autenticação precisamos implementar a parte de usuários da nossa aplicação. Vamos começar criando tests, controller, model e a configuração das rotas para os usuários.

Neste capítulo não vou mostrar o ciclo de TDD passo-a-passo, pois já focamos nisso anteriormente, se surgir alguma dúvida lembre-se que é o mesmo processo que seguimos ao criar a parte de products.

Começaremos pelo controller, pelos testes unitários, crie um arquivo em test/unit/controllers/users_spec.js com o seguinte código:

  1 import UsersController from '../../../src/controllers/users';
  2 import sinon from 'sinon';
  3 import User from '../../../src/models/user';
  4 
  5 describe('Controller: Users', () => {
  6   const defaultUser = [
  7     {
  8       __v: 0,
  9       _id: '56cb91bdc3464f14678934ca',
 10       name: 'Default User',
 11       email: 'user@mail.com',
 12       password: 'password',
 13       role: 'user'
 14     }
 15   ];
 16 
 17   const defaultRequest = {
 18     params: {}
 19   };
 20 
 21   describe('get() users', () => {
 22     it('should return a list of users', async () => {
 23       const response = {
 24         send: sinon.spy()
 25       };
 26       User.find = sinon.stub();
 27 
 28       User.find.withArgs({}).resolves(defaultUser);
 29 
 30       const usersController = new UsersController(User);
 31 
 32       await usersController.get(defaultRequest, response);
 33       sinon.assert.calledWith(response.send, defaultUser);
 34     });
 35 
 36     it('should return 400 when an error occurs', async () => {
 37       const request = {};
 38       const response = {
 39         send: sinon.spy(),
 40         status: sinon.stub()
 41       };
 42 
 43       response.status.withArgs(400).returns(response);
 44       User.find = sinon.stub();
 45       User.find.withArgs({}).rejects({ message: 'Error' });
 46 
 47       const usersController = new UsersController(User);
 48 
 49       await usersController.get(request, response);
 50       sinon.assert.calledWith(response.send, 'Error');
 51     });
 52   });
 53 
 54   describe('getById()', () => {
 55     it('should call send with one user', async () => {
 56       const fakeId = 'a-fake-id';
 57       const request = {
 58         params: {
 59           id: fakeId
 60         }
 61       };
 62       const response = {
 63         send: sinon.spy()
 64       };
 65 
 66       User.find = sinon.stub();
 67       User.find.withArgs({ _id: fakeId }).resolves(defaultUser);
 68 
 69       const usersController = new UsersController(User);
 70 
 71       await usersController.getById(request, response);
 72       sinon.assert.calledWith(response.send, defaultUser);
 73     });
 74   });
 75 
 76   describe('create() user', () => {
 77     it('should call send with a new user', async () => {
 78       const requestWithBody = Object.assign(
 79         {},
 80         { body: defaultUser[0] },
 81         defaultRequest
 82       );
 83       const response = {
 84         send: sinon.spy(),
 85         status: sinon.stub()
 86       };
 87       class fakeUser {
 88         save() {}
 89       }
 90 
 91       response.status.withArgs(201).returns(response);
 92       sinon
 93         .stub(fakeUser.prototype, 'save')
 94         .withArgs()
 95         .resolves();
 96 
 97       const usersController = new UsersController(fakeUser);
 98 
 99       await usersController.create(requestWithBody, response);
100       sinon.assert.calledWith(response.send);
101     });
102 
103     context('when an error occurs', () => {
104       it('should return 422', async () => {
105         const response = {
106           send: sinon.spy(),
107           status: sinon.stub()
108         };
109 
110         class fakeUser {
111           save() {}
112         }
113 
114         response.status.withArgs(422).returns(response);
115         sinon
116           .stub(fakeUser.prototype, 'save')
117           .withArgs()
118           .rejects({ message: 'Error' });
119 
120         const usersController = new UsersController(fakeUser);
121 
122         await usersController.create(defaultRequest, response);
123         sinon.assert.calledWith(response.status, 422);
124       });
125     });
126   });
127 
128   describe('update() user', () => {
129     it('should respond with 200 when the user has been updated', async () => {
130       const fakeId = 'a-fake-id';
131       const updatedUser = {
132         _id: fakeId,
133         name: 'Updated User',
134         email: 'user@mail.com',
135         password: 'password',
136         role: 'user'
137       };
138       const request = {
139         params: {
140           id: fakeId
141         },
142         body: updatedUser
143       };
144       const response = {
145         sendStatus: sinon.spy()
146       };
147       class fakeUser {
148         static findById() {}
149         save() {}
150       }
151       const fakeUserInstance = new fakeUser();
152 
153       const saveSpy = sinon.spy(fakeUser.prototype, 'save');
154       const findByIdStub = sinon.stub(fakeUser, 'findById');
155       findByIdStub.withArgs(fakeId).resolves(fakeUserInstance);
156 
157       const usersController = new UsersController(fakeUser);
158 
159       await usersController.update(request, response);
160       sinon.assert.calledWith(response.sendStatus, 200);
161       sinon.assert.calledOnce(saveSpy);
162     });
163 
164     context('when an error occurs', () => {
165       it('should return 422', async () => {
166         const fakeId = 'a-fake-id';
167         const updatedUser = {
168           _id: fakeId,
169           name: 'Updated User',
170           email: 'user@mail.com',
171           password: 'password',
172           role: 'user'
173         };
174         const request = {
175           params: {
176             id: fakeId
177           },
178           body: updatedUser
179         };
180         const response = {
181           send: sinon.spy(),
182           status: sinon.stub()
183         };
184 
185         class fakeUser {
186           static findById() {}
187         }
188 
189         const findByIdStub = sinon.stub(fakeUser, 'findById');
190         findByIdStub.withArgs(fakeId).rejects({ message: 'Error' });
191         response.status.withArgs(422).returns(response);
192 
193         const usersController = new UsersController(fakeUser);
194 
195         await usersController.update(request, response);
196         sinon.assert.calledWith(response.send, 'Error');
197       });
198     });
199   });
200 
201   describe('delete() user', () => {
202     it('should respond with 204 when the user has been deleted', async () => {
203       const fakeId = 'a-fake-id';
204       const request = {
205         params: {
206           id: fakeId
207         }
208       };
209       const response = {
210         sendStatus: sinon.spy()
211       };
212 
213       class fakeUser {
214         static remove() {}
215       }
216 
217       const removeStub = sinon.stub(fakeUser, 'remove');
218 
219       removeStub.withArgs({ _id: fakeId }).resolves([1]);
220 
221       const usersController = new UsersController(fakeUser);
222 
223       await usersController.remove(request, response);
224       sinon.assert.calledWith(response.sendStatus, 204);
225     });
226 
227     context('when an error occurs', () => {
228       it('should return 400', async () => {
229         const fakeId = 'a-fake-id';
230         const request = {
231           params: {
232             id: fakeId
233           }
234         };
235         const response = {
236           send: sinon.spy(),
237           status: sinon.stub()
238         };
239 
240         class fakeUser {
241           static remove() {}
242         }
243 
244         const removeStub = sinon.stub(fakeUser, 'remove');
245 
246         removeStub.withArgs({ _id: fakeId }).rejects({ message: 'Error' });
247         response.status.withArgs(400).returns(response);
248 
249         const usersController = new UsersController(fakeUser);
250 
251         await usersController.remove(request, response);
252         sinon.assert.calledWith(response.send, 'Error');
253       });
254     });
255   });
256 });

O próximo passo é criar o controller para usuários em src/controllers/users.js, o arquivo deve ter o seguinte conteúdo:

 1 class UsersController {
 2   constructor(User) {
 3     this.User = User;
 4   }
 5 
 6   async get(req, res) {
 7     try {
 8       const users = await this.User.find({});
 9       res.send(users);
10     } catch (err) {
11       res.status(400).send(err.message);
12     }
13   }
14 
15   async getById(req, res) {
16     const {
17       params: { id }
18     } = req;
19 
20     try {
21       const user = await this.User.find({ _id: id });
22       res.send(user);
23     } catch (err) {
24       res.status(400).send(err.message);
25     }
26   }
27 
28   async create(req, res) {
29     const user = new this.User(req.body);
30 
31     try {
32       await user.save();
33       res.status(201).send(user);
34     } catch (err) {
35       res.status(422).send(err.message);
36     }
37   }
38 
39   async update(req, res) {
40     const body = req.body;
41     try {
42       const user = await this.User.findById(req.params.id);
43 
44       user.name = body.name;
45       user.email = body.email;
46       user.role = body.role;
47       if (body.password) {
48         user.password = body.password;
49       }
50       await user.save();
51 
52       res.sendStatus(200);
53     } catch (err) {
54       res.status(422).send(err.message);
55     }
56   }
57 
58   async remove(req, res) {
59     try {
60       await this.User.deleteOne({ _id: req.params.id });
61       res.sendStatus(204);
62     } catch (err) {
63       res.status(400).send(err.message);
64     }
65   }
66 }
67 
68 export default UsersController;

No método update do UsersController temos um cenário diferente do controller de products, vamos entender melhor o porquê em seguida. No momento, basta entender que vamos buscar o usuário do banco de dados e atualizar as propriedades. Caso o campo password esteja setado, ele também será atualizado. Este método update é referente ao método PUT do http. No PUT é esperado que seja enviado todos os campos que aparecem na requisição quando se faz um GET, por exemplo, se fizermos um get users/id a resposta vai conter name, email, role mas não deve conter o password por motivos de segurança. Sendo assim, o password só será recebido no update quando a intenção for alterar a senha, pois o campo não é obrigatório.

Depois de criar o controller é a hora de criarmos o Model em src/models/user.js com o seguinte trecho de código:

 1 import mongoose from 'mongoose';
 2 
 3 const schema = new mongoose.Schema({
 4   name: String,
 5   email: String,
 6   password: String,
 7   role: String
 8 });
 9 
10 schema.set('toJSON', {
11   transform: (doc, ret, options) => ({
12     _id: ret._id,
13     email: ret.email,
14     name: ret.name,
15     role: ret.role
16   })
17 });
18 
19 const User = mongoose.model('User', schema);
20 
21 export default User;

No Model, além de criar o schema também sobrescrevemos o método toJSON que é responsável por transformar os dados que vem do MongoDB para o formato json; vamos utilizar a função transform, que é nativa do Mongoose, para remover o campo password do objeto final, pois não devemos expor a senha do usuário mesmo como hash. Sempre que o Mongoose faz uma busca no Mongo os dados vem em BSON o formato nativo do MongoDB, similar ao JSON só que binário. Depois de receber os dados o Mongoose faz o processo de serialização onde transforma o BSON que veio do banco em JSON para ser utilizado na aplicação, nesse momento é possível intervir nessa serialização e customizar o resultado final, exatamente o que implementamos no toJSON.

Com o Model pronto, vamos agora criar a rota em src/routes/users.js:

 1 import express from 'express';
 2 import UsersController from '../controllers/users';
 3 import User from '../models/user';
 4 
 5 const router = express.Router();
 6 const usersController = new UsersController(User);
 7 router.get('/', (req, res) => usersController.get(req, res));
 8 router.get('/:id', (req, res) => usersController.getById(req, res));
 9 router.post('/', (req, res) => usersController.create(req, res));
10 router.put('/:id', (req, res) => usersController.update(req, res));
11 router.delete('/:id', (req, res) => usersController.remove(req, res));
12 
13 export default router;

Agora precisamos atualizar o index das rotas em src/routes/index.js, para carregar a rota de usuários:

 1 import express from 'express';
 2 import productsRoute from './products';
 3 +import usersRoute from './users';
 4  
 5  const router = express.Router();
 6  
 7  router.use('/products', productsRoute);
 8 +router.use('/users', usersRoute);
 9  router.get('/', (req, res) => res.send('Hello World!'));
10  
11  export default router;

A próxima etapa será adicionar o arquivo de testes, mas, antes disso, vamos atualizar uma configuração nos nossos testes para que seja possível reutilizar a configuração do Supertest.

Vamos começar tornando global a rotina before que atualmente está disponível apenas para products. Vamos remover o trecho de código referente ao before do arquivo test/integration/routes/products_spec.js:

 1 import Product from '../../../src/models/product';
 2  
 3  describe('Routes: Products', () => {
 4 -  let request;
 5 -  let app;
 6 -
 7 -  before(async () => {
 8 -    app = await setupApp();
 9 -    request = supertest(app);
10 -  });
11 -
12 - after(async () => await app.database.connection.close());
13 - 
14    const defaultId = '56cb91bdc3464f14678934ca';
15    const defaultProduct = {
16      name: 'Default product',

E vamos inserir esse mesmo bloco nas definições globais, em um novo arquivo que vamos chamar de test/integration/global.js:

1 before(async () => {
2   const app = await setupApp();
3   global.app = app;
4   global.request = supertest(app);
5 });
6 
7 after(async () => await app.database.connection.close());

A última etapa da nossa refatoração é atualizar o arquivo test/integration/mocha.opts adicionando a chamada para o arquivo global.js que acabamos de criar:

1 --require @babel/register
2 - --require test/integration/helpers.js
3 + --require test/integration/helpers.js test/integration/global.js
4 --reporter spec
5 --slow 5000
6 + --timeout 5000

Dessa maneira o Mocha vai carregar esse arquivo global e executar o método before sempre antes de qualquer outro callback. Assim, o Supertest vai ser inicializado antes de todos os testes de integração. Também aumentamos o timeout para 5000ms, para evitar que algum teste de integração um pouco mais lento possa quebrar o nosso teste.

Agora basta criar os testes de integração para o modulo de users em test/integration/routes/users_spec.js, o código será o seguinte:

  1 import User from '../../../src/models/user';
  2 
  3 describe('Routes: Users', () => {
  4   const defaultId = '56cb91bdc3464f14678934ca';
  5   const defaultAdmin = {
  6     name: 'Jhon Doe',
  7     email: 'jhon@mail.com',
  8     password: '123password',
  9     role: 'admin'
 10   };
 11   const expectedAdminUser = {
 12     _id: defaultId,
 13     name: 'Jhon Doe',
 14     email: 'jhon@mail.com',
 15     role: 'admin'
 16   };
 17 
 18   beforeEach(async() => {
 19     const user = new User(defaultAdmin);
 20     user._id = '56cb91bdc3464f14678934ca';
 21     await User.deleteMany({});
 22     await user.save();
 23   });
 24 
 25   afterEach(async() => await User.deleteMany({}));
 26 
 27   describe('GET /users', () => {
 28     it('should return a list of users', done => {
 29 
 30       request
 31         .get('/users')
 32         .end((err, res) => {
 33           expect(res.body).to.eql([expectedAdminUser]);
 34           done(err);
 35         });
 36     });
 37 
 38     context('when an id is specified', done => {
 39       it('should return 200 with one user', done => {
 40 
 41         request
 42           .get(`/users/${defaultId}`)
 43           .end((err, res) => {
 44             expect(res.statusCode).to.eql(200);
 45             expect(res.body).to.eql([expectedAdminUser]);
 46             done(err);
 47           });
 48       });
 49     });
 50   });
 51 
 52   describe('POST /users', () => {
 53     context('when posting an user', () => {
 54 
 55       it('should return a new user with status code 201', done => {
 56         const customId = '56cb91bdc3464f14678934ba';
 57         const newUser = Object.assign({},{ _id: customId, __v:0 }, defaultAdmin);
 58         const expectedSavedUser = {
 59           _id: customId,
 60           name: 'Jhon Doe',
 61           email: 'jhon@mail.com',
 62           role: 'admin'
 63         };
 64 
 65         request
 66           .post('/users')
 67           .send(newUser)
 68           .end((err, res) => {
 69             expect(res.statusCode).to.eql(201);
 70             expect(res.body).to.eql(expectedSavedUser);
 71             done(err);
 72           });
 73       });
 74     });
 75   });
 76 
 77   describe('PUT /users/:id', () => {
 78     context('when editing an user', () => {
 79       it('should update the user and return 200 as status code', done => {
 80         const customUser = {
 81           name: 'Din Doe',
 82         };
 83         const updatedUser = Object.assign({}, defaultAdmin, customUser)
 84 
 85         request
 86           .put(`/users/${defaultId}`)
 87           .send(updatedUser)
 88           .end((err, res) => {
 89             expect(res.status).to.eql(200);
 90             done(err);
 91           });
 92       });
 93     });
 94   });
 95 
 96   describe('DELETE /users/:id', () => {
 97     context('when deleting an user', () => {
 98       it('should delete an user and return 204 as status code', done => {
 99 
100         request
101           .delete(`/users/${defaultId}`)
102           .end((err, res) => {
103             expect(res.status).to.eql(204);
104             done(err);
105           });
106       });
107     });
108   });
109 
110 });

Executando os testes:

1 $ npm test

Todos os testes devem estar passando, inclusive os testes de usuários.

Encriptando senhas com Bcrypt

Antes de começarmos a trabalhar na autenticação efetivamente, é necessário fazer mais uma melhoria na API. Note que adicionamos usuários, que possuem senhas, e estamos salvando as senhas diretamente no banco como texto.

Senhas em texto plano

Salvar senhas como texto é a maneira mais simples guardar essa informação, e também a mais insegura, pois, se um hacker tiver acesso ao servidor terá acesso às senhas dos usuários. Como as pessoas costumam utilizar a mesma senha para diferentes fins essa falha na nossa aplicação pode comprometer a segurança dos usuários.

Senhas com hashing de mão única

Hashing de mão única é uma prática de encriptação onde se encripta uma mensagem utilizando um algoritmo que não permite a desencriptação, bem mais seguro do que o texto plano. Porém, se o hacker descobrir o algoritmo utilizado pode fazer uso do mesmo para gerar senhas infinitamente, mais cedo ou mais tarde ele vai encontrar a certa, esse ataque é chamado de brute-force.

Exemplo: SHA256("minhasenha") = "79809644A830EF92424A66227252B87BBDFB633A9DAB18BA450C1B8D35665F20"

Senhas com hashing e salt

Hashing oferece mais segurança do que o texto plano, mas ataques como vimos acima podem acontecer. Uma solução para esse problema é utilizar um salt. Um salt nada mais é do que uma string concatenada a uma mensagem (a senha no nosso caso). Dessa maneira, havendo uma string única para a aplicação é possível gerar um hash que é muito difícil de ser quebrado por brute-force.

Exemplo: SHA256("minhasenha" + "meusalt") = "697FDEADE02B2F4C86A5696D1DF998ADA97A6B1420F5BA0C7B4EE2024DBECD1F"

Note que o hash gerado é diferente do exemplo anterior, para alguém gerar um hash igual a esse utilizando brute-force será necessário saber o salt. Ainda temos uma falha de segurança nesse cenário: Se alguém hackear o servidor e descobrir o salt conseguirá gerar hashs com brute-force que serão iguais aos gerados pela aplicação. Pode parecer muito difícil hackear o servidor, descobrir o salt, descobrir o algoritmo e quebrá-lo com brute-force, mas não é. Se você tem um produto aberto deve se preocupar muito com isso, hackers tentam esse tipo de coisa 24h por dia.

Criando senhas seguras com Bcrypt

Bcrypt é um algoritmo de hashing baseado em Blowfish e com algumas características únicas, como a “key factor” que se refere a habilidade de aumentar a quantidade necessária de processamento para criptografar a informação. Aumentar a complexidade de processamento impossibilita a quebra de hashing por ataques como o brute-force por exemplo, pois o tempo necessário para gerar um hash similar é muito grande. O Bcrypt utiliza ainda um salt que é concatenado com o texto (nesse caso, a senha) para aumentar ainda mais a segurança e aleatoriedade do hash final gerado. Uma boa dica é utilizar um salt aleatório para cada senha gerada, isso garante que, mesmo que existam senhas iguais, elas não terão o mesmo hash final. Mas aí vem a pergunta: se vamos gerar hash aleatórios, como é possível verificar a senha do usuário no momento de fazer login? Mágica! Se passarmos a senha em texto plano (que o usuário vai fornecer na hora do login) e o hash gerado quando o usuário foi salvo no banco, o hash gerado pelo algoritmo será igual ao salvo no banco de dados. Vamos ver como isso funciona na prática, começando com a instalação do Bcrypt:

1 $ npm install bcrypt@^3.0.7

Após a instalação do Bcrypt vamos atualizar o Model de user adicionando o seguinte:

 1 import mongoose from 'mongoose';
 2 +import Util from 'util';
 3 +import bcrypt from 'bcrypt';
 4 
 5 +const hashAsync = Util.promisify(bcrypt.hash);
 6 const schema = new mongoose.Schema({
 7   name: String,
 8   email: String,
 9   password: String,
10   role: String
11 });
12 
13 +schema.pre('save', async function(next) {
14 +  if (!this.password || !this.isModified('password')) {
15 +    return next();
16 +  }
17 +  try {
18 +    const hashedPassword = await hashAsync(this.password, 10);
19 +    this.password = hashedPassword;
20 +  } catch (error) {
21 +    next(err);
22 +  }
23 +});

Muita coisa acontece nesse bloco de código, vamos começar pelo Bcrypt. O módulo nativo do Bcrypt não suporta promises, ou seja, teríamos que usar callbacks, mas como em todo nosso código utilizamos promises vamos seguir o padrão. Atualmente é simples transformar uma função que utiliza callback para se comportar como uma Promise, basta utilizar o módulo nativo util do Node.js e chamar o método promisify passando a referência da função que utiliza callback, como fizemos com bcrypt.hash, o retorno será uma função que utiliza Promise.

Middlewares no Mongoose

Para garantir que sempre que um usuário for salvo a senha dele será encriptada vamos utilizar uma funcionalidade do Mongoose chamada middlewares (também conhecido como pre e post hooks). No trecho de código anterior utilizamos o pre save, ou seja, esse código será automaticamente executado sempre antes da função save do Model. Começamos verificando se o campo password foi realmente alterado:

1   if(!this.password || !this.isModified('password')) {
2     return next();
3   };

Se o campo password não foi alterado não podemos gerar um hash novo, se não estaríamos gerando um hash de um hash e o usuário não conseguiria mais utilizar a senha. Caso o campo password tenha sido alterado, o trecho de código acima vai gerar um hash para a nova senha do usuário e substituir a antiga no Model, na sequência o Model vai salvar o hash ao invés da senha em texto plano que o usuário enviou. A função hashAsync é a função do Bcrypt que transformamos em Promise, ela será responsável por criar um hash a partir da senha que o usuário enviou.

Além da senha em texto, também passamos número 10 para o Brcrypt, esse número se refere ao factor; o factor é utilizado para dizer ao Bcrypt o número de complexidade que desejamos para gerar o hash, quanto maior for o número mais tempo ele vai levar para gerar o hash e mais difícil será para desencriptar. Na sequência substituimos o password que o usuário enviou pelo hash:

1       this.password = hashedPassword;

O this nesse contexto se refere ao Model do Mongoose, como estamos utilizando um middleware no momento que chamamos o next ele vai chamar a próxima ação da cadeia de middlewares, que provavelmente será a ação de save, caso não exista outro middleware, então o usuário será salvo no banco com o password como hash.

Agora que temos toda a lógica necessária para criar senhas com segurança vamos voltar ao método update do UsersController para entendê-lo melhor:

 1   async update(req, res) {
 2     const body = req.body;
 3     try {
 4       const user = await this.User.findById(req.params.id);
 5 
 6       user.name = body.name;
 7       user.email = body.email;
 8       user.role = body.role;
 9       if (body.password) {
10         user.password = body.password;
11       }
12       await user.save();
13 
14       res.sendStatus(200);
15     } catch (err) {
16       res.status(422).send(err.message);
17     }
18   }

Não vamos alterar nada no código, apenas destaquei o método update novamente para explicar o seu comportamento. Provavelmente alguns de vocês leitores já trabalharam com MongoDB e sabem que ele possui um método de update e nesse caso estamos buscando o usuário do banco, atualizando os dados e chamando o método save. Isso acontece por que o update nativo do MongoDB é baixo nível, ou seja, ele não pertence ao Mongoose, nós não podemos utilizar o middleware schema.pre('update') por exemplo para ter acesso aos dados anteriores e aos novos que estão sendo salvos.

Esse comportamento tem uma razão, o MongoDB como explicado anteriormente, é um banco de dados NoSQL, ou seja, ele não garante integridade dos dados. Vamos analisar um exemplo do método update do Mongoose:

1   async update(req, res) {
2     try {
3       await this.Product.updateOne({ _id: req.params.id }, req.body);
4       res.sendStatus(200);
5     } catch (err) {
6       res.status(422).send(err.message);
7     }
8   }

Esse é o ProductsController, como não utilizamos nenhum middleware do Mongoose nesse update podemos utilizar o método update nativo do MongoDB. O que acontece internamente no findOneAndUpdate é o seguinte:

1   db.products.update(
2   { _id: "example-id" },
3     {
4       name: "Updated Name",
5     }
6 )

O Mongoose não busca o produto para atualizar e salvar novamente, ele apenas traduz o updateOne em uma query nativa de update do MongoDB, sendo assim mesmo que seja adicionado um middleware no pre update não teremos acesso aos dados anteriores para comparar se a senha do usuário mudou ou não e não saberemos se é necessário gerar outro hash.

Antes de pensar que esse comportamento do Mongoose é “burro” vamos lembrar das premissas do MongoDB enquanto NoSQL é não prover Atomicidade e Consistência, isso significa que se o Moongose buscar o produto para a memória, atualizar os campos que mudaram e salvar novamente ele teria que lidar com concorrência, pois, imagine que nesse meio tempo em que o produto está em memória sendo atualizado outro processo também tentar atualizar o mesmo produto, isso causaria inconsistência. Por isso o Mongoose delega essa responsabilidade para o MongoDB que nativamente trata os updates de forma sequencial.

O código desse capitulo está disponvivel aqui.