propel_authentication 0.1.4 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +43 -2
  3. data/README.md +6 -6
  4. data/lib/generators/propel_authentication/install_generator.rb +135 -153
  5. data/lib/generators/propel_authentication/templates/application_mailer.rb +6 -0
  6. data/lib/generators/propel_authentication/templates/auth/passwords_controller.rb.tt +84 -78
  7. data/lib/generators/propel_authentication/templates/auth/signup_controller.rb.tt +242 -0
  8. data/lib/generators/propel_authentication/templates/{tokens_controller.rb.tt → auth/tokens_controller.rb.tt} +39 -22
  9. data/lib/generators/propel_authentication/templates/auth_mailer.rb +3 -1
  10. data/lib/generators/propel_authentication/templates/authenticatable.rb +8 -2
  11. data/lib/generators/propel_authentication/templates/concerns/confirmable.rb +1 -1
  12. data/lib/generators/propel_authentication/templates/concerns/lockable.rb +4 -2
  13. data/lib/generators/propel_authentication/templates/concerns/{propel_authentication.rb → propel_authentication_concern.rb} +33 -3
  14. data/lib/generators/propel_authentication/templates/concerns/recoverable.rb +16 -6
  15. data/lib/generators/propel_authentication/templates/core/configuration_methods.rb +104 -64
  16. data/lib/generators/propel_authentication/templates/db/seeds.rb +50 -4
  17. data/lib/generators/propel_authentication/templates/doc/signup_flow.md +315 -0
  18. data/lib/generators/propel_authentication/templates/models/agency.rb.tt +13 -0
  19. data/lib/generators/propel_authentication/templates/models/agent.rb.tt +13 -0
  20. data/lib/generators/propel_authentication/templates/{invitation.rb → models/invitation.rb.tt} +6 -0
  21. data/lib/generators/propel_authentication/templates/models/organization.rb.tt +12 -0
  22. data/lib/generators/propel_authentication/templates/{user.rb → models/user.rb.tt} +5 -0
  23. data/lib/generators/propel_authentication/templates/propel_authentication.rb.tt +94 -9
  24. data/lib/generators/propel_authentication/templates/routes/auth_routes.rb.tt +55 -0
  25. data/lib/generators/propel_authentication/templates/services/auth_notification_service.rb +3 -3
  26. data/lib/generators/propel_authentication/templates/test/concerns/confirmable_test.rb.tt +34 -10
  27. data/lib/generators/propel_authentication/templates/test/concerns/propel_authentication_test.rb.tt +1 -1
  28. data/lib/generators/propel_authentication/templates/test/concerns/recoverable_test.rb.tt +4 -4
  29. data/lib/generators/propel_authentication/templates/test/controllers/auth/lockable_integration_test.rb.tt +18 -15
  30. data/lib/generators/propel_authentication/templates/test/controllers/auth/password_reset_integration_test.rb.tt +38 -40
  31. data/lib/generators/propel_authentication/templates/test/controllers/auth/signup_controller_test.rb.tt +201 -0
  32. data/lib/generators/propel_authentication/templates/test/controllers/auth/tokens_controller_test.rb.tt +33 -25
  33. data/lib/generators/propel_authentication/templates/test/mailers/auth_mailer_test.rb.tt +51 -36
  34. data/lib/generators/propel_authentication/templates/views/auth_mailer/email_confirmation.html.erb +2 -2
  35. data/lib/generators/propel_authentication/templates/views/auth_mailer/email_confirmation.text.erb +1 -1
  36. data/lib/generators/propel_authentication/test/generators/authentication/install_generator_test.rb +4 -4
  37. data/lib/generators/propel_authentication/test/generators/authentication/uninstall_generator_test.rb +1 -1
  38. data/lib/generators/propel_authentication/test/integration/generator_integration_test.rb +1 -1
  39. data/lib/generators/propel_authentication/test/integration/multi_version_generator_test.rb +13 -12
  40. data/lib/generators/propel_authentication/unpack_generator.rb +19 -15
  41. data/lib/propel_authentication.rb +1 -1
  42. metadata +14 -11
  43. data/lib/generators/propel_authentication/templates/agency.rb +0 -7
  44. data/lib/generators/propel_authentication/templates/agent.rb +0 -7
  45. data/lib/generators/propel_authentication/templates/auth/base_passwords_controller.rb.tt +0 -99
  46. data/lib/generators/propel_authentication/templates/auth/base_tokens_controller.rb.tt +0 -90
  47. data/lib/generators/propel_authentication/templates/organization.rb +0 -7
