13 Mayıs 2025 Salı

Rails 7 Denemeler 7

https://ujk-ujk.blogspot.com/2025/05/rails-7-denemeler-7.html
İçindekiler +

 

Selam Rails 7 öğrenmeye devam ediyorum. 



Basit Login İşlemi

Kullanıcılar artık sitemize kayıt olabiliyor, sıra geldi kullanıcıların sisteme giriş ve çıkış yapmaları için login ve logout işlemlerini tanımlamaya. Basit ama çalışan bir sistem tasarlayacağız, kullanıcı tarayıcısını kapatana kadar giriş yapmış olarak kalmasını sağlayacağız. Sonuç yetkilendirme sisteminde kullanıcılara yetkilerine göre gerekirse site yapısını bile değiştirme izinleri vereceğiz. 



Oturumlar 

HTTP durumları olmayan bir protokoldür ve her gelen isteği. bağımsız işler. Bu yüzden daha önce yapılmış olan işlemlerle ilgili bilgi sahibi değildir. İşte bunun anlamı giriş yapan kullanıcı hakkında öyle bir şey yapmalıyız ki yetkili olduğunu bilelim. Kullanıcı girişi yapılan web uygulamaları bu yüzden oturumları (session) kullanır. Bu iki bilgisayar arasında yarı kalıcı bir bağlantıdır (mesela kullanıcının bilgisayarındaki tarayıcı ve server'daki Rails uygulaması arasuında bir bağlantı gibi). 

Rails'de en yaygın kullanılan oturum açma biçimi cookie (çerez) kullanmaktır. Bunlar kullanıcının bilgisayarının tarayıcısında saklanan küçük yazılardır ve sayfadan sayfaya geçerken değişmediği için kullanıcının oturumunun açık kalması için gereken id değerleri gibi şeyleri kaydetmek için kullanılabilir. Burada Rails'in session metodunu kullanarak kullanıcı tarayıcısını kapatana kadar açık kalacak oturumlar oluşturacağız. Daha ileride buna benzer ama daha uzun süren oturumlar için cookies metodunu göreceğiz. 

Oturum oluşturmak için genel eğilim olarak REST yapısında bir model kullanılır. Login sayfasına gidilirse yeni bir oturum için form gösterilir (new). Girişin gerçekleşmesi durumunda bir oturum nesnesi oluşur (create). Kullanıcı sistemden çıkış yapınca oturum nesnesi sonlandırılır (destroy). Kullanıcılar için oluşturduğumuz User modeli kayıtlarını veri tabanında saklıyordu, ancak oturumlar verileri saklamak için cookie kullanacak. Bu ve sonraki bölümde bir Sessions kontrolörü oluşturacağız, bir login formu ve ilgili kontrolör eylemlerini tanımlayacağız. Daha sonra ilgili oturum işleme kodlarını ekleyerek sisteme giriş yapma olaylarını bitireceğiz. 



Sessions kontrolörü

REST yapısına karşı gelen giriş çıkış işlemlerine bakarsak, login işlemi new eylemine GET isteği ile bir form gösterecek, form gönderilince (POST) create eylemi çalışacak ve oturumu başlatacak, sistemden çıkmak için de destroy eylemine bir DELETE isteği göndermek gerekir. 

Kontrolörümüzü oluşturarak başlayalım.

$ rails generate controller Sessions new


Sadece new eylemini koyduk, çünkü burada vereceğimiz eylemlere karşılık görsel dosyaları da otomatik olarak üretilir. Bizim sadece new eylemi için bir form görseline ihtiyacımız var, create ve destroy eylemlerinde görsel olmayacağı için komutta kullanmadık daha sonra elle ekleyeceğiz. 

Uygulama sayfalarımızın üzerindeki navigasyon barında bağlantısını yapmadığımız bir Giriş Yap linki vardı, ona tıklanınca bir login formu açmayı planlıyoruz. Formumuzda sadece email ve şifre girmek için kutular ve bir gönderme butonu olacak. Yeni kullanıcılar bu sayfaya gelirse kayıt olmak için de bir link koysak iyi olur. 

Users resource tanımlarken tüm REST eylemleri kullanmak için 

  resources :users

satırı ile tüm eylemleri otomatik tanımlamıştık, ancak Sessions resource için sadece login adrsine GET ve POST isteklerini ve logout adresine yapılan DELETE isteğini bağlamak için tek tek elle routes.rb dosyasına ekleyeceğiz. Ayrıca Rails generate komutuyla otomatik oluşturulan yönlendirmeyi de sileceğiz. 

config/routes.rb

Rails.application.routes.draw do
  # get "sessions/new"
  root "static_pages#home"
  get "/help", to: "static_pages#help"
  get "/about", to: "static_pages#about"
  get "/contact", to: "static_pages#contact"
  get "/signup", to: "users#new"
  get "/login", to: "sessions#new"
  post "/login", to: "sessions#create"
  delete "/logout", to: "sessions#destroy"
  resources :users
.....
end


İlk yapmamız gereken bu yönlendirmelere göre kontrolör üretilirken otomatik oluşan test rutinini düzenlemek.

test/controllers/sessions_controller_test.rb

require "test_helper"

class SessionsControllerTest < ActionDispatch::IntegrationTest
  test "should get new" do
    get login_path
    assert_response :success
  end
end


Bu eklediğimiz yönlendirmelerin de bir tablosunu düşünürsek

HTTP metod   URL        Eylem      İsimlendirilmiş     Amacı
--------------------------------------------------------------------------------
GET         /login      new        login_path          Yeni oturum açma sayfası
POST        /login      create     login_path          Yeni oturum üretmek
DELETE      /logout     destroy    logout_path         Oturumu kapatma işi


Şu ana kadar bir sürü yönlendirmeyi uygulamamıza ekledik, neler var diye kontrol etmeye kalksak, terminalde görebiliriz.

$ rails routes
   Prefix    Verb   URI Pattern            Controller#Action
   root      GET    /                      static_pages#home
   help      GET    /help(.:format)        static_pages#help
   about     GET    /about(.:format)       static_pages#about
   contact   GET    /contact(.:format)     static_pages#contact
   signup    GET    /signup(.:format)      users#new
   login     GET    /login(.:format)       sessions#new
             POST   /login(.:format)       sessions#create
   logout    DELETE /logout(.:format)       sessions#destroy
   users     GET    /users(.:format)       users#index
             POST   /users(.:format)       users#create
   new_user  GET    /users/new(.:format)     users#new
   edit_user GET    /users/:id/edit(.:format)  users#edit
   user      GET    /users/:id(.:format)       users#show
             PATCH  /users/:id(.:format)       users#update
             PUT    /users/:id(.:format)       users#update
             DELETE /users/:id(.:format)       users#destroy


Bu çıktıyı daha anlaşılabilir olsun diye biraz düzenledim ve şu anda bizim ilgimizde olmayan yönlendirmeleri listeden çıkardım. Amacım şunu söylemek,  rails routes komutunu kullanarak uygulamamızda kullanılan adresleri isimlendirilmiş şekillerini ve hangi kontrolörün hangi eylemine ait olduğunu görebiliriz. 



Login formu

İlgili kontrolör ve yönlendirmeleri tanımladıktan sonra sıra geldi login formumuzu tanımlamaya. Login formumuz aşağı yukarı kayıt olma formunun sadece email ve şifre olan şekli gibi. 

Daha önce kayıt formunda yaptığımız gibi yanlış veri girilerek sisteme giriş yapmaya kalkılınca bir hata mesajı vererek tekrar forma döneceğiz. Ancak daha önce hata mesajları ActiveRecord tarafından otomatik oluşuyordu ve bizim Session nesnemiz bir ActiveRecord nesnesi değil. Bu yüzden oturumlar için flash mesaj yöntemini tercih edeceğiz. 

Önce forma odaklanalım, daha önce kayıt olma form görselinde bir blok yapı kullanmıştık.

    <%= form_with model: @user do |f| %>
      ...
...
...
    <% end %>


Oturum açma formu ile kayıt olma formu arasındaki fark bir Session modelimizin olmaması. ve bu yüzden @user gibi bir @session değerimiz yok. Burada form_with metodunu farklı parametreler ile kullanacağız. 

form_with model: @user

ile formun gönderme eyleminin /users adresine bir POST isteği olarak gerçekleşeceğini bildiriyoruz. Üretilen form elemanına bakarsak

<form action="/users" accept-charset="UTF-8" method="post">

olduğunu görürüz. Konu oturumlara gelince hedef URL ve kapsamı belirterek form oluşturacağız.

form_with url: login_path, scope: :session


Şimdi bu bilgiler doğrultusunda login sayfamızın görselini düzenleyelim.

app/views/sessions/new.html.erb

<% provide(:title, "Log in") %>
<h1>Giriş Yapınız</h1>

<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <%= form_with(url: login_path, scope: :session) do |f| %>
      <%= f.label :email %>
      <%= f.email_field :email, class: 'form-control' %>
      <%= f.label :password, "Şifre" %>
      <%= f.password_field :password, class: 'form-control' %>
      <%= f.submit "Giriş", class: "btn btn-primary" %>
    <% end %>
    <br/><p>Yeni Kullanıcı? - <%= link_to "Kayıt olun!", signup_path %></p>
  </div>
</div>


Giriş yap linkini henüz bağlamadık, ancak sayfayı http://localhost:3000/login adresinde görebiliriz.


Kodumuz tarafından oluşturulan formun HTML'i şuna banzer.

<form action="/login" accept-charset="UTF-8" method="post">
  <input type="hidden" name="authenticity_token" value="..." autocomplete="off">
  <label for="session_email">Email</label>
  <input class="form-control" type="email" name="session[email]" id="session_email">
  <label for="session_password">Şifre</label>
  <input class="form-control" type="password" name="session[password]"
      id="session_password">
  <input type="submit" name="commit" value="Giriş"
      class="btn btn-primary" data-disable-with="Giriş">
</form>


Buradan da görüleceği üzere gönderilen formdaki bilgilere params[:session][:email] ve params[:session][:password] isimleri ile erişebileceğiz. 



Kullanıcıyı bulmak ve yetkilendirmek

Kayıt işlemlerinde olduğu gibi login işlemlerinde de ilk adım geçersiz girişleri işlemek. Öncelikle form gönderilince neler olduğunu inceleyip, sonrasında geçersiz girişler için bir mesaj yayınlaması yapacağız. En son da geçerli giriş yapılmasını çalışacağız. 

Kontrolörümüzde minimalist bir create eylemi ve boş new ve destroy eylemleri tanımlayarak başlayalım. create eyleminde şimdilik sadece sanki geçersiz giriş yapılmış gibi new eylemine bir yönlendirme yapalım. 

app/controllers/sessions_controller.rb

class SessionsController < ApplicationController
  def new
  end
  def create
    render "new", status: :unprocessable_entity
  end
  def destroy
  end
end


Şimdi bir giriş yapmaya çalışıp formu gönderince debug parametrelerinde gönderdiğimiz bilgileri görebiliriz.

#<ActionController::Parameters {
    "authenticity_token"=>"...",
    "session"=>{
      "email"=>"user@example.com",
      "password"=>"foobar"
    },
    "commit"=>"Giriş",
    "controller"=>"sessions",
    "action"=>"create"
  } permitted: false>


