# 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 就可以使用

把前面的東西都指定給後面那個
```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

ujs 非侵入式 javascript
在 html 裡面 data- 開頭的東西就是用這個
turbolink
xhr 非同步傳輸
跟一般HTML寫法不一樣
所以 DOMContentLoaded 會失效
前端工具
actioncable
js跟websocket工具
從後端叫前端把資料整個換掉
activestorage 檔案上傳使用
channel 即時訊息使用