---
# System prepended metadata

title: 0419 加密、使用者登入登出、資料庫關聯、webpacker
tags: [Ruby / Rails]

---

# 0419 加密、使用者登入登出、資料庫關聯、webpacker
###### tags: `Ruby / Rails`

## 作業檢討(JS)
```javascript=
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <script src="jq.js"></script>    // 帶入後才能使用像 jQuery 的功能
</head>
<body>
  <h1 id="h">hello</h1>
  <script>
    $('#h').on('click', function() {
      this.innerHTML = '被按了'
    })
  </script>
</body>
</html>
```
- 建立 `jq.js`
- 缺少 $ 方法
* 最簡單的做法就是直接給他需要的東西
```javascript=
$ = function (el) {    // 最好不直接指定全域變數
  console.log(el)
  return {
    on:function(evt, callback){
    
    }
  }
}
```
* 加功能上去
```javascript=
window.$ = function(el) {
  let element = document.querySelector(el)
  return {
    element: element,
    on : function(evt, callback) {
      switch(evt) {
      case 'click':
        this.element.addEventListener('click', callback)
        break
      }
    }
  } 
}
```
- 注意 let 宣告變數的存活範圍
```javascript=
window.$ = function(el) {
  if (el === document) {
    let element = document
  }
  else{
    let element = document.querySelector(el)
    }
  
  return {
    element: element,
    on : function(evt, callback) {
      switch(evt) {
      case 'click':
        this.element.addEventListener('click', callback)
        break
      }
    }
  } 
}
```


- 連續技
- 把回傳值包成一個物件(Object)
- 如果是一個物件就能繼續在後面加功能及使用功能


`.ready()`
`.DOMContentLoaded`
DOMContentLoaded事件是當document被完整的讀取跟解析後就會被觸發,不會等待 stylesheets, 圖片和subframes完成讀取  (load事件可以用來作為判斷頁面已經完整讀取的方法).

```javascript=
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <script src="jq.js"></script> 
</head>
<body>
  <h1 id="h">hello</h1>
  <script>
    $(body).ready( function() {
      $('#h').on('click', function() {
      this.innerHTML = '被按了'
      })
    }) // 加入 ready 方法
    
  </script>
</body>
</html>
```
#### 加入 ready 方法
```javascript=
const globalObject = (typeof window === "object") ? window : global

globalObject.$ = function(el) {
  let element = (el === document) ? document : document.querySelector(el)
  // 因為 document 丟到 querySelector 會壞 所以要加判斷式
  return {
    element,  // element: element
    ready: function(callback) {
      this.element.addEventListener('DOMContentLoaded', callback)
    },
    on: function(evt, callback) {
      switch(evt) {
        case 'click':
          this.element.addEventListener('click', callback)
          break
      }
    }
  }
}
```

- 建議可以看一下 jQuery 的 source code

- jQuery 的 slim 版本
  - 少了網路功能跟動畫特效（淡入淡出...）
  - 只要用 query 就可以使用
