I did generated devise controllers and views and defined both User and Account models like the following:
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
class Account < ApplicationRecord
belongs_to :user, autosave: true
end
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" = 'batman@dc.com' LIMIT 1 /*action='create',application='Freighteen',controller='registrations'*/
CACHE User Exists? (0.0ms) SELECT 1 AS one FROM "users" WHERE "users"."email" = 'batman@dc.com' 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 ('batman@dc.com', '$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.
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.