propel_authentication 0.1.3 → 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 (113) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +128 -0
  3. data/README.md +254 -116
  4. data/lib/generators/{propel_auth → propel_authentication}/install_generator.rb +152 -170
  5. data/lib/generators/propel_authentication/templates/application_mailer.rb +6 -0
  6. data/lib/generators/propel_authentication/templates/auth/passwords_controller.rb.tt +132 -0
  7. data/lib/generators/propel_authentication/templates/auth/signup_controller.rb.tt +242 -0
  8. data/lib/generators/{propel_auth/templates → propel_authentication/templates/auth}/tokens_controller.rb.tt +39 -22
  9. data/lib/generators/{propel_auth → propel_authentication}/templates/auth_mailer.rb +3 -1
  10. data/lib/generators/{propel_auth → propel_authentication}/templates/authenticatable.rb +10 -4
  11. data/lib/generators/{propel_auth → propel_authentication}/templates/concerns/confirmable.rb +3 -3
  12. data/lib/generators/{propel_auth → propel_authentication}/templates/concerns/lockable.rb +10 -8
  13. data/lib/generators/{propel_auth/templates/concerns/propel_authentication.rb → propel_authentication/templates/concerns/propel_authentication_concern.rb} +33 -3
  14. data/lib/generators/{propel_auth → propel_authentication}/templates/concerns/recoverable.rb +21 -11
  15. data/lib/generators/propel_authentication/templates/core/configuration_methods.rb +191 -0
  16. data/lib/generators/propel_authentication/templates/db/seeds.rb +75 -0
  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_auth/templates/invitation.rb → propel_authentication/templates/models/invitation.rb.tt} +8 -2
  21. data/lib/generators/propel_authentication/templates/models/organization.rb.tt +12 -0
  22. data/lib/generators/{propel_auth/templates/user.rb → propel_authentication/templates/models/user.rb.tt} +5 -0
  23. data/lib/generators/propel_authentication/templates/propel_authentication.rb.tt +218 -0
  24. data/lib/generators/propel_authentication/templates/routes/auth_routes.rb.tt +55 -0
  25. data/lib/generators/{propel_auth → propel_authentication}/templates/services/auth_notification_service.rb +3 -3
  26. data/lib/generators/{propel_auth → propel_authentication}/templates/test/concerns/confirmable_test.rb.tt +34 -10
  27. data/lib/generators/{propel_auth → propel_authentication}/templates/test/concerns/lockable_test.rb.tt +12 -12
  28. data/lib/generators/{propel_auth → propel_authentication}/templates/test/concerns/propel_authentication_test.rb.tt +2 -2
  29. data/lib/generators/{propel_auth → propel_authentication}/templates/test/concerns/recoverable_test.rb.tt +11 -11
  30. data/lib/generators/{propel_auth → propel_authentication}/templates/test/controllers/auth/lockable_integration_test.rb.tt +18 -15
  31. data/lib/generators/{propel_auth → propel_authentication}/templates/test/controllers/auth/password_reset_integration_test.rb.tt +38 -40
  32. data/lib/generators/propel_authentication/templates/test/controllers/auth/signup_controller_test.rb.tt +201 -0
  33. data/lib/generators/{propel_auth → propel_authentication}/templates/test/controllers/auth/tokens_controller_test.rb.tt +33 -25
  34. data/lib/generators/{propel_auth → propel_authentication}/templates/test/mailers/auth_mailer_test.rb.tt +51 -36
  35. data/lib/generators/{propel_auth → propel_authentication}/templates/user_test.rb.tt +1 -1
  36. data/lib/generators/{propel_auth → propel_authentication}/templates/views/auth_mailer/email_confirmation.html.erb +2 -2
  37. data/lib/generators/{propel_auth → propel_authentication}/templates/views/auth_mailer/email_confirmation.text.erb +1 -1
  38. data/lib/generators/{propel_auth → propel_authentication}/test/generators/authentication/install_generator_test.rb +4 -4
  39. data/lib/generators/{propel_auth → propel_authentication}/test/generators/authentication/uninstall_generator_test.rb +1 -1
  40. data/lib/generators/{propel_auth → propel_authentication}/test/integration/generator_integration_test.rb +1 -1
  41. data/lib/generators/{propel_auth → propel_authentication}/test/integration/multi_version_generator_test.rb +13 -12
  42. data/lib/generators/{propel_auth → propel_authentication}/unpack_generator.rb +55 -38
  43. data/lib/propel_authentication.rb +3 -0
  44. metadata +101 -98
  45. data/lib/generators/propel_auth/core/configuration_methods.rb +0 -134
  46. data/lib/generators/propel_auth/pack_generator.rb +0 -277
  47. data/lib/generators/propel_auth/templates/agency.rb +0 -7
  48. data/lib/generators/propel_auth/templates/agent.rb +0 -7
  49. data/lib/generators/propel_auth/templates/auth/base_passwords_controller.rb.tt +0 -99
  50. data/lib/generators/propel_auth/templates/auth/base_tokens_controller.rb.tt +0 -90
  51. data/lib/generators/propel_auth/templates/auth/passwords_controller.rb.tt +0 -126
  52. data/lib/generators/propel_auth/templates/db/seeds.rb +0 -29
  53. data/lib/generators/propel_auth/templates/organization.rb +0 -7
  54. data/lib/generators/propel_auth/templates/propel_auth.rb.tt +0 -141
  55. data/lib/propel_auth.rb +0 -3
  56. /data/lib/generators/{propel_auth → propel_authentication}/templates/concerns/rack_session_disable.rb +0 -0
  57. /data/lib/generators/{propel_auth → propel_authentication}/templates/config/environments/development_email.rb +0 -0
  58. /data/lib/generators/{propel_auth → propel_authentication}/templates/db/migrate/create_agencies.rb +0 -0
  59. /data/lib/generators/{propel_auth → propel_authentication}/templates/db/migrate/create_agents.rb +0 -0
  60. /data/lib/generators/{propel_auth → propel_authentication}/templates/db/migrate/create_invitations.rb +0 -0
  61. /data/lib/generators/{propel_auth → propel_authentication}/templates/db/migrate/create_organizations.rb +0 -0
  62. /data/lib/generators/{propel_auth → propel_authentication}/templates/db/migrate/create_users.rb +0 -0
  63. /data/lib/generators/{propel_auth → propel_authentication}/templates/test/mailers/previews/auth_mailer_preview.rb +0 -0
  64. /data/lib/generators/{propel_auth → propel_authentication}/templates/views/auth_mailer/account_unlock.html.erb +0 -0
  65. /data/lib/generators/{propel_auth → propel_authentication}/templates/views/auth_mailer/account_unlock.text.erb +0 -0
  66. /data/lib/generators/{propel_auth → propel_authentication}/templates/views/auth_mailer/password_reset.html.erb +0 -0
  67. /data/lib/generators/{propel_auth → propel_authentication}/templates/views/auth_mailer/password_reset.text.erb +0 -0
  68. /data/lib/generators/{propel_auth → propel_authentication}/templates/views/auth_mailer/user_invitation.html.erb +0 -0
  69. /data/lib/generators/{propel_auth → propel_authentication}/templates/views/auth_mailer/user_invitation.text.erb +0 -0
  70. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/Dockerfile +0 -0
  71. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/Gemfile +0 -0
  72. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/README.md +0 -0
  73. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/Rakefile +0 -0
  74. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/app/assets/stylesheets/application.css +0 -0
  75. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/app/controllers/application_controller.rb +0 -0
  76. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/app/helpers/application_helper.rb +0 -0
  77. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/app/jobs/application_job.rb +0 -0
  78. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/app/mailers/application_mailer.rb +0 -0
  79. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/app/models/application_record.rb +0 -0
  80. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/app/views/layouts/application.html.erb +0 -0
  81. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/app/views/layouts/mailer.html.erb +0 -0
  82. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/app/views/layouts/mailer.text.erb +0 -0
  83. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/app/views/pwa/manifest.json.erb +0 -0
  84. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/app/views/pwa/service-worker.js +0 -0
  85. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/bin/brakeman +0 -0
  86. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/bin/dev +0 -0
  87. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/bin/docker-entrypoint +0 -0
  88. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/bin/rails +0 -0
  89. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/bin/rake +0 -0
  90. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/bin/rubocop +0 -0
  91. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/bin/setup +0 -0
  92. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/bin/thrust +0 -0
  93. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/application.rb +0 -0
  94. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/boot.rb +0 -0
  95. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/cable.yml +0 -0
  96. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/credentials.yml.enc +0 -0
  97. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/database.yml +0 -0
  98. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/environment.rb +0 -0
  99. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/environments/development.rb +0 -0
  100. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/environments/production.rb +0 -0
  101. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/environments/test.rb +0 -0
  102. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/initializers/assets.rb +0 -0
  103. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/initializers/content_security_policy.rb +0 -0
  104. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/initializers/filter_parameter_logging.rb +0 -0
  105. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/initializers/inflections.rb +0 -0
  106. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/locales/en.yml +0 -0
  107. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/master.key +0 -0
  108. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/puma.rb +0 -0
  109. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/routes.rb +0 -0
  110. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/storage.yml +0 -0
  111. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config.ru +0 -0
  112. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/db/schema.rb +0 -0
  113. /data/lib/generators/{propel_auth → propel_authentication}/test/generators/authentication/controllers/tokens_controller_test.rb +0 -0
