ASTRO Camp Day29 - RUBY(7)
RUBY 第七堂課
1、會員系統 User model
WishList第二個MODEL => user會員系統
新增會員會有的路徑
> /users/new => 會員註冊又等於新增user
> /users/create
> /users/show => show出會員資料
> /users/edit => 修改會員資料
> /users/update
> /users/destroy => 刪除會員資料
user路徑創造,我們這邊使用單數的resource,這樣show不用帶id
> resource :user
resource使用慣例
單數 :單數
複數 :複數
resource + only
路徑也可以這樣寫,這樣可以限制只產生指定action的路徑
resource :user, only:[:index, :show]
要記得把筆記寫完,應該是11/18 - 第二個影片
resource :user Vs. resources :users
把這兩者的路徑全部印出來 resource :user
沒有format :id
把這兩者的路徑全部印出來 resources :users
有format :id
做前台,不應該讓網址帶有id,像是會員系統的id、購物車的id,都不用讓它帶有id
2、User會員系統的CRUD
2-1、第一步驟:navbar
因為登入連結通常會做在navbar上,所以我們做一個navbar出來
不過因為是全站都看得到,所以寫在layout那邊
> <nav id="nav">
>
> <%= link_to "註冊", new_users_path %>
> <%= link_to "登入", "#" %>
> <%= link_to "登出", "#" %>
>
> </nav>
但是還記得之前我們做的shared資料夾嗎?我們可以把nav放在這邊,並用render渲染過去
> <body>
> <%= render "shared/navbar" %> # 這一行
> <%= render "shared/flash" %>
> <%= yield %>
> </body>
2-2、第二步驟:form_with
new頁面用form_with做表單,要注意事項
> <%= form_with(model: @user) do |form| %>
> <div>
> <%= form.label :nickname %>
> <%= form.text_field :nickname %>
> </div>
> <div>
> <%= form.label :email %>
> <%= form.email_field :email %>
> </div>
> <div>
> <%= form.label :password %>
> <%= form.password_field :password %>
> </div>
>
> <%= form.submit "註冊" %>
> <% end %>
but!!!,會發現form_with,會找不到users_path,因此要來更改一下路徑
> resource :user, as: "users"
這個可以把path變成都是負數
不過要記得在路徑後面+as 動的是 prefix,實際上網址還是單數
2-3、第三步驟:密碼再次輸入
2-3-1、confirmation
密碼再次確認的欄位 password_confirmation
model的confirmation方法,用這個東西可以不用再幫model開欄位,rails會幫你做雙重確認
# user的model
class User < ApplicationRecord
validates :email, presence: true
validates :password, confirmation: true # 加上密碼確認
end
user的view,增加這個欄位
> <div>
> <%= form.label :password_confirmation %>
> <%= form.password_field :password_confirmation %>
> </div>
2-3-2、length
再多加一個條件,密碼最小長度不能小於6個字 -> length: {minimum: 6}
# user的model
class User < ApplicationRecord
validates :email, presence: true
validates :password, confirmation: true, length: {minimum: 6} # 最小長度
end
2-4、第四步驟:user_params
做user的create,create的流程,記得先做資料驗證的方法(password_confirmation要記得給進去喔)
然後可以先把它params出來看看,會發現
def create
render html: user_params
end
private
def user_params
params.require(:user).permit(:nickname, :email, :password, :password_confirmation)
end
2-5、第五步驟:create user
把create寫進資料庫
def create
@user = User.new(user_params)
if @user.save
redirect_to root_path, notice: "註冊成功"
else
render :new
end
end
2-6、第六步驟:密碼加密
官網詳細callback => callback流程
Ps. creating an object -> 會有一整個週期,所以我們現在create物件時,要在某個階段,對密碼作加密
2-6-1、before_save
密碼加密原理
為啥要密碼加密呢?因為沒加密的話,存到資料庫的資料是使用者真實輸入的值,沒有轉換過,這樣是不對的
使用雜湊演算法 MD5 SHA1 SHA256
假設你今天的輸入值是123,他的輸出值會一樣
第一次輸入的時候,密碼會用加密演算法,轉成一段亂碼後,並把他丟進資料庫
之後還要進入此網站的話,使用者會再輸入一次密碼,如果密碼一樣,演算法轉出來的輸出值也會一樣
這樣就可以登入了
資料庫存的是被hexdigest演算法轉換後的結果
> User model
> class User < ApplicationRecord
>
> ....這邊有兩個validate(先省略)
>
> before_save :encrypt_password -> 設定好加密後,在before_save階段
>
> private
> def encrypt_password
> # HASH
> # self.password = 編碼過的password
> self.password = Digest::SHA1.hexdigest(self.password) -> ruby官網有標準函式庫可以做此加密
> end
> end
2-6-2、before_create
但!!!!有個很重要的東西,不只在create object有before_save,update object也有before_save
所以假設model寫before_save,就會在你更新實體的時候,再次加密(加密兩次),這會導致密碼直接不見喔!!
要改成before_create
> before_create :encrypt_password -> before_save改成before_create
2-6-3、灑鹽
透過sha-1加密後,還是覺得不夠安全,我們現在來增加密碼複雜度(這個動作稱為灑鹽 salting)
> self.password = Digest::SHA1.hexdigest("xy#{elf.password}zz") # 在原本密碼上加別的字串,增加複雜度
鹽(密碼學)
在密碼學中,是指在雜湊之前將雜湊內容(例如:密碼)的任意固定位置插入特定的字串。
這個在雜湊中加入字串的方式稱為「加鹽」。其作用是讓加鹽後的雜湊結果和沒有加鹽的結果不相同,
在不同的應用情景中,這個處理可以增加額外的安全性。
清掉資料庫所有資料
到rails c
User.destroy.all
Ps. 這個指令使用要小心
2-7、第七步驟:登入路徑
> 登入路徑預計為 : /user/login (不過目前沒有此路徑)
> resource :user, as: "users" do
> # /user/login # 我想要的網址是這條
> collection do # 用collection可以製造出上面那一條路徑
> get :login
> end
>
> end
創好路徑後,就可以把login的action、view設定好了
2-7-1、member VS. collection
member
如果今天想要新增的網址是有帶ID的,ex. /user/:id/login(.:format)
就可以使用member,用member做出的路徑是這樣
path | verb | url | Controller#Action |
---|---|---|---|
login_users_path | GET | /users/:id/login(.:format) | users#login |
collection
不過如果今天要沒有要讓新分頁有id,ex. /user/login(.:format)
path | verb | url | Controller#Action |
---|---|---|---|
login_users_path | GET | /users/login(.:format) | users#login |
那什麼時候會不想要讓連結內帶有id呢??
其實以登入的網址來說,就不會帶ID,因為帶ID的話,不就讓別人知道你的ID是多少了
2-8、第八步驟:session路徑
新增session的路徑
為什麼要新增這個路徑呢?
因為在做登入/登出的時候,其實算是額外不屬於user model的行為
因此你拿著只要拿著號碼牌,就可以登入、登出,所以我們把兩者分開寫
Ps. 我們的session路徑只需要新增和刪除
resource :session, only: [:create, :destroy]
2-9、第九步驟:login表單
User的login表單完成(這邊是user的view)
注意事項:
- 要記得這是User Model的表單,不過我們要Post到的地方是session的path(原因第八步驟有寫)
- 千萬要記得form_with用括號寫的話,和後面的url不能有空格,要不然會讀不到
- form_with可以不用寫model的原因是,今天不是在做新增,如果寫model的話,他會幫你做幾件事
- (1) 幫input欄位用:user包起來,這樣在params時候可以一包抓
- (2) rails 會幫你猜路徑
不過今天url是我們客制的(導到session_path),再加上也不太需要包成一包,兩個欄位分別params就好
所以這邊model可以不用寫。
> <%= form_with(url: session_path) do |form| %>
>
> <div>
> <%= form.label :email %>
> <%= form.email_field :email %>
> </div>
> <div>
> <%= form.label :password %>
> <%= form.password_field :password %>
>
> </div>
> <%= form.submit "登入" %>
>
> <% end %>
如果今天用form_for,這邊就不能不寫model,會報錯誤
2-10、第十步驟:session create
登入表單設計好後,要來寫完session的create了 流程是這樣
- 先抓住剛剛送過來來的兩個資料email、password,不過要記得分開抓,因為我們form表單沒寫model,不會包成一包
- 接著把登入者輸入的密碼,在做一次跟註冊帳號時一樣的加密流程,用find_by方法跟已經寫進資料庫的帳號比對
- 如果比對的結果是一樣的,就發一張號碼牌給該為使用者(現在還沒寫,後面會提到)
> session的create action
> def create
> # render html: params
> email = params[:email]
> password = params[:password]
>
> # hashed_password ...(password)
> hashed_password = Digest::SHA1.hexdigest("xy#{password}zz")
>
> user = User.find_by(email: email, password: hashed_password)
>
> if user
> # 發牌 # 這個等等寫
>
> # 重導
> redirect_to root_path, notice: "登入成功"
> else
> redirect_to login_users_path, alert: "登入失敗"
> end
>
> end
2-10-1、create流程優化
不過上面的寫法有點髒亂,我們整理一下
在User model多創一個login方法,把剛剛那些加密的邏輯寫進去,這樣就可以讓controller乾淨一點
此方法的邏輯
- 把剛剛加密、比對的兩行程式碼放過來
- 因為這個是User的類別方法,記得要加上self
> User model
> class User < ApplicationRecord
>
> def self.login(email, password)
> hashed_password = Digest::SHA1.hexdigest("xy#{password}zz")
> find_by(email: email, password: hashed_password)
> end
>
> end
session的create變這樣(原本兩行變一行)
> 舊
> hashed_password = Digest::SHA1.hexdigest("xy#{password}zz")
> user = User.find_by(email: email, password: hashed_password)
> 新(取代上面兩行)
> user = User.login(email, password)
session 的 create 變乾淨了
> class SessionsController < ApplicationController
> def create
>
> email = params[:email]
> password = params[:password]
>
> user = User.login(email, password)
> if user
> # 發牌
>
> # 重導
> redirect_to root_path, notice: "登入成功"
> else
> redirect_to login_users_path, alert: "登入失敗"
> end
>
> end
> end
為啥大部分商業邏輯放在model那邊
大部分的商業邏輯都整理在model裡面
因為model很常重複使用
controller基本上不常重複使用
所以會把一些很常重複使用的方法寫在model那邊
2-11、第十一步驟:發牌-session[:handsome]
什麼叫登入成功??
成功拿到號碼牌,才算登入成功
如果只是帳號密碼比對成功,只是比對資料,不算是登入喔
—
2-11-1、session vs cookie
使用者今天進來網站,登入成功會拿到一張號碼牌,這個號碼牌就是cookie,session則是會存在server那邊
- session是server端的(櫃檯是session)
- cookie是使用者端的(瀏覽器會存cookie,使用者拿cookie)
最後這兩張號碼牌會對起來,假設server、使用者的號碼牌有對起來,這樣就可以讓使用者登入
假設server重開,導致session今天整個重設,就算cookie一樣還活著,使用者一樣登不進去
接著我們就把到session的create,把號碼牌發給登入資料比對成功的使用者
> if user
> # 發牌
> session[:handsome] = user.id # 如果資料比對成功的話,發一張號碼牌給這個使用者
> # 重導
> redirect_to root_path, notice: "登入成功"
> else
> redirect_to login_users_path, alert: "登入失敗"
> end
號碼牌的注意事項
session[:handsome] = user.id
(1) [:handsome] > 這個變數可以隨便亂取,只要記得就好
(2) user.id > 這個也可以隨變取,但是通常我們知後會要用到剛剛登入者的資料,顯示在瀏覽器上,所以給他有意義的變數
2-12、第十二步驟:session登出
把號碼牌丟掉,把session設定為nil,這樣就可以登出了
> def destroy
> session[:handsome] = nil
> redirect_to root_path, notice: "已登出"
> end
2-13、第十三步驟:按鈕狀態顯示
到設定navbar的地方,把判斷登入要出現的按鈕和登出會顯示的按鈕切開 假設現在有拿到號碼牌,navbar應該要顯示登出,如果現在還沒拿到號碼牌,應該要顯示登入、註冊
這裡在shared/_navbar/html/erb新增判斷
> <nav id="nav">
> <% if session[:handsome] %>
> <%= link_to "登出", session_path, method: "delete" %>
> <% else %>
> <%= link_to "註冊", new_users_path %>
> <%= link_to "登入", login_users_path %>
> <% end %>
> </nav>
2-14、第十三步驟:current_user
接著我想在navbar顯示使用者的名字,可以怎麼做
Ps. 還記得我們前面session[:handsome]有先 = user.id嗎?這時候我們就可以拿來用了
2-14-1、第一種方法 - 直接寫在navbar
> <nav id="nav">
> <% if session[:handsome] %>
> <% u = User.find_by(id: session[:handsome]) %> # 先抓到登入者id
> <%= u.nickname %> # 使用者id的nickname
> <%= link_to "我的許願卡", new_wish_path %>
> <%= link_to "登出", session_path, method: "delete" %>
> <% else %>
>
> <%= link_to "註冊", new_users_path %>
> <%= link_to "登入", login_users_path %>
> <% end %>
> </nav>
2-14-2、第二種寫法,寫在helper上
第一種寫法,會讓view頁面看起來好醜(太多不屬於view頁面的判斷式了)
到user的helper寫上current_user方法
> module ApplicationHelper
> def current_user # 新增這個方法
> User.find_by(id: session[:handsome]) # 如果有找到id,會回傳一個實體,否則回傳nil
> end
> end
不太清楚的話,我們把current_user印出來看看(所有的view頁面都可以印出來)
> <%= current_user %> # <User:0x000000010a7be9f8>,印出一個實體
接著我們就可以用current_user把東西印出來了
> <nav id="nav">
> <%# if session[:handsome] %>
>
> <%= current_user.nickname %> # 這一行
>
> <%= link_to "註冊", new_users_path %>
> <%= link_to "登入", login_users_path %>
> <%# else %>
> <%= link_to "登出", session_path, method: "delete" %>
> <%# end %>
> </nav>
2-14-3、優化current_user效能
如果今天要連續抓current_user五次,這樣會非常耗效能(連續賦值五次)
使用||=的預設值概念,有了這個東西後,一開始沒有該值,就給User.後面那一串,有的話就@user
因此來稍微改寫一下current_user的寫法
> module ApplicationHelper
> def current_user
> @_user_ ||= User.find_by(id: session[:handsome])
> end
> end
把值存在實體變數裡面(那個變數的名字不重要,只要跟別的做好區隔就可以),如果第一次呼叫current_user這個方法
就把抓到的實體賦予到一個變數裡面,如果後面再繼續呼叫這個方法,就直接把剛剛那個變數丟給他就好
Ps. 這個手法叫做memorization(紀錄),可以幫助SQL語法不會重複做事
2-14-4、製作”user_signed_in?”方法
除了current_user外,我們再把navbar的view檔案優化一樣,把原本如果有發號碼牌,改成比較好閱讀懂的方法
首先到helper,製作新方法
> def user_signed_in?
> !!session[:handsome]
> end
Ps. 兩個驚嘆號的意思是,可以強制把一個字串、數字轉成boolean,第一個!可以轉成false,第二個!轉成true
> 詳解兩個驚嘆號
> session[:handsome] # 一開始 = user.id
> !session[:handsome] # 一個驚嘆號 = false
> !!session[:handsome] # 第二個驚嘆號 = true
這樣修改後,就可以讓這個方法取代原本的是否有登入的判斷
> <% if user_signed_in? %>
> <% end %>
2-15、第十四步驟:登入狀態顯示頁面
接下來設定有登入才能有權限看的頁面,如果沒有登入,把某些頁面關掉(不能看到)
Ps. 如果你沒有號碼牌,就有把你踢走
到wishlist的new頁面,多加一段判斷,如果有號碼牌,才讓你可以new新實體
def new
if session[:handsome]
@wish_list = WishList.new
else
redirect_to login_users_path, alert: "請先登入"
end
end
2-16、第十五步驟:優化未登入狀態authenticate_user!
因為除了new,其他create……等等action,也要用到是否登入的判斷
因此我們多做一個方法,讓這些action在啟動前就先觸發
> wishlist controller
> before_action :authenticate_user! # 全部的action都會做登入判斷
> 如果沒有號碼牌,那就跳回登入頁面,並且顯示請你登入的訊息
> def authenticate_user!
> redirect_to login_users_path, notice: "請先登入" unless session[:handsome]
> end
不過authenticate_user在其他地方也會用到,所以把這個加到application那邊
> class ApplicationController < ActionController::Base
>
> ...上面還有其他的方法先省略
>
> def authenticate_user!
> redirect_to login_users_path, alert: "請先登入" unless session[:handsome]
> end
> end
2-17、第十六步驟:include helper
我們再來優化一下剛剛的authenticate_user!,把號碼牌的領取,用剛剛寫在helper的的方法user_signed_in?
> 換比較好懂的寫法(不要用unless),如果沒有登入就踢出去
>
> def authenticate_user!
> if not user_signed_in?
> redirect_to login_users_path, alert: "請先登入"
> end
> end
2-17-1、helper使用注意事項
但!!!!上面那個寫法會報錯喔,會噴MethodError!!為什麼呢??
原因就是因為,寫在helper的方法,是給所有的view用的,不是給controller用的,所以現在
要在controller使用,當然是不行的
不過要解決的方法也非常簡單,只要在controller,import小幫手的模組就好了,這樣就可以讓controller使用helper
—
讓寫在helper的方法,讓controller可以用
> class ApplicationController < ActionController::Base
> rescue_from ActiveRecord::RecordNotFound, with: :record_not_found
>
> include ApplicationHelper # 把helper import過來
>
> def record_not_found
> render file: "#{Rails.root}/public/404.html", status: 404, layout: false
> end
>
> def authenticate_user!
> if not user_signed_in? # 這裡就可以用helper了
> redirect_to login_users_path, alert: "請先登入"
> end
> end
> end
讓controller的東西可以給view用
在controller的檔案,寫上下面這一行
helper_method :current_user, :user_signed_in?
Ps. 那兩個方法一樣是寫在controller喔,只是我們把方法匯入過去
2-18、rails 的會員套件推薦
devise 會員套件
sorcery 會員套件
3、user、wishlist的關聯性
建立user、wishlist的關聯性
幫wishlist開一個user_id欄位,連結到user
3-1、migration add_index、add_belongs_to
3-1-1、第一種寫法 - 開一個數字欄位
> def change
> add_column :wish_lists, :user_id, :integer
> add_index :wish_lists, user_id
> end
3-1-2、第二種寫法 - 開一個references欄位
> def change
> add_belongs_to :wish_lists, :user
> end
add_belongs_to = add_references
上面兩種寫法,都會在wish_lists的table產生下面兩行
t.integer "user_id"
t.index ["user_id"], name: "index_wish_lists_on_user_id"
二元樹
無聊可以看一下
到user的model,去寫has_many(一個使用者,有很多張的許願卡)
> User Model
> has_many :wish_lists
>
> 這樣寫,可以讓user多幾種方法用
> #wish_lists
> #wish_lists=
> #build
> #create
還有到WishList的model,寫上belongs_to
> WishList Model
> belongs_to :user
has_many、belongs_to沒有一定要寫,他們只是幫你創造出一些方法,讓你方便存取
3-2、建立關聯後,新增新卡片
但是因為我們現在多了一個user_id的欄位,而且已經建立關聯了,所以在新增實體的時候,此欄位一定要存在
所以用current_id設定為新增許願卡的人的id(還記得我們前面有號碼牌才進得來,所以才能這樣設定)
> def create
> @wish_list = WishList.new(clean_params)
> @wish_list.user_id = current_user.id
>
> ...下面是資料儲存後成功與否的判斷
> end
不過上面那樣寫很醜,這兩行被下面那一行取代
> @wish_list = WishList.new(clean_params)
> @wish_list.user_id = current_user.id
>
> @wish_list = current_user.wish_lists.new(clean_params)
> 由user的角度,創造出一張新的許願卡,而不是把user_id塞進去
最終wish_list的create的樣子
> def create
>
> @wish_list = current_user.wish_lists.new(clean_params)
>
> if @wish_list.save
> redirect_to make_a_wish_path, notice: "success new wish_list"
> else
> render :new
> end
> end
3-3、登入者只能看到自己的東西
還記得我們before_action有設定一個find_wish_list的方法嗎?他是在做edit等等action可以操作的範圍
現在我們要限制,只有登入者能對自己的資料crud(總不會讓別人也能修改你的資料)
> before_action :find_wish_list, only: [:edit, :update, :show, :destroy]
因此我們修改一下方法,一樣用current_user實體的角度出發,去找許願卡的id
> def find_wish_list
> # @wish_list = WishList.find_by(id: params[:num]) # 舊的,可以看到所有的資料
> @wish_list = current_user.wish_lists.find(params[:id]) # 新的,只能看到自己登入的資料
> end