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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +43 -2
- data/README.md +6 -6
- data/lib/generators/propel_authentication/install_generator.rb +135 -153
- data/lib/generators/propel_authentication/templates/application_mailer.rb +6 -0
- data/lib/generators/propel_authentication/templates/auth/passwords_controller.rb.tt +84 -78
- data/lib/generators/propel_authentication/templates/auth/signup_controller.rb.tt +242 -0
- data/lib/generators/propel_authentication/templates/{tokens_controller.rb.tt → auth/tokens_controller.rb.tt} +39 -22
- data/lib/generators/propel_authentication/templates/auth_mailer.rb +3 -1
- data/lib/generators/propel_authentication/templates/authenticatable.rb +8 -2
- data/lib/generators/propel_authentication/templates/concerns/confirmable.rb +1 -1
- data/lib/generators/propel_authentication/templates/concerns/lockable.rb +4 -2
- data/lib/generators/propel_authentication/templates/concerns/{propel_authentication.rb → propel_authentication_concern.rb} +33 -3
- data/lib/generators/propel_authentication/templates/concerns/recoverable.rb +16 -6
- data/lib/generators/propel_authentication/templates/core/configuration_methods.rb +104 -64
- data/lib/generators/propel_authentication/templates/db/seeds.rb +50 -4
- 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_authentication/templates/{invitation.rb → models/invitation.rb.tt} +6 -0
- data/lib/generators/propel_authentication/templates/models/organization.rb.tt +12 -0
- data/lib/generators/propel_authentication/templates/{user.rb → models/user.rb.tt} +5 -0
- data/lib/generators/propel_authentication/templates/propel_authentication.rb.tt +94 -9
- data/lib/generators/propel_authentication/templates/routes/auth_routes.rb.tt +55 -0
- data/lib/generators/propel_authentication/templates/services/auth_notification_service.rb +3 -3
- data/lib/generators/propel_authentication/templates/test/concerns/confirmable_test.rb.tt +34 -10
- data/lib/generators/propel_authentication/templates/test/concerns/propel_authentication_test.rb.tt +1 -1
- data/lib/generators/propel_authentication/templates/test/concerns/recoverable_test.rb.tt +4 -4
- data/lib/generators/propel_authentication/templates/test/controllers/auth/lockable_integration_test.rb.tt +18 -15
- data/lib/generators/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_authentication/templates/test/controllers/auth/tokens_controller_test.rb.tt +33 -25
- data/lib/generators/propel_authentication/templates/test/mailers/auth_mailer_test.rb.tt +51 -36
- data/lib/generators/propel_authentication/templates/views/auth_mailer/email_confirmation.html.erb +2 -2
- data/lib/generators/propel_authentication/templates/views/auth_mailer/email_confirmation.text.erb +1 -1
- data/lib/generators/propel_authentication/test/generators/authentication/install_generator_test.rb +4 -4
- data/lib/generators/propel_authentication/test/generators/authentication/uninstall_generator_test.rb +1 -1
- data/lib/generators/propel_authentication/test/integration/generator_integration_test.rb +1 -1
- data/lib/generators/propel_authentication/test/integration/multi_version_generator_test.rb +13 -12
- data/lib/generators/propel_authentication/unpack_generator.rb +19 -15
- data/lib/propel_authentication.rb +1 -1
- metadata +14 -11
- data/lib/generators/propel_authentication/templates/agency.rb +0 -7
- data/lib/generators/propel_authentication/templates/agent.rb +0 -7
- data/lib/generators/propel_authentication/templates/auth/base_passwords_controller.rb.tt +0 -99
- data/lib/generators/propel_authentication/templates/auth/base_tokens_controller.rb.tt +0 -90
- data/lib/generators/propel_authentication/templates/organization.rb +0 -7
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
end
|
|
5
|
-
|
|
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 <%=
|
|
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
|
|
18
|
-
unless email_address.match?(
|
|
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
|
|
30
|
-
#
|
|
31
|
-
|
|
23
|
+
if user
|
|
24
|
+
# Generate and save reset token
|
|
25
|
+
user.generate_password_reset_token
|
|
32
26
|
|
|
33
|
-
#
|
|
34
|
-
if
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
42
|
-
render json: {
|
|
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
|
-
#
|
|
46
|
-
def
|
|
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
|
-
|
|
54
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
70
|
+
token: token
|
|
84
71
|
}
|
|
85
72
|
}, status: :ok
|
|
86
73
|
else
|
|
87
|
-
render json: {
|
|
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
|
-
#
|
|
92
|
-
def
|
|
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
|
|
90
|
+
# Validate required parameters
|
|
96
91
|
if token.blank?
|
|
97
|
-
return render json: { error:
|
|
92
|
+
return render json: { error: 'Reset token is required' }, status: :unprocessable_entity
|
|
98
93
|
end
|
|
99
94
|
|
|
100
|
-
|
|
101
|
-
|
|
95
|
+
if new_password.blank?
|
|
96
|
+
return render json: { error: 'New password is required' }, status: :unprocessable_entity
|
|
97
|
+
end
|
|
102
98
|
|
|
103
|
-
if
|
|
104
|
-
render json: {
|
|
105
|
-
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
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 = PropelAuthentication.configuration.frontend_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
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
|
|
@@ -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
|
|
@@ -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
|
|
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]
|