propel_authentication 0.1.3 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +128 -0
  3. data/README.md +254 -116
  4. data/lib/generators/{propel_auth → propel_authentication}/install_generator.rb +152 -170
  5. data/lib/generators/propel_authentication/templates/application_mailer.rb +6 -0
  6. data/lib/generators/propel_authentication/templates/auth/passwords_controller.rb.tt +132 -0
  7. data/lib/generators/propel_authentication/templates/auth/signup_controller.rb.tt +242 -0
  8. data/lib/generators/{propel_auth/templates → propel_authentication/templates/auth}/tokens_controller.rb.tt +39 -22
  9. data/lib/generators/{propel_auth → propel_authentication}/templates/auth_mailer.rb +3 -1
  10. data/lib/generators/{propel_auth → propel_authentication}/templates/authenticatable.rb +10 -4
  11. data/lib/generators/{propel_auth → propel_authentication}/templates/concerns/confirmable.rb +3 -3
  12. data/lib/generators/{propel_auth → propel_authentication}/templates/concerns/lockable.rb +10 -8
  13. data/lib/generators/{propel_auth/templates/concerns/propel_authentication.rb → propel_authentication/templates/concerns/propel_authentication_concern.rb} +33 -3
  14. data/lib/generators/{propel_auth → propel_authentication}/templates/concerns/recoverable.rb +21 -11
  15. data/lib/generators/propel_authentication/templates/core/configuration_methods.rb +191 -0
  16. data/lib/generators/propel_authentication/templates/db/seeds.rb +75 -0
  17. data/lib/generators/propel_authentication/templates/doc/signup_flow.md +315 -0
  18. data/lib/generators/propel_authentication/templates/models/agency.rb.tt +13 -0
  19. data/lib/generators/propel_authentication/templates/models/agent.rb.tt +13 -0
  20. data/lib/generators/{propel_auth/templates/invitation.rb → propel_authentication/templates/models/invitation.rb.tt} +8 -2
  21. data/lib/generators/propel_authentication/templates/models/organization.rb.tt +12 -0
  22. data/lib/generators/{propel_auth/templates/user.rb → propel_authentication/templates/models/user.rb.tt} +5 -0
  23. data/lib/generators/propel_authentication/templates/propel_authentication.rb.tt +218 -0
  24. data/lib/generators/propel_authentication/templates/routes/auth_routes.rb.tt +55 -0
  25. data/lib/generators/{propel_auth → propel_authentication}/templates/services/auth_notification_service.rb +3 -3
  26. data/lib/generators/{propel_auth → propel_authentication}/templates/test/concerns/confirmable_test.rb.tt +34 -10
  27. data/lib/generators/{propel_auth → propel_authentication}/templates/test/concerns/lockable_test.rb.tt +12 -12
  28. data/lib/generators/{propel_auth → propel_authentication}/templates/test/concerns/propel_authentication_test.rb.tt +2 -2
  29. data/lib/generators/{propel_auth → propel_authentication}/templates/test/concerns/recoverable_test.rb.tt +11 -11
  30. data/lib/generators/{propel_auth → propel_authentication}/templates/test/controllers/auth/lockable_integration_test.rb.tt +18 -15
  31. data/lib/generators/{propel_auth → propel_authentication}/templates/test/controllers/auth/password_reset_integration_test.rb.tt +38 -40
  32. data/lib/generators/propel_authentication/templates/test/controllers/auth/signup_controller_test.rb.tt +201 -0
  33. data/lib/generators/{propel_auth → propel_authentication}/templates/test/controllers/auth/tokens_controller_test.rb.tt +33 -25
  34. data/lib/generators/{propel_auth → propel_authentication}/templates/test/mailers/auth_mailer_test.rb.tt +51 -36
  35. data/lib/generators/{propel_auth → propel_authentication}/templates/user_test.rb.tt +1 -1
  36. data/lib/generators/{propel_auth → propel_authentication}/templates/views/auth_mailer/email_confirmation.html.erb +2 -2
  37. data/lib/generators/{propel_auth → propel_authentication}/templates/views/auth_mailer/email_confirmation.text.erb +1 -1
  38. data/lib/generators/{propel_auth → propel_authentication}/test/generators/authentication/install_generator_test.rb +4 -4
  39. data/lib/generators/{propel_auth → propel_authentication}/test/generators/authentication/uninstall_generator_test.rb +1 -1
  40. data/lib/generators/{propel_auth → propel_authentication}/test/integration/generator_integration_test.rb +1 -1
  41. data/lib/generators/{propel_auth → propel_authentication}/test/integration/multi_version_generator_test.rb +13 -12
  42. data/lib/generators/{propel_auth → propel_authentication}/unpack_generator.rb +55 -38
  43. data/lib/propel_authentication.rb +3 -0
  44. metadata +101 -98
  45. data/lib/generators/propel_auth/core/configuration_methods.rb +0 -134
  46. data/lib/generators/propel_auth/pack_generator.rb +0 -277
  47. data/lib/generators/propel_auth/templates/agency.rb +0 -7
  48. data/lib/generators/propel_auth/templates/agent.rb +0 -7
  49. data/lib/generators/propel_auth/templates/auth/base_passwords_controller.rb.tt +0 -99
  50. data/lib/generators/propel_auth/templates/auth/base_tokens_controller.rb.tt +0 -90
  51. data/lib/generators/propel_auth/templates/auth/passwords_controller.rb.tt +0 -126
  52. data/lib/generators/propel_auth/templates/db/seeds.rb +0 -29
  53. data/lib/generators/propel_auth/templates/organization.rb +0 -7
  54. data/lib/generators/propel_auth/templates/propel_auth.rb.tt +0 -141
  55. data/lib/propel_auth.rb +0 -3
  56. /data/lib/generators/{propel_auth → propel_authentication}/templates/concerns/rack_session_disable.rb +0 -0
  57. /data/lib/generators/{propel_auth → propel_authentication}/templates/config/environments/development_email.rb +0 -0
  58. /data/lib/generators/{propel_auth → propel_authentication}/templates/db/migrate/create_agencies.rb +0 -0
  59. /data/lib/generators/{propel_auth → propel_authentication}/templates/db/migrate/create_agents.rb +0 -0
  60. /data/lib/generators/{propel_auth → propel_authentication}/templates/db/migrate/create_invitations.rb +0 -0
  61. /data/lib/generators/{propel_auth → propel_authentication}/templates/db/migrate/create_organizations.rb +0 -0
  62. /data/lib/generators/{propel_auth → propel_authentication}/templates/db/migrate/create_users.rb +0 -0
  63. /data/lib/generators/{propel_auth → propel_authentication}/templates/test/mailers/previews/auth_mailer_preview.rb +0 -0
  64. /data/lib/generators/{propel_auth → propel_authentication}/templates/views/auth_mailer/account_unlock.html.erb +0 -0
  65. /data/lib/generators/{propel_auth → propel_authentication}/templates/views/auth_mailer/account_unlock.text.erb +0 -0
  66. /data/lib/generators/{propel_auth → propel_authentication}/templates/views/auth_mailer/password_reset.html.erb +0 -0
  67. /data/lib/generators/{propel_auth → propel_authentication}/templates/views/auth_mailer/password_reset.text.erb +0 -0
  68. /data/lib/generators/{propel_auth → propel_authentication}/templates/views/auth_mailer/user_invitation.html.erb +0 -0
  69. /data/lib/generators/{propel_auth → propel_authentication}/templates/views/auth_mailer/user_invitation.text.erb +0 -0
  70. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/Dockerfile +0 -0
  71. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/Gemfile +0 -0
  72. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/README.md +0 -0
  73. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/Rakefile +0 -0
  74. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/app/assets/stylesheets/application.css +0 -0
  75. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/app/controllers/application_controller.rb +0 -0
  76. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/app/helpers/application_helper.rb +0 -0
  77. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/app/jobs/application_job.rb +0 -0
  78. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/app/mailers/application_mailer.rb +0 -0
  79. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/app/models/application_record.rb +0 -0
  80. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/app/views/layouts/application.html.erb +0 -0
  81. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/app/views/layouts/mailer.html.erb +0 -0
  82. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/app/views/layouts/mailer.text.erb +0 -0
  83. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/app/views/pwa/manifest.json.erb +0 -0
  84. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/app/views/pwa/service-worker.js +0 -0
  85. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/bin/brakeman +0 -0
  86. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/bin/dev +0 -0
  87. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/bin/docker-entrypoint +0 -0
  88. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/bin/rails +0 -0
  89. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/bin/rake +0 -0
  90. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/bin/rubocop +0 -0
  91. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/bin/setup +0 -0
  92. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/bin/thrust +0 -0
  93. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/application.rb +0 -0
  94. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/boot.rb +0 -0
  95. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/cable.yml +0 -0
  96. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/credentials.yml.enc +0 -0
  97. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/database.yml +0 -0
  98. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/environment.rb +0 -0
  99. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/environments/development.rb +0 -0
  100. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/environments/production.rb +0 -0
  101. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/environments/test.rb +0 -0
  102. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/initializers/assets.rb +0 -0
  103. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/initializers/content_security_policy.rb +0 -0
  104. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/initializers/filter_parameter_logging.rb +0 -0
  105. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/initializers/inflections.rb +0 -0
  106. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/locales/en.yml +0 -0
  107. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/master.key +0 -0
  108. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/puma.rb +0 -0
  109. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/routes.rb +0 -0
  110. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/storage.yml +0 -0
  111. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config.ru +0 -0
  112. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/db/schema.rb +0 -0
  113. /data/lib/generators/{propel_auth → propel_authentication}/test/generators/authentication/controllers/tokens_controller_test.rb +0 -0