Burayı dikkatli incelersek params değerinde iç içe hash değerler mevcut , bunlardan biri

{"session"=>{ "email"=>"user@example.com",  "password"=>"foobar" } }

Yani params[:session] değişkeninde 

{ "email"=>"user@example.com",  "password"=>"foobar" }

Hash değerini alırız. Bu durumda params[:session][:email] değişkeninde formda gönderilen email adresini ve params[:session][:password] değişkeninde girilen şifreyi okuruz. 

Bir diğer deyişle create eylemi içinde (çağrıldığında - form gönderildiğinde) params değişkeni içinde kullanıcıyı yetkilendirmek için gereken tüm bilgiler var. Şimdi ActiveRecord sınıfının sağladığı User.find_by metodu ve has_secure_password ile otomatik olarak gelen authenticate metodlarından yararlanacağız. 

Verilen değerlerle eğer kullanıcı email değeri tablomuzda varsa bir User nesnesi oluşturacağız ve bu nesnenin authenticate metodunu verilen şifre ile çağırırsak ve eğer şifre doğruysa true değer dönecektir. Şimdi create eylemimize bu kontrolü yapmak için bir ilave yapalım.

app/controllers/sessions_controller.rb

class SessionsController < ApplicationController
  def new
  end

  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      # Kullanıcı girişi geçerli yap ve profiline gönder
    else
      # Bir hata mesajı üret
      render 'new', status: :unprocessable_entity
    end
  end

  def destroy
  end
end


İlavelerimizde ilk satırda girilen email adresine sahip kullanıcı kaydını veri tabanından buluyoruz. Email adreslerini küçük harf olarak kaydetmiştik, bu yüzden girilen email adresini de küçük harfe çevirerek tabloda arıyoruz. Sonraki satır biraz kafa karıştırıcı gibi görünüyor ama hem Rails hem de Ruby programlamada çok kullanılan bir tekniktir.

    if user && user.authenticate(params[:session][:password])

Burada && işleminin (and) true çıktı vermesi için her iki tarafında da verilen işlemlerin sonucu true olmalıdır. Eğer user nesnesi yoksa , yani kayıt bulunamadıysa ilk işlem false değeri döner ve && işlemi sağ tarafa bakmadan olumsuz sonuç verir. Ruby'de karşılaştırma yaparken false ve nil harici tüm değerler true kabul edilir. Verilen email adresine sahip bir kullanıcı tabloda bulunmuyorsa user nesnesi değeri nil olacaktır. 

Eğer kullanıcı tabloda bulunmuşsa && işlemi sağ tarafına geçilir, ve o kullanıcı nesnesinde authenticate metodu girilen şifre ile çağrılır. Eğer şifre doğruysa authenticate metodu true değer döner ve yetkilendirme için gerekenleri ekleyeceğimiz bloğa girilir. 



Flash mesajı ile yönlendirme

Daha önce User nesnelerini eklerken hata olduğunda ActiveRecord tarafından üretilen otomatik mesajları kullanmıştık. Burada aynısını yapamayacağız , çünkü Session nesnemiz bir ActiveRecord nesnesi değil. Burada hata mesajı göndermek için, daha önce yeni kullanıcı kaydı başarılı olunca hoş geldin mesajı vermek için kullandığımız flash değişkenini kullanacağız. 

app/controllers/sessions_controller.rb

....
  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      # Kullanıcı girişi geçerli yap ve profiline gönder
    else
      flash[:danger] = 'Geçersiz email/şifre kombinasyonu' # Tam doğru değil!

      render 'new', status: :unprocessable_entity
    end
  end
....


Flash mesajının yayınlanmasını yerleşim dosyasında yaptığımız için default yerleşimi kullanan her sayfada olduğu gibi login sayfamızda da flash mesajları görünecektir. 

      <% flash.each do |mesaj_tipi, mesaj| %>
        <div class="alert alert-<%= mesaj_tipi %>"><%= mesaj %></div>
      <% end %>

Bu koda göre geçersiz kullanıcı giriş yapmaya kalkınca 

<div class="alert alert-danger">Geçersiz email/şifre kombinasyonu</div>

Elemanı oluşturulacak ve Bootstrap'ta bu eleman için tanımlı sınıflar ile stili otomatik gelecektir. Şimdi geçersiz bir giriş deneyelim.


Gördüğümüz gibi Bootstrap hazır CSS kurallarını kullanarak kolayca görsel etkiler oluşturabiliyoruz. Flash mesajı kodlarken yanına yorumda tam olmadı yazmıştık, nedeni render ile yapılan yönlendirmenin tarayıcıdan yapılan bir istek olarak değerlendirilmemesidir. Bu durumda flash değişkeni tarayıcıdan başka bir istek yapıldığında hala değerini koruyor olacaktır. Yani örneğin geçersiz bir giriş yapmayı denedik ve tekrar mesaj ile aynı form açıldı, ve kullanıcı burada yukarıdan Ana Sayfa linkine tıklayıp geçiş yaptı, ana sayfada hala mesaj görünür kalacaktır.


