[RUBY] Build a bulletin board API with authentication and authorization with Rails # 18 ・ Implementation of final user controller

Building a bulletin board API with authentication authorization in Rails 6 # 17 Addition of administrator authority

This is the end of the 18-part serialization.

This time we will create a user controller. It's a compilation of the past, so we recommend that you try it without looking at the example code.

specification

It is mainly intended for use by administrators to manage users and as a group of functions for users to update and delete themselves.

procedure

  1. Create users_controller
  2. Create user_policy
  3. Implementation of user_policy test
  4. Implementation of user_policy
  5. Implementation of users_controller test
  6. Implementation of users_controller

I will proceed with the procedure.

1. Create users_controller

Implementation example
It's easy because you just hit the command.
$ rails g controller v1/users

Here is a group of files that have been slightly modified so that rubocop does not get angry.

app/controllers/v1/users_controller.rb


# frozen_string_literal: true

module V1
  #
  #  users controller
  #
  class UsersController < ApplicationController
  end
end

spec/requests/v1/users_request_spec.rb


# frozen_string_literal: true

require "rails_helper"

RSpec.describe "V1::Users", type: :request do
end

2. Create user_policy

Implementation example

Hit the command to create a file.

$ rails g pundit:policy user

After making minor modifications so that rubocop does not get angry, it is completed for the time being.

app/policies/user_policy.rb


# frozen_string_literal: true

#
#user policy class
#
class UserPolicy < ApplicationPolicy
  #
  # scope
  #
  class Scope < Scope
    def resolve
      scope.all
    end
  end
end

spec/policies/user_policy_spec.rb


# frozen_string_literal: true

require "rails_helper"

RSpec.describe UserPolicy, type: :policy do
  let(:user) { User.new }

  subject { described_class }

  permissions ".scope" do
    pending "add some examples to (or delete) #{__FILE__}"
  end

  permissions :show? do
    pending "add some examples to (or delete) #{__FILE__}"
  end

  permissions :create? do
    pending "add some examples to (or delete) #{__FILE__}"
  end

  permissions :update? do
    pending "add some examples to (or delete) #{__FILE__}"
  end

  permissions :destroy? do
    pending "add some examples to (or delete) #{__FILE__}"
  end
end

3. Implementation of user_policy test

Implementation example

Review of specifications.

  • #index Only admins can view
  • #show Only available to yourself or the administrator
  • #create Only admins can create
  • #update Only you or your administrator can update
  • #destroy Only admin can delete

It seems that you can standardize with index, #create, #destroy, and common with #show, #update.

Here is the implementation with that in mind.

spec/policies/user_policy_spec.rb


# frozen_string_literal: true

require "rails_helper"

RSpec.describe UserPolicy, type: :policy do
  let(:user) { create(:user) }
  let(:another_user) { create(:user) }
  let(:admin_user) { create(:user, :admin) }

  subject { described_class }

  permissions :index?, :create?, :destroy? do
    it "Not allowed when not logged in" do
      expect(subject).not_to permit(nil, user)
    end
    it "Not allowed when logged in as a non-admin user" do
      expect(subject).not_to permit(user, user)
    end
    it "Allowed when logged in as admin user" do
      expect(subject).to permit(admin_user, user)
    end
  end

  permissions :show?, :update? do
    it "Not allowed when not logged in" do
      expect(subject).not_to permit(nil, user)
    end
    it "Not allowed when logged in but another user" do
      expect(subject).not_to permit(user, another_user)
    end
    it "Allowed when logged in as admin user" do
      expect(subject).to permit(admin_user, user)
    end
    it "Allowed when logged in and the same user" do
      expect(subject).to permit(user, user)
    end
  end
end