@@ -0,0 +1,218 @@
1
+ # frozen_string_literal: true
2
+
3
+ # PropelAuthentication Runtime and Configuration
4
+ # This file was generated by: rails generate propel_auth:install
5
+ #
6
+ # This contains ALL PropelAuthentication runtime functionality - no gem dependency required!
7
+ # This module provides JWT-based authentication for Rails applications
8
+ # with features like account lockout, password reset, and email confirmation.
9
+
10
+ require "rails"
11
+
12
+ module PropelAuthentication
13
+ class Error < StandardError; end
14
+
15
+ class << self
16
+ def configuration
17
+ @configuration ||= Configuration.new
18
+ end
19
+
20
+ def configure
21
+ yield(configuration)
22
+ end
23
+
24
+ def reset_configuration!
25
+ @configuration = Configuration.new
26
+ end
27
+ end
28
+
29
+ class Configuration
30
+ attr_accessor :jwt_secret,
31
+ :jwt_expiration,
32
+ :jwt_algorithm,
33
+ :password_length,
34
+ :allow_registration,
35
+ :require_email_confirmation,
36
+ :confirmation_period,
37
+ :confirmation_resend_interval,
38
+ :max_failed_attempts,
39
+ :lockout_duration,
40
+ :password_reset_expiration,
41
+ :password_reset_rate_limit,
42
+ :frontend_url,
43
+ :email_from_address,
44
+ :support_email,
45
+ :enable_email_notifications,
46
+ :enable_sms_notifications,
47
+ :agency_tenancy,
48
+ :require_organization_id,
49
+ :require_user_id,
50
+ :namespace,
51
+ :version,
52
+ :auth_scope
53
+
54
+ def initialize
55
+ # JWT Configuration defaults
56
+ @jwt_secret = nil # Will be set in configuration block
57
+ @jwt_expiration = 24.hours
58
+ @jwt_algorithm = 'HS256'
59
+
60
+ # Password requirements
61
+ @password_length = 8..128
62
+
63
+ # User registration settings
64
+ @allow_registration = true
65
+
66
+ # Email confirmation settings
67
+ @require_email_confirmation = false
68
+ @confirmation_period = 24.hours # How long confirmation tokens are valid
69
+ @confirmation_resend_interval = 1.minute # Minimum time between resend attempts
70
+
71
+ # Account lockout settings
72
+ @max_failed_attempts = 10
73
+ @lockout_duration = 30.minutes
74
+
75
+ # Password reset settings
76
+ @password_reset_expiration = 15.minutes
77
+ @password_reset_rate_limit = 1.minute
78
+
79
+ # Frontend URL for email links (configure for your frontend application)
80
+ # Priority: Rails credentials -> ENV variables -> environment-specific fallbacks
81
+ @frontend_url = Rails.application.credentials.frontend_url ||
82
+ ENV['FRONTEND_URL'] ||
83
+ case Rails.env
84
+ when 'development', 'test'
85
+ 'http://localhost:3000'
86
+ when 'staging'
87
+ 'https://staging.yourapp.com' # Replace with your staging URL
88
+ when 'production'
89
+ 'https://yourapp.com' # Replace with your production URL
90
+ else
91
+ 'http://localhost:3000' # Safe fallback for any other environment
92
+ end
93
+
94
+ # Email configuration
95
+ # Priority: Rails credentials -> ENV variables -> sensible defaults
96
+ @email_from_address = Rails.application.credentials.email_from_address ||
97
+ ENV['EMAIL_FROM_ADDRESS'] ||
98
+ "noreply@#{Rails.application.class.module_parent.name.downcase}.com"
99
+
100
+ @support_email = Rails.application.credentials.support_email ||
101
+ ENV['SUPPORT_EMAIL'] ||
102
+ "support@#{Rails.application.class.module_parent.name.downcase}.com"
103
+
104
+ # Notification preferences
105
+ @enable_email_notifications = true
106
+ @enable_sms_notifications = false # Requires SMS provider configuration
107
+
108
+ # Tenancy configuration
109
+ # Controls whether multi-agency tenancy is enabled within organizations
110
+ @agency_tenancy = true # Enable agency-level tenancy by default
111
+
112
+ # API tenancy requirements (for PropelApi integration)
113
+ # Controls whether APIs require explicit tenancy context in requests
114
+ @require_organization_id = false # Auto-assign organization when missing (developer-friendly default)
115
+ @require_user_id = false # Auto-assign current user when missing (common workflow default)
116
+
117
+ # API namespace and versioning configuration
118
+ # Default to no namespace/version/auth_scope for clean URLs like /login, /me, /signup
119
+ @namespace = nil
120
+ @version = nil
121
+ @auth_scope = nil
122
+ end
123
+ end
124
+ end
125
+
126
+ # Configure PropelAuthentication with secure defaults
127
+ PropelAuthentication.configure do |config|
128
+ # JWT Configuration for API authentication
129
+ # Priority: Rails credentials -> ENV variables -> fallback (development/test only)
130
+ config.jwt_secret = Rails.application.credentials.jwt_secret ||
131
+ ENV['JWT_SECRET'] ||
132
+ Rails.application.credentials.secret_key_base ||
133
+ ENV['SECRET_KEY_BASE'] ||
134
+ (Rails.env.development? || Rails.env.test? ? 'development-secret-key' :
135
+ raise('JWT_SECRET must be set in production'))
136
+ config.jwt_expiration = 24.hours
137
+
138
+ # Password requirements
139
+ config.password_length = 8..128
140
+
141
+ # User registration settings
142
+ config.allow_registration = true
143
+
144
+ # Multi-tenancy settings
145
+ # Controls whether agency-level tenancy is enabled within organizations
146
+ # When true: Organization -> Agency -> Resources structure
147
+ # When false: Organization -> Resources structure (simpler)
148
+ config.agency_tenancy = true
149
+
150
+ # API tenancy requirements (for PropelApi integration)
151
+ # Controls whether clients must provide explicit tenancy context in API requests
152
+
153
+ # Require explicit organization_id in API requests?
154
+ # false (recommended): Auto-assigns current user's organization_id when missing
155
+ # true (strict): Returns 422 error if organization_id not provided by client
156
+ config.require_organization_id = false
157
+
158
+ # Require explicit user_id in API requests?
159
+ # false (recommended): Auto-assigns current user as creator when missing
160
+ # true (admin mode): Returns 422 error if user_id not provided (for delegation scenarios)
161
+ config.require_user_id = false
162
+
163
+ # Email confirmation (for future use)
164
+ config.require_email_confirmation = false
165
+
166
+ # Account lockout settings
167
+ config.max_failed_attempts = 10
168
+ config.lockout_duration = 30.minutes
169
+
170
+ # Password reset settings
171
+ config.password_reset_expiration = 15.minutes
172
+ config.password_reset_rate_limit = 1.minute
173
+
174
+ # Environment-specific settings
175
+ if Rails.env.development?
176
+ # Development-specific settings
177
+ config.jwt_expiration = 24.hours
178
+ elsif Rails.env.production?
179
+ # Production-specific settings - shorter token expiration for security
180
+ config.jwt_expiration = 2.hours
181
+ end
182
+
183
+ # URL structure configuration (set by generator based on your choices)
184
+ # - Default: Clean URLs like /login, /me, /signup (no additional namespacing)
185
+ # - With PropelApi: Automatically adopts API namespace for consistency
186
+ # - Override: Set explicit values to customize URL structure
187
+ #
188
+ # Current configuration:
189
+ config.namespace = <%= @auth_namespace ? "'#{@auth_namespace}'" : 'nil' %>
190
+ config.version = <%= @auth_version ? "'#{@auth_version}'" : 'nil' %>
191
+ config.auth_scope = <%= @auth_scope ? "'#{@auth_scope}'" : 'nil' %>
192
+
193
+ # Examples for customization:
194
+ # For clean URLs (default):
195
+ # config.namespace = nil
196
+ # config.version = nil
197
+ # config.auth_scope = nil # → /login, /me → TokensController
198
+ #
199
+ # For organized URLs with auth scope:
200
+ # config.namespace = nil
201
+ # config.version = nil
202
+ # config.auth_scope = 'auth' # → /auth/login, /auth/me → Auth::TokensController
203
+ #
204
+ # For API-style URLs:
205
+ # config.namespace = 'api'
206
+ # config.version = 'v1'
207
+ # config.auth_scope = nil # → /api/v1/login, /api/v1/me → Api::V1::TokensController
208
+ #
209
+ # For API-style URLs with auth scope:
210
+ # config.namespace = 'api'
211
+ # config.version = 'v1'
212
+ # config.auth_scope = 'auth' # → /api/v1/auth/login → Api::V1::Auth::TokensController
213
+ end
214
+
215
+ # PropelAuthentication is now fully extracted to your application!
216
+ # - All runtime code is in this file
217
+ # - Generator logic can be extracted with: rails generate propel_auth:unpack
218
+ # - You can remove 'propel_auth' from your Gemfile after installation
@@ -0,0 +1,55 @@
1
+ # JWT Authentication routes for <%= auth_route_prefix %>
2
+ <%- if @auth_namespace.present? || @auth_version.present? || @auth_scope.present? -%>
3
+ <%- namespace_parts = [] -%>
4
+ <%- namespace_parts << @auth_namespace if @auth_namespace.present? -%>
5
+ <%- namespace_parts << @auth_version if @auth_version.present? -%>
6
+ <%- namespace_parts.each_with_index do |part, index| -%>
7
+ <%= " " * index %>namespace :<%= part %> do
8
+ <%- end -%>
9
+ <%- if @auth_scope.present? -%>
10
+ <%= " " * namespace_parts.length %>namespace :<%= @auth_scope %> do
11
+ <%= " " * (namespace_parts.length + 1) %>post 'signup', to: 'signup#create', as: :signup
12
+ <%= " " * (namespace_parts.length + 1) %>post 'login', to: 'tokens#create', as: :login
13
+ <%= " " * (namespace_parts.length + 1) %>get 'me', to: 'tokens#show', as: :me
14
+ <%= " " * (namespace_parts.length + 1) %>delete 'logout', to: 'tokens#destroy', as: :logout
15
+ <%= " " * (namespace_parts.length + 1) %>post 'unlock', to: 'tokens#unlock', as: :unlock
16
+ <%= " " * (namespace_parts.length + 1) %>post 'reset', to: 'passwords#create', as: :reset
17
+ <%= " " * (namespace_parts.length + 1) %>get 'reset', to: 'passwords#show', as: :verify_reset
18
+ <%= " " * (namespace_parts.length + 1) %>patch 'reset', to: 'passwords#update', as: :update_reset
19
+ <%= " " * namespace_parts.length %>end
20
+ <%- else -%>
21
+ <%= " " * namespace_parts.length %>post 'signup', to: 'signup#create', as: :signup
22
+ <%= " " * namespace_parts.length %>post 'login', to: 'tokens#create', as: :login
23
+ <%= " " * namespace_parts.length %>get 'me', to: 'tokens#show', as: :me
24
+ <%= " " * namespace_parts.length %>delete 'logout', to: 'tokens#destroy', as: :logout
25
+ <%= " " * namespace_parts.length %>post 'unlock', to: 'tokens#unlock', as: :unlock
26
+ <%= " " * namespace_parts.length %>post 'reset', to: 'passwords#create', as: :reset
27
+ <%= " " * namespace_parts.length %>get 'reset', to: 'passwords#show', as: :verify_reset
28
+ <%= " " * namespace_parts.length %>patch 'reset', to: 'passwords#update', as: :update_reset
29
+ <%- end -%>
30
+ <%- namespace_parts.length.times do |index| -%>
31
+ <%= " " * (namespace_parts.length - 1 - index) %>end
32
+ <%- end -%>
33
+ <%- else -%>
34
+ <%- if @auth_scope.present? -%>
35
+ namespace :<%= @auth_scope %> do
36
+ post 'signup', to: 'signup#create', as: :signup
37
+ post 'login', to: 'tokens#create', as: :login
38
+ get 'me', to: 'tokens#show', as: :me
39
+ delete 'logout', to: 'tokens#destroy', as: :logout
40
+ post 'unlock', to: 'tokens#unlock', as: :unlock
41
+ post 'reset', to: 'passwords#create', as: :reset
42
+ get 'reset', to: 'passwords#show', as: :verify_reset
43
+ patch 'reset', to: 'passwords#update', as: :update_reset
44
+ end
45
+ <%- else -%>
46
+ post 'signup', to: 'signup#create', as: :signup
47
+ post 'login', to: 'tokens#create', as: :login
48
+ get 'me', to: 'tokens#show', as: :me
49
+ delete 'logout', to: 'tokens#destroy', as: :logout
50
+ post 'unlock', to: 'tokens#unlock', as: :unlock
51
+ post 'reset', to: 'passwords#create', as: :reset
52
+ get 'reset', to: 'passwords#show', as: :verify_reset
53
+ patch 'reset', to: 'passwords#update', as: :update_reset
54
+ <%- end -%>
55
+ <%- end -%>
@@ -64,17 +64,17 @@ class AuthNotificationService
64
64
  private