@@ -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 = PropelAuth.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
- exp: PropelAuth.configuration.jwt_expiration.from_now.to_i
18
+ exp: PropelAuthentication.configuration.jwt_expiration.from_now.to_i
18
19
  }
19
20
 
20
- JWT.encode(payload, PropelAuth.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
@@ -25,7 +31,7 @@ module Authenticatable
25
31
  def find_by_jwt_token(token)
26
32
  return nil unless token
27
33
 
28
- decoded_token = JWT.decode(token, PropelAuth.configuration.jwt_secret, true, { algorithm: 'HS256' })
34
+ decoded_token = JWT.decode(token, PropelAuthentication.configuration.jwt_secret, true, { algorithm: 'HS256' })
29
35
  payload = decoded_token.first
30
36
 
31
37
  # Check if token is expired (JWT gem handles this, but double-check)
@@ -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
@@ -108,11 +108,11 @@ module Confirmable
108
108
  end
109
109
 
110
110
  def confirmation_period
111
- PropelAuth.configuration.confirmation_period || 24.hours
111
+ PropelAuthentication.configuration.confirmation_period || 24.hours
112
112
  end
113
113
 
114
114
  def confirmation_resend_interval
115
- PropelAuth.configuration.confirmation_resend_interval || 1.minute
115
+ PropelAuthentication.configuration.confirmation_resend_interval || 1.minute
116
116
  end
117
117
 
118
118
  module ClassMethods
@@ -6,7 +6,7 @@ module Lockable
6
6
  return false unless locked_at.present?
7
7
 
8
8
  # Check if lockout duration has passed (automatic unlock)
9
- if locked_at + PropelAuth.configuration.lockout_duration > Time.current
9
+ if locked_at + PropelAuthentication.configuration.lockout_duration > Time.current
10
10
  true # Still within lockout period
11
11
  else
12
12
  # Lockout period expired, automatically unlock
@@ -26,7 +26,7 @@ module Lockable
26
26
 
27
27
  self.failed_login_attempts = (failed_login_attempts || 0) + 1
28
28
 
29
- if failed_login_attempts >= PropelAuth.configuration.max_failed_attempts
29
+ if failed_login_attempts >= PropelAuthentication.configuration.max_failed_attempts
30
30
  lock_account!
31
31
  else
32
32
  save!
@@ -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
- failed_login_attempts: PropelAuth.configuration.max_failed_attempts
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]
@@ -75,7 +77,7 @@ module Lockable
75
77
  exp: 1.hour.from_now.to_i # Unlock tokens expire in 1 hour
