React + Rails初心者の歩み 〜初級編：超基礎アプリ開発④〜

# はじめに
:::note info
前回2本立てと言いましたが、想像以上に内容が長くなったので、分割します。
とぅみまてぇん。
:::
今回は問題集：初心者編の、
「Userモデルを作成して、email, password_digestを持たせる」
を進めていきます。

https://qiita.com/tukaji/items/649d7dc3e0d43d597250

# Userモデルを作成
RailsでUserモデルを作成します。

```
# Userモデルを作成（初期テーブルにemailとpassword_digestを追加）
rails g model User email:string password_digest:string

# DBに反映
rails db:migrate
```

Userモデルを作成したら、パスワードの暗号化とバリデーションを設定していきます。

```
# app/models/user.rb
class User < ApplicationRecord
  has_secure_password
  validates :email, presence: true, uniqueness: true
end
```
1つ1つ解説していきます。

## パスワードを暗号化
ユーザーのパスワードを安全に守るためにハッシュ化します。

### パスワードのハッシュ化について
ハッシュ化とは一方向性の変換です。
要は元のパスワードを全く別のものに変換することです。
なぜパスワードをハッシュ化する必要があるのか。
#### 元のパスワードをわからなくして漏洩の被害を減らすため
もしデータベースが漏洩したときに、パスワードがそのまま保存されていたら不正アクセスし放題になります。
```
password: password123
```
それを防ぐためにハッシュ化で元のパスワードをわからなくすることで、
被害を減らすことができます。
```
元のパスワード: "password123"
ハッシュ化: "$2a$12$uYzqsf0cHF...GxHgGOzUwG9xNQ7K8mWT2ka"
```
ハッシュ化したものから元のパスワードを求めるのは非常に困難になります。

### has_secure_password
これを使うことで次の機能が有効になります。
* passwordおよびpassword_confirmationという仮想属性が追加される。
（実際のカラムには存在しない）。
* password_digestというカラムに、passwordのハッシュを保存する。
* パスワードのバリデーション機能（例：空でないこと、確認との一致など）
* authenticateメソッドが追加され、パスワードの検証ができる。

#### authenticateとは？
認証用のメソッドで、主に「入力されたパスワードが正しいかどうか」を確認するためのもの。
```
user = User.find_by(email: "test@example.com")
if user&.authenticate("入力されたパスワード")
  # 認証成功
else
  # 認証失敗
end
```
また、has_secure_password（= BCrypt）は毎回異なるソルト（salt）というランダムデータを混ぜてハッシュ化します。
どういうことかというと、複数のユーザーが同じパスワードを使って登録しても、ハッシュ化の結果はランダムとなり被ることはありません。
これによりハッシュ化したパスワードが漏洩しても同じパスワードを使っていることがバレることはありません。
```
3人の元のパスワード: "password123"
1人目ハッシュ化: "$2a$12$uYzqsf0cHF...GxHgGOzUwG9xNQ7K8mWT2ka"
2人目ハッシュ化: "a$sld&ashdioh...gpjoaspojaspdojaspdjoakjsdojka"
3人目ハッシュ化: "&asdkjas234#...sdpgihjsdighsoiddfsplrkqweq34"
```
そして、ユーザーがログインするときは、入力されたパスワードを登録したときと同じ方法でハッシュ化することで、データベース内のハッシュ化パスワードと比較します。

## バリデーションの設定
### validates :email, presence: true, uniqueness: true
emailカラムに対してバリデーションの設定をしています。
パスワードはhas_secure_passwordで自動的にバリデーション設定されているので、記述する必要はなし。
* presence: true
空でないこと。（空文字やnilを許容しない）
* uniqueness: true
他のユーザーと重複していないこと。

:::note warn
uniquenessはアプリケーションレベルでの検証です。厳密な一意性を保証するためには、DBのインデックスにユニーク制約を加えることも重要です。
By GPTえもん
:::

これでUserモデルの作成完了！
次にUserコントローラーを作成します。

# Userコントローラーを作成
RailsでUserコントローラーを作成します。

```
rails g controller Users
```
作成したUserコントローラーに、会員登録機能を実装するための記述を書いていきます。
```
# app/controllers/users_controller.rb

class UsersController < ApplicationController
  def create
    user = User.new(user_params)
    if user.save
      session[:user_id] = user.id
      render json: {status: 'created',user: user}
    else
      render json: {erroes: user.errors.full_message},status: 422
    end
  end

  private

  def user_params
    params.require(:user).permit(:email,:password,:password_confirmation)
  end
end
```
1つ1つ解説していきます。

## create
### user = User.new(user_params)
これで先ほど作成したUserモデルの新しいインスタンスを作成。
「user_params」は許可されたパラメータ（例: email, password, name など）をハッシュで返すプライベートメソッド（private以下の記述）
Strong Parameters を使って安全にパラメータを受け取っています。
「user_params」についてはのちほど。

### if user.save
バリデーションを通過して、ユーザーがデータベースに保存できたかを確認。
成功していれば中の処理に進む。

### session[:user_id] = user.id
セッションに登録したユーザーIDを保存して、
ログイン状態を作成します。
Railsはsessionを使ってログイン中のユーザーを識別します。

### render json: { status: 'created', user: user }
ユーザー作成成功時に、JSON形式で「status」と作成された「user」オブジェクト全体を返しています。

### render json: { errors: user.errors.full_message }, status: 422
ユーザー作成に失敗した場合の処理です。
ステータスコード「422」は「Unprocessable Entity（処理できないエンティティ）」で、バリデーションエラー時によく使います。