65
65
 
66
66
  def build_password_reset_url(token)
67
- base_url = PropelAuth.configuration.frontend_url || default_frontend_url
67
+ base_url = PropelAuthentication.configuration.frontend_url || default_frontend_url
68
68
  "#{base_url}/reset-password?token=#{token}"
69
69
  end
70
70
 
71
71
  def build_account_unlock_url(token)
72
- base_url = PropelAuth.configuration.frontend_url || default_frontend_url
72
+ base_url = PropelAuthentication.configuration.frontend_url || default_frontend_url
73
73
  "#{base_url}/unlock-account?token=#{token}"
74
74
  end
75
75
 
76
76
  def build_invitation_url(token)
77
- base_url = PropelAuth.configuration.frontend_url || default_frontend_url
77
+ base_url = PropelAuthentication.configuration.frontend_url || default_frontend_url
78
78
  "#{base_url}/accept-invitation?token=#{token}"
79
79
  end
80
80
 
@@ -10,8 +10,8 @@ class ConfirmableTest < ActiveSupport::TestCase
10
10
 
11
11
  test "should generate confirmation token on user creation" do
12
12
  user = User.new(
13
- email_address: 'jane@example.com',
14
- username: 'jane_doe',
13
+ email_address: 'test_token@example.com',
14
+ username: 'test_token_user',
15
15
  password: 'securepassword123',
16
16
  password_confirmation: 'securepassword123',
17
17
  organization: @organization
@@ -23,7 +23,7 @@ class ConfirmableTest < ActiveSupport::TestCase
23
23
  user.save!
24
24
 
25
25
  assert_not_nil user.confirmation_token, "Confirmation token should be generated on save"
26
- assert_not_nil user.confirmation_sent_at, "Confirmation sent at should be set on save"
26
+ assert_not_nil user.confirmation_sent_at, "Confirmation sent at should be set after save due to after_create callback"
27
27
  assert user.confirmation_token.length >= 32, "Confirmation token should be at least 32 characters"
28
28
  end
29
29
 
@@ -114,8 +114,8 @@ class ConfirmableTest < ActiveSupport::TestCase
114
114
  test "should send confirmation email on user creation" do
115
115
  assert_emails 1 do
116
116
  User.create!(
117
- email_address: 'newuser@example.com',
118
- username: 'newuser',
117
+ email_address: 'test_email_creation@example.com',
118
+ username: 'test_email_creation_user',
119
119
  password: 'securepassword123',
120
120
  password_confirmation: 'securepassword123',
121
121
  organization: @organization
@@ -123,9 +123,21 @@ class ConfirmableTest < ActiveSupport::TestCase
123
123
  end
124
124
 
125
125
  email = ActionMailer::Base.deliveries.last
126
- assert_equal 'newuser@example.com', email.to.first, "Email should be sent to user's email address"
126
+ assert_equal 'test_email_creation@example.com', email.to.first, "Email should be sent to user's email address"
127
127
  assert_match(/confirm/i, email.subject, "Email subject should mention confirmation")
128
- assert_match(/confirm/i, email.body.to_s, "Email body should contain confirmation instructions")
128
+
129
+ # Test multipart email content properly
130
+ if email.multipart?
131
+ text_part = email.body.parts.find { |part| part.content_type.include?('text/plain') }
132
+ html_part = email.body.parts.find { |part| part.content_type.include?('text/html') }
133
+
134
+ assert_not_nil text_part, "Email should have text part"
135
+ assert_not_nil html_part, "Email should have HTML part"
136
+ assert_match(/confirm/i, text_part.body.to_s, "Text part should contain confirmation instructions")
137
+ assert_match(/confirm/i, html_part.body.to_s, "HTML part should contain confirmation instructions")
138
+ else
139
+ assert_match(/confirm/i, email.body.to_s, "Email body should contain confirmation instructions")
140
+ end
129
141
  end
130
142
 
131
143
  test "should send confirmation email when resending instructions" do
@@ -137,7 +149,18 @@ class ConfirmableTest < ActiveSupport::TestCase
137
149
 
138
150
  email = ActionMailer::Base.deliveries.last
139
151
  assert_equal @user.email_address, email.to.first, "Email should be sent to user's email address"
140
- assert_match(@user.confirmation_token, email.body.to_s, "Email should contain confirmation token")
152
+
153
+ # Test multipart email content properly for confirmation token
154
+ if email.multipart?
155
+ text_part = email.body.parts.find { |part| part.content_type.include?('text/plain') }
156
+ html_part = email.body.parts.find { |part| part.content_type.include?('text/html') }
157
+
158
+ assert_not_nil text_part, "Email should have text part"
159
+ assert_match(@user.confirmation_token, text_part.body.to_s, "Text part should contain confirmation token")
160
+ assert_match(@user.confirmation_token, html_part.body.to_s, "HTML part should contain confirmation token")
161
+ else
162
+ assert_match(@user.confirmation_token, email.body.to_s, "Email should contain confirmation token")
163
+ end
141
164
  end
142
165
 
143
166
  test "should not send confirmation email to already confirmed users" do
@@ -177,12 +200,13 @@ class ConfirmableTest < ActiveSupport::TestCase
177
200
  original_email = @user.email_address
178
201
 
179
202
  assert_emails 1 do
180
- @user.update!(email_address: 'newemail@example.com')
203
+ @user.update!(email_address: 'test_email_change@example.com')
181
204
  end
182
205
 
183
206
  @user.reload
184
207
  assert_not @user.confirmed?, "Should require reconfirmation after email change"
185
- assert_equal 'newemail@example.com', @user.email_address, "Email should be updated"
208
+ assert_equal original_email, @user.email_address, "Email should remain original until confirmed"
209
+ assert_equal 'test_email_change@example.com', @user.unconfirmed_email_address, "New email should be stored for confirmation"
186
210
  assert_not_nil @user.confirmation_token, "Should generate new confirmation token"
187
211
  end
188
212
 
@@ -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
- PropelAuth.configuration.max_failed_attempts.times do
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 #{PropelAuth.configuration.max_failed_attempts} failed attempts"
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 PropelAuth.configuration.max_failed_attempts, @user.failed_login_attempts, "Failed attempts should equal max"
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 = (PropelAuth.configuration.lockout_duration + 1.minute).ago
57
+ past_time = (PropelAuthentication.configuration.lockout_duration + 1.minute).ago
58
58
  @user.update!(
59
59
  locked_at: past_time,
60
- failed_login_attempts: PropelAuth.configuration.max_failed_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: PropelAuth.configuration.max_failed_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 = PropelAuth.configuration.max_failed_attempts
134
- PropelAuth.configuration.max_failed_attempts = 3
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
- PropelAuth.configuration.max_failed_attempts = original_max
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, PropelAuth.configuration.jwt_secret, 'HS256')
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, PropelAuth.configuration.jwt_secret, true, { algorithm: 'HS256' })
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, PropelAuth.configuration.jwt_secret, true, { algorithm: 'HS256' })
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']
@@ -2,7 +2,7 @@ require 'test_helper'
2
2
 
