diff --git a/Gemfile b/Gemfile index d4d7a05..9470864 100644 --- a/Gemfile +++ b/Gemfile @@ -5,6 +5,8 @@ ruby '3.2.2' gem 'rails', '~> 7.1.2' gem 'bootsnap', require: false +gem 'cpf_cnpj' + gem 'cssbundling-rails' gem 'devise' gem 'jbuilder' diff --git a/Gemfile.lock b/Gemfile.lock index 092623c..25fed0c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -95,6 +95,7 @@ GEM xpath (~> 3.2) concurrent-ruby (1.2.2) connection_pool (2.4.1) + cpf_cnpj (0.5.0) crass (1.0.6) cssbundling-rails (1.3.3) railties (>= 6.0.0) @@ -307,6 +308,7 @@ PLATFORMS DEPENDENCIES bootsnap capybara (>= 2.15) + cpf_cnpj cssbundling-rails cuprite debug diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 09705d1..4c08f2d 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,2 +1,9 @@ class ApplicationController < ActionController::Base + before_action :configure_permitted_parameters, if: :devise_controller? + + protected + + def configure_permitted_parameters + devise_parameter_sanitizer.permit(:sign_up, keys: %i[full_name citizen_id_number]) + end end diff --git a/app/models/user.rb b/app/models/user.rb index c9f596f..d62525b 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,3 +1,5 @@ +require 'cpf_cnpj' + class User < ApplicationRecord # Include default devise modules. Others available are: # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable @@ -6,5 +8,14 @@ class User < ApplicationRecord has_one :profile, dependent: :destroy has_many :posts, dependent: :destroy + validates :full_name, :citizen_id_number, presence: true + validates :citizen_id_number, uniqueness: true + validate :validate_citizen_id_number + + private + + def validate_citizen_id_number + errors.add(:citizen_id_number, 'inválido') unless CPF.valid?(citizen_id_number) + end enum role: { user: 0, admin: 10 } end diff --git a/app/views/devise/confirmations/new.html.erb b/app/views/devise/confirmations/new.html.erb new file mode 100644 index 0000000..b12dd0c --- /dev/null +++ b/app/views/devise/confirmations/new.html.erb @@ -0,0 +1,16 @@ +

Resend confirmation instructions

+ +<%= form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: { method: :post }) do |f| %> + <%= render "devise/shared/error_messages", resource: resource %> + +
+ <%= f.label :email %>
+ <%= f.email_field :email, autofocus: true, autocomplete: "email", value: (resource.pending_reconfirmation? ? resource.unconfirmed_email : resource.email) %> +
+ +
+ <%= f.submit "Resend confirmation instructions" %> +
+<% end %> + +<%= render "devise/shared/links" %> diff --git a/app/views/devise/mailer/confirmation_instructions.html.erb b/app/views/devise/mailer/confirmation_instructions.html.erb new file mode 100644 index 0000000..dc55f64 --- /dev/null +++ b/app/views/devise/mailer/confirmation_instructions.html.erb @@ -0,0 +1,5 @@ +

Welcome <%= @email %>!

+ +

You can confirm your account email through the link below:

+ +

<%= link_to 'Confirm my account', confirmation_url(@resource, confirmation_token: @token) %>

diff --git a/app/views/devise/mailer/email_changed.html.erb b/app/views/devise/mailer/email_changed.html.erb new file mode 100644 index 0000000..32f4ba8 --- /dev/null +++ b/app/views/devise/mailer/email_changed.html.erb @@ -0,0 +1,7 @@ +

Hello <%= @email %>!

+ +<% if @resource.try(:unconfirmed_email?) %> +

We're contacting you to notify you that your email is being changed to <%= @resource.unconfirmed_email %>.

+<% else %> +

We're contacting you to notify you that your email has been changed to <%= @resource.email %>.

