Sinatra環境で簡易ログイン機能を実装

# はじめに
掲示板を作成する中でログイン機能を実装する必要がある場合があるので、初心者でも理解しやすい簡易的なログイン機能の作り方をまとめることにした。

# 開発の概要
アカウント作成時にusersテーブルにログインIDとパスワードを保存し、ログイン時に一致するものがあればログイン成功とする。ログイン情報はCookieに保存し、ページを切り替えてもログインが維持されるようにする。

# 必要なファイル
マテリアル内のbbsを複製し`bbs_login`として使用
これ以降、`bbs_login`をルートフォルダとして進める

# usersテーブルの作成
まず`config/database.yml`内のデータベース名を変更する（これをしないと、掲示板を既に作っている場合データベース名が被って正しくデータベースの作成ができない）

```yaml:config/database.yml
default_env: &default
  adapter: postgresql
  encoding: unicode
  database: bbs_login #変更

development:
  <<: *default

production:
  <<: *default
```
　　
データベース名を変更後、データベースを作成

```bash:bash
$ rake db:create
```
　　
マイグレーションファイルを作成

```bash:bash
$ rake db:create_migration NAME=create_users
```
　　
マイグレーションファイルを編集
必要なのは表示名、ログインid、パスワードの3つ
カラム名はそれぞれ `name`, `login_id`, `pass`とする。

```ruby:db/migrate/xxxxxxxxxxxxxx_create_users
class CreateUsers < ActiveRecord::Migration[6.1]
  def change
    create_table :users do |t|
      t.string :name
      t.string :login_id
      t.string :pass
    end
  end
end
```
　　
マイグレーションを実行

```bash:bash
$ rake db:migrate
```
　　
マイグレーション実行後の`schema.rb`（確認）

```ruby:db/schema.rb
ActiveRecord::Schema.define(version: xxxx_xx_xx_xxxxxx) do

  create_table "users", force: :cascade do |t|
    t.string "name"
    t.string "login_id"
    t.string "pass"
  end

end
```

# Userモデルの作成
初期状態では`contribution.rb`というファイルになっているので、これを`user.rb`に変更する
「ファイル名を変更」で行ってもいいが、ターミナルを用いて変更する場合には以下のコマンドを実行する。

```bash:bash
$ mv models/contribution.rb models/user.rb
```
　　
ファイル名を変更することができたら、ファイル内のクラス名も変更する。

```ruby:models/user.rb
ActiveRecord::Base.establish_connection
class User < ActiveRecord::Base #変更

end
```
　　
`app.rb`で読み込むファイル名も変更しておく。
```ruby:app.rb
require 'bundler/setup'
Bundler.require
require 'sinatra/reloader' if development?

require "./models/user.rb" #変更
```
　　
以上でusersテーブルおよびUserモデルの作成が完了した。

# アカウント作成画面の作成
~amazonaws.com/signupでアカウントを作成できるようにする。
まずは`app.rb`でルーティングし、テンプレートファイルとして`signup.erb`をレンダリングする。

```ruby:app.rb
get "/signup" do
  erb :signup
end
```
　　
続いて`index.erb`を複製し`signup.erb`を作成。ターミナルを用いて実行する場合はcpコマンドを用いる。

```bash:bash
$ cp views/index.erb views/signup.erb
```
　　
`signup.erb`にアカウント作成用のフォームを作成。POSTリクエストの送信先は/signupとする。
inputタグの属性を複数用いているので、詳しくは以下のサイト等を参照。

https://web-camp.io/magazine/archives/85005

```erb:views/signup.erb
<!--略-->

<body>
  <form action="/signup" method="post">

    <label>ユーザー名</label>
    <input type="text" name="name" required>

    <label>ログインID（8文字以下、英数字で入力してください）</label>
    <input type="text" name="login_id" maxlength="8" pattern="^[0-9A-Za-z]+$" required>

    <label>パスワード（16文字以下、英数字で入力してください）</label>
    <input type="text" name="pass" maxlength="16" pattern="^[0-9A-Za-z]+$" required>

    <button type="submit">アカウント作成</button>

  </form>
</body>

<!--略-->
```

# アカウント作成機能の実装
`app.rb`にリクエスト送信時の処理を記述する。
ユーザー名およびパスワードの被りは問題ないが、ログインIDの被りは許容することができないので、既に使われているログインIDを入力した場合はデータベースに登録せずに/signupへリダイレクトするようにした。

- `find_by` 
テーブル内のレコードから条件に一致するものを探し、見つけた場合ひとつだけ返し、見つからなかった場合は`nil`を返す。今回は`login_id`が`params[:login_id]`と一致するレコードを探している。

- `nil?`
値が`nil`である場合には`true`を返し、それ以外の場合は`false`を返す。

```ruby:app.rb
post "/signup" do

  if User.find_by(login_id: params[:login_id]).nil?
        User.create({
            name: params[:name],
            login_id: params[:login_id],
            pass: params[:pass]
        })
        redirect "/"
    else
        redirect "/signup"
    end

end
```
# ログイン画面の作成
今度は/loginでログインできるようにする。先程同様`app.rb`でルーティングを行い、テンプレートファイルとして`login.erb`をレンダリングする。

```ruby:app.rb
get "/login" do
  erb :login
end
```
　　
`index.erb`を複製して`login.erb`を作成。

