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,96 @@
1
+ <%- if api_versioned? -%>
2
+ # Dynamic API versioning controller - inherits from base controller
3
+ class <%= controller_namespace %>::TokensController < Api::Auth::BaseTokensController
4
+ end
5
+ <%- else -%>
6
+ class <%= controller_namespace %>::TokensController < ApplicationController
7
+ <%- unless api_only_app? -%>
8
+ include RackSessionDisable
9
+ <%- end -%>
10
+ include PropelAuthentication
11
+
12
+ # POST <%= api_versioned? ? "/#{api_namespace}" : "" %>/auth/login
13
+ def create
14
+ user = User.find_by(email_address: params[:user][:email_address])
15
+
16
+ if user&.authenticate(params[:user][:password])
17
+ if user.status == 1 # inactive
18
+ render json: { error: 'Account is inactive' }, status: :unauthorized
19
+ return
20
+ end
21
+
22
+ # Update last login timestamp
23
+ user.update(last_login_at: Time.current)
24
+
25
+ # Reset failed login attempts on successful login
26
+ user.reset_failed_attempts! if user.respond_to?(:reset_failed_attempts!)
27
+
28
+ render json: {
29
+ token: user.generate_jwt_token,
30
+ user: user_response(user)
31
+ }, status: :ok
32
+ else
33
+ # Increment failed login attempts if user exists and has lockable functionality
34
+ if user&.respond_to?(:increment_failed_attempts!)
35
+ user.increment_failed_attempts!
36
+ end
37
+
38
+ render json: { error: 'Invalid credentials' }, status: :unauthorized
39
+ end
40
+ rescue ActionController::ParameterMissing
41
+ render json: { error: 'Missing required parameters' }, status: :unprocessable_entity
42
+ end
43
+
44
+ # DELETE <%= api_versioned? ? "/#{api_namespace}" : "" %>/auth/logout
45
+ def destroy
46
+ # For JWT, logout is handled client-side by removing the token
47
+ # Server-side logout would require token blacklisting (future enhancement)
48
+ render json: { message: 'Logged out successfully' }, status: :ok
49
+ end
50
+
51
+ # GET <%= api_versioned? ? "/#{api_namespace}" : "" %>/auth/me
52
+ def me
53
+ return unless authenticate_user
54
+ render json: { user: user_response(current_user) }, status: :ok
55
+ end
56
+
57
+ def refresh
58
+ render json: { token: @current_user.generate_jwt_token }
59
+ end
60
+
61
+ # POST <%= api_versioned? ? "/#{api_namespace}" : "" %>/auth/unlock
62
+ def unlock
63
+ token = params[:token]
64
+
65
+ if token.blank?
66
+ render json: { error: 'Token is required' }, status: :unprocessable_entity
67
+ return
68
+ end
69
+
70
+ user = User.find_by_unlock_token(token)
71
+
72
+ if user
73
+ user.unlock_account!
74
+ render json: { message: 'Account unlocked successfully' }, status: :ok
75
+ else
76
+ render json: { error: 'Invalid or expired unlock token' }, status: :unauthorized
77
+ end
78
+ rescue => e
79
+ render json: { error: 'Invalid unlock token' }, status: :unauthorized
80
+ end
81
+
82
+ private
83
+
84
+ def user_response(user)
85
+ {
86
+ id: user.id,
87
+ email_address: user.email_address,
88
+ username: user.username,
89
+ first_name: user.first_name,
90
+ last_name: user.last_name,
91
+ organization_id: user.organization_id,
92
+ last_login_at: user.last_login_at
93
+ }
94
+ end
95
+ end
96
+ <%- end -%>
@@ -0,0 +1,21 @@
1
+ class User < ApplicationRecord
2
+ # Multi-tenant associations
3
+ belongs_to :organization
4
+ has_many :invitations, foreign_key: :inviter_id, dependent: :destroy
5
+ has_many :agents, dependent: :destroy
6
+
7
+ # Validations
8
+ validates :email_address, presence: true, uniqueness: true
9
+ validates :username, presence: true, uniqueness: true
10
+ validates :organization, presence: true
11
+
12
+ # Authentication and security concerns
13
+ include Authenticatable
14
+ include Lockable
15
+ include Recoverable
16
+ include Confirmable
17
+
18
+ # Future enhancements (Epic 2)
19
+ # include Invitable
20
+ # include Statusable
21
+ end
@@ -0,0 +1,81 @@
1
+ require "test_helper"
2
+
3
+ class UserTest < ActiveSupport::TestCase
4
+ def setup
5
+ @organization = Organization.create!(name: "Test Organization")
6
+ @user_attributes = {
7
+ email_address: "test@example.com",
8
+ username: "testuser",
9
+ password: "password123",
10
+ organization: @organization
11
+ }
12
+ end
13
+
14
+ test "should be valid with all attributes" do
15
+ user = User.new(@user_attributes)
16
+ assert user.valid?, "User should be valid with all required attributes"
17
+ end
18
+
19
+ test "should authenticate with correct password" do
20
+ user = User.create!(@user_attributes)
21
+ assert user.authenticate("password123")
22
+ assert_not user.authenticate("wrongpassword")
23
+ end
24
+
25
+ test "should not save user without email_address" do
26
+ user = User.new(@user_attributes.except(:email_address))
27
+ assert_not user.save, "Saved the user without an email_address"
28
+ end
29
+
30
+ test "should not save user without username" do
31
+ user = User.new(@user_attributes.except(:username))
32
+ assert_not user.save, "Saved the user without a username"
33
+ end
34
+
35
+ test "should not save user with short password" do
36
+ user = User.new(@user_attributes.merge(password: "123"))
37
+ assert_not user.save, "Saved the user with a password that is too short"
38
+ end
39
+
40
+ test "should generate valid JWT token" do
41
+ user = User.create!(@user_attributes)
42
+
43
+ token = user.generate_jwt_token
44
+ assert token.present?, "Should generate JWT token"
45
+
46
+ # Verify token structure
47
+ assert_equal 3, token.split('.').length, "JWT should have 3 parts"
48
+
49
+ # Verify token content
50
+ payload = JWT.decode(token, PropelAuth.configuration.jwt_secret, true, { algorithm: 'HS256' })[0]
51
+ assert_equal user.id, payload['user_id'], "Token should include user ID"
52
+ assert_equal user.organization_id, payload['organization_id'], "Token should include organization ID"
53
+ assert payload['exp'].present?, "Token should include expiration"
54
+ end
55
+
56
+ test "should validate JWT token and find user" do
57
+ user = User.create!(@user_attributes)
58
+
59
+ token = user.generate_jwt_token
60
+ found_user = User.find_by_jwt_token(token)
61
+
62
+ assert_equal user.id, found_user.id, "Should find user by JWT token"
63
+ assert_equal user.email_address, found_user.email_address, "Should return correct user"
64
+ end
65
+
66
+ test "should validate email_address uniqueness" do
67
+ User.create!(@user_attributes)
68
+
69
+ user2 = User.new(@user_attributes.merge(username: "newuser"))
70
+
71
+ assert_not user2.save, "Should not save user with duplicate email_address"
72
+ assert user2.errors[:email_address].present?, "Should have email_address validation error"
73
+ end
74
+
75
+ test "should validate password strength" do
76
+ user = User.new(@user_attributes.merge(password: "weak"))
77
+
78
+ assert_not user.save, "Should not save user with weak password"
79
+ assert user.errors[:password].present?, "Should have password validation error"
80
+ end
81
+ end
@@ -0,0 +1,213 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Account Locked - Security Notification</title>
7
+ <style>
8
+ body {
9
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
10
+ line-height: 1.6;
11
+ color: #333;
12
+ max-width: 600px;
13
+ margin: 0 auto;
14
+ padding: 20px;
15
+ background-color: #f8f9fa;
16
+ }
17
+ .container {
18
+ background-color: #ffffff;
19
+ padding: 40px;
20
+ border-radius: 8px;
21
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
22
+ }
23
+ .header {
24
+ text-align: center;
25
+ margin-bottom: 30px;
26
+ }
27
+ .logo {
28
+ font-size: 24px;
29
+ font-weight: bold;
30
+ color: #007bff;
31
+ margin-bottom: 10px;
32
+ }
33
+ h1 {
34
+ color: #dc3545;
35
+ font-size: 28px;
36
+ margin-bottom: 20px;
37
+ text-align: center;
38
+ }
39
+ .greeting {
40
+ font-size: 18px;
41
+ margin-bottom: 20px;
42
+ }
43
+ .message {
44
+ margin-bottom: 30px;
45
+ line-height: 1.8;
46
+ }
47
+ .cta-button {
48
+ display: inline-block;
49
+ background-color: #28a745;
50
+ color: white;
51
+ padding: 15px 30px;
52
+ text-decoration: none;
53
+ border-radius: 5px;
54
+ font-weight: bold;
55
+ text-align: center;
56
+ margin: 20px 0;
57
+ }
58
+ .cta-button:hover {
59
+ background-color: #218838;
60
+ }
61
+ .security-notice {
62
+ background-color: #f8d7da;
63
+ border: 1px solid #f5c6cb;
64
+ border-radius: 5px;
65
+ padding: 20px;
66
+ margin: 20px 0;
67
+ }
68
+ .security-notice h3 {
69
+ color: #721c24;
70
+ margin-top: 0;
71
+ }
72
+ .warning-notice {
73
+ background-color: #fff3cd;
74
+ border: 1px solid #ffeaa7;
75
+ border-radius: 5px;
76
+ padding: 20px;
77
+ margin: 20px 0;
78
+ }
79
+ .warning-notice h3 {
80
+ color: #856404;
81
+ margin-top: 0;
82
+ }
83
+ .expiry-notice {
84
+ background-color: #d1ecf1;
85
+ border: 1px solid #bee5eb;
86
+ border-radius: 5px;
87
+ padding: 15px;
88
+ margin: 20px 0;
89
+ text-align: center;
90
+ }
91
+ .footer {
92
+ margin-top: 40px;
93
+ padding-top: 20px;
94
+ border-top: 1px solid #eee;
95
+ font-size: 14px;
96
+ color: #666;
97
+ }
98
+ .token-info {
99
+ background-color: #f8f9fa;
100
+ border: 1px solid #dee2e6;
101
+ border-radius: 5px;
102
+ padding: 15px;
103
+ margin: 20px 0;
104
+ font-family: monospace;
105
+ word-break: break-all;
106
+ }
107
+ .auto-unlock-info {
108
+ background-color: #d4edda;
109
+ border: 1px solid #c3e6cb;
110
+ border-radius: 5px;
111
+ padding: 15px;
112
+ margin: 20px 0;
113
+ }
114
+ @media (max-width: 600px) {
115
+ .container {
116
+ padding: 20px;
117
+ }
118
+ .cta-button {
119
+ display: block;
120
+ width: 100%;
121
+ text-align: center;
122
+ }
123
+ }
124
+ </style>
125
+ </head>
126
+ <body>
127
+ <div class="container">
128
+ <div class="header">
129
+ <div class="logo"><%= @organization_name %></div>
130
+ </div>
131
+
132
+ <h1>🔒 Account Locked - Security Notification</h1>
133
+
134
+ <div class="greeting">
135
+ Hello <%= @display_name %>,
136
+ </div>
137
+
138
+ <div class="security-notice">
139
+ <h3>🚨 Security Alert</h3>
140
+ <p><strong>Your account has been temporarily locked</strong> due to multiple failed login attempts.</p>
141
+ <p>This is an automated security measure to protect your account from unauthorized access.</p>
142
+ </div>
143
+
144
+ <div class="message">
145
+ <p><strong>Account Details:</strong></p>
146
+ <ul>
147
+ <li><strong>Email:</strong> <%= @user.email_address %></li>
148
+ <li><strong>Organization:</strong> <%= @organization_name %></li>
149
+ <li><strong>Lock Time:</strong> <%= @user.locked_at&.strftime("%B %d, %Y at %I:%M %p %Z") %></li>
150
+ </ul>
151
+ </div>
152
+
153
+ <div class="warning-notice">
154
+ <h3>⚠️ What happened?</h3>
155
+ <p>Your account was locked after multiple failed login attempts. This could be due to:</p>
156
+ <ul>
157
+ <li>Forgotten password</li>
158
+ <li>Someone attempting to access your account</li>
159
+ <li>Browser auto-fill using an old password</li>
160
+ </ul>
161
+ </div>
162
+
163
+ <div class="message">
164
+ <p><strong>To unlock your account immediately, click the button below:</strong></p>
165
+ </div>
166
+
167
+ <div style="text-align: center;">
168
+ <a href="<%= @unlock_url %>" class="cta-button">Unlock My Account</a>
169
+ </div>
170
+
171
+ <div class="expiry-notice">
172
+ ⏰ <strong>This unlock link will expire in <%= @expiration_hours %> hour<%= @expiration_hours > 1 ? 's' : '' %></strong> for your security.
173
+ </div>
174
+
175
+ <div class="auto-unlock-info">
176
+ <h3>🕐 Automatic Unlock</h3>
177
+ <p><strong>Don't want to use the link?</strong> Your account will automatically unlock after 30 minutes from the lock time.</p>
178
+ <p>You can try logging in again after: <strong><%= (@user.locked_at + 30.minutes).strftime("%B %d, %Y at %I:%M %p %Z") %></strong></p>
179
+ </div>
180
+
181
+ <div class="security-notice">
182
+ <h3>🛡️ Security Recommendations</h3>
183
+ <ul>
184
+ <li><strong>Change your password</strong> if you suspect unauthorized access</li>
185
+ <li><strong>Use a password manager</strong> to avoid repeated failed attempts</li>
186
+ <li><strong>Contact support</strong> if you continue having issues</li>
187
+ <li><strong>Review recent activity</strong> in your account settings</li>
188
+ </ul>
189
+ </div>
190
+
191
+ <div class="message">
192
+ <p><strong>If the button doesn't work, copy and paste this link into your browser:</strong></p>
193
+ <div class="token-info">
194
+ <%= @unlock_url %>
195
+ </div>
196
+ </div>
197
+
198
+ <div class="footer">
199
+ <p><strong>Need Help?</strong></p>
200
+ <p>If you didn't attempt to log in or suspect unauthorized access, please contact our support team immediately at <a href="mailto:<%= @support_email %>"><%= @support_email %></a></p>
201
+
202
+ <p>This security notification was sent to <%= @user.email_address %> for your <%= @organization_name %> account.</p>
203
+
204
+ <hr style="margin: 20px 0; border: none; border-top: 1px solid #eee;">
205
+
206
+ <p style="font-size: 12px; color: #999;">
207
+ <strong><%= @organization_name %></strong><br>
208
+ This is an automated security notification. Please do not reply directly to this email.
209
+ </p>
210
+ </div>
211
+ </div>
212
+ </body>
213
+ </html>
@@ -0,0 +1,56 @@
1
+ ACCOUNT LOCKED - SECURITY NOTIFICATION - <%= @organization_name %>
2
+ =========================================================
3
+
4
+ Hello <%= @display_name %>,
5
+
6
+ 🚨 SECURITY ALERT
7
+ ------------------
8
+ Your account has been temporarily locked due to multiple failed login attempts.
9
+ This is an automated security measure to protect your account from unauthorized access.
10
+
11
+ ACCOUNT DETAILS
12
+ ---------------
13
+ • Email: <%= @user.email_address %>
14
+ • Organization: <%= @organization_name %>
15
+ • Lock Time: <%= @user.locked_at&.strftime("%B %d, %Y at %I:%M %p %Z") %>
16
+
17
+ ⚠️ WHAT HAPPENED?
18
+ ------------------
19
+ Your account was locked after multiple failed login attempts. This could be due to:
20
+ • Forgotten password
21
+ • Someone attempting to access your account
22
+ • Browser auto-fill using an old password
23
+
24
+ UNLOCK YOUR ACCOUNT IMMEDIATELY
25
+ --------------------------------
26
+ To unlock your account right now, visit this link:
27
+
28
+ <%= @unlock_url %>
29
+
30
+ ⏰ IMPORTANT: This unlock link will expire in <%= @expiration_hours %> hour<%= @expiration_hours > 1 ? 's' : '' %> for your security.
31
+
32
+ 🕐 AUTOMATIC UNLOCK
33
+ -------------------
34
+ Don't want to use the link? Your account will automatically unlock after 30 minutes from the lock time.
35
+
36
+ You can try logging in again after: <%= (@user.locked_at + 30.minutes).strftime("%B %d, %Y at %I:%M %p %Z") %>
37
+
38
+ 🛡️ SECURITY RECOMMENDATIONS
39
+ ----------------------------
40
+ • Change your password if you suspect unauthorized access
41
+ • Use a password manager to avoid repeated failed attempts
42
+ • Contact support if you continue having issues
43
+ • Review recent activity in your account settings
44
+
45
+ NEED HELP?
46
+ ----------
47
+ If you didn't attempt to log in or suspect unauthorized access, please contact our support team immediately at <%= @support_email %>
48
+
49
+ For security reasons, this unlock link can only be used once and will expire in <%= @expiration_hours %> hour<%= @expiration_hours > 1 ? 's' : '' %>.
50
+
51
+ ---
52
+
53
+ <%= @organization_name %>
54
+ This is an automated security notification. Please do not reply directly to this email.
55
+
56
+ This security notification was sent to <%= @user.email_address %> for your <%= @organization_name %> account.
@@ -0,0 +1,213 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
5
+ <style>
6
+ /* Reset styles */
7
+ body {
8
+ margin: 0;
9
+ padding: 0;
10
+ font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
11
+ font-size: 16px;
12
+ line-height: 1.6;
13
+ color: #333;
14
+ background-color: #f8f9fa;
15
+ }
16
+
17
+ /* Container */
18
+ .email-container {
19
+ max-width: 600px;
20
+ margin: 0 auto;
21
+ background-color: #ffffff;
22
+ border-radius: 8px;
23
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
24
+ overflow: hidden;
25
+ }
26
+
27
+ /* Header */
28
+ .header {
29
+ background-color: #007bff;
30
+ color: #ffffff;
31
+ text-align: center;
32
+ padding: 30px 20px;
33
+ }
34
+
35
+ .header h1 {
36
+ margin: 0;
37
+ font-size: 28px;
38
+ font-weight: 300;
39
+ }
40
+
41
+ /* Content */
42
+ .content {
43
+ padding: 40px 30px;
44
+ }
45
+
46
+ .content h2 {
47
+ color: #333;
48
+ font-size: 24px;
49
+ margin-bottom: 20px;
50
+ font-weight: 400;
51
+ }
52
+
53
+ .content p {
54
+ margin-bottom: 20px;
55
+ line-height: 1.7;
56
+ }
57
+
58
+ /* Confirmation button */
59
+ .confirm-button {
60
+ display: inline-block;
61
+ background-color: #28a745;
62
+ color: #ffffff;
63
+ text-decoration: none;
64
+ padding: 15px 30px;
65
+ border-radius: 5px;
66
+ font-weight: 600;
67
+ text-align: center;
68
+ margin: 20px 0;
69
+ transition: background-color 0.3s ease;
70
+ }
71
+
72
+ .confirm-button:hover {
73
+ background-color: #218838;
74
+ }
75
+
76
+ /* Token section */
77
+ .token-section {
78
+ background-color: #f8f9fa;
79
+ border-left: 4px solid #007bff;
80
+ padding: 15px 20px;
81
+ margin: 20px 0;
82
+ border-radius: 4px;
83
+ }
84
+
85
+ .token-code {
86
+ font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
87
+ font-size: 14px;
88
+ color: #6c757d;
89
+ word-break: break-all;
90
+ }
91
+
92
+ /* Security info */
93
+ .security-info {
94
+ background-color: #fff3cd;
95
+ border: 1px solid #ffeaa7;
96
+ border-radius: 4px;
97
+ padding: 15px;
98
+ margin: 20px 0;
99
+ }
100
+
101
+ .security-info h3 {
102
+ color: #856404;
103
+ margin: 0 0 10px 0;
104
+ font-size: 16px;
105
+ }
106
+
107
+ .security-info p {
108
+ color: #856404;
109
+ margin: 0;
110
+ font-size: 14px;
111
+ }
112
+
113
+ /* Footer */
114
+ .footer {
115
+ background-color: #f8f9fa;
116
+ padding: 20px 30px;
117
+ text-align: center;
118
+ border-top: 1px solid #e9ecef;
119
+ }
120
+
121
+ .footer p {
122
+ margin: 0;
123
+ font-size: 14px;
124
+ color: #6c757d;
125
+ }
126
+
127
+ .footer a {
128
+ color: #007bff;
129
+ text-decoration: none;
130
+ }
131
+
132
+ /* Responsive */
133
+ @media only screen and (max-width: 600px) {
134
+ .email-container {
135
+ margin: 0;
136
+ border-radius: 0;
137
+ }
138
+
139
+ .content {
140
+ padding: 30px 20px;
141
+ }
142
+
143
+ .header {
144
+ padding: 20px;
145
+ }
146
+
147
+ .header h1 {
148
+ font-size: 24px;
149
+ }
150
+ }
151
+ </style>
152
+ </head>
153
+ <body>
154
+ <div class="email-container">
155
+ <!-- Header -->
156
+ <div class="header">
157
+ <h1><%= @organization_name %></h1>
158
+ </div>
159
+
160
+ <!-- Content -->
161
+ <div class="content">
162
+ <h2>Welcome <%= @display_name %>! 👋</h2>
163
+
164
+ <p>Thank you for signing up! To get started, please confirm your email address by clicking the button below:</p>
165
+
166
+ <div style="text-align: center;">
167
+ <a href="<%= confirmation_url(@user) %>" class="confirm-button">
168
+ Confirm My Email Address
169
+ </a>
170
+ </div>
171
+
172
+ <p>This confirmation link will expire in <strong>24 hours</strong> for security reasons.</p>
173
+
174
+ <!-- Alternative method -->
175
+ <p>If the button above doesn't work, you can copy and paste this link into your browser:</p>
176
+
177
+ <div class="token-section">
178
+ <p style="margin: 0;"><strong>Confirmation Link:</strong></p>
179
+ <p class="token-code"><%= confirmation_url(@user) %></p>
180
+ </div>
181
+
182
+ <!-- Security information -->
183
+ <div class="security-info">
184
+ <h3>🔒 Security Notice</h3>
185
+ <p>If you didn't create an account with us, please ignore this email. Your email address will not be added to our system.</p>
186
+ </div>
187
+
188
+ <p>Once confirmed, you'll be able to:</p>
189
+ <ul>
190
+ <li>Access your account dashboard</li>
191
+ <li>Update your profile settings</li>
192
+ <li>Receive important notifications</li>
193
+ <li>Take advantage of all platform features</li>
194
+ </ul>
195
+
196
+ <p>Need help? Feel free to contact our support team.</p>
197
+ </div>
198
+
199
+ <!-- Footer -->
200
+ <div class="footer">
201
+ <p>
202
+ <strong><%= @organization_name %></strong><br>
203
+ <a href="mailto:<%= @support_email %>">Contact Support</a> |
204
+ <a href="<%= unsubscribe_url(@user) if respond_to?(:unsubscribe_url) %>">Unsubscribe</a>
205
+ </p>
206
+ <p style="margin-top: 10px; font-size: 12px;">
207
+ This email was sent to <%= @email_address || @user.email_address %>.
208
+ If you have questions, contact us at <a href="mailto:<%= @support_email %>"><%= @support_email %></a>.
209
+ </p>
210
+ </div>
211
+ </div>
212
+ </body>
213
+ </html>