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.