簡単ログイン・ゲストログイン機能の実装方法（ポートフォリオ用）

ポートフォリオ用のWebサイトに必須と言われる<font color="Red">**ゲストログイン機能**</font>。

特に`Devise`の`confirmable`を使用している場合は，ゲストログイン機能が無いと，<font color="Red">新規登録が面倒</font>という理由でポートフォリオを見てすらもらえない可能性が高くなります。

ところが，普通のWebサイトには導入しないためか，記事が見当たりませんでした。また，<font color="Red">ゲストユーザー機能が動作しなくなるリスク</font>を抱えた実装も複数見かけました。そこで，私のたどりついた実装方法をアップさせていただきます。

以下，`Devise`を使用している前提とし，トップページが **root '<font color="Red">homes</font>#index'** で設定されていることとします。

```routes.rb
Rails.application.routes.draw do
  # トップページは homes#index の前提
  root "homes#index"
end
```

## YouTube

- YouTubeに解説動画をアップしました:grinning:
  - [【Rails】あなたの簡単ログイン機能は大丈夫？　ゲストログイン機能の実装方法 (前半)](https://youtu.be/s4OGhzx91pA)
  - [【Rails】あなたの簡単ログイン機能は大丈夫？　ゲストログイン機能の実装方法 (後半)](https://youtu.be/Nj_YJ4hRN9E)

## その1: ゲストログイン機能の実装方法（簡易版）

```config/routes.rb
# 以下を追加
  post '/homes/guest_sign_in', to: 'homes#guest_sign_in'
```

```app/controllers/homes_controller.rb
# 以下を追加
  def guest_sign_in
    user = User.find_or_create_by!(email: 'guest@example.com') do |user|
      user.password = SecureRandom.urlsafe_base64
      # user.skip_confirmation!  # Confirmable を使用している場合は必要
      # 例えば name を入力必須としているならば， user.name = "ゲスト" なども必要
    end
    sign_in user
    redirect_to root_path, notice: 'ゲストユーザーとしてログインしました。'
  end
```

```erb:app/views/homes/index.html.erb
# 以下を追加
<%= link_to 'ゲストログイン（閲覧用）', homes_guest_sign_in_path, method: :post %>
```

- ポイントは`Devise`の`sign_in`メソッドを利用することです。

- `find_by`ではなく，`find_or_create_by`を利用しています。これにより，ゲストユーザーをあらかじめ作成する手間を省けます。また，<font color="Red">ゲストユーザーを削除されてゲスト機能が動作しなくなる</font>リスクも回避できます。

- パスワードを特定されると，ユーザー編集ページから<font color="Red">**メールアドレス**・**パスワード**を変更される</font>可能性があるため，**パスワードはランダム文字列**にしています。

- バリデーションの影響でゲストユーザーを作成できない場合は，エラーを発生させるように設定しております。

## その2: ゲストログイン機能の実装方法

上記の実装方法は理解しやすいものの，ゲストログイン機能を`HomesController`に任せることには違和感があります。本来は，ログイン機能同様，`Devise`の`SessionsController`に任せるべきでしょう。

例えば次のように実装すると，より自然なものになるのではないでしょうか。

### 2-1. ゲストログイン機能の設定

- まず，`SessionsController`に新しいアクション`guest_sign_in`を準備します。

```config/routes.rb
# 以下を追加
  devise_scope :user do
    post 'users/guest_sign_in', to: 'users/sessions#guest_sign_in'
  end
```

- アクション`guest_sign_in`を設定するため，`app/controllers`に`users`ディレクトリを作成し，その中に次の`sessions_controller.rb`を作成します。ゲストユーザーを探す or 作成する機能は `User.rb` に移動させるとコントローラーがスッキリします。

```app/controllers/users/sessions_controller.rb
class Users::SessionsController < Devise::SessionsController
  def guest_sign_in
    user = User.guest
    sign_in user
    redirect_to root_path, notice: 'ゲストユーザーとしてログインしました。'
  end
end
```

```app/models/user.rb
# 以下を追加
  def self.guest
    find_or_create_by!(email: 'guest@example.com') do |user|
      user.password = SecureRandom.urlsafe_base64
      # user.confirmed_at = Time.now  # Confirmable を使用している場合は必要
      # 例えば name を入力必須としているならば， user.name = "ゲスト" なども必要
    end
  end
```

- トップページにゲストログインボタンを用意する場合は，以下を追加すればOKです。（スタイルは各自で追加して下さい）

```erb:app/views/homes/index.html.erb
# 以下を追加
<%= link_to 'ゲストログイン（閲覧用）', users_guest_sign_in_path, method: :post %>
```

### 2-2. ゲストユーザーを削除できないようにする

上記の実装方法ならば，仮にゲストユーザーを削除されたとしても，ゲスト機能が動作しなくなることはありません。

ですが，例えば２名の方が同時にログインされている状態で，片方の方がゲストユーザーを削除しますと，もう片方の方も強制的にログアウトさせられてしまいます。ポートフォリオの場合はレアケースだと思いますが，念のためゲストユーザーを削除できないように設定しておきましょう。

- ゲストユーザーが削除機能を使用できないようにするには，`registrations.rb`を編集する必要があります。まずは，ルーティングを変更します。

```config/routes.rb
# devise_for :users を次に置き換える
  devise_for :users, controllers: {
    registrations: 'users/registrations'
  }
```

- destroyアクションの動作前に，メールアドレスがゲストユーザー用になっていないかチェックするように設定します。
  - ゲストユーザーならばフラッシュを出した上でトップページにリダイレクトさせるように設定しています。

```app/controllers/users/registrations_controller.rb
class Users::RegistrationsController < Devise::RegistrationsController
  before_action :ensure_normal_user, only: :destroy

  def ensure_normal_user
    if resource.email == 'guest@example.com'
      redirect_to root_path, alert: 'ゲストユーザーは削除できません。'
    end
  end

end
```

### 2-3 ゲストユーザーがメールアドレス・パスワードを変更できないようにする

上記の実装ならば，「ユーザー編集機能」や「パスワード再設定機能」により`メールアドレス`・`パスワード`を変更される可能性は非常に低いですし，仮に変更されたとしてもポートフォリオならば問題にならないかと思います。

それでも，「ゲストユーザーのメールアドレス・パスワードを絶対に変更されたくない！」という場合は，更に次のような設定をすればOKです。

- 削除機能を止めるのと同じ手法で，ゲストユーザーがメールアドレス・パスワードを編集できないように設定します。

```diff:app/controllers/users/registrations_controller.rb
- before_action :ensure_normal_user, only: :destroy
+ before_action :ensure_normal_user, only: %i[update destroy]


- redirect_to root_path, alert: 'ゲストユーザーは削除できません。'
+ redirect_to root_path, alert: 'ゲストユーザーの更新・削除はできません。'
```

- パスワード再設定メールの送信機能を止めるには，`passwords_controller.rb`の`create`アクションの動作前にチェックすればOKです。まずはルーティングを変更します。

```config/routes.rb
#   devise_for :users, controllers: {
#     registrations: 'users/registrations'
#   }
# を次に置き換える。（,の付け忘れに注意！）
  devise_for :users, controllers: {
    registrations: 'users/registrations',
    passwords: 'users/passwords'
  }
```

- パスワード再設定ページのフォームに入力されたメールアドレスは`params[:user][:email]`で受け取れるので，これを利用してゲストユーザーを特定します。
  - メールアドレスは大文字が小文字に変換されて保存されているため，`downcase`メソッドが必要です。

```app/controllers/users/passwords_controller.rb
class Users::PasswordsController < Devise::PasswordsController
  before_action :ensure_normal_user, only: :create

  def ensure_normal_user
    if params[:user][:email].downcase == 'guest@example.com'
      redirect_to new_user_session_path, alert: 'ゲストユーザーのパスワード再設定はできません。'
    end
  end
end
```

## 備考

- 上記はあくまで最低限度の実装です。例えば，ゲストユーザーの情報は他の利用者にも引き継がれますので，必要があれば`Trackable`を入れて，`current_sign_in_at`を基準に次のような実装をしてもよいかもしれません。

  - 定期的にデータを初期化する設定を入れる
  - ゲストユーザーを複数用意して，現在ログイン日時の最も古いゲストユーザーでログインする

- 実は，最初，`devise/sessions/new.html.erb`の真似をして，`hidden_field`を加えて……という手順で実装しようとしました。ところが，この方法では`user_email`, `user_password`などのidセレクタが重複するエラーが出てしまいます。そこで，`sign_in`メソッドの存在を思い出し，このような実装方法にたどりつきました。