Sayfayı yenilersek ikinci defa istek yapılınca flash değişkenindeki değer silinecek ve mesaj yok olacaktır. Şimdi bu hatayı test için bir rutin yazıp sonra da hatayı düzeltmeye çalışalım. 



Flash için bir test

Bu yanlış flash davranışı uygulamamızda küçük bir bug. Daha önce öğrendiklerimize binaen bu hatayı test eden bir test rutini yazarak yolumuza devam edebiliriz. Login formunun gönderilmesi için kısa bir entegrasyon testi hazırlayacağız. Bu bize daha ileride yapacağımız benzer entegrasyon testleri için de örnek olacak. İlk önce terminalde entegrasyon testini üreterek başlayalım.

$ rails generate integration_test users_login
      invoke  test_unit
      create    test/integration/users_login_test.rb


Şimdi neler yapacağımızı kafada bir toplayalım.

  1. Login sayfasını ziyaret edeceğiz
  2. Kullanıcı giriş formunun sağlıklı yayınlandığını göreceğiz
  3. Geçersiz bilgilerle dolu bir bir params değeri ile formun gönderilmesini sağlayacağız (POST)
  4. Kullanıcı giriş formunun tekrar ve beklenen status koduyla geldiğini göreceğiz
  5. Flash mesajının da geldiğini göreceğiz
  6. Başka bir sayfayı ziyaret edeceğiz (örn. Ana Sayfa)
  7. Flash mesajının artık görünmediğini kontrol edeceğiz

Bu doğrultuda hazırladığımız test dosyası kodu.

test/integration/users_login_test.rb

require "test_helper"

class UsersLoginTest < ActionDispatch::IntegrationTest
  test "geçersiz bilgi ile giriş" do
    get login_path
    assert_template 'sessions/new'
    post login_path, params: { session: { email: "", password: "" } }
    assert_response :unprocessable_entity
    assert_template 'sessions/new'
    assert_not flash.empty?
    get root_path
    assert flash.empty?
  end
end


Bu testi şu anda denersek başarısız olacaktır. Sadece bu testi denemek için.

$ rails test test/integration/users_login_test.rb
Running 1 tests in a single process
...
Failure:
UsersLoginTest#test_geçersiz_bilgi_ile_giriş
[test/integration/users_login_test.rb:12]:
Expected false to be truthy.
...
1 runs, 5 assertions, 1 failures, 0 errors, 0 skips

Burada 12. satırda yani 

    assert flash.empty?

Beklentisi gerçekleşmedi diyor, yani ana sayfaya geçince hala hata mesajı görünüyor (flash değeri bir şey içeriyor, nil değil). 

Burada çözümü flash yerine flash.now nesnesini render öncesi kullanarak yaparız. flash.now nesnesi böyle render işlemleri için tanımlanmıştır, ve sadece bulunulan eylem içinde geçerlidir.

app/controllers/sessions_controller.rb

....
  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      # Kullanıcı girişi geçerli yap ve profiline gönder
    else
      flash.now[:danger] = 'Geçersiz email/şifre kombinasyonu'
      render 'new', status: :unprocessable_entity
    end
  end
....


Şimdi test denersek başarılı olacaktır. 



Başarılı giriş işlemi

Başarısız giriş denemelerini işledik, sırada başarılı giriş işleminde yapılacaklar var. Bu bölümde bir oturum cookie'si yardımı ile tarayıcı kapatılana kadar kullanıcıyı giriş yapmış göstermesini öğreneceğiz. İleride nasipse tarayıcı kapatılıp açıldıktan sonra da geçerli kalan giriş işlemini yapacağız. 

Oturumları yönetebilmek için kontrolörler ve görseller arasında çalışacak bir çok metodlar tanımlamamız gerekiyor. Daha önce görmüştük Ruby buna benzer metodları bir paket içinde toplamak amacıyla Module bloklarını kullanır. Sessions kontrolörümüz oluşturulurken yardımcı kodlarını yerleştirmemiz için helper dosyaları da otomatik olarak üretiliyor. 

app/helpers/sessions_helper.rb

module SessionsHelper
end


Bu yardımcı dosyanın tüm kontrolörlerde geçerli olabilmesi için ana kontrolörümüz (Application controller) içine dahil edilmesi yeterli olacaktır. 

app/controllers/application_controller.rb

class ApplicationController < ActionController::Base
  include SessionsHelper
...




log_in metodu

Kullanıcının giriş yaptığını kontrol etmek Rails tarafından sağlanan session metodu ile kolayca yapılır (bu metodun bizim tanımladığımız Sessions kontrolörü ile alakası yoktur, Rails metodudur). session nesnesini bir Hash olarak düşünerek aşağıdaki gibi bir atama yapabiliriz. 

session[:user_id] = user.id


Bu atama ile kullanıcının tarayıcısında user.id değerinin şifrelenmiş bir versiyonu cookie olarak saklanır. Bunu kullanarak uygulamamızın sayfalarında session[:user_id] değerini o kullanıcının giriş yapmış olduğunu test için kullanabiliriz. Bir kısa bilgi kalıcı cookie oluştururken de cookies metodu kullanılır, session kullanarak üretilen cookie'ler tarayıcı kapanınca silinecektir. 

Kullanıcının giriş yapıp yapmadığını birçok yerde kontrol etmek gerekeceği için ilk önce Sessions yardımcı dosyası içinde kullanıcı giriş yapınca cookie oluşturan bir log_in metodu tanımlayalım. 

app/helpers/sessions_helper.rb

module SessionsHelper
  # verilen kullanıcıya giriş yaptır.
  def log_in(user)
    session[:user_id] = user.id
  end
end


Geçici cookie'ler session metodu ile üretilirken şifrelenerek üretileceği için tarayıcı ve server arasındaki trafiği izleyen bir saldırganın bu bilgiden kullanıcı id değerini öğrenmesi imkansızdır. Bizim ekstra bir şey yapmamıza gerek yok. Aslında tam da koruma sağlayamaz , değerin ne olduğunu çözemese de saldırgan tarayıcı ve server arasındaki trafiği kopyalayarak kullanabilir. Bu konuda Rails klavuzlarında bir makale mevcut. Ancak şimdilik oldukça güvenilir bir yöntem olarak bu yöntemle devam edeceğiz daha sonra tekrar bu konulara döneriz nasipse. 

Bir de Session Fixation diye bir saldırı metodu var , bunu önlemek için de kısaca her kullanıcı girişi öncesi Rails'in reset_session metodunu kullanmak gerekiyor. Şimdi bu bilgiler ışığında Sessions kontrolörü içindeki create eylemimizde kullanıcının giriş yapmasını sağlayan rutini düzenleyelim.

app/controllers/sessions_controller.rb 

class SessionsController < ApplicationController
  def new
  end

  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      reset_session
      log_in user
      redirect_to user
    else
      flash.now[:danger] = 'Geçersiz email/şifre kombinasyonu'
      render 'new', status: :unprocessable_entity
    end
  end

  def destroy
  end
end


Buradaki kısaltılmış yönlendirmeye bakalım

      redirect_to user


Bunu daha önce de görmüştük aslında bu satırı görünce Rails otomatik olarak 

user_url(user)

kullanıcı profil sayfasına yönlendirme yapar. 

Bu ilavelerimiz ile kullanıcı sisteme giriş yapacaktır, ancak bir kontrol edersek kullanıcının giriş yapmış olduğuna dair herhangi bir bilgi sayfalarda görünmeyecektir. 



Şu andaki kullanıcı

