Torihaji's Growth Diary

Little by little, no hurry.

初めての個人開発日記 4日目

はじめに

おはようございます。torihaziです。

現在、朝の7時前です。

昨日はdevise-jwtの入りまでで終わったので今日こそは認証を終わらせたいと思います。

技術選定

[frontend]

  • Nextjs(pages router) => App Routerの理解に苦しんだため
  • MUI => 調べたランキングでtopだったため
  • react-hook-form => フォーム管理と言ったらこれでは?
  • zod => 少しだけ使い慣れてるから

[backend]

  • Rails 7 => 使い慣れているため
  • devise => 定番だから
  • devise-jwt => devise-token-authが古いらしいのでこっち

[github]

GitHub - torihazi/diary_front

GitHub - torihazi/diary_back

devise-jwt

昨日はdevise-jwtをcredentials:editで設定したところまで行ったのかな。

ということで続き。

次は何したらいいんだろうか。

user.rbにこれを追記するらしい。

これを書くことでどのモデルでjwt認証を可能にするかを設定できる。

class User < ApplicationRecord
  devise :database_authenticatable,・・・・
         :jwt_authenticatable, jwt_revocation_strategy: Denylist => これ。
end

続いて、htmlリクエストにおいてhtmlヘッダーのAuthorizationというものを

許可しなきゃならないということなので 、initilizers/cors.rbにおいて

      headers: %w(Authorization),
      methods: :any,
      expose: %w(Authorization),

rails generate model jwt_denylist jti:string exp:datetime
rails db:migrate

しようとしたけど、