+<% end %> diff --git a/app/views/devise/mailer/password_change.html.erb b/app/views/devise/mailer/password_change.html.erb new file mode 100644 index 0000000..b41daf4 --- /dev/null +++ b/app/views/devise/mailer/password_change.html.erb @@ -0,0 +1,3 @@ +

Hello <%= @resource.email %>!

+ +

We're contacting you to notify you that your password has been changed.

diff --git a/app/views/devise/mailer/reset_password_instructions.html.erb b/app/views/devise/mailer/reset_password_instructions.html.erb new file mode 100644 index 0000000..f667dc1 --- /dev/null +++ b/app/views/devise/mailer/reset_password_instructions.html.erb @@ -0,0 +1,8 @@ +

Hello <%= @resource.email %>!

+ +

Someone has requested a link to change your password. You can do this through the link below.

+ +

<%= link_to 'Change my password', edit_password_url(@resource, reset_password_token: @token) %>

+ +

If you didn't request this, please ignore this email.

+

Your password won't change until you access the link above and create a new one.

diff --git a/app/views/devise/mailer/unlock_instructions.html.erb b/app/views/devise/mailer/unlock_instructions.html.erb new file mode 100644 index 0000000..41e148b --- /dev/null +++ b/app/views/devise/mailer/unlock_instructions.html.erb @@ -0,0 +1,7 @@ +

Hello <%= @resource.email %>!

+ +

Your account has been locked due to an excessive number of unsuccessful sign in attempts.

+ +

Click the link below to unlock your account:

+ +

<%= link_to 'Unlock my account', unlock_url(@resource, unlock_token: @token) %>

diff --git a/app/views/devise/passwords/edit.html.erb b/app/views/devise/passwords/edit.html.erb new file mode 100644 index 0000000..5fbb9ff --- /dev/null +++ b/app/views/devise/passwords/edit.html.erb @@ -0,0 +1,25 @@ +

Change your password

+ +<%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put }) do |f| %> + <%= render "devise/shared/error_messages", resource: resource %> + <%= f.hidden_field :reset_password_token %> + +
+ <%= f.label :password, "New password" %>
+ <% if @minimum_password_length %> + (<%= @minimum_password_length %> characters minimum)
+ <% end %> + <%= f.password_field :password, autofocus: true, autocomplete: "new-password" %> +
+ +
+ <%= f.label :password_confirmation, "Confirm new password" %>
+ <%= f.password_field :password_confirmation, autocomplete: "new-password" %> +
+ +
+ <%= f.submit "Change my password" %> +
+<% end %> + +<%= render "devise/shared/links" %> diff --git a/app/views/devise/passwords/new.html.erb b/app/views/devise/passwords/new.html.erb new file mode 100644 index 0000000..9b486b8 --- /dev/null +++ b/app/views/devise/passwords/new.html.erb @@ -0,0 +1,16 @@ +

Forgot your password?

+ +<%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post }) do |f| %> + <%= render "devise/shared/error_messages", resource: resource %> + +
+ <%= f.label :email %>
+ <%= f.email_field :email, autofocus: true, autocomplete: "email" %> +
+ +
+ <%= f.submit "Send me reset password instructions" %> +
+<% end %> + +<%= render "devise/shared/links" %> diff --git a/app/views/devise/registrations/edit.html.erb b/app/views/devise/registrations/edit.html.erb new file mode 100644 index 0000000..b82e336 --- /dev/null +++ b/app/views/devise/registrations/edit.html.erb @@ -0,0 +1,43 @@ +

Edit <%= resource_name.to_s.humanize %>

