[ Rails ] LINEアカウントIDを取得して個人アプリのユーザーIDと連携させる

<h3>はじめに</h3>
こんにちは！<br>
前回の記事の続きになりますが、LINEアカウントIDを取得して個人アプリのユーザーIDと連携させる方法についてまとめていきます。<br>
前回記事はこちら

https://qiita.com/CarBoss1996/items/e619335206076301e00f

追記：今回の連携方法はワンタイムコードを用いての方法ですが、LINEログインを実装するとログイン時にuidが送られてきて、そのuidがline_user_idとなるので、今回のこの実装がより簡単になります。<br>

<h3>開発環境</h3>
・ Ruby on Rails  7.1.3.4<br>
・ Ruby 3.4.4


<h3>完成物</h3>
今回のアプリでは、ユーザーが「通知設定」とメッセージを送ると、ワンタイムコードとリダイレクトURLが返されます。

![IMG_4673.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3871424/6967780e-eb50-e5fa-a6e1-758f88143bfd.jpeg)


<h3>手順</h3>
1. ワンタイムコードの生成<br>
2. リダイレクトURLの作成<br>
3. IDとワンタイムコードをパラメータに含めたままログイン<br>
4. ワンタイムコードの検証と連携完了<br>

<h3>1. ワンタイムコードの生成</h3>
ユーザーが公式LINEに「通知設定」と送信すると、時間制限付きのワンタイムコードが生成されます。<br>
これによりセキュリティが向上し、データの整合性も保たれます。 <br>
今回は、10分で有効期限が切れる4桁のランダムコードを生成します。<br>

<h4>1-1. マイグレーションファイルの作成</h4>
まずはワンタイムコードを保存できるようにマイグレーションファイルを作成します。
ターミナルで以下のコマンドを実行します。

```
rails g migration CreateOneTimeCodes user_id:bigint code:integer expires_at:datetime
```
これでワンタイムコードテーブルにコードと有効期限を保持するカラムが作成されました。

usersテーブルにもLINEのユーザーIDを保持できるようにカラムを追加します。
ターミナルで以下のコマンドを実行します。

```
rails generate migration AddLineUserIdToUsers line_user_id:string
```
そしてマイグレートします。
```
rails db:migrate
```

これでマイグレーションファイルの作成は完了しました。

<h4>1-2. メソッドの作成</h4>
ワンタイムコードのモデルを作成して、コードを生成するメソッドを記述します。

```one_time_code.rb
class OneTimeCode < ApplicationRecord
  belongs_to :user

  validates :code, presence: true
  validates :expires_at, presence: true

  def generate_unique_code(user_id)
    # 一意の識別コードを生成するロジック...
    # 1000から9999の間のランダムな整数を生成します：
    code = rand(1000..9999)

    # 有効期限を設定します。この例では、コードの有効期限を10分後に設定します：
    expires_at = 10.minutes.from_now

    # 一意のコードとその有効期限をデータベースに保存します：
    # 10分後に有効期限が切れるOneTimeCodeを作成
    OneTimeCode.create(user_id: user_id, code: code, expires_at: Time.current + 10.minutes)

    code
  end
end
```

コントローラを修正、以下を記述します。

```notifications_controller.rb
class NotificationsController < ApplicationController
  # 省略
  # 以下のメソッドを追記
  private

  def user_params
    params.require(:user).permit(:unique_code)
  end

  def handle_one_time_code
    if @one_time_code.code == params[:user][:unique_code].to_i && @one_time_code.expires_at && @one_time_code.expires_at > Time.now
      link_line_account_or_redirect
    elsif @one_time_code.expires_at.nil? || @one_time_code.expires_at <= Time.now
      flash.now['danger'] = 'ワンタイムコードの有効期限が切れています。'
      render 'notifications/link_line_account', status: :unprocessable_entity
    end
  end
  # ここまで

  # 以下のメソッドを修正
  def handle_message_event(event)
  received_text = event['message']['text']
    line_user_id = event['source']['userId']

    unless received_text == '連携'
      message = {
        type: 'text',
        text: "アカウント連携をしたい場合は、「連携」とメッセージを送ってください。"
      }
      return client.reply_message(event['replyToken'], message)
    end
    
      unique_code = SecureRandom.hex(10)  # ここを追加

     # 以下を修正
      messages = [
      {
        type: 'text',
        text: "あなたの一意の識別コードは #{unique_code} です。アプリケーションでこのコードを入力してください。このコードの有効期限は１０分です。"
      },
      {
        type: 'text',
        text: "認証を完了するには、次のリンクをクリックしてください。"
      }
    ]
    return client.reply_message(event['replyToken'], messages)
  end

  def handle_post_request
    return redirect_with_alert unless params[:user]

    @one_time_code = OneTimeCode.find_by(code: params[:user][:unique_code].to_i)
    return render_with_danger if @one_time_code.nil?

    handle_one_time_code
  end
  
  def handle_one_time_code
    if @one_time_code.code == params[:user][:unique_code].to_i && @one_time_code.expires_at && @one_time_code.expires_at > Time.now
      link_line_account_or_redirect
    elsif @one_time_code.expires_at.nil? || @one_time_code.expires_at <= Time.now
      flash.now['danger'] = 'ワンタイムコードの有効期限が切れています。'
      render 'notifications/link_line_account', status: :unprocessable_entity
    end
  end
end
```
これでワンタイムコードを作成するロジックが完了しました。