![](https://i.imgur.com/hgKhVIa.png)

把前面的東西都指定給後面那個
```javascript=
window.jQuery = window.$ = jQuery
```
結果：產生一樣效果
```javascript=
$()
jQuery()
```

- 建立簡寫
```javascript=
var arr = []
var slice = arr.slice
// 以後要使用 arr.slice 的時候直接輸入 slice 就可以
```
---

## 加密
```ruby=
module Encrytor
  require 'digest'
```

## 使用者登入
- `session` 方法：發號碼牌，暫存資料
- `session` 是一個很大的 Hash（本來就存在） 把號碼牌放到裡面
- 在瀏覽器裡開一個 cookie (server 跟 瀏覽器都產生)
- 開格子 thankyou9527
/sessions_controller.rb
```ruby=
def create
    user = User.login(user_params) # 見下面方法 （之前做好了）
    if user
      # 發號碼牌
      session[:thankyou9527] = user.id # 用login方法抓到的資料
      # user.id 方法是因為有 user 這個 Model, 資料庫有 users 這個 table 裡面有 id  這個欄位
      # 轉去首頁
      redirect_to root_path
    else
      redirect_to sign_in_sessions_path
    end
  end
```
換頁之後 user 就消失了
透過session / 網路傳輸 所有值會變成一個字串（記憶體位置符號）（如果是數字或單純的值的話會比較容易轉回來）
  不要放陣列、物件或太複雜的東西
通常 session 後面會接一個 setCookie 方法，目前rails 會自動幫忙建立



#### 回顧 User.login 方法
/models/user.rb
```ruby=
 def self.login(params)
    email = params[:email]
    password = params[:password]

    salted_password = Encryptor.salted(password)
    encrypted_password = Encryptor.encrypt(salted_password)

    find_by(email: email, password: encrypted_password)
    # find_by回傳值或 null
  end
```

有 session 代表有登入？  => 有號碼牌不代表有買飲料
後面能不能有存取行為，這是看後面的事情決定
http沒有狀態，所以也沒有登入的狀態

登入是「行為」，透過行為拿到 session 跟 cookie，不是一個狀態

如果有號碼牌 => 可以到該頁面
如果沒有號碼牌 => 回到首頁

可以在開發者工具裡面找 application 把 cookie 刪除
每次送 request 的時候都會跟著一個號碼牌，server 就會根據號碼牌給你看你可以看的東西
例：
```ruby=
def new
    if not session[:thankyou9527]
      redirect_to root_path
    else
      @restaurant = Restaurant.new
    end
  end
```

#### 回顧 routes
```ruby=
  resources :sessions, path: 'users', only: [:destroy] do
    collection do
      get :sign_in, action: 'new'
      post :sign_in, action: 'create'
    end
  end
```
#### 製作登出連結（只有在有登入的時候才會出現）
index.html.erb  （先做出想要的效果）

```ruby=
# 在網站最上方加入
<% if session[:thankyou9527] %>
  登出
<% else %>
  註冊 | 登入
<% end %>
```

### view helper
使用者 id 重複出現 thankyou9527 => 在 view 裡面所以可以建立一個 view helper
view helper 只有 view 才能用
找一個 app/helpers/ 裡的檔案塞進去
session 或是 params 只有在 controller 跟 view 裡面才能取用 model 不能用
app/helpers/sessions_helper.rb
```ruby=
module SessionsHelper
  def user_signed_in?
    if session[:thankyou9527]
      return true
    else
      return false
    end
  end
end
```
index.html.erb
```ruby=
# 把判斷式改掉以防打錯資料
<% if user_signed_in? %>
  登出
<% else %>
  註冊 | 登入
<% end %>
```
#### 再建立一個小幫手提供使用者 email 顯示
```ruby=
<% if user_signed_in? %>
  <%= current_user.email %> | 登出
<% else %>
  註冊 | 登入
<% end %>
```
#### 根據model找到使用者資料
app/helpers/sessions_helper.rb
```ruby=
module SesssionsHelper
  def current_user
    User.find_by(id: session[:thankyou9527])
  end
end
```
使用 find 方法會跳激烈的錯誤訊息，造成被停權的使用者無法順利登出或進行動作

## 使用者登出
route
因為之前的route 後面可以指定 id，會造成可以幫其他使用者登出的風險
(DELETE => /users/:id(.:format) => sessions#destroy )
所以把它直接改掉
```ruby=
  resources :sessions, path: 'users', only: [] do
    collection do
      get :sign_in, action: 'new'
      post :sign_in, action: 'create'
      delete :sign_out, action: 'destroy'
    end
  end
```
sessions_controller
```ruby=
  def destroy
    # 登出 讓號碼牌失效 (撕票)
    session[:thankyou9527] = nil
    # 轉址
    redirect_to root_path
  end 
```
加入登出連結
index.html.erb
```ruby=
<% if user_signed_in? %>
  <%= current_user.email %> |
  <%= link_to '登出', sign_out_sessions_path, method: 'delete' %> 
<% else %>
  註冊 | 登入
<% end %>
```
確認route然後在view裡建立註冊登入連結
index.html.erb
```ruby=
<% if user_signed_in? %>
  <%= current_user.email %> |
  <%= link_to '登出', sign_out_sessions_path, method: 'delete' %>
<% else %>
  <%= link_to '註冊', sign_up_users_path %> |
  <%= link_to '登入', sign_in_sessions_path %>
<% end %>
```

:thankyou9527 是一個變數，讓全站都一樣就好
環境變數好處：統一管理
可以使用 figaro 套件
[github](https://github.com/laserlemon/figaro)
[查詢版號](https://rubygems.org/gems/figaro/versions/1.1.1)
把套件放在Gemfile裡面
如果上線需要使用就放在外面
如果只是開發需要使用就放在develop底下
terminal 執行 bundle
在 config/application.yml 裡設定變數
application.yml 不會進版控上線(包含敏感資料) 
所以 figaro 會建立一個，之後可以直接建立環境變數
在執行的時候不會執行 application，但是環境變數還是會進去
如果git clone的話會因為沒有那個檔案而出錯，可以私下跟擁有者要
如果要給對方看架構，可以把 appliction.yml 改成 application.yml.sample 把骨架給他看
所以以後如果拿到有.sample的檔案就可以把.sample刪掉然後再修改裡面的資料就跑得動了
 
拿到新專案記得去 config 裡面看有沒有 sample檔
把原來的變數換成環境變數
```ruby=
 def destroy
    # 把使用的變數改成環境變數名稱
    session[ENV['session_name']] = nil

    # 轉
    redirect_to root_path
  end
```
## 資料庫關聯

通常會加一個欄位做對應
慣例：要連結到的 model 名字小寫_id (格式是 integer)
```shell=
$ rails g migration add_user_id_to_restaurant
```

```ruby=
class AddUserIdToRestaurant < ActiveRecord::Migration[6.1]
  def change
    add_column :restaurant, :user_id, :integer
    add_index :restaurant, :user_id
    
    #等同上面兩行                 #會自動生成id
    add_references(:restaurants, :user) # 會自動產生 index
    # add_references(:restaurants, :user, index: false) => 不要產生 index
  end
end
```
可使用 add_reference 功能
[Ruby API](https://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html#method-i-add_reference)


如果一開始在建立 model 時已經想到了，可直接做關聯
```shell=
$rails g model Menu title restaurant_id:integer
$rails g model Menu title restaurant:belongs_to
```
建立關聯

model層的設定，使model互相有關聯
models/user.rb
```ruby=
  has_many :restaurants  #最好使用複數
  # 如果只有一間餐廳可用 has_one :restaurant
  # 有上面的設定才能使用下面的指令
  > u.restaurants -> Restaurant.where(user_id: u.id)

  has_many 會建立下面兩個方法
  - restaurants
  - restaurants=
```
has_many 做的東西有點像是 attr_accessor
會自動幫你生出好幾個方法，包含長出reader跟writer
* has_many 跟 belongs to 只是為了快速找到相關的東西跟建立方法使用
models/restaurant.rb
```ruby=
class Restaurant < ApplicationRecord
  belongs_to :user
end
```
設定 belongs_to 會把 user_id 設為必填值 (rails 5 之後的功能)
(如果要把它關掉的話可以寫 `belongs_to :user, optional: true` 把選填打開  `optional: false` 必填)

在找資料的時候可以連用方法
```shell=
> r1 = restaurant.first
> r1.user ## 要有 belongs_to 的話才可以使用
> r1.user.restaurants ## r1 擁有者所擁有的所有餐廳
## 極端用法
> r1.user.restaurants.first.user.restaurants.last.user.email
```

lazy querying

```ruby=
r2 = Restaurant.new(title: 'ccc')

>r2.errors.any?

>r2.errors.full_messages
#顯示錯誤訊息內容

>r2.user_id = 10
```
沒有要從餐廳反查使用者的資訊的話可以不使用 belongs_to

複習 SQL
```sql=
$ select * from restaurants where user_id = 7
$ Restaurant.where(user_id: 7)
$ Restaurant.where(user_id: u7.id)
```

在 restaurants_controller 驗證是否登入
```ruby=
def new
    if user_signed_in?  # 出錯，沒有這個方法
      @restaurant = Restaurant.new
    else
      redirect_to root_path
    end
  end
```
放新方法到 private 裡面
```ruby=
    def current_user
      User.find_by(id: session[ENV['session_name']])
    end

    def user_signed_in?
      if session[ENV['session_name']]
        return true
      else
        return false
      end
    end
```
加入確認是否登入的方法(放在private後面)
```ruby=
    def check_user!
      redirect_to root_path if not user_signed_in?
    end

```
執行方法前都會先執行這個方法
```ruby=
before_action :check_user!, except: [:index, :show]
```
簡化 new 方法
```ruby=
 def new
    @restaurant = Restaurant.new
  end
```
因為在其他controller也可能會使用，所以把方法直接放到上層裡去
之後如果需要確認使用者是否登入的話就直接使用
controllers/application_controller
```ruby=
    def current_user
      User.find_by(id: session[ENV['session_name']])
    end

    def user_signed_in?
      if session[ENV['session_name']]
        return true
      else
        return false
      end
    end

    def check_user!
      redirect_to root_path if not user_signed_in?
    end
```
因為 view helper 只有 view 可以用，controller 的方法只有 controller 可以用，
目前在 view 跟 controller 中都需要使用相同的方法，所以要寫兩次
所以可以在 controller 中加入特別的指令讓 view helper 也能有這個方法
controllers/application_controller
```ruby=
helper_method :current_user, :user_signed_in?
# 把方法匯出讓 helper 也可以使用
```
這樣 sessions_helper 裡的方法就可以拿掉
```ruby=
module SesssionsHelper
  # 變成什麼都沒有
end
```
或是可以直接把 view helper 的模組在 controller 裡面 include 進來
(如果在 view 裡面用的比較多就放在 view 裡面，在 controller 裡面用比較多就放在 controller 裡面，之後再做處理)

views/restaurants/_hello.html.erb
```ruby=
  <div class="field">
    <%= f.label :user %>
    <%= f.text_field :user %>
  </div>
```
如果這樣做的話可能會有不能放未清洗資料進系統的錯誤
直接清洗 user id 也可以解決但最好不要

restaurants_controller
```ruby=
def create
    # @restaurant = Restaurant.new(restaurant_params)  => 跟下面其中一個結合
    # @restaurant.user_id = current_user.id  => 把 id 傳給他
    # @restaurant.user = current_user  => 直接給他 user (同上一行)
    
    # 或是直接使用下面一行就可以了
    @restaurant = current_user.restaurants.new(restaurant_params)

    if @restaurant.save
      redirect_to restaurants_path
    else
      render :new
    end
  end
```
透過目前登入的 session 去抓欄位做更新，因為已經有對應的 user_id，所以會直接帶入

限制使用者只能看到自己擁有的餐廳
restaurants_controller (放在 private 底下)
```ruby=
    def find_restaurant
      # @restaurant = Restaurant.find(params[:id])
      
        # 方法一 餐廳角度出發
      @restaurant = Restaurant.find_by!(
        id: params[:id],
        user_id: current_user.id
      )
      
        # 方法二 使用者角度出發
      @restaurant = current_user.restaurants.find(params[:id])
    
    end    
```
如果 show 底下沒有要檢查的話：把本來加在 find_restaurant 底下的方法直接加到 show 底下，然後從 before_action 裡面拿掉
```ruby=
  before_action :find_restaurant, only: [:edit, :update, :destroy]

  def show
    @restaurant = Restaurant.find(params[:id])
  end
```
此時如果對非使用者所有的餐廳做編輯或刪除就會出現 404 (因為 before_action 裡的方法)

---
### 限制權限

- authentication 驗證、有無登入（門禁卡）
- authorization 授權，能不能做某件事情

---
### Rails 6 裡面的 JavaScript 要寫在哪裡？
 - 被拉到了第一層  
`app/javascript/packs/application.js`
- 可直接在裡面做編修
- 或是在`packs`層新建一個目錄，再創一個`ooxx.js` `import ''` 過去
- 如果在連結的介面做修改，重新整理要花很多時間 => 因為要整包重新編修
- 如果另外建一個資料夾放 js 檔的話，可以在裡面建立一個 `index.js` 把裡面的檔案都 import 進去
- 最後再從 packs 裡面把 `index.js` 引入
- 可以在 `javascript` 資料夾裡面加入一個 styles 資料夾，把 scss 或 css 檔案放進去，讓 webpack 一起打包
  - [CSS-in-JS・可寫文章展現自己的火力](https://2019.stateofcss.com/tw/technologies/css-in-js/)
- (原本在 assets/stylesheets 使用 assets pipeline)
- 缺點是要一次開兩個server
  - rails server
  - bin/webpack-dev-server



（JavaScript 的 import、require 差別）[可參考某網紅的文章](https://blog.niclin.tw/2019/10/03/nodejs-require-vs-es6-import-export/)

### webpack  =>  JavaScript 工具
webpacker gem  => 把 webpack 打包成一份ruby檔案或靜態檔 (為了讓大家都能使用 webpack 做的)

雖然設定麻煩又囉唆，但還是會妥協

$ bin/webpack-dev-server => 負責打包 rails 裡面 js 的東西

好處：會自動 reload 網頁、重整花較少時間

在 `config/webpacker.yml` 裡面會寫 webpacker 去哪裡找各個檔案包起來

---


ujs - 非侵入式的JS / Unobtrusive JavaScript
把 JavaScript 程式和 HTML 區分開來，讓程式碼看起來比較乾淨和整齊


[＊ SPA - 單頁應用 Single Page Application](https://en.wikipedia.org/wiki/Single-page_application)


---
## 題外話

GDPR 規定公司要問使用者是否可以塞 cookie 到使用者個電腦裡
個資法

request 送出時會有 header, body 瀏覽器版本 作業系統....
header 裡面會有一個是 cookie 資料
第一次進入時網站會問你是否可以塞入
可以的話下次瀏覽該網頁就會讀取你的cookie，如果有設定，網頁就會記住你的設定同不同意、取消、識別用

cookie 換裝置的話會無效 
cookie 會跟 header 裡面其他資訊配合起來之後使用，如果換電腦、換瀏覽器... cookie 就不會適用
cookie 會設定時效性，有些是用完就消失、有些是一天、有些是一個月...
例：flash[] 就是一種 cookie，用完就失效

User-agent
[MDN 說明](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent)

cookie hijacking
劫持 cookie
從網路設備把你的 cookie 攔下來，然後用你的 cookie 去做壞事
最好不要相信免費網路，經過特別的網路裝置可能就會被攔截 cookie

wireshark 分析網路行為
看裝置送封包到哪裡去

去 Gemfile 找 rack-mini-compiler 可以把左上角的查詢狀態關掉

如果使用rails 功能跑很久還出不來的話可使用：
```shell=
$ spring stop
// 將暫存停下來，偶爾卡住的時候可以使用
```

hotwire
讓前端和後端做即時通訊
websocket
透過actioncable向後端要東西放到 HTML 上
讓網頁按到某個地方的時候把form_for傳進來
讓 view 簡單化，hotwire 
[官網連結](https://hotwire.dev/)

[inline editing](https://demos.telerik.com/kendo-ui/grid/editing-inline)
inline editing rails
=> 新的 hotwire 會跟後端要東西蓋在原本表格上，編輯完在直接打回後端確認資料儲存並更新到前端

hirb-unicode 套件
https://rubygems.org/gems/hirb-unicode?locale=zh-TW
![](https://i.imgur.com/ahmPqmT.png)



ujs 非侵入式 javascript
在 html 裡面 data- 開頭的東西就是用這個

turbolink
xhr 非同步傳輸
跟一般HTML寫法不一樣
所以 DOMContentLoaded 會失效
前端工具

actioncable
js跟websocket工具
從後端叫前端把資料整個換掉

activestorage 檔案上傳使用

channel 即時訊息使用

