propel_authentication 0.1.3 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +128 -0
- data/README.md +254 -116
- data/lib/generators/{propel_auth → propel_authentication}/install_generator.rb +152 -170
- data/lib/generators/propel_authentication/templates/application_mailer.rb +6 -0
- data/lib/generators/propel_authentication/templates/auth/passwords_controller.rb.tt +132 -0
- data/lib/generators/propel_authentication/templates/auth/signup_controller.rb.tt +242 -0
- data/lib/generators/{propel_auth/templates → propel_authentication/templates/auth}/tokens_controller.rb.tt +39 -22
- data/lib/generators/{propel_auth → propel_authentication}/templates/auth_mailer.rb +3 -1
- data/lib/generators/{propel_auth → propel_authentication}/templates/authenticatable.rb +10 -4
- data/lib/generators/{propel_auth → propel_authentication}/templates/concerns/confirmable.rb +3 -3
- data/lib/generators/{propel_auth → propel_authentication}/templates/concerns/lockable.rb +10 -8
- data/lib/generators/{propel_auth/templates/concerns/propel_authentication.rb → propel_authentication/templates/concerns/propel_authentication_concern.rb} +33 -3
- data/lib/generators/{propel_auth → propel_authentication}/templates/concerns/recoverable.rb +21 -11
- data/lib/generators/propel_authentication/templates/core/configuration_methods.rb +191 -0
- data/lib/generators/propel_authentication/templates/db/seeds.rb +75 -0
- data/lib/generators/propel_authentication/templates/doc/signup_flow.md +315 -0
- data/lib/generators/propel_authentication/templates/models/agency.rb.tt +13 -0
- data/lib/generators/propel_authentication/templates/models/agent.rb.tt +13 -0
- data/lib/generators/{propel_auth/templates/invitation.rb → propel_authentication/templates/models/invitation.rb.tt} +8 -2
- data/lib/generators/propel_authentication/templates/models/organization.rb.tt +12 -0
- data/lib/generators/{propel_auth/templates/user.rb → propel_authentication/templates/models/user.rb.tt} +5 -0
- data/lib/generators/propel_authentication/templates/propel_authentication.rb.tt +218 -0
- data/lib/generators/propel_authentication/templates/routes/auth_routes.rb.tt +55 -0
- data/lib/generators/{propel_auth → propel_authentication}/templates/services/auth_notification_service.rb +3 -3
- data/lib/generators/{propel_auth → propel_authentication}/templates/test/concerns/confirmable_test.rb.tt +34 -10
- data/lib/generators/{propel_auth → propel_authentication}/templates/test/concerns/lockable_test.rb.tt +12 -12
- data/lib/generators/{propel_auth → propel_authentication}/templates/test/concerns/propel_authentication_test.rb.tt +2 -2
- data/lib/generators/{propel_auth → propel_authentication}/templates/test/concerns/recoverable_test.rb.tt +11 -11
- data/lib/generators/{propel_auth → propel_authentication}/templates/test/controllers/auth/lockable_integration_test.rb.tt +18 -15
- data/lib/generators/{propel_auth → propel_authentication}/templates/test/controllers/auth/password_reset_integration_test.rb.tt +38 -40
- data/lib/generators/propel_authentication/templates/test/controllers/auth/signup_controller_test.rb.tt +201 -0
- data/lib/generators/{propel_auth → propel_authentication}/templates/test/controllers/auth/tokens_controller_test.rb.tt +33 -25
- data/lib/generators/{propel_auth → propel_authentication}/templates/test/mailers/auth_mailer_test.rb.tt +51 -36
- data/lib/generators/{propel_auth → propel_authentication}/templates/user_test.rb.tt +1 -1
- data/lib/generators/{propel_auth → propel_authentication}/templates/views/auth_mailer/email_confirmation.html.erb +2 -2
- data/lib/generators/{propel_auth → propel_authentication}/templates/views/auth_mailer/email_confirmation.text.erb +1 -1
- data/lib/generators/{propel_auth → propel_authentication}/test/generators/authentication/install_generator_test.rb +4 -4
- data/lib/generators/{propel_auth → propel_authentication}/test/generators/authentication/uninstall_generator_test.rb +1 -1
- data/lib/generators/{propel_auth → propel_authentication}/test/integration/generator_integration_test.rb +1 -1
- data/lib/generators/{propel_auth → propel_authentication}/test/integration/multi_version_generator_test.rb +13 -12
- data/lib/generators/{propel_auth → propel_authentication}/unpack_generator.rb +55 -38
- data/lib/propel_authentication.rb +3 -0
- metadata +101 -98
- data/lib/generators/propel_auth/core/configuration_methods.rb +0 -134
- data/lib/generators/propel_auth/pack_generator.rb +0 -277
- data/lib/generators/propel_auth/templates/agency.rb +0 -7
- data/lib/generators/propel_auth/templates/agent.rb +0 -7
- data/lib/generators/propel_auth/templates/auth/base_passwords_controller.rb.tt +0 -99
- data/lib/generators/propel_auth/templates/auth/base_tokens_controller.rb.tt +0 -90
- data/lib/generators/propel_auth/templates/auth/passwords_controller.rb.tt +0 -126
- data/lib/generators/propel_auth/templates/db/seeds.rb +0 -29
- data/lib/generators/propel_auth/templates/organization.rb +0 -7
- data/lib/generators/propel_auth/templates/propel_auth.rb.tt +0 -141
- data/lib/propel_auth.rb +0 -3
- /data/lib/generators/{propel_auth → propel_authentication}/templates/concerns/rack_session_disable.rb +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/templates/config/environments/development_email.rb +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/templates/db/migrate/create_agencies.rb +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/templates/db/migrate/create_agents.rb +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/templates/db/migrate/create_invitations.rb +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/templates/db/migrate/create_organizations.rb +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/templates/db/migrate/create_users.rb +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/templates/test/mailers/previews/auth_mailer_preview.rb +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/templates/views/auth_mailer/account_unlock.html.erb +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/templates/views/auth_mailer/account_unlock.text.erb +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/templates/views/auth_mailer/password_reset.html.erb +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/templates/views/auth_mailer/password_reset.text.erb +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/templates/views/auth_mailer/user_invitation.html.erb +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/templates/views/auth_mailer/user_invitation.text.erb +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/Dockerfile +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/Gemfile +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/README.md +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/Rakefile +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/app/assets/stylesheets/application.css +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/app/controllers/application_controller.rb +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/app/helpers/application_helper.rb +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/app/jobs/application_job.rb +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/app/mailers/application_mailer.rb +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/app/models/application_record.rb +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/app/views/layouts/application.html.erb +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/app/views/layouts/mailer.html.erb +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/app/views/layouts/mailer.text.erb +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/app/views/pwa/manifest.json.erb +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/app/views/pwa/service-worker.js +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/bin/brakeman +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/bin/dev +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/bin/docker-entrypoint +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/bin/rails +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/bin/rake +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/bin/rubocop +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/bin/setup +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/bin/thrust +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/application.rb +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/boot.rb +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/cable.yml +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/credentials.yml.enc +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/database.yml +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/environment.rb +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/environments/development.rb +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/environments/production.rb +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/environments/test.rb +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/initializers/assets.rb +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/initializers/content_security_policy.rb +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/initializers/filter_parameter_logging.rb +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/initializers/inflections.rb +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/locales/en.yml +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/master.key +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/puma.rb +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/routes.rb +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/storage.yml +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config.ru +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/db/schema.rb +0 -0
- /data/lib/generators/{propel_auth → propel_authentication}/test/generators/authentication/controllers/tokens_controller_test.rb +0 -0
|
@@ -0,0 +1,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 =
|
|
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 =
|
|
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 =
|
|
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: '
|
|
14
|
-
username: '
|
|
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
|
|
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: '
|
|
118
|
-
username: '
|
|
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 '
|
|
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
|
-
|
|
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
|
-
|
|
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: '
|
|
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
|
|
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
|
-
|
|
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']
|
|
@@ -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
|
|
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,
|
|
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(:
|
|
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,
|
|
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"
|
|
@@ -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.
|
|
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.
|
|
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,
|
|
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.
|
|
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,
|
|
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
|