Rails Can Can Canで権限管理 とてもgood!

CanCanCanという権限管理できるgemを利用してみる。

WEBの業務アプリで権限といえば、必要ないものはない!と言えるくらい大切なものです。

参考サイト 1000万以上のダウンロード すごい!

cancancan | RubyGems.org | your community gem host

本家サイト

GitHub - CanCanCommunity/cancancan: The authorization Gem for Ruby on Rails.

gemのインストール

gem 'cancancan', '~> 2.3'

1. Abilitiesのモデルをcancanを利用して作ります。

このクラスが大切になります。 モデルで管理するので安心です。

rails g cancan:ability

生成された、Abilityモデルで定義していきます。

alias_actionで、railsのコントローラーに記載するアクションを決定します。

alias_action :index,:create, :show, :update, :destroy, to: :crud でそれぞれのメソッドがクラッドできるということです。更新、削除等

class Ability
  include CanCan::Ability

  def initialize(user)
    
    # Define abilities for the passed in user here. For example:
    //ここでアクションを記載
    alias_action :index,:create, :show, :update, :destroy, to: :crud
    
      user ||= User.new # guest user (not logged in)
      
      if user.company_admin?
        
        can :crud, [User,Category], {company_id: user.company_id}
      else
        can [:update, :show], User, {company_id: user.company_id, id: user.id}
      end

ちなみに||= は左辺が未定義、または偽ならば、右辺を代入するという意味

Swiftの ?? に似ている。

userが未定義ならuserを作成しなさいの意味

   user ||= User.new 

user ||= User.new # guest user (not logged in)

もしログインしていなければ、カラユーザーを作成して判定用に用いる。

ログインしていなくて、アドレス叩く人もいるかも知れない。 saveしていないので、関係ないユーザーは作成されない。

if user.company_admin?

Userテーブルのcompany_admin カラムが真ならば

can :crud, [User,Category], {company_id: user.company_id}

そのユーザーは,User,CategoryテーブルのCRUDができます。 Userモデルのcompany_idカラムが  user.company_idと等しいものは。 という意味になります。

ここでcanメソッドの定義を見てみましょう

canは2つの引数を取ります。

第一引数は権限を設定しようとする、アクションです。

defでコントローラーに定義するやつですね。

第2引数は、それを設定しようとする、モデルになります。

can :update, Article

Articleモデルのupdateメソッドに権限設定するということですね。

引数に配列を渡すこともできます。

can [:update, :destroy], [Article, Comment]

そこにアクセス許可できるものを更に追加する場合は、条件のハッシュを渡せます。

それが上記の記載したコードになります。

{company_id: user.company_id}

これは、company_id が現在ログインしているuserのcompany_idが同じものという条件を追加しています。

つまり同じ会社のuserしか見れないということですね。 他の会社の情報が見れたら問題です。

あとは、load_and_authorize_resource メソッドを適用するコントローラーに記載します。

これで先程Abilityモデルで定義した内容がそのコントローラーに適用されます。

class UsersController < ApplicationController
  
  before_action :set_user, only: [:show, :edit, :update]
  before_action :sign_in_auth, only: [:show, :edit, :update]
//ここがcan can
  load_and_authorize_resource

上記のbefore_action :sign_in_auth, only: [:show, :edit, :update]メソッドでは、毎回コントローラーで表示する度に、関数をセットしていて 非常に危ない持ち方をしていた。

cancanでモデルで一括管理できるのは大変にありがたい。

この後、画面遷移でもadmin がfalseのuserは特定の画面に遷移できなかった。コントローラーごとにメソッドを書かなくてよいのは大変にありがたい。

ほかサイトではroleカラムなどを追加してenumなどで状態を管理していた。

このサイトは、管理したいuserモデルにcompany_adminカラムをあらかじめ持っていた箇所が違う。

参考されるときは、まず管理したいモデルに、Bool型のadminカラムを追加してからにして下さい。

Swift TableView Section ごとに表示内容を分ける

セクションごとに表示したい内容を変えたい時

PurposeItemはtext,photo,map,linkを持つ それを表示させる。 ポイントは引数のデフォルトにnilを指定しておく。 こうすることにより、photoだけ入った配列を作れる。=>tableviewに一つだけ表示できる。

class PurposeItem {
    
    enum ItemType: Int {
        case text = 1
        case photo = 2
        case map = 3
        case link = 4
    }
    

    var photo:UIImage?
    var map:CLLocationCoordinate2D?
    var link:URL?
    var itemType:ItemType!
    
    init(photo: UIImage, link:URL? = nil, map:CLLocationCoordinate2D? = nil, itemType:ItemType){
        self.photo = photo
        self.link = link
        self.map = map
        self.itemType = itemType
    }

独自セルの登録を済ませておく。 これはハマってしまった。

tableView.register このメソッドで登録しないとだめ

xibジブファイルの作り方は別のサイトを参考にして下さい。 これをViewDidloadで呼び出す。