## sessionについて
RailsをAPIモードで作成した場合、セッション（session[:user_id]など）をデフォルトで使うことはできません。
なぜなら、APIモードは「JSON APIに特化した軽量なモード」であり、Cookieやセッションなどの機能を省略しているからです。
その為、Cookieやセッションを有効化する必要があります。

```
# config/application.rb

class Application < Rails::Application
    # 以下を追加
    config.middleware.use ActionDispatch::Cookies
    config.middleware.use ActionDispatch::Session::CookieStore, key: '_your_app_session'
end
```

```
# config/initializers/cors.rb

Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins "http://localhost:5173"
    resource "*",
      headers: :any,
      credentials: true, # 超大事！必ずtrueにする
      methods: [:get, :post, :put, :patch, :delete, :options, :head]
  end
end
```
セッションについては以上！

## 「private」と「user_params」
```
private

def user_params
    params.require(:user).permit(:email,:password,:password_confirmation)
end
```

### private
private（プライベートメソッド）とは、クラスの内部だけで使えるメソッドのことです。
他のクラスやコントローラ、ビューなどから直接呼び出すことができないようにすることで、プログラムの安全性や設計の明確さを保ちます。
#### なぜ「プライベート」にするのか？
目的は「隠す」ことです。
* 内部的な処理のみに使うメソッドを外部からアクセスできないようにして、安全性や保守性を高める。
* 開発者に「これは外から触るべきじゃないメソッドですよ」と示す。

「private」以降に定義されたメソッドはすべてプライベートになります。

### user_params
このuser_paramsはStrong Parametersを使用しています。
#### Strong Parameters（ストロングパラメータ）とは？
Strong Parametersとは、RailsでフォームやAPIから送られてきたパラメータの中から、許可されたものだけを取り出す仕組みです。
Rails4以降で導入されたセキュリティ機能で、「マスアサインメント脆弱性」からアプリを守るために使われます。

#### なぜ必要なのか？
```
user = User.new(params[:user])
```
上記のようなコードの場合、外部から送られてきたデータに、
例えば「admin:true」といったような権限が付与されていた場合、
誰でも管理者になれてしまいます。
そういった渡したくない情報がある場合、
渡す情報を制限することで安全な操作になります。
今回だと、:email, :password, :password_confirmationの3つを許可しています。

# routes
次にroutesを記述します。
```
# config/routes.rb
post "/signup/" => "users#create"
```
今回はユーザー作成の為、データ送信を行うのでpostにしています。

# React側の作成
```
import { useEffect, useState } from "react"
import axios from "axios";
import '../App.css'

function SignupForm() {
  const [email,setEmail] = useState('')
  const [password,setPassword] = useState('')
  const [passwordConfirmation,setPasswordConfirmation] = useState('')
  const [message,setMessage] = useState('')

  const handleSubmit = async(e)=> {
    e.preventDefault();
    try {
      const res = await axios.post('http://127.0.0.1:3000/signup',
        {
          user: {email,password,password_confirmation: passwordConfirmation}
        },
        {
          headers: {
            "Content-Type": "application/json"
          },
          withCredentials: true
        }
      )
      setMessage('登録成功！');
    }catch(err){
      setMessage('登録失敗: ' + err.response?.data?.errors?.join(', '));
    }
  }
  return (
    <>
      <form onSubmit={handleSubmit}>
        <input
            type="email"
            name="email"
            value={email}
            onChange={e=>setEmail(e.target.value)}
            placeholder="email"
        />
        <input
            type="password"
            name="password"
            value={password}
            onChange={e=>setPassword(e.target.value)}
            placeholder="password"
        />
        <input
            type="password"
            name="passwordConfirmation"
            value={passwordConfirmation}
            onChange={e=>setPasswordConfirmation(e.target.value)}
            placeholder="password"
        />
        <button className="btn" type="submit">登録</button>
        <p>{message}</p>
      </form>
    </>
  )
}

export default SignupForm
```
1つ1つ解説していきます。

## e.preventDefault()
フォーム標準の再読み込み動作を防ぎます。

## stateの定義
今回は「メールアドレス」「パスワード」「パスワード再確認」「エラーメッセージ」のstateを定義しています。

## handleSubmit
JSONのやりとりについては、以前投稿機能を作成した際のコードと似ていますので、詳しい解説は省きます。

https://qiita.com/tukaji/items/e482330116cbd1c0986a

```
const res = await axios.post('http://127.0.0.1:3000/signup',
```
投稿とは違い今回はデータを送信しますので、「get」ではなく「post」。
送信先は「http://127.0.0.1:3000/signup 」に指定しています。

```
user: {
  email,
  password,
  password_confirmation: passwordConfirmation
}
```
送信するデータです。

## 出力
```
return (
    <>
      <form onSubmit={handleSubmit}>
        <input
            type="email"
            name="email"
            value={email}
            onChange={e=>setEmail(e.target.value)}
            placeholder="email"
        />
        <input
            type="password"
            name="password"
            value={password}
            onChange={e=>setPassword(e.target.value)}
            placeholder="password"
        />
        <input
            type="password"
            name="passwordConfirmation"
            value={passwordConfirmation}
            onChange={e=>setPasswordConfirmation(e.target.value)}
            placeholder="password"
        />
        <button className="btn" type="submit">登録</button>
        <p>{message}</p>
      </form>
    </>
)
```

コントローラーまで作っちゃいましたが、
以上で完了！！！！

# まとめ
パッと見た感じ、「あ、短そうだし2本立てでいっか！」と軽い感じで思っていたんですが、深掘っていくとめっちゃ重要な機能の追加だったので、長くなりすぎないように分割させていただきました。
次回は「SessionControllerを作成して、emailとpasswordでログインできるAPIを実装」を進めていきます。
ではまた。

https://qiita.com/tukaji/items/649d7dc3e0d43d597250