Kullanıcının id değerini güvenli bir şekilde oturum cookie'sinde sakladıktan sonra, şimdi diğer sayfalarda da bunu okumayı görelim. Bu amaçla tanımlayacağımız current_user metodunu veri tabanından ilgili kullanıcının bilgilerini okumak için kullanacağız. Bu metodun amacı

<%= current_user.name %>

veya

redirect_to current_user

kodlarının doğru işlem yapmasını sağlamak. 

Kullanıcının bilgilerini veri tabanında bulmak için find metodunu kullanabiliriz. 

User.find(session[:user_id])

Ancak find metodu verilen id değerinde bir kayıt bulamayınca (giriş yapmamış kullanıcı) Rails bir hata üretecektir. Bunun yerine aynı kontrolörde email ile kullanıcı bulduğumuz gibi

User.find_by(id: session[:user_id])

şeklinde find_by metodunu kullanırız. Bu durumda bir hata mesajı üretilmeyecek ve eğer kullanıcı kaydı bulunamazsa user nesnesine nil değeri gelecektir. Şimdi current_user metodunu şöyle tanımlayabiliriz.

  def current_user
    if session[:user_id]
      User.find_by(id: session[:user_id])
    end
  end


Eğer session cookie olarak bulunamazsa metodumuz beklediğimiz gibi nil değer dönecektir. Fakat sayfada current_user defalarca kullanılırsa her biri için veri tabanına tekrar başvuru yapılacaktır. Bunu iyileştirmek için User.find_by sonucunu bir oluşum değişkenine atmak bir Ruby geleneğidir. Böylece bir kere kullanıcı bilgisi veri tabanından çekildikten sonra bu oluşum değeri kullanılabilir. 

if @current_user.nil?
  @current_user = User.find_by(id: session[:user_id])
else
  @current_user
end

Bu kodlama notasyonu Ruby'de çok tanıdık bir notasyon ama daha da kısaltılarak kullanılır.

@current_user = @current_user || User.find_by(id: session[:user_id])

Biliyoruz ki || (veya) işlemi herhangi bir tarafı true verirse onu döner, bu amaçla ilk önce operatörün sol tarafına bakar ve eğer orası boolean olarak Ruby tarafından false değilse (yani değer nil ya da false değilse) o değeri döner operatörün sağ tarafına bakmaz bile. Eğer sol taraf Ruby için boolean false ise operatörün sağ tarafındaki işlemin sonucunu döner. Ruby dilinde bunun da daha kısaltılmış bir versiyonu var.

@current_user ||= User.find_by(id: session[:user_id])


Bu karmaşık görünüyor ama Ruby'nin meşhur ||= (veya ataması) operatörünü kullanıyor.

Bu ||= operatörü ne ola ki?

Veya ataması operatörü Ruby'de yaygın kullanılır, ve Ruby kodlayanların bunu bilmesi önemlidir. İlk başta büyülü bir şey gibi görünebilir. Basit bir eşitlik ile başlayalım.

x = x + 1

Bu işlem için birçok programlama dilinde bir kısaltma vardır.

x += 1

Diğer işlemler için de benzer operatörler kullanılabilir.

$ rails c
Loading development environment (Rails 7.2.2)
>> x = 1
=> 1
>> x += 1
=> 2
>> x *= 3
=> 6
>> x -= 8
=> -2
>> x /= 2
=> -1

Her durumda x = x işlem y yerine x işlem= y kullanılır. 

Başka bir Ruby şablonu da bir değişkene değeri nil ise bir değer atamak , ama nil değilse değerini ellememek. Bunu veya operatörü || kullanarak yaparız.

>> @foo
=> nil
>> @foo = @foo || "bar"
=> "bar"
>> @foo = @foo || "baz"
=> "bar"

Boolean olarak nil değeri Ruby tarafından false kabul edileceği için, ilk atamada  nil || "bar" işlemi sonucu "bar" değeri dönecektir. Benzer şekilde ikinci atamadaki @foo || "baz" işlemi "bar" || "baz" olarak değerlendirilir ve "bar" değeri döner. Bunun nedeni bir değer nil ya da false değilse boolean olarak true kabul edilir, ve || işlemi ilk bulduğu true değerde işlemi sonlandıracağı için ilk değeri geri dönecektir. 

Burada kritik nokta Ruby operatörün iki tarafına boolean karşılığının true ya da false olmasına göre karar verirken, işlem sonucu olarak ilk true kabul ettiği değeri döner. Kafa karıştıran hep bu true ya da false değil de değerin işlem sonucu olarak dönmesi oluyor. 

Yukarıdaki bilgiler sonucunda 

@foo = @foo || "bar" yerine @foo ||= "bar"

yazılabilir. İşte bu

@current_user ||= User.find_by(id: session[:user_id])

eşitliğinin nasıl olduğunu anlamamızı sağlıyor.


Bu açıklamalar doğrultusunda current_user metodumuzu yardımcı dosyamıza ekleyelim.

app/helpers/sessions_helper.rb

module SessionsHelper
  # verilen kullanıcıya giriş yaptır.
  def log_in(user)
    session[:user_id] = user.id
  end
  # Eğer giriş yaptıysa kullanıcı nesnesini döner
  def current_user
    if session[:user_id]
      @current_user ||= User.find_by(id: session[:user_id])
    end
  end
end


Artık çalışan bir current_user metodumuz olduğuna göre uygulamamızın davranışlarını şu andaki kullanıcıya göre değiştirmeye başlayabiliriz.



Yerleşimdeki linkleri kullanıcıya göre değiştirmek

İlk hedefimiz kullanıcı giriş yapmayı başarınca yerleşimdeki linkleri değiştirmek olacak. Mesela giriş yapılınca çıkış yapmak için link olmalı, kullanıcının profiline gitmesi için link olmalı vs. Örneğim bir Hesabım açılan menüsünde Profilim ve Çıkış yap linkleri olsa iyi olur. 

Yerleşimdeki linkleri kullanıcının giriş yapıp yapmadığına göre değiştirmek için bir if bloğu kullanabiliriz. 

<% if logged_in? %>
  # Giriş yapmış kullanıcı linkleri
<% else %>
  # Giriş yapmamış kullanıcı linkleri
<% end %>


Burada düşündüğümüz logged_in? metodu boolean değer dönen bir metod olacak. Bir kullanıcı giriş yapmışsa oturum değişkenlerinde varolan kullanıcı var mı? diye bakarız. Yani current_user değeri nil olmaması gerekir. Şimdi yardımcı metodlarımıza bunu da ekleyelim.

app/helpers/sessions_helper.rb

module SessionsHelper
  # verilen kullanıcıya giriş yaptır.
  def log_in(user)
    session[:user_id] = user.id
  end
  # Eğer giriş yaptıysa kullanıcı nesnesini döner
  def current_user
    if session[:user_id]
      @current_user ||= User.find_by(id: session[:user_id])
    end
  end
  # Kullanıcı giriş yaptıysa true döner diğer durumlarda false
  def logged_in?
    !current_user.nil?
  end
end


Bu ilaveler ile birlikte linklerimizi kullanıcının giriş yaptığına göre değiştirebiliriz. 4 yeni linkimiz olacak bunlardan ikisini ilerideki bölümlerde işleyeceğiz şimdilik sadece oraya koyacağız. 

<%= link_to "Kullanıcılar", '#' %>
<%= link_to "Ayarlar", '#' %>


Çıkış yapma linki de daha önce tanımladığımız logout_path adresine gönderecek.

<%= link_to "Çıkış yapın", logout_path,
    data: { "turbo-method": :delete } %>


Dikkat ettiyseniz çıkış yapma linkimiz argümanında bir HTTP DELETE isteği yapılacağını belirtiyor, yani session nesnesi silinecek (Tarayıcılar DELETE istekleri işlemezler, Rails bunu JavaScript ile yapar).

Ayrıca cırrent_user sayfasına gönderen bir Hesabım linkimiz de olacak.

<%= link_to "Hesabım", current_user %>

Bunu daha açık ifadeyle şöyle de yazabilirdik.

<%= link_to "Hesabım", user_path(current_user) %>


