propel_authentication 0.1.3 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +128 -0
  3. data/README.md +254 -116
  4. data/lib/generators/{propel_auth → propel_authentication}/install_generator.rb +152 -170
  5. data/lib/generators/propel_authentication/templates/application_mailer.rb +6 -0
  6. data/lib/generators/propel_authentication/templates/auth/passwords_controller.rb.tt +132 -0
  7. data/lib/generators/propel_authentication/templates/auth/signup_controller.rb.tt +242 -0
  8. data/lib/generators/{propel_auth/templates → propel_authentication/templates/auth}/tokens_controller.rb.tt +39 -22
  9. data/lib/generators/{propel_auth → propel_authentication}/templates/auth_mailer.rb +3 -1
  10. data/lib/generators/{propel_auth → propel_authentication}/templates/authenticatable.rb +10 -4
  11. data/lib/generators/{propel_auth → propel_authentication}/templates/concerns/confirmable.rb +3 -3
  12. data/lib/generators/{propel_auth → propel_authentication}/templates/concerns/lockable.rb +10 -8
  13. data/lib/generators/{propel_auth/templates/concerns/propel_authentication.rb → propel_authentication/templates/concerns/propel_authentication_concern.rb} +33 -3
  14. data/lib/generators/{propel_auth → propel_authentication}/templates/concerns/recoverable.rb +21 -11
  15. data/lib/generators/propel_authentication/templates/core/configuration_methods.rb +191 -0
  16. data/lib/generators/propel_authentication/templates/db/seeds.rb +75 -0
  17. data/lib/generators/propel_authentication/templates/doc/signup_flow.md +315 -0
  18. data/lib/generators/propel_authentication/templates/models/agency.rb.tt +13 -0
  19. data/lib/generators/propel_authentication/templates/models/agent.rb.tt +13 -0
  20. data/lib/generators/{propel_auth/templates/invitation.rb → propel_authentication/templates/models/invitation.rb.tt} +8 -2
  21. data/lib/generators/propel_authentication/templates/models/organization.rb.tt +12 -0
  22. data/lib/generators/{propel_auth/templates/user.rb → propel_authentication/templates/models/user.rb.tt} +5 -0
  23. data/lib/generators/propel_authentication/templates/propel_authentication.rb.tt +218 -0
  24. data/lib/generators/propel_authentication/templates/routes/auth_routes.rb.tt +55 -0
  25. data/lib/generators/{propel_auth → propel_authentication}/templates/services/auth_notification_service.rb +3 -3
  26. data/lib/generators/{propel_auth → propel_authentication}/templates/test/concerns/confirmable_test.rb.tt +34 -10
  27. data/lib/generators/{propel_auth → propel_authentication}/templates/test/concerns/lockable_test.rb.tt +12 -12
  28. data/lib/generators/{propel_auth → propel_authentication}/templates/test/concerns/propel_authentication_test.rb.tt +2 -2
  29. data/lib/generators/{propel_auth → propel_authentication}/templates/test/concerns/recoverable_test.rb.tt +11 -11
  30. data/lib/generators/{propel_auth → propel_authentication}/templates/test/controllers/auth/lockable_integration_test.rb.tt +18 -15
  31. data/lib/generators/{propel_auth → propel_authentication}/templates/test/controllers/auth/password_reset_integration_test.rb.tt +38 -40
  32. data/lib/generators/propel_authentication/templates/test/controllers/auth/signup_controller_test.rb.tt +201 -0
  33. data/lib/generators/{propel_auth → propel_authentication}/templates/test/controllers/auth/tokens_controller_test.rb.tt +33 -25
  34. data/lib/generators/{propel_auth → propel_authentication}/templates/test/mailers/auth_mailer_test.rb.tt +51 -36
  35. data/lib/generators/{propel_auth → propel_authentication}/templates/user_test.rb.tt +1 -1
  36. data/lib/generators/{propel_auth → propel_authentication}/templates/views/auth_mailer/email_confirmation.html.erb +2 -2
  37. data/lib/generators/{propel_auth → propel_authentication}/templates/views/auth_mailer/email_confirmation.text.erb +1 -1
  38. data/lib/generators/{propel_auth → propel_authentication}/test/generators/authentication/install_generator_test.rb +4 -4
  39. data/lib/generators/{propel_auth → propel_authentication}/test/generators/authentication/uninstall_generator_test.rb +1 -1
  40. data/lib/generators/{propel_auth → propel_authentication}/test/integration/generator_integration_test.rb +1 -1
  41. data/lib/generators/{propel_auth → propel_authentication}/test/integration/multi_version_generator_test.rb +13 -12
  42. data/lib/generators/{propel_auth → propel_authentication}/unpack_generator.rb +55 -38
  43. data/lib/propel_authentication.rb +3 -0
  44. metadata +101 -98
  45. data/lib/generators/propel_auth/core/configuration_methods.rb +0 -134
  46. data/lib/generators/propel_auth/pack_generator.rb +0 -277
  47. data/lib/generators/propel_auth/templates/agency.rb +0 -7
  48. data/lib/generators/propel_auth/templates/agent.rb +0 -7
  49. data/lib/generators/propel_auth/templates/auth/base_passwords_controller.rb.tt +0 -99
  50. data/lib/generators/propel_auth/templates/auth/base_tokens_controller.rb.tt +0 -90
  51. data/lib/generators/propel_auth/templates/auth/passwords_controller.rb.tt +0 -126
  52. data/lib/generators/propel_auth/templates/db/seeds.rb +0 -29
  53. data/lib/generators/propel_auth/templates/organization.rb +0 -7
  54. data/lib/generators/propel_auth/templates/propel_auth.rb.tt +0 -141
  55. data/lib/propel_auth.rb +0 -3
  56. /data/lib/generators/{propel_auth → propel_authentication}/templates/concerns/rack_session_disable.rb +0 -0
  57. /data/lib/generators/{propel_auth → propel_authentication}/templates/config/environments/development_email.rb +0 -0
  58. /data/lib/generators/{propel_auth → propel_authentication}/templates/db/migrate/create_agencies.rb +0 -0
  59. /data/lib/generators/{propel_auth → propel_authentication}/templates/db/migrate/create_agents.rb +0 -0
  60. /data/lib/generators/{propel_auth → propel_authentication}/templates/db/migrate/create_invitations.rb +0 -0
  61. /data/lib/generators/{propel_auth → propel_authentication}/templates/db/migrate/create_organizations.rb +0 -0
  62. /data/lib/generators/{propel_auth → propel_authentication}/templates/db/migrate/create_users.rb +0 -0
  63. /data/lib/generators/{propel_auth → propel_authentication}/templates/test/mailers/previews/auth_mailer_preview.rb +0 -0
  64. /data/lib/generators/{propel_auth → propel_authentication}/templates/views/auth_mailer/account_unlock.html.erb +0 -0
  65. /data/lib/generators/{propel_auth → propel_authentication}/templates/views/auth_mailer/account_unlock.text.erb +0 -0
  66. /data/lib/generators/{propel_auth → propel_authentication}/templates/views/auth_mailer/password_reset.html.erb +0 -0
  67. /data/lib/generators/{propel_auth → propel_authentication}/templates/views/auth_mailer/password_reset.text.erb +0 -0
  68. /data/lib/generators/{propel_auth → propel_authentication}/templates/views/auth_mailer/user_invitation.html.erb +0 -0
  69. /data/lib/generators/{propel_auth → propel_authentication}/templates/views/auth_mailer/user_invitation.text.erb +0 -0
  70. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/Dockerfile +0 -0
  71. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/Gemfile +0 -0
  72. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/README.md +0 -0
  73. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/Rakefile +0 -0
  74. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/app/assets/stylesheets/application.css +0 -0
  75. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/app/controllers/application_controller.rb +0 -0
  76. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/app/helpers/application_helper.rb +0 -0
  77. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/app/jobs/application_job.rb +0 -0
  78. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/app/mailers/application_mailer.rb +0 -0
  79. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/app/models/application_record.rb +0 -0
  80. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/app/views/layouts/application.html.erb +0 -0
  81. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/app/views/layouts/mailer.html.erb +0 -0
  82. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/app/views/layouts/mailer.text.erb +0 -0
  83. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/app/views/pwa/manifest.json.erb +0 -0
  84. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/app/views/pwa/service-worker.js +0 -0
  85. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/bin/brakeman +0 -0
  86. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/bin/dev +0 -0
  87. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/bin/docker-entrypoint +0 -0
  88. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/bin/rails +0 -0
  89. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/bin/rake +0 -0
  90. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/bin/rubocop +0 -0
  91. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/bin/setup +0 -0
  92. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/bin/thrust +0 -0
  93. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/application.rb +0 -0
  94. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/boot.rb +0 -0
  95. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/cable.yml +0 -0
  96. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/credentials.yml.enc +0 -0
  97. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/database.yml +0 -0
  98. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/environment.rb +0 -0
  99. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/environments/development.rb +0 -0
  100. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/environments/production.rb +0 -0
  101. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/environments/test.rb +0 -0
  102. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/initializers/assets.rb +0 -0
  103. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/initializers/content_security_policy.rb +0 -0
  104. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/initializers/filter_parameter_logging.rb +0 -0
  105. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/initializers/inflections.rb +0 -0
  106. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/locales/en.yml +0 -0
  107. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/master.key +0 -0
  108. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/puma.rb +0 -0
  109. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/routes.rb +0 -0
  110. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config/storage.yml +0 -0
  111. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/config.ru +0 -0
  112. /data/lib/generators/{propel_auth → propel_authentication}/test/dummy/db/schema.rb +0 -0
  113. /data/lib/generators/{propel_auth → propel_authentication}/test/generators/authentication/controllers/tokens_controller_test.rb +0 -0
@@ -0,0 +1,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, PropelAuth.configuration.jwt_secret, 'HS256')
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, PropelAuth.configuration.jwt_secret, true, { algorithm: 'HS256' })[0]
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