@@ -1,11 +1,10 @@
1
- <%- if api_versioned? -%>
2
- # Dynamic API versioning controller - inherits from base controller
3
- class <%= controller_namespace %>::PasswordsController < Api::Auth::BasePasswordsController
4
- end
5
- <%- else -%>
6
- class <%= controller_namespace %>::PasswordsController < ApplicationController
1
+ class <%= auth_controller_class_name('passwords') %> < ApplicationController
2
+ <%- unless api_only_app? -%>
3
+ include RackSessionDisable
4
+ <%- end -%>
5
+ include PropelAuthenticationConcern
7
6
 
8
- # POST <%= api_versioned? ? "/#{api_namespace}" : "" %>/auth/reset - Request password reset
7
+ # POST <%= auth_route_prefix %>/reset
9
8
  def create
10
9
  email_address = params[:email_address]
11
10
 
@@ -14,113 +13,120 @@ class <%= controller_namespace %>::PasswordsController < ApplicationController
14
13
  return render json: { error: "Email address is required" }, status: :unprocessable_entity
15
14
  end
16
15
 
17
- # Validate email_address format
18
- unless email_address.match?(/\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i)
16
+ # Validate email format
17
+ unless email_address.match?(URI::MailTo::EMAIL_REGEXP)
19
18
  return render json: { error: "Email address format is invalid" }, status: :unprocessable_entity
20
19
  end
21
20
 
22
- # For security, always return the same response whether user exists or not
23
- # This prevents user enumeration attacks
24
- response_message = "If an account with that email address exists, password reset instructions have been sent to your email"
25
-
26
- # Find user by email_address and send reset email if user exists
27
21
  user = User.find_by(email_address: email_address)
28
22
 
29
- if user && PropelAuth.configuration.enable_email_notifications
30
- # Send password reset email using the notification service
31
- email_result = AuthNotificationService.send_password_reset_email(user)
23
+ if user
24
+ # Generate and save reset token
25
+ user.generate_password_reset_token
32
26
 
33
- # Log the result but don't expose it to prevent user enumeration
34
- if email_result[:success]
35
- Rails.logger.info "Password reset email sent successfully to #{email_address}"
36
- else
37
- Rails.logger.error "Failed to send password reset email to #{email_address}: #{email_result[:error]}"
27
+ # Send password reset email
28
+ if PropelAuthentication.configuration.enable_email_notifications
29
+ email_result = AuthNotificationService.send_password_reset_email(user)
30
+
31
+ if email_result[:success]
32
+ Rails.logger.info "Password reset email sent successfully to #{user.email_address}"
33
+ else
34
+ Rails.logger.error "Failed to send password reset email to #{user.email_address}: #{email_result[:error]}"
35
+ end
38
36
  end
37
+
38
+ render json: {
39
+ message: "Password reset instructions have been sent to your email address"
40
+ }, status: :ok
41
+ else
42
+ # Don't reveal whether the email exists for security
43
+ render json: {
44
+ message: "Password reset instructions have been sent to your email address"
45
+ }, status: :ok
39
46
  end
40
-
41
- # Always return the same response for security
42
- render json: { message: response_message }, status: :ok
47
+ rescue => e
48
+ Rails.logger.error "Password reset creation error: #{e.message}"
49
+ render json: { error: "Unable to process password reset request" }, status: :unprocessable_entity
43
50
  end
44
51
 
45
- # PUT <%= api_versioned? ? "/#{api_namespace}" : "" %>/auth/reset - Confirm password reset with token
46
- def update
52
+ # GET <%= auth_route_prefix %>/reset
53
+ def show
47
54
  token = params[:token]
48
- password = params[:password]
49
- password_confirmation = params[:password_confirmation]
50
55
 
51
- # Validate required parameters
52
56
  if token.blank?