İkisi de aynı çalışır çünkü Rails current_user nesnesinin bir User nesnesi olduğunu bildiği için ona yapılan linkleri otomatik olarak user_path(current_user) adresine çevirir. 

Son olarak eğer kullanıcı giriş yapmadıysa login sayfasına bir linkimiz olmalı. Aslında bu linki daha önce yerleşime ekledik ama adresini bağlamamıştık.

<%= link_to "Giriş Yap", login_path %>


Şimdi bunlar doğrultusunda uygulamamız yerleşiminin linklerinin bulunduğu header kısmi görseline bu değişiklikleri ekleyelim.

app/views/layouts/_header.html.erb

<header class="navbar navbar-fixed-top navbar-inverse">
  <div class="container">
    <%= link_to "Yeni App", root_path, id: "logo" %>
    <nav>
      <ul class="nav navbar-nav navbar-right">
        <li><%= link_to "Ana Sayfa", root_path %></li>
        <li><%= link_to "Yardım", help_path %></li>
        <% if logged_in? %>
          <li><%= link_to "Kullanıcılar", '#' %></li>
          <li class="dropdown">
            <a href="#" id="account" class="dropdown-toggle">
              Hesabım <b class="caret"></b>
            </a>
            <ul id="dropdown-menu" class="dropdown-menu">
              <li><%= link_to "Profil", current_user %></li>
              <li><%= link_to "Ayarlar", '#' %></li>
              <li class="divider"></li>
              <li>
                <%= link_to "Çıkış Yap", logout_path,
                  data: { "turbo-method": :delete } %>
              </li>
            </ul>
          </li>
        <% else %>
          <li><%= link_to "Giriş Yap", login_path %></li>
        <% end %>
      </ul>
    </nav>
  </div>
</header>


Bu haliyle çalıştırınca sadece Giriş Yap kısmı çalışacaktır ve giriş yaptığınızda şimdilik dropdown menü açılmayacak. Eğer çıkış yapmak isterseniz tarayıcıyı kapatıp açabilirsiniz. Tarayıcıyı kapatıp açmak işe yaramıyorsa tarayıcınızın ayarlarından bu site için çerezlerin pencere kapatılınca silinmesini seçmeniz gerekebilir. Örneğin Chrome tarayıcıda adresin solundaki "site bilgilerini görüntüle" tıkladıktan sonra açılan menüden "Çerezler ve site verileri", "Cihaz üzerindeki site verilerini yönetin" sekmesinde localhost seçeneklerde "Tüm pencereler kapatılınca verileri sil" seçiniz.




Menü toggle

Giriş işlemi yaptığımızda Hesabım açılır menüsü görünecek ama tıkladığımızda menü açılmayacaktır. Bunların çalıştırılması için Rails, JavaScript yardımı kullanmalıdır. Yıllar boyunca Rails uygulamalarında JavaScript kullanmak için birçok teknikler kullanılmış. Şu anda Importmap kullanılıyor, bunun için importmap-rails gem'i zaten Gemfile içinde mevcut. Bu noktada Rails'e importmap, turbo ve stimulus kullanacağımızı belirtmeliyiz. 

$ rails importmap:install turbo:install stimulus:install


Aslında rails new ile uygulama üretirken bunlar otomatik kuruluyormuş, ama biz --skip-bundle opsiyonu kullanıp daha sonra manual olarak bundle işlerini yaptığımız için kurulmadan kalmış. Bu komutla JavaScript manifesto dosyamızda bazı ilave dosyalar uygulamamıza eklenecektir.

app/assets/config/manifest.js

//= link_tree ../images
//= link_directory ../stylesheets .css
//= link_tree ../../javascript .js
//= link_tree ../../../vendor/javascript .js


Bu komutla beraber yerleşim ana dosyamız içine de

    <%= javascript_importmap_tags %>

satırı ilavesi gelecektir. 

Yapılandırmamız tamam , sırada açılır menüyü çalıştırmak için yapacağımız kod ilaveleri var. 

  1. Bir CSS active sınıfı kuralı ekleyerek açılır menünün görünür olmasını sağlayacağız.
  2. JavaScript kodlarımız için bir custom klasörü ve menü için bir menu.js kod dosyası ekleyeceğiz.
  3. Importmap yardımıyla Rails'e JavaScript dosyalarımızı nasıl kullanacağını tarif edeceğiz.
  4. menuı.js kod dosyamızı application.js dosyamızdan dahil edeceğiz.


Adım 1'de active sınıfını display özelliği block olacak şekilde stil dosyamıza ekliyoruz. Bu kural Bootstrap sınıfı dropdown-menu davranışını değiştirerek açılır menünün görünür olmasını sağlayacaktır.  

app/assets/stylesheets/custom.scss

.
.
.
/* Dropdown menu */
.dropdown-menu.active {
  display: block;
}


İkinci adım ise uygulamamız app/javascript klasörü içerisine custom adında bir alt klasör ekleyecek ve o klasörde menu.js adında bir yeni kod dosyası ekleyecek. 

$ mkdir app/javascript/custom
$ touch app/javascript/custom/menu.js


Şimdi en karışık olanı. menu.js dosyası içine bir olay izleme kodu ekleyerek Hesabım linki tıklanınca menüyü aktif/pasif etmek için active sınıfını elemana ekleyecek ya da çıkaracak. Olay işleyicinin sayfanın tamamen yüklenmesinden sonra devreye girmesini sağlamak için ikinci bir olay işleyici ile sayfanın yüklendiğini takip edeceğiz. Sayfanın tamamen yüklenmesini DOMContentLoaded olayını işleyerek, genel tıklama olaylarında ise click olayını işleyerek takip ederiz. Turbo dolayısıyla birinci olayın ismi farklı olacak Turbo dokümanında belirtildiği üzere olay ismimiz turbo:load olmalıdır. 

app/javascript/custom/menu.js

// Menü işleme

// Tıklandıkça active sınıf adını ekle-çıkar
document.addEventListener("turbo:load", function() {
  let account = document.querySelector("#account");
  account.addEventListener("click", function(event) {
    event.preventDefault();
    let menu = document.querySelector("#dropdown-menu");
    menu.classList.toggle("active");
  });
});


Kısaca incelemek gerekirse , ana blok sayfa tamamen yüklenince devreye giriyor (turbo:load olayı gerçekleşince). Sayfada id değeri account olan eleman bulunuyor (ki bu bizim Hesabım linkimiz) ve o tıklanınca id değeri dropdown-menu olan elemanın sınıf isimlerine active değeri ekleniyor ya da çıkarılıyor (ki bu da şu anda sayfada görünmez olan menü listemiz). Burada konsantrasyonumuz Rails olduğu için bu JavaScript kodunu ayrıntılı incelememize gerek yok, fakat merak edenler internette JavaScript olay işleme teknikleri üzerine araştırma yapabilir. 


Adım 3 bir yapılandırma ayar adımı. Importmap kullanarak Rails'e custom klasöründeki kod dosyalarını da içermesi gerektiğini anlatacağız.  

config/importmap.rb

# Pin npm packages by running ./bin/importmap

pin "application"
pin "@hotwired/turbo-rails", to: "turbo.min.js"
pin "@hotwired/stimulus", to: "stimulus.min.js"
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js"
pin_all_from "app/javascript/controllers", under: "controllers"
pin_all_from "app/javascript/custom", under: "custom"


Son olarak ta ana JavaScript dosyamızdan bu yeni eklediğimiz JavaScript dosyasını uygulamamıza dahil edeceğiz. 

app/javascript/application.js

// Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails
import "@hotwired/turbo-rails"
import "controllers"
import "custom/menu"


Bu kodun ilk satırında importmap yapılandırmasının nasıl yapılacağına ve sebebine dair bilgileri içeren bir bağlantı adresi var, dileyen inceleyebilir.

Artık menümüz tıklanınca açılır hale gelmiş olmalı. 



Mobil Stili - Rails server'a mobil cihaz ile bağlanmak