The point is that another user called ʻanother_user` is defined. Now, at this point, we haven't set a policy, so make sure that some tests are moss.

4. Implementation of user_policy

Implementation example

app/policies/user_policy.rb


# frozen_string_literal: true

#
#user policy class
#
class UserPolicy < ApplicationPolicy
  def index?
    admin?
  end

  def show?
    me? || admin?
  end

  def create?
    admin?
  end

  def update?
    me? || admin?
  end

  def destroy?
    admin?
  end

  private

  def me?
    @record == @user
  end

  #
  # scope
  #
  class Scope < Scope
    def resolve
      scope.all
    end
  end
end

The point is to define a private method called me? in user_policy.rb.

Mine? In application_policy.rb compared @ record.user == @user with @ record's ʻuser. However, this time we will check if @ record and @ usermatch, so the English grammar will beme?instead ofmine?. Besides, it seems that only users_controller can be compared with ʻuser itself at the moment, so I proposed to user_policy instead of application_policy.

The test should have passed without any special mention. </ div> </ details>

5. Implementation of users_controller test

Implementation example

spec/requests/v1/users_request_spec.rb


# frozen_string_literal: true

require "rails_helper"

RSpec.describe "V1::Users", type: :request do
  before do
    @user = create(:user, name: "user test")
    @authorized_headers = authorized_user_headers @user
    admin = create(:user, :admin)
    @authorized_admin_headers = authorized_user_headers admin
  end
  describe "GET /v1/users#index" do
    before do
      create_list(:user, 3)
    end
    it "Normal response code is returned" do
      get v1_users_url, headers: @authorized_admin_headers
      expect(response.status).to eq 200
    end
    it "The number is returned correctly" do
      get v1_users_url, headers: @authorized_admin_headers
      json = JSON.parse(response.body)
      expect(json["users"].length).to eq(3 + 2) #Includes 2 for headers
    end
    it "Responses are returned in descending order of id" do
      get v1_users_url, headers: @authorized_admin_headers
      json = JSON.parse(response.body)
      first_id = json["users"][0]["id"]
      expect(json["users"][1]["id"]).to eq(first_id - 1)
      expect(json["users"][2]["id"]).to eq(first_id - 2)
      expect(json["users"][3]["id"]).to eq(first_id - 3)
      expect(json["users"][4]["id"]).to eq(first_id - 4)
    end
  end

  describe "GET /v1/users#show" do
    it "Normal response code is returned" do
      get v1_user_url({ id: @user.id }), headers: @authorized_headers
      expect(response.status).to eq 200
    end
    it "name is returned correctly" do
      get v1_user_url({ id: @user.id }), headers: @authorized_headers
      json = JSON.parse(response.body)
      expect(json["user"]["name"]).to eq("user test")
    end
    it "404 response is returned when id does not exist" do
      last_user = User.last
      get v1_user_url({ id: last_user.id + 1 }), headers: @authorized_headers
      expect(response.status).to eq 404
    end
  end

  describe "POST /v1/users#create" do
    let(:new_user) do
      attributes_for(:user, name: "create_name test", email: "[email protected]", admin: true)
    end
    it "Normal response code is returned" do
      post v1_users_url, params: new_user, headers: @authorized_admin_headers
      expect(response.status).to eq 200
    end
    it "One more case will be returned" do
      expect do
        post v1_users_url, params: new_user, headers: @authorized_admin_headers
      end.to change { User.count }.by(1)
    end
    it "name, email,admin returns correctly" do
      post v1_users_url, params: new_user, headers: @authorized_admin_headers
      json = JSON.parse(response.body)
      expect(json["user"]["name"]).to eq("create_name test")
      expect(json["user"]["email"]).to eq("[email protected]")
      expect(json["user"]["admin"]).to be true
    end
    it "Errors are returned when the parameter is invalid" do
      post v1_users_url, params: {}, headers: @authorized_admin_headers
      json = JSON.parse(response.body)
      expect(json.key?("errors")).to be true
    end
  end

  describe "PUT /v1/users#update" do
    let(:update_param) do
      update_param = attributes_for(:user, name: "update_name test", email: "[email protected]", admin: true)
      update_param[:id] = @user.id
      update_param
    end
    it "Normal response code is returned" do
      put v1_user_url({ id: update_param[:id] }), params: update_param, headers: @authorized_headers
      expect(response.status).to eq 200
    end
    it "name, email,admin returns correctly" do
      put v1_user_url({ id: update_param[:id] }), params: update_param, headers: @authorized_headers
      json = JSON.parse(response.body)
      expect(json["user"]["name"]).to eq("update_name test")
      expect(json["user"]["email"]).to eq("[email protected]")
      expect(json["user"]["admin"]).to be false #Since it is a problem if the admin authority can be rewritten, leave it as false
    end
    it "Errors are returned when the parameter is invalid" do
      put v1_user_url({ id: update_param[:id] }), params: { name: "" }, headers: @authorized_headers
      json = JSON.parse(response.body)
      expect(json.key?("errors")).to be true
    end
    it "404 response is returned when id does not exist" do
      last_user = User.last
      put v1_user_url({ id: last_user.id + 1 }), params: update_param, headers: @authorized_admin_headers
      expect(response.status).to eq 404
    end
  end

  describe "DELETE /v1/users#destroy" do
    it "Normal response code is returned" do
      delete v1_user_url({ id: @user.id }), headers: @authorized_admin_headers
      expect(response.status).to eq 200
    end
    it "One less and returns" do
      expect do
        delete v1_user_url({ id: @user.id }), headers: @authorized_admin_headers
      end.to change { User.count }.by(-1)
    end
    it "404 response is returned when id does not exist" do
      last_user = User.last
      delete v1_user_url({ id: last_user.id + 1 }), headers: @authorized_admin_headers
      expect(response.status).to eq 404
    end
  end
end

There are some considerations that are different from those of post.

  • Since create (: user) is done when creating header, write the test in consideration of those two cases.
  • Similarly, when checking the id that does not exist, get last_user and +1
  • Since it is a problem if the admin flag is freely rewritten, the admin flag is reflected by create but not by update.

I'm incorporating around.

Also, since there is an admin judgment in the response, it is necessary to modify the serializer.

app/serializers/user_serializer.rb


 # user serializer
 #
 class UserSerializer < ActiveModel::Serializer
-  attributes :id, :name, :email
+  attributes :id, :name, :email, :admin
 end

Now you are ready. Next we will enter the controller implementation. </ div> </ details>

6. Implementation of users_controller

Implementation example

Modify the routes so that you can access the controller.

config/routes.rb


 Rails.application.routes.draw do
   namespace "v1" do
     resources :posts
+    resources :users
     mount_devise_token_auth_for "User", at: "auth"
   end

Next is the controller. This can be almost diverted from post.

app/controllers/v1/users_controller.rb


# frozen_string_literal: true

module V1
  #
  #  users controller
  #
  class UsersController < ApplicationController
    before_action :set_user, only: %i[show update destroy]

    def index
      users = User.order(created_at: :desc).limit(20)
      authorize users
      render json: users
    end

    def show
      authorize @user
      render json: @user
    end

    def create
      user = User.new(user_create_params)
      user[:provider] = :email
      authorize user
      if user.save
        render json: user
      else
        render json: { errors: user.errors }
      end
    end

    def update
      authorize @user
      if @user.update(user_params)
        render json: @user
      else
        render json: { errors: @user.errors }
      end
    end

    def destroy
      authorize @user
      @user.destroy
      render json: @user
    end

    private

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

    def user_create_params
      #Allow admin privileges to be set only when creating
      params.permit(:name, :email, :admin, :password)
    end

    def user_params
      params.permit(:name, :email, :password)
    end
  end
end

As I wrote in the test section, admin only allows it when creating. </ div> </ details>

that's all. Thank you for visiting our website 18 times.

Based on this, please expand the functions such as comment function and social login.

table of contents

[To the serial table of contents]

Recommended Posts