53
- return render json: { error: "Reset token is required" }, status: :unprocessable_entity
54
- end
55
-
56
- if password.blank?
57
- return render json: { error: "Password is required" }, status: :unprocessable_entity
58
- end
59
-
60
- if password_confirmation.blank?
61
- return render json: { error: "Password confirmation is required" }, status: :unprocessable_entity
57
+ render json: { error: 'Reset token is required' }, status: :unprocessable_entity
58
+ return
62
59
  end
63
60
 
64
- # Validate password confirmation
65
- if password != password_confirmation
66
- return render json: { error: "Password and confirmation do not match" }, status: :unprocessable_entity
67
- end
68
-
69
- # Find user by token
70
- user = User.find_user_by_password_reset_token(token)
61
+ user = User.find_by_jwt_password_reset_token(token)
71
62
 
72
- unless user
73
- return render json: { error: "Invalid or expired reset token" }, status: :unauthorized
74
- end
75
-
76
- # Attempt password reset
77
- if user.reset_password_with_token!(token, password, password_confirmation)
63
+ if user
78
64
  render json: {
79
- message: "Password has been reset successfully",
65
+ valid: true, # Test expects this field
66
+ message: 'Valid reset token',
80
67
  user: {
81
68
  id: user.id,
82
69
  email_address: user.email_address,
83
- username: user.username
70
+ token: token
84
71
  }
85
72
  }, status: :ok
86
73
  else
87
- render json: { error: "Password is too short (minimum is 8 characters)" }, status: :unprocessable_entity
74
+ render json: {
75
+ valid: false, # Test expects this field
76
+ error: 'Invalid or expired reset token'
77
+ }, status: :unauthorized
88
78
  end
79
+ rescue => e
80
+ Rails.logger.error "Password reset show error: #{e.message}"
81
+ render json: { error: 'Invalid reset token' }, status: :unauthorized
89
82
  end
90
83
 
91
- # GET <%= api_versioned? ? "/#{api_namespace}" : "" %>/auth/reset - Verify reset token
92
- def show
84
+ # PATCH <%= auth_route_prefix %>/reset
85
+ def update
93
86
  token = params[:token]
87
+ new_password = params[:password]
88
+ password_confirmation = params[:password_confirmation]
94
89
 
95
- # Validate token parameter
90
+ # Validate required parameters
96
91
  if token.blank?
97
- return render json: { error: "Token parameter is required" }, status: :unprocessable_entity
92
+ return render json: { error: 'Reset token is required' }, status: :unprocessable_entity
98
93
  end
99
94
 
100
- # Find user by token
101
- user = User.find_user_by_password_reset_token(token)
95
+ if new_password.blank?
96
+ return render json: { error: 'New password is required' }, status: :unprocessable_entity
97
+ end
102
98
 