76
78
  }
77
79
 
78
- JWT.encode(payload, PropelAuth.configuration.jwt_secret, 'HS256')
80
+ JWT.encode(payload, PropelAuthentication.configuration.jwt_secret, 'HS256')
79
81
  end
80
82
 
81
83
  # Unlock account using a valid unlock token
@@ -83,7 +85,7 @@ module Lockable
83
85
  return false unless token.present?
84
86
 
85
87
  begin
86
- decoded_token = JWT.decode(token, PropelAuth.configuration.jwt_secret, true, { algorithm: 'HS256' })
88
+ decoded_token = JWT.decode(token, PropelAuthentication.configuration.jwt_secret, true, { algorithm: 'HS256' })
87
89
  payload = decoded_token.first
88
90
 
89
91
  # Verify token is for this user and is an unlock token
@@ -104,7 +106,7 @@ module Lockable
104
106
  return nil unless token.present?
105
107
 
106
108
  begin
107
- decoded_token = JWT.decode(token, PropelAuth.configuration.jwt_secret, true, { algorithm: 'HS256' })
109
+ decoded_token = JWT.decode(token, PropelAuthentication.configuration.jwt_secret, true, { algorithm: 'HS256' })
108
110
  payload = decoded_token.first
109
111
 
110
112
  # Check if token is expired (JWT gem handles this, but double-check)
@@ -1,10 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module PropelAuthentication
3
+ module PropelAuthenticationConcern
4
4
  extend ActiveSupport::Concern
5
5
 
6
- private
7
-
8
6
  def authenticate_user
9
7
  token = extract_jwt_token
10
8
 
@@ -19,6 +17,10 @@ module PropelAuthentication
19
17
  render json: { error: 'Invalid token' }, status: :unauthorized
20
18
  return false
21
19
  end
20
+
21
+ # Extract organization context from JWT payload
22
+ extract_organization_context(token)
23
+
22
24
  rescue JWT::ExpiredSignature
23
25
  render json: { error: 'Token expired' }, status: :unauthorized
24
26
  return false
@@ -34,6 +36,25 @@ module PropelAuthentication
34
36
  @current_user
35
37
  end
36
38
 