<h3>2. リダイレクトURLの作成</h3>
LINEからアプリに戻るためのリダイレクトURLを作成します。

```notifications_controller.rb
 def handle_message_event(event)
 # 省略
    uniquw_code = SecureRandom.hex(10)
    # 以下を追加
    redirect_url = "https://kousiennow.onrender.com/notifications/link_line_account?line_user_id=#{line_user_id}&unique_code=#{unique_code}"

 # 省略
 end
```
リダイレクトURLにLINEのユーザーIDと生成されたワンタイムコードをパラメータとして乗せて、アプリにそれらの情報を送信できるようにしています。
ですが、このリダイレクトURLをクリックするとログインを必要とする場合があります。
アカウント連携の際にはユーザーがログインしていることが必須なので、リダイレクトしてからログイン画面に飛ばされると、このままではパラメータのIDとワンタイムコードが失われてしまいます。
なのでその事態を防ぐために、ログイン後パラメータの値も含めたまま該当のページにリダイレクトする設定をしていきます。

<h3>3. IDとワンタイムコードをパラメータに含めたままログイン</h3>
実はこれはすでに記事にまとめてあります。

https://qiita.com/CarBoss1996/items/f5907febefcaba013d64

上記の記事のように、application_controller.rb に以下を記述していきます。

```application_controller.rb
class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception
  before_action :configure_permitted_parameters, if: :devise_controller?
  add_flash_types :success, :danger

  def authenticate_user!
    if user_signed_in?
      super
    else
      session[:user_return_to] = request.fullpath  # 現在のURL(パラメータを含む)をセッションに保存
      redirect_to new_user_session_path, :notice => 'ログインしてください。'
    end
  end

  private

  def after_sign_in_path_for(resource)
    session.delete(:user_return_to) || root_path   # セッションからURLを取り出し、そのURLにリダイレクト
  end
end
```
ログインページにリダイレクトする前のURL（パラメータを含む）をセッションに保存し、ログイン後にそのURLへリダイレクトするようになっています。


<h3>4. ワンタイムコードの検証と連携完了</h3>
notifications_controllerに以下を追記して、ワンタイムコード検証して、連携完了です！

```notifications_controller.rb
class NotificationsController < ApplicationController
   # 省略
  def link_line_account
    @user = User.find(current_user.id)

    return handle_get_request if request.get?
    return handle_post_request if request.post?
  end

    private

   # 省略
  def handle_get_request
    @one_time_code = OneTimeCode.find_by(code: params[:unique_code].to_i)
    @unique_code = params[:unique_code]
  end

  def handle_post_request
    return redirect_with_alert unless params[:user]

    @one_time_code = OneTimeCode.find_by(code: params[:user][:unique_code].to_i)
    return render_with_danger if @one_time_code.nil?

    handle_one_time_code
  end

  def redirect_with_alert
    flash[:alert] = 'フォームを正しく送信してください。'
    redirect_to link_line_account_notifications_path
  end

  def render_with_danger
    flash.now['danger'] = 'ワンタイムコードが見つかりません。'
    render 'notifications/link_line_account', status: :unprocessable_entity
  end

  def handle_one_time_code
    if @one_time_code.code == params[:user][:unique_code].to_i && @one_time_code.expires_at && @one_time_code.expires_at > Time.now
      link_line_account_or_redirect
    elsif @one_time_code.expires_at.nil? || @one_time_code.expires_at <= Time.now
      flash.now['danger'] = 'ワンタイムコードの有効期限が切れています。'
      render 'notifications/link_line_account', status: :unprocessable_entity
    end
  end

  def link_line_account_or_redirect
    if @user.line_user_id.nil?
      @user.line_user_id = params[:line_user_id]
      if @user.save
        flash[:notice] = 'LINEアカウントが正常にリンクされました。'
        redirect_to profile_path
      else
        flash[:alert] = 'LINEアカウントのリンクに失敗しました。'
        render 'notifications/link_line_account'
      end
    else
      flash[:notice] = 'LINEアカウントは既にリンクされています。'
      redirect_to profile_path
    end
  end
   # 省略
end
```
link_line_account で、ユーザーがLINEアカウントをアプリケーションのアカウントにリンクするためのエンドポイントを設定しています。
handle_get_request は、ユーザーがリダイレクトURLを使用してアプリに戻ってきたときに呼び出されます。
リダイレクトURLのパラメータに含まれていたワンタイムコードを取得し、それをデータベースから検索します。
そして、ユーザーが入力した値とデータベースのワンタイムコードが正しいかどうかを、handle_post_request で検証します。
検証の内容は、データベースにそのコードが存在する場合、コードが正しいか、有効期限が切れていないかを確認します。
そしてこれらの条件が満たされている場合、ユーザーのアカウント連携が完了します。

お疲れ様でした！

<h3>最後に</h3>
最後までお読みいただきありがとうございます！<br>
アプリからLINEにパラメータをつけてユーザーIDを送っても受け取ることができないとCopilotに言われたので、LINEからLINEのユーザーIDをつけてアプリにリダイレクトさせるようにしました。<br>
ワンタイムコードを使用することでセキュリティの向上も見込めて、簡単に連携が完了するのでLINEアカウント連携を考えている方はぜひやってみてください！<br>
備忘録ですが、参考になれば幸いです。<br>
初学者ゆえ、誤解や説明不足があるかもしれませんが、ご指摘いただければ幸いです。