103
- if user
104
- render json: {
105
- valid: true,
99
+ if new_password != password_confirmation
100
+ return render json: { error: 'Password confirmation does not match password' }, status: :unprocessable_entity
101
+ end
102
+
103
+ user = User.find_by_jwt_password_reset_token(token)
104
+
105
+ if user.nil?
106
+ return render json: { error: 'Invalid or expired reset token' }, status: :unauthorized
107
+ end
108
+
109
+ # Update password and clear reset token
110
+ if user.reset_password_with_token!(token, new_password)
111
+ render json: {
112
+ message: 'Password has been reset successfully',
106
113
  user: {
107
114
  id: user.id,
108
- email_address: user.email_address,
109
- username: user.username
115
+ email_address: user.email_address
110
116
  }
111
117
  }, status: :ok
112
118
  else
113
- render json: {
114
- valid: false,
115
- error: "Invalid or expired reset token"
116
- }, status: :unauthorized
119
+ # Return specific validation errors from the user model
120
+ error_messages = user.errors.full_messages
121
+ primary_error = error_messages.first || 'Password validation failed'
122
+
123
+ render json: {
124
+ error: primary_error, # Use specific validation message
125
+ details: error_messages
126
+ }, status: :unprocessable_entity
117
127
  end
128
+ rescue => e
129
+ Rails.logger.error "Password reset update error: #{e.message}"
130
+ render json: { error: 'Unable to reset password' }, status: :unprocessable_entity
118
131
  end
119
-
120
- private
121
-
122
- def password_reset_params
123
- params.permit(:email_address, :token, :password, :password_confirmation)
124
- end
125
- end
126
- <%- end -%>
132
+ end
@@ -0,0 +1,242 @@
1
+ class <%= auth_controller_class_name('signup') %> < ApplicationController
2
+ <%- unless api_only_app? -%>
3
+ include RackSessionDisable
4
+ <%- end -%>
5
+
6
+ # POST <%= auth_route_prefix %>/signup
7
+ def create
8
+ # Validate agency requirements if agency tenancy is enabled
9
+ return unless validate_agency_requirements!
10
+
11
+ ActiveRecord::Base.transaction do
12
+ @organization = Organization.create!(organization_params)
13
+ @user = @organization.users.create!(user_params)
14
+
15
+ # Create agency if agency tenancy is enabled and agency params provided
16
+ if agency_tenancy_enabled? && agency_params.present?
17
+ @agency = @organization.agencies.create!(agency_params)
18
+
19
+ # Create agent relationship so user has immediate agency access
20
+ @user.agents.create!(
21
+ agency: @agency,
22
+ role: agent_role_param
23
+ )
24
+ end
25
+
26
+ # Generate JWT for immediate login
27
+ token = @user.generate_jwt_token
28
+
29
+ render json: {
30
+ token: token,
31
+ user: user_response(@user),
32
+ organization: organization_response(@organization),
33
+ agency: agency_created? ? agency_response(@agency) : nil,
34
+ agent: agency_created? ? agent_response(@user.agents.last) : nil,
35
+ message: "Account created successfully! Ready to start working.",
36
+ next_steps: next_steps_for_user
37
+ }, status: :created
38
+ end
39
+ rescue ActiveRecord::RecordInvalid => e
40
+ render json: {
41
+ error: 'Validation failed',
42
+ details: extract_validation_errors(e),
43
+ message: 'Please correct the errors and try again'
44
+ }, status: :unprocessable_entity
45
+ rescue => e
46
+ render json: {
47
+ error: 'Account creation failed',
48
+ message: 'An unexpected error occurred. Please try again.'
49
+ }, status: :internal_server_error
50
+ end
51
+
52
+ private
53
+
54
+ def user_params
55
+ params.require(:user).permit(
56
+ :email_address,
57
+ :username,
58
+ :password,
59
+ :password_confirmation,
60
+ :first_name,
61
+ :last_name,
62
+ :phone_number,
63
+ :time_zone
64
+ )
65
+ end
66
+
67
+ def organization_params
68
+ params.require(:organization).permit(
69
+ :name,
70
+ :website,
71
+ :time_zone,
72
+ :description
73
+ )
74
+ end
75
+
76
+ def agency_params
77
+ return {} unless params[:agency].present?
78
+
79
+ params.require(:agency).permit(
80
+ :name,
81
+ :phone_number,
82
+ :address
83
+ )
84
+ end
85
+
86
+ def agent_role_param
87
+ params.dig(:agent, :role) || 'owner'
88
+ end
89
+
90
+ def user_response(user)
91
+ {
92
+ id: user.id,
93
+ email_address: user.email_address,
94
+ username: user.username,
95
+ first_name: user.first_name,
96
+ last_name: user.last_name,
97
+ organization_id: user.organization_id,
98
+ created_at: user.created_at
99
+ }
100
+ end
101
+
102
+ def organization_response(organization)
103
+ {
104
+ id: organization.id,
105
+ name: organization.name,
106
+ website: organization.website,
107
+ time_zone: organization.time_zone
108
+ }
109
+ end
110
+
111
+ def agency_response(agency)
112
+ return nil unless agency
113
+
114
+ {
115
+ id: agency.id,
116
+ name: agency.name,
117
+ organization_id: agency.organization_id
118
+ }
119
+ end
120
+
121
+ def agent_response(agent)
122
+ return nil unless agent
123
+
124
+ {
125
+ id: agent.id,
126
+ user_id: agent.user_id,
127
+ agency_id: agent.agency_id,
128
+ role: agent.role,
129
+ created_at: agent.created_at
130
+ }
131
+ end
132
+
133
+ def agency_created?
134
+ defined?(@agency) && @agency.present?
135
+ end
136
+
137
+ def next_steps_for_user
138
+ steps = []
139
+
140
+ if agency_tenancy_enabled?
141
+ if agency_created?
142
+ steps << {
143
+ action: 'create_additional_agencies',
144
+ description: 'Add more agencies to organize your team',
145
+ endpoint: '<%= auth_route_prefix %>/agencies',
146
+ method: 'POST'
147
+ }
148
+ steps << {
149
+ action: 'start_creating_resources',
150
+ description: 'Begin creating and managing your content (you can create resources immediately)',
151
+ endpoint: '<%= auth_route_prefix %>/',
152
+ note: 'Your agency is ready for resource creation'
153
+ }
154
+ else
155
+ steps << {
156
+ action: 'create_agency',
157
+ description: 'Create an agency to organize your resources',
158
+ endpoint: '<%= auth_route_prefix %>/agencies',
159
+ method: 'POST',
160
+ required: true
161
+ }
162
+ end
163
+
164
+ steps << {
165
+ action: 'invite_team_members',
166
+ description: 'Invite colleagues to join your organization',
167
+ endpoint: '<%= auth_route_prefix %>/invitations',
168
+ method: 'POST'
169
+ }
170
+ else
171
+ steps << {
172
+ action: 'invite_team_members',
173
+ description: 'Invite colleagues to join your organization',
174
+ endpoint: '<%= auth_route_prefix %>/invitations',
175
+ method: 'POST'
176
+ }
177
+ steps << {
178
+ action: 'start_creating_resources',
179
+ description: 'Begin creating and managing your content',
180
+ endpoint: '<%= auth_route_prefix %>/'
181
+ }
182
+ end
183
+
184
+ steps
185
+ end
186
+
187
+ def agency_tenancy_enabled?
188
+ # Check PropelAuthentication configuration (owns tenancy models)
189
+ if defined?(PropelAuthentication) && PropelAuthentication.respond_to?(:configuration)
190
+ PropelAuthentication.configuration.agency_tenancy
191
+ else
192
+ true # Safe default - enables agency tenancy when configuration unavailable
193
+ end
194
+ rescue => e
195
+ # Default to true if configuration is not available (standalone auth setup)
196
+ true
197
+ end
198
+
199
+ def extract_validation_errors(exception)
200
+ return {} unless exception.record
201
+
202
+ errors = {}
203
+ exception.record.errors.each do |error|
204
+ field = error.attribute
205
+ errors[field] ||= []
206
+ errors[field] << error.full_message
207
+ end
208
+
209
+ # If no specific field errors, use base errors
210
+ if errors.empty? && exception.record.errors.any?
211
+ errors[:base] = exception.record.errors.full_messages
212
+ end
213
+
214
+ errors
215
+ end
216
+
217
+ # Validate that required agency params are present when agency tenancy is enabled
218
+ def validate_agency_requirements!
219
+ return unless agency_tenancy_enabled?
220
+
221
+ if params[:agency].blank?
222
+ render json: {
223
+ error: 'Agency information required',
224
+ message: 'Agency details must be provided when agency tenancy is enabled',
225
+ code: 'MISSING_AGENCY_DATA',
226
+ hint: 'Include an "agency" object with name and other details in your request'
227
+ }, status: :unprocessable_entity
228
+ return false
229
+ end
230
+
231
+ if agency_params[:name].blank?
232
+ render json: {
233
+ error: 'Agency name required',
234
+ message: 'Agency name is required when creating an agency',
235
+ code: 'MISSING_AGENCY_NAME'
236
+ }, status: :unprocessable_entity
237
+ return false
238
+ end
239
+
240
+ true
241
+ end
242
+ end
@@ -1,18 +1,22 @@
1
- <%- if api_versioned? -%>
2
- # Dynamic API versioning controller - inherits from base controller
3
- class <%= controller_namespace %>::TokensController < Api::Auth::BaseTokensController
4
- end
5
- <%- else -%>
6
- class <%= controller_namespace %>::TokensController < ApplicationController
1
+ class <%= auth_controller_class_name('tokens') %> < ApplicationController
7
2
  <%- unless api_only_app? -%>
8
3
  include RackSessionDisable
9
4
  <%- end -%>
10
- include PropelAuthentication
5
+ include PropelAuthenticationConcern
11
6
 
12
- # POST <%= api_versioned? ? "/#{api_namespace}" : "" %>/auth/login
7
+ before_action :authenticate_user, only: [:show, :refresh]
8
+ before_action :validate_login_parameters, only: [:create]
9
+
10
+ # POST <%= auth_route_prefix %>/login
13
11
  def create
14
12
  user = User.find_by(email_address: params[:user][:email_address])
15
13
 
14
+ # Check if user exists and account is locked
15
+ if user&.respond_to?(:locked?) && user.locked?
16
+ render json: { error: 'Account is locked due to too many failed login attempts' }, status: :locked
17
+ return
18
+ end
19
+
16
20
  if user&.authenticate(params[:user][:password])
17
21
  if user.status == 1 # inactive
18
22
  render json: { error: 'Account is inactive' }, status: :unauthorized
@@ -22,17 +26,17 @@ class <%= controller_namespace %>::TokensController < ApplicationController
22
26
  # Update last login timestamp
23
27
  user.update(last_login_at: Time.current)
24
28
 
25
- # Reset failed login attempts on successful login
26
- user.reset_failed_attempts! if user.respond_to?(:reset_failed_attempts!)
29
+ # Reset failed login attempts on successful login (no bang!)
30
+ user.reset_failed_attempts if user.respond_to?(:reset_failed_attempts)
27
31
 
28
32
  render json: {
29
33
  token: user.generate_jwt_token,
30
34
  user: user_response(user)
31
35
  }, status: :ok
32
36
  else
33
- # Increment failed login attempts if user exists and has lockable functionality
34
- if user&.respond_to?(:increment_failed_attempts!)
35
- user.increment_failed_attempts!
37
+ # Increment failed login attempts if user exists and has lockable functionality (no bang!)
38
+ if user&.respond_to?(:increment_failed_attempts)
39
+ user.increment_failed_attempts
36
40
  end
37
41
 
38
42
  render json: { error: 'Invalid credentials' }, status: :unauthorized
@@ -41,24 +45,26 @@ class <%= controller_namespace %>::TokensController < ApplicationController
41
45
  render json: { error: 'Missing required parameters' }, status: :unprocessable_entity
42
46
  end
43
47
 
44
- # DELETE <%= api_versioned? ? "/#{api_namespace}" : "" %>/auth/logout
48
+ # DELETE <%= auth_route_prefix %>/logout
45
49
  def destroy
46
50
  # For JWT, logout is handled client-side by removing the token
47
51
  # Server-side logout would require token blacklisting (future enhancement)
48
52
  render json: { message: 'Logged out successfully' }, status: :ok
49
53
  end
50
54
 
51
- # GET <%= api_versioned? ? "/#{api_namespace}" : "" %>/auth/me
52
- def me
53
- return unless authenticate_user
54
- render json: { user: user_response(current_user) }, status: :ok
55
+ # GET <%= auth_route_prefix %>/me
56
+ def show
57
+ render json: {
58
+ user: user_response(@current_user)
59
+ }, status: :ok
55
60
  end
56
61
 
62
+ # POST <%= auth_route_prefix %>/refresh
57
63
  def refresh
58
64
  render json: { token: @current_user.generate_jwt_token }
59
65
  end
60
66
 
61
- # POST <%= api_versioned? ? "/#{api_namespace}" : "" %>/auth/unlock
67
+ # POST <%= auth_route_prefix %>/unlock
62
68
  def unlock
63
69
  token = params[:token]
64
70
 
@@ -71,7 +77,10 @@ class <%= controller_namespace %>::TokensController < ApplicationController
71
77
 
72
78
  if user
73
79
  user.unlock_account!
74
- render json: { message: 'Account unlocked successfully' }, status: :ok
80
+ render json: {
81
+ message: 'Account unlocked successfully',
82
+ user: user_response(user)
83
+ }, status: :ok
75
84
  else
76
85
  render json: { error: 'Invalid or expired unlock token' }, status: :unauthorized
77
86
  end
@@ -88,9 +97,17 @@ class <%= controller_namespace %>::TokensController < ApplicationController
88
97
  username: user.username,
89
98
  first_name: user.first_name,
90
99
  last_name: user.last_name,
100
+ status: user.status,
91
101
  organization_id: user.organization_id,
102
+ agency_ids: user.agency_ids,
103
+ confirmed_at: user.confirmed_at,
92
104
  last_login_at: user.last_login_at
93
105
  }
