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,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,7 @@
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
+ 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