見出し画像

Railsで挫折した人のためのSinatra -5-

新しいテーブルの追加

前回はコチラ

今回からログイン機能の追加に入っていきますが、ボリュームがすごいので注意です!

ログイン機能を追加するにあたって、ユーザーのデータをDBに保管しなければいけないので、DB設計から確認していきましょう。

現在のDBはテーブルが一つ存在しています。
もちろん、haikusテーブルです。基本的な命名規則としてテーブル名は複数形にすると良いです。
そして、このテーブルは以下の様なカラムで構成されています。

- idカラム integer型
- mainカラム string型

この2つです。さて、ここで最初にデータベースのセットアップで使ったinit.sqlファイルを見てみましょう。
場所は/db/以下です。

# ① このsqlを実行するときに既にDBにhaikusテーブルがあったら、それを削除してから実行する
DROP TABLE IF EXISTS haikus;

# ② haikusテーブルを新規作成する
create table haikus (
 # ②-① idカラムをinteger型 主キーとする そしてidは連番で自動的に作成されるようにAUTOINCREMENTに設定する
 id INTEGER PRIMARY KEY AUTOINCREMENT,
 # ②-① mainカラムをtext型(文字列) notnull制約(データを保存する際null値は受け取らない)を設定する
 main TEXT NOT NULL
);

# ③ haikusテーブルにidとmainからなるデータを保存する
insert into haikus values (1, "古池や蛙飛こむ水のおと");
insert into haikus values (2, "閑さや岩にしみ入蝉の声");

コメントでざっと説明したのでこれを読んで貰えればよいのですが、主キーというのはそのデータを一意に表すもので、他のデータと被らないユニークなものを使います。
このデータをもとにするとhaikusテーブルは現状下のような形のテーブルになっています。

画像1

さて、DBについて軽く説明したので、これからログインのために必要なユーザーテーブルを作りましょう。

今回ログインするためにユーザー名とパスワードを使うので、userテーブルはnameカラムとpasswordカラムを持ちます。それに加えてidカラムを持っているので合計3つのカラムを作ります。

画像2

表にするとこんな感じになります。
nameカラムは他のユーザーと被るとよろしくないので、UNIQUEオプションがついています。これはこのカラムの一意性を担保するものです。

これで必要なテーブルの内容は網羅出来たように思えますが、実は誰が作った俳句かを見分けるための情報がありません。

こういうときは、haikuデータを作る際、userのid情報を持たせておきます。そうすることでhaikuが持っているuserのidからuserのデータを引っ張ってくることができるようになります。

画像3

付け足すとこうなります。
では、このDBを形成するために/db/init.sqlを書き換えましょう。

DROP TABLE IF EXISTS haikus;
DROP TABLE IF EXISTS users;

create table haikus (
 id INTEGER PRIMARY KEY AUTOINCREMENT,
 main TEXT NOT NULL,
 user_id INTEGER NOT NULL
);

create table users (
 id INTEGER PRIMARY KEY AUTOINCREMENT,
 name TEXT NOT NULL UNIQUE,
 password TEXT NOT NULL
);


insert into users values (1, "takashi01", "0123456");

insert into haikus values (1, "古池や蛙飛こむ水のおと", 1);
insert into haikus values (2, "閑さや岩にしみ入蝉の声", 1);

テーブルの作成と、haikusテーブルにuser_idを付け足しています。
書き換えることが出来たら、以下のコマンドでDBを書き換えましょう。

$ cd db
 
$ sqlite3 sinatra.db < init.sql

ちなみにこのコマンドを打ち込むたびにDBを作り変えてくれるので、グチャグチャなデータになってしまったら、このコマンドを打つというのもありです。(※DBのデータは削除されます。)

続いてモデルとコントローラーの作成をしましょう。

モデルとコントローラーの作成

まず、モデルから作っていきます。
モデルフォルダでActiveRecordを使ってsqliteに接続しているのですが、modelsファイルが増えてきたので、modelsフォルダ内にhaiku.rbを以下のように書き換え、base.rbとuser.rbを追加してください。