94
106
  end
95
- end
96
- <%- end -%>
107
+
108
+ def validate_login_parameters
109
+ unless params[:user].present? && params[:user][:email_address].present? && params[:user][:password].present?
110
+ render json: { error: 'Email address and password are required' }, status: :unprocessable_entity
111
+ end
112
+ end
113
+ end
@@ -85,6 +85,7 @@ class AuthMailer < ApplicationMailer
85
85
  @organization_name = organization_name(@user)
86
86
  @display_name = display_name(@user)
87
87
  @support_email = support_email
88
+ @confirmation_url = confirmation_url(@user)
88
89
 
89
90
  # Validate required parameters
90
91
  raise ArgumentError, "User is required" unless @user
@@ -167,7 +168,8 @@ class AuthMailer < ApplicationMailer
167
168
  # Generate confirmation URL with token
168
169
  def confirmation_url(user)
169
170
  token = user.confirmation_token
170
- base_url = PropelAuthentication.configuration.frontend_url || "#{request.protocol}#{request.host_with_port}"
171
+ base_url = PropelAuthentication.configuration.frontend_url ||
172
+ (defined?(request) ? "#{request.protocol}#{request.host_with_port}" : "http://localhost:3000")
171
173
  "#{base_url}/auth/confirm?token=#{token}"