3
3
  # A dummy controller to test the concern
4
4
  class PropelAuthenticationTestController < ActionController::Base
5
- include PropelAuthentication
5
+ include PropelAuthenticationConcern
6
6
  before_action :authenticate_user
7
7
 
8
8
  def index
@@ -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, PropelAuth.configuration.jwt_secret, 'HS256')
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}" }
@@ -2,7 +2,7 @@ require 'test_helper'
2
2
 
3
3
  class RecoverableTest < ActiveSupport::TestCase
4
4
  def setup
5
- @user = users(:john_user)
5
+ @user = users(:confirmed_user)
6
6
  @organization = @user.organization
7
7
  end
8
8
 
@@ -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, PropelAuth.configuration.jwt_secret, true, { algorithm: 'HS256' })
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, PropelAuth.configuration.jwt_secret, true, { algorithm: 'HS256' })
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, PropelAuth.configuration.jwt_secret, 'HS256')
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"
@@ -234,7 +234,7 @@ class RecoverableTest < ActiveSupport::TestCase
234
234
  reset_token = @user.generate_password_reset_token
235
235
 
236
236
  # EXECUTE LOOKUP: Find user by reset token
237
- found_user = User.find_user_by_password_reset_token(reset_token)
237
+ found_user = User.find_by_jwt_password_reset_token(reset_token)
238
238
 
