Rails Devise 管理者機能 ユーザー作成機能 所属機能 複数モデル

# デモ
管理者(Admin)が職員(Worker)を作成する
Workerは店舗(Shop)に所属している

<iframe width="560" height="315" src="https://www.youtube.com/embed/62vvbEWL7-E" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

# 環境
```
$ rails -v
Rails 6.1.7

$ ruby -v
ruby 3.0.2
```
既にRuby、Railsの環境構築とrails newはしてある前提です。

全てのコードはこちらのリポジトリから

https://github.com/kakeruAoyama/admin_worker_shop

# ER図
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/324819/1b17fb02-e761-5fd3-acd3-7fe897567f4c.png)

:::note info
Workerにis_adminのようなカラムを付け足して管理者機能を実装することも考えられますが、今回はAdminとWorkerのできることが大きく違うため、別モデルで実装します。
管理者と非管理者の違いがそこまでないような場合はカラムを追加するだけの実装が簡単かもしれません。
以下のような記事参考にしてください。
https://qiita.com/kubochiro/items/340f32c366740a962cb8
:::


# deviseの導入
公式はこちら

https://github.com/heartcombo/devise

```ruby:Gemfile
gem 'devise'
```

```
$ bundle install
$ rails generate devise:install
```

`<body>`の下に追加

```diff_erb:app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
  <head>
    <title>AdminWorkerShop</title>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
  </head>

  <body>
+    <p class="notice"><%= notice %></p>
+    <p class="alert"><%= alert %></p>
    <%= yield %>
  </body>
</html>
```

ここまでの差分

https://github.com/kakeruAoyama/admin_worker_shop/commit/466c49a09ea567b69d420085b62af030a81b8cd2



# Adminの作成

```
$ rails generate devise Admin
$ rails db:migrate
```

### 初期Adminをseedで作成

```ruby:db/seeds.rb
Admin.create!(email: "admin@example.com", password: "p@ssword")
```

```
$ rails db:seed
```

ここまでの差分

https://github.com/kakeruAoyama/admin_worker_shop/commit/101c553b420292e374e4e4de6e928caadbb44310

# Shopの作成
### 店舗のCRUDを作成

```
$ rails g scaffold Shop name:string
$ rails db:migrate
```

### rootパスを追加
``` diff_ruby:config/routes.rb
Rails.application.routes.draw do
  resources :shops
  devise_for :admins
+ root 'shops#index'
end
```

### 管理者しか店舗を作成できないようにする

```diff_ruby:app/controllers/shops_controller.rb
class ShopsController < ApplicationController
+ before_action :authenticate_admin!
  before_action :set_shop, only: %i[ show edit update destroy ]

# 以下省略
```

### 管理者のログアウトボタンを設置

```diff_erb:app/views/shops/index.html.erb
<%= link_to "ログアウト", destroy_admin_session_path, method: :delete %>
```

ここで、`rails s`をして、ログイン機能と店舗の作成等がうまく実装されているか確認してみてください！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/324819/7d44bd8c-bad2-c16b-6b69-fe1fac826650.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/324819/22bcfca6-3bd6-3fd6-2c49-a63c45649b4d.png)



ここまでの差分

https://github.com/kakeruAoyama/admin_worker_shop/commit/02de8aa7b95a3f508e270a2ec259c8c06939a39e


# Workerの作成

### DeviseでWorkerモデルを作成

```
$ rails generate devise Worker
```

### nameカラムとshop_idカラムを追加

```diff_ruby:db/migrate/devise_create_workers.rb
class DeviseCreateWorkers < ActiveRecord::Migration[6.1]
  def change
    create_table :workers do |t|
      ## Database authenticatable
      t.string :email,              null: false, default: ""
      t.string :encrypted_password, null: false, default: ""
+     t.string :name
+     t.integer :shop_id
# 省略

```

```
$ rails db:migrate
```

### Workersのログインのビューを作成する

```
$ rails generate devise:views workers
```

```diff_ruby:config/initializers/devise.rb
# 省略
  # ==> Scopes configuration
  # Turn scoped views on. Before rendering "sessions/new", it will first check for
  # "users/sessions/new". It's turned off by default because it's slower if you
  # are using only default views.
- # config.scoped_views = false
+ config.scoped_views = true

  # Configure the default scope given to Warden. By default it's the first
  # devise role declared in your routes (usually :user).
  # config.default_scope = :user
# 省略
```

### Shopの選択と名前の入力をできるようにする

```diff_erb:app/views/workers/registrations/new.html.erb
<h2>Sign up</h2>

<%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %>
  <%= render "workers/shared/error_messages", resource: resource %>

+  <div class="field">
+    <%= f.label :name %><br />
+    <%= f.text_field :name %>
+  </div>

+  <div class="field">
+    <%= f.label :shop %><br />
+    <%= f.collection_select :shop_id, Shop.all, :id, :name, include_blank: "選択して下さい" %>
+  </div>

# 省略
```

### ストロングパラメータを設定

```diff_ruby:app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
+  before_action :configure_permitted_parameters, if: :devise_controller?
+
+  protected
+  def configure_permitted_parameters
+    devise_parameter_sanitizer.permit(:sign_up, keys: [:name, :shop_id])
+    devise_parameter_sanitizer.permit(:account_update, keys: [:name, :shop_id])
+  end
end

```



ここまでで`rails s`して`http://localhost:3000/workers/sign_up`でWorkerのログインができるか確認する

差分はこちら