172
174
  end
173
175
 
@@ -7,17 +7,23 @@ module Authenticatable
7
7
  validates :password, presence: true, length: { minimum: 8 }, if: :password_digest_changed?
8
8
  end
9
9
 
10
- # Generate JWT token for user authentication
10
+ # Generate JWT token for user authentication with minimal claims
11
11
  def generate_jwt_token
12
12
  payload = {
13
13
  user_id: id,
14
14
  email_address: email_address,
15
15
  organization_id: organization_id,
16
+ # Removed: agency_ids (now looked up in real-time for better security)
16
17
  iat: Time.now.to_i,
17
18
  exp: PropelAuthentication.configuration.jwt_expiration.from_now.to_i
18
19
  }
19
20
 
20
- JWT.encode(payload, PropelAuthentication.configuration.jwt_secret, 'HS256')
21
+ JWT.encode(payload, PropelAuthentication.configuration.jwt_secret, 'HS256')
22
+ end
23
+
24
+ # Get all agency IDs this user has access to through agent profiles
25
+ def agency_ids
26
+ agents.pluck(:agency_id)
21
27
  end
22
28
 
23
29
  class_methods do
@@ -73,7 +73,7 @@ module Confirmable
73
73
 
74
74
  def generate_confirmation_token
75
75
  self.confirmation_token = generate_secure_token
76
- self.confirmation_sent_at = Time.current
76
+ # Don't set confirmation_sent_at here - it will be set when email is actually sent
77
77
  end
78
78
 
79
79
  def generate_confirmation_token_if_email_changed
@@ -38,15 +38,17 @@ module Lockable
38
38
  update!(failed_login_attempts: 0)
39
39
  end
40
40
 
41
- # Manually lock the account
41
+ # Manually lock the account (idempotent - won't change timestamp if already locked)
42
42
  def lock_account!
43
+ return if locked? # Don't re-lock if already locked (preserves original timestamp)
44
+
43
45
  update!(
44
46
  locked_at: Time.current,
45
47
  failed_login_attempts: PropelAuthentication.configuration.max_failed_attempts
46
48
  )
47
49
 
48
50
  # Send account locked email notification if enabled
49
- if PropelAuth.configuration.enable_email_notifications
51
+ if PropelAuthentication.configuration.enable_email_notifications
50
52
  email_result = AuthNotificationService.send_account_unlock_email(self)
51
53
 
52
54
  if email_result[:success]