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,43 @@
|
|
1
|
+
# Email configuration for development environment
|
2
|
+
# This file should be included in config/environments/development.rb
|
3
|
+
|
4
|
+
# Configure Action Mailer for development
|
5
|
+
config.action_mailer.delivery_method = :letter_opener
|
6
|
+
config.action_mailer.perform_deliveries = true
|
7
|
+
config.action_mailer.raise_delivery_errors = true
|
8
|
+
config.action_mailer.default_options = { from: 'development@localhost' }
|
9
|
+
|
10
|
+
# Configure Letter Opener to open emails in browser
|
11
|
+
config.action_mailer.letter_opener_settings = {
|
12
|
+
location: Rails.root.join('tmp', 'letter_opener')
|
13
|
+
}
|
14
|
+
|
15
|
+
# URL options for email links
|
16
|
+
config.action_mailer.default_url_options = {
|
17
|
+
host: 'localhost',
|
18
|
+
port: 3000,
|
19
|
+
protocol: 'http'
|
20
|
+
}
|
21
|
+
|
22
|
+
# Enable email previews
|
23
|
+
config.action_mailer.show_previews = true
|
24
|
+
config.action_mailer.preview_path = Rails.root.join('test', 'mailers', 'previews')
|
25
|
+
|
26
|
+
# Log email delivery
|
27
|
+
config.action_mailer.logger = Logger.new(STDOUT)
|
28
|
+
config.action_mailer.log_level = :debug
|
29
|
+
|
30
|
+
# SMTP settings for development (if you prefer real email delivery)
|
31
|
+
# Uncomment and configure for services like Mailhog, MailCatcher, etc.
|
32
|
+
# config.action_mailer.delivery_method = :smtp
|
33
|
+
# config.action_mailer.smtp_settings = {
|
34
|
+
# address: 'localhost',
|
35
|
+
# port: 1025,
|
36
|
+
# domain: 'localhost',
|
37
|
+
# authentication: :plain,
|
38
|
+
# enable_starttls_auto: false
|
39
|
+
# }
|
40
|
+
|
41
|
+
puts "📧 Email development configuration loaded!"
|
42
|
+
puts "🔧 Letter Opener will open emails in your browser"
|
43
|
+
puts "👀 Email previews available at: http://localhost:3000/rails/mailers"
|
@@ -0,0 +1,20 @@
|
|
1
|
+
class CreateAgencies < ActiveRecord::Migration[8.0]
|
2
|
+
def change
|
3
|
+
create_table :agencies do |t|
|
4
|
+
t.string :name, null: false
|
5
|
+
t.references :organization, null: false, foreign_key: true
|
6
|
+
t.string :phone_number
|
7
|
+
t.string :address
|
8
|
+
t.string :time_zone
|
9
|
+
if ActiveRecord::Base.connection.adapter_name.downcase.starts_with?('postgresql')
|
10
|
+
t.jsonb :meta, default: {}
|
11
|
+
t.jsonb :settings, default: {}
|
12
|
+
else
|
13
|
+
t.json :meta, default: {}
|
14
|
+
t.json :settings, default: {}
|
15
|
+
end
|
16
|
+
|
17
|
+
t.timestamps
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
class CreateAgents < ActiveRecord::Migration[8.0]
|
2
|
+
def change
|
3
|
+
create_table :agents do |t|
|
4
|
+
t.references :user, null: false, foreign_key: true
|
5
|
+
t.references :agency, null: false, foreign_key: true
|
6
|
+
t.string :role, null: false, default: 'agent'
|
7
|
+
t.string :title
|
8
|
+
t.timestamps
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
class CreateInvitations < ActiveRecord::Migration[8.0]
|
2
|
+
def change
|
3
|
+
create_table :invitations do |t|
|
4
|
+
# Multi-tenant associations
|
5
|
+
t.references :organization, null: false, foreign_key: true
|
6
|
+
t.references :inviter, null: false, foreign_key: { to_table: :users }
|
7
|
+
|
8
|
+
# Rails 8 compatible email and user info fields
|
9
|
+
t.string :email_address, null: false
|
10
|
+
t.string :first_name, null: false
|
11
|
+
t.string :last_name, null: false
|
12
|
+
|
13
|
+
# Invitation status tracking (enum: pending, accepted, expired, revoked)
|
14
|
+
t.integer :status, default: 0, null: false
|
15
|
+
|
16
|
+
# Acceptance tracking
|
17
|
+
t.datetime :accepted_at
|
18
|
+
t.references :accepted_user, foreign_key: { to_table: :users }, null: true
|
19
|
+
|
20
|
+
t.timestamps
|
21
|
+
end
|
22
|
+
|
23
|
+
# Indexes for performance and uniqueness
|
24
|
+
add_index :invitations, [:email_address, :organization_id], unique: true, name: 'index_invitations_on_email_and_org'
|
25
|
+
add_index :invitations, :status
|
26
|
+
add_index :invitations, :created_at
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
class CreateOrganizations < ActiveRecord::Migration[8.0]
|
2
|
+
def change
|
3
|
+
create_table :organizations do |t|
|
4
|
+
t.string :name, null: false
|
5
|
+
t.string :website
|
6
|
+
t.string :time_zone
|
7
|
+
if ActiveRecord::Base.connection.adapter_name.downcase.starts_with?('postgresql')
|
8
|
+
t.jsonb :meta, default: {}
|
9
|
+
t.jsonb :settings, default: {}
|
10
|
+
else
|
11
|
+
t.json :meta, default: {}
|
12
|
+
t.json :settings, default: {}
|
13
|
+
end
|
14
|
+
|
15
|
+
t.timestamps
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
class CreateUsers < ActiveRecord::Migration[8.0]
|
2
|
+
def change
|
3
|
+
create_table :users do |t|
|
4
|
+
t.string :email_address, null: false
|
5
|
+
t.string :username, null: false
|
6
|
+
t.string :phone_number
|
7
|
+
t.string :password_digest, null: false
|
8
|
+
|
9
|
+
t.string :first_name
|
10
|
+
t.string :middle_name
|
11
|
+
t.string :last_name
|
12
|
+
t.string :time_zone
|
13
|
+
|
14
|
+
# Email confirmation fields
|
15
|
+
t.string :confirmation_token
|
16
|
+
t.datetime :confirmed_at
|
17
|
+
t.datetime :confirmation_sent_at
|
18
|
+
t.string :unconfirmed_email_address
|
19
|
+
|
20
|
+
t.integer :status, default: 0
|
21
|
+
t.datetime :last_login_at
|
22
|
+
t.integer :failed_login_attempts, default: 0
|
23
|
+
t.datetime :locked_at
|
24
|
+
|
25
|
+
# Multi-tenant association
|
26
|
+
t.references :organization, null: false, foreign_key: true
|
27
|
+
|
28
|
+
if ActiveRecord::Base.connection.adapter_name.downcase.starts_with?('postgresql')
|
29
|
+
t.jsonb :meta, default: {}
|
30
|
+
t.jsonb :settings, default: {}
|
31
|
+
else
|
32
|
+
t.json :meta, default: {}
|
33
|
+
t.json :settings, default: {}
|
34
|
+
end
|
35
|
+
|
36
|
+
t.timestamps
|
37
|
+
end
|
38
|
+
add_index :users, :email_address, unique: true
|
39
|
+
add_index :users, :username, unique: true
|
40
|
+
add_index :users, :phone_number, unique: true
|
41
|
+
add_index :users, :confirmation_token, unique: true
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,29 @@
|
|
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 test organization and user for authentication testing
|
12
|
+
organization = Organization.find_or_create_by!(name: 'Test Organization') do |org|
|
13
|
+
org.website = 'https://test.example.com'
|
14
|
+
org.time_zone = 'UTC'
|
15
|
+
end
|
16
|
+
|
17
|
+
user = User.find_or_create_by!(email_address: 'test@example.com') do |u|
|
18
|
+
u.username = 'testuser'
|
19
|
+
u.email_address = 'test@example.com'
|
20
|
+
u.password = 'password123'
|
21
|
+
u.password_confirmation = 'password123'
|
22
|
+
u.organization = organization
|
23
|
+
u.first_name = 'Test'
|
24
|
+
u.last_name = 'User'
|
25
|
+
end
|
26
|
+
|
27
|
+
puts "Created test organization: #{organization.name}"
|
28
|
+
puts "Created test user: #{user.email_address} (password: password123)"
|
29
|
+
puts "You can now test login with POST /auth/login"
|
@@ -0,0 +1,133 @@
|
|
1
|
+
class Invitation < ApplicationRecord
|
2
|
+
# Multi-tenant associations
|
3
|
+
belongs_to :organization
|
4
|
+
belongs_to :inviter, class_name: 'User'
|
5
|
+
|
6
|
+
# Rails 8 compatible email field validation
|
7
|
+
validates :email_address, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
|
8
|
+
validates :first_name, presence: true
|
9
|
+
validates :last_name, presence: true
|
10
|
+
|
11
|
+
# Status enum for invitation lifecycle - Rails 8 syntax
|
12
|
+
enum :status, { pending: 0, accepted: 1, expired: 2, revoked: 3 }
|
13
|
+
|
14
|
+
# Scopes for invitation management
|
15
|
+
scope :valid, -> { where(status: :pending).where('created_at > ?', 7.days.ago) }
|
16
|
+
scope :recent, -> { where('created_at > ?', 30.days.ago) }
|
17
|
+
|
18
|
+
# Check if invitation is still valid (not expired)
|
19
|
+
def invitation_valid?
|
20
|
+
pending? && created_at > 7.days.ago
|
21
|
+
end
|
22
|
+
|
23
|
+
# Check if invitation has expired
|
24
|
+
def expired?
|
25
|
+
!invitation_valid?
|
26
|
+
end
|
27
|
+
|
28
|
+
# Generate JWT-based invitation token
|
29
|
+
def generate_invitation_token
|
30
|
+
# Use high precision timestamp and organization context for uniqueness
|
31
|
+
now = Time.now
|
32
|
+
|
33
|
+
payload = {
|
34
|
+
invitation_id: self.id,
|
35
|
+
email_address: self.email_address,
|
36
|
+
organization_id: self.organization_id,
|
37
|
+
inviter_id: self.inviter_id,
|
38
|
+
type: 'invitation',
|
39
|
+
iat: now.to_f, # Use float for higher precision
|
40
|
+
exp: 7.days.from_now.to_i, # 7 days expiration
|
41
|
+
# Bind token to invitation data to prevent tampering
|
42
|
+
invitation_hash: generate_invitation_hash
|
43
|
+
}
|
44
|
+
|
45
|
+
JWT.encode(payload, PropelAuth.configuration.jwt_secret, 'HS256')
|
46
|
+
end
|
47
|
+
|
48
|
+
# Find invitation by JWT token with validation
|
49
|
+
def self.find_by_invitation_token(token)
|
50
|
+
begin
|
51
|
+
# Decode and validate the JWT token
|
52
|
+
payload = JWT.decode(token, PropelAuth.configuration.jwt_secret, true, { algorithm: 'HS256' })[0]
|
53
|
+
|
54
|
+
# Verify this is an invitation token
|
55
|
+
return nil unless payload['type'] == 'invitation'
|
56
|
+
|
57
|
+
# Find the invitation
|
58
|
+
invitation = find_by(id: payload['invitation_id'])
|
59
|
+
return nil unless invitation
|
60
|
+
|
61
|
+
# Verify token matches this invitation
|
62
|
+
return nil unless payload['email_address'] == invitation.email_address
|
63
|
+
return nil unless payload['organization_id'] == invitation.organization_id
|
64
|
+
return nil unless payload['inviter_id'] == invitation.inviter_id
|
65
|
+
|
66
|
+
# Verify invitation hash to prevent tampering
|
67
|
+
return nil unless payload['invitation_hash'] == invitation.generate_invitation_hash
|
68
|
+
|
69
|
+
# Verify invitation is still valid
|
70
|
+
return nil unless invitation.invitation_valid?
|
71
|
+
|
72
|
+
invitation
|
73
|
+
rescue JWT::DecodeError, JWT::ExpiredSignature, JWT::InvalidIssuerError
|
74
|
+
nil
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# Accept invitation and create user account
|
79
|
+
def accept_with_user_params!(user_params)
|
80
|
+
return false unless invitation_valid?
|
81
|
+
|
82
|
+
begin
|
83
|
+
# Create the user account
|
84
|
+
user = User.new(user_params.merge(
|
85
|
+
email_address: self.email_address,
|
86
|
+
first_name: self.first_name,
|
87
|
+
last_name: self.last_name,
|
88
|
+
organization: self.organization
|
89
|
+
))
|
90
|
+
|
91
|
+
if user.save
|
92
|
+
# Mark invitation as accepted
|
93
|
+
update!(status: :accepted)
|
94
|
+
|
95
|
+
# Create agent relationship if needed
|
96
|
+
Agent.create!(
|
97
|
+
user: user,
|
98
|
+
agency: self.organization.agencies.first || self.organization.agencies.create!(name: 'Default Agency')
|
99
|
+
)
|
100
|
+
|
101
|
+
user
|
102
|
+
else
|
103
|
+
false
|
104
|
+
end
|
105
|
+
rescue ActiveRecord::RecordInvalid
|
106
|
+
false
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
# Revoke invitation (admin action)
|
111
|
+
def revoke!
|
112
|
+
update!(status: :revoked)
|
113
|
+
end
|
114
|
+
|
115
|
+
# Get display name for invitee
|
116
|
+
def display_name
|
117
|
+
if first_name.present? && last_name.present?
|
118
|
+
"#{first_name} #{last_name}"
|
119
|
+
elsif first_name.present?
|
120
|
+
first_name
|
121
|
+
else
|
122
|
+
email_address.split('@').first.humanize
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
private
|
127
|
+
|
128
|
+
# Generate hash for invitation data to prevent token tampering
|
129
|
+
def generate_invitation_hash
|
130
|
+
data = "#{id}-#{email_address}-#{organization_id}-#{inviter_id}-#{created_at.to_i}"
|
131
|
+
Digest::SHA256.hexdigest(data)[0..10] # First 11 characters for brevity
|
132
|
+
end
|
133
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PropelAuth
|
4
|
+
# PropelAuth configuration and error classes
|
5
|
+
# This file was generated by PropelAuth and contains runtime functionality
|
6
|
+
# that was extracted from the gem to your application for customization.
|
7
|
+
|
8
|
+
class Error < StandardError; end
|
9
|
+
|
10
|
+
class << self
|
11
|
+
def configuration
|
12
|
+
@configuration ||= Configuration.new
|
13
|
+
end
|
14
|
+
|
15
|
+
def configure
|
16
|
+
yield(configuration)
|
17
|
+
end
|
18
|
+
|
19
|
+
def reset_configuration!
|
20
|
+
@configuration = nil
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class Configuration
|
25
|
+
attr_accessor :jwt_secret,
|
26
|
+
:jwt_expiration,
|
27
|
+
:jwt_algorithm,
|
28
|
+
:password_length,
|
29
|
+
:allow_registration,
|
30
|
+
:require_email_confirmation,
|
31
|
+
:confirmation_period,
|
32
|
+
:confirmation_resend_interval,
|
33
|
+
:max_failed_attempts,
|
34
|
+
:lockout_duration,
|
35
|
+
:password_reset_expiration,
|
36
|
+
:password_reset_rate_limit,
|
37
|
+
:frontend_url,
|
38
|
+
:email_from_address,
|
39
|
+
:support_email,
|
40
|
+
:enable_email_notifications,
|
41
|
+
:enable_sms_notifications,
|
42
|
+
:api_version
|
43
|
+
|
44
|
+
def initialize
|
45
|
+
# JWT Configuration defaults
|
46
|
+
@jwt_secret = nil # Will be set in initializer
|
47
|
+
@jwt_expiration = 24.hours
|
48
|
+
@jwt_algorithm = 'HS256'
|
49
|
+
|
50
|
+
# Password requirements
|
51
|
+
@password_length = 8..128
|
52
|
+
|
53
|
+
# User registration settings
|
54
|
+
@allow_registration = true
|
55
|
+
|
56
|
+
# Email confirmation settings
|
57
|
+
@require_email_confirmation = false
|
58
|
+
@confirmation_period = 24.hours # How long confirmation tokens are valid
|
59
|
+
@confirmation_resend_interval = 1.minute # Minimum time between resend attempts
|
60
|
+
|
61
|
+
# Account lockout settings
|
62
|
+
@max_failed_attempts = 10
|
63
|
+
@lockout_duration = 30.minutes
|
64
|
+
|
65
|
+
# Password reset settings
|
66
|
+
@password_reset_expiration = 15.minutes
|
67
|
+
@password_reset_rate_limit = 1.minute
|
68
|
+
|
69
|
+
# Frontend URL for email links (configure for your frontend application)
|
70
|
+
@frontend_url = Rails.env.development? ? 'http://localhost:3000' : nil
|
71
|
+
|
72
|
+
# Email configuration
|
73
|
+
@email_from_address = "noreply@#{Rails.application.class.module_parent.name.downcase}.com"
|
74
|
+
@support_email = "support@#{Rails.application.class.module_parent.name.downcase}.com"
|
75
|
+
|
76
|
+
# Notification preferences
|
77
|
+
@enable_email_notifications = true
|
78
|
+
@enable_sms_notifications = false # Requires SMS provider configuration
|
79
|
+
|
80
|
+
# API versioning configuration
|
81
|
+
@api_version = 'v1' # Default API version
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# PropelAuth Configuration
|
4
|
+
# This file was generated by: rails generate propel_auth:install
|
5
|
+
#
|
6
|
+
# This module provides JWT-based authentication for Rails applications
|
7
|
+
# with features like account lockout, password reset, and email confirmation.
|
8
|
+
|
9
|
+
require "rails"
|
10
|
+
|
11
|
+
module PropelAuth
|
12
|
+
class Error < StandardError; end
|
13
|
+
|
14
|
+
class << self
|
15
|
+
def configuration
|
16
|
+
@configuration ||= Configuration.new
|
17
|
+
end
|
18
|
+
|
19
|
+
def configure
|
20
|
+
yield(configuration)
|
21
|
+
end
|
22
|
+
|
23
|
+
def reset_configuration!
|
24
|
+
@configuration = Configuration.new
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class Configuration
|
29
|
+
attr_accessor :jwt_secret,
|
30
|
+
:jwt_expiration,
|
31
|
+
:jwt_algorithm,
|
32
|
+
:password_length,
|
33
|
+
:allow_registration,
|
34
|
+
:require_email_confirmation,
|
35
|
+
:confirmation_period,
|
36
|
+
:confirmation_resend_interval,
|
37
|
+
:max_failed_attempts,
|
38
|
+
:lockout_duration,
|
39
|
+
:password_reset_expiration,
|
40
|
+
:password_reset_rate_limit,
|
41
|
+
:frontend_url,
|
42
|
+
:email_from_address,
|
43
|
+
:support_email,
|
44
|
+
:enable_email_notifications,
|
45
|
+
:enable_sms_notifications,
|
46
|
+
:api_version
|
47
|
+
|
48
|
+
def initialize
|
49
|
+
# JWT Configuration defaults
|
50
|
+
@jwt_secret = nil # Will be set in configuration block
|
51
|
+
@jwt_expiration = 24.hours
|
52
|
+
@jwt_algorithm = 'HS256'
|
53
|
+
|
54
|
+
# Password requirements
|
55
|
+
@password_length = 8..128
|
56
|
+
|
57
|
+
# User registration settings
|
58
|
+
@allow_registration = true
|
59
|
+
|
60
|
+
# Email confirmation settings
|
61
|
+
@require_email_confirmation = false
|
62
|
+
@confirmation_period = 24.hours # How long confirmation tokens are valid
|
63
|
+
@confirmation_resend_interval = 1.minute # Minimum time between resend attempts
|
64
|
+
|
65
|
+
# Account lockout settings
|
66
|
+
@max_failed_attempts = 10
|
67
|
+
@lockout_duration = 30.minutes
|
68
|
+
|
69
|
+
# Password reset settings
|
70
|
+
@password_reset_expiration = 15.minutes
|
71
|
+
@password_reset_rate_limit = 1.minute
|
72
|
+
|
73
|
+
# Frontend URL for email links (configure for your frontend application)
|
74
|
+
@frontend_url = Rails.env.development? ? 'http://localhost:3000' : nil
|
75
|
+
|
76
|
+
# Email configuration
|
77
|
+
@email_from_address = "noreply@#{Rails.application.class.module_parent.name.downcase}.com"
|
78
|
+
@support_email = "support@#{Rails.application.class.module_parent.name.downcase}.com"
|
79
|
+
|
80
|
+
# Notification preferences
|
81
|
+
@enable_email_notifications = true
|
82
|
+
@enable_sms_notifications = false # Requires SMS provider configuration
|
83
|
+
|
84
|
+
# API versioning configuration
|
85
|
+
@api_version = 'v1' # Default API version
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
class Engine < Rails::Engine
|
90
|
+
initializer 'propel_auth.load_generators' do |app|
|
91
|
+
config.generators do |g|
|
92
|
+
g.test_framework :minitest, fixture: true
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# Configure PropelAuth with secure defaults
|
99
|
+
PropelAuth.configure do |config|
|
100
|
+
# JWT Configuration for API authentication
|
101
|
+
config.jwt_secret = Rails.application.credentials.secret_key_base || ENV['SECRET_KEY_BASE'] || 'your-secret-key'
|
102
|
+
config.jwt_expiration = 24.hours
|
103
|
+
|
104
|
+
# Password requirements
|
105
|
+
config.password_length = 8..128
|
106
|
+
|
107
|
+
# User registration settings
|
108
|
+
config.allow_registration = true
|
109
|
+
|
110
|
+
# Email confirmation (for future use)
|
111
|
+
config.require_email_confirmation = false
|
112
|
+
|
113
|
+
# Account lockout settings
|
114
|
+
config.max_failed_attempts = 10
|
115
|
+
config.lockout_duration = 30.minutes
|
116
|
+
|
117
|
+
# Password reset settings
|
118
|
+
config.password_reset_expiration = 15.minutes
|
119
|
+
config.password_reset_rate_limit = 1.minute
|
120
|
+
|
121
|
+
# Environment-specific settings
|
122
|
+
if Rails.env.development?
|
123
|
+
# Development-specific settings
|
124
|
+
config.jwt_expiration = 24.hours
|
125
|
+
elsif Rails.env.production?
|
126
|
+
# Production-specific settings - shorter token expiration for security
|
127
|
+
config.jwt_expiration = 2.hours
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
# Note: Generator files are loaded from gem unless unpacked
|
132
|
+
# To customize generators, run: rails generate propel_auth:unpack
|
@@ -0,0 +1,89 @@
|
|
1
|
+
class AuthNotificationService
|
2
|
+
class << self
|
3
|
+
def send_password_reset_email(user)
|
4
|
+
return false unless user&.persisted?
|
5
|
+
|
6
|
+
begin
|
7
|
+
reset_token = user.generate_password_reset_token
|
8
|
+
reset_url = build_password_reset_url(reset_token)
|
9
|
+
|
10
|
+
AuthMailer.with(
|
11
|
+
user: user,
|
12
|
+
token: reset_token,
|
13
|
+
reset_url: reset_url
|
14
|
+
).password_reset.deliver_now
|
15
|
+
|
16
|
+
{ success: true, message: "Password reset email sent successfully" }
|
17
|
+
rescue StandardError => e
|
18
|
+
Rails.logger.error "Failed to send password reset email: #{e.message}"
|
19
|
+
{ success: false, error: "Failed to send password reset email" }
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def send_account_unlock_email(user)
|
24
|
+
return false unless user&.persisted? && user.locked?
|
25
|
+
|
26
|
+
begin
|
27
|
+
unlock_token = user.generate_unlock_token
|
28
|
+
unlock_url = build_account_unlock_url(unlock_token)
|
29
|
+
|
30
|
+
AuthMailer.with(
|
31
|
+
user: user,
|
32
|
+
token: unlock_token,
|
33
|
+
unlock_url: unlock_url
|
34
|
+
).account_unlock.deliver_now
|
35
|
+
|
36
|
+
{ success: true, message: "Account unlock email sent successfully" }
|
37
|
+
rescue StandardError => e
|
38
|
+
Rails.logger.error "Failed to send account unlock email: #{e.message}"
|
39
|
+
{ success: false, error: "Failed to send account unlock email" }
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def send_user_invitation_email(invitation)
|
44
|
+
return false unless invitation&.persisted? && invitation.valid?
|
45
|
+
|
46
|
+
begin
|
47
|
+
invitation_token = invitation.generate_invitation_token
|
48
|
+
invitation_url = build_invitation_url(invitation_token)
|
49
|
+
|
50
|
+
AuthMailer.with(
|
51
|
+
invitation: invitation,
|
52
|
+
inviter: invitation.inviter,
|
53
|
+
token: invitation_token,
|
54
|
+
invitation_url: invitation_url
|
55
|
+
).user_invitation.deliver_now
|
56
|
+
|
57
|
+
{ success: true, message: "Invitation email sent successfully" }
|
58
|
+
rescue StandardError => e
|
59
|
+
Rails.logger.error "Failed to send invitation email: #{e.message}"
|
60
|
+
{ success: false, error: "Failed to send invitation email" }
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def build_password_reset_url(token)
|
67
|
+
base_url = PropelAuth.configuration.frontend_url || default_frontend_url
|
68
|
+
"#{base_url}/reset-password?token=#{token}"
|
69
|
+
end
|
70
|
+
|
71
|
+
def build_account_unlock_url(token)
|
72
|
+
base_url = PropelAuth.configuration.frontend_url || default_frontend_url
|
73
|
+
"#{base_url}/unlock-account?token=#{token}"
|
74
|
+
end
|
75
|
+
|
76
|
+
def build_invitation_url(token)
|
77
|
+
base_url = PropelAuth.configuration.frontend_url || default_frontend_url
|
78
|
+
"#{base_url}/accept-invitation?token=#{token}"
|
79
|
+
end
|
80
|
+
|
81
|
+
def default_frontend_url
|
82
|
+
if Rails.env.development?
|
83
|
+
"http://localhost:3000"
|
84
|
+
else
|
85
|
+
"https://#{Rails.application.class.module_parent.name.downcase}.com"
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|