/models/haiku.rb ↓

require './models/base.rb'

class Haiku < ActiveRecord::Base
 validates :main, presence: true
 belongs_to :user
end

/models/base.rb ↓

require 'bundler'
Bundler.require

enable :session

ActiveRecord::Base.establish_connection(
 adapter: 'sqlite3',
 database: './db/sinatra.db'
)

/models/user.rb ↓

require './models/base.rb'

class User < ActiveRecord::Base
 validates :name, presence: true
 validates :password, presence: true
 has_many :haikus, dependent: :destroy
 def authenticate(user_id)
   return self.id == user_id ? true : false
 end
end

models/base.rbは共通の記述をまとめているだけです。
その中にenable sessionというものがありますが、これはログインに必要な機能でsinatraではdefaultでは実装されていませんので、ここでenableとしておきます。

そのbase.rbをhaiku.rbとuser.rbで呼んでいますのでrequireを忘れずに書きましょう。
さて、haiku.rbにbelongs_toという記述があると思います。これはuser.rbのhas_manyと対応していて、Railsでも使っていたアソシエーションという各モデルの結びつけに使います。user.haikuとかのようにuserに結びついたhaikuのデータを取得できるようになったりします。

あとは、user.rb内のauthenticateという関数ですが、これはあとでログインしているユーザーかどうかを確認するときに使うインスタンスメソッドで引数として受け取ったidとそのインスタンスが持つuser_idを比べて真偽値を返します。

これでuserモデルの作成とモデルフォルダの記述の整理ができました。

続いてコントローラーを実装する前にconfig.ruで、userコントローラーにルーティングの振り分けをします。

/config.ru ↓

require './controllers/haiku.rb'
require './controllers/user.rb' # 追加
require './controllers/top.rb'

run Rack::URLMap.new({
 '/' => TopController.new,
 '/haiku' => HaikuController.new,
 '/user' => UserController.new # 追加
})

/userにUserControllerクラスを割り当てます。

user.rbとhaiku.rbを以下の様に記述しましょう。

/controllers/user.rb ↓

require './controllers/base.rb'
require './models/user.rb'
class UserController < Base
 # /user/signup に signup.erbを出力する
 get '/signup' do
   erb :signup
 end
 
 # /user/login に login.erbを出力する
 get '/login' do
   erb :login
 end
 
 # ユーザーの場合ユーザー作成とサインアップが同じ動作内で行われるのでuser作成が成功したときに
 # session[:user_id]にユーザーインスタンスのidを挿入している
 post '/create' do
   @user = User.new({name: params[:name], password: params[:password]})
   # isExistでnameが既に使われているかを判断している
   isExsist = User.find_by(name: @user[:name]) ? true : false
   if isExsist
     @message = "This name is already in use"
     erb :signup
   else
     if @user.save
       session[:user_id] = @user.id.to_i
       redirect "/user/#{@user.id}" 
     else
       if @user.errors.present?
         @errors = @user.errors
         erb :errors
       end
     end
   end
 end

 # login動作/user/authにpostメソッドを実行するとユーザーの確認をして、sessionにログインしたユーザーのidを入れる
 post '/auth' do
   @user = User.find_by(name: params[:name], password: params[:password])
   if @user.nil?
     redirect "/user/login"
   end
   session[:user_id] = @user.id.to_i
   redirect "/user/#{@user.id}"
 end
 
 # sessionをリセットするとログアウトができる
 post '/logout' do
   session.clear
   redirect '/'
 end
 
 # user詳細ページ自分のページにしかアクセス出来ない。
 get '/:id' do
   @user = User.find(params[:id])
   if @user && @user.authenticate(session[:user_id])
     erb :user
   else
     redirect '/user/login'
   end
 end
end

色々書いてありますが、login.erbとsignup.erb、user.erbの準備をこれからしなければいけないのと先程出てきたsessionを使っています。