Öncelikle mobil cihazımızdan yerel ağdaki bilgisayarımızda çalışan Rails server'ına erişebilmemiz gerekiyor. Bunun için ilk önce güvenlik duvarına bir kural ekleyip 3000 portuna gelen isteklere izin vermesini sağlamalıyız. Ben Windows üzerinde çalıştığım WSL Ubuntu terminalinde bunu şöyle yaptım.

Önce uygulama arama penceresinde Güvenlik duvarı ve ağ koruması'nı buldum. Gelişmiş ayarlar bölümünde Gelen Kuralları sekmesini açtım. Yeni Kural linkine tıklayarak yeni bir kural ekleme penceresini açtım. Kural Türü olarak Bağlantı Noktası seçip Sonraki butonuna tıkladım. Protokol TCP olacak ve Belirli yerel bağlantı noktaları kutucuğuna Rails server portu olan 3000 değerini girdim ve Sonraki butonuna tıkladım. Eylem olarak Bağlantıya izin ver seçili olarak Sonraki butonuna tıkladım. Profil sekmesinde olası tüm yerel bağlantı türleri (Etki alanı, özel, ortak) seçili olarak Sonraki butonuna tıkladım. En son sekmede kuralıma bir isim vererek Son butonuna tıkladım ve yeni kuralım oluştu, böylece bilgisayarım 3000 portuna gelen bağlantı isteklerine izin verecek.

Normalde Rails server Localhost'tan gelen bağlantılara cevap veriyor. SSL bağlantı için komutumuz şöyleydi.

$ rails server -b ssl://localhost:3000

Bunun yerine tüm ağa cevap vermesi için bu komutu

$ rails server -b ssl://0.0.0.0

Şeklinde girersek tüm ağa cevap verecektir. Şimdi cep telefonumuzdan bilgisayarımızın IP adresini girersek mesela bende https://192.168.1.27:3000 bilgisayarımızdaki Rails server'a erişebiliriz. 

Biliyorsunuz burada Gelişmiş bağlantısına tıklayıp 192.168.1.27 sitesine ilerle linkine tıklarsam sayfam açılacak.

Buna şükür en azından cep telefonu ile Rails server'a bağlandık. 

Burada amacımız Web sayfa tasarımı ile ilgili bilgi vermek olmadığı için basit bir şekilde mobil uyumlu bir görsel oluşturmaya çalışacağız. Aslında mobildeki görünümü tarayıcımızın pencere boyutunu değiştirerek de deneyebiliriz ancak gerçek bir cihazla test etmek bana daha mantıklı geldi. 

İlk adımımız sayfa yerleşimimizde head kısmına viewport adında bir meta tag var mı bakmak olacak. 

app/views/layouts/application.html.erb

...
  <head>
    <title><%= full_title yield(:title) %></title>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <meta name="apple-mobile-web-app-capable" content="yes">
...

Normalde bunun otomatik olarak eklenmesi gerek, ama erken versiyon kullananlarda eklenmemiş olabilir. 

Mobil cihazlarda menü görünümü sorununu çözmek için Hamburger Menu denen yapı kullanılır. Bunu yapabilmek için header dosyamıza bazı kodlar eklemeliyiz. 

app/views/layouts/_header.html.erb

<header class="navbar navbar-fixed-top navbar-inverse">
  <div class="container">
    <%= link_to "Yeni App", root_path, id: "logo" %>
    <nav>
      <div class="navbar-header">
        <button id="hamburger" type="button" class="navbar-toggle
            collapsed">
          <span class="sr-only">Toggle navigation</span>
          <span class="icon-bar"></span>
          <span class="icon-bar"></span>
          <span class="icon-bar"></span>
        </button>
      </div>
      <ul id="navbar-menu"
          class="nav navbar-nav navbar-right collapse navbar-collapse">


En alttaki <ul> elemanı zaten oradaydı ona değişiklik yaptık, diğer satırlarsa yeni geldi. navbar-header sınıf adına sahip eleman Bootstrap CSS kuralları gereği 768 pikselden geniş ekranlarda görünmez olur, bu yüzden hamburger buton normal PC ekranında görünmez. collapse navbar-collapse sınıf isimlerinin her ikisine sahip eleman da (yani ul elemanı) 768 pikselden dar ekranlarda görünmez olur.  Şimdi JavaScript kodumuza ilave yaparak açılan menü gibi bir açılma sağlayacağız, ama bu sefer active sınıf adı kullanarak değil Bootstrap'ın collapse sınıf adını kullanarak. 

app/javascript/custom/menu.js

// Menü işleme