/app/app/models/user.rb:6:in `<class:User>': uninitialized constant User::Denylist (NameError)

         :jwt_authenticatable, jwt_revocation_strategy: Denylist

て出た。順序が逆らしい。

ということでtable作ってからやったらいけた。

なんか大変だ。これはあとでよく整理しないと。

ただ英語読めたらバッチリ書いてあるから。英語がところどころ読める感じは甘いのかな。

ええとその後にさっき作ったjwt_denylist.rbにこれを書いて

class JwtDenylist < ApplicationRecord
  include Devise::JWT::RevocationStrategies::Denylist
  self.table_name = 'jwt_denylist'
end

これの名前を変えて

  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable,
         :jwt_authenticatable, jwt_revocation_strategy: JwtDenylist <= これね。Jwtをつけた・
end

でconfig/initializers/devise.rbに

Devise.setup do |config|
  # ... 他の設定 ...
  config.jwt do |jwt|
    jwt.secret = Rails.application.credentials.devise_jwt_secret_key!
    
    # ログイン(トークン発行)のリクエスト
    jwt.dispatch_requests = [
      ['POST', %r{^/users/sign_in$}]
    ]
    
    # ログアウト(トークン無効化)のリクエスト
    jwt.revocation_requests = [
      ['DELETE', %r{^/users/sign_out$}]
    ]
    
    jwt.expiration_time = 1.day.to_i
  end
end

書けば、いけるのかな?

試しにリクエストを投げてみよう

docker compose up -dであげて、

パスは/usersにPOSTで投げたらいいらしいので。

あーでもあれか。controller変えたからコントローラも新しく作らないといけないのかな・

でもどうやって認識させるんだろうか。

controllerの作り方はdeviseの方に書いてあるけど。

あれ、元々deviseでuserを新しく作るには?

デフォルトだと、/usersにpostメソッドでパラメータ添えて送ったら作られるんだよね。

なんでかっていうと/usersにpostsで送ると/devise/registrations#createに送られるから。

ということは?

何すればいいんだ。

まぁいいや。まずregistrationsのcontrollerを作るか。

いや作る必要ないのか。

やるのはあのストロングパラメータ追加したコントローラをdeviseが継承するようにってこと。

なぜそれをやらないといけないかというと、

デフォルトだと usersにpostで投げても、passwordとpassword_confirmationとかしかパラメータを受け付けないから、

追加するにはapplication_controller.rbにさらに記述をする必要がある。

そのためapplication_controller.rbに書きたいが

apiモードでやっているのでApplicationControllerはApplicationController::APIを継承している

このAPIはApplicationController::Baseの軽量版である。しかしDeviseはBaseを推奨している。

だから新しくBaseを継承したものを作る必要がある。(のかな?)

そのため新しくそのBaseを継承したcontrollerを作ってそれをdeviseに読み込ませるようにしたい。

話は戻るがdeviseはデフォルトでApplicationControllerを継承するらしい。

class DeviseController < Devise.parent_controller.constantize

Devise.parent_controllerとかいうのがinitilizers/devise.rbにおいてコメントアウトされている

# config.parent_controller = 'DeviseController'

だからこれをさっき新しく作ったもので置き換えて

config.parent_controller = 'Api::V1::BaseController'

こんな感じで書けば OKのはず。 (BaseControllerに名前書き直した。)

module Api
  module V1
    class BaseController < ApplicationController::Base
      before_action :configure_permitted_parameters, if: :devise_controller?

      protected
      def configure_permitted_parameters
        devise_parameter_sanitizer.permit(:sign_up, keys:[:name, :email, :password, :password_confirmation])
      end
    end
  end
end

こうすることでdevise、DeviseControllerはもうこの子を継承するようになり、それに追随して

registrations_controller、sessions_controllerとかも変わって万事OK。であるはず。と推測。

あとはroutes.rbにおいて

Rails.application.routes.draw do

  namespace :api do
    namespace :v1 do
      devise_for :users
    end
  end

  get "up" => "rails/health#show", as: :rails_health_check

end

こんな感じでよくあるふうに変えれば

/usersに投げればよかったものが/api/v1/usersになる。

これでカスタムコントローラとか作らんくて良くなったはず。

ということで役者は揃ったのでAPI開発ツールでテスト。

使うのはこれ。

https://chromewebstore.google.com/detail/talend-api-tester-free-ed/aejoelaoggembcahagimdiliamlcdmfm

一回、dockerを再起動させて、ltg。

無事にエラーとかなく立ち上がったのを確認。

緊張の瞬間。

あれ。404だ。dockerの方にもログは上がってない。

port間違えた?あーcompose.ymlで3001にしてたから?

あーそーだ。

dockerにも行っているからとりあえずリクエストとしては届いる。

Api::V1::RegitsrationControllerなんてないよ、ということだが。

ルーティングをよく理解していない?

そもそもルーティングは

Railsルーターは受け取ったURLを認識し、適切なコントローラ内アクションやRackアプリケーションに振り分けます

Rails のルーティング - Railsガイド

だから?えーと。

元々deviseを入れると自動的に

Devise::RegistrationsController
Devise::SessionsController
Devise::PasswordsController

とかが使えるようになって、最初のroutingに書かれてたdevise_for :usersだと

POST   /users(.:format) devise/registrations#create

ということは /api/v1/usersにpostで投げれば devise/registrations#createに処理が渡されるようにすればいい。

で今のroutingだと、

Rails.application.routes.draw do
  namespace :api do
    namespace :v1 do
      devise_for :users
   end
  end

  get "up" => "rails/health#show", as: :rails_health_check
end

------------------------------------------------------------
POST   /api/v1/users(.:format) api/v1/registrations#create

で向き先のcontrollerがおかしいから怒られている、と理解。

ということは /api/v1/usersにpostで投げれば devise/registrations#createに処理が渡されるようにすればいい。

これを実現するには次のように書く必要があると。

Rails.application.routes.draw do

  namespace :api do
    namespace :v1 do
      devise_for :users, controllers: {
        registrations: 'devise/registrations',
        # 他のDeviseコントローラーも必要に応じて
      }    
    end
  end

  get "up" => "rails/health#show", as: :rails_health_check

end

------------------------------------------------------------
POST   /api/v1/users(.:format) devise/registrations#create

Method: ActionDispatch::Routing::Mapper#devise_for — Documentation for heartcombo/devise (main)

これでいけるのでは?

dockerをリスタートして再度リベンジ。

およ?

500error.

"exception": "#<NameError: uninitialized constant Api::V1::BaseController>",

あーbaseコントローラのファイル名がおかしかった

で再チャレンジ。

NameError (uninitialized constant ApplicationController::Base):

。。。

調べたところ APIモードでは Baseはない?みたい。

よくわかんなくなってきた。ええととりあえず、戻そう。

class ApplicationController < ActionController::API

  before_action :configure_permitted_parameters, if: :devise_controller?

  protected
  def configure_permitted_parameters
    devise_parameter_sanitizer.permit(:sign_up, keys:[:name, :email, :password, :password_confirmation])
  end

end

devise.rbのparent_controllerをコメントアウトする。

  # config.parent_controller = 'Api::V1::BaseController' <= これを消す。

でルーティングはこのまま

Rails.application.routes.draw do

  namespace :api do
    namespace :v1 do
      devise_for :users, controllers: {
        registrations: 'devise/registrations',
        sessions: 'devise/sessions'
        # 他のDeviseコントローラーも必要に応じて
      }    
    end
  end

  get "up" => "rails/health#show", as: :rails_health_check

end

でdockerリスタート。

で投げてみるか。

ていうかActionController::APIじゃん。継承してたの。

結果は422。もういいじゃん。登録させてよ。

一旦調べ直し。

Dakota Lee Martinez

よく公式ドキュメントを最初から読んでみる。

Configuring devise for APIs · waiting-for-dev/devise-jwt Wiki · GitHub

これを中途半端にやっていたかもしれないので見直す。

最初。Responding to json

class ApplicationController < ActionController::API
  include ActionController::MimeResponds
  respond_to :json
  before_action :configure_permitted_parameters, if: :devise_controller?

  protected
  def configure_permitted_parameters
    devise_parameter_sanitizer.permit(:sign_up, keys:[:name, :email, :password, :password_confirmation])
  end

end

次。Overriding controllers。これは一旦なしで。

次。Defaulting to json as format。これは意味あるのかな?

ということで

Rails.application.routes.draw do

  namespace :api do
    namespace :v1 do
      devise_for :users, controllers: {
        registrations: 'devise/registrations',
        sessions: 'devise/sessions'
        # 他のDeviseコントローラーも必要に応じて
      },defaults: { format: :json }
    end
  end

  get "up" => "rails/health#show", as: :rails_health_check

end

docker をrestartしてリクエスト投げてみる。

{
  "name":"hoge1",
  "email":"hoge3@mail.com",
  "password": "password",
  "password_confirmation": "password",
}

422だけど、なんか帰ってきた。

てかあれ、nameとか追加されてないじゃん。

てか、apiモードでrespond_toメソッド使うには、ていうのが調べたらあった。

RailsのAPIモードでrespond_to を使うにはMimeRespondsをincludeする必要がある - その辺にいるWebエンジニアの備忘録

少し進んだ。。公式ドキュメントを読み飛ばすとえらい目に遭う。

やっぱり最初に見るべきは公式ていうのは本当らしい。

少し休憩。

ただいま11:23

再開。

というかなんでこんなerrrosなんて返ってきたのか。

コードを見てみる。

  # POST /resource
  def create
    build_resource(sign_up_params)

    resource.save
    yield resource if block_given?
    if resource.persisted?
      if resource.active_for_authentication?
        set_flash_message! :notice, :signed_up
        sign_up(resource_name, resource)
        respond_with resource, location: after_sign_up_path_for(resource)
      else
        set_flash_message! :notice, :"signed_up_but_#{resource.inactive_message}"
        expire_data_after_sign_in!
        respond_with resource, location: after_inactive_sign_up_path_for(resource)
      end
    else
      clean_up_passwords resource
      set_minimum_password_length
      respond_with resource
    end
  end

ここに処理が渡っていることは確か。

resourceはActiveRecordオブジェクトだと思うから、persisted?で保存できてないのでそこの

ifから飛んで結局はrespond_with resourceか。

でそのActiveRecordオブジェクトのバリデーションエラーががrespond_withされただけか。

sign_up_paramsを覗きに行きたい。

でこうするにはカスタムコントローラを作るしかない。

ということで作る。

rails generate devise:controllers [scope]

rails generate devise:controllers users -c registrationsで。

controllers/users/registrations_controller.rbができるから。

class Api::V1::Users::RegistrationsController < Devise::RegistrationsController
  before_action :configure_sign_up_params, only: [:create]

~~
  def create
    super
  end

  private
  def configure_sign_up_params
    devise_parameter_sanitizer.permit(:sign_up, keys:[:name, :email, :password, :password_confirmation])
  end
~~~
end

てしたら?ダメだ。

なぜ?

というかこのsign_up_paramsをparams.require[:user].permitてすればいけると思うんだけどどうだろう。

うん。やっぱりいけた。

でもそしたらなんで公式ドキュメントのものがダメだったんじゃ。

あと jti の notnullviolationが出てる。

詰み。

今、14:14。


現実逃避してた。今、14:48。

もう一回見直してみる。

DenylistでやっていたのにJTIMathcerで指定されていたUserモデルにjtiカラムを追加してたから

nullViolationが出ていたと思う。ということでdenylistでやり直す。

rollbackして最初から。

migration user、jwt_denylistのファイル

# frozen_string_literal: true

class DeviseCreateUsers < ActiveRecord::Migration[7.1]
  def change
    create_table :users do |t|
      ## Database authenticatable
      t.string :email,              null: false, default: ""
      t.string :encrypted_password, null: false, default: ""
      
      ## Recoverable
      t.string   :reset_password_token
      t.datetime :reset_password_sent_at
      
      ## Rememberable
      t.datetime :remember_created_at
      
      ## Trackable
      # t.integer  :sign_in_count, default: 0, null: false
      # t.datetime :current_sign_in_at
      # t.datetime :last_sign_in_at
      # t.string   :current_sign_in_ip
      # t.string   :last_sign_in_ip
      
      ## Confirmable
      t.string   :confirmation_token
      t.datetime :confirmed_at
      t.datetime :confirmation_sent_at
      t.string   :unconfirmed_email # Only if using reconfirmable
      
      ## Lockable
      # t.integer  :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts
      # t.string   :unlock_token # Only if unlock strategy is :email or :both
      # t.datetime :locked_at
      
      t.string :name, null: false
      t.timestamps null: false
    end

    add_index :users, :email,                unique: true
    add_index :users, :reset_password_token, unique: true
    add_index :users, :confirmation_token,   unique: true
    # add_index :users, :unlock_token,         unique: true
  end
end
class CreateJwtDenylists < ActiveRecord::Migration[7.1]
  def change
    create_table :jwt_denylist do |t|
      t.string :jti, null: false
      t.datetime :exp, null: false
    end
    add_index :jwt_denylist, :jti
  end
end

終わったので rails db:migrate

class JwtDenylist < ApplicationRecord
  include Devise::JWT::RevocationStrategies::Denylist

  self.table_name = 'jwt_denylist'
end

書いて、user.rbに

class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable,
         :jwt_authenticatable, jwt_revocation_strategy: JwtDenylist
end

でいいのかな。

ていうか今頃気づいたけど、このカスタムコントローラ作った時にあった

  def configure_sign_up_params
    devise_parameter_sanitizer.permit(:sign_up, keys: [:name, :email, :password, :password_confirmation])
  end

これってあってるの?configure_sign_up_paramsなんて公式に書いてないと思う。

もういいや。これで設定できないんじゃね?知らんけど。

def sign_up_params
    params.require[:user].permit(:name, :email, :password,:password_confirmation)
end

でいいや。

で次。

いろいろ右往左往して。登録はできたけど、セッション関連で怒られる。

登録ができるまでの道筋

このまま書き続けると永遠に終わらなさそうなので、この形式にする。

現状はroutes.rbは

  namespace :api, defaults: {format: :json} do
    namespace :v1 do
      devise_for :users, controllers: { 
        registrations: 'api/v1/users/registrations' 
      }
    end
  end

にして、api/v1/users/registrations_controller.rbに

class Api::V1::Users::RegistrationsController < Devise::RegistrationsController

  def create
    super
  end

    def sign_up_params
    params.require(:user).permit(:name, :email, :password, :password_confirmation)
  end

にして

これで投げたら、

ActionDispatch::Request::Session::DisabledSessionError: Your application has sessions disabled. To write to the session you must first configure a session store>

みたいな感じになった。

で、rails cしてみると、User自体は登録はできているらしい。

ただ、セッション周りのエラーが起きている、という状況。

DeviseはWardenに依存し、Wardenはセッションを必要とする。

で今、apiモードっていうセッション使わないモードでセッション使おうとしてるからエラーになってるらしい。

ということでこれやってみる。

https://github.com/heartcombo/devise/issues/5443

controllers/concerns/rack_session_fix.rbという名前で

module RackSessionFix
  extend ActiveSupport::Concern

  class FakeRackSession < Hash
    def enabled?
      false
    end
  end

  included do
    before_action :set_fake_rack_session_for_devise
  end

  private

  def set_fake_rack_session_for_devise
    request.env['rack.session'] ||= FakeRackSession.new
  end
end

でregistrations_controller.rbに

class Api::V1::Users::RegistrationsController < Devise::RegistrationsController
  include RackSessionFix
~~

こんな感じで追加。

コンテナリスタートして再チャレンジ。

やったーーーー~~~~~~~~~~~~~~~~~~~~~~~~ーーーーーーーーー。

ただいま 16:42。

でもどうして responseヘッダにAuthorizationのアレが出るんだろう。

まぁいいや。

もう疲れた。しかもauthorizationて小文字だし。

ということで、登録はできたので次はログイン。

ログインは普通にログインすればいいのかな。

でも疲れたから、一旦運動してこよ。

今、16:54。

かいし.18:16。

rails g devise:controllers api/v1/users -c sessions

これでもいけるっぽい。直接api/v1/users下に作られる。

class Api::V1::Users::SessionsController < Devise::SessionsController
  include RackSessionFix
~~~
  # POST /resource/sign_in
  def create
    super
  end

  def sign_in_params
    params.permit(:user).permit(:email, :password)
  end
end

こんな感じ。

これでいけるかな?

401のunauthorizedだ。

あれか。認証メール見てないからか?

確認メールの設定をするためにletter_openerを入れる。

gem letter_opener を試してみる #Rails - Qiita

これで入れた。

ということで、この状態でユーザ登録して確認メールがいけばおk

でこのconfirm とかいう下のリンククリックして、

うまく行くはずだがいかず。

試しにconsoleからuserのconfirmed_atにTime.nowを入れてみる

そしてログインしたらうまく行くんでは?

だめ。もー。なんでよー。

ただいま現実逃避中」。繰り返す。現実逃避中。

21:14。

{
    "error": "You need to sign in or sign up before continuing."
}

これ出るんだけどなんあん。

unauthenticatedということらしい。devise_en.ymlに書いてあった。

んー。なんかdeviseで詰まってね。jwt云々の前に。

あれ、こんな詰まったっけ。昔触ったことあったんだけど。

終わりに

あら、終わらんかった。

deviseの理解が酷い、ということに気づいた。

なんでストロングパラメータ機能しなかったんだろ。

ま、いいや。