39
+ def current_organization_id
40
+ @current_organization_id
41
+ end
42
+
43
+ def current_organization
44
+ @current_organization ||= Organization.find_by(id: current_organization_id) if current_organization_id
45
+ end
46
+
47
+ def current_agency_ids
48
+ return [] unless current_user
49
+
50
+ # Request-scoped memoization for performance without security risk
51
+ @current_agency_ids ||= current_user.agency_ids
52
+ end
53
+
54
+ def has_agency_access?(agency_id)
55
+ current_agency_ids.include?(agency_id.to_i)
56
+ end
57
+
37
58
  def extract_jwt_token
38
59
  auth_header = request.headers['Authorization']
39
60
  return nil unless auth_header
@@ -41,4 +62,13 @@ module PropelAuthentication
41
62
  # Extract token from "Bearer <token>" format
42
63
  auth_header.split(' ').last if auth_header.start_with?('Bearer ')
43
64
  end
65
+
66
+ private
67
+
68
+ def extract_organization_context(token)
69
+ decoded_token = JWT.decode(token, PropelAuthentication.configuration.jwt_secret, true, { algorithm: 'HS256' })
70
+ payload = decoded_token.first
71
+ @current_organization_id = payload['organization_id']
72
+ # Removed: @current_agency_ids (now looked up in real-time via current_user_agency_ids)
73
+ end
44
74
  end
@@ -15,12 +15,12 @@ module Recoverable
15
15
  email_address: self.email_address,
16
16
  type: 'password_reset',
17
17
  iat: now.to_f, # Use float for higher precision
18
- exp: PropelAuth.configuration.password_reset_expiration.from_now.to_i,
18
+ exp: PropelAuthentication.configuration.password_reset_expiration.from_now.to_i,
19
19
  password_hash: self.password_digest[0..10], # Bind token to current password
20
20
  nonce: SecureRandom.hex(8) # Add random nonce for uniqueness
21
21
  }
22
22
 
23
- JWT.encode(payload, PropelAuth.configuration.jwt_secret, 'HS256')
23
+ JWT.encode(payload, PropelAuthentication.configuration.jwt_secret, 'HS256')
24
24
  end
25
25
 
26
26
  # Validate password reset token
@@ -28,7 +28,7 @@ module Recoverable
28
28
  return false if token.blank?
29
29
 
30
30
  begin
31
- decoded = JWT.decode(token, PropelAuth.configuration.jwt_secret, true, { algorithm: 'HS256' })
31
+ decoded = JWT.decode(token, PropelAuthentication.configuration.jwt_secret, true, { algorithm: 'HS256' })
32
32
  payload = decoded.first
33
33
 
34
34
  # Validate token structure and content
@@ -51,12 +51,22 @@ module Recoverable
51
51
  # Reset password with token validation
52
52
  def reset_password_with_token!(token, new_password, new_password_confirmation = new_password)
53
53
  return false unless valid_password_reset_token?(token)
54
- return false if new_password.blank?
55
- return false if new_password != new_password_confirmation
56
54
 
57
- # Validate password length (configuration is a Range)
58
- password_range = PropelAuth.configuration.password_length
59
- return false unless password_range.include?(new_password.length)
55
+ if new_password.blank?
56
+ errors.add(:password, "cannot be blank")
57
+ return false
58
+ end
59
+
60
+ if new_password != new_password_confirmation
61
+ errors.add(:password_confirmation, "doesn't match Password")
62
+ return false
63
+ end
64
+
65
+ # Validate password length using Rails validations (populates errors)
66
+ self.password = new_password
67
+ unless valid?
68
+ return false # Rails validations populate errors automatically
69
+ end
60
70
 
61
71
  begin
62
72
  # Update password and clear failed login attempts
@@ -73,11 +83,11 @@ module Recoverable
73
83
 
74
84
  class_methods do
75
85
  # Find user by JWT password reset token
76
- def find_user_by_password_reset_token(token)
86
+ def find_by_jwt_password_reset_token(token)
77
87
  return nil if token.blank?
78
88
 
79
89
  begin
80
- decoded = JWT.decode(token, PropelAuth.configuration.jwt_secret, true, { algorithm: 'HS256' })
90
+ decoded = JWT.decode(token, PropelAuthentication.configuration.jwt_secret, true, { algorithm: 'HS256' })
81
91
  payload = decoded.first
82
92
 
83
93
  # Validate token structure
@@ -102,7 +112,7 @@ module Recoverable
102
112
  return nil if token.blank?
103
113
 
104
114
  begin
105
- decoded = JWT.decode(token, PropelAuth.configuration.jwt_secret, true, { algorithm: 'HS256' })
115
+ decoded = JWT.decode(token, PropelAuthentication.configuration.jwt_secret, true, { algorithm: 'HS256' })
106
116
  decoded.first['user_id']
107
117
  rescue JWT::DecodeError
108
118
  nil