はじめに
昨日は寝落ちしました。
技術選定
[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画面作りたい。