-3

I did generated devise controllers and views and defined both User and Account models like the following:

User

class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable,
         :confirmable, :lockable, :timeoutable, :trackable,
         :omniauthable, omniauth_providers: [ :google_oauth2 ]

  has_one :account, autosave: true, dependent: :destroy, inverse_of: :user

  before_validation :set_account
  enum :role, { basic: 0, admin: 1, courier: 2, client: 3 }

  def set_account
    self.build_account
  end
  accepts_nested_attributes_for :account
end

Account

class Account < ApplicationRecord
  belongs_to :user, autosave: true
end

Strong parameter config

class Users::RegistrationsController < Devise::RegistrationsController
  before_action :configure_signup_parameters, only: :create
.
.
.
 protected

  # If you have extra params to permit, append them to the sanitizer.
  def configure_signup_parameters
    added_attrs = [ :first_name,
                    :last_name,
                    :name,
                    :email,
                    :password,
                    :password_confirmation,
                    :card_name,
                    :card_number,
                    :card_cvv,
                    :card_expiration_month,
                    :card_expiration_year,
                    account_attributes: [
                      :name,
                      :plan,
                      :plan_discount_pct,
                      :plan_period,
                      :plan_price,
                      :tier
                    ]
    ]
    devise_parameter_sanitizer.permit(:sign_up, keys: added_attrs)
  end
end

In the logs you can see that the data for the nested attributes are there but following that you see rails creating the Account with all empty values:

Processing by Users::RegistrationsController#create as HTML
  Parameters: {"authenticity_token"=>"[FILTERED]", "user"=>**{"account_attributes"=>{"tier"=>"corporate", "plan"=>"SMB", "name"=>"Batman", "plan_period"=>"GoldAnnual"}**, "first_name"=>"Bruce", "last_name"=>"Wayne", "name"=>"Batman", "email"=>"[FILTERED]", "password"=>"[FILTERED]", "password_confirmation"=>"[FILTERED]"}, "card_name"=>"Bruce Wayne", "card_number"=>"[FILTERED]", "card_expiration_month"=>"[FILTERED]", "card_expiration_year"=>"[FILTERED]", "card_cvv"=>"[FILTERED]", "save"=>"1", "commit"=>"Submit"}
  TRANSACTION (0.2ms)  BEGIN /*action='create',application='Freighteen',controller='registrations'*/
  User Exists? (6.5ms)  SELECT 1 AS one FROM "users" WHERE "users"."email" = '[email protected]' LIMIT 1 /*action='create',application='Freighteen',controller='registrations'*/
  CACHE User Exists? (0.0ms)  SELECT 1 AS one FROM "users" WHERE "users"."email" = '[email protected]' LIMIT 1
  User Create (0.5ms)  INSERT INTO "users" ("email", "encrypted_password", "reset_password_token", "reset_password_sent_at", "remember_created_at", "sign_in_count", "current_sign_in_at", "last_sign_in_at", "current_sign_in_ip", "last_sign_in_ip", "confirmation_token", "confirmed_at", "confirmation_sent_at", "unconfirmed_email", "failed_attempts", "unlock_token", "locked_at", "created_at", "updated_at", "name", "first_name", "last_name", "role") 

VALUES ('[email protected]', '$2a$12$MoG2NgRuikEE9yEMguK4iOybH7MN3BmDRS6sf78SUNJpK.rx9z3Cu', NULL, NULL, NULL, 0, NULL, NULL, NULL, NULL, 'sGBMeBMesDm-2h3j9Jcj', NULL, '2025-01-05 04:41:22.332196', NULL, 0, NULL, NULL, '2025-01-05 04:41:22.331966', '2025-01-05 04:41:22.331966', 'Batman', 'Bruce', 'Wayne', 0) RETURNING "id" /*action='create',application='Freighteen',controller='registrations'*/
  **Account Create (0.6ms)  INSERT INTO "accounts" ("user_id", "created_at", "updated_at", "tier", "plan", "plan_period", "plan_discount_pct", "plan_price", "name") VALUES (11, '2025-01-05 04:41:22.341682', '2025-01-05 04:41:22.341682', NULL, NULL, NULL, NULL, 0.0, NULL) RETURNING "id"**

Note that account attributes are present but don't get created.

What am I missing? I didn't think I have to build manually given the configuration.

5
  • you're initializing a new blank account every time you're saving a user self.build_account. Commented Jan 5 at 5:16
  • in your model before_validation :set_account, def set_account self.build_account end. why do you have this? it's not needed. Commented Jan 5 at 13:47
  • On a side note you're likely missing a model here for Plan. Letting the user pass whatever price, rebates etc they want is asking for trouble as a malicous user could pass anything they want. Instead they should just select the plan_id of an existing plan. In addition this will avoid denormalization. Commented Jan 5 at 14:13
  • I'm at the beginning of a proj. I was attempting to think through multi-tenancy where the account needs to be set to segregate data but right shouldn't be in model. Commented Jan 5 at 17:32
  • i attach discounts and pricing to the account is so that if plans change accounts can still get grandfathered in. but yeah all attributes should be not permitted but I am presently just testing performance of multitenancy. like can postgres do this performantly with schema segregation vs just appending an account to every table and using acts_as_tenancy. the callback was from an erroneous multitenancy article here. I try/experience/test options then put the best one in the actual app Commented Jan 5 at 19:15

1 Answer 1

3

The first issue here is that your set_account callback is replacing the user input with a new instance of Account. Do not use model callbacks to "seed" associations for a form.

class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable,
         :confirmable, :lockable, :timeoutable, :trackable,
         :omniauthable, omniauth_providers: [ :google_oauth2 ]

  has_one :account, autosave: true, dependent: :destroy, inverse_of: :user
  enum :role, { basic: 0, admin: 1, courier: 2, client: 3 }
  accepts_nested_attributes_for :account
end

Instead do it in the controller so that it only actually happens when you need it. All the Devise controllers have cleverly placed yields to make it really easy to tap into the flow.

class Users::RegistrationsController < Devise::RegistrationsController
  before_action :configure_signup_parameters, only: :create
  # ...

  def new
    super do |user|
      user.build_account
    end
  end
end

There are also some rather egregious code smells to what input you're accepting from the user and how you're modeling your data.

Surely the user should not be able to choose whatever price and rebates they want? What happens when I send plan_discount_pct=100 with cURL? Guess it's free then? Thanks.

Create a Plan model and have the user select the ID of a plan instead and you'll also avoid denormalizing your data.

You really should not be storing credit card details, you're a single SQL injection attack away from some serious litagation. Let whatever payment vendor you're using take care of it instead. If you really really wanted to this use a separate table with application level encryption and make the association one to many so that you can actually track what card was used for what payment.

Sign up to request clarification or add additional context in comments.

7 Comments

i appreciate the answer and comments, yes i understand -- cc and plans will be handled/replaced by stripe etc but there are reasons I need to model this in the short term before integrating; but yes it can be modeled better.
will mark later tonight once i have a moment to go through.
When dealing with payments please just take the time to get it right in the first go.
i get your outrage. but it doesn't matter. this is a quick and dirty multitenancy test (schema vs column) and stripe will ultimately handle everything via js b/t user and them. but yes i am stubbing a few things in the interim so I can share code with the actual app. this was supposed to be a quick and dirty multitenancy so that I can make a final decision but I got hung up on the disappearing account which is the very thing tenancy is based on! no account no multitenancy. nothing sensitive is stored or in logs. and strong params alone can eliminate injection when attributes are removed.
instead of ensuring the account was set in application controller i stupidly added it to the model and as you and alex pointed out the call back was mangling it. Thank you both.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.