239
239
  # VERIFY LOOKUP: Should find correct user
240
240
  assert_equal @user.id, found_user.id, "Should find user by valid reset token"
@@ -247,7 +247,7 @@ class RecoverableTest < ActiveSupport::TestCase
247
247
  invalid_token = "invalid.jwt.token"
248
248
 
249
249
  # EXECUTE LOOKUP: Try to find user with invalid token
250
- found_user = User.find_user_by_password_reset_token(invalid_token)
250
+ found_user = User.find_by_jwt_password_reset_token(invalid_token)
251
251
 
252
252
  # VERIFY REJECTION: Should return nil
253
253
  assert_nil found_user, "Should return nil for invalid token"
@@ -264,10 +264,10 @@ 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, PropelAuth.configuration.jwt_secret, 'HS256')
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
- found_user = User.find_user_by_password_reset_token(expired_token)
270
+ found_user = User.find_by_jwt_password_reset_token(expired_token)
271
271
 
272
272
  # VERIFY EXPIRATION: Should return nil
273
273
  assert_nil found_user, "Should return nil for 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, PropelAuth.configuration.jwt_secret, true, { algorithm: 'HS256' })
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, PropelAuth.configuration.jwt_secret, 'HS256')
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, PropelAuth.configuration.jwt_secret, true, { algorithm: 'HS256' })
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