    func registerTableViewCell() {
        let photoXib = UINib(nibName: "PhotoViewCell", bundle: nil)
        tableView.register(photoXib, forCellReuseIdentifier: "photo")
        
        let mapXib = UINib(nibName: "MapViewCell", bundle: nil)
        tableView.register(mapXib,forCellReuseIdentifier: "map")

        let linkXib = UINib(nibName: "LinkViewCell", bundle: nil)
        tableView.register(linkXib, forCellReuseIdentifier: "link")
        
        let textXib = UINib(nibName: "LinkViewCell", bundle: nil)
        tableView.register(textXib, forCellReuseIdentifier: "text")
        
    }

ここが結構良いサイトです 2.XIBに別ファイルとして出してそれを使う を参照下さい。圧倒的にこちらの方法がいいです。 qiita.com

一つ抜けがありますので、そこだけ記載しておきますね。

ストーリーボードでは、2箇所の設定が必要です

1-TableViewCellを継承した自分が作成したクラスを指定

f:id:happy_teeth_ago:20181208134531p:plain

このようにCellを何種類でも登録できます。便利です。

2-Cellのidentifierを設定

f:id:happy_teeth_ago:20181208134705p:plain

この文字をテーブルを表示するデリゲートメソッドの中で利用します。 この絵は、一応xibファイルの接続の状態ですね。

f:id:happy_teeth_ago:20181208134344p:plain
xibを接続しておきます。上記サイトに詳しく書いてあります。

セクションを分けます。今回は2つに分けます。

通常は、配列を用意してその数だけセクションを分けたりします。

    func numberOfSections(in tableView: UITableView) -> Int {
        return 2

  //配列がある場合はこんな感じ       
  //return items.count
    }

各テーブルのセルの数を返す関数

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        //もし初めのセクションなら
        if section == 0{
            return items.count
        //もし2番めのセクションなら
        }else if section == 1{
            return articles.count
        }else{
            return 0
        }

    }

セルの内容を表示する

   
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        //indexPathがrowとsectionを持っているので、sectionで切り分ける。ここがポイント
        if indexPath.section == 0{
            let item = items[indexPath.row]
            switch item.itemType{
            case .photo?:
                let photoCell = tableView.dequeueReusableCell(withIdentifier: "photo") as! PhotoViewCell
                photoCell.photo.image = item.photo
                tableView.rowHeight = UITableView.automaticDimension
                return photoCell
                
            case .map?:
                let photoCell = tableView.dequeueReusableCell(withIdentifier: "photo") as! PhotoViewCell
                photoCell.photo.image = item.photo
                tableView.rowHeight = UITableView.automaticDimension
                return photoCell
                
            default:
                let cell = UITableViewCell()
                return cell
        }
        
        }else if indexPath.section == 1{
            
         //ここに2番めのセクションに表示したいものを記載する
            //すみません、時間の関係で最後までかけませんでした。
            
            
        }
        
    }

Swift completionHandlerをなるべくわかりやすく説明

まずクロージャとは

自分を囲むスコープにある変数を参照する関数のこと。

 { (引数名:引数の型) -> 戻り値の型 in
    処理
 }

let closure = { () -> () in print("クロージャテスト") }
closure()

swift3以降のクロージャーはNon-Escape

変数や戻り値で外に持ち出せない。

間違って、第三者に保持されない

どんな変数を囲い込んでも循環参照がおきない=>循環参照エラーの回避

クロージャーは外の変数を参照できるのだ! これはとてもいい!

sample Notification

     //5秒後にpush通知
        //通知許可
        UNUserNotificationCenter.current().getNotificationSettings{
            (settings) in
            if(settings.authorizationStatus == .authorized){
                self.push()
            }else{
                UNUserNotificationCenter.current().requestAuthorization(options: [.sound, .badge, .alert], completionHandler:{
                     //ここでユーザーが許可するかどうか尋ねています。trueは一つ以上OK,falseはすべて拒否が帰ります。
                    (granted, error) in
                    if let error = error{
                        print(error)
                    }else{
                        if(granted){
                            self.push()
                        }
                    }
                })
            }
        }

User authorization は

アプリケーションに通知するために、UNUserNotificationCenter を使う。

ローカル通知やリモート通知でも同じ

関数定義

 func requestAuthorization(options: UNAuthorizationOptions = [], completionHandler: @escaping (Bool, Error?) -> Void)

ここをみるとこの関数がcompletionHandlerを通じて、外に持ち出していることがわかる。

@escapingと書いてありますので、、

これがないと外へは持ち出せないということです。

例えば、初めてアプリケーションがメソッドを呼び出すと、システムはユーザーに要求された対話を許可するように促します。

"写真へのアクセスを許可しますか?"などのアラートですね。

ユーザーは認可を許可または拒否することができ、システムはユーザーの応答を保管して、このメソッドへの後続の呼び出しでユーザーに再度プロンプトを出さないようにします。

ユーザーがだめというと、falseが帰り、利用できなくなります。

わかり易くなかったです。 すみません。

Rails Devise 複数Deviseの親子関係同時登録

1. まずDeviseで複数モデルを作成

rails generate devise user
rails generate devise company

2. routesの設定

devise_for :users, path: 'users'
# eg. http://localhost:3000/users/sign_in
devise_for :companies, path: 'companies'
# eg. http://localhost:3000/admins/sign_in