https://github.com/kakeruAoyama/admin_worker_shop/commit/ec18a8d867d6b0e09b81f9682da7fbb26eb4d2ff

# Workersを作成

### コントローラーの作成
```
$ rails g controller workers index
```

```diff_ruby:app/controllers/workers_controller.rb
class WorkersController < ApplicationController
+ before_action :authenticate_admin!
  def index
+   @workers = Worker.joins(:shop).all
  end
end
```

### routesを設定

```diff_ruby:app/views/workers/index.html.erb
Rails.application.routes.draw do
+ get 'workers/index'
  devise_for :workers
  resources :shops
  devise_for :admins
+ resources :workers
  root 'shops#index'
end
```

### アソシエーションを設定

```diff_ruby:app/models/shop.rb
class Shop < ApplicationRecord
+ has_many :workers
end
```

```diff_ruby:app/models/worker.rb
class Worker < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable
+ belongs_to :shop
end
```

### ビューを作成
```diff_erb:app/views/workers/index.html.erb
<h1>Workers#index</h1>
<% @workers.each do |worker| %>
  <div>
    名前：<%= worker.name %>
    所属：<%= worker.shop.name %>
  </div>
<% end %>
```

ここで、`rails s`してみて、`http://localhost:3000/workers`にアクセスして（Adminのログインが必要な場合もあり）Worker一覧が表示されれば、問題ありません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/324819/2f6c3c05-ae6a-b389-0d5c-7c04d805629c.png)

ここまでの差分

https://github.com/kakeruAoyama/admin_worker_shop/commit/40a63b7702043c05d523149905ed345bf99a718a

# Deviseの設定を調整

### AdminとWorkerを新規作成をさせないようにする

```diff_ruby:app/models/admin.rb
class Admin < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
-  devise :database_authenticatable, :registerable,
+  devise :database_authenticatable,
         :recoverable, :rememberable, :validatable
end
```

```diff_ruby:app/models/worker.rb
class Worker < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
-  devise :database_authenticatable, :registerable,
+  devise :database_authenticatable,
         :recoverable, :rememberable, :validatable
  belongs_to :shop
end
```

### それぞれのログイン後のリダイレクト先を変更
```diff_ruby:app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  before_action :configure_permitted_parameters, if: :devise_controller?

+  def after_sign_in_path_for(resource)
+    case resource
+    when Admin
+      shops_path
+    when Worker
+      worker_path(current_worker)
+    end
+  end

  protected
  def configure_permitted_parameters
    devise_parameter_sanitizer.permit(:sign_up, keys: [:name, :shop_id])
    devise_parameter_sanitizer.permit(:account_update, keys: [:name, :shop_id])
  end
end
```

### workers#showを作成し、Workerの新規登録をAdmin以外ができなくする
```diff_ruby:app/controllers/workers_controller.rb
class WorkersController < ApplicationController
  def index
    authenticate_admin!
    @workers = Worker.joins(:shop).all
  end

  def new 
    authenticate_admin!
    @worker = Worker.new
  end

  def create 
    authenticate_admin!
    Worker.create!(worker_params)
    redirect_to workers_path
  end

  def show
    authenticate_worker!
    @worker = current_worker
  end

  private
  def worker_params
    params.require(:worker).permit(:name, :password, :email, :shop_id)
  end
end
```
ここまでの変更

https://github.com/kakeruAoyama/admin_worker_shop/commit/151f8705f6d00d9fed5913168dbcf394709a6ded

# ビューの調整
それぞれのページ(の好きなところ)に追加

```diff_erb:app/views/shops/index.html.erb
<%= link_to "Worker一覧", workers_path %>
```


```diff_erb:app/views/shops/show.html.erb
<h4>Worker一覧</h4>
<% @shop.workers.each do |worker| %>
  <%= worker.name %><br>
<% end %>
```

```diff_erb:app/views/workers/index.html.erb
<%= link_to "新規職員", new_worker_path %>
<%= link_to 'ログアウト', destroy_admin_session_path, method: :delete %>
<%= link_to "店舗一覧", shops_path %>
```

```diff_erb:app/views/workers/new.html.erb
<%= form_for @worker do |f| %>
  <div class="field">
    <%= f.label :name %><br />
    <%= f.text_field :name %>
  </div>

  <div class="field">
    <%= f.label :shop %><br />
    <%= f.collection_select :shop_id, Shop.all, :id, :name, include_blank: "選択して下さい" %>
  </div>

  <div class="field">
    <%= f.label :email %><br />
    <%= f.email_field :email, autofocus: true, autocomplete: "email" %>
  </div>

  <div class="field">
    <%= f.label :password %>
    <%= f.password_field :password, autocomplete: "new-password" %>
  </div>

  <div class="actions">
    <%= f.submit "新規登録" %>
  </div>
<% end %>
```

```diff_erb:app/views/workers/show.html.erb
<h1>Workers#Show</h1>
<%= @worker.name %>
<%= @worker.shop %>

<% if current_admin %>
  <%= link_to "職員一覧", wokers_path %>
<% end %>

```

ここまでの変更

https://github.com/kakeruAoyama/admin_worker_shop/commit/78bc56e18f5d1508df3de56fbc86c2b847d6fd2e


以上で完成になります！

# あとがき
Deviseはパッと作るのには向いてるけど、カスタマイズしようとすると、仕様をちゃんと理解していないと大変。

:::note warn
この記事の内容で、エラーや誤植等あれば、遠慮なくご指摘ください！
:::