propel_authentication 0.1.3 → 0.1.4
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 +87 -0
- data/README.md +251 -113
- data/lib/generators/{propel_auth → propel_authentication}/install_generator.rb +17 -17
- data/lib/generators/{propel_auth → propel_authentication}/templates/auth_mailer.rb +1 -1
- data/lib/generators/{propel_auth → propel_authentication}/templates/authenticatable.rb +3 -3
- data/lib/generators/{propel_auth → propel_authentication}/templates/concerns/confirmable.rb +2 -2
- data/lib/generators/{propel_auth → propel_authentication}/templates/concerns/lockable.rb +6 -6
- data/lib/generators/{propel_auth → propel_authentication}/templates/concerns/recoverable.rb +6 -6
- data/lib/generators/{propel_auth → propel_authentication/templates}/core/configuration_methods.rb +38 -21
- data/lib/generators/{propel_auth → propel_authentication}/templates/invitation.rb +2 -2
- data/lib/generators/{propel_auth/templates/propel_auth.rb.tt → propel_authentication/templates/propel_authentication.rb.tt} +6 -14
- 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 +1 -1
- data/lib/generators/{propel_auth → propel_authentication}/templates/test/concerns/recoverable_test.rb.tt +7 -7
- data/lib/generators/{propel_auth → propel_authentication}/templates/user_test.rb.tt +1 -1
- data/lib/generators/{propel_auth → propel_authentication}/unpack_generator.rb +38 -25
- data/lib/propel_authentication.rb +3 -0
- metadata +98 -98
- data/lib/generators/propel_auth/pack_generator.rb +0 -277
- data/lib/propel_auth.rb +0 -3
- /data/lib/generators/{propel_auth → propel_authentication}/templates/agency.rb +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/templates/agent.rb +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/templates/auth/base_passwords_controller.rb.tt +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/templates/auth/base_tokens_controller.rb.tt +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/templates/auth/passwords_controller.rb.tt +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/templates/concerns/propel_authentication.rb +0 -0
- /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/db/seeds.rb +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/templates/organization.rb +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/templates/services/auth_notification_service.rb +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/templates/test/concerns/confirmable_test.rb.tt +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/templates/test/controllers/auth/lockable_integration_test.rb.tt +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/templates/test/controllers/auth/password_reset_integration_test.rb.tt +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/templates/test/controllers/auth/tokens_controller_test.rb.tt +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/templates/test/mailers/auth_mailer_test.rb.tt +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/tokens_controller.rb.tt +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/templates/user.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/email_confirmation.html.erb +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/templates/views/auth_mailer/email_confirmation.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
- /data/lib/generators/{propel_auth → propel_authentication}/test/generators/authentication/install_generator_test.rb +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/generators/authentication/uninstall_generator_test.rb +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/integration/generator_integration_test.rb +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/integration/multi_version_generator_test.rb +0 -0
|
@@ -2,15 +2,15 @@ require 'rails/generators/base'
|
|
|
2
2
|
require 'rails/generators/active_record'
|
|
3
3
|
require_relative 'unpack_generator'
|
|
4
4
|
##
|
|
5
|
-
#
|
|
5
|
+
# PropelAuthentication installer that provides JWT-based authentication for Rails applications
|
|
6
6
|
#
|
|
7
7
|
# This creates a COMPLETELY STANDALONE authentication system with no gem dependencies.
|
|
8
8
|
# Usage:
|
|
9
|
-
# rails generate
|
|
9
|
+
# rails generate propel_authentication:install # Full standalone system (runtime + generators)
|
|
10
10
|
#
|
|
11
11
|
# This generator:
|
|
12
12
|
# 1. Installs all runtime code (models, controllers, views, tests, etc.)
|
|
13
|
-
# 2. Automatically extracts generator logic to lib/generators/
|
|
13
|
+
# 2. Automatically extracts generator logic to lib/generators/propel_authentication/
|
|
14
14
|
# 3. Creates a system that requires NO gem dependencies
|
|
15
15
|
#
|
|
16
16
|
# After installation, you can remove 'propel_auth' from your Gemfile completely.
|
|
@@ -19,13 +19,13 @@ require_relative 'unpack_generator'
|
|
|
19
19
|
# Like Rails' built-in generators (action_text:install, devise:install),
|
|
20
20
|
# this generator is designed to be run once per application.
|
|
21
21
|
#
|
|
22
|
-
require_relative 'core/configuration_methods'
|
|
22
|
+
require_relative 'templates/core/configuration_methods'
|
|
23
23
|
|
|
24
|
-
module
|
|
24
|
+
module PropelAuthentication
|
|
25
25
|
class InstallGenerator < Rails::Generators::Base
|
|
26
26
|
source_root File.expand_path("templates", __dir__)
|
|
27
27
|
include Rails::Generators::Migration
|
|
28
|
-
include
|
|
28
|
+
include PropelAuthentication::ConfigurationMethods
|
|
29
29
|
|
|
30
30
|
def self.next_migration_number(dir)
|
|
31
31
|
ActiveRecord::Generators::Base.next_migration_number(dir)
|
|
@@ -37,12 +37,12 @@ module PropelAuth
|
|
|
37
37
|
initialize_propel_auth_settings
|
|
38
38
|
|
|
39
39
|
if behavior == :revoke
|
|
40
|
-
remove_file "config/initializers/
|
|
41
|
-
say "Removed
|
|
40
|
+
remove_file "config/initializers/propel_authentication.rb"
|
|
41
|
+
say "Removed PropelAuthentication configuration", :red
|
|
42
42
|
else
|
|
43
43
|
# Convert to ERB template to support configuration
|
|
44
|
-
template "
|
|
45
|
-
say "Created
|
|
44
|
+
template "propel_authentication.rb.tt", "config/initializers/propel_authentication.rb"
|
|
45
|
+
say "Created PropelAuthentication configuration with namespace: #{namespace_display}, version: #{version_display}", :green
|
|
46
46
|
end
|
|
47
47
|
end
|
|
48
48
|
|
|
@@ -136,7 +136,7 @@ module PropelAuth
|
|
|
136
136
|
end
|
|
137
137
|
|
|
138
138
|
def extract_generator_for_customization
|
|
139
|
-
generator_path = "lib/generators/
|
|
139
|
+
generator_path = "lib/generators/propel_authentication"
|
|
140
140
|
|
|
141
141
|
if File.exist?(generator_path)
|
|
142
142
|
say ""
|
|
@@ -147,13 +147,13 @@ module PropelAuth
|
|
|
147
147
|
say "📦 Extracting generator logic for full customization...", :blue
|
|
148
148
|
|
|
149
149
|
# Automatically run the unpack generator to extract generator logic
|
|
150
|
-
invoke
|
|
150
|
+
invoke PropelAuthentication::UnpackGenerator, [], { force: false }
|
|
151
151
|
|
|
152
152
|
say ""
|
|
153
|
-
say "✅ Generator logic extracted to lib/generators/
|
|
153
|
+
say "✅ Generator logic extracted to lib/generators/propel_authentication/", :green
|
|
154
154
|
say "💡 Your application is now completely standalone - no gem dependency needed!", :cyan
|
|
155
155
|
say "🗑️ You can now remove 'propel_auth' from your Gemfile", :yellow
|
|
156
|
-
|
|
156
|
+
say "📦 All PropelAuthentication runtime code is now in config/initializers/propel_authentication.rb", :blue
|
|
157
157
|
end
|
|
158
158
|
end
|
|
159
159
|
|
|
@@ -175,11 +175,11 @@ module PropelAuth
|
|
|
175
175
|
end
|
|
176
176
|
|
|
177
177
|
say "\n🎨 Customization:", :bold
|
|
178
|
-
say "• Generator logic: lib/generators/
|
|
179
|
-
say "• Templates: lib/generators/
|
|
178
|
+
say "• Generator logic: lib/generators/propel_authentication/install_generator.rb", :blue
|
|
179
|
+
say "• Templates: lib/generators/propel_authentication/templates/", :blue
|
|
180
180
|
say "• Modify any part of the system - it's all yours now!", :cyan
|
|
181
181
|
|
|
182
|
-
say "\n🗑️ To uninstall: rails destroy
|
|
182
|
+
say "\n🗑️ To uninstall: rails destroy propel_authentication:install", :yellow
|
|
183
183
|
end
|
|
184
184
|
|
|
185
185
|
private
|
|
@@ -167,7 +167,7 @@ class AuthMailer < ApplicationMailer
|
|
|
167
167
|
# Generate confirmation URL with token
|
|
168
168
|
def confirmation_url(user)
|
|
169
169
|
token = user.confirmation_token
|
|
170
|
-
base_url =
|
|
170
|
+
base_url = PropelAuthentication.configuration.frontend_url || "#{request.protocol}#{request.host_with_port}"
|
|
171
171
|
"#{base_url}/auth/confirm?token=#{token}"
|
|
172
172
|
end
|
|
173
173
|
|
|
@@ -14,10 +14,10 @@ module Authenticatable
|
|
|
14
14
|
email_address: email_address,
|
|
15
15
|
organization_id: organization_id,
|
|
16
16
|
iat: Time.now.to_i,
|
|
17
|
-
exp:
|
|
17
|
+
exp: PropelAuthentication.configuration.jwt_expiration.from_now.to_i
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
JWT.encode(payload,
|
|
20
|
+
JWT.encode(payload, PropelAuthentication.configuration.jwt_secret, 'HS256')
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
class_methods do
|
|
@@ -25,7 +25,7 @@ module Authenticatable
|
|
|
25
25
|
def find_by_jwt_token(token)
|
|
26
26
|
return nil unless token
|
|
27
27
|
|
|
28
|
-
decoded_token = JWT.decode(token,
|
|
28
|
+
decoded_token = JWT.decode(token, PropelAuthentication.configuration.jwt_secret, true, { algorithm: 'HS256' })
|
|
29
29
|
payload = decoded_token.first
|
|
30
30
|
|
|
31
31
|
# Check if token is expired (JWT gem handles this, but double-check)
|
|
@@ -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!
|
|
@@ -42,7 +42,7 @@ module Lockable
|
|
|
42
42
|
def lock_account!
|
|
43
43
|
update!(
|
|
44
44
|
locked_at: Time.current,
|
|
45
|
-
failed_login_attempts:
|
|
45
|
+
failed_login_attempts: PropelAuthentication.configuration.max_failed_attempts
|
|
46
46
|
)
|
|
47
47
|
|
|
48
48
|
# Send account locked email notification if enabled
|
|
@@ -75,7 +75,7 @@ module Lockable
|
|
|
75
75
|
exp: 1.hour.from_now.to_i # Unlock tokens expire in 1 hour
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
JWT.encode(payload,
|
|
78
|
+
JWT.encode(payload, PropelAuthentication.configuration.jwt_secret, 'HS256')
|
|
79
79
|
end
|
|
80
80
|
|
|
81
81
|
# Unlock account using a valid unlock token
|
|
@@ -83,7 +83,7 @@ module Lockable
|
|
|
83
83
|
return false unless token.present?
|
|
84
84
|
|
|
85
85
|
begin
|
|
86
|
-
decoded_token = JWT.decode(token,
|
|
86
|
+
decoded_token = JWT.decode(token, PropelAuthentication.configuration.jwt_secret, true, { algorithm: 'HS256' })
|
|
87
87
|
payload = decoded_token.first
|
|
88
88
|
|
|
89
89
|
# Verify token is for this user and is an unlock token
|
|
@@ -104,7 +104,7 @@ module Lockable
|
|
|
104
104
|
return nil unless token.present?
|
|
105
105
|
|
|
106
106
|
begin
|
|
107
|
-
decoded_token = JWT.decode(token,
|
|
107
|
+
decoded_token = JWT.decode(token, PropelAuthentication.configuration.jwt_secret, true, { algorithm: 'HS256' })
|
|
108
108
|
payload = decoded_token.first
|
|
109
109
|
|
|
110
110
|
# Check if token is expired (JWT gem handles this, but double-check)
|
|
@@ -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
|
|
@@ -55,7 +55,7 @@ module Recoverable
|
|
|
55
55
|
return false if new_password != new_password_confirmation
|
|
56
56
|
|
|
57
57
|
# Validate password length (configuration is a Range)
|
|
58
|
-
password_range =
|
|
58
|
+
password_range = PropelAuthentication.configuration.password_length
|
|
59
59
|
return false unless password_range.include?(new_password.length)
|
|
60
60
|
|
|
61
61
|
begin
|
|
@@ -77,7 +77,7 @@ module Recoverable
|
|
|
77
77
|
return nil if token.blank?
|
|
78
78
|
|
|
79
79
|
begin
|
|
80
|
-
decoded = JWT.decode(token,
|
|
80
|
+
decoded = JWT.decode(token, PropelAuthentication.configuration.jwt_secret, true, { algorithm: 'HS256' })
|
|
81
81
|
payload = decoded.first
|
|
82
82
|
|
|
83
83
|
# Validate token structure
|
|
@@ -102,7 +102,7 @@ module Recoverable
|
|
|
102
102
|
return nil if token.blank?
|
|
103
103
|
|
|
104
104
|
begin
|
|
105
|
-
decoded = JWT.decode(token,
|
|
105
|
+
decoded = JWT.decode(token, PropelAuthentication.configuration.jwt_secret, true, { algorithm: 'HS256' })
|
|
106
106
|
decoded.first['user_id']
|
|
107
107
|
rescue JWT::DecodeError
|
|
108
108
|
nil
|
data/lib/generators/{propel_auth → propel_authentication/templates}/core/configuration_methods.rb
RENAMED
|
@@ -1,33 +1,33 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
##
|
|
4
|
-
# Module providing configuration detection and validation for
|
|
4
|
+
# Module providing configuration detection and validation for PropelAuthentication generators
|
|
5
5
|
#
|
|
6
|
-
module
|
|
6
|
+
module PropelAuthentication
|
|
7
7
|
module ConfigurationMethods
|
|
8
8
|
|
|
9
|
-
# Shared class options across all
|
|
9
|
+
# Shared class options across all PropelAuthentication generators
|
|
10
10
|
def self.included(base)
|
|
11
11
|
base.class_option :namespace,
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
type: :string,
|
|
13
|
+
default: nil,
|
|
14
|
+
desc: "Authentication namespace (e.g., 'api', 'admin_api'). Use 'none' for no namespace. Defaults to PropelAuthentication configuration."
|
|
15
15
|
|
|
16
16
|
base.class_option :version,
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
type: :string,
|
|
18
|
+
default: nil,
|
|
19
|
+
desc: "Authentication version (e.g., 'v1', 'v2'). Use 'none' for no versioning. Defaults to PropelAuthentication configuration."
|
|
20
20
|
|
|
21
21
|
# Legacy support for existing --api-version option
|
|
22
22
|
base.class_option :api_version,
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
type: :string,
|
|
24
|
+
default: nil,
|
|
25
|
+
desc: "Legacy: Use --version instead. Authentication version (e.g., 'v1', 'v2')."
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
protected
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
# Initialize shared PropelAuthentication settings used across all generators
|
|
31
31
|
def initialize_propel_auth_settings
|
|
32
32
|
@auth_namespace = determine_auth_namespace
|
|
33
33
|
@auth_version = determine_auth_version
|
|
@@ -36,7 +36,7 @@ module PropelAuth
|
|
|
36
36
|
# Determine the authentication namespace to use
|
|
37
37
|
# Priority order:
|
|
38
38
|
# 1. Command line option (--namespace)
|
|
39
|
-
# 2.
|
|
39
|
+
# 2. PropelAuthentication configuration (if exists)
|
|
40
40
|
# 3. Default fallback (nil for clean URLs like /login)
|
|
41
41
|
def determine_auth_namespace
|
|
42
42
|
if options[:namespace] == 'none'
|
|
@@ -47,10 +47,10 @@ module PropelAuth
|
|
|
47
47
|
return options[:namespace]
|
|
48
48
|
end
|
|
49
49
|
|
|
50
|
-
# Try to read from
|
|
50
|
+
# Try to read from PropelAuthentication configuration
|
|
51
51
|
begin
|
|
52
|
-
if defined?(
|
|
53
|
-
config_namespace =
|
|
52
|
+
if defined?(PropelAuthentication) && PropelAuthentication.configuration.respond_to?(:namespace)
|
|
53
|
+
config_namespace = PropelAuthentication.configuration.namespace
|
|
54
54
|
return config_namespace if config_namespace.present? && config_namespace != 'none'
|
|
55
55
|
end
|
|
56
56
|
rescue => e
|
|
@@ -64,7 +64,7 @@ module PropelAuth
|
|
|
64
64
|
# Determine the authentication version to use
|
|
65
65
|
# Priority order:
|
|
66
66
|
# 1. Command line option (--version or legacy --api-version)
|
|
67
|
-
# 2.
|
|
67
|
+
# 2. PropelAuthentication configuration (if exists)
|
|
68
68
|
# 3. Default fallback (nil for clean URLs like /login)
|
|
69
69
|
def determine_auth_version
|
|
70
70
|
# Check for 'none' in either option
|
|
@@ -82,10 +82,10 @@ module PropelAuth
|
|
|
82
82
|
return options[:api_version]
|
|
83
83
|
end
|
|
84
84
|
|
|
85
|
-
# Try to read from
|
|
85
|
+
# Try to read from PropelAuthentication configuration
|
|
86
86
|
begin
|
|
87
|
-
if defined?(
|
|
88
|
-
config_version =
|
|
87
|
+
if defined?(PropelAuthentication) && PropelAuthentication.configuration.respond_to?(:version)
|
|
88
|
+
config_version = PropelAuthentication.configuration.version
|
|
89
89
|
return config_version if config_version.present? && config_version != 'none'
|
|
90
90
|
end
|
|
91
91
|
rescue => e
|
|
@@ -130,5 +130,22 @@ module PropelAuth
|
|
|
130
130
|
path_parts << 'auth'
|
|
131
131
|
path_parts.join('/')
|
|
132
132
|
end
|
|
133
|
+
|
|
134
|
+
# Helper methods for templates
|
|
135
|
+
def api_versioned?
|
|
136
|
+
@auth_version.present? && @auth_version != 'none'
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def api_only_app?
|
|
140
|
+
defined?(Rails.application.config.api_only) && Rails.application.config.api_only
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def controller_namespace
|
|
144
|
+
if api_versioned?
|
|
145
|
+
auth_controller_namespace
|
|
146
|
+
else
|
|
147
|
+
'Auth'
|
|
148
|
+
end
|
|
149
|
+
end
|
|
133
150
|
end
|
|
134
151
|
end
|
|
@@ -42,14 +42,14 @@ class Invitation < ApplicationRecord
|
|
|
42
42
|
invitation_hash: generate_invitation_hash
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
JWT.encode(payload,
|
|
45
|
+
JWT.encode(payload, PropelAuthentication.configuration.jwt_secret, 'HS256')
|
|
46
46
|
end
|
|
47
47
|
|
|
48
48
|
# Find invitation by JWT token with validation
|
|
49
49
|
def self.find_by_invitation_token(token)
|
|
50
50
|
begin
|
|
51
51
|
# Decode and validate the JWT token
|
|
52
|
-
payload = JWT.decode(token,
|
|
52
|
+
payload = JWT.decode(token, PropelAuthentication.configuration.jwt_secret, true, { algorithm: 'HS256' })[0]
|
|
53
53
|
|
|
54
54
|
# Verify this is an invitation token
|
|
55
55
|
return nil unless payload['type'] == 'invitation'
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
#
|
|
3
|
+
# PropelAuthentication Runtime and Configuration
|
|
4
4
|
# This file was generated by: rails generate propel_auth:install
|
|
5
5
|
#
|
|
6
|
-
# This contains ALL
|
|
6
|
+
# This contains ALL PropelAuthentication runtime functionality - no gem dependency required!
|
|
7
7
|
# This module provides JWT-based authentication for Rails applications
|
|
8
8
|
# with features like account lockout, password reset, and email confirmation.
|
|
9
9
|
|
|
10
10
|
require "rails"
|
|
11
11
|
|
|
12
|
-
module
|
|
12
|
+
module PropelAuthentication
|
|
13
13
|
class Error < StandardError; end
|
|
14
14
|
|
|
15
15
|
class << self
|
|
@@ -88,18 +88,10 @@ module PropelAuth
|
|
|
88
88
|
@version = nil # Default to no version for clean URLs like /login
|
|
89
89
|
end
|
|
90
90
|
end
|
|
91
|
-
|
|
92
|
-
class Engine < Rails::Engine
|
|
93
|
-
initializer 'propel_auth.load_generators' do |app|
|
|
94
|
-
config.generators do |g|
|
|
95
|
-
g.test_framework :minitest, fixture: true
|
|
96
|
-
end
|
|
97
|
-
end
|
|
98
|
-
end
|
|
99
91
|
end
|
|
100
92
|
|
|
101
|
-
# Configure
|
|
102
|
-
|
|
93
|
+
# Configure PropelAuthentication with secure defaults
|
|
94
|
+
PropelAuthentication.configure do |config|
|
|
103
95
|
# JWT Configuration for API authentication
|
|
104
96
|
config.jwt_secret = Rails.application.credentials.secret_key_base || ENV['SECRET_KEY_BASE'] || 'your-secret-key'
|
|
105
97
|
config.jwt_expiration = 24.hours
|
|
@@ -135,7 +127,7 @@ PropelAuth.configure do |config|
|
|
|
135
127
|
config.version = <%= @auth_version ? "'#{@auth_version}'" : 'nil' %>
|
|
136
128
|
end
|
|
137
129
|
|
|
138
|
-
#
|
|
130
|
+
# PropelAuthentication is now fully extracted to your application!
|
|
139
131
|
# - All runtime code is in this file
|
|
140
132
|
# - Generator logic can be extracted with: rails generate propel_auth:unpack
|
|
141
133
|
# - You can remove 'propel_auth' from your Gemfile after installation
|
|
@@ -21,13 +21,13 @@ class LockableTest < ActiveSupport::TestCase
|
|
|
21
21
|
|
|
22
22
|
test "should lock account after max failed attempts" do
|
|
23
23
|
# Simulate 10 failed attempts
|
|
24
|
-
|
|
24
|
+
PropelAuthentication.configuration.max_failed_attempts.times do
|
|
25
25
|
@user.increment_failed_attempts
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
-
assert @user.locked?, "User should be locked after #{
|
|
28
|
+
assert @user.locked?, "User should be locked after #{PropelAuthentication.configuration.max_failed_attempts} failed attempts"
|
|
29
29
|
assert @user.locked_at.present?, "Locked at timestamp should be set"
|
|
30
|
-
assert_equal
|
|
30
|
+
assert_equal PropelAuthentication.configuration.max_failed_attempts, @user.failed_login_attempts, "Failed attempts should equal max"
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
test "should reset failed attempts on successful login" do
|
|
@@ -54,10 +54,10 @@ class LockableTest < ActiveSupport::TestCase
|
|
|
54
54
|
|
|
55
55
|
test "should automatically unlock after lockout duration" do
|
|
56
56
|
# Lock the account with a past timestamp
|
|
57
|
-
past_time = (
|
|
57
|
+
past_time = (PropelAuthentication.configuration.lockout_duration + 1.minute).ago
|
|
58
58
|
@user.update!(
|
|
59
59
|
locked_at: past_time,
|
|
60
|
-
failed_login_attempts:
|
|
60
|
+
failed_login_attempts: PropelAuthentication.configuration.max_failed_attempts
|
|
61
61
|
)
|
|
62
62
|
|
|
63
63
|
# Check if automatically unlocked
|
|
@@ -69,7 +69,7 @@ class LockableTest < ActiveSupport::TestCase
|
|
|
69
69
|
recent_time = 5.minutes.ago
|
|
70
70
|
@user.update!(
|
|
71
71
|
locked_at: recent_time,
|
|
72
|
-
failed_login_attempts:
|
|
72
|
+
failed_login_attempts: PropelAuthentication.configuration.max_failed_attempts
|
|
73
73
|
)
|
|
74
74
|
|
|
75
75
|
# Should still be locked
|
|
@@ -130,15 +130,15 @@ class LockableTest < ActiveSupport::TestCase
|
|
|
130
130
|
|
|
131
131
|
test "should integrate with failed login attempt configuration" do
|
|
132
132
|
# Test with different configuration
|
|
133
|
-
original_max =
|
|
134
|
-
|
|
133
|
+
original_max = PropelAuthentication.configuration.max_failed_attempts
|
|
134
|
+
PropelAuthentication.configuration.max_failed_attempts = 3
|
|
135
135
|
|
|
136
136
|
# Should lock after 3 attempts with new config
|
|
137
137
|
3.times { @user.increment_failed_attempts }
|
|
138
138
|
assert @user.locked?, "Should respect updated configuration"
|
|
139
139
|
|
|
140
140
|
# Restore original config
|
|
141
|
-
|
|
141
|
+
PropelAuthentication.configuration.max_failed_attempts = original_max
|
|
142
142
|
end
|
|
143
143
|
|
|
144
144
|
# CRITICAL EDGE CASE TESTS - Missing from initial implementation
|
|
@@ -164,7 +164,7 @@ class LockableTest < ActiveSupport::TestCase
|
|
|
164
164
|
iat: Time.now.to_i,
|
|
165
165
|
exp: 1.second.ago.to_i
|
|
166
166
|
}
|
|
167
|
-
expired_token = JWT.encode(expired_payload,
|
|
167
|
+
expired_token = JWT.encode(expired_payload, PropelAuthentication.configuration.jwt_secret, 'HS256')
|
|
168
168
|
|
|
169
169
|
assert_not @user.unlock_with_token!(expired_token), "Should reject expired unlock token"
|
|
170
170
|
assert @user.locked?, "Should remain locked with expired token"
|
|
@@ -256,7 +256,7 @@ class LockableTest < ActiveSupport::TestCase
|
|
|
256
256
|
@user.lock_account!
|
|
257
257
|
|
|
258
258
|
unlock_token = @user.generate_unlock_token
|
|
259
|
-
decoded = JWT.decode(unlock_token,
|
|
259
|
+
decoded = JWT.decode(unlock_token, PropelAuthentication.configuration.jwt_secret, true, { algorithm: 'HS256' })
|
|
260
260
|
payload = decoded.first
|
|
261
261
|
|
|
262
262
|
assert_equal @user.id, payload['user_id'], "Token should contain correct user ID"
|
|
@@ -272,7 +272,7 @@ class LockableTest < ActiveSupport::TestCase
|
|
|
272
272
|
@user.lock_account!
|
|
273
273
|
|
|
274
274
|
unlock_token = @user.generate_unlock_token
|
|
275
|
-
decoded = JWT.decode(unlock_token,
|
|
275
|
+
decoded = JWT.decode(unlock_token, PropelAuthentication.configuration.jwt_secret, true, { algorithm: 'HS256' })
|
|
276
276
|
payload = decoded.first
|
|
277
277
|
|
|
278
278
|
token_lifetime = payload['exp'] - payload['iat']
|
|
@@ -48,7 +48,7 @@ class PropelAuthenticationTest < ActionDispatch::IntegrationTest
|
|
|
48
48
|
user_id: @user.id,
|
|
49
49
|
exp: 1.hour.ago.to_i
|
|
50
50
|
}
|
|
51
|
-
expired_token = JWT.encode(payload,
|
|
51
|
+
expired_token = JWT.encode(payload, PropelAuthentication.configuration.jwt_secret, 'HS256')
|
|
52
52
|
|
|
53
53
|
with_test_routes do
|
|
54
54
|
get '/test_auth', headers: { 'Authorization' => "Bearer #{expired_token}" }
|
|
@@ -17,7 +17,7 @@ class RecoverableTest < ActiveSupport::TestCase
|
|
|
17
17
|
assert_equal 3, reset_token.split('.').length, "JWT should have 3 parts separated by dots"
|
|
18
18
|
|
|
19
19
|
# VERIFY TOKEN CONTENT: Decode and validate payload
|
|
20
|
-
decoded = JWT.decode(reset_token,
|
|
20
|
+
decoded = JWT.decode(reset_token, PropelAuthentication.configuration.jwt_secret, true, { algorithm: 'HS256' })
|
|
21
21
|
payload = decoded.first
|
|
22
22
|
|
|
23
23
|
assert_equal @user.id, payload['user_id'], "Token should contain correct user ID"
|
|
@@ -50,7 +50,7 @@ class RecoverableTest < ActiveSupport::TestCase
|
|
|
50
50
|
assert @user.valid_password_reset_token?(reset_token), "Valid token should pass validation"
|
|
51
51
|
|
|
52
52
|
# VERIFY TOKEN BINDING: Token should be bound to specific user
|
|
53
|
-
decoded = JWT.decode(reset_token,
|
|
53
|
+
decoded = JWT.decode(reset_token, PropelAuthentication.configuration.jwt_secret, true, { algorithm: 'HS256' })
|
|
54
54
|
payload = decoded.first
|
|
55
55
|
assert_equal @user.id, payload['user_id'], "Token should be bound to correct user"
|
|
56
56
|
end
|
|
@@ -67,7 +67,7 @@ class RecoverableTest < ActiveSupport::TestCase
|
|
|
67
67
|
exp: 1.second.ago.to_i, # Already expired
|
|
68
68
|
password_hash: @user.password_digest[0..10]
|
|
69
69
|
}
|
|
70
|
-
expired_token = JWT.encode(expired_payload,
|
|
70
|
+
expired_token = JWT.encode(expired_payload, PropelAuthentication.configuration.jwt_secret, 'HS256')
|
|
71
71
|
|
|
72
72
|
# VERIFY EXPIRATION: Expired token should be rejected
|
|
73
73
|
assert_not @user.valid_password_reset_token?(expired_token), "Expired token should be rejected"
|
|
@@ -264,7 +264,7 @@ class RecoverableTest < ActiveSupport::TestCase
|
|
|
264
264
|
exp: 1.second.ago.to_i, # Already expired
|
|
265
265
|
password_hash: @user.password_digest[0..10]
|
|
266
266
|
}
|
|
267
|
-
expired_token = JWT.encode(expired_payload,
|
|
267
|
+
expired_token = JWT.encode(expired_payload, PropelAuthentication.configuration.jwt_secret, 'HS256')
|
|
268
268
|
|
|
269
269
|
# EXECUTE LOOKUP: Try to find user with expired token
|
|
270
270
|
found_user = User.find_user_by_password_reset_token(expired_token)
|
|
@@ -277,7 +277,7 @@ class RecoverableTest < ActiveSupport::TestCase
|
|
|
277
277
|
# SECURITY: Verify token expiration is reasonable (not too long)
|
|
278
278
|
|
|
279
279
|
reset_token = @user.generate_password_reset_token
|
|
280
|
-
decoded = JWT.decode(reset_token,
|
|
280
|
+
decoded = JWT.decode(reset_token, PropelAuthentication.configuration.jwt_secret, true, { algorithm: 'HS256' })
|
|
281
281
|
payload = decoded.first
|
|
282
282
|
|
|
283
283
|
token_lifetime = payload['exp'] - payload['iat']
|
|
@@ -299,7 +299,7 @@ class RecoverableTest < ActiveSupport::TestCase
|
|
|
299
299
|
exp: 1.hour.from_now.to_i,
|
|
300
300
|
password_hash: @user.password_digest[0..10]
|
|
301
301
|
}
|
|
302
|
-
unlock_token = JWT.encode(unlock_payload,
|
|
302
|
+
unlock_token = JWT.encode(unlock_payload, PropelAuthentication.configuration.jwt_secret, 'HS256')
|
|
303
303
|
|
|
304
304
|
# VERIFY TYPE VALIDATION: Unlock token should not work for password reset
|
|
305
305
|
assert_not @user.valid_password_reset_token?(unlock_token), "Should reject wrong token type"
|
|
@@ -313,7 +313,7 @@ class RecoverableTest < ActiveSupport::TestCase
|
|
|
313
313
|
# SECURITY: Verify token includes password hash binding
|
|
314
314
|
|
|
315
315
|
reset_token = @user.generate_password_reset_token
|
|
316
|
-
decoded = JWT.decode(reset_token,
|
|
316
|
+
decoded = JWT.decode(reset_token, PropelAuthentication.configuration.jwt_secret, true, { algorithm: 'HS256' })
|
|
317
317
|
payload = decoded.first
|
|
318
318
|
|
|
319
319
|
# VERIFY BINDING: Token should contain password hash fragment
|
|
@@ -47,7 +47,7 @@ class UserTest < ActiveSupport::TestCase
|
|
|
47
47
|
assert_equal 3, token.split('.').length, "JWT should have 3 parts"
|
|
48
48
|
|
|
49
49
|
# Verify token content
|
|
50
|
-
payload = JWT.decode(token,
|
|
50
|
+
payload = JWT.decode(token, PropelAuthentication.configuration.jwt_secret, true, { algorithm: 'HS256' })[0]
|
|
51
51
|
assert_equal user.id, payload['user_id'], "Token should include user ID"
|
|
52
52
|
assert_equal user.organization_id, payload['organization_id'], "Token should include organization ID"
|
|
53
53
|
assert payload['exp'].present?, "Token should include expiration"
|