# [08/10_b] 假刪除/會員系統/Gemfile/session(含面試題)
手刻會員系統是面試題。
## 解說: Gemfile
https://railsbook.tw/chapters/09-using-gems.html
## 解說: session
HTTP是一種無狀態的通訊協定,在這邊做任何事情後,跳到其他頁面再回來並不會保留資訊。
可以透過Params 或 Cookies 讓瀏覽器能夠在切換頁面時記住資訊,
Rails 提供了所謂的 Session 可以更方便的操作,**用來記住登入的狀態、記住使用者購物車的內容等等**。
發 session(cookie) = 一種認證機制
拿到 cookie 不等於登入
登入 = 拿到 session 而且可以做裡面的事
登入成功會發令牌(cookie)給你,瀏覽器會留一個 session 做比對,看是否正確
會有時間性,如果過期或是對方 server 重新啟動都要再重新登入
Session 是一個實體方法,用法跟 Hash 很像。
可以把 Session 當作是 Hash,把 user.id 這個值存到屬性 thankyou9527,取名隨意。
```rb
session[:thankyou9527] = user.id
```
## 更改預設首頁
在Route第一行加入
```ruby=
get "/", to: "notes#index"
```
## 製作假刪除
在note增加「假刪除」欄位 deleted_at
1. 用rails g 增加一個migration(名 add_deleted_at)
2. 在db/migrate加欄位(column)與索引(index 增加速度)
3. 欄位三格代表: 增加欄位的表格, 新欄位名稱, 型態=時間戳記
4. 然後記得 $ rails db:migrate
```ruby=
def change
add_column :notes, :deleted_at, :datetime
add_index :notes, :deleted_at
end
```
### 方法一
**在Controller**(notes_controller.rb)
在def destroy方法
* 將原本的 @note.destroy改成刪除時間
* 讓他從原本的刪除變成更新資料 destroy 改 update
* 更新刪除的時間,預設都是 null 點了之後會變成 true
```ruby=
@note.update(deleted_at: Time.now)
```
在def index,
* 將原本的 @notes = Note.order(id: :desc)先關掉。
* 用where篩選顯示條件:設deleted_at沒資料(沒被按刪除 沒印時間)才印出來。
```ruby=
@notes = Note.where(deleted_at: nil).order(id: :desc)
```
### 方法二
定義類別方法。用available方法簡化那段where。
```ruby=
@notes = Note.available.order(id: :desc) #反向排序
```
被簡化的available,移到Model(note.rb)加為**類別方法(要加self)**
```ruby=
def self.available
where(deleted_at: nil)
end
```
### 方法三 scope
或寫為 用scope指向lambda
* 商業邏輯,以後如果要改需求改這邊就好
* 定義類別方法作「假刪除」,加入刪除時間,讓他看起來被刪掉
* 用 where 過濾,deleted_at 預設 nil,沒點刪除的都是 nil(有點,會加入假時間)
```ruby=
def self.available
where(deleted_at: nil)
end
scope :available
```
或寫為**預設值default_scope**
* default_scope 所有的搜尋都會加上後面的條件
* 一般的 scope 還是要在類別後面加這個方法名稱,default不用 => 不用寫 scope :available
```ruby=
default_scope { where(deleted_at: nil) }
```
取消scope的寫法
```ruby=
Note.unscope(:where)
```
#### 套件 pranoia
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)
## 註冊會員
**在Route**
增加users路徑,不需要show與index。
* 關掉 get "/users", to: "users#profile" ,對user只要開建user的路徑就好
* 不需要使用者列表(後臺看到就好)
* 用collection 追加路徑
* 方法: [:方法名稱] 取代GET路徑:
* GET /users/sign_up 註冊表單
* GET /users/sign_in 登入表單
```ruby=
resources :users, only: [:create] do
collection do
get :sign_up # GET /users/sign_up 註冊表單
end
end
```
**在Controller**
users_controller.rb 要定義新方法 @user = User.new(不讓網頁中form_for引用時是空的),在View要新增網頁。
```ruby=
def sign_in
@user = User.new
end
```
**新Model**
註冊也需要新欄位,都是string。
1. 用rails g 增加一個migration: rails g model User email password nickname
2. 然後記得 $ rails db:migrate
**在View**
在註冊會員網頁(sign_up.html.erb)增加form_for表格,依次放:
* Email
* 密碼
* 密碼驗證
* 帳號
指定@users的路徑,以免系統猜錯。
* email_field也是防呆機制(瀏覽器自動檢查)。
* 直接給@user明確路徑,以免它猜錯(可看路徑表)。*
```htmlmixed=
<%= form_for(@user, url: "/users") do |f| %>
<div class="field">
<%= f.label :email %>
<%= f.email_field :email %>
</div>
<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 %>
```
**回到Controller**
由於在Route已經設定sign_up送出時,only送到create。所以要在Controller裡設定後續(新增會員的動作):
```ruby=
def create
@user = User.new(user_params)
if @user.save
redirect_to "/"
else
render :sign_up
end
end
```
**在Model**
user.rb 加上驗證,要求若干表格「必填」
https://guides.rubyonrails.org/active_record_validations.html
```ruby=
validates :email, presence: true
validates :account, presence: true
validates :password, presence: true
```
**在Controller**
清洗資料,只允許這些表格(user_params 在 create)送資料
```ruby=
private
def user_params
params.require(:user).permit(:email,
:password,
:password_confirmation,
:account)
end
```
### 沒有password_confirmation
* 可先在Model利用Rails的一個Active Model,做出暫時可讀寫效果
* confirmation = 確認兩個欄位值是一樣的, uniqueness = 唯一值
> attr_accessor :password_confirmation
### 密碼加密
不要在db存進明碼,必須加密。查官方手冊Active Record Callback,可看到Model建立一筆資料的各個階段(before_save之類)。
在user.rb ,加入一個Model前的驗證器。
* 加密(digest)有MD5與SHA1兩種方法,在開頭引用require進來。
* 密碼加鹽巴之後,帶給類別方法self.password。
* 由於before_save在新增、更新時都生效,會重複加密,因此改掛before_create。
```ruby=
require 'digest' #也能放在外面這裡
before_create :encrypt_password
private
def encrypt_password
require 'digest'
salted_pw = "xyz#{self.password}827128#{self.password}82-12j23h"
self.password = Digest::SHA1.hexdigest(salted_pw)
end
```
### 提示訊息 password_confirmation
若兩次密碼不一樣,應該跳出提示訊息。
**在Model**(user.rb)利用confirmation驗證器的password驗證補寫。兒原本 attr_accessor :password_confirmation 已不需要,移除。
```ruby=
validates :password, presence: true, confirmation: true
```
帳號與郵件必須是唯一的(不重複),加上uniqueness,最後寫為:
```ruby=
validates :email, presence: true, uniqueness: true
validates :account, presence: true, uniqueness: true
validates :password, presence: true, confirmation: true
```
**在View**
在sign_up網頁開頭,也加入if any error,標出異常時上色。
```ruby=
<% if @user.errors.any? %>
<ul>
<% @user.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
<% end %>
```
## 會員登入
**在Route**
新增 get :sign_in
若用 post :sign_in 是拉出去做,會在網址列公開帳密
```ruby=
resources :users, only: [:create] do
collection do
get :sign_up # GET /users/sign_up 註冊表單
get :sign_in # GET /users/sign_in 登入表單
end
end
```
**在Controller**
直接給要定義新方法(不讓網頁中form_for引用時是空的),在View要新增網頁
```ruby=
def sign_up
@user = User.new
end
```
**在View**
在註冊會員網頁(sign_up)增加form_for表格,依次放:
* Email
* 密碼
方法只能是POST,因此依路徑表,只有 login_path
若放在Route的resources下,路徑會是 #POST /users/sign_in 登入
> post: sign_in
但建議分給不同的Controller。
* 登入跟註冊是兩件事情,註冊是一般的 CRUD,但登入會**用到 sessions**,所以把他獨立拉出來寫成SessionController
* sessions 是某個 controller 的名字
* **在Route**規定凡遇到登入,就連到session controller下的create。
* 寫入 sessions = 登入,用 as 換 path 名字,看起來比較乾淨
```ruby=
post "/users/sign_in", to: "sessions#create", as: "login"
```
* 也因此,目前網頁form_for的路徑會是'/users/sign_in'
再建立這個**新Controller**「sessions_controller」,並增加其create。
> rails g controller sessions
得到 sessions_controller.rb
* 一樣先在private下引入清洗資料,這次(登入)只需要2欄。
* 一樣驗證user_params
* 因為密碼已經加密過,如同user.rb引入一樣的撒鹽密碼(hashed_password),才能用find_by無誤驗證。
* if else 判斷
* 成功的話發session 且轉回首頁(session就類似cookie)
* 失敗的話回登入頁
* 登出 = 把sessions刪掉,之後在下方加 def destory
```ruby=
def create
# 驗證 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[:thankyou9527] = user.id # session自由取名 # 用 session 方法把 user.id 送進來
redirect_to "/"
else
redirect_to "/users/sign_in"
end
private
def user_params
params.require(:user).permit(:email, :password)
end
end
```
* 到def new增加檢查。若session是空的,就回到登入頁。
* 假設而已,可撤除。
```ruby=
def new
redirect_to "/user/sign_in" if session[:thankyou9527].nil?
@note = Note.new
end
end
```
確認路徑可寫為url: login_path,完善View的網頁表單。
```htmlmixed=
<%= form_for(@user, url: login_path) do |f| %>
<div class="field">
<%= f.label :email %>
<%= f.email_field :email %>
</div>
<div class="field">
<%= f.label :password %>
<%= f.password_field :password %>
</div>
<%= f.submit "Login" %>
<% end %>
```
## 顯示登入登出狀態(layout)
**在View**
在公版(layout)每一頁左上角放登入/登出狀態
* 在/view/layout/application.html.erb 的body內加nav
* 可再加一行把session印出來,原本是使用者數字,必須改印出email或名稱 =>再丟入helper裡面(print_out_session)。
> <%= User.find(session[:thankyou9527]).email %>
```htmlmixed=
<nav>
<%= link_to "登入", sign_in_users_path%>
<%= link_to "註冊", sign_up_users_path%>
<%= link_tp "登出", ""%>
<%= print_out_session %>
</nav>
```
* 建議nav與之後的footer獨立出去,在views下建目錄/shared/_新網頁.html.erb。
* application.html.erb 再渲染render回來。
```htmlmixed=
<body>
<%= render "shared/navbar" %>
<%= yield %>
<%= render "shared/footer" %>
</body>
```
承上,在獨立的 _navbar.html.erb 裡,區分登出/登入看到的不同狀態。
* 如果登入的話(新方法 user_signed_in? 後面在Helper建起來),會看到登出與Email狀態。
* 否則就需要登入或註冊。
經過後面Route的as寫法後,可以得到 logout_path 的路徑與方法,給登出用。
```htmlmixed=
<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>
```
在Helper定義方法
在app/helpers/ 下的sessions_helper 移到 users_helper
* 放在helper,所有的View都能用
* 方法名**current_user代表登入中的user**
```ruby=
def current_user
if session[:thankyou9527]
User.find(session[:thankyou9527])
else
nil
end
```
拉出新方法user_sign_in?方便分類。換句話說,登入時session不為空。
```ruby=
def user_sign_in?
session[:thankyou9527] != nil #
end
def current_user
if user_sign_in?
User.find(session[:thankyou9527])
else
nil
end
end
```
有需要的話,可以在stylesheets下建立任意scss檔案
> .{ mainfooter
> 樣式
> }
網頁_footer中可以寫
```htmlmixed=
<footer class="mainfooter">©版權所有</footer>
```
## 會員登出
Route的登入路徑下加一行 delete(如同post 來自路徑表)
```ruby=
post "/users/sign_in", to: "sessions#create", as: "login"
delete "/users", to: "sessions#destroy", as: "logout"
```
Route的**as:"login" 寫法**後,可以得到 login_path與 logout_path的路徑與方法。
而sing_in網頁中form_for的url連結,也能改為 logout_path
> <%= form_for(@user, url: login_path) do |f| %>
在sessions controller將剛剛新寫的destroy加進去。
把上面的if user複製改一下:如果沒有票(nil),就回首頁("/")。
```ruby=
def destroy
session[:thankyou9527] = nil
redirect_to "/"
end
```
為了讓View都能使用user_sign_in?,將它移到**最高層Controller(application_controller)**。
* 若不移,那controller與view helper,就要include helper(include UserHelper)。
* 但是,若向上面一樣user_helper移走之後,nav的 if user_sign_in 就壞掉了。
* 所以,在controller加一行 helper_method,讓helper繼續能用得到。
```ruby=
helper_method :user_sign_in?, :current_user #讓helper繼續用得到
private
def user_sign_in?
session[:thankyou9527] != nil #登入代表session不為nil
end
def current_user
if user_sign_in?
User.find(session[:thankyou9527]) #若找到user的session就能登入
else
nil
end
end
```
也將note_controller中def new開頭「檢查未登入(check_login!)if..end」移到**最高層controller**,仍保留 @note = Note.new 就好。
* 確認裡面要有這些方法
* user_signed_in?
* current_user
* check_login!
* record_not_found
```ruby=
def check_login!
if not user_signed_in?
redirect_to "/users/sign_in" #若未登入 回登入頁
end
end
```
而原本note_controller再掛回來,除了show與index,都要登入才能做(新增、修改、刪除)。
```ruby=
before_action :check_login!, except: [:index, :show]
```
#### Device套件
在model,可以掛 before_action :authenticate_user! 功能,直接檢查有無登入。
> rails g devise MODEL
MODEL 要換字! 看你要取什麼名字
套件裡面的 Wiki 有各式套件,照抄就好,抄官網最好,別亂抄來路不明的文章
#### Paranoia套件
套件安裝(按官方文件)
[paranoia](https://github.com/rubysherpas/paranoia)
放在 Model 裡面
直接用 acts_as_paranoid 內建套件取代 default_scope { where(deleted_at: nil) }
1. 到Gemfile貼上 "paranoia", "~> 2.2"
2. 終端機跑 bundle install
3. 到Model的rb貼上 acts_as_paranoid
## 補充:增加migration欄位
終端機 rails g migration 名字-通常會寫add作什麼
在Model目錄中的新rb檔內的change內,設定要作什麼。
## 補充:Lighthouse 燈塔
可以從開發者工具看,分析評分。
黃色以上較佳。
###### tags: `Rails`
###### tags: `面試題`