sessionはハッシュのように使ってsession[:user_id]にログインしたユーザーのidを保存して色々なところで使っていきます。

/controllers/haiku.rb ↓

require './controllers/base.rb'
require './models/haiku.rb'
class HaikuController < Base
 get '/' do
   @haikulist = Haiku.all
   erb :index
 end
 get '/:id' do
   @haiku = Haiku.find(params[:id])
   erb :show
 end
 post '/create' do
   # ログインユーザーでないと俳句を投稿出来ないようにする
   if session[:user_id]
     @haiku = Haiku.new({main: params[:main], user_id: session[:user_id]})
     if @haiku.save
       redirect '/haiku'
     else
       if @haiku.errors.present?
         erb :errors
       end
     end
   else
     # ログインせずに投稿すると@messageを返す
     @haikulist = Haiku.all
     @message = "You cannot post without logging in"
     erb :index
   end
 end
 post '/:id/delete' do
   @haiku = Haiku.find(params[:id])
   @haiku.delete
   redirect '/haiku'
 end
end

session[:user_id]の有無を確かめてログイン状態を取得する。

ただsessionというものには生存時間のようなものがあって設定をしないとかなり短い時間でまともに使えないので/controllers/base.rbにsessionの設定をします。

/controllers/base.rb ↓

require 'sinatra/base'

class Base < Sinatra::Base
 set :root, File.join(File.dirname(__FILE__), '..')
 set :views, Proc.new { File.join(root, "views") }
 configure do
   use Rack::Session::Cookie, :key => 'rack.session',
     :expire_after => 2592000,
     :secret => Digest::SHA256.hexdigest(rand.to_s)
 end
end

色々と訳のわからないようなものが書いてありますが、sessionはconfig.ruで使ったRackというものを使います。expire_afterというのがsessionの生存時間のものなので2592000秒に設定しましょう。だいたい1ヶ月ですかね?

これで、モデルとコントローラーの設定が出来ました。

ログイン機能の実装

最後にviewsで、今まで作ってきたログイン機能を実装しましょう。

まず以下のようにlogin.erb、signup.erb、user.erbを実装しましょう。

/views/login.erb ↓

<main>
 <form method="post" action="/user/auth">
   <p><b>Log in</b></p>
   <p><input type= "text" name="name"></p>
   <p><input type= "password" name="password"></p>
   <p><input type= "submit" value="Log in"></p>
 </form>
</main>

/views/signup.erb ↓

<main>
 <form method="post" action="/user/create">
   <p><b>New User</b></p>
   <p><input type= "text" name="name"></p>
   <p><input type= "password" name="password"></p>
   <p><input type= "submit" value="Sign up"></p>
   <% if @message %><p class="error-message"><%= @message %></p><% end %>
 </form>
</main>
<style>
.error-message {
 color: red;
 font-size: 14px;
}
</style>

コードを読むとわかるのですが、コードがほぼ同じです。
nameが被ったとき、エラーメッセージを表示するところと
actionでの送り先URLが違うだけですね。

/views/user.erb ↓

<main>
 <p><%= @user.id %></p>
 <p><%= @user.name %></p>
 <p><%= @user.password %></p>
</main>

とりあえずユーザーの情報を表示しているだけなので後で修正しましょう。

ついでにindex.erbとshow.erb、layout.erbのスタイルを整えましょう。

/views/index.erb ↓

<main>
 <div class="container">
   <form method="post" action="/haiku/create">
     <p><b>Body</b></p>
     <p><input type= "text" name="main"></p>
     <p><input type="submit" value="Create"></p>
     <% if @message %><p class="error-message"><%= @message %><p><% end %>
   </form>
   <div>
     <h1>Haiku list</h1>
     <% @haikulist.each do |haiku| %>
       <p>
         <b>No.<%= haiku[:id] %></b> <a href=<%="/haiku/#{haiku[:id]}"%> ><%= haiku[:main] %></a>
         <b>By</b><%= haiku.user[:name] %>
       </p>
     <% end %>
   </div>
 </div> 
