Torihaji's Growth Diary

Little by little, no hurry.

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

はじめに

昨日は寝落ちしました。

技術選定

[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

認証続き。

もう一回設定を見直してみよう。

昨日から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

認証が終わったので

どうしようか。というかまだ終わりじゃないか。

ログインはできたからあとは日記の一覧ページに飛ぶ、みたいなところまでやってみよう

なのでまずするべきことは次のとおり。

  1. backendでdiaries_controllerたるものを作成
  2. 3つくらい適当にdiary作る
  3. def index でall を返す
  4. frontでログイン成功時はheaderのautorizationからものを取り出す。
  5. それをどこかに保存する。
  6. その保存したauthorizationを添えて、frontから 3のindexへリクエス
  7. 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画面作りたい。