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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +290 -0
- data/Rakefile +12 -0
- data/lib/generators/propel_auth/install_generator.rb +486 -0
- data/lib/generators/propel_auth/pack_generator.rb +277 -0
- data/lib/generators/propel_auth/templates/agency.rb +7 -0
- data/lib/generators/propel_auth/templates/agent.rb +7 -0
- data/lib/generators/propel_auth/templates/auth/base_passwords_controller.rb.tt +99 -0
- data/lib/generators/propel_auth/templates/auth/base_tokens_controller.rb.tt +90 -0
- data/lib/generators/propel_auth/templates/auth/passwords_controller.rb.tt +126 -0
- data/lib/generators/propel_auth/templates/auth_mailer.rb +180 -0
- data/lib/generators/propel_auth/templates/authenticatable.rb +38 -0
- data/lib/generators/propel_auth/templates/concerns/confirmable.rb +145 -0
- data/lib/generators/propel_auth/templates/concerns/lockable.rb +123 -0
- data/lib/generators/propel_auth/templates/concerns/propel_authentication.rb +44 -0
- data/lib/generators/propel_auth/templates/concerns/rack_session_disable.rb +19 -0
- data/lib/generators/propel_auth/templates/concerns/recoverable.rb +124 -0
- data/lib/generators/propel_auth/templates/config/environments/development_email.rb +43 -0
- data/lib/generators/propel_auth/templates/db/migrate/create_agencies.rb +20 -0
- data/lib/generators/propel_auth/templates/db/migrate/create_agents.rb +11 -0
- data/lib/generators/propel_auth/templates/db/migrate/create_invitations.rb +28 -0
- data/lib/generators/propel_auth/templates/db/migrate/create_organizations.rb +18 -0
- data/lib/generators/propel_auth/templates/db/migrate/create_users.rb +43 -0
- data/lib/generators/propel_auth/templates/db/seeds.rb +29 -0
- data/lib/generators/propel_auth/templates/invitation.rb +133 -0
- data/lib/generators/propel_auth/templates/lib/propel_auth.rb +84 -0
- data/lib/generators/propel_auth/templates/organization.rb +7 -0
- data/lib/generators/propel_auth/templates/propel_auth.rb +132 -0
- data/lib/generators/propel_auth/templates/services/auth_notification_service.rb +89 -0
- data/lib/generators/propel_auth/templates/test/concerns/confirmable_test.rb.tt +247 -0
- data/lib/generators/propel_auth/templates/test/concerns/lockable_test.rb.tt +282 -0
- data/lib/generators/propel_auth/templates/test/concerns/propel_authentication_test.rb.tt +75 -0
- data/lib/generators/propel_auth/templates/test/concerns/recoverable_test.rb.tt +327 -0
- data/lib/generators/propel_auth/templates/test/controllers/auth/lockable_integration_test.rb.tt +196 -0
- data/lib/generators/propel_auth/templates/test/controllers/auth/password_reset_integration_test.rb.tt +471 -0
- data/lib/generators/propel_auth/templates/test/controllers/auth/tokens_controller_test.rb.tt +265 -0
- data/lib/generators/propel_auth/templates/test/mailers/auth_mailer_test.rb.tt +216 -0
- data/lib/generators/propel_auth/templates/test/mailers/previews/auth_mailer_preview.rb +161 -0
- data/lib/generators/propel_auth/templates/tokens_controller.rb.tt +96 -0
- data/lib/generators/propel_auth/templates/user.rb +21 -0
- data/lib/generators/propel_auth/templates/user_test.rb.tt +81 -0
- data/lib/generators/propel_auth/templates/views/auth_mailer/account_unlock.html.erb +213 -0
- data/lib/generators/propel_auth/templates/views/auth_mailer/account_unlock.text.erb +56 -0
- data/lib/generators/propel_auth/templates/views/auth_mailer/email_confirmation.html.erb +213 -0
- data/lib/generators/propel_auth/templates/views/auth_mailer/email_confirmation.text.erb +32 -0
- data/lib/generators/propel_auth/templates/views/auth_mailer/password_reset.html.erb +166 -0
- data/lib/generators/propel_auth/templates/views/auth_mailer/password_reset.text.erb +32 -0
- data/lib/generators/propel_auth/templates/views/auth_mailer/user_invitation.html.erb +194 -0
- data/lib/generators/propel_auth/templates/views/auth_mailer/user_invitation.text.erb +51 -0
- data/lib/generators/propel_auth/test/dummy/Dockerfile +72 -0
- data/lib/generators/propel_auth/test/dummy/Gemfile +63 -0
- data/lib/generators/propel_auth/test/dummy/Gemfile.lock +394 -0
- data/lib/generators/propel_auth/test/dummy/README.md +24 -0
- data/lib/generators/propel_auth/test/dummy/Rakefile +6 -0
- data/lib/generators/propel_auth/test/dummy/app/assets/stylesheets/application.css +10 -0
- data/lib/generators/propel_auth/test/dummy/app/controllers/application_controller.rb +4 -0
- data/lib/generators/propel_auth/test/dummy/app/helpers/application_helper.rb +2 -0
- data/lib/generators/propel_auth/test/dummy/app/jobs/application_job.rb +7 -0
- data/lib/generators/propel_auth/test/dummy/app/mailers/application_mailer.rb +4 -0
- data/lib/generators/propel_auth/test/dummy/app/models/application_record.rb +3 -0
- data/lib/generators/propel_auth/test/dummy/app/views/layouts/application.html.erb +27 -0
- data/lib/generators/propel_auth/test/dummy/app/views/layouts/mailer.html.erb +13 -0
- data/lib/generators/propel_auth/test/dummy/app/views/layouts/mailer.text.erb +1 -0
- data/lib/generators/propel_auth/test/dummy/app/views/pwa/manifest.json.erb +22 -0
- data/lib/generators/propel_auth/test/dummy/app/views/pwa/service-worker.js +26 -0
- data/lib/generators/propel_auth/test/dummy/bin/brakeman +7 -0
- data/lib/generators/propel_auth/test/dummy/bin/dev +2 -0
- data/lib/generators/propel_auth/test/dummy/bin/docker-entrypoint +14 -0
- data/lib/generators/propel_auth/test/dummy/bin/rails +4 -0
- data/lib/generators/propel_auth/test/dummy/bin/rake +4 -0
- data/lib/generators/propel_auth/test/dummy/bin/rubocop +8 -0
- data/lib/generators/propel_auth/test/dummy/bin/setup +34 -0
- data/lib/generators/propel_auth/test/dummy/bin/thrust +5 -0
- data/lib/generators/propel_auth/test/dummy/config/application.rb +42 -0
- data/lib/generators/propel_auth/test/dummy/config/boot.rb +4 -0
- data/lib/generators/propel_auth/test/dummy/config/cable.yml +10 -0
- data/lib/generators/propel_auth/test/dummy/config/credentials.yml.enc +1 -0
- data/lib/generators/propel_auth/test/dummy/config/database.yml +41 -0
- data/lib/generators/propel_auth/test/dummy/config/environment.rb +5 -0
- data/lib/generators/propel_auth/test/dummy/config/environments/development.rb +72 -0
- data/lib/generators/propel_auth/test/dummy/config/environments/production.rb +89 -0
- data/lib/generators/propel_auth/test/dummy/config/environments/test.rb +53 -0
- data/lib/generators/propel_auth/test/dummy/config/initializers/assets.rb +10 -0
- data/lib/generators/propel_auth/test/dummy/config/initializers/content_security_policy.rb +25 -0
- data/lib/generators/propel_auth/test/dummy/config/initializers/filter_parameter_logging.rb +8 -0
- data/lib/generators/propel_auth/test/dummy/config/initializers/inflections.rb +16 -0
- data/lib/generators/propel_auth/test/dummy/config/locales/en.yml +31 -0
- data/lib/generators/propel_auth/test/dummy/config/master.key +1 -0
- data/lib/generators/propel_auth/test/dummy/config/puma.rb +41 -0
- data/lib/generators/propel_auth/test/dummy/config/routes.rb +2 -0
- data/lib/generators/propel_auth/test/dummy/config/storage.yml +34 -0
- data/lib/generators/propel_auth/test/dummy/config.ru +6 -0
- data/lib/generators/propel_auth/test/dummy/db/schema.rb +14 -0
- data/lib/generators/propel_auth/test/generators/authentication/controllers/tokens_controller_test.rb +230 -0
- data/lib/generators/propel_auth/test/generators/authentication/install_generator_test.rb +490 -0
- data/lib/generators/propel_auth/test/generators/authentication/uninstall_generator_test.rb +408 -0
- data/lib/generators/propel_auth/test/integration/generator_integration_test.rb +158 -0
- data/lib/generators/propel_auth/test/integration/multi_version_generator_test.rb +125 -0
- data/lib/generators/propel_auth/unpack_generator.rb +345 -0
- data/lib/propel_auth.rb +3 -0
- 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,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 -%>
|