</main>
<style>
.container {
 display: flex;
 justify-content: start;
}
.container div {
 margin: 50px 30px;
}
.container form {
 margin: 70px 30px;
 width: 250px;
}
.container form input[type="submit"], input[type="text"] {
 width: 100%;
 height: 30px;
}
.check input[type="checkbox"] {
 width: 20px;
 height: 20px;
}
.check span {
 height: 30px;
 line-height: 30px;
}
.error-message {
 color: red;
 font-size: 14px;
}
</style>

haiku.user[:name]のところでアソシエーションの設定をしたのでhaiku.userで、取りだしているhaikuのuser_idをidに持つuserをとってきていることに注意しておきましょう。

/views/show.erb ↓

<main>
 <h1>Haiku details</h1>
 <p>
   <%= @haiku[:main] %>
   <b>By</b><%= @haiku.user[:name] %>
 </p>
 <% if @haiku.user && @haiku.user.authenticate(session[:user_id]) %>
   <form method="post" action=<%= "/haiku/#{@haiku[:id]}/delete" %>>
     <input type="submit" value="Delete">
   </form>
 <% end %>
</main>

- ログインユーザーとhaikuの持っているuser_idが同じときだけdeleteボタンが表示される
- つまり、投稿したユーザーしか削除出来ないようにしている。

の2点に注意して進めましょう!

/views/layout.erb ↓

<html>
<head>
 <title>easy-haiku</title>
 <style>
   header {
     display: flex;
     justify-content: start;
   }
   header a {
     margin: 0 10px;
     text-decoration: none;
     font-size: 18px;
     font-weight: bold;
     height: 40px;
     line-height: 40px;
   }
   form {
     margin: 0 10px;
   }
   button {
     color: #551B8B;
     border: none;
     font-weight: bold;
     font-size: 18px;
     background-color: white;
     outline: none;
     line-height: 40px;
     height: 40px;
   }
 </style>
</head>
<body>
 <header>
   <a href="/">to top</a>
   <a href="/haiku">to list</a>
   <% if session[:user_id] %>
   <a href=<%= "/user/#{session[:user_id]}" %>>to mypage</a>
   <form method="post" action="/user/logout">
     <button>logout</button>
   </form>
   <% else %>
     <a href="/user/login">login</a>
     <a href="/user/signup">signup</a>
   <% end %>
 </header>
 <%= yield %>
 <footer></footer>
</body>
</html>

ログインしているときとログインしていないときのヘッダーの表示を変えています。

さて、これでeasy-haikuにログイン機能を追加出来ました。
deviseなどのgemを使っても良いのですが、session等の使い方を覚えておくとコードの書き方の幅が広がると思います。

dockerを立ち上げ直して確認しましょう!

うまくログイン出来たでしょうか?

最後にtreeでディレクトリ構成を確認しましょう。

$ tree .
 
.
├── Dockerfile
├── Gemfile
├── Gemfile.lock
├── config.ru
├── controllers
│   ├── base.rb
│   ├── haiku.rb
│   ├── top.rb
│   └── user.rb
├── db
│   ├── init.sql
│   └── sinatra.db
├── docker-compose.yml
├── init.sh
├── models
│   ├── base.rb
│   ├── haiku.rb
│   └── user.rb
├── start.sh
├── vendor
│   └── bundle
│       └── ruby(以下略)
└── views
   ├── errors.erb
   ├── index.erb
   ├── layout.erb
   ├── login.erb
   ├── show.erb
   ├── signup.erb
   ├── top.erb
   └── user.erb

このような構成となっています。

ここまでお付き合いいただきありがとうございました!
次章が最終章となります。
javascriptの使用やどんな機能を追加するかも書くので最後までよろしくお願いいたします。

最終章へ >

この記事が気に入ったらサポートをしてみませんか?