```bash:bash
$ cp views/index.erb views/login.erb
```
　　
`login.erb`にログイン用のフォームを作成。リクエストの送信先は/loginとする。

```erb:views/login.erb
<!--略-->

<body>

  <form action="/login" method="post">

    <label>ログインID</label>
    <input type="text" name="login_id" required>

    <label>パスワード</label>
    <input type="password" name="pass" required>

    <button type="submit">ログイン</button>

  </form>

</body>

<!--略-->
```

# ログイン機能の実装
まず`session`の保存先としてcookieを使用するために`app.rb`に以下の記述をする。

```ruby:app.rb
require 'bundler/setup'
Bundler.require
require 'sinatra/reloader' if development?

require "./models/user.rb"
use Rack::Session::Cookie #新たに追加
```
　　
続いてリクエスト送信時の処理を書く。
`check`という変数を用意し、フォームに入力された`login_id`と値が一致するレコードがあればこの変数に格納するようにした。続くif文で、もし`check`が空でなければ`check.pass`と`params[:pass]`を比較し、さらにこれが一致していれば`session[:user]`に`check`を格納しcookieに保存している。また、フォームに入力された`login_id`と値が一致するレコードが存在しないか、存在していたとしてもパスワードが間違っている場合は/loginにリダイレクトするようにした。

```ruby:app.rb
post "/login" do

  check = User.find_by(login_id: params[:login_id])

  if check
    if check.pass == params[:pass]
      session[:user] = check
    else
        redirect "/login"
    end

end
```

# ログイン状態の維持
ログインしているかどうかの判定を`app.rb`内のbeforeブロックに記述する。ログインされている場合は`session[:user]`を`@user`に格納してテンプレートファイルに渡せるようにする。

```ruby:app.rb
before do

  if session[:user]
    @user = session[:user]
  end

end
```
# ログアウト機能の追加
```erb:views/index.erb
<!--略-->

<body>

  <a href="/logout">ログアウト</a>

</body>

<!--略-->
```

```ruby:app.rb
get "/logout" do
  session[:user] = nil
  redirect "/login"
end
```

# マイページの作成
先程までの内容で基本的なログイン機能が完成したので、後は追加の機能を作成していく。

```ruby:app.rb
get "/" do

  if session[:user].nil?
    redirect "/login"
  end

  erb :index
end
```

```erb:views/index.erb
<!--略-->

<body>

  <!--新たに追加-->
  <h1><%= @user.name %>さんのマイページ</h1>

  <a href="/logout">ログアウト</a>

</body>

<!--略-->
```

# 投稿機能の実装

```bash:bash
$ rake db:create_migration NAME=create_contributions
```

```ruby:xxxxxxxxxxxxxx_create_contributions.rb
class CreateContributions < ActiveRecord::Migration[6.1]
  def change
    create_table :contributions do |t|
      t.string :body
      t.integer :user_id
    end
  end
end
```
　　
`schema.rb`は以下のように更新される。
```ruby:schema.rb
ActiveRecord::Schema.define(version: xxxx_xx_xx_xxxxxx) do

  enable_extension "plpgsql"

  create_table "contributions", force: :cascade do |t|
    t.string "body"
    t.integer "user_id"
  end

  create_table "users", force: :cascade do |t|
    t.string "name"
    t.string "login_id"
    t.string "pass"
  end

end
```
　　
Contributionモデル及びアソシエーションを作成。ユーザーと投稿は一対多の関係になってる。

```ruby:models/user.rb
ActiveRecord::Base.establish_connection
class User < ActiveRecord::Base
    has_many :contributions
end

class Contribution < ActiveRecord::Base
    belongs_to :user
end
```
　　
投稿フォームの追加

```erb:views/index.erb
<!--略-->
    
<a href="/logout">ログアウト</a>

<!--新たに追加-->
<form action="/new" method="post">

  <input type="text" name="body" placeholder="投稿内容">
  <button type="submit">投稿</button>

</form>
<!--ここまで-->

<!--略-->
```
　　
リクエストに対する処理を追加する。アソシエーションの記述をしているため、通常では`Contribution.create`とするところを`@user.contributions.create`としており、これによって`user_id`には自動で投稿したユーザーのアカウントの`id`が入る。

```ruby:app.rb
get "/new" do
  @user.contributions.create({
    body: params[:body]
  })
  redirect "/"
end
```
　　
全投稿の表示（最新のものが上に来るように降順で並び替え）
```ruby:app.rb
get "/" do
    
    if session[:user].nil?
        redirect "/login"
    end
    
    @contributions = Contribution.all.order("id desc") #新たに追加
    
    erb :index
end
```

```erb:views/index.erb
    </form>
    
    <!--新たに追加-->
    <% @contributions.each do |contribution| %>
    
      <div>
        <h3><%= User.find(contribution.user_id).name %></h3>
        <p><%= contribution.body %></p>
      </div>
    
    <% end %>
    <!--ここまで-->
    
  </body>
```
# まとめ
今回はCookieを用いて簡易的なログイン機能を実装した。Ruby関連の記事はRails環境であることが多く、Sinatraに関連する記事が少ないと感じたのでもしよければ参考にして欲しい。また、本格的なログイン機能を実装する場合とも基本的な部分は共通しているので、has_secure_password等を用いたログイン機能の開発の礎となれば幸いである。

パスワードの暗号化に関しては以下のgithubを参照
https://github.com/bcrypt-ruby/bcrypt-ruby