3認証関係の画面を生成

# config/initializers/devise.rb
config.scoped_views = true
# run
rails g devise:views users
rails g devise:views companies

4コントローラーのカスタマイズ

ここが必ず必要。

rails generate devise:controllers users
rails generate devise:controllers companies

5変更をルーティングに反映

Devise関係のrouteを一番先に書くこと!!

sessions はセッション関係

registrations は登録関係

この2つは必須

 devise_for :companies, path: 'companies', controllers: {
    sessions: 'companies/sessions',
    registrations: 'companies/registrations'
  }
  
  devise_for :users, path: 'users', controllers: {
    sessions: 'users/sessions',
    registrations: 'users/registrations'
  }

controllers>users>registrations_controller.rb

Deviseでコントローラーを作成

結局ここからはDeviseを使わないということ。

というよりSuperでoverrrideする前に自分のしたい処理を書いておく。

class Users::RegistrationsController < Devise::RegistrationsController
  before_action :configure_sign_up_params, only: [:create]
  # before_action :configure_account_update_params, only: [:update]

  # GET /resource/sign_up
  def new
 //関連テーブルもオブジェクトを生成しておく
    @user = User.new
    @user.company = Company.new
    super
    
  end


  # POST /resource
  def create
 //関連テーブルを保存してから、そのidでもって小テーブルを作成
    @company = Company.new()
    @company.email = params[:user][:email]
    @company.password = params[:user][:password]
    @company.save!

    params[:user][:company_id] = @company.id
    
    # params[:user][:name] = "#{params[:user][:name]} #{params[:user][:kana]}"

  //ここからDeviseに任せる
    super
    
  end

本家参考サイト

github.com

application controllerに書きなさい

とあります。

このように書きました。 関連テーブルのストロングパラメータもここに書く。

ここで関連テーブルを許可しておく。

class ApplicationController < ActionController::Base

  def configure_sign_up_params
    devise_parameter_sanitizer.permit(:sign_up, keys: [:company_id, :name, :kana])
  end

end

View?? fields_forを回さないと行けないはず?

いいえ、もうdeviseのuser>registration_controllerに記載しているので、Viewでfield_forを回す必要はない。

ついでにtransaction処理を書いておく。userが作成されなかったときに、必要ないcompanyができてしまうので、、

   def create
    
    User.transaction do
  
      @company = Company.new()
      @company.email = params[:user][:email]
      @company.password = params[:user][:password]
      
      # emailとpasswordがないと例外発生
      @company.save!
  
      params[:user][:company_id] = @company.id
      
      # params[:user][:name] = "#{params[:user][:name]} #{params[:user][:kana]}"

          build_resource(sign_up_params)


      # devise
      resource.save!
      
      yield resource if block_given?
      if resource.persisted?
        if resource.active_for_authentication?
          set_flash_message! :notice, :signed_up
          sign_up(resource_name, resource)
          respond_with resource, location: after_sign_up_path_for(resource)
        else
          set_flash_message! :notice, :"signed_up_but_#{resource.inactive_message}"
          expire_data_after_sign_in!
          respond_with resource, location: after_inactive_sign_up_path_for(resource)
        end
      else
        clean_up_passwords resource
        set_minimum_password_length
        respond_with resource
      end
      
    end
    
    # エラー表示
    rescue => e
      set_flash_message! :alert, "#{e.message}" 
      clean_up_passwords resource
      set_minimum_password_length
      
      respond_with resource
  
  end

rails開発-3 DBのチェック

これでDBの中身を見る

rails dbconsole

DBの中で、テーブルの定義を見る

show create table users

userを作ろうとしたら、companyの小テーブルになっているので、companyを作らないといけない

f:id:happy_teeth_ago:20181128161154p:plain この場合どうするか? deviseでカスタマイズしたいコントローラーを指定。

 rails g devise:controllers users

しかし、うまくいかない。

class User < ApplicationRecord
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable
         
  has_many :clients

  belongs_to :company, optional: true

optional: trueを設定して、親テーブルしなくてもuserを作成できる。

deviseで2つ管理権限持たせられるらしい。 これを利用する。 github.com

rails 開発-2 flashメッセージの表示、ログの表示

headerを分ける

 <!-- ヘッダ ナビーfrom here -->
    <%= render 'layouts/header' %>

app>views>layoutsフォルダに_header.html.erbファイルを作成して、中身を移動

flashメッセージを表示

viewのapplication.html.erb

  <% flash.each do |key, value| %>
        <div class="alert <%= bootstrap_class_for(key) %>"><%= value %></div>
      <% end %>

コントローラーには

 flash[:success] = "デモとしてsuccessを表示しています。"

[:success]がkey これはBootstrapにより決まる

value が右辺

更にデバック情報を表示

 <div class="container">
      <% flash.each do |key, value| %>
        <div class="alert <%= bootstrap_class_for(key) %>"><%= value %></div>
      <% end %>
      <%= yield %>
      <%= render 'layouts/footer' %>
      <%= debug(params) if Rails.env.development? %>