owned this note
owned this note
Published
Linked with GitHub
# Fazendo Login com Devise
###### tags: `tutorial` `rails` `devise`
---
[ToC]
## Introdução
Esse tutorial é de como iremos fazer login e cadastro dos usuário usando a gema [Devise](https://github.com/heartcombo/devise). Ela é uma gema bem completa que trás diversas opões para gerenciamento de sessões, desde criação de usuário até envio de email de esquecimento de senha.
Nesse tutorial iremos:
- Instalar e configurar a gema Devise
- Atualizar o App para fazermos login
## Instalando Devise
Por ser uma gema, temos que entrar no arquivo `Gemfile` e em algum lugar antes de `group :development, :test do` devemos adicionar essa linha:
```ruby=28
gem 'devise'
```
E agora precisamos executar dois comandos no terminal. `bundle install` para instalar a nova dependência e `rails g devise:install`, que é para criar as configurações iniciais da gema.
Esse último comando já imprime algumas informações no terminal para nós. Nós vamos seguí-las, mas com algumas alterações.
A primeira etapa é entrar no arquivo `config/environments/development.rb:` e algum lugar depois de `Rails.application.configure do` e antes do `end` adicionar o seguinte:
```ruby=39
config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }
```
Isso vai configurar as opções de envio de email.
Precisamos proteger nosso app de pessoas não cadastradas, então devemos entrar no `app/controllers/application_controller.rb` e adicionar essa linha depois de `class ApplicationController < ActionController::Base`:
```ruby=
before_action :authenticate_user!
```
:::info
:bulb: **Nota:** O **`before_action`** é um utilitário dos controllers do Rails que executa uma ação antes de qualquer ação de rota. Esse **`authenticate_user!`** é o nome do metodo que executamos, que no caso é um utilitário do próprio Devise que verifica se o usuário está logado, e caso contrário, redireciona para a página de login.
:::
Isso vai tonar todas as nossas ações dos controladores precisarem de login, mas não é bem iss o que queremos. Queremos, por exemplo, poder acessar a nossa home, então devemos adicionar uma exceção. No arquivo `app/controllers/home_controller.rb` vamos adicionar depois de `class HomeController < ApplicationController`:
```ruby=
skip_before_action :authenticate_user!, only: %i[index]
```
:::info
:bulb: **Nota:** O **`skip_before_action`** é um outro utilitário dos controllers do Rails que ignora algum `before_action` configurado previamente. Como no **`ApplicationController`** definimos um, o **`HomeController`** herda tudo, inclusive os **`before_action`**
:::
Antes de seguir com as views, precisamos adicionar uns *helpers* para nos ajudar. No arquivo `app/helpers/application_helper.rb` adicione os métodos no módulo `ApplicationHelper`:
```ruby=
def resource_name
:user
end
def resource
@resource ||= User.new
end
def resource_class
User
end
def devise_mapping
@devise_mapping ||= Devise.mappings[:user]
end
```
:::warning
:exclamation: **Nota:** Os *helpers* são entidades que provém códigos rápidos ou pequenas configurações globais. Neste caso, estamos definindo alguns parâmetros do Devise, dizendo que a classe (ou no caso o recurso) que queremos atuar sobre é o modelo **`User`**. Caso sua aplicação tenha mais de um recurso com Devise, será necessário uma lógica mais detalhada.
:::
Agora estamos prontos para editar as views do nosso app. Vamos adicionar os alertas, mas vamos fazer um pouco diferente do sugerido pelo Devise, já que temos o Bootstrap. No arquivo `app/views/layouts/application.html.erb` vamos trocar a linha `<%= yield %>` por:
```htmlembedded=17
<% if notice %>
<p class="alert alert-success"><%= notice %></p>
<% end %>
<% if alert %>
<p class="alert alert-danger"><%= alert %></p>
<% end %>
<div class="container">
<%= yield %>
</div>
```
:::info
:bulb: **Nota:** Esse é apenas um jeito dos milhares de configurar essa notificações com bootstrap! Tente descobrir outros modos.
:::
Isso vai permitir que avisos de sucesso ou falha aparecam em qualquer tela da aplicação. A `div` que cobre o `yield` a classe container que serve para deixar as páginas num layout separado.
O próximo passo é criar o modelo do nosso usuário. A gema já possui um utilitário para nos auxiliar, então vamos usá-lo: `rails g devise user`. Como esse comando que fizemos cria um modelo do banco de dados, devemos rodar uma migração, então devemos levantar a aplicação e rodar a migração, assim:
```shell=
docker-compose build
docker-compose up
```
e num outro terminal:
```shell=
docker-compose exec web rails db:migrate
```
:::info
:bulb: **Nota:** O **`user`** no comando se refere ao nome do modelo. Caso queira outra coisa, tipo **account**, **coisa** ou **pessoa** fique a vontade.
:::
Após rodas as migrações, podemos começar a alterar as views para nos mostrar as informações de login.
## Login via NavBar
Primeiro iremos adicionar textos e links na Navbar indicando que o usuário pode se cadastrar ou entrar. Para isso, vamos entrar no arquivo `app/views/layouts/_navbar.html.erb` e fazer alguns ajustes.
Primeiro vamos trocar a linha `<%= link_to 'Home', home_path, class: 'nav-link active' %>` por:
```htmlembedded=10
<%= link_to 'Home', home_path, class: 'nav-link' + (current_page?(home_path) ? ' active' : '') %>
```
Essa pequena mudança deixa o link à home branco caso estamos nela. Agora antes de `</nav>` vamos colocar esse código:
```htmlembedded=
<ul class="navbar-nav flex-row ml-md-auto d-none d-md-flex">
<% if user_signed_in? %>
<li class="navbar-text">
Acessando como <strong><%= current_user.email %></strong>
</li>
<li class="nav-item">
<%= link_to 'Editar perfil', edit_user_registration_path, :class => 'nav-link' + (current_page?(edit_user_registration_path) ? ' active' : '') %>
</li>
<li class="nav-item">
<%= link_to "Sair", destroy_user_session_path, method: :delete, :class => 'nav-link' + (current_page?(destroy_user_session_path) ? ' active' : '') %>
</li>
<% else %>
<li class="nav-item">
<%= link_to "Cadastrar", new_user_registration_path, :class => 'nav-link' + (current_page?(new_user_registration_path) ? ' active' : '') %>
</li>
<li class="nav-item">
<%= link_to "Entrar", new_user_session_path, :class => 'nav-link' + (current_page?(new_user_session_path) ? ' active' : '') %>
</li>
<% end %>
</ul>
```
Esse código faz algumas coisas. Primeiro criamos o `ul` para adicionar itens na direita da Navbar. Depois vemos se o usuário está ou não logado com `user_signed_in?` e se sim, criamos três componentes, um dizendo qual usuário está logado, um botão para editar o perfil e outro para sair. Caso não esteja logado, criamos um botão para entrar e outro para se cadastrar.
Se iniciarmos de novo o nosso App, podemos ver e clicar nos botões, e se clicarmos, veremos isso: ![](https://i.imgur.com/LQ8VeNt.png)
Ainda tem bastante coisas a fazermos.
## Extraindo telas principais
Podemos perceber que nenhuma *view* foi gerada. Isso acontece pq o Devise possui todas as views gravadas internamente. Mas se quisermos editá-las precisamos extrair elas. Felizmente, já existe um utilitário do Devise para isso, basta executar
```shell
rails g devise:views
```
Esse comando vai gerar um monte de arquivos na pasta `app/views/devise`, mas primeiramente vamos nos preocupar apenas com três. O primeiro é o `app/views/devise/registrations/new.html.erb`.
A primeira mudança é colocar todo o código do arquivo entre uma tag `div`, ficando mais ou menos assim:
```htmlembedded=
<div>
<h2>Sign up</h2>
...
</div>
```
Depois, vamos mudar a tag `h2` para `h1` (apenas questão de estética) e traduzir o texto, se quiser. Então iremos extrair todo o código desde `<%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %>` até `<% end %>` para um novo arquivo chamado `app/views/devise/shared/_sign_up.html.erb`. De volta no arquivo `new.html.erb`, iremos colocar antes de `<%= render "devise/shared/links" %>` a linha
```htmlembedded=4
<%= render "devise/shared/sign_up" %>
```
O arquivo final vai ficar assim:
```htmlembedded=
<div>
<h1>Cadastrar</h1>
<%= render "devise/shared/sign_up" %>
<%= render "devise/shared/links" %>
</div>
```
:::info
:bulb: **Nota:** Podemos colocar a classe `class="text-center"` nos H1 para deixar o título das páginas centralizados.
:::
Essa extração que fizemos será usada em outra parte do projeto.
Iremos fazer as mesmas alterações e extrações no arquivo `app/views/devise/sessions/new.html.erb`. Colocamos o código entre `div`, mudamos o `h2` para `h1` (e opcionalmente traduzimos o texto), extraimos o código desde o `form_for` até `end` para o arquivo `app/views/devise/shared/_login.html.erb` e adicionamos o `render 'devise/shared/login'`. No final o arquivo vai ficar assim:
```htmlembedded=
<div>
<h1>Entrar</h1>
<%= render "devise/shared/login" %>
<%= render "devise/shared/links" %>
</div>
```
## Editando a tela de Login para usar Bootstrap
As telas de login e cadastro já são funcionais, mas elas não fazem uso do bootstrap. Então vamos atualizá-las. No arquivo `app/views/devise/shared/_login.html.erb` vamos atualizar o código por:
```htmlembedded=
<%= form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| %>
<div class="form-group">
<%= f.label :email %><br />
<%= f.email_field :email, autofocus: true, autocomplete: "email", class: 'form-control', placeholder: 'Digite seu email' %>
<small id="emailHelp" class="form-text text-muted">Não iremos compartilhar seus dados com ninguém.</small>
</div>
<div class="form-group">
<%= f.label :password, 'Senha' %><br />
<%= f.password_field :password, autocomplete: "current-password", class: 'form-control' %>
</div>
<% if devise_mapping.rememberable? %>
<div class="form-check">
<%= f.check_box :remember_me, class: 'form-check-input' %>
<%= f.label :remember_me, 'Lembrar de mim' %>
</div>
<% end %>
<div class="btn-group d-flex">
<%= f.submit "Entrar", class: 'btn btn-success' %>
</div>
<% end %>
```
Não existem grandes alterações, apenas usando classes do Bootstrap. Faremos algo parecido com o arquivo `app/views/devise/shared/_sign_up.html.erb`, mudando o código para:
```htmlembedded=
<%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %>
<%= render "devise/shared/error_messages", resource: resource %>
<div class="form-group">
<%= f.label :email %>
<%= f.email_field :email, autofocus: true, autocomplete: "email", class: 'form-control', placeholder: 'Digite seu email' %>
<small id="emailHelp" class="form-text text-muted">Não iremos compartilhar seus dados com ninguém.</small>
</div>
<div class="form-group">
<%= f.label :password, 'Senha' %>
<%= f.password_field :password, autocomplete: "new-password", class: 'form-control' %>
<% if @minimum_password_length %>
<small id="pwHelp" class="form-text text-muted">Mínimo de <%= @minimum_password_length %> characteres</small>
<% end %>
</div>
<div class="form-group">
<%= f.label :password_confirmation, 'Confirmação de Senha' %><br />
<%= f.password_field :password_confirmation, autocomplete: "new-password", class: 'form-control' %>
</div>
<div class="btn-group d-flex">
<%= f.submit "Criar Conta", class: 'btn btn-success' %>
</div>
<% end %>
```
Por fim, vamos fazer as mesmas coisas no arquivo `app/views/devise/shared/_links.html.erb`, subsituindo tudo por:
```htmlembedded=
<hr>
<div class="btn-group">
<% if controller_name != 'sessions' %>
<%= link_to "Entrar", new_session_path(resource_name), class: 'btn btn-primary' %>
<% end %>
<% if devise_mapping.registerable? && controller_name != 'registrations' %>
<%= link_to "Cadastrar", new_registration_path(resource_name), class: 'btn btn-primary' %>
<% end %>
<% if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %>
<%= link_to "Esqueceu sua senha?", new_password_path(resource_name), class: 'btn btn-warning' %>
<% end %>
<% if devise_mapping.confirmable? && controller_name != 'confirmations' %>
<%= link_to "Não recebeu email de confirmação?", new_confirmation_path(resource_name), class: 'btn btn-warning' %>
<% end %>
<% if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks' %>
<%= link_to "Não recebeu email para desbloquear conta?", new_unlock_path(resource_name), class: 'btn btn-warning' %>
<% end %>
<% if devise_mapping.omniauthable? %>
<% resource_class.omniauth_providers.each do |provider| %>
<%= link_to "Entre com #{OmniAuth::Utils.camelize(provider)}", omniauth_authorize_path(resource_name, provider), class: 'btn btn-primary' %>
<% end %>
<% end %>
</div>
```
Agora falta atualizar a nossa Home!
## Atualizando a Home
No nosso arquivo `app/views/home/index.html.erb` vamos atualizar o código para:
```htmlembedded=
<div class="card mx-auto" style="width: 40%; min-width: 200px; margin-top: 10%">
<div class="card-header">
<% if user_signed_in? %>
<i class="bi-card-text"></i> Home
<% else %>
<i class="bi-box-arrow-in-right"></i> Login
<% end %>
</div>
<div class="card-body">
<h5 class="card-title">Bem Vindo!</h5>
<% if user_signed_in? %>
<p class="card-text">Seja bem vindo ao meu site :D</p>
<p class="card-text">Por favor, faça login ou se cadastre:</p>
<div class="btn-group" role="group">
<%= link_to 'Upload de Imagens', '', :class => 'navbar-link' %>
</div>
<% else %>
<p class="card-text">Seja bem vindo ao meu site :D</p>
<p class="card-text">Por favor, faça login ou se cadastre:</p>
<div class="btn-group d-flex" role="group">
<%= button_tag 'Entrar', class: 'btn btn-primary', data: { toggle: 'modal', target: '#login_modal' } %>
<%= button_tag 'Criar conta', class: 'btn btn-secondary', data: { toggle: 'modal', target: '#sign_up_modal' } %>
</div>
<% end %>
</div>
</div>
<%= render 'login_modal' %>
<%= render 'sign_up_modal' %>
```
Aqui o que fazemos (além de alguns ajustes de layout) são alguns checks para verificar se o usuário está logado ou não com o método `user_signed_in?`. Se ele está, colocamos um link básico (spoiler do que vamos fazer) senão mostramos botões para que ele faça login. Mas esse botões são um pouco diferentes, eles não levam a gente para uma outra tela, mas sim **mostram um modal**.
:::info
:bulb: **Nota:** Modal é uma tela que fica acima das outras telas. Pode ser usada para notificações ou para ações simples que não fazem muito sentido na tela atual (como login na home).
:::
Em baixo do arquivo, temos dois renders, um do `login_modal` e outro do `sign_up_modal`. Esses são os modais que irão fazer login e cadastro do usuário (e foi por causa deles que fizemos a extração anterior). Primeiro vamos criar o `app/views/home/_modal_form.html.erb` e colocar esse código:
```htmlembedded=
<div class="modal fade" id="<%= modal_id %>" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">Entrar</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<%= render render_resource %>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-danger" data-dismiss="modal">Fechar</button>
</div>
</div>
</div>
</div>
```
Esse código é bem simples, bem parecido com exemplos de Modal do Bootstrap. A diferença é que no ID a gente coloca um ID que recebemos e no render a gente usa outro argumento.
E para usar esse *partial*, vamos criar os dois modals que usamos anteriormente, o `app/views/home/_login_modal.html.erb`, com esse código:
```htmlembedded=
<%= render 'modal_form', modal_id: 'login_modal', render_resource: 'devise/shared/login' %>
```
e o `app/views/home/_sign_up_modal.html.erb` com esse:
```htmlembedded=
<%= render 'modal_form', modal_id: 'sign_up_modal', render_resource: 'devise/shared/sign_up' %>
```
E agora, acessando o nosso app, temos um fluxo completo de entrar na home -> criar conta -> deslogar -> logar de novo!
:::warning
:rocket: **Desafio:** Neste tutorial nós mexemos em apenas três arquivos de front do Devise. Como desafio, sugiro atualizar os demais arquivos com classes do Bootstrap :slightly_smiling_face:
:::
:::success
:rocket: **Próximo passo:** Siga o link ➜ [Recebendo imagens com ActiveStorage](https://hackmd.io/fbYkNFBOQk-Yl2MiWkw3gw)
:::