diff --git a/backend/app/controllers/admin/users_controller.rb b/backend/app/controllers/admin/users_controller.rb new file mode 100644 index 00000000..425d2432 --- /dev/null +++ b/backend/app/controllers/admin/users_controller.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module Admin + class UsersController < ApplicationController + before_action :prepare_user_params, only: :create + + def create + @user = User.new(user_params) + authorize @user + + respond_to do |format| + if @user.save + format.json { render json: @user, status: :created } + else + format.json { render json: @user.errors, status: :unprocessable_entity } + end + end + end + + def user_params + surveyor_params = %i[city email firstname lastname phone state street_address zipcode] + + params.require(:user).permit(:email, :role, surveyor_attributes: surveyor_params) + end + + def prepare_user_params + return if params[:user].blank? + + params[:user][:surveyor_attributes] = params[:user].delete(:surveyor) + + # For now we have email on User and Surveyor, which we are unsure yet if it is redundant + # In the meantime, copy email from User onto Surveyor + return if params[:user][:surveyor_attributes].blank? + + params[:user][:surveyor_attributes][:email] ||= params[:user][:email] + end + end +end diff --git a/backend/app/models/surveyor.rb b/backend/app/models/surveyor.rb index 42dfd8a5..8e2562d9 100644 --- a/backend/app/models/surveyor.rb +++ b/backend/app/models/surveyor.rb @@ -1,17 +1,26 @@ # frozen_string_literal: true class Surveyor < ApplicationRecord + STATUS_ACTIVE = 'active' + belongs_to :user has_and_belongs_to_many :assignments + after_initialize :set_default_status, if: :new_record? + validates :firstname, presence: true validates :lastname, presence: true validates :email, presence: true validates :phone, presence: true validates :street_address, presence: true - validates :geocode, presence: true validates :city, presence: true validates :zipcode, presence: true validates :state, presence: true validates :status, presence: true + + private + + def set_default_status + self.status ||= STATUS_ACTIVE + end end diff --git a/backend/app/models/user.rb b/backend/app/models/user.rb index 5e612274..e7497a48 100644 --- a/backend/app/models/user.rb +++ b/backend/app/models/user.rb @@ -4,6 +4,8 @@ class User < ApplicationRecord has_one :surveyor, dependent: :destroy enum role: { user: 0, surveyor: 1, admin: 2 } after_initialize :set_default_role, if: :new_record? + after_initialize :set_random_password, if: :new_record? + accepts_nested_attributes_for :surveyor def set_default_role self.role ||= :user @@ -22,4 +24,10 @@ def jwt_payload 'surveyorId' => surveyor&.id } end + + private + + def set_random_password + self.password ||= SecureRandom.base64(15) + end end diff --git a/backend/app/policies/user_policy.rb b/backend/app/policies/user_policy.rb new file mode 100644 index 00000000..8e629ab6 --- /dev/null +++ b/backend/app/policies/user_policy.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class UserPolicy < ApplicationPolicy + def create? + user&.admin? + end +end diff --git a/backend/config/brakeman.ignore b/backend/config/brakeman.ignore index fe6940fc..4f7f9920 100644 --- a/backend/config/brakeman.ignore +++ b/backend/config/brakeman.ignore @@ -1,5 +1,28 @@ { "ignored_warnings": [ + { + "warning_type": "Mass Assignment", + "warning_code": 105, + "fingerprint": "435e540ce53e70774e6d8dea57d81c95ea0a0c06ae0ce1784ba72b31f3cab4d7", + "check_name": "PermitAttributes", + "message": "Potentially dangerous key allowed for mass assignment", + "file": "app/controllers/admin/users_controller.rb", + "line": 21, + "link": "https://brakemanscanner.org/docs/warning_types/mass_assignment/", + "code": "params.require(:user).permit(:email, :role, :surveyor_attributes => ([:city, :email, :firstname, :lastname, :phone, :state, :street_address, :zipcode]))", + "render_path": null, + "location": { + "type": "method", + "class": "Admin::UsersController", + "method": "user_params" + }, + "user_input": ":role", + "confidence": "Medium", + "cwe_id": [ + 915 + ], + "note": "" + }, { "warning_type": "Mass Assignment", "warning_code": 105, @@ -47,6 +70,6 @@ "note": "" } ], - "updated": "2023-07-25 20:57:41 -0400", - "brakeman_version": "5.4.1" + "updated": "2024-07-26 14:10:58 -0400", + "brakeman_version": "6.1.2" } diff --git a/backend/config/routes.rb b/backend/config/routes.rb index 931d0019..9b723251 100644 --- a/backend/config/routes.rb +++ b/backend/config/routes.rb @@ -24,4 +24,8 @@ passwords: 'users/passwords' }, defaults: { format: :json } root 'homes#index' + + namespace :admin do + resources :users, only: [:create] + end end diff --git a/backend/spec/requests/admin/users_spec.rb b/backend/spec/requests/admin/users_spec.rb new file mode 100644 index 00000000..66f4b412 --- /dev/null +++ b/backend/spec/requests/admin/users_spec.rb @@ -0,0 +1,124 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe '/admin/users', type: :request do + include Devise::Test::IntegrationHelpers + + let(:admin) do + User.create(email: 'admin@test.com', password: 'password', role: :admin) + end + + describe 'POST /create' do + context 'with valid parameters for a surveyor' do + let(:valid_attributes) do + { + email: 'alice@example.com', + role: 'surveyor', + surveyor: { + city: 'Boston', + firstname: 'Alice', + lastname: 'Smith', + phone: '1234567890', + state: 'MA', + street_address: '1 Main St', + zipcode: '02110' + } + } + end + + before { sign_in admin } + + it 'creates a new user who is a surveyor' do + expect do + post admin_users_url, params: { user: valid_attributes }, as: :json + end.to change(User, :count).by(1) + + user = User.last + expect(user.email).to eq('alice@example.com') + expect(user.role).to eq('surveyor') + expect(user.surveyor).to be_present + + surveyor = user.surveyor + expect(surveyor.city).to eq('Boston') + expect(surveyor.firstname).to eq('Alice') + expect(surveyor.lastname).to eq('Smith') + expect(surveyor.phone).to eq('1234567890') + expect(surveyor.state).to eq('MA') + expect(surveyor.street_address).to eq('1 Main St') + expect(surveyor.zipcode).to eq('02110') + end + + it 'redirects to the created user' do + post admin_users_url, params: { user: valid_attributes }, as: :json + expect(response).to have_http_status(:created) + end + end + + context 'with valid parameters for an admin' do + let(:valid_attributes) do + { + email: 'bob@example.com', + role: 'admin' + } + end + + before { sign_in admin } + + it 'creates a new user who is an admin' do + expect do + post admin_users_url, params: { user: valid_attributes }, as: :json + end.to change(User, :count).by(1) + + user = User.last + expect(user.email).to eq('bob@example.com') + expect(user.role).to eq('admin') + end + + it 'redirects to the created user' do + post admin_users_url, params: { user: valid_attributes }, as: :json + expect(response).to have_http_status(:created) + end + end + + context 'with invalid parameters' do + let(:invalid_attributes) do + { + email: '', + role: 'admin' + } + end + + before { sign_in admin } + + it 'does not create a new user' do + expect do + post admin_users_url, params: { user: invalid_attributes }, as: :json + end.to change(User, :count).by(0) + end + + it 'renders a response with 422 status' do + post admin_users_url, params: { user: invalid_attributes }, as: :json + expect(response).to have_http_status(:unprocessable_entity) + + error_message = JSON.parse(response.body)['email'][0] + expect(error_message).to eq("can't be blank") + end + end + + context 'as unauthorized user' do + let(:valid_attributes) do + { + email: 'bob@example.com', + role: 'admin' + } + end + + it 'raises an error' do + expect do + post admin_users_url, params: { user: valid_attributes }, as: :json + end.to raise_error(Pundit::NotAuthorizedError) + end + end + end +end