# [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">&copy;版權所有</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: `面試題`