Struggling to implement Devise user roles with rails backend, react + redux frontend

I am building an app in which there are two types of users, member and business_event. I am struggling to implement this. I have a sign-up form for a user wherein they can select from a dropdown menu whether they are a “member” or “business or event”. When I try to sign up for a specific role, my rails console always shows that “role” is “member” (the default I assigned).

On my rails backend, I added the devise gem and this migration:

class AddRoleToUsers < ActiveRecord::Migration[6.1]
  def change
    add_column :users, :role, :integer
  end
end

I then changed the User model to the following:

class User < ApplicationRecord
    has_secure_password
    enum role: [:member, :business_event]
    after_initialize :set_default_role, :if => :new_record?

    def set_default_role
      self.role ||= :member
    end
end

On my react frontend, here is the form I wanted to use for signing a user up:

import React from 'react'
import { connect } from 'react-redux'
import { updateSignupForm } from "../../actions/signupForm.js"
import { signup } from "../../actions/currentUser.js"

const Signup = ({ signupFormData, updateSignupForm, signup }) => {

    const handleUserInfoInputChange = event => {
      const { name, value } = event.target
      const updatedFormInfo = {
        ...signupFormData,
        [name]: value
      }
      updateSignupForm(updatedFormInfo)
    }
  
     
  
    const handleSubmit = event => {
      event.preventDefault()
       
      signup(signupFormData)
    }
  
    return (
      <div>
        <h1>Sign Up </h1>

      <form onSubmit={handleSubmit}>
        <div className="ui input">
        <select>
        <option value={signupFormData.role}>Member</option>
        <option value={signupFormData.role}>Business or Event</option>
        </select>
         <input placeholder="username" value={signupFormData.username} name="username" type="text" onChange={handleUserInfoInputChange} /> 
        <input placeholder="password" value={signupFormData.password} name="password" type="text" onChange={handleUserInfoInputChange} /> 
         <input placeholder="email" value={signupFormData.email} name="email" type="text" onChange={handleUserInfoInputChange}/><br/><br/>

        <button className="button button-signup" type="submit" value="Sign Up" > Sign up</button>

        </div>
      </form>
      </div>
    
    )
  }
  
  const mapStateToProps = state => {
    return {
      signupFormData: state.signupForm
    }
  }
  
  export default connect(mapStateToProps, { updateSignupForm, signup } )(Signup)

And here in my signupForm reducer, I decided to set the initial value of “role” to “0” since it is an integer in the backend. I am not sure if that is where I went wrong:

  const initialState = {
    username: "",
    password: "",
    email: "",
    role: 0
  }
  
  export default (state=initialState, action) => {
    switch (action.type) {
      case "UPDATE_SIGNUP_FORM":
        return action.formData
      case "RESET_SIGNUP_FORM":
        return initialState
      default:
        return state
    }
  }

I am not sure why the User is always reverting to the default role of “member.” Any help would be appreciated.

Answer

You really should just focus on installing Devise properly and adding tests for your basic authentication system before you add additional features. Stash your work and backtrack.

First run the generator to generate the configuration files:

rails g devise:install

Then setup your model with:

rails g devise user

This adds the following:

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

You do not want has_secure_password which is Rails own password encryption macro for building your own authentication system. The Devise database_authenticatable module has you covered. Now write integration tests which cover signing up and signing in without getting react involved. This will pay off in spades when it comes to maintaining your sanity.

Now you can actually start with the roles feature.

Add the enum column with a default on the database level:

class AddRoleToUsers < ActiveRecord::Migration[6.1]
  def change
    add_column :users, :role, :integer, default: 0
  end
end

Setting attribute defaults with a callback is an outdated and hacky approach with many known pitfalls. Use database defaults unless you really need to evaluate the default on the application level – in that case you want to do it through the attributes api.

Add the enum macro to the model:

class User < ApplicationRecord
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable
  enum role: [:member, :business_event]
end

If you really want to be able to set the role from the parameters when signing a user up you would need to whitelist the parameter in Devise:

class ApplicationController < ActionController::Base
  before_action :configure_permitted_parameters, if: :devise_controller?

  protected

  def configure_permitted_parameters
    devise_parameter_sanitizer.permit(:sign_up, keys: [:role])
  end
end

But you need to carefully consider the safety implications of doing so as it can allow a malicous user to grant themselves elevated priveledges by simply sending the request though cURL or manipulating the inputs. This may not be relevant right now but keep it in mind if you ever add an admin role for example.

The integer value of the role is also an implementation detail which should only be known to the model. Your front-end and everything else such as controllers should only know the keys:

const initialState = {
  username: "",
  password: "",
  email: "",
  role: "member"
}