+ +<%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put }) do |f| %> + <%= render "devise/shared/error_messages", resource: resource %> + +
+ <%= f.label :email %>
+ <%= f.email_field :email, autofocus: true, autocomplete: "email" %> +
+ + <% if devise_mapping.confirmable? && resource.pending_reconfirmation? %> +
Currently waiting confirmation for: <%= resource.unconfirmed_email %>
+ <% end %> + +
+ <%= f.label :password %> (leave blank if you don't want to change it)
+ <%= f.password_field :password, autocomplete: "new-password" %> + <% if @minimum_password_length %> +
+ <%= @minimum_password_length %> characters minimum + <% end %> +
+ +
+ <%= f.label :password_confirmation %>
+ <%= f.password_field :password_confirmation, autocomplete: "new-password" %> +
+ +
+ <%= f.label :current_password %> (we need your current password to confirm your changes)
+ <%= f.password_field :current_password, autocomplete: "current-password" %> +
+ +
+ <%= f.submit "Update" %> +
+<% end %> + +

Cancel my account

+ +
Unhappy? <%= button_to "Cancel my account", registration_path(resource_name), data: { confirm: "Are you sure?", turbo_confirm: "Are you sure?" }, method: :delete %>
+ +<%= link_to "Back", :back %> diff --git a/app/views/devise/registrations/new.html.erb b/app/views/devise/registrations/new.html.erb new file mode 100644 index 0000000..34df5cc --- /dev/null +++ b/app/views/devise/registrations/new.html.erb @@ -0,0 +1,39 @@ +

Cadastro

+ +<%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %> + <%= render "devise/shared/error_messages", resource: resource %> + +
+ <%= f.label :full_name %>
+ <%= f.text_field :full_name, autofocus: true %> +
+ +
+ <%= f.label :citizen_id_number %>
+ <%= f.text_field :citizen_id_number %> +
+ +
+ <%= f.label :email %>
+ <%= f.email_field :email, autocomplete: "email" %> +
+ +
+ <%= f.label :password %> + <% if @minimum_password_length %> + (mínimo <%= @minimum_password_length %> carácteres) + <% end %>
+ <%= f.password_field :password, autocomplete: "new-password" %> +
+ +
+ <%= f.label :password_confirmation %>
+ <%= f.password_field :password_confirmation, autocomplete: "new-password" %> +
+ +
+ <%= f.submit "Cadastrar" %> +
+<% end %> + +<%= render "devise/shared/links" %> diff --git a/app/views/devise/sessions/new.html.erb b/app/views/devise/sessions/new.html.erb new file mode 100644 index 0000000..7d63519 --- /dev/null +++ b/app/views/devise/sessions/new.html.erb @@ -0,0 +1,26 @@ +

Log in

+ +<%= form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| %> +
+ <%= f.label :email %>
+ <%= f.email_field :email, autofocus: true, autocomplete: "email" %> +
+ +
+ <%= f.label :password %>
+ <%= f.password_field :password, autocomplete: "current-password" %> +
+ + <% if devise_mapping.rememberable? %> +
+ <%= f.check_box :remember_me %> + <%= f.label :remember_me %> +
+ <% end %> + +
+ <%= f.submit "Entrar" %> +
+<% end %> + +<%= render "devise/shared/links" %> diff --git a/app/views/devise/shared/_error_messages.html.erb b/app/views/devise/shared/_error_messages.html.erb new file mode 100644 index 0000000..cabfe30 --- /dev/null +++ b/app/views/devise/shared/_error_messages.html.erb @@ -0,0 +1,15 @@ +<% if resource.errors.any? %> +
+

+ <%= I18n.t("errors.messages.not_saved", + count: resource.errors.count, + resource: resource.class.model_name.human.downcase) + %> +

+ +
+<% end %> diff --git a/app/views/devise/shared/_links.html.erb b/app/views/devise/shared/_links.html.erb new file mode 100644 index 0000000..7a409f8 --- /dev/null +++ b/app/views/devise/shared/_links.html.erb @@ -0,0 +1,27 @@ +<%- if controller_name != 'sessions' %> + <%= link_to "Log in", new_session_path(resource_name) %>
+<% end %> + +<%- if devise_mapping.registerable? && controller_name != 'registrations' %> + <%= link_to "Cadastrar", new_registration_path(resource_name) %>
+<% end %> + + + +<%- if devise_mapping.confirmable? && controller_name != 'confirmations' %> + <%= link_to "Didn't receive confirmation instructions?", new_confirmation_path(resource_name) %>
+<% end %> + +<%- if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks' %> + <%= link_to "Didn't receive unlock instructions?", new_unlock_path(resource_name) %>
+<% end %> + +<%- if devise_mapping.omniauthable? %> + <%- resource_class.omniauth_providers.each do |provider| %> + <%= button_to "Sign in with #{OmniAuth::Utils.camelize(provider)}", omniauth_authorize_path(resource_name, provider), data: { turbo: false } %>
+ <% end %> +<% end %> diff --git a/app/views/devise/unlocks/new.html.erb b/app/views/devise/unlocks/new.html.erb new file mode 100644 index 0000000..ffc34de --- /dev/null +++ b/app/views/devise/unlocks/new.html.erb @@ -0,0 +1,16 @@ +

Resend unlock instructions

+ +<%= form_for(resource, as: resource_name, url: unlock_path(resource_name), html: { method: :post }) do |f| %> + <%= render "devise/shared/error_messages", resource: resource %> + +
+ <%= f.label :email %>
+ <%= f.email_field :email, autofocus: true, autocomplete: "email" %> +
+ +
+ <%= f.submit "Resend unlock instructions" %> +
+<% end %> + +<%= render "devise/shared/links" %> diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index cdea826..2de1230 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -1,7 +1,7 @@ - Cola?Bora + Portfoliorrr <%= csrf_meta_tags %> <%= csp_meta_tag %> diff --git a/app/views/shared/_navbar.html.erb b/app/views/shared/_navbar.html.erb index 440e18b..152ad20 100644 --- a/app/views/shared/_navbar.html.erb +++ b/app/views/shared/_navbar.html.erb @@ -1,5 +1,14 @@ \ No newline at end of file + + diff --git a/config/initializers/locale.rb b/config/initializers/locale.rb index f748d1b..966e7f8 100644 --- a/config/initializers/locale.rb +++ b/config/initializers/locale.rb @@ -1,5 +1,3 @@ -# Permitted locales available for the application -I18n.available_locales = [:en, :'pt-BR'] -# Set default locale to something other than :en +I18n.available_locales = [:en, :'pt-BR'] I18n.default_locale = :'pt-BR' diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml new file mode 100644 index 0000000..6e20207 --- /dev/null +++ b/config/locales/pt-BR.yml @@ -0,0 +1,5 @@ +pt-BR: + sign up: 'Cadastrar' + log in: 'Entrar' + log out: 'Sair' + \ No newline at end of file diff --git a/config/locales/user.yml b/config/locales/user.yml new file mode 100644 index 0000000..99b891d --- /dev/null +++ b/config/locales/user.yml @@ -0,0 +1,12 @@ +pt-BR: + activerecord: + models: + user: Usuário + + attributes: + user: + full_name: Nome Completo + email: E-mail + citizen_id_number: CPF + password: Senha + password_confirmation: Confirme sua Senha diff --git a/db/migrate/20240118140014_add_unique_index_to_user_citizen_id_number.rb b/db/migrate/20240118140014_add_unique_index_to_user_citizen_id_number.rb new file mode 100644 index 0000000..1236f30 --- /dev/null +++ b/db/migrate/20240118140014_add_unique_index_to_user_citizen_id_number.rb @@ -0,0 +1,6 @@ +class AddUniqueIndexToUserCitizenIdNumber < ActiveRecord::Migration[7.1] + def change + remove_index :users, :citizen_id_number + add_index :users, :citizen_id_number, unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 91e6a29..96b83bb 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.1].define(version: 2024_01_18_054350) do +ActiveRecord::Schema[7.1].define(version: 2024_01_19_143304) do create_table "job_categories", force: :cascade do |t| t.string "name" t.datetime "created_at", null: false @@ -18,6 +18,19 @@ t.index ["name"], name: "index_job_categories_on_name", unique: true end + create_table "personal_infos", force: :cascade do |t| + t.integer "profile_id", null: false + t.string "street" + t.string "area" + t.string "city" + t.string "state" + t.string "phone" + t.boolean "visibility" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["profile_id"], name: "index_personal_infos_on_profile_id" + end + create_table "posts", force: :cascade do |t| t.integer "user_id", null: false t.string "title" @@ -44,13 +57,14 @@ t.datetime "created_at", null: false t.datetime "updated_at", null: false t.string "full_name" - t.integer "role", default: 0 t.string "citizen_id_number" - t.index ["citizen_id_number"], name: "index_users_on_citizen_id_number" + t.integer "role", default: 0 + t.index ["citizen_id_number"], name: "index_users_on_citizen_id_number", unique: true t.index ["email"], name: "index_users_on_email", unique: true t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true end + add_foreign_key "personal_infos", "profiles" add_foreign_key "posts", "users" add_foreign_key "profiles", "users" end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index ab0da92..085dceb 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -1,4 +1,42 @@ require 'rails_helper' RSpec.describe User, type: :model do + describe '#valid?' do + context 'presença' do + it 'nome completo não pode ficar em branco' do + user = User.new email: 'teste@email.com', password: '123456', + full_name: '', citizen_id_number: '88257290068' + + expect(user).not_to be_valid + end + + it 'CPF não pode ficar em branco' do + user = User.new email: 'teste@email.com', password: '123456', + full_name: 'Usuário A', citizen_id_number: '' + + expect(user).not_to be_valid + end + end + + context 'unicidade' do + it 'CPF não pode estar em uso' do + User.create! email: 'usuario_a@email.com', password: '123456', + full_name: 'Usuário A', citizen_id_number: '88257290068' + + user = User.new email: 'usuario_b@email.com', password: '123456', + full_name: 'Usuário B', citizen_id_number: '88257290068' + + expect(user).not_to be_valid + end + end + + context 'legitimidade' do + it 'CPF deve ser reconhecido' do + user = User.new email: 'usuario_b@email.com', password: '123456', + full_name: 'Usuário B', citizen_id_number: '88257290060' + + expect(user).not_to be_valid + end + end + end end diff --git a/spec/system/visitor_logs_in_spec.rb b/spec/system/visitor_logs_in_spec.rb new file mode 100644 index 0000000..9144757 --- /dev/null +++ b/spec/system/visitor_logs_in_spec.rb @@ -0,0 +1,56 @@ +require 'rails_helper' + +describe 'Usuário acessa a página de login' do + it 'e realiza o log in com sucesso' do + create(:user, email: 'joaoalmeida@email.com', password: '123456') + + visit root_path + + click_on 'Entrar' + + within '#new_user' do + fill_in 'E-mail', with: 'joaoalmeida@email.com' + fill_in 'Senha', with: '123456' + + click_on 'Entrar' + end + + expect(current_path).to eq root_path + expect(page).to have_content 'Login efetuado com sucesso' + + within 'nav' do + expect(page).not_to have_link 'Entrar' + expect(page).not_to have_link 'Cadastrar Usuário' + end + end + + context 'e falha' do + it 'e-mail e senha não conferem' do + visit root_path + + click_on 'Entrar' + + within '#new_user' do + fill_in 'E-mail', with: 'joaoalmeida@email.com' + fill_in 'Senha', with: '123456' + + click_on 'Entrar' + end + expect(page).to have_content 'E-mail ou senha inválidos' + end + + it 'e-mail ou senha estão em branco' do + visit root_path + + click_on 'Entrar' + + within '#new_user' do + fill_in 'E-mail', with: '' + fill_in 'Senha', with: '' + + click_on 'Entrar' + end + expect(page).to have_content 'E-mail ou senha inválidos' + end + end +end diff --git a/spec/system/visitor_views_home_spec.rb b/spec/system/visitor_views_home_spec.rb index 33226e1..eb24031 100644 --- a/spec/system/visitor_views_home_spec.rb +++ b/spec/system/visitor_views_home_spec.rb @@ -1,10 +1,91 @@ require 'rails_helper' -describe 'Visitor views home' do - it 'successfully' do +describe 'Usuário acessa página de cadastro de usuário' do + it 'a partir da home' do visit root_path - expect(page).to have_content 'Portfoliorrr' + click_on 'Cadastrar Usuário' + + expect(current_path).to eq new_user_registration_path + end + + it 'e realiza o cadastro com sucesso' do + visit new_user_registration_path + + fill_in 'Nome Completo', with: 'João Almeida' + fill_in 'E-mail', with: 'joaoalmeida@email.com' + fill_in 'CPF', with: '88257290068' + fill_in 'Senha', with: '123456' + fill_in 'Confirme sua Senha', with: '123456' + click_on 'Cadastrar' + + expect(current_path).to eq root_path + expect(page).to have_content 'Boas vindas 👋 Você realizou seu cadastro com sucesso.' + end + + context 'e realiza o cadastro com falhas' do + it 'campos não podem ficar em brancos' do + visit new_user_registration_path + + fill_in 'Nome Completo', with: '' + fill_in 'E-mail', with: '' + fill_in 'CPF', with: '' + fill_in 'Senha', with: '' + fill_in 'Confirme sua Senha', with: '' + click_on 'Cadastrar' + + expect(page).to have_content 'Não foi possível salvar usuário' + expect(page).to have_content 'Nome Completo não pode ficar em branco' + expect(page).to have_content 'E-mail não pode ficar em branco' + expect(page).to have_content 'CPF não pode ficar em branco' + expect(page).to have_content 'Senha não pode ficar em branco' + end + + it 'senha não pode ter menos de 6 caracteres' do + visit new_user_registration_path + + fill_in 'Nome Completo', with: 'João Almeida' + fill_in 'E-mail', with: 'joaoalmeida@email.com' + fill_in 'CPF', with: '88257290068' + fill_in 'Senha', with: '1234' + fill_in 'Confirme sua Senha', with: '1234' + click_on 'Cadastrar' + + expect(page).to have_content 'Não foi possível salvar usuário' + expect(page).to have_content 'Senha é muito curto (mínimo: 6 caracteres)' + end + + it 'com CPF ou confirmação de senha inválidos' do + visit new_user_registration_path + + fill_in 'Nome Completo', with: 'João Almeida' + fill_in 'E-mail', with: 'joaoalmeida@email.com' + fill_in 'CPF', with: '88257290060' + fill_in 'Senha', with: '123456' + fill_in 'Confirme sua Senha', with: '123467' + click_on 'Cadastrar' + + expect(page).to have_content 'Não foi possível salvar usuário' + expect(page).to have_content 'Confirme sua Senha não é igual a Senha' + expect(page).to have_content 'CPF inválido' + end + + it 'CPF e e-mail devem ser únicos' do + create(:user, email: 'joaoalmeida@email.com', citizen_id_number: '88257290068') + + visit new_user_registration_path + + fill_in 'Nome Completo', with: 'João Almeida' + fill_in 'E-mail', with: 'joaoalmeida@email.com' + fill_in 'CPF', with: '88257290068' + fill_in 'Senha', with: '123456' + fill_in 'Confirme sua Senha', with: '123456' + click_on 'Cadastrar' + + expect(page).to have_content 'Não foi possível salvar usuário' + expect(page).to have_content 'E-mail já está em uso' + expect(page).to have_content 'CPF já está em uso' + end end it 'and prints message' do