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,191 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
##
|
|
4
|
+
# Module providing configuration detection and validation for PropelAuthentication generators
|
|
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
|
+
#
|
|
21
|
+
module PropelAuthentication
|
|
22
|
+
module ConfigurationMethods
|
|
23
|
+
|
|
24
|
+
# Shared class options across all PropelAuthentication generators
|
|
25
|
+
def self.included(base)
|
|
26
|
+
base.class_option :namespace,
|
|
27
|
+
type: :string,
|
|
28
|
+
default: nil,
|
|
29
|
+
desc: "Authentication namespace (e.g., 'api', 'admin_api'). Use 'none' for no namespace. Defaults to PropelAuthentication configuration."
|
|
30
|
+
|
|
31
|
+
base.class_option :version,
|
|
32
|
+
type: :string,
|
|
33
|
+
default: nil,
|
|
34
|
+
desc: "Authentication version (e.g., 'v1', 'v2'). Use 'none' for no versioning. Defaults to PropelAuthentication configuration."
|
|
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
|
+
|
|
41
|
+
# Legacy support for existing --api-version option
|
|
42
|
+
base.class_option :api_version,
|
|
43
|
+
type: :string,
|
|
44
|
+
default: nil,
|
|
45
|
+
desc: "Legacy: Use --version instead. Authentication version (e.g., 'v1', 'v2')."
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
protected
|
|
49
|
+
|
|
50
|
+
|
|
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)
|
|
58
|
+
end
|
|
59
|
+
|
|
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'
|
|
67
|
+
return nil
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
if options[option_key].present?
|
|
71
|
+
return options[option_key]
|
|
72
|
+
end
|
|
73
|
+
|
|
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'
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# 2. Try PropelAuthentication configuration
|
|
80
|
+
begin
|
|
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'
|
|
86
|
+
end
|
|
87
|
+
rescue StandardError
|
|
88
|
+
# Configuration not available, continue
|
|
89
|
+
end
|
|
90
|
+
|
|
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
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# Display helpers for logging
|
|
112
|
+
def namespace_display
|
|
113
|
+
@auth_namespace.present? ? @auth_namespace : 'none'
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def version_display
|
|
117
|
+
@auth_version.present? ? @auth_version : 'none'
|
|
118
|
+
end
|
|
119
|
+
|
|
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
|
|
125
|
+
def auth_route_prefix
|
|
126
|
+
path_parts = []
|
|
127
|
+
path_parts << @auth_namespace if @auth_namespace.present?
|
|
128
|
+
path_parts << @auth_version if @auth_version.present?
|
|
129
|
+
path_parts << @auth_scope if @auth_scope.present?
|
|
130
|
+
|
|
131
|
+
return '/' if path_parts.empty?
|
|
132
|
+
'/' + path_parts.join('/')
|
|
133
|
+
end
|
|
134
|
+
|
|
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)
|
|
139
|
+
class_parts = []
|
|
140
|
+
class_parts << @auth_namespace.camelize if @auth_namespace.present?
|
|
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('::')
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Authentication controller directory path
|
|
149
|
+
def auth_controller_directory
|
|
150
|
+
path_parts = ['app', 'controllers']
|
|
151
|
+
path_parts << @auth_namespace if @auth_namespace.present?
|
|
152
|
+
path_parts << @auth_version if @auth_version.present?
|
|
153
|
+
path_parts << 'auth'
|
|
154
|
+
path_parts.join('/')
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Helper methods for templates
|
|
158
|
+
def api_versioned?
|
|
159
|
+
@auth_version.present? && @auth_version != 'none'
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def api_only_app?
|
|
163
|
+
defined?(Rails.application.config.api_only) && Rails.application.config.api_only
|
|
164
|
+
end
|
|
165
|
+
|
|
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('/')
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# This file should ensure the existence of records required to run the application in every environment (production,
|
|
2
|
+
# development, test). The code here should be idempotent so that it can be executed at any point in every environment.
|
|
3
|
+
# The data can then be loaded with the bin/rails db:seed command (or created alongside the database with db:setup).
|
|
4
|
+
#
|
|
5
|
+
# Example:
|
|
6
|
+
#
|
|
7
|
+
# ["Action", "Comedy", "Drama", "Horror"].each do |genre_name|
|
|
8
|
+
# MovieGenre.find_or_create_by!(name: genre_name)
|
|
9
|
+
# end
|
|
10
|
+
|
|
11
|
+
# Create comprehensive tenancy structure for authentication testing
|
|
12
|
+
puts "🏢 Creating test organization structure..."
|
|
13
|
+
|
|
14
|
+
organization = Organization.find_or_create_by!(name: 'Test Organization') do |org|
|
|
15
|
+
org.website = 'https://test.example.com'
|
|
16
|
+
org.time_zone = 'UTC'
|
|
17
|
+
end
|
|
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
|
|
31
|
+
user = User.find_or_create_by!(email_address: 'test@example.com') do |u|
|
|
32
|
+
u.username = 'testuser'
|
|
33
|
+
u.email_address = 'test@example.com'
|
|
34
|
+
u.password = 'password123'
|
|
35
|
+
u.password_confirmation = 'password123'
|
|
36
|
+
u.organization = organization
|
|
37
|
+
u.first_name = 'Test'
|
|
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'
|
|
64
|
+
end
|
|
65
|
+
|
|
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"
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
# Progressive Signup Flow
|
|
2
|
+
|
|
3
|
+
This authentication system supports a **progressive multi-step signup flow** that allows users to create organizations and agencies based on their needs.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
The signup flow creates:
|
|
8
|
+
1. **Organization** - The main tenant/company
|
|
9
|
+
2. **User** - The primary user account (owner role)
|
|
10
|
+
3. **Agency** - Optional organizational unit (if agency tenancy enabled + provided)
|
|
11
|
+
4. **Agent** - Automatic relationship between user and agency (if agency created)
|
|
12
|
+
|
|
13
|
+
## Configuration
|
|
14
|
+
|
|
15
|
+
Agency tenancy can be enabled/disabled in `config/initializers/propel_api.rb`:
|
|
16
|
+
|
|
17
|
+
```ruby
|
|
18
|
+
PropelApi.configure do |config|
|
|
19
|
+
config.agency_tenancy = true # Enable agency-level tenancy
|
|
20
|
+
# config.agency_tenancy = false # Disable for organization-only tenancy
|
|
21
|
+
end
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Signup Endpoints
|
|
25
|
+
|
|
26
|
+
### Simple Signup (Organization + User only)
|
|
27
|
+
```http
|
|
28
|
+
POST /signup
|
|
29
|
+
Content-Type: application/json
|
|
30
|
+
|
|
31
|
+
{
|
|
32
|
+
"user": {
|
|
33
|
+
"email_address": "john@newcompany.com",
|
|
34
|
+
"username": "john_doe",
|
|
35
|
+
"password": "securepassword123",
|
|
36
|
+
"password_confirmation": "securepassword123",
|
|
37
|
+
"first_name": "John",
|
|
38
|
+
"last_name": "Doe",
|
|
39
|
+
"phone_number": "+1-555-0123",
|
|
40
|
+
"time_zone": "America/New_York"
|
|
41
|
+
},
|
|
42
|
+
"organization": {
|
|
43
|
+
"name": "NewCo Inc",
|
|
44
|
+
"website": "https://newco.com",
|
|
45
|
+
"time_zone": "America/New_York",
|
|
46
|
+
"description": "Innovative solutions company"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
#### Response (agency tenancy disabled)
|
|
52
|
+
```json
|
|
53
|
+
{
|
|
54
|
+
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
|
|
55
|
+
"user": {
|
|
56
|
+
"id": 123,
|
|
57
|
+
"email_address": "john@newcompany.com",
|
|
58
|
+
"username": "john_doe",
|
|
59
|
+
"first_name": "John",
|
|
60
|
+
"last_name": "Doe",
|
|
61
|
+
"organization_id": 456,
|
|
62
|
+
"created_at": "2024-01-15T10:30:00Z"
|
|
63
|
+
},
|
|
64
|
+
"organization": {
|
|
65
|
+
"id": 456,
|
|
66
|
+
"name": "NewCo Inc",
|
|
67
|
+
"website": "https://newco.com",
|
|
68
|
+
"time_zone": "America/New_York"
|
|
69
|
+
},
|
|
70
|
+
"agency": null,
|
|
71
|
+
"agent": null,
|
|
72
|
+
"message": "Account created successfully! Ready to start working.",
|
|
73
|
+
"next_steps": [
|
|
74
|
+
{
|
|
75
|
+
"action": "invite_team_members",
|
|
76
|
+
"description": "Invite colleagues to join your organization",
|
|
77
|
+
"endpoint": "/invitations",
|
|
78
|
+
"method": "POST"
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"action": "start_creating_resources",
|
|
82
|
+
"description": "Begin creating and managing your content",
|
|
83
|
+
"endpoint": "/"
|
|
84
|
+
}
|
|
85
|
+
]
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Complete Signup (Organization + User + Agency + Agent)
|
|
90
|
+
```http
|
|
91
|
+
POST /signup
|
|
92
|
+
Content-Type: application/json
|
|
93
|
+
|
|
94
|
+
{
|
|
95
|
+
"user": {
|
|
96
|
+
"email_address": "sarah@designstudio.com",
|
|
97
|
+
"username": "sarah_creative",
|
|
98
|
+
"password": "securepassword123",
|
|
99
|
+
"password_confirmation": "securepassword123",
|
|
100
|
+
"first_name": "Sarah",
|
|
101
|
+
"last_name": "Designer"
|
|
102
|
+
},
|
|
103
|
+
"organization": {
|
|
104
|
+
"name": "Creative Design Studio",
|
|
105
|
+
"website": "https://designstudio.com",
|
|
106
|
+
"time_zone": "UTC"
|
|
107
|
+
},
|
|
108
|
+
"agency": {
|
|
109
|
+
"name": "Creative Department",
|
|
110
|
+
"description": "Main creative and design operations",
|
|
111
|
+
"website": "https://creative.designstudio.com",
|
|
112
|
+
"phone_number": "+1-555-DESIGN"
|
|
113
|
+
},
|
|
114
|
+
"agent": {
|
|
115
|
+
"role": "owner"
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
#### Response (agency tenancy enabled)
|
|
121
|
+
```json
|
|
122
|
+
{
|
|
123
|
+
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
|
|
124
|
+
"user": {
|
|
125
|
+
"id": 789,
|
|
126
|
+
"email_address": "sarah@designstudio.com",
|
|
127
|
+
"username": "sarah_creative",
|
|
128
|
+
"first_name": "Sarah",
|
|
129
|
+
"last_name": "Designer",
|
|
130
|
+
"organization_id": 101,
|
|
131
|
+
"created_at": "2024-01-15T14:20:00Z"
|
|
132
|
+
},
|
|
133
|
+
"organization": {
|
|
134
|
+
"id": 101,
|
|
135
|
+
"name": "Creative Design Studio",
|
|
136
|
+
"website": "https://designstudio.com",
|
|
137
|
+
"time_zone": "UTC"
|
|
138
|
+
},
|
|
139
|
+
"agency": {
|
|
140
|
+
"id": 202,
|
|
141
|
+
"name": "Creative Department",
|
|
142
|
+
"organization_id": 101,
|
|
143
|
+
"description": "Main creative and design operations",
|
|
144
|
+
"website": "https://creative.designstudio.com"
|
|
145
|
+
},
|
|
146
|
+
"agent": {
|
|
147
|
+
"id": 303,
|
|
148
|
+
"user_id": 789,
|
|
149
|
+
"agency_id": 202,
|
|
150
|
+
"role": "owner",
|
|
151
|
+
"created_at": "2024-01-15T14:20:00Z"
|
|
152
|
+
},
|
|
153
|
+
"message": "Account created successfully! Ready to start working.",
|
|
154
|
+
"next_steps": [
|
|
155
|
+
{
|
|
156
|
+
"action": "create_additional_agencies",
|
|
157
|
+
"description": "Add more agencies to organize your team",
|
|
158
|
+
"endpoint": "/agencies",
|
|
159
|
+
"method": "POST"
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
"action": "start_creating_resources",
|
|
163
|
+
"description": "Begin creating and managing your content (you can create resources immediately)",
|
|
164
|
+
"endpoint": "/",
|
|
165
|
+
"note": "Your agency is ready for resource creation"
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
"action": "invite_team_members",
|
|
169
|
+
"description": "Invite colleagues to join your organization",
|
|
170
|
+
"endpoint": "/invitations",
|
|
171
|
+
"method": "POST"
|
|
172
|
+
}
|
|
173
|
+
]
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Progressive Enhancement Flow
|
|
178
|
+
|
|
179
|
+
### Step 1: Core Signup
|
|
180
|
+
User creates account with minimal information (User + Organization).
|
|
181
|
+
|
|
182
|
+
### Step 2: Add Agencies (Optional)
|
|
183
|
+
```http
|
|
184
|
+
POST /api/v1/agencies
|
|
185
|
+
Authorization: Bearer {token_from_signup}
|
|
186
|
+
Content-Type: application/json
|
|
187
|
+
|
|
188
|
+
{
|
|
189
|
+
"data": {
|
|
190
|
+
"name": "Marketing Department",
|
|
191
|
+
"description": "Marketing and growth operations"
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Step 3: Invite Team Members
|
|
197
|
+
```http
|
|
198
|
+
POST /api/v1/invitations
|
|
199
|
+
Authorization: Bearer {token_from_signup}
|
|
200
|
+
Content-Type: application/json
|
|
201
|
+
|
|
202
|
+
{
|
|
203
|
+
"data": {
|
|
204
|
+
"email_address": "teammate@designstudio.com",
|
|
205
|
+
"agency_id": 202,
|
|
206
|
+
"role": "manager"
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### Step 4: Start Creating Resources
|
|
212
|
+
With the JWT token, users can immediately create resources:
|
|
213
|
+
|
|
214
|
+
```http
|
|
215
|
+
POST /api/v1/projects
|
|
216
|
+
Authorization: Bearer {token_from_signup}
|
|
217
|
+
Content-Type: application/json
|
|
218
|
+
|
|
219
|
+
{
|
|
220
|
+
"data": {
|
|
221
|
+
"name": "New Website Design",
|
|
222
|
+
"agency_id": 202
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## Error Handling
|
|
228
|
+
|
|
229
|
+
### Missing Required Agency Data (when agency tenancy enabled)
|
|
230
|
+
```json
|
|
231
|
+
{
|
|
232
|
+
"error": "Agency information required",
|
|
233
|
+
"message": "Agency details must be provided when agency tenancy is enabled",
|
|
234
|
+
"code": "MISSING_AGENCY_DATA",
|
|
235
|
+
"hint": "Include an 'agency' object with name and other details in your request"
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### Validation Errors
|
|
240
|
+
```json
|
|
241
|
+
{
|
|
242
|
+
"error": "Validation failed",
|
|
243
|
+
"details": {
|
|
244
|
+
"email_address": ["Email address has already been taken"],
|
|
245
|
+
"password": ["Password confirmation doesn't match Password"]
|
|
246
|
+
},
|
|
247
|
+
"message": "Please correct the errors and try again"
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
## JWT Token Structure
|
|
252
|
+
|
|
253
|
+
The returned JWT token contains:
|
|
254
|
+
```json
|
|
255
|
+
{
|
|
256
|
+
"user_id": 789,
|
|
257
|
+
"email_address": "sarah@designstudio.com",
|
|
258
|
+
"organization_id": 101,
|
|
259
|
+
"agency_ids": [202],
|
|
260
|
+
"iat": 1705329600,
|
|
261
|
+
"exp": 1705416000
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
This enables immediate API access with proper organizational and agency scoping.
|
|
266
|
+
|
|
267
|
+
## User-Provided vs. System-Automatic
|
|
268
|
+
|
|
269
|
+
### 🔴 REQUIRED User Input
|
|
270
|
+
- **email_address** - Validated for format & uniqueness
|
|
271
|
+
- **username** - Validated for uniqueness
|
|
272
|
+
- **password** - Minimum 8 characters, secure validation
|
|
273
|
+
- **password_confirmation** - Must match password
|
|
274
|
+
- **organization.name** - Organization name (required)
|
|
275
|
+
|
|
276
|
+
### 🟡 OPTIONAL User Input
|
|
277
|
+
- **first_name, last_name** - For personalization (optional)
|
|
278
|
+
- **phone_number** - Contact information (optional)
|
|
279
|
+
- **time_zone** - User/organization timezone (optional)
|
|
280
|
+
- **organization.website** - Company website (optional)
|
|
281
|
+
- **organization.description** - Company description (optional)
|
|
282
|
+
- **agency.{name, description, etc}** - When agency tenancy enabled (optional)
|
|
283
|
+
|
|
284
|
+
### 🤖 SYSTEM-AUTOMATIC
|
|
285
|
+
- **user.id** - Auto-generated primary key
|
|
286
|
+
- **password_digest** - Auto-hashed with bcrypt
|
|
287
|
+
- **organization_id** - Auto-assigned from created organization
|
|
288
|
+
- **role: 'owner'** - Auto-assigned for signup user
|
|
289
|
+
- **JWT token** - Auto-generated for immediate API access
|
|
290
|
+
- **Agent record** - Auto-created if agency provided
|
|
291
|
+
- **created_at/updated_at** - Auto-set timestamps
|
|
292
|
+
- **Organization/agency scoping** - Auto-applied to all resources
|
|
293
|
+
|
|
294
|
+
## Architecture Benefits
|
|
295
|
+
|
|
296
|
+
1. **Immediate Access**: Users can start working immediately after signup
|
|
297
|
+
2. **Progressive Enhancement**: Start simple, add complexity as needed
|
|
298
|
+
3. **Proper Scoping**: All resources automatically scoped to organization/agency
|
|
299
|
+
4. **Security**: JWT tokens contain proper context for authorization
|
|
300
|
+
5. **Flexibility**: Works with or without agency tenancy
|
|
301
|
+
6. **Smart Defaults**: Minimal required input, system handles the rest
|
|
302
|
+
|
|
303
|
+
## Configuration Options
|
|
304
|
+
|
|
305
|
+
Users can choose their tenancy model:
|
|
306
|
+
|
|
307
|
+
- **Organization-only tenancy**: `agency_tenancy = false`
|
|
308
|
+
- Simpler data model
|
|
309
|
+
- Resources scoped to organization only
|
|
310
|
+
- Good for single-agency organizations
|
|
311
|
+
|
|
312
|
+
- **Multi-agency tenancy**: `agency_tenancy = true` (default)
|
|
313
|
+
- More complex but flexible
|
|
314
|
+
- Resources scoped to organization + agency
|
|
315
|
+
- Good for multi-department organizations
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
class Agency < ApplicationRecord
|
|
2
|
+
# Multi-tenant associations
|
|
3
|
+
belongs_to :organization
|
|
4
|
+
has_many :agents, dependent: :destroy
|
|
5
|
+
|
|
6
|
+
validates :name, presence: true
|
|
7
|
+
<% if @rendering_engine == 'json_facet' -%>
|
|
8
|
+
|
|
9
|
+
# Facets
|
|
10
|
+
json_facet :short, fields: [:id, :name]
|
|
11
|
+
json_facet :details, fields: [:id, :name, :organization_id, :created_at, :updated_at]
|
|
12
|
+
<% end -%>
|
|
13
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
class Agent < ApplicationRecord
|
|
2
|
+
# Multi-tenant associations
|
|
3
|
+
belongs_to :agency
|
|
4
|
+
belongs_to :user
|
|
5
|
+
|
|
6
|
+
validates :user_id, uniqueness: { scope: :agency_id }
|
|
7
|
+
<% if @rendering_engine == 'json_facet' -%>
|
|
8
|
+
|
|
9
|
+
# Facets
|
|
10
|
+
json_facet :short, fields: [:id, :title]
|
|
11
|
+
json_facet :details, fields: [:id, :title, :role, :organization_id, :created_at, :updated_at]
|
|
12
|
+
<% end -%>
|
|
13
|
+
end
|
|
@@ -15,6 +15,12 @@ class Invitation < ApplicationRecord
|
|
|
15
15
|
scope :valid, -> { where(status: :pending).where('created_at > ?', 7.days.ago) }
|
|
16
16
|
scope :recent, -> { where('created_at > ?', 30.days.ago) }
|
|
17
17
|
|
|
18
|
+
<% if @rendering_engine == 'json_facet' -%>
|
|
19
|
+
# Facets
|
|
20
|
+
json_facet :short, fields: [:id, :email_address, :organization_id]
|
|
21
|
+
json_facet :details, fields: [:id, :name, :status, :organization_id, :created_at, :updated_at]
|
|
22
|
+
<% end -%>
|
|
23
|
+
|
|
18
24
|
# Check if invitation is still valid (not expired)
|
|
19
25
|
def invitation_valid?
|
|
20
26
|
pending? && created_at > 7.days.ago
|
|
@@ -42,14 +48,14 @@ class Invitation < ApplicationRecord
|
|
|
42
48
|
invitation_hash: generate_invitation_hash
|
|
43
49
|
}
|
|
44
50
|
|
|
45
|
-
JWT.encode(payload,
|
|
51
|
+
JWT.encode(payload, PropelAuthentication.configuration.jwt_secret, 'HS256')
|
|
46
52
|
end
|
|
47
53
|
|
|
48
54
|
# Find invitation by JWT token with validation
|
|
49
55
|
def self.find_by_invitation_token(token)
|
|
50
56
|
begin
|
|
51
57
|
# Decode and validate the JWT token
|
|
52
|
-
payload = JWT.decode(token,
|
|
58
|
+
payload = JWT.decode(token, PropelAuthentication.configuration.jwt_secret, true, { algorithm: 'HS256' })[0]
|
|
53
59
|
|
|
54
60
|
# Verify this is an invitation token
|
|
55
61
|
return nil unless payload['type'] == 'invitation'
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
class Organization < ApplicationRecord
|
|
2
|
+
# Multi-tenant associations
|
|
3
|
+
has_many :users, dependent: :destroy
|
|
4
|
+
has_many :agencies, dependent: :destroy
|
|
5
|
+
|
|
6
|
+
validates :name, presence: true
|
|
7
|
+
<% if @rendering_engine == 'json_facet' -%>
|
|
8
|
+
# Facets
|
|
9
|
+
json_facet :short, fields: [:id, :name, :website, :time_zone]
|
|
10
|
+
json_facet :details, fields: [:id, :name, :website, :time_zone, :meta, :settings, :created_at, :updated_at]
|
|
11
|
+
<% end -%>
|
|
12
|
+
end
|
|
@@ -3,6 +3,7 @@ class User < ApplicationRecord
|
|
|
3
3
|
belongs_to :organization
|
|
4
4
|
has_many :invitations, foreign_key: :inviter_id, dependent: :destroy
|
|
5
5
|
has_many :agents, dependent: :destroy
|
|
6
|
+
has_many :agencies, through: :agents
|
|
6
7
|
|
|
7
8
|
# Validations
|
|
8
9
|
validates :email_address, presence: true, uniqueness: true
|
|
@@ -18,4 +19,8 @@ class User < ApplicationRecord
|
|
|
18
19
|
# Future enhancements (Epic 2)
|
|
19
20
|
# include Invitable
|
|
20
21
|
# include Statusable
|
|
22
|
+
|
|
23
|
+
# Facets
|
|
24
|
+
json_facet :short, fields: [:id, :email_address, :username, :phone_number, :first_name, :middle_name, :last_name, :time_zone, :status]
|
|
25
|
+
json_facet :details, fields: [:id, :email_address, :username, :phone_number, :first_name, :middle_name, :last_name, :time_zone, :confirmation_sent_at, :unconfirmed_email_address, :confirmed_at, :status, :last_login_at, :failed_login_attempts, :locked_at, :organization_id, :meta, :settings, :created_at, :updated_at], include: [:organization]
|
|
21
26
|
end
|