propel_authentication 0.1.1

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 (102) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +290 -0
  4. data/Rakefile +12 -0
  5. data/lib/generators/propel_auth/install_generator.rb +486 -0
  6. data/lib/generators/propel_auth/pack_generator.rb +277 -0
  7. data/lib/generators/propel_auth/templates/agency.rb +7 -0
  8. data/lib/generators/propel_auth/templates/agent.rb +7 -0
  9. data/lib/generators/propel_auth/templates/auth/base_passwords_controller.rb.tt +99 -0
  10. data/lib/generators/propel_auth/templates/auth/base_tokens_controller.rb.tt +90 -0
  11. data/lib/generators/propel_auth/templates/auth/passwords_controller.rb.tt +126 -0
  12. data/lib/generators/propel_auth/templates/auth_mailer.rb +180 -0
  13. data/lib/generators/propel_auth/templates/authenticatable.rb +38 -0
  14. data/lib/generators/propel_auth/templates/concerns/confirmable.rb +145 -0
  15. data/lib/generators/propel_auth/templates/concerns/lockable.rb +123 -0
  16. data/lib/generators/propel_auth/templates/concerns/propel_authentication.rb +44 -0
  17. data/lib/generators/propel_auth/templates/concerns/rack_session_disable.rb +19 -0
  18. data/lib/generators/propel_auth/templates/concerns/recoverable.rb +124 -0
  19. data/lib/generators/propel_auth/templates/config/environments/development_email.rb +43 -0
  20. data/lib/generators/propel_auth/templates/db/migrate/create_agencies.rb +20 -0
  21. data/lib/generators/propel_auth/templates/db/migrate/create_agents.rb +11 -0
  22. data/lib/generators/propel_auth/templates/db/migrate/create_invitations.rb +28 -0
  23. data/lib/generators/propel_auth/templates/db/migrate/create_organizations.rb +18 -0
  24. data/lib/generators/propel_auth/templates/db/migrate/create_users.rb +43 -0
  25. data/lib/generators/propel_auth/templates/db/seeds.rb +29 -0
  26. data/lib/generators/propel_auth/templates/invitation.rb +133 -0
  27. data/lib/generators/propel_auth/templates/lib/propel_auth.rb +84 -0
  28. data/lib/generators/propel_auth/templates/organization.rb +7 -0
  29. data/lib/generators/propel_auth/templates/propel_auth.rb +132 -0
  30. data/lib/generators/propel_auth/templates/services/auth_notification_service.rb +89 -0
  31. data/lib/generators/propel_auth/templates/test/concerns/confirmable_test.rb.tt +247 -0
  32. data/lib/generators/propel_auth/templates/test/concerns/lockable_test.rb.tt +282 -0
  33. data/lib/generators/propel_auth/templates/test/concerns/propel_authentication_test.rb.tt +75 -0
  34. data/lib/generators/propel_auth/templates/test/concerns/recoverable_test.rb.tt +327 -0
  35. data/lib/generators/propel_auth/templates/test/controllers/auth/lockable_integration_test.rb.tt +196 -0
  36. data/lib/generators/propel_auth/templates/test/controllers/auth/password_reset_integration_test.rb.tt +471 -0
  37. data/lib/generators/propel_auth/templates/test/controllers/auth/tokens_controller_test.rb.tt +265 -0
  38. data/lib/generators/propel_auth/templates/test/mailers/auth_mailer_test.rb.tt +216 -0
  39. data/lib/generators/propel_auth/templates/test/mailers/previews/auth_mailer_preview.rb +161 -0
  40. data/lib/generators/propel_auth/templates/tokens_controller.rb.tt +96 -0
  41. data/lib/generators/propel_auth/templates/user.rb +21 -0
  42. data/lib/generators/propel_auth/templates/user_test.rb.tt +81 -0
  43. data/lib/generators/propel_auth/templates/views/auth_mailer/account_unlock.html.erb +213 -0
  44. data/lib/generators/propel_auth/templates/views/auth_mailer/account_unlock.text.erb +56 -0
  45. data/lib/generators/propel_auth/templates/views/auth_mailer/email_confirmation.html.erb +213 -0
  46. data/lib/generators/propel_auth/templates/views/auth_mailer/email_confirmation.text.erb +32 -0
  47. data/lib/generators/propel_auth/templates/views/auth_mailer/password_reset.html.erb +166 -0
  48. data/lib/generators/propel_auth/templates/views/auth_mailer/password_reset.text.erb +32 -0
  49. data/lib/generators/propel_auth/templates/views/auth_mailer/user_invitation.html.erb +194 -0
  50. data/lib/generators/propel_auth/templates/views/auth_mailer/user_invitation.text.erb +51 -0
  51. data/lib/generators/propel_auth/test/dummy/Dockerfile +72 -0
  52. data/lib/generators/propel_auth/test/dummy/Gemfile +63 -0
  53. data/lib/generators/propel_auth/test/dummy/Gemfile.lock +394 -0
  54. data/lib/generators/propel_auth/test/dummy/README.md +24 -0
  55. data/lib/generators/propel_auth/test/dummy/Rakefile +6 -0
  56. data/lib/generators/propel_auth/test/dummy/app/assets/stylesheets/application.css +10 -0
  57. data/lib/generators/propel_auth/test/dummy/app/controllers/application_controller.rb +4 -0
  58. data/lib/generators/propel_auth/test/dummy/app/helpers/application_helper.rb +2 -0
  59. data/lib/generators/propel_auth/test/dummy/app/jobs/application_job.rb +7 -0
  60. data/lib/generators/propel_auth/test/dummy/app/mailers/application_mailer.rb +4 -0
  61. data/lib/generators/propel_auth/test/dummy/app/models/application_record.rb +3 -0
  62. data/lib/generators/propel_auth/test/dummy/app/views/layouts/application.html.erb +27 -0
  63. data/lib/generators/propel_auth/test/dummy/app/views/layouts/mailer.html.erb +13 -0
  64. data/lib/generators/propel_auth/test/dummy/app/views/layouts/mailer.text.erb +1 -0
  65. data/lib/generators/propel_auth/test/dummy/app/views/pwa/manifest.json.erb +22 -0
  66. data/lib/generators/propel_auth/test/dummy/app/views/pwa/service-worker.js +26 -0
  67. data/lib/generators/propel_auth/test/dummy/bin/brakeman +7 -0
  68. data/lib/generators/propel_auth/test/dummy/bin/dev +2 -0
  69. data/lib/generators/propel_auth/test/dummy/bin/docker-entrypoint +14 -0
  70. data/lib/generators/propel_auth/test/dummy/bin/rails +4 -0
  71. data/lib/generators/propel_auth/test/dummy/bin/rake +4 -0
  72. data/lib/generators/propel_auth/test/dummy/bin/rubocop +8 -0
  73. data/lib/generators/propel_auth/test/dummy/bin/setup +34 -0
  74. data/lib/generators/propel_auth/test/dummy/bin/thrust +5 -0
  75. data/lib/generators/propel_auth/test/dummy/config/application.rb +42 -0
  76. data/lib/generators/propel_auth/test/dummy/config/boot.rb +4 -0
  77. data/lib/generators/propel_auth/test/dummy/config/cable.yml +10 -0
  78. data/lib/generators/propel_auth/test/dummy/config/credentials.yml.enc +1 -0
  79. data/lib/generators/propel_auth/test/dummy/config/database.yml +41 -0
  80. data/lib/generators/propel_auth/test/dummy/config/environment.rb +5 -0
  81. data/lib/generators/propel_auth/test/dummy/config/environments/development.rb +72 -0
  82. data/lib/generators/propel_auth/test/dummy/config/environments/production.rb +89 -0
  83. data/lib/generators/propel_auth/test/dummy/config/environments/test.rb +53 -0
  84. data/lib/generators/propel_auth/test/dummy/config/initializers/assets.rb +10 -0
  85. data/lib/generators/propel_auth/test/dummy/config/initializers/content_security_policy.rb +25 -0
  86. data/lib/generators/propel_auth/test/dummy/config/initializers/filter_parameter_logging.rb +8 -0
  87. data/lib/generators/propel_auth/test/dummy/config/initializers/inflections.rb +16 -0
  88. data/lib/generators/propel_auth/test/dummy/config/locales/en.yml +31 -0
  89. data/lib/generators/propel_auth/test/dummy/config/master.key +1 -0
  90. data/lib/generators/propel_auth/test/dummy/config/puma.rb +41 -0
  91. data/lib/generators/propel_auth/test/dummy/config/routes.rb +2 -0
  92. data/lib/generators/propel_auth/test/dummy/config/storage.yml +34 -0
  93. data/lib/generators/propel_auth/test/dummy/config.ru +6 -0
  94. data/lib/generators/propel_auth/test/dummy/db/schema.rb +14 -0
  95. data/lib/generators/propel_auth/test/generators/authentication/controllers/tokens_controller_test.rb +230 -0
  96. data/lib/generators/propel_auth/test/generators/authentication/install_generator_test.rb +490 -0
  97. data/lib/generators/propel_auth/test/generators/authentication/uninstall_generator_test.rb +408 -0
  98. data/lib/generators/propel_auth/test/integration/generator_integration_test.rb +158 -0
  99. data/lib/generators/propel_auth/test/integration/multi_version_generator_test.rb +125 -0
  100. data/lib/generators/propel_auth/unpack_generator.rb +345 -0
  101. data/lib/propel_auth.rb +3 -0
  102. metadata +195 -0
