---
# System prepended metadata

title: Ruby(09/14) 五倍紅寶石(第八屆共筆)(2021/8/16)
tags: [Rails, Ruby]

---

# Ruby(09/14) 五倍紅寶石(第八屆共筆)(2021/8/16)
## Lighthouse 燈塔
可以從開發者工具看，分析評分
黃色以上才比較ok


## evernote 專案
### 讓刪除變假刪除
1. 新增 deleted_at 欄位
rails g migration deleted_at
再去db:migration檔案新增change方法跟增加欄位
最後db:migrate顯示

20210816115437_deleted_at.rb
```rb
class DeletedAt < ActiveRecord::Migration[6.1]
  # 新增欄位: 資料表名稱,欄位名稱,型態
  # 新增索引: 加快查詢，概念有點像寫書會加的附錄
  def change
    add_column :notes, :deleted_at, :datetime
    add_index :notes, :deleted_at
  end
end
```
2. 讓 index 直接在首頁顯示
routes.rb
```rb
# 讓 index 直接在首頁顯示
  get "/", to: "notes#index"
  resources :notes
```
3. 讓刪除變成假刪除
notes_controller.rb
```rb
第一種方式
def destroy
    # @note = Note.find(params[:id])
    # 控制能不能顯示，不要真的刪掉
    # 讓他從原本的刪除變成更新資料 destroy 改 update
    # 更新刪除的時間，預設都是 null 點了之後會變成 true
    @note.update(deleted_at: Time.now)
    redirect_to "/notes"
  end
```
note.rb
```rb
第二種方式
# 定義類別方法
    def self.available
      where(deleted_at: nil)
    end
```
```rb
第三種方式
scope :avaliable, -> { where(deleted_at: nil)}
    # default_scope 所有的搜尋都會加上後面的條件
    # 可以用 unscope 把他去掉
    # 商業邏輯，以後如果要改需求改這邊就好
    # 把刪除時間加進去讓他看起來是被刪掉的
    # 用 where 過濾，沒點刪除的都是 nil
  更好的寫法：
  default_scope { where(deleted_at: nil) }
  
  取消scope的寫法：
  Note.unscope(:where)
  #意思是我要取消scope裡面的where方法
  
    # 一般的 scope 還是要在類別後面加這個方法名稱，default不用 => 不用寫 available
    # scope :available

    # https://github.com/rubysherpas/paranoia
    # 直接用 acts_as_paranoid 取代 default_scope { where(deleted_at: nil) }
    # 使用前先在 Gemfile 寫入 gem "paranoia", "~> 2.2"
    # 接著輸入 bundle install，如果沒反應就重開 server
    acts_as_paranoid
```
### 註冊系統
1. 新增路徑
routes.rb
```rb
# get "/users", to: "users#profile"
  # 對於使用者只要開建立使用者的路徑就好
  # 不需要使用者列表(後臺看到就好)
  resources :users, only: [:create] do
    # 路徑： GET /users/sign_up 註冊表單
    # 路徑： GET /users/sign_in 登入表單
    # collection 追加路徑，方法 :方法名稱
    collection do
      get :sign_up
      get :sign_in
    end
  end
end
```
2. 定義方法
users_controller.rb
```rb
    def sign_up
        @user = User.new
    end
    def sign_in
        @user = User.new
    end
    # sign_up 送出後會送到 create
    def create
        @user = User.new(user_params)

        # 不要存明碼到資料庫
        # CRUD callback 要知道
        if @user.save
            redirect_to "/"
        else
            render :sign_up
        end
    end

    def profile
    end

    private
    def user_params
      params.require(:user).permit(:account, :password, :email)
    end 
```
3. view 顯示出來
sign_up.html.erb
```rb
<%# 直接跟他講路徑，不要讓他猜，可以用GET方法顯示使用者跟POST方法新增使用者 %>
<%#  url: "/users" 可參考路徑對照表 %>

<%= form_for(@user, url: "/users") do |f| %>

<%# email 防呆 %>
<div class="field">
  <%= f.label :email %>
  <%= f.email_field :email %>
</div>

<%#  password_field 讓他不顯示出來 %>
<div class="field">
  <%= f.label :password %>
  <%= f.password_field :password %>
</div>

<%# 密碼再確認 %>
<div class="field">
  <%= f.label :password_confirmation %>
  <%= f.password_field :password_confirmation %>
</div>

<div class="field">
  <%= f.label :account %>
  <%= f.text_field :account %>
</div>

<%= f.submit %>

<% end %>
```
4. 驗證 + 加密密碼
user.rb
```rb
# 使用 Digest 方法要先 require 進來
require 'digest'

class User < ApplicationRecord
    # 可以用 attr accessor 騙他，實際上沒有這個欄位（不建議）
    # attr_accessor: :password_confirmation
    # 不要在資料庫存明碼
    # confirmation = 確認兩個欄位值是一樣的, uniqueness = 唯一值，
    # 兩個都是驗證器
    # https://guides.rubyonrails.org/active_record_validations.html
    validates :email, presence: true, uniqueness: true
    validates :password, presence: true, confirmation: true
    validates :account, presence: true, uniqueness: true

    # 各種 callback https://guides.rubyonrails.org/active_record_callbacks.html
    # 放before_create 因爲怕會跟其他的 callback 重複，新增跟修改都會觸發
    # before after 自己要看，知道他們在做啥
    # 使用者被建立前先做加密
    before_create :encrypt_password

    # 手刻會員系統，面試可能會考

    # 加密方法：
    # MD5 已被破解
    # SHA 比較安全

    # 密碼只能被加密一次，兩次以上會看不懂
    require "digest"
    def encrypt_password
        # Digest::MD5.hexdigest(pw)
        # salting:在密碼裡面加東西，讓他比較複雜
        salted_pw = "eiojre#{self.password}rewropwr"
        self.password = Digest::SHA1.hexdigest(salted_pw)
    end
end
```
### 登入系統
1. 新增路徑
routes.rb
```rb
  # post :sign_in 拉出去做
  # 登入跟註冊是兩件事情，註冊是一般的 CRUD，但登入會用到 sessions，所以把他獨立拉出來寫
  # sessions 是某個 controller 的名字
  # 寫入 sessions = 登入，用 as 換 path 名字，看起來比較乾淨
  post "/users/sign_in", to: "sessions#create", as: "login"
  # 刪除 sessions = 登出
  delete "/users", to: "sessions#destroy", as: "logout"
```
2. 建 controller
rails g controller sessions
sessions_controller.rb
```rb
def create
      # 先加密再驗證才會正確
 
      # 發 session(cookie): 一種認證機制
      # 拿到 cookie 不等於登入
      # 登入 = 拿到 session 而且可以做裡面的事
      # 登入成功會發令牌(cookie)給你，瀏覽器會留一個 session 做比對，看是否正確
      # 會有時間性，如果過期要再重新登入
      # 或是對方 server 重新啟動也要重新登入

      # HTTP本身是沒有狀態的，在這邊做任何事情，接著跳到其他頁面再回來不會保留
      # 透過 params 或是 cookie 知道前一頁的資料
      # 可以把前一頁的資料存在 cookie 留到下一頁用
      # localstorage = 大型 cookie
      # 看你有 cookie 就讓你登入
      # 沒有就不讓你登入

      # 驗證 email & password
      # 對照 user.rb 的
      pw = user_params[:password]
      salted_pw = "xyz#{pw}827128#{pw}82-12j23h"
      hashed_password = Digest::SHA1.hexdigest(salted_pw)

      user = User.find_by(email: user_params[:email],
                          password: hashed_password)

      if user
        # session 取名隨意
        # 用 session 方法把 user.id 送進來 thankyou9527
        session[:thankyou9527] = user.id
        redirect_to "/"
      else
        redirect_to "/users/sign_in"
      end
    end

    
    # 登出 = 把sessions刪掉
    def destroy
      session[:thankyou9527] = nil
      redirect_to "/"
    end

    private
    def user_params
      params.require(:user).permit(:email, :password)
    end
```
3. 同樣的放公版
views/layouts/
application.html.erb
```rb
    <%# 如果要寫在別的地方要寫好路徑讓他找，建shared資料夾%>
    <%#使用者登入跟沒登入會出現不同資訊%>
    <%# 用render 去抓另外一個公版到這邊呈現，看起來比較不那麼亂%>
    <%= render "shared/navbar" %>
    <%= yield %>
```
_navbar.html.erb
```rb
<%# 共用的通常會創一個 shared 資料夾來放 %>
<nav>
  <% if user_signed_in? %>
    <%= link_to "登出", logout_path, method: "delete" %> |
    <%= current_user.email %>
  <% else %>
    <%= link_to "登入", sign_in_users_path %> |
    <%= link_to "註冊", sign_up_users_path %>
  <% end %>
</nav>
```
4. 共用的 controller
application_controller.rb
```rb
# view helper 跟 controller 都會用到 helper_method 裡面的方法
  # 用 helper_method 把 controller 功能匯出到 view helper，寫一次就好
  # 或是寫在 view helper 再外掛模組進來也可以
  helper_method :user_signed_in?, :current_user

	private

  # 有 session
  def user_signed_in?
    session[:thankyou9527] != nil
  end

  # 如果有用戶的 session 就可以登入
  def current_user
    if user_signed_in?
      User.find(session[:thankyou9527])
    else
      nil
    end
  end

  # 沒有 session 就要重登
	def check_login!
		if not user_signed_in?
			redirect_to "/users/sign_in"
		end
	end
```
5. 要登入才能做新增修改刪除等動作
```rb
before_action :check_login!, except: [:index, :show]
```

## 套件

[devise套件](https://github.com/heartcombo/devise)

rails g devise MODEL
MODEL 要換字！ 看你要取什麼名字

套件裡面的 Wiki 有各式套件，照抄就好，抄官網最好，別亂抄來路不明的文章


[五倍紅寶石 github 作業](https://github.com/5xTraining/TrainingPrograms/blob/master/T101.md)
步驟20登入登出功能 hen 重要

## 關聯性
- 1:1
- 1:N
- N:N

user has_many notes
notes belongs_to user
取名 User 變小寫 + id
user_id = FK

### 加 FK key
  在終端機操作，產生 user_id
1. g migration name
   - migration擋內新增欄位
   - add_reference(:... ,:... )，產生 user_id以外還會加索引
20210816155453_add_user_to_note.rb
```rb
 def change
    # notes -> user_id
    add_reference :notes, :user, foreign_key: true
  end
```
2. db:migrate
### has_many
類別方法
不是建立關聯，建立關聯是 add_reference，has_many 會幫你建立 4 個方法!
不一定兩邊都要寫，看你有沒有需要用到那個方法
has_many :notes
.notes
抓到目標user
.notes=

belongs_to :user

---

###### tags: `Ruby` `Rails`