はじめに
昨日は寝落ちしました。
技術選定
[frontend]
- Nextjs(pages router) => App Routerの理解に苦しんだため
- MUI => 調べたランキングでtopだったため
- react-hook-form => フォーム管理と言ったらこれでは?
- zod => 少しだけ使い慣れてるから
[backend]
- Rails 7 => 使い慣れているため
- devise => 定番だから
- devise-jwt => devise-token-authが古いらしいのでこっち
[github]
認証続き。
もう一回設定を見直してみよう。
昨日からdebugして1行1行見直してたけど、
self.resource = warden.authenticate!(auth_options)
どうやらこいつ入れると、"you need to sign in or sign up before continuing"って出るな。
強引に直したこれだとloginできた。
def create Rails.logger.debug("sign_in_params: #{sign_in_params}") # self.resource = warden.authenticate!(auth_options) self.resource = User.find_by(email: sign_in_params[:email]) Rails.logger.debug("resource: #{resource}") sign_in(resource_name, resource) # json_response = "json形式のレスポンスデータを生成" render json: {"message": resource}, status: :ok end
resourceのとこも要はこういうことやってるんでしょ。知らんけど。
authorizationの値も返ってきてるし、
一旦これで行こうかな。
logoutもさっきのauthorizationの値をheaderに添えて難なくいけてそうだし。
週末、わかる人に聞こう。
ちょっと無理そう。
もう一度登録からログイン、ログアウトまで一連の流れでやってみよう。
登録した。確認メールしないでログインすると
"error": "You have to confirm your email address before continuing."
これが出た。これは期待値。
なので確認メールで確認した後にログインすると
ログインできた。
が、Authorizationのheaderが返ってきてない。
あれ、さっき帰ってきてたのに。
あーそうか。
routes.rbを直したのに、cors.rbのjwt.dispatch_requestsを直してなかったからだ。
sign_inをloginに変えてたのに。
config.jwt do |jwt| jwt.secret = Rails.application.credentials.devise_jwt_secret_key jwt.dispatch_requests = [ ['POST', %r{^/api/v1/users/sign_in$}] ] jwt.revocation_requests = [ ['DELETE', %r{^/api/v1/users/sign_out$}] ] jwt.expiration_time = 1.day.to_i end
Rails.application.routes.draw do if Rails.env.development? mount LetterOpenerWeb::Engine, at: "/letter_opener" end namespace :api, defaults: {format: :json} do namespace :v1 do devise_for :users, controllers: { registrations: 'api/v1/users/registrations', sessions: 'api/v1/users/sessions', confirmations: 'devise/confirmations' } end end get "up" => "rails/health#show", as: :rails_health_check end
OK、コンテナ再起動して、再度ログイン飛ばすと、
やっぱりおk。authorizationにもBearerなんとかが入ってる。
あとはこれをコピーしてlogoutの時にheaderに添えてやれば OK
無事ログアウトできた。
というかやっぱり問題は# self.resource = warden.authenticate!(auth_options)
だったんだ。
これ失敗したら 401のunauthorizedエラーが出た。
ログインに使用した情報は一緒なのに。
実際、このauth_optionsに渡す値がおかしかったのか。
auth_options: {:scope=>:api_v1_user, :recall=>"api/v1/users/sessions#new"}
このscopeとかいうのがなんかおかしそうではある.
なんなんだろう、これ。
まぁいっか。結局返す情報は変わんないんだし、これで進めよう。
あとでわかる人に聞こう。
frontとの繋ぎこみ
ええと何はともあれ一応認証はできたので、
次はfront。
ログイン画面はできているから。ログインができたらそうだな、
適当に画面遷移するようにしようか。
というかfrontとbackは繋げているのかな。
繋ぐにはfrontからapi叩けばいいのか
ということでfrontにaxios入れよう。
入れたのでインスタンス化
const instance = axios.create({ baseURL: 'https://some-domain.com/api/', timeout: 1000, headers: {'X-Custom-Header': 'foobar'} });
こんな感じで。サンプルから少し変えるけど。
まずはfrontから新規登録してみよう。
frontではこういうデータを受け取る。
{ name: 'hoge', email: 'hoge@mail.com', password: 'hogehogeman', passwordConfirmation: 'hogehogeman' }
zodがバリデーションをしてくれるので実際にpostで投げる時は
正常な値がくるとしておk。
ただこれをこのままbackendに投げると問題が2つ。
- スネークケースではない。
- backendは { user: { OOO} }という形式を指定している。
ということなので送る直前に形式を置き換える方針をとる。
1つ目は snake-case - npm を使う。
2つ目はちょっとObject.entriesを使う。他にも方法あるかもだが。
要はobjectのkeyを全部スネークケースにして送れば良い。
この方針でまずはsnake_caseを入れる。
入れたので次。
続いて、配列をobjectに変換する。
少し段階踏んで。
まず目標を確認すると
{ name: 'hoge', email: 'hoge@mail.com', password: 'hogehogeman', passwordConfirmation: 'hogehogeman' }
が
{ user: { name: 'hoge', email: 'hoge@mail.com', password: 'hogehogeman', password_confirmation: 'hogehogeman' } }
こんなになればいい。
色々方法あるみたいだが、【JavaScript(ES6)】配列からオブジェクトへ変換、どの実装が速い? | Qreat 。
せっかくなら一番早いやつで。
const user = {name: 'hoge', email: 'hoge@mail.com', password: 'hogehogeman', passwordConfirmation: 'hogehogeman'} console.log(Object.entries(user)); => こう帰ってくる。 [ [ 'name', 'hoge' ], [ 'email', 'hoge@mail.com' ], [ 'password', 'hogehogeman' ], [ 'passwordConfirmation', 'hogehogeman' ] ]
これにreduce(callbackFn, initialValue)を噛ませればいいので。
export const useSignUp = (data: UserSignUpScheemaType) => { const snakeCaseData = { user: Object.entries(data).reduce( (acc, [key, value]) => ({ ...acc, [snakeCase(key)]: value, }), {} as Record<string, string> ), }; return client.post("/api/vi/users", snakeCaseData); };
こんな感じ。実務のも参考にしながら、こんな感じ。
これでさっきの問題はクリアしたと思う。
const onSubmit: SubmitHandler<UserSignUpScheemaType> = async ( data: UserSignUpScheemaType ) => { try { const newUser = await useSignUp(data); console.log(newUser); router.push("/home"); } catch (err) { console.error(err); router.push("signup"); } };
こんな感じで行けんじゃないの。知らんけど。
今はjwtをheaderに入れて、とかやってない。
送られてるデータはOKそう。
ただ
HTTP parse error, malformed request: #<Puma::HttpParserError: Invalid HTTP format, parsing fails. Are you trying to open an SSL connection to a non-SSL Puma
とか出てる。
httpsで繋ぎに行こうとしてね?というエラーらしい。
axiosのインスタンス作る時にbaseURLにhttp"s"ってつけてた。これじゃね。
リベンジ。
行けたわ。
ようやく登録が終わった。もちろんバリデーションとかまだ甘々だけど。
後々ということで。
次。ログイン。
やることは一緒。
export const UserSignInScheema = z.object({ email: z.string().min(1, "Required").email(), password: z.string().min(1, "Required"), }); export type UserSignInScheemaType = z.infer<typeof UserSignInScheema>; export const useSignIn = (data: UserSignInScheemaType) => { const snakeCaseData = { user: Object.entries(data).reduce( (acc, [key, value]) => ({ ...acc, [snakeCase(key)]: value, }), {} as Record<string, string> ), }; console.log(snakeCaseData); return client.post("/api/v1/users/sign_in", snakeCaseData); };
これ。あとはさっきの届いた認証メールを確認してから、ログインしてみよ。
OKいけた。
レスポンスヘッダに "authorization: Bearer OOO・・・"みたいなの届いてるし
ログインしたuserの情報も帰ってきてる。
def create Rails.logger.debug("sign_in_params: #{sign_in_params}") # self.resource = warden.authenticate!(auth_options) self.resource = User.find_by(email: sign_in_params[:email]) Rails.logger.debug("resource: #{resource}") sign_in(resource_name, resource) # json_response = "json形式のレスポンスデータを生成" render json: {"message": resource}, status: :ok end
こうしてるからそのまんまだけど。
だーーー。疲れたー。
次はなんだ
あとこれ、地味に知らなかった。
このobjectのキーに動的にアクセスする方法
Javascriptのobjectのkeyに変数を使う方法 – Tech Blog
認証が終わったので
どうしようか。というかまだ終わりじゃないか。
ログインはできたからあとは日記の一覧ページに飛ぶ、みたいなところまでやってみよう
なのでまずするべきことは次のとおり。
- backendでdiaries_controllerたるものを作成
- 3つくらい適当にdiary作る
- def index でall を返す
- frontでログイン成功時はheaderのautorizationからものを取り出す。
- それをどこかに保存する。
- その保存したauthorizationを添えて、frontから 3のindexへリクエスト
- allを受け取ってfrontに反映
みたいな感じ
てな感じで作ってみよう。
コントローラは複数形だから
rails g controller api/v1/diaries みたいな感じ?
いけた。
class Api::V1::DiariesController < ApplicationController before_action :authenticate_user! def index @diaries = Diary.all render json: @diaries, status: :ok end end
でこんな感じにして。
あ、Diaryテーブル作らないと。
これなので、モデルは単数系
rails g model Diaryてして、
できました。あとは外部キーつけるのどうやるんだっけ。
あとindexも。
Railsの外部キー制約とreference型について #MySQL - Qiita
class CreateDiaries < ActiveRecord::Migration[7.1] def change create_table :diaries do |t| t.references :user, null: false, foreign_key: true t.string :title, null: false t.string :content, null: false t.timestamps end end end
これで勝手に貼ってくれるみたい。
あと外部キー制約って、このカラムの値は外部のテーブルの値の中からしか使いませんよ、という制約らしい。
色々作り方あるらしいけどこれが一番手っ取り早いそう。
言語化しないといざってときに言葉で出てこないから、これからも続けていこう。
で、rails db:migrateして
UserとDiaryにhas_manyとbelongs_toつけてモデルは終了。
class Diary < ApplicationRecord belongs_to :user end class User < ApplicationRecord include Devise::JWT::RevocationStrategies::JTIMatcher # Include default devise modules. Others available are: # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable devise :database_authenticatable, :registerable,:confirmable, :recoverable, :validatable, :jwt_authenticatable, jwt_revocation_strategy: self has_many :diaries end
てな感じ。
次に3件データ入れるやつをseedで入れてみよう
はい、作り方忘れました。
はいほい。理解。
seed.rbにUser.createとか書いていけばいいのか。
毎回登録ユーザ作るのもだるいし、これを機に作るか。
- 要件は5人ユーザ作って、確認メールをスキップする感じ。
- あとはそれぞれのUserに対してdiaryを3つずつ作る
て感じかな。
なので5.timesとかuser.skip_confirmationとか作っていけばいいのかな
confirmation skipはconfirmed_atにTime.nowとか適当に入れれば確認されたことになるらしい
昔色々死ぬほどやったのにさっぱり忘れてる。
書き始めてみる。
参考にしたもの
deviseでconfirmable設定をした際の確認メールをスキップさせる skip_confirmation! は、対象のオブジェクトをsaveする前に書く #Rails - Qiita
Ruby | timesメソッドやuptoメソッドを使って指定した回数だけ繰り返す
【Ruby】 do...end と {...}の違いでハマった <= これについてはへーという感じ。
devise/lib/devise/models/confirmable.rb at main · heartcombo/devise · GitHub
【Ruby on Rails】一括インサートを行うinsert_allとは - Galapagos Tech Blog
def create_user(i) User.new( name: "user#{i}", email: "user#{i}@mail.com", password: "password#{i}", password_confimation: "password#{i}" ) end def create_diaries(user) 3.times do |j| user.diaries.build( title: "Diary #{j + 1} of #{user.name}", content: "Content for diary #{j + 1} of #{user.name}" ) end end ActiveRecord::Base.transaction do 5.times do |i| user = create_user(i) user.skip_confirmation! puts "user: #{user.attributes}" begin user.save! diaries = create_diaries(user) user.diaries.insert_all!(diaries.map(&:attributes)) puts "Created #{user.name} with #{diaries.size} diaries" rescue ActiveRecord::RecordInvalid => e puts "Failed to create #{user.name}: #{e.record.errors.full_messages.join(', ')}" rescue ActiveRecord::StatementInvalid => e puts "Failed to create diaries for #{user.name}: #{e.message}" end end end
ではltg。失敗。
password_confimatoinでした。rが抜けてた。
リベンジ。
create_diariesのところに3.times.mapつけるの忘れてた。
これでどうだ。
insert_allダメだ。timestampe入らないし、使い方が悪いのかもしれない。
作りなおそ。リファクタ claudeに頼んだけど、やっぱ理解してやらんとダメだ。
def create_user(i) User.new( name: "user#{i}", email: "user#{i}@mail.com", password: "password#{i}", jti: SecureRandom.uuid ) end def create_diary(user, j) user.diaries.build( title: "Diary #{j + 1} of #{user.name}", content: "Content for diary #{j + 1} of #{user.name}" ) end 5.times do |i| user = create_user(i) user.skip_confirmation! if user.save 3.times do |j| diary = create_diary(user,j) if diary.save puts "create #{diary.title}" else puts "failed to create diary#{j + 1}" end end else puts "failed to create user#{i + 1}" end end
一旦これでいいや。いつかみて、あーでもないこーでもないしよう。
動かして、登録されてそうなので、次。
frontでログインしたあと
見出しの付け方が雑だけど
これでデータは入っているので、試しにAPI開発ツールでdiary一覧のapiを叩いてみる。
の前にroutesも登録して、
Rails.application.routes.draw do if Rails.env.development? mount LetterOpenerWeb::Engine, at: "/letter_opener" end namespace :api, defaults: {format: :json} do namespace :v1 do devise_for :users, controllers: { registrations: 'api/v1/users/registrations', sessions: 'api/v1/users/sessions', confirmations: 'devise/confirmations' } resources :diaries end end get "up" => "rails/health#show", as: :rails_health_check end
こんな感じ。これで /api/v1/diaries に対して get叩いたら、一覧が返ってくるはず。
まずは開発ツールでさっき登録したuserでログイン。
indexがずれてたので、i + 1したり j + 1ていうふうに変更。
db:migrate:resetしてコンテナ上げ直して再度ログイン。
で成功したのでレスポンスヘッダのAuthorizationの値をコピって
それを元に/api/v1/diariesのindexにアクセス。
あら、authenticate_user!がundefinedらしい。500が帰ってきた。
名前違ったっけ。
[Rails] undefined method authenticate_user!の対処方法 #Ruby - Qiita
そういえばこんなんあったわ。忘れてた。
今の場合は authenticate_api_v1_user!みたいな感じか。
class Api::V1::DiariesController < ApplicationController before_action :authenticate_api_v1_user! def index @diaries = Diary.all render json: @diaries, status: :ok end end
これでいけるのでは?
リベンジ。
ビンゴ。無事帰ってきた。
いやー長かった。
試しにauthorizationつけずにやるとどうなるか、
無事 401のunauthorizedエラーが出ました。よかった。
えー次です。
ログインが成功したらヘッダー内のAuthorizationを保存、
以降logoutされるまでその値を使って通信するという仕様を作る。
どうやるんだろ。
まぁいいか。今日はここまでかな。
終わりに
今日は夜サッカーして、帰ってきて23時なので終了。
明日には日記のcrud画面作りたい。