@@ -0,0 +1,277 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##
4
+ # PropelAuth packer that bundles customized authentication code back into gem format
5
+ #
6
+ # Usage:
7
+ # rails generate propel_auth:pack # Pack all customized code
8
+ # rails generate propel_auth:pack --output-dir=../gems # Pack to specific directory
9
+ # rails generate propel_auth:pack --version=0.2.0 # Pack with custom version
10
+ #
11
+ # This bundles your customized PropelAuth generator and templates back into a
12
+ # distributable gem format for sharing across projects or organizations.
13
+ #
14
+ class PropelAuth::PackGenerator < Rails::Generators::Base
15
+ desc "Pack customized PropelAuth code back into distributable gem format"
16
+
17
+ class_option :output_dir,
18
+ type: :string,
19
+ default: "tmp/propel_auth_custom",
20
+ desc: "Output directory for packed gem"
21
+
22
+ class_option :version,
23
+ type: :string,
24
+ default: "0.1.0-custom",
25
+ desc: "Version for the packed gem"
26
+
27
+ class_option :gem_name,
28
+ type: :string,
29
+ default: "propel_auth_custom",
30
+ desc: "Name for the packed gem"
31
+
32
+ def pack_propel_auth_generator
33
+ @output_dir = options[:output_dir]
34
+ @version = options[:version]
35
+ @gem_name = options[:gem_name]
36
+
37
+ show_welcome_message
38
+ validate_source_files
39
+ create_gem_structure
40
+ copy_generator_logic
41
+ copy_templates
42
+ create_gemspec
43
+ create_readme
44
+ show_completion_message
45
+ end
46
+
47
+ private
48
+
49
+ def show_welcome_message
50
+ say ""
51
+ say "╔══════════════════════════════════════╗", :cyan
52
+ say "║ PACK PROPEL AUTH GENERATOR ║", :cyan
53
+ say "╚══════════════════════════════════════╝", :cyan
54
+ say ""
55
+ say "Packing your customized PropelAuth code into distributable gem format", :blue
56
+ say ""
57
+ say "📁 FROM: lib/generators/propel_auth/ (customized)", :yellow
58
+ say "📦 TO: #{@output_dir}/ (distributable gem)", :green
59
+ say ""
60
+ end
61
+
62
+ def validate_source_files
63
+ source_path = "lib/generators/propel_auth"
64
+
65
+ unless File.exist?(source_path)
66
+ raise Thor::Error, "❌ No customized PropelAuth generator found at #{source_path}. Run 'rails generate propel_auth:unpack' first."
67
+ end
68
+
69
+ required_files = %w[install_generator.rb]
70
+ missing_files = required_files.reject { |file| File.exist?(File.join(source_path, file)) }
71
+
72
+ if missing_files.any?
73
+ raise Thor::Error, "❌ Missing required files: #{missing_files.join(', ')}"
74
+ end
75
+
76
+ say "✅ Source files validated", :green
77
+ end
78
+
79
+ def create_gem_structure
80
+ FileUtils.mkdir_p(File.join(@output_dir, "lib", @gem_name))
81
+ FileUtils.mkdir_p(File.join(@output_dir, "lib", "generators", @gem_name))
82
+ say "📁 Created gem directory structure", :blue
83
+ end
84
+
85
+ def copy_generator_logic
86
+ source_path = "lib/generators/propel_auth"
87
+ dest_path = File.join(@output_dir, "lib", "generators", @gem_name)
88
+
89
+ # Copy install generator
90
+ if File.exist?(File.join(source_path, "install_generator.rb"))
91
+ source_content = File.read(File.join(source_path, "install_generator.rb"))
92
+
93
+ # Update module name for custom gem
94
+ modified_content = source_content.gsub(
95
+ /^module PropelAuth$/,
96
+ "module #{@gem_name.camelize}"
97
+ )
98
+
99
+ File.write(File.join(dest_path, "install_generator.rb"), modified_content)
100
+ say " ✅ Install generator packed", :green
101
+ end
102
+
103
+ # Create unpack generator for the custom gem
104
+ create_unpack_generator(dest_path)
105
+ end
106
+
107
+ def copy_templates
108
+ source_templates = "lib/generators/propel_auth/templates"
109
+ dest_templates = File.join(@output_dir, "lib", "generators", @gem_name, "templates")
110
+
111
+ if Dir.exist?(source_templates)
112
+ FileUtils.cp_r(source_templates, dest_templates)
113
+ say " ✅ Templates packed", :green
114
+ end
115
+ end
116
+
117
+ def create_gemspec
118
+ gemspec_content = <<~GEMSPEC
119
+ # frozen_string_literal: true
120
+
121
+ require_relative "lib/#{@gem_name}/version"
122
+
123
+ Gem::Specification.new do |spec|
124
+ spec.name = "#{@gem_name}"
125
+ spec.version = #{@gem_name.camelize}::VERSION
126
+ spec.authors = ["#{`git config user.name`.strip}"]
127
+ spec.email = ["#{`git config user.email`.strip}"]
128
+
129
+ spec.summary = "Customized PropelAuth authentication system"
130
+ spec.description = "Self-extracting authentication gem based on PropelAuth with custom modifications"
131
+ spec.homepage = "https://github.com/yourorg/#{@gem_name}"
132
+ spec.license = "MIT"
133
+
134
+ spec.metadata["homepage_uri"] = spec.homepage
135
+ spec.metadata["source_code_uri"] = spec.homepage
136
+ spec.metadata["changelog_uri"] = "\#{spec.homepage}/blob/main/CHANGELOG.md"
137
+
138
+ spec.files = Dir.chdir(__dir__) do
139
+ `git ls-files -z`.split("\\x0").reject do |f|
140
+ (File.expand_path(f) == __FILE__) ||
141
+ f.start_with?(*%w[bin/ test/ spec/ features/ .git .github appveyor Gemfile])
142
+ end
143
+ end
144
+
145
+ spec.bindir = "exe"
146
+ spec.executables = spec.files.grep(%r{\\Aexe/}) { |f| File.basename(f) }
147
+ spec.require_paths = ["lib"]
148
+
149
+ spec.add_dependency "rails", ">= 7.0"
150
+ spec.add_development_dependency "rake", "~> 13.0"
151
+ end
152
+ GEMSPEC
153
+
154
+ File.write(File.join(@output_dir, "#{@gem_name}.gemspec"), gemspec_content)
155
+
156
+ # Create version file
157
+ version_content = <<~VERSION
158
+ # frozen_string_literal: true
159
+
160
+ module #{@gem_name.camelize}
161
+ VERSION = "#{@version}"
162
+ end
163
+ VERSION
164
+
165
+ FileUtils.mkdir_p(File.join(@output_dir, "lib", @gem_name))
166
+ File.write(File.join(@output_dir, "lib", @gem_name, "version.rb"), version_content)
167
+
168
+ # Create main gem file
169
+ main_file_content = <<~MAIN
170
+ # frozen_string_literal: true
171
+
172
+ require_relative "#{@gem_name}/version"
173
+
174
+ module #{@gem_name.camelize}
175
+ # Self-extracting gem - no runtime code here
176
+ # All functionality is copied to host app during installation
177
+ end
178
+ MAIN
179
+
180
+ File.write(File.join(@output_dir, "lib", "#{@gem_name}.rb"), main_file_content)
181
+
182
+ say " ✅ Gemspec and gem files created", :green
183
+ end
184
+
185
+ def create_unpack_generator(dest_path)
186
+ unpack_content = <<~UNPACK
187
+ # frozen_string_literal: true
188
+
189
+ class #{@gem_name.camelize}::UnpackGenerator < Rails::Generators::Base
190
+ source_root File.expand_path("templates", __dir__)
191
+
192
+ desc "Extract #{@gem_name} generator to lib/generators/#{@gem_name}/ for customization"
193
+
194
+ def unpack_generator
195
+ destination_path = "lib/generators/#{@gem_name}"
196
+
197
+ if File.exist?(destination_path) && !options[:force]
198
+ say "⚠️ \#{destination_path} already exists (use --force to overwrite)", :yellow
199
+ return
200
+ end
201
+
202
+ say "📦 Extracting #{@gem_name} generator...", :green
203
+
204
+ # Copy install generator
205
+ copy_file "install_generator.rb", "\#{destination_path}/install_generator.rb"
206
+
207
+ # Copy all templates
208
+ directory ".", "\#{destination_path}/templates"
209
+
210
+ say "✅ #{@gem_name} generator extracted to \#{destination_path}/", :green
211
+ end
212
+ end
213
+ UNPACK
214
+
215
+ File.write(File.join(dest_path, "unpack_generator.rb"), unpack_content)
216
+ end
217
+
218
+ def create_readme
219
+ readme_content = <<~README
220
+ # #{@gem_name.humanize}
221
+
222
+ Custom authentication gem based on PropelAuth with organization-specific modifications.
223
+
224
+ ## Installation
225
+
226
+ Add this line to your application's Gemfile:
227
+
228
+ ```ruby
229
+ gem '#{@gem_name}'
230
+ ```
231
+
232
+ ## Usage
233
+
234
+ ### Install (One-time setup)
235
+ ```bash
236
+ rails generate #{@gem_name}:install
237
+ ```
238
+
239
+ ### Unpack for customization
240
+ ```bash
241
+ rails generate #{@gem_name}:unpack
242
+ ```
243
+
244
+ ## Development
245
+
246
+ This gem was created by packing a customized PropelAuth installation.
247
+
248
+ ## License
249
+
250
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
251
+ README
252
+
253
+ File.write(File.join(@output_dir, "README.md"), readme_content)
254
+ say " ✅ README created", :green
255
+ end
256
+
257
+ def show_completion_message
258
+ say ""
259
+ say "🎉 PropelAuth PACKED successfully!", :green
260
+ say ""
261
+ say "📦 Custom gem created at: #{@output_dir}/", :bold
262
+ say ""
263
+
264
+ say "🚀 Next steps:", :yellow
265
+ say " 1. cd #{@output_dir}"
266
+ say " 2. gem build #{@gem_name}.gemspec"
267
+ say " 3. gem install #{@gem_name}-#{@version}.gem"
268
+ say " 4. Use in other projects: gem '#{@gem_name}'"
269
+ say ""
270
+
271
+ say "💡 Your custom gem includes:", :blue
272
+ say " • Your customized install generator"
273
+ say " • All your modified templates"
274
+ say " • Unpack generator for further customization"
275
+ say " • Self-extracting architecture (no runtime dependencies)"
276
+ end
277
+ end
@@ -0,0 +1,7 @@
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
+ end
@@ -0,0 +1,7 @@
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
+ end
@@ -0,0 +1,99 @@
1
+ class Api::Auth::BasePasswordsController < ApplicationController
2
+ <%- unless api_only_app? -%>
3
+ include RackSessionDisable
4
+ <%- end -%>
5
+ include PropelAuthentication
6
+
7
+ # POST /api/{version}/auth/reset
8
+ def create
9
+ user = User.find_by(email_address: params[:email_address])
10
+
11
+ if user
12
+ # Generate and save reset token
13
+ user.generate_password_reset_token!
14
+
15
+ # Send password reset email
16
+ AuthNotificationService.send_password_reset_email(user)
17
+
18
+ render json: {
19
+ message: 'Password reset instructions sent to your email'
20
+ }, status: :ok
21
+ else
22
+ # Don't reveal whether user exists for security
23
+ render json: {
24
+ message: 'Password reset instructions sent to your email'
25
+ }, status: :ok
26
+ end
27
+ rescue => e
28
+ render json: {
29
+ error: 'Unable to process password reset request'
30
+ }, status: :unprocessable_entity
31
+ end
32
+
33
+ # GET /api/{version}/auth/reset?token=xyz
34
+ def show
35
+ token = params[:token]
36
+
37
+ if token.blank?
38
+ render json: { error: 'Reset token is required' }, status: :unprocessable_entity
39
+ return
40
+ end
41
+
42
+ user = User.find_by_password_reset_token(token)
43
+
44
+ if user&.password_reset_token_valid?
45
+ render json: {
46
+ message: 'Token is valid',
47
+ token: token
48
+ }, status: :ok
49
+ else
50
+ render json: {
51
+ error: 'Invalid or expired reset token'
52
+ }, status: :unauthorized
53
+ end
54
+ end
55
+
56
+ # PATCH /api/{version}/auth/reset
57
+ def update
58
+ token = params[:token]
59
+ new_password = params[:password]
60
+ password_confirmation = params[:password_confirmation]
61
+
62
+ if token.blank? || new_password.blank?
63
+ render json: {
64
+ error: 'Token and password are required'
65
+ }, status: :unprocessable_entity
66
+ return
67
+ end
68
+
69
+ if new_password != password_confirmation
70
+ render json: {
71
+ error: 'Password confirmation does not match'
72
+ }, status: :unprocessable_entity
73
+ return
74
+ end
75
+
76
+ user = User.find_by_password_reset_token(token)
77
+
78
+ if user&.password_reset_token_valid?
79
+ if user.reset_password!(new_password)
80
+ render json: {
81
+ message: 'Password successfully reset'
82
+ }, status: :ok
83
+ else
84
+ render json: {
85
+ error: 'Password could not be reset',
86
+ details: user.errors.full_messages
87
+ }, status: :unprocessable_entity
88
+ end
89
+ else
90
+ render json: {
91
+ error: 'Invalid or expired reset token'
92
+ }, status: :unauthorized
93
+ end
94
+ rescue => e
95
+ render json: {
96
+ error: 'Unable to reset password'
97
+ }, status: :unprocessable_entity
98
+ end
99
+ end
@@ -0,0 +1,90 @@
1
+ class Api::Auth::BaseTokensController < ApplicationController
2
+ <%- unless api_only_app? -%>
3
+ include RackSessionDisable
4
+ <%- end -%>
5
+ include PropelAuthentication
6
+
7
+ # POST /api/{version}/auth/login
8
+ def create
9
+ user = User.find_by(email_address: params[:user][:email_address])
10
+
11
+ if user&.authenticate(params[:user][:password])
12
+ if user.status == 1 # inactive
13
+ render json: { error: 'Account is inactive' }, status: :unauthorized
14
+ return
15
+ end
16
+
17
+ # Update last login timestamp
18
+ user.update(last_login_at: Time.current)
19
+
20
+ # Reset failed login attempts on successful login
21
+ user.reset_failed_attempts! if user.respond_to?(:reset_failed_attempts!)
22
+
23
+ render json: {
24
+ token: user.generate_jwt_token,
25
+ user: user_response(user)
26
+ }, status: :ok
27
+ else
28
+ # Increment failed login attempts if user exists and has lockable functionality
29
+ if user&.respond_to?(:increment_failed_attempts!)
30
+ user.increment_failed_attempts!
31
+ end
32
+
33
+ render json: { error: 'Invalid credentials' }, status: :unauthorized
34
+ end
35
+ rescue ActionController::ParameterMissing
36
+ render json: { error: 'Missing required parameters' }, status: :unprocessable_entity
37
+ end
38
+
39
+ # DELETE /api/{version}/auth/logout
40
+ def destroy
41
+ # For JWT, logout is handled client-side by removing the token
42
+ # Server-side logout would require token blacklisting (future enhancement)
43
+ render json: { message: 'Logged out successfully' }, status: :ok
44
+ end
45
+
46
+ # GET /api/{version}/auth/me
47
+ def me
48
+ return unless authenticate_user
49
+ render json: { user: user_response(current_user) }, status: :ok
50
+ end
51
+
52
+ def refresh
53
+ render json: { token: @current_user.generate_jwt_token }
54
+ end
55
+
56
+ # POST /api/{version}/auth/unlock
57
+ def unlock
58
+ token = params[:token]
59
+
60
+ if token.blank?
61
+ render json: { error: 'Token is required' }, status: :unprocessable_entity
62
+ return
63
+ end
64
+
65
+ user = User.find_by_unlock_token(token)
66
+
67
+ if user
68
+ user.unlock_account!
69
+ render json: { message: 'Account unlocked successfully' }, status: :ok
70
+ else
71
+ render json: { error: 'Invalid or expired unlock token' }, status: :unauthorized
72
+ end
73
+ rescue => e
74
+ render json: { error: 'Invalid unlock token' }, status: :unauthorized
75
+ end
76
+
77
+ private
78
+
79
+ def user_response(user)
80
+ {
81
+ id: user.id,
82
+ email_address: user.email_address,
83
+ username: user.username,
84
+ first_name: user.first_name,
85
+ last_name: user.last_name,
86
+ organization_id: user.organization_id,
87
+ last_login_at: user.last_login_at
88
+ }
89
+ end
90
+ end
@@ -0,0 +1,126 @@
1
+ <%- if api_versioned? -%>
2
+ # Dynamic API versioning controller - inherits from base controller
3
+ class <%= controller_namespace %>::PasswordsController < Api::Auth::BasePasswordsController
4
+ end
5
+ <%- else -%>
6
+ class <%= controller_namespace %>::PasswordsController < ApplicationController
7
+
8
+ # POST <%= api_versioned? ? "/#{api_namespace}" : "" %>/auth/reset - Request password reset
9
+ def create
10
+ email_address = params[:email_address]
11
+
12
+ # Validate email_address parameter
13
+ if email_address.blank?
14
+ return render json: { error: "Email address is required" }, status: :unprocessable_entity
15
+ end
16
+
17
+ # Validate email_address format
18
+ unless email_address.match?(/\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i)
19
+ return render json: { error: "Email address format is invalid" }, status: :unprocessable_entity
20
+ end
21
+
22
+ # For security, always return the same response whether user exists or not
23
+ # This prevents user enumeration attacks
24
+ response_message = "If an account with that email address exists, password reset instructions have been sent to your email"
25
+
26
+ # Find user by email_address and send reset email if user exists
27
+ user = User.find_by(email_address: email_address)
28
+
29
+ if user && PropelAuth.configuration.enable_email_notifications
30
+ # Send password reset email using the notification service
31
+ email_result = AuthNotificationService.send_password_reset_email(user)
32
+
33
+ # Log the result but don't expose it to prevent user enumeration
34
+ if email_result[:success]
35
+ Rails.logger.info "Password reset email sent successfully to #{email_address}"
36
+ else
37
+ Rails.logger.error "Failed to send password reset email to #{email_address}: #{email_result[:error]}"
38
+ end
39
+ end
40
+
41
+ # Always return the same response for security
42
+ render json: { message: response_message }, status: :ok
43
+ end
44
+
45
+ # PUT <%= api_versioned? ? "/#{api_namespace}" : "" %>/auth/reset - Confirm password reset with token
46
+ def update
47
+ token = params[:token]
48
+ password = params[:password]
49
+ password_confirmation = params[:password_confirmation]
50
+
51
+ # Validate required parameters
52
+ if token.blank?
53
+ return render json: { error: "Reset token is required" }, status: :unprocessable_entity
54
+ end
55
+
56
+ if password.blank?
57
+ return render json: { error: "Password is required" }, status: :unprocessable_entity
58
+ end
59
+
60
+ if password_confirmation.blank?
61
+ return render json: { error: "Password confirmation is required" }, status: :unprocessable_entity
62
+ end
63
+
64
+ # Validate password confirmation
65
+ if password != password_confirmation
66
+ return render json: { error: "Password and confirmation do not match" }, status: :unprocessable_entity
67
+ end
68
+
69
+ # Find user by token
70
+ user = User.find_user_by_password_reset_token(token)
71
+
72
+ unless user
73
+ return render json: { error: "Invalid or expired reset token" }, status: :unauthorized
74
+ end
75
+
76
+ # Attempt password reset
77
+ if user.reset_password_with_token!(token, password, password_confirmation)
78
+ render json: {
79
+ message: "Password has been reset successfully",
80
+ user: {
81
+ id: user.id,
82
+ email_address: user.email_address,
83
+ username: user.username
84
+ }
85
+ }, status: :ok
86
+ else
87
+ render json: { error: "Password is too short (minimum is 8 characters)" }, status: :unprocessable_entity
88
+ end
89
+ end
90
+
91
+ # GET <%= api_versioned? ? "/#{api_namespace}" : "" %>/auth/reset - Verify reset token
92
+ def show
93
+ token = params[:token]
94
+
95
+ # Validate token parameter
96
+ if token.blank?
97
+ return render json: { error: "Token parameter is required" }, status: :unprocessable_entity
98
+ end
99
+
100
+ # Find user by token
101
+ user = User.find_user_by_password_reset_token(token)
102
+
103
+ if user
104
+ render json: {
105
+ valid: true,
106
+ user: {
107
+ id: user.id,
108
+ email_address: user.email_address,
109
+ username: user.username
110
+ }
111
+ }, status: :ok
112
+ else
113
+ render json: {
114
+ valid: false,
115
+ error: "Invalid or expired reset token"
116
+ }, status: :unauthorized
117
+ end
118
+ end
119
+
120
+ private
121
+
122
+ def password_reset_params
123
+ params.permit(:email_address, :token, :password, :password_confirmation)
124
+ end
125
+ end
126
+ <%- end -%>