propel_authentication 0.1.4 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +43 -2
- data/README.md +6 -6
- data/lib/generators/propel_authentication/install_generator.rb +135 -153
- data/lib/generators/propel_authentication/templates/application_mailer.rb +6 -0
- data/lib/generators/propel_authentication/templates/auth/passwords_controller.rb.tt +84 -78
- data/lib/generators/propel_authentication/templates/auth/signup_controller.rb.tt +242 -0
- data/lib/generators/propel_authentication/templates/{tokens_controller.rb.tt → auth/tokens_controller.rb.tt} +39 -22
- data/lib/generators/propel_authentication/templates/auth_mailer.rb +3 -1
- data/lib/generators/propel_authentication/templates/authenticatable.rb +8 -2
- data/lib/generators/propel_authentication/templates/concerns/confirmable.rb +1 -1
- data/lib/generators/propel_authentication/templates/concerns/lockable.rb +4 -2
- data/lib/generators/propel_authentication/templates/concerns/{propel_authentication.rb → propel_authentication_concern.rb} +33 -3
- data/lib/generators/propel_authentication/templates/concerns/recoverable.rb +16 -6
- data/lib/generators/propel_authentication/templates/core/configuration_methods.rb +104 -64
- data/lib/generators/propel_authentication/templates/db/seeds.rb +50 -4
- data/lib/generators/propel_authentication/templates/doc/signup_flow.md +315 -0
- data/lib/generators/propel_authentication/templates/models/agency.rb.tt +13 -0
- data/lib/generators/propel_authentication/templates/models/agent.rb.tt +13 -0
- data/lib/generators/propel_authentication/templates/{invitation.rb → models/invitation.rb.tt} +6 -0
- data/lib/generators/propel_authentication/templates/models/organization.rb.tt +12 -0
- data/lib/generators/propel_authentication/templates/{user.rb → models/user.rb.tt} +5 -0
- data/lib/generators/propel_authentication/templates/propel_authentication.rb.tt +94 -9
- data/lib/generators/propel_authentication/templates/routes/auth_routes.rb.tt +55 -0
- data/lib/generators/propel_authentication/templates/services/auth_notification_service.rb +3 -3
- data/lib/generators/propel_authentication/templates/test/concerns/confirmable_test.rb.tt +34 -10
- data/lib/generators/propel_authentication/templates/test/concerns/propel_authentication_test.rb.tt +1 -1
- data/lib/generators/propel_authentication/templates/test/concerns/recoverable_test.rb.tt +4 -4
- data/lib/generators/propel_authentication/templates/test/controllers/auth/lockable_integration_test.rb.tt +18 -15
- data/lib/generators/propel_authentication/templates/test/controllers/auth/password_reset_integration_test.rb.tt +38 -40
- data/lib/generators/propel_authentication/templates/test/controllers/auth/signup_controller_test.rb.tt +201 -0
- data/lib/generators/propel_authentication/templates/test/controllers/auth/tokens_controller_test.rb.tt +33 -25
- data/lib/generators/propel_authentication/templates/test/mailers/auth_mailer_test.rb.tt +51 -36
- data/lib/generators/propel_authentication/templates/views/auth_mailer/email_confirmation.html.erb +2 -2
- data/lib/generators/propel_authentication/templates/views/auth_mailer/email_confirmation.text.erb +1 -1
- data/lib/generators/propel_authentication/test/generators/authentication/install_generator_test.rb +4 -4
- data/lib/generators/propel_authentication/test/generators/authentication/uninstall_generator_test.rb +1 -1
- data/lib/generators/propel_authentication/test/integration/generator_integration_test.rb +1 -1
- data/lib/generators/propel_authentication/test/integration/multi_version_generator_test.rb +13 -12
- data/lib/generators/propel_authentication/unpack_generator.rb +19 -15
- data/lib/propel_authentication.rb +1 -1
- metadata +14 -11
- data/lib/generators/propel_authentication/templates/agency.rb +0 -7
- data/lib/generators/propel_authentication/templates/agent.rb +0 -7
- data/lib/generators/propel_authentication/templates/auth/base_passwords_controller.rb.tt +0 -99
- data/lib/generators/propel_authentication/templates/auth/base_tokens_controller.rb.tt +0 -90
- data/lib/generators/propel_authentication/templates/organization.rb +0 -7
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
module
|
|
3
|
+
module PropelAuthenticationConcern
|
|
4
4
|
extend ActiveSupport::Concern
|
|
5
5
|
|
|
6
|
-
private
|
|
7
|
-
|
|
8
6
|
def authenticate_user
|
|
9
7
|
token = extract_jwt_token
|
|
10
8
|
|
|
@@ -19,6 +17,10 @@ module PropelAuthentication
|
|
|
19
17
|
render json: { error: 'Invalid token' }, status: :unauthorized
|
|
20
18
|
return false
|
|
21
19
|
end
|
|
20
|
+
|
|
21
|
+
# Extract organization context from JWT payload
|
|
22
|
+
extract_organization_context(token)
|
|
23
|
+
|
|
22
24
|
rescue JWT::ExpiredSignature
|
|
23
25
|
render json: { error: 'Token expired' }, status: :unauthorized
|
|
24
26
|
return false
|
|
@@ -34,6 +36,25 @@ module PropelAuthentication
|
|
|
34
36
|
@current_user
|
|
35
37
|
end
|
|
36
38
|
|
|
39
|
+
def current_organization_id
|
|
40
|
+
@current_organization_id
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def current_organization
|
|
44
|
+
@current_organization ||= Organization.find_by(id: current_organization_id) if current_organization_id
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def current_agency_ids
|
|
48
|
+
return [] unless current_user
|
|
49
|
+
|
|
50
|
+
# Request-scoped memoization for performance without security risk
|
|
51
|
+
@current_agency_ids ||= current_user.agency_ids
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def has_agency_access?(agency_id)
|
|
55
|
+
current_agency_ids.include?(agency_id.to_i)
|
|
56
|
+
end
|
|
57
|
+
|
|
37
58
|
def extract_jwt_token
|
|
38
59
|
auth_header = request.headers['Authorization']
|
|
39
60
|
return nil unless auth_header
|
|
@@ -41,4 +62,13 @@ module PropelAuthentication
|
|
|
41
62
|
# Extract token from "Bearer <token>" format
|
|
42
63
|
auth_header.split(' ').last if auth_header.start_with?('Bearer ')
|
|
43
64
|
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def extract_organization_context(token)
|
|
69
|
+
decoded_token = JWT.decode(token, PropelAuthentication.configuration.jwt_secret, true, { algorithm: 'HS256' })
|
|
70
|
+
payload = decoded_token.first
|
|
71
|
+
@current_organization_id = payload['organization_id']
|
|
72
|
+
# Removed: @current_agency_ids (now looked up in real-time via current_user_agency_ids)
|
|
73
|
+
end
|
|
44
74
|
end
|
|
@@ -51,12 +51,22 @@ module Recoverable
|
|
|
51
51
|
# Reset password with token validation
|
|
52
52
|
def reset_password_with_token!(token, new_password, new_password_confirmation = new_password)
|
|
53
53
|
return false unless valid_password_reset_token?(token)
|
|
54
|
-
return false if new_password.blank?
|
|
55
|
-
return false if new_password != new_password_confirmation
|
|
56
54
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
55
|
+
if new_password.blank?
|
|
56
|
+
errors.add(:password, "cannot be blank")
|
|
57
|
+
return false
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
if new_password != new_password_confirmation
|
|
61
|
+
errors.add(:password_confirmation, "doesn't match Password")
|
|
62
|
+
return false
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Validate password length using Rails validations (populates errors)
|
|
66
|
+
self.password = new_password
|
|
67
|
+
unless valid?
|
|
68
|
+
return false # Rails validations populate errors automatically
|
|
69
|
+
end
|
|
60
70
|
|
|
61
71
|
begin
|
|
62
72
|
# Update password and clear failed login attempts
|
|
@@ -73,7 +83,7 @@ module Recoverable
|
|
|
73
83
|
|
|
74
84
|
class_methods do
|
|
75
85
|
# Find user by JWT password reset token
|
|
76
|
-
def
|
|
86
|
+
def find_by_jwt_password_reset_token(token)
|
|
77
87
|
return nil if token.blank?
|
|
78
88
|
|
|
79
89
|
begin
|
|
@@ -3,6 +3,21 @@
|
|
|
3
3
|
##
|
|
4
4
|
# Module providing configuration detection and validation for PropelAuthentication generators
|
|
5
5
|
#
|
|
6
|
+
# INDEPENDENCE ARCHITECTURE:
|
|
7
|
+
# - PropelAuthentication works completely standalone with sensible defaults
|
|
8
|
+
# - PropelApi integration is OPTIONAL - when both gems are present, PropelAuthentication
|
|
9
|
+
# adopts PropelApi's namespace/version for consistency
|
|
10
|
+
# - Neither gem requires the other as a dependency
|
|
11
|
+
# - Graceful fallback ensures no errors when either gem is missing
|
|
12
|
+
#
|
|
13
|
+
# STANDALONE DEFAULTS:
|
|
14
|
+
# - PropelAuthentication: Clean URLs (/login, /me, /signup) for simplicity
|
|
15
|
+
# - PropelApi: REST convention (/api/v1/resources) for standard API patterns
|
|
16
|
+
#
|
|
17
|
+
# INTEGRATION BENEFITS:
|
|
18
|
+
# - When both gems are used together, authentication routes automatically align
|
|
19
|
+
# with API namespace structure for consistent developer experience
|
|
20
|
+
#
|
|
6
21
|
module PropelAuthentication
|
|
7
22
|
module ConfigurationMethods
|
|
8
23
|
|
|
@@ -18,6 +33,11 @@ module PropelAuthentication
|
|
|
18
33
|
default: nil,
|
|
19
34
|
desc: "Authentication version (e.g., 'v1', 'v2'). Use 'none' for no versioning. Defaults to PropelAuthentication configuration."
|
|
20
35
|
|
|
36
|
+
base.class_option :auth_scope,
|
|
37
|
+
type: :string,
|
|
38
|
+
default: nil,
|
|
39
|
+
desc: "Authentication scope namespace (e.g., 'auth', 'admin'). Use 'none' for no auth scope. Defaults to nil for clean URLs."
|
|
40
|
+
|
|
21
41
|
# Legacy support for existing --api-version option
|
|
22
42
|
base.class_option :api_version,
|
|
23
43
|
type: :string,
|
|
@@ -27,75 +47,67 @@ module PropelAuthentication
|
|
|
27
47
|
|
|
28
48
|
protected
|
|
29
49
|
|
|
30
|
-
# Initialize shared PropelAuthentication settings used across all generators
|
|
31
|
-
def initialize_propel_auth_settings
|
|
32
|
-
@auth_namespace = determine_auth_namespace
|
|
33
|
-
@auth_version = determine_auth_version
|
|
34
|
-
end
|
|
35
50
|
|
|
36
|
-
|
|
37
|
-
#
|
|
38
|
-
#
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
return nil
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
if options[:namespace].present?
|
|
47
|
-
return options[:namespace]
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
# Try to read from PropelAuthentication configuration
|
|
51
|
-
begin
|
|
52
|
-
if defined?(PropelAuthentication) && PropelAuthentication.configuration.respond_to?(:namespace)
|
|
53
|
-
config_namespace = PropelAuthentication.configuration.namespace
|
|
54
|
-
return config_namespace if config_namespace.present? && config_namespace != 'none'
|
|
55
|
-
end
|
|
56
|
-
rescue => e
|
|
57
|
-
# Configuration not available, continue to default
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
# Default fallback - no namespace for clean URLs
|
|
61
|
-
nil
|
|
51
|
+
|
|
52
|
+
# Single method to determine all authentication configuration
|
|
53
|
+
# Handles namespace, version, and auth_scope with consistent priority logic
|
|
54
|
+
def determine_configuration
|
|
55
|
+
@auth_namespace = determine_config_value(:namespace, nil)
|
|
56
|
+
@auth_version = determine_config_value(:version, nil)
|
|
57
|
+
@auth_scope = determine_config_value(:auth_scope, nil)
|
|
62
58
|
end
|
|
63
59
|
|
|
64
|
-
#
|
|
65
|
-
# Priority
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if options[:version] == 'none' || options[:api_version] == 'none'
|
|
60
|
+
# Generic configuration value determination with consistent priority
|
|
61
|
+
# Priority: 1) Command line, 2) PropelAuthentication config, 3) PropelApi config (optional), 4) Default
|
|
62
|
+
def determine_config_value(config_key, default_value)
|
|
63
|
+
option_key = config_key.to_s.gsub('_', '-').to_sym
|
|
64
|
+
|
|
65
|
+
# 1. Explicit command line override
|
|
66
|
+
if options[option_key] == 'none'
|
|
72
67
|
return nil
|
|
73
68
|
end
|
|
74
69
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
return options[:version]
|
|
70
|
+
if options[option_key].present?
|
|
71
|
+
return options[option_key]
|
|
78
72
|
end
|
|
79
73
|
|
|
80
|
-
#
|
|
81
|
-
if options[:api_version].present?
|
|
82
|
-
return options[:api_version]
|
|
74
|
+
# Legacy support for --api-version
|
|
75
|
+
if config_key == :version && options[:api_version].present?
|
|
76
|
+
return options[:api_version] unless options[:api_version] == 'none'
|
|
83
77
|
end
|
|
84
78
|
|
|
85
|
-
# Try
|
|
79
|
+
# 2. Try PropelAuthentication configuration
|
|
86
80
|
begin
|
|
87
|
-
if defined?(PropelAuthentication) &&
|
|
88
|
-
|
|
89
|
-
|
|
81
|
+
if defined?(PropelAuthentication) &&
|
|
82
|
+
PropelAuthentication.respond_to?(:configuration) &&
|
|
83
|
+
PropelAuthentication.configuration.respond_to?(config_key)
|
|
84
|
+
config_value = PropelAuthentication.configuration.send(config_key)
|
|
85
|
+
return config_value if config_value.present? && config_value != 'none'
|
|
90
86
|
end
|
|
91
|
-
rescue
|
|
92
|
-
# Configuration not available, continue
|
|
87
|
+
rescue StandardError
|
|
88
|
+
# Configuration not available, continue
|
|
93
89
|
end
|
|
94
90
|
|
|
95
|
-
#
|
|
96
|
-
|
|
91
|
+
# 3. OPTIONAL: Try PropelApi for namespace/version consistency (when both gems are used)
|
|
92
|
+
if [:namespace, :version].include?(config_key)
|
|
93
|
+
begin
|
|
94
|
+
if defined?(PropelApi) &&
|
|
95
|
+
PropelApi.respond_to?(:configuration) &&
|
|
96
|
+
PropelApi.configuration.respond_to?(config_key)
|
|
97
|
+
api_value = PropelApi.configuration.send(config_key)
|
|
98
|
+
return api_value if api_value.present? && api_value != 'none'
|
|
99
|
+
end
|
|
100
|
+
rescue StandardError
|
|
101
|
+
# PropelApi not available - this is fine, PropelAuthentication works independently
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# 4. Sensible default
|
|
106
|
+
default_value
|
|
97
107
|
end
|
|
98
108
|
|
|
109
|
+
|
|
110
|
+
|
|
99
111
|
# Display helpers for logging
|
|
100
112
|
def namespace_display
|
|
101
113
|
@auth_namespace.present? ? @auth_namespace : 'none'
|
|
@@ -105,21 +117,32 @@ module PropelAuthentication
|
|
|
105
117
|
@auth_version.present? ? @auth_version : 'none'
|
|
106
118
|
end
|
|
107
119
|
|
|
108
|
-
|
|
120
|
+
def auth_scope_display
|
|
121
|
+
@auth_scope.present? ? @auth_scope : 'none'
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Authentication route path generation - single source of truth for complete route paths
|
|
109
125
|
def auth_route_prefix
|
|
110
126
|
path_parts = []
|
|
111
127
|
path_parts << @auth_namespace if @auth_namespace.present?
|
|
112
128
|
path_parts << @auth_version if @auth_version.present?
|
|
129
|
+
path_parts << @auth_scope if @auth_scope.present?
|
|
130
|
+
|
|
113
131
|
return '/' if path_parts.empty?
|
|
114
132
|
'/' + path_parts.join('/')
|
|
115
133
|
end
|
|
116
134
|
|
|
117
|
-
|
|
118
|
-
|
|
135
|
+
|
|
136
|
+
# Single method to generate controller class names for all cases
|
|
137
|
+
# Handles both regular controllers and versioned controllers
|
|
138
|
+
def auth_controller_class_name(controller_type = 'tokens', version = nil)
|
|
119
139
|
class_parts = []
|
|
120
140
|
class_parts << @auth_namespace.camelize if @auth_namespace.present?
|
|
121
|
-
class_parts << @auth_version.upcase if @auth_version.present?
|
|
122
|
-
class_parts
|
|
141
|
+
class_parts << (version || @auth_version).upcase if (version || @auth_version).present?
|
|
142
|
+
class_parts << @auth_scope.camelize if @auth_scope.present?
|
|
143
|
+
class_parts << "#{controller_type.camelize}Controller"
|
|
144
|
+
|
|
145
|
+
class_parts.join('::')
|
|
123
146
|
end
|
|
124
147
|
|
|
125
148
|
# Authentication controller directory path
|
|
@@ -140,12 +163,29 @@ module PropelAuthentication
|
|
|
140
163
|
defined?(Rails.application.config.api_only) && Rails.application.config.api_only
|
|
141
164
|
end
|
|
142
165
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
166
|
+
# Extract namespace prefix for test class names (includes trailing :: when needed)
|
|
167
|
+
# E.g., "Api::V1::Auth::TokensController" → "Api::V1::Auth::"
|
|
168
|
+
# E.g., "TokensController" → "" (empty, no namespace)
|
|
169
|
+
def controller_namespace(controller_type)
|
|
170
|
+
controller_class = auth_controller_class_name(controller_type)
|
|
171
|
+
namespace_parts = controller_class.split('::')[0..-2] # Remove "TokensController" part
|
|
172
|
+
|
|
173
|
+
# Return namespace with trailing :: or empty string
|
|
174
|
+
namespace_parts.empty? ? '' : namespace_parts.join('::') + '::'
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Build dynamic controller file path based on current configuration
|
|
178
|
+
# E.g., Api::V1::Auth::TokensController → app/controllers/api/v1/auth/tokens_controller.rb
|
|
179
|
+
def controller_file_path(controller_type)
|
|
180
|
+
controller_class = auth_controller_class_name(controller_type)
|
|
181
|
+
|
|
182
|
+
# Convert class name to file path
|
|
183
|
+
path_parts = ['app', 'controllers']
|
|
184
|
+
namespace_parts = controller_class.split('::')[0..-2] # Remove "TokensController" part
|
|
185
|
+
namespace_parts.each { |part| path_parts << part.downcase }
|
|
186
|
+
path_parts << "#{controller_type}_controller.rb"
|
|
187
|
+
|
|
188
|
+
path_parts.join('/')
|
|
149
189
|
end
|
|
150
190
|
end
|
|
151
191
|
end
|
|
@@ -8,12 +8,26 @@
|
|
|
8
8
|
# MovieGenre.find_or_create_by!(name: genre_name)
|
|
9
9
|
# end
|
|
10
10
|
|
|
11
|
-
# Create
|
|
11
|
+
# Create comprehensive tenancy structure for authentication testing
|
|
12
|
+
puts "🏢 Creating test organization structure..."
|
|
13
|
+
|
|
12
14
|
organization = Organization.find_or_create_by!(name: 'Test Organization') do |org|
|
|
13
15
|
org.website = 'https://test.example.com'
|
|
14
16
|
org.time_zone = 'UTC'
|
|
15
17
|
end
|
|
16
18
|
|
|
19
|
+
# Create agencies for proper tenancy structure
|
|
20
|
+
main_agency = Agency.find_or_create_by!(name: 'Main Department', organization: organization) do |agency|
|
|
21
|
+
agency.phone_number = '+1-555-0101'
|
|
22
|
+
agency.address = '123 Main Street'
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
support_agency = Agency.find_or_create_by!(name: 'Support Department', organization: organization) do |agency|
|
|
26
|
+
agency.phone_number = '+1-555-0102'
|
|
27
|
+
agency.address = '123 Support Street'
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Create test users with proper confirmations
|
|
17
31
|
user = User.find_or_create_by!(email_address: 'test@example.com') do |u|
|
|
18
32
|
u.username = 'testuser'
|
|
19
33
|
u.email_address = 'test@example.com'
|
|
@@ -22,8 +36,40 @@ user = User.find_or_create_by!(email_address: 'test@example.com') do |u|
|
|
|
22
36
|
u.organization = organization
|
|
23
37
|
u.first_name = 'Test'
|
|
24
38
|
u.last_name = 'User'
|
|
39
|
+
u.confirmed_at = 1.week.ago # Confirmed user for easier testing
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
admin_user = User.find_or_create_by!(email_address: 'admin@example.com') do |u|
|
|
43
|
+
u.username = 'adminuser'
|
|
44
|
+
u.email_address = 'admin@example.com'
|
|
45
|
+
u.password = 'password123'
|
|
46
|
+
u.password_confirmation = 'password123'
|
|
47
|
+
u.organization = organization
|
|
48
|
+
u.first_name = 'Admin'
|
|
49
|
+
u.last_name = 'User'
|
|
50
|
+
u.confirmed_at = 1.week.ago # Confirmed admin for easier testing
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Create agent associations for agency access (CRITICAL for tenancy validation)
|
|
54
|
+
Agent.find_or_create_by!(user: user, agency: main_agency) do |agent|
|
|
55
|
+
agent.role = 'member'
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
Agent.find_or_create_by!(user: admin_user, agency: main_agency) do |agent|
|
|
59
|
+
agent.role = 'manager'
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
Agent.find_or_create_by!(user: admin_user, agency: support_agency) do |agent|
|
|
63
|
+
agent.role = 'admin'
|
|
25
64
|
end
|
|
26
65
|
|
|
27
|
-
puts "Created test organization: #{organization.name}"
|
|
28
|
-
puts "Created
|
|
29
|
-
puts "
|
|
66
|
+
puts "✅ Created test organization: #{organization.name}"
|
|
67
|
+
puts "✅ Created agencies: #{Agency.where(organization: organization).pluck(:name).join(', ')}"
|
|
68
|
+
puts "✅ Created test user: #{user.email_address} (password: password123)"
|
|
69
|
+
puts "✅ Created admin user: #{admin_user.email_address} (password: password123)"
|
|
70
|
+
puts "✅ Created agent associations for proper tenancy access"
|
|
71
|
+
puts ""
|
|
72
|
+
puts "🎯 Test user agency access: #{user.agency_ids}"
|
|
73
|
+
puts "🎯 Admin user agency access: #{admin_user.agency_ids}"
|
|
74
|
+
puts ""
|
|
75
|
+
puts "🚀 You can now test login with POST /api/v1/login"
|