document.addEventListener("turbo:load", function() {
  let hamburger = document.querySelector("#hamburger");
  hamburger.addEventListener("click", function(event) {
    event.preventDefault();
    let menu = document.querySelector("#navbar-menu");
    menu.classList.toggle("collapse");
  });

  let account = document.querySelector("#account");
...


Koda eklediklerimize kısaca bakarsak, önce id değeri hamburger olan elemanı buluyoruz. Elemana tıklanınca bizim ul elemanını bulup sınıf isimlerinde collapse sınıf adını varsa yok yoksa var ediyoruz. Böylece dar ekranda hamburger butona her tıkladığımızda menü görünür ya da görünmez oluyor.


Son olarak footer bölümündeki linklerin de mobil cihazlarda yan yana değil alt alta olmasını sağlasak iyi olacak. Bu amaçla CSS media gruplaması kullanarak yazdığımız kuralların sadece 768 pikselden dar ekranlarda uygulanmasını sağlayacağız. 

app/assets/stylesheets/custom.scss

...
/* footer */
footer {
  .
.
.
}
@media (max-width: 768px) {
  footer {
    small {
      display: block;
      float: none;
      margin-bottom: 1em;
    }
    ul {
      float: none;
      padding: 0;
      li {
        float: none;
        margin-left: 0;
      }
    }
  }
}


Şimdi mobil cihazda footer linkleri alt alta yerleşecektir. 


Görseli hallettikten sonra biraz toparlama yapalım. JavaScript kodumuz içinde iki farklı eleman için benzer kodlar yazdık, bu bize bu kodların ileride de tekrarlayabileceği olasılığını gösterir. Bu yüzden kendimizi tekrarlamamak adına (DRY prensibi) JavaScript kodumuzu daha fonksiyonel bir hale getirelim.

app/javascript/custom/menu.js

// Menü işleme

// Sınıf adını var/yok eden bir olay işleyici ekler
function addToggleListener(selected_id, menu_id, toggle_class) {
  let selected_element = document.querySelector(`#${selected_id}`);
  selected_element.addEventListener("click", function(event) {
    event.preventDefault();
    let menu = document.querySelector(`#${menu_id}`)
    menu.classList.toggle(toggle_class);
  });
}

// Tıklamalar için fonksiyonu kullan
document.addEventListener("turbo:load", function() {
  addToggleListener("hamburger", "navbar-menu", "collapse");
  addToggleListener("account", "dropdown-menu", "active");
});


Artık bir elemana tıklanınca bir diğer elemanın sınıf isimlerine ekleme-çıkarma yapmak gerekirse sadece alt bloğa bir satır daha ekleyerek yapabiliriz. 



Görsel Değişikliklerini Test Edelim

Elle kontrol ederken uygulamamızın kullanıcı girişi yapabildiğini gördük. Şimdi bunları yazılımsal görmek için bir entegrasyon testi yazacağız. Şu eylemleri test edeceğiz.

  • Giriş yap sayfasını ziyaret edeceğiz
  • Oturum bilgilerine geçerli veri göndereceğiz (yazılımla giriş yapacağız)
  • Giriş yap linkinin yok olduğunu göreceğiz
  • Çıkış yap linkinin var olduğunu göreceğiz
  • Hesabım linkinin var olduğunu göreceğiz

Bu değişiklikleri görebilmemiz için test rutinlerimiz veri tabanında geçerli bir kullanıcının verileri ile giriş yapmalıdır. Bunu yapmanın default yolu Rails'in fixtures denilen özelliklerini kullanmaktır. Bunlar veritabanına yüklenecek verileri ifade etmenin bir yoludur. Şimdi bir boş dosyada kendi fixture bilgilerimizi saklayacağız. 

Şu andaki hedefimize göre bize geçerli email adresi ve şifresi olan bir tek kullanıcı yeterli. Kullanıcıya giriş yaptıracağımız için Sessions kontrolörü create eylemine gönderecek geçerli bir şifremiz olması gerekiyor. Hatırlarsak şifre veritabanında olduğu gibi saklanmıyor ve bcrypt ile kripto kodlanmış olarak saklanıyor. Bu yüzden fixture bilgilerimizde password_digest özelliği bulunacak ve biz bunu elde etmek için kullanıcı şifremizi kendi yazdığımız bir digest metodu yardımıyla kripto kodlayacağız. 

Daha önce gördük ki password_digest alanına yazılan değer kullanıcının kayıt olurken girdiği şifreden bcrypt ile kodlanıyor (has_secure_password sayesinde). Aynı tekniği kullanarak fixture bilgilerimizdeki şifreyi de dönüştürmeliyiz. secure password kaynak kodunu incelediğimizde bunu şöyle yapabileceğimizi görürüz.

BCrypt::Password.create(string, cost: cost)


Burada ilk parametre kodlanacak olan şifre değeridir. cost ise hesaplamanın karmaşıklık derecesini belirler, cost maliyet demek olduğuna göre bu arttıkça hesap daha karmaşık olacaktır. Hesap karmaşıklık derecesi arttıkça şifre kodlamasının çözülmesi imkansıza doğru gider. Normalde sitemiz çalışırken işi en karmaşığa getirmek güvenliği arttırır, ancak test esnasında hesabı daha basitleştirip işlemlerin kısa sürede bitmesi işimize gelir. 

Rails test işlemlerinde min_cost denen bir değer kullanıyor, bu değer test için true değerinde. Bunu kullanarak cost değerini bulmak için 

cost = ActiveModel::SecurePassword.min_cost ?
  BCrypt::Engine::MIN_COST :
  BCrypt::Engine.cost

Eşitliğini kullanabiliriz. Eşitliğin sağ tarafındaki formüle ternary operatör işlemi denir, kısaca açıklarsak ? ve : ile ayrılmış üçdeğer var. 

bu_doğru_mu ? doğruysa_değer_bu : değilse_bu

şeklinde çalışır. Bir karşılaştırma yapılır sonuç true ise iki nokta üstüste işaretinin solundaki değer, sonuç false ise sağındaki değer işlemden döner. Bu durumda testler çalışırken cost değerimiz BCrypt::Engine::MIN_COST olacaktır ve basitleştirilmiş kodlama yapılacaktır. Test dışında ise standart BCrypt::Engine.cost değeri kullanılacaktır. 

Kendi digest metodumuzu koyabileceğimiz değişik yerler var ama mantıklısı model dosyamız olan user.rb dosyasına eklemek olacak. 


app/models/user.rb

class User < ApplicationRecord
  before_save { self.email = email.downcase }
  validates :name, presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+-.]+@[a-z\d\-.]+\.[a-z]+\z/i  
  validates :email, presence: true, length: { maximum: 255 },
      format: { with: VALID_EMAIL_REGEX },
      uniqueness:  true

  has_secure_password
  validates :password, presence: true, length: { minimum: 6 }

  # Verilen stringin kodlanmış hash değerini dönen sınıf metodu
  def User.digest(string)
  cost = ActiveModel::SecurePassword.min_cost ?
    BCrypt::Engine::MIN_COST :
    BCrypt::Engine.cost
    BCrypt::Password.create(string, cost: cost)
  end
end


Metodumuz hazır olduğuna gör fixture değerlerimizin olduğu dosyayı yazabiliriz. Bu dosya aslında Rails tarafından uygulamamızın test/fixtures klasöründe users.yml olarak oluşturulmuş durumda ancak içine şimdi geçerli bilgiler yazacağız. Şu anda dosyada otomatik üretilmiş geçersiz bilgiler var.

# Read about fixtures at
# https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html

one:
  name: MyString
  email: MyString

two:
  name: MyString
  email: MyString


Şimdi geçerli bir kullanıcı için bilgilerimizi yazalım.

test/fixtures/users.yml


deneme:
  name: Ümit Örnek
  email: umit@example.com
  password_digest: <%= User.digest('password') %>


Burada password_digest değerini hesaplamak için kendi yazdığımız User.digest metodunu kullandık ve "password" karakterlerinden oluşan şifremizin kodlanmasını sağladık. 

<%= User.digest('password') %>

Aynı görsellerin arasına Ruby kodu yazar gibi yml dosyası içindeki değerlere de Ruby kodunun hesapladığı değeri girebiliyoruz. 

Şimdi fixture içinde geçerli bir kullanıcı verileri koyduğumuza göre bunlar test kodlarımızdan bu kullanıcı verilerine 

user = users(:deneme)

olarak ulaşabiliriz. Burada users değeri bizim users.yml dosyamızın içindeki bilgilere karşılık gelir. Oradan anahtar değeri deneme olan veriler okunacak.

Artık entegrasyon test kodlarımızı yazabiliriz. Daha önce geçersiz kullanıcıları test ettiğimiz login test dosyasında ilaveler yapacağız.

test/integration/users_login_test.rb

require "test_helper"

class UsersLoginTest < ActionDispatch::IntegrationTest
  def setup
    @user = users(:deneme)
  end

  test "geçersiz bilgi ile giriş" do
.
.
.

  test "geçerli bilgi ile giriş" do
    post login_path, params: { session: { email: @user.email,
      password: 'password' }
    }
    assert_redirected_to @user
    follow_redirect!
    assert_template 'users/show'
    assert_select "a[href=?]", login_path, count: 0
    assert_select "a[href=?]", logout_path
    assert_select "a[href=?]", user_path(@user)
  end
end


Burada kullandıklarımız:

    assert_redirected_to @user

Kullanıcı sayfasına yönlendirildiğini bekler.


    follow_redirect!

Yönlendirilen sayfaya gidilmiş olmasını bekler.


    assert_select "a[href=?]", login_path, count: 0

Giriş yap linklerinin olmadığını (daha doğrusu sıfır tane olduğunu) bekler.


Şimdi test işlemini çalıştırıp görelim.

$ rails test test/integration/users_login_test.rb

ve sonuç hatasız.

Running 2 tests in a single process (parallelization threshold is 50)
Run options: --seed 31253

# Running:

..

Finished in 11.205785s, 0.1785 runs/s, 0.9816 assertions/s.
2 runs, 11 assertions, 0 failures, 0 errors, 0 skips




Kayıt Sonrası Otomatik Giriş

Yetkilendirme sistemimiz şu anda çalışıyor , ancak yeni kullanıcılar kayıt yaptırdıktan sonra neden otomatik olarak giriş yapmış olmadıkları konusunda kafa karışıklığı yaşayabilirler. Çünkü şu anda yeni kullanıcı kayıt yaptıktan sonra kullanıcı sayfasına gidiliyor fakat hala giriş yap linkine tıklayıp form doldurması gerekiyor. Kayıt işleminin bir devamı olarak yeni kayıt yapanların sisteme girişlerini otomatik gerçekleştireceğiz. Bunu gerçekleştirmek için yapmamız gereken Users kontrolörü create eylemini işlerken log_in metodunu çağırmak.  Oturumla ilgili siber saldırılara karşı da log_in metodu çağırmadan hemen önce oturumu sonlandırarak güvenliği arttıracağız. 

app/controllers/users_controller.rb

class UsersController < ApplicationController
  def new
    @user = User.new
  end

  def show
    @user = User.find(params[:id])
  end

  def create
    @user = User.new(user_params)
    if @user.save
      reset_session
      log_in @user
      flash[:success] = "Yeni App uygulamamıza hoş geldiniz!"
      redirect_to @user
    else
      render 'new', status: :unprocessable_entity
    end
  end  

  private
    def user_params
      params.require(:user).permit(:name, :email, :password,
        :password_confirmation)
    end
end


Bu davranışı test etmek için de geçerli kayıt işlemini test ettiğimiz kodlara kullanıcının giriş yapmış olup olmadığını görmek için ilave yapacağız. Öncelikle sessions_helper.rb dosyasındaki logged_in? metodu gibi çalışacak bir is_logged_in? metodu tanımlayacağız. Çünkü yardımcı metod dosyalarındaki metodlara test kodları erişemez. Örneğin yine aynı dosyadaki current_user metodunu da test kodunda kullanamayız ancak session değerlerine ulaşarak yapacağız. Aslında sessions_helper.rb dosyasını test dosyamızdan çağırıp kullanabiliriz ancak bunun bazı sakıncalarını daha sonra testlerde cookie işlemleri yaparken göreceğiz. Şimdilik testlerde çalışan bir yardımcı dosya içine yazmak iyi olacak. Mevcut test_helper.rb dosyamıza ilave yapacağız.

test/test_helper.rb

ENV["RAILS_ENV"] ||= "test"
require_relative "../config/environment"
require "rails/test_help"

module ActiveSupport
  class TestCase
    # Run tests in parallel with specified workers
    parallelize(workers: :number_of_processors)

    # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
    fixtures :all

    # Add more helper methods to be used by all tests here...
    # Kullanıcı giriş yaptıysa true döner.
    def is_logged_in?
      !session[:user_id].nil?
    end
  end
end


Artık yeni kayıt testimizin koduna kullanıcının giriş yapmış olması beklentisini ekleyebiliriz.

test/integration/users_signup_test.rb

require "test_helper"

class UsersSignupTest < ActionDispatch::IntegrationTest
  test "geçersiz kayıt bilgisi" do
    ...
  end
  test "geçerli kayıt bilgisi" do
    assert_difference 'User.count', 1 do
      post users_path, params: { user: { name: "Example User",
        email: "user@example.com",
        password: "password",
        password_confirmation: "password" } }
    end
    follow_redirect!
    assert_template 'users/show'
    assert is_logged_in?
  end
end


Test yeşil çıkmalıdır.

$ rails test





Çıkış Yapmak

Şimdiye kadar yaptıklarımız sayesinde kullanıcı her zaman giriş yapmış olarak kalıyor. Bu kısımda da çıkış yapabilme kabiliyeti ekleyeceğiz. Çıkış Yap linkimiz şu anda Hesabım açılır menüsü içinde mevcut ama çalışmıyor. Yapmamız gereken burası için oturumu sonlandıracak bir kontrolör eylemi tanımlamak. 

Sessions kontrolörümüzün REST yapıya uygun olması için giriş yapma sayfasında new eylemini ve girişi gerçekleştirmek için create eylemini kullanıyoruz. Bu doğrultuda oturumu sonlandırmak için destroy eylemi kullanmamız gerekir. 

Çıkış yapma işlemini gerçekleştirmek için log_in metodunun yaptıklarını yok etmek yeterli olacaktır. Bunun için de 

session.delete :user_id

kodunu kullanabiliriz. Çünkü şu anda oturumdaki tek değer user_id değeri ve şimdilik işimizi de görür. Fakat ileride oturuma başka değerler de ekleneceğini düşünerek , güvenlik amacıyla tüm oturumu sonlandıran reset_session metodunu kullanmak daha doğru olacaktır.  

Oturumu sıfırlamanın dışında aktif kullanıcı bilgisini de nil yapmamız gerekir ki kullanıcı hala giriş yapmış gibi görünmesin. Çıkış yapıldıktan sonra da ana sayfaya geri dönülmesini sağlayacağız. Şimdi log_in yardımcı metodumuz gibi bir log_out yardımcı metodu kodlayarak başlayalım. 

app/helpers/sessions_helper.rb

module SessionsHelper
  # verilen kullanıcıya giriş yaptır.
  def log_in(user)
    session[:user_id] = user.id
  end
  .
.
  # Aktif kullanıcıya çıkış yaptırır.
  def log_out
    reset_session
    @current_user = nil
  end
end


Şimdi bu log_out metodumuzu Sessions kontrolörü destroy eyleminde kullanabiliriz. Rails HTTP durum kodları sayfasında bulunan durum kodlarından 303 :see_other değerini yönlendirme yaparken kullanacağız. Bu tarayıcının gönderdiği verinin server tarafından alınmış olduğunu kabul ederek yeni sayfaya gitmesini sağlıyormuş. 

app/controllers/sessions_controller.rb

class SessionsController < ApplicationController
  .
.

  def destroy
    log_out
    redirect_to root_url, status: :see_other
  end
end


Artık Hesabım açılır menüsü altındaki Çıkış Yap linkimiz işini görmeye başlamış olmalıdır. Şimdi bunun için de bir test yazalım. Bu amaçla users_login_test.rb test kodlarımıza ilaveler yapacağız. 

test/integration/users_login_test.rb

require "test_helper"

class UsersLoginTest < ActionDispatch::IntegrationTest
  def setup
    @user = users(:deneme)
  end

  test "geçerli email/geçersiz şifre ile giriş" do
    get login_path
    assert_template 'sessions/new'
    post login_path, params: { session: { email: @user.email,
      password: "invalid" } }
    assert_not is_logged_in?
    assert_response :unprocessable_entity
    assert_template 'sessions/new'
    assert_not flash.empty?
    get root_path
    assert flash.empty?
  end

  test "geçerli bilgi ile giriş ve arkasından çıkış" do
    post login_path, params: { session: { email: @user.email,
      password: 'password' }
    }
    assert is_logged_in?
    assert_redirected_to @user
    follow_redirect!
    assert_template 'users/show'
    assert_select "a[href=?]", login_path, count: 0
    assert_select "a[href=?]", logout_path
    assert_select "a[href=?]", user_path(@user)
    delete logout_path
    assert_not is_logged_in?
    assert_response :see_other
    assert_redirected_to root_url
    follow_redirect!
    assert_select "a[href=?]", login_path
    assert_select "a[href=?]", logout_path, count: 0
    assert_select "a[href=?]", user_path(@user), count: 0
  end
end


Öncelikle geçersiz giriş yapıldığında is_logged_in? metodumuzun çalışmasını da teste ekledik. Sonrasında geçerli giriş test rutini arkasına çıkış yapmak için delete logout_path ile logout_path adresine bir DELETE isteği gönderiyoruz, yani çıkış yap linki tıklanmış gibi. Çıkış olduysa kullanıcı giriş yapmış görünmemelidir, durum kodunun :see_other olmasını bekliyoruz, ana sayfaya yönlendirme olmasını bekliyoruz ve sayfaya geçişi takip ediyoruz. Giriş Yap linkinin var olduğunu, Çıkış Yap linkinin yok olduğunu, Profil linkinin yok olduğunu bekliyoruz. Bu test kodu bir hayli uzun oldu inşallah ileride parçalara ayırırız. 

Test çalıştıralım sorunsuz çalışacaktır

$ rails test



Bu Bölümde Neler Öğrendik

  • Rails bir sayfadan diğerine geçerken saklaması gereken bilgileri session metodunu kullanarak çerezlerde saklayabilir. 
  • Giriş yapma formu ile yeni bir oturum açarak kullanıcı girişi yapıldı
  • Yayınlanan sayfalarda flash.now metodu ile mesajlar gösterildi
  • session metodu kullanarak tarayıcıya güvenli bir şekilde kullanıcı id değeri saklayabiliriz
  • Giriş yapılmış olması durumuna göre linklerin görünmesi yada görünmemesi sağlanabilir
  • Entegrasyon testlerinde yönlendirmeleri, veri tabanındaki değişimleri, yerleşimde olması gereken değişimler kontrol edilebilir

Bu kısım da burada bitti, sonraki bölümde nasipse gelişmiş giriş  işlemleri üzerinde duracağız. Şimdilik kalın sağlıcakla..






Hiç yorum yok:

Yorum Gönder