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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +128 -0
- data/README.md +254 -116
- data/lib/generators/{propel_auth → propel_authentication}/install_generator.rb +152 -170
- data/lib/generators/propel_authentication/templates/application_mailer.rb +6 -0
- data/lib/generators/propel_authentication/templates/auth/passwords_controller.rb.tt +132 -0
- data/lib/generators/propel_authentication/templates/auth/signup_controller.rb.tt +242 -0
- data/lib/generators/{propel_auth/templates → propel_authentication/templates/auth}/tokens_controller.rb.tt +39 -22
- data/lib/generators/{propel_auth → propel_authentication}/templates/auth_mailer.rb +3 -1
- data/lib/generators/{propel_auth → propel_authentication}/templates/authenticatable.rb +10 -4
- data/lib/generators/{propel_auth → propel_authentication}/templates/concerns/confirmable.rb +3 -3
- data/lib/generators/{propel_auth → propel_authentication}/templates/concerns/lockable.rb +10 -8
- data/lib/generators/{propel_auth/templates/concerns/propel_authentication.rb → propel_authentication/templates/concerns/propel_authentication_concern.rb} +33 -3
- data/lib/generators/{propel_auth → propel_authentication}/templates/concerns/recoverable.rb +21 -11
- data/lib/generators/propel_authentication/templates/core/configuration_methods.rb +191 -0
- data/lib/generators/propel_authentication/templates/db/seeds.rb +75 -0
- data/lib/generators/propel_authentication/templates/doc/signup_flow.md +315 -0
- data/lib/generators/propel_authentication/templates/models/agency.rb.tt +13 -0
- data/lib/generators/propel_authentication/templates/models/agent.rb.tt +13 -0
- data/lib/generators/{propel_auth/templates/invitation.rb → propel_authentication/templates/models/invitation.rb.tt} +8 -2
- data/lib/generators/propel_authentication/templates/models/organization.rb.tt +12 -0
- data/lib/generators/{propel_auth/templates/user.rb → propel_authentication/templates/models/user.rb.tt} +5 -0
- data/lib/generators/propel_authentication/templates/propel_authentication.rb.tt +218 -0
- data/lib/generators/propel_authentication/templates/routes/auth_routes.rb.tt +55 -0
- data/lib/generators/{propel_auth → propel_authentication}/templates/services/auth_notification_service.rb +3 -3
- data/lib/generators/{propel_auth → propel_authentication}/templates/test/concerns/confirmable_test.rb.tt +34 -10
- data/lib/generators/{propel_auth → propel_authentication}/templates/test/concerns/lockable_test.rb.tt +12 -12
- data/lib/generators/{propel_auth → propel_authentication}/templates/test/concerns/propel_authentication_test.rb.tt +2 -2
- data/lib/generators/{propel_auth → propel_authentication}/templates/test/concerns/recoverable_test.rb.tt +11 -11
- data/lib/generators/{propel_auth → propel_authentication}/templates/test/controllers/auth/lockable_integration_test.rb.tt +18 -15
- data/lib/generators/{propel_auth → propel_authentication}/templates/test/controllers/auth/password_reset_integration_test.rb.tt +38 -40
- data/lib/generators/propel_authentication/templates/test/controllers/auth/signup_controller_test.rb.tt +201 -0
- data/lib/generators/{propel_auth → propel_authentication}/templates/test/controllers/auth/tokens_controller_test.rb.tt +33 -25
- data/lib/generators/{propel_auth → propel_authentication}/templates/test/mailers/auth_mailer_test.rb.tt +51 -36
- data/lib/generators/{propel_auth → propel_authentication}/templates/user_test.rb.tt +1 -1
- data/lib/generators/{propel_auth → propel_authentication}/templates/views/auth_mailer/email_confirmation.html.erb +2 -2
- data/lib/generators/{propel_auth → propel_authentication}/templates/views/auth_mailer/email_confirmation.text.erb +1 -1
- data/lib/generators/{propel_auth → propel_authentication}/test/generators/authentication/install_generator_test.rb +4 -4
- data/lib/generators/{propel_auth → propel_authentication}/test/generators/authentication/uninstall_generator_test.rb +1 -1
- data/lib/generators/{propel_auth → propel_authentication}/test/integration/generator_integration_test.rb +1 -1
- data/lib/generators/{propel_auth → propel_authentication}/test/integration/multi_version_generator_test.rb +13 -12
- data/lib/generators/{propel_auth → propel_authentication}/unpack_generator.rb +55 -38
- data/lib/propel_authentication.rb +3 -0
- metadata +101 -98
- data/lib/generators/propel_auth/core/configuration_methods.rb +0 -134
- data/lib/generators/propel_auth/pack_generator.rb +0 -277
- data/lib/generators/propel_auth/templates/agency.rb +0 -7
- data/lib/generators/propel_auth/templates/agent.rb +0 -7
- data/lib/generators/propel_auth/templates/auth/base_passwords_controller.rb.tt +0 -99
- data/lib/generators/propel_auth/templates/auth/base_tokens_controller.rb.tt +0 -90
- data/lib/generators/propel_auth/templates/auth/passwords_controller.rb.tt +0 -126
- data/lib/generators/propel_auth/templates/db/seeds.rb +0 -29
- data/lib/generators/propel_auth/templates/organization.rb +0 -7
- data/lib/generators/propel_auth/templates/propel_auth.rb.tt +0 -141
- data/lib/propel_auth.rb +0 -3
- /data/lib/generators/{propel_auth → propel_authentication}/templates/concerns/rack_session_disable.rb +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/templates/config/environments/development_email.rb +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/templates/db/migrate/create_agencies.rb +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/templates/db/migrate/create_agents.rb +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/templates/db/migrate/create_invitations.rb +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/templates/db/migrate/create_organizations.rb +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/templates/db/migrate/create_users.rb +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/templates/test/mailers/previews/auth_mailer_preview.rb +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/templates/views/auth_mailer/account_unlock.html.erb +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/templates/views/auth_mailer/account_unlock.text.erb +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/templates/views/auth_mailer/password_reset.html.erb +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/templates/views/auth_mailer/password_reset.text.erb +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/templates/views/auth_mailer/user_invitation.html.erb +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/templates/views/auth_mailer/user_invitation.text.erb +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/Dockerfile +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/Gemfile +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/README.md +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/Rakefile +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/app/assets/stylesheets/application.css +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/app/controllers/application_controller.rb +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/app/helpers/application_helper.rb +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/app/jobs/application_job.rb +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/app/mailers/application_mailer.rb +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/app/models/application_record.rb +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/app/views/layouts/application.html.erb +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/app/views/layouts/mailer.html.erb +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/app/views/layouts/mailer.text.erb +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/app/views/pwa/manifest.json.erb +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/app/views/pwa/service-worker.js +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/bin/brakeman +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/bin/dev +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/bin/docker-entrypoint +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/bin/rails +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/bin/rake +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/bin/rubocop +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/bin/setup +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/bin/thrust +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/application.rb +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/boot.rb +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/cable.yml +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/credentials.yml.enc +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/database.yml +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/environment.rb +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/environments/development.rb +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/environments/production.rb +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/environments/test.rb +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/initializers/assets.rb +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/initializers/content_security_policy.rb +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/initializers/filter_parameter_logging.rb +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/initializers/inflections.rb +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/locales/en.yml +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/master.key +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/puma.rb +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/routes.rb +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/storage.yml +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config.ru +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/db/schema.rb +0 -0
- /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
|
-
|
|
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
|
|
5
|
+
include PropelAuthenticationConcern
|
|
11
6
|
|
|
12
|
-
|
|
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
|
|
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 <%=
|
|
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 <%=
|
|
52
|
-
def
|
|
53
|
-
|
|
54
|
-
|
|
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 <%=
|
|
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: {
|
|
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
|
-
|
|
96
|
-
|
|
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 =
|
|
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
|
-
|
|
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:
|
|
18
|
+
exp: PropelAuthentication.configuration.jwt_expiration.from_now.to_i
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
111
|
+
PropelAuthentication.configuration.confirmation_period || 24.hours
|
|
112
112
|
end
|
|
113
113
|
|
|
114
114
|
def confirmation_resend_interval
|
|
115
|
-
|
|
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 +
|
|
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 >=
|
|
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:
|
|
47
|
+
failed_login_attempts: PropelAuthentication.configuration.max_failed_attempts
|
|
46
48
|
)
|
|
47
49
|
|
|
48
50
|
# Send account locked email notification if enabled
|
|
49
|
-
if
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
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:
|
|
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,
|
|
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,
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
|
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,
|
|
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,
|
|
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
|