rails_simple_auth 1.0.0

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 (40) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +32 -0
  3. data/MIT-LICENSE +21 -0
  4. data/README.md +276 -0
  5. data/app/controllers/rails_simple_auth/base_controller.rb +16 -0
  6. data/app/controllers/rails_simple_auth/confirmations_controller.rb +45 -0
  7. data/app/controllers/rails_simple_auth/omniauth_callbacks_controller.rb +32 -0
  8. data/app/controllers/rails_simple_auth/passwords_controller.rb +66 -0
  9. data/app/controllers/rails_simple_auth/registrations_controller.rb +52 -0
  10. data/app/controllers/rails_simple_auth/sessions_controller.rb +90 -0
  11. data/app/mailers/rails_simple_auth/auth_mailer.rb +46 -0
  12. data/app/views/rails_simple_auth/confirmations/new.html.erb +27 -0
  13. data/app/views/rails_simple_auth/mailers/confirmation.html.erb +28 -0
  14. data/app/views/rails_simple_auth/mailers/magic_link.html.erb +28 -0
  15. data/app/views/rails_simple_auth/mailers/password_reset.html.erb +28 -0
  16. data/app/views/rails_simple_auth/passwords/edit.html.erb +33 -0
  17. data/app/views/rails_simple_auth/passwords/new.html.erb +27 -0
  18. data/app/views/rails_simple_auth/registrations/new.html.erb +55 -0
  19. data/app/views/rails_simple_auth/sessions/magic_link_form.html.erb +27 -0
  20. data/app/views/rails_simple_auth/sessions/new.html.erb +59 -0
  21. data/lib/generators/rails_simple_auth/css/css_generator.rb +36 -0
  22. data/lib/generators/rails_simple_auth/css/templates/rails_simple_auth.css +344 -0
  23. data/lib/generators/rails_simple_auth/install/install_generator.rb +65 -0
  24. data/lib/generators/rails_simple_auth/install/templates/initializer.rb +106 -0
  25. data/lib/generators/rails_simple_auth/install/templates/migration.rb +42 -0
  26. data/lib/generators/rails_simple_auth/views/views_generator.rb +39 -0
  27. data/lib/rails_simple_auth/configuration.rb +96 -0
  28. data/lib/rails_simple_auth/controllers/concerns/authentication.rb +104 -0
  29. data/lib/rails_simple_auth/controllers/concerns/session_management.rb +74 -0
  30. data/lib/rails_simple_auth/engine.rb +18 -0
  31. data/lib/rails_simple_auth/models/concerns/authenticatable.rb +65 -0
  32. data/lib/rails_simple_auth/models/concerns/confirmable.rb +42 -0
  33. data/lib/rails_simple_auth/models/concerns/magic_linkable.rb +21 -0
  34. data/lib/rails_simple_auth/models/concerns/oauth_connectable.rb +101 -0
  35. data/lib/rails_simple_auth/models/current.rb +7 -0
  36. data/lib/rails_simple_auth/models/session.rb +22 -0
  37. data/lib/rails_simple_auth/routes.rb +46 -0
  38. data/lib/rails_simple_auth/version.rb +5 -0
  39. data/lib/rails_simple_auth.rb +39 -0
  40. metadata +117 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 9da9b2ca95ab7bc4d65390921ab753bfc4181efc88c28ed745bebae4268310a6
4
+ data.tar.gz: 0bf68d88f90560c275c4f680b5c741cb458badc4a328d1fd24ae8ff854834a87
5
+ SHA512:
6
+ metadata.gz: 6099797336414bc988d390ced5d6bb73f8def1c1ac57cee123d86c12945c2784b613b3e476c49ac7334de115a5ae53338da0107282cbd9cc6b0e2a4d01d2710d
7
+ data.tar.gz: 5f676fbf88a8b574cadaa43c96bc52c9e928c966b1d187f31b826c360d1472b160ed473e11582cfa84e232a242275e2ce2fb06714ba9d0a98501734023f94399
data/CHANGELOG.md ADDED
@@ -0,0 +1,32 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [1.0.0] - 2025-01-18
11
+
12
+ ### Added
13
+
14
+ - Initial release extracted from [Writero](https://github.com/ivankuznetsov/writero)
15
+ - Session-based authentication with `has_secure_password`
16
+ - Magic link authentication (passwordless login via email)
17
+ - Email confirmation flow for new registrations
18
+ - Password reset flow with secure tokens
19
+ - OAuth support (Google, GitHub) via OmniAuth
20
+ - Rate limiting on authentication endpoints (Rails 8 rate_limit DSL)
21
+ - Configurable callbacks (`after_sign_in_callback`, `after_sign_out_callback`, etc.)
22
+ - Configurable redirect paths (`after_sign_in_path`, `after_sign_out_path`, etc.)
23
+ - Views generator (`rails g rails_simple_auth:views`) for customization
24
+ - CSS generator (`rails g rails_simple_auth:css`) for styling
25
+ - Install generator (`rails g rails_simple_auth:install`) for setup
26
+ - Session management with automatic cleanup of expired sessions
27
+ - Current user tracking via `RailsSimpleAuth::Current`
28
+ - Comprehensive security measures:
29
+ - Open redirect prevention
30
+ - Configurable OAuth account linking
31
+ - Secure signed tokens for password reset and magic links
32
+ - Session invalidation on password change
data/MIT-LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Ivan Kuznetsov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,276 @@
1
+ # RailsSimpleAuth
2
+
3
+ Simple, secure authentication for Rails 8+ applications. Built on Rails primitives with no magic.
4
+
5
+ ## Features
6
+
7
+ - **Email/Password authentication** with bcrypt
8
+ - **Magic link** (passwordless) authentication
9
+ - **Email confirmation** with signed tokens
10
+ - **Password reset** with signed tokens
11
+ - **OAuth support** (Google, GitHub, etc.)
12
+ - **Rate limiting** built-in
13
+ - **Session tracking** with IP and user agent
14
+ - **Customizable styling** via CSS variables
15
+ - **No dependencies** beyond Rails and bcrypt
16
+
17
+ ## Installation
18
+
19
+ Add to your Gemfile:
20
+
21
+ ```ruby
22
+ gem "rails_simple_auth"
23
+ ```
24
+
25
+ Then run:
26
+
27
+ ```bash
28
+ bundle install
29
+ ```
30
+
31
+ Run the installer:
32
+
33
+ ```bash
34
+ rails generate rails_simple_auth:install
35
+ rails db:migrate
36
+ ```
37
+
38
+ Add concerns to your User model:
39
+
40
+ ```ruby
41
+ class User < ApplicationRecord
42
+ include RailsSimpleAuth::Models::Concerns::Authenticatable
43
+ include RailsSimpleAuth::Models::Concerns::Confirmable # optional
44
+ include RailsSimpleAuth::Models::Concerns::MagicLinkable # optional
45
+ include RailsSimpleAuth::Models::Concerns::OAuthConnectable # optional
46
+
47
+ # Your custom fields and validations
48
+ validates :company_name, presence: true
49
+ end
50
+ ```
51
+
52
+ Protect your routes:
53
+
54
+ ```ruby
55
+ class ApplicationController < ActionController::Base
56
+ before_action :require_authentication
57
+ end
58
+ ```
59
+
60
+ ## User Model Customization
61
+
62
+ The gem doesn't own your User model—you do. Add any custom fields:
63
+
64
+ ```ruby
65
+ # db/migrate/xxx_create_users.rb
66
+ class CreateUsers < ActiveRecord::Migration[8.0]
67
+ def change
68
+ create_table :users do |t|
69
+ # Required by gem
70
+ t.string :email_address, null: false
71
+ t.string :password_digest, null: false
72
+ t.datetime :confirmed_at # if using Confirmable
73
+
74
+ # Your custom fields
75
+ t.string :name
76
+ t.string :company_name
77
+ t.boolean :admin, default: false
78
+ t.string :oauth_provider
79
+ t.string :oauth_uid
80
+
81
+ t.timestamps
82
+ end
83
+
84
+ add_index :users, :email_address, unique: true
85
+ end
86
+ end
87
+ ```
88
+
89
+ ## Styling
90
+
91
+ The gem ships with **no CSS by default** (Option B). Generate base styles:
92
+
93
+ ```bash
94
+ rails generate rails_simple_auth:css
95
+ ```
96
+
97
+ Then customize by overriding CSS variables:
98
+
99
+ ```css
100
+ /* In your application.css */
101
+ :root {
102
+ --rsa-color-primary: #22c55e; /* Your brand color */
103
+ --rsa-color-background-form: #f0fdf4; /* Form background */
104
+ --rsa-color-text: #166534; /* Text color */
105
+ }
106
+ ```
107
+
108
+ Or edit `rails_simple_auth.css` directly for complete control.
109
+
110
+ ### CSS Variables Reference
111
+
112
+ | Variable | Default | Description |
113
+ |----------|---------|-------------|
114
+ | `--rsa-color-primary` | `#3b82f6` | Primary button/link color |
115
+ | `--rsa-color-primary-hover` | `#2563eb` | Primary hover state |
116
+ | `--rsa-color-background-form` | `#ffffff` | Form container background |
117
+ | `--rsa-color-text` | `#374151` | Main text color |
118
+ | `--rsa-color-text-muted` | `#6b7280` | Secondary text color |
119
+ | `--rsa-color-border` | `#e5e7eb` | Border color |
120
+ | `--rsa-color-danger` | `#dc2626` | Error message color |
121
+
122
+ ## View Customization
123
+
124
+ Copy views for full customization:
125
+
126
+ ```bash
127
+ rails generate rails_simple_auth:views
128
+
129
+ # Or specific views only
130
+ rails generate rails_simple_auth:views --only sessions passwords
131
+ ```
132
+
133
+ Views use BEM naming: `.rsa-auth-form`, `.rsa-auth-form__input`, etc.
134
+
135
+ ## Configuration
136
+
137
+ ```ruby
138
+ # config/initializers/rails_simple_auth.rb
139
+ RailsSimpleAuth.configure do |config|
140
+ # Features
141
+ config.magic_link_enabled = true
142
+ config.email_confirmation_enabled = true
143
+ config.enable_oauth(:google, :github)
144
+
145
+ # Token expiration
146
+ config.magic_link_expiry = 15.minutes
147
+ config.password_reset_expiry = 15.minutes
148
+ config.confirmation_expiry = 24.hours
149
+
150
+ # Paths (symbol, string, or proc)
151
+ config.after_sign_in_path = :dashboard_path
152
+ config.after_sign_out_path = -> { new_session_path }
153
+
154
+ # Layout
155
+ config.layout = "auth" # Use a custom layout
156
+
157
+ # Mailer
158
+ config.mailer_sender = "auth@myapp.com"
159
+
160
+ # Password requirements
161
+ config.password_minimum_length = 12
162
+
163
+ # Callbacks
164
+ config.after_sign_in_callback = ->(user, controller) {
165
+ Analytics.track("sign_in", user_id: user.id)
166
+ }
167
+ end
168
+ ```
169
+
170
+ ## OAuth Setup
171
+
172
+ 1. Enable providers:
173
+
174
+ ```ruby
175
+ RailsSimpleAuth.configure do |config|
176
+ config.enable_oauth(:google, :github)
177
+ end
178
+ ```
179
+
180
+ 2. Configure OmniAuth:
181
+
182
+ ```ruby
183
+ # config/initializers/omniauth.rb
184
+ Rails.application.config.middleware.use OmniAuth::Builder do
185
+ provider :google_oauth2, ENV["GOOGLE_CLIENT_ID"], ENV["GOOGLE_CLIENT_SECRET"]
186
+ provider :github, ENV["GITHUB_CLIENT_ID"], ENV["GITHUB_CLIENT_SECRET"]
187
+ end
188
+ ```
189
+
190
+ 3. Optionally map OAuth fields:
191
+
192
+ ```ruby
193
+ class User < ApplicationRecord
194
+ include RailsSimpleAuth::Models::Concerns::OAuthConnectable
195
+
196
+ def assign_oauth_attributes(auth_hash)
197
+ self.name = auth_hash.dig("info", "name")
198
+ self.avatar_url = auth_hash.dig("info", "image")
199
+ self.oauth_provider = auth_hash["provider"]
200
+ self.oauth_uid = auth_hash["uid"]
201
+ end
202
+ end
203
+ ```
204
+
205
+ ## Controller Customization
206
+
207
+ Subclass controllers for custom behavior:
208
+
209
+ ```ruby
210
+ # app/controllers/sessions_controller.rb
211
+ class SessionsController < RailsSimpleAuth::SessionsController
212
+ def after_sign_in(user)
213
+ track_login(user)
214
+ super
215
+ end
216
+ end
217
+ ```
218
+
219
+ Update routes to use your controller:
220
+
221
+ ```ruby
222
+ rails_simple_auth_routes(sessions_controller: "sessions")
223
+ ```
224
+
225
+ ## Helpers
226
+
227
+ Available in controllers and views:
228
+
229
+ ```ruby
230
+ current_user # The signed-in user (or nil)
231
+ user_signed_in? # Boolean
232
+ require_authentication # Redirects if not signed in
233
+ ```
234
+
235
+ Access anywhere via:
236
+
237
+ ```ruby
238
+ RailsSimpleAuth::Current.user
239
+ ```
240
+
241
+ ## Routes
242
+
243
+ The gem adds these routes:
244
+
245
+ | Method | Path | Description |
246
+ |--------|------|-------------|
247
+ | GET | `/session/new` | Sign in form |
248
+ | POST | `/session` | Create session |
249
+ | DELETE | `/session` | Sign out |
250
+ | GET | `/sign_up` | Sign up form |
251
+ | POST | `/sign_up` | Create account |
252
+ | GET | `/passwords/new` | Password reset form |
253
+ | POST | `/passwords` | Send reset email |
254
+ | GET | `/passwords/:token/edit` | New password form |
255
+ | PATCH | `/passwords/:token` | Update password |
256
+ | GET | `/confirmations/new` | Resend confirmation |
257
+ | POST | `/confirmations` | Send confirmation |
258
+ | GET | `/confirmations/:token` | Confirm email |
259
+ | GET | `/magic_link_form` | Magic link form |
260
+ | POST | `/request_magic_link` | Send magic link |
261
+ | GET | `/magic_link` | Login via magic link |
262
+
263
+ ## Security Features
264
+
265
+ - **BCrypt password hashing** with salts
266
+ - **Constant-time comparison** prevents timing attacks
267
+ - **Signed tokens** for all email links
268
+ - **Rate limiting** on all auth endpoints
269
+ - **HttpOnly cookies** for session tokens
270
+ - **SameSite=Lax** CSRF protection
271
+ - **Session invalidation** on password change
272
+ - **IP and user agent tracking** for audit
273
+
274
+ ## License
275
+
276
+ MIT License
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsSimpleAuth
4
+ class BaseController < ::ApplicationController
5
+ include RailsSimpleAuth::Controllers::Concerns::Authentication
6
+ include RailsSimpleAuth::Controllers::Concerns::SessionManagement
7
+
8
+ layout -> { RailsSimpleAuth.configuration.layout }
9
+
10
+ private
11
+
12
+ def user_class
13
+ RailsSimpleAuth.configuration.user_class
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsSimpleAuth
4
+ class ConfirmationsController < BaseController
5
+ skip_before_action :require_authentication, only: %i[new create show], raise: false
6
+
7
+ unless Rails.env.local?
8
+ rate_limit to: 3, within: 1.hour, by: -> { client_ip }, only: :create,
9
+ with: -> { redirect_to new_confirmation_path, alert: "Too many confirmation requests. Please try again later." }
10
+ end
11
+
12
+ def new
13
+ end
14
+
15
+ def create
16
+ user = user_class.find_by_email(params[:email_address])
17
+
18
+ if user && user.respond_to?(:unconfirmed?) && user.unconfirmed?
19
+ token = user.generate_confirmation_token
20
+ RailsSimpleAuth.configuration.mailer.confirmation(user, token).deliver_later
21
+ end
22
+
23
+ redirect_to new_session_path, notice: "If an unconfirmed account exists with that email, confirmation instructions have been sent."
24
+ end
25
+
26
+ def show
27
+ user = user_class.find_signed(params[:token], purpose: :email_confirmation)
28
+
29
+ if user
30
+ user.confirm! if user.respond_to?(:confirm!)
31
+ run_after_confirmation_callback(user)
32
+ redirect_to resolve_path(:after_confirmation_path), notice: "Email confirmed! You can now sign in."
33
+ else
34
+ redirect_to new_confirmation_path, alert: "Invalid or expired confirmation link."
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ def run_after_confirmation_callback(user)
41
+ callback = RailsSimpleAuth.configuration.after_confirmation_callback
42
+ callback&.call(user, self)
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsSimpleAuth
4
+ class OmniauthCallbacksController < BaseController
5
+ skip_before_action :require_authentication, raise: false
6
+ skip_before_action :verify_authenticity_token, only: :create
7
+
8
+ def create
9
+ auth_hash = request.env["omniauth.auth"]
10
+ provider = params[:provider]
11
+
12
+ unless RailsSimpleAuth.configuration.oauth_provider_enabled?(provider)
13
+ redirect_to new_session_path, alert: "OAuth provider not enabled."
14
+ return
15
+ end
16
+
17
+ user = user_class.from_oauth(auth_hash)
18
+
19
+ if user&.persisted?
20
+ create_session_for(user)
21
+ run_after_sign_in_callback(user)
22
+ redirect_to resolve_path(:after_sign_in_path), notice: "Signed in successfully with #{provider.to_s.capitalize}."
23
+ else
24
+ redirect_to new_session_path, alert: "Could not authenticate with #{provider.to_s.capitalize}."
25
+ end
26
+ end
27
+
28
+ def failure
29
+ redirect_to new_session_path, alert: "Authentication failed. Please try again."
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsSimpleAuth
4
+ class PasswordsController < BaseController
5
+ skip_before_action :require_authentication, only: %i[new create edit update], raise: false
6
+ before_action :set_user_from_token, only: %i[edit update]
7
+
8
+ unless Rails.env.local?
9
+ rate_limit to: 3, within: 1.hour, by: -> { client_ip }, only: :create,
10
+ with: -> { redirect_to new_password_path, alert: "Too many password reset requests. Please try again later." }
11
+ end
12
+
13
+ def new
14
+ end
15
+
16
+ def create
17
+ user = user_class.find_by_email(params[:email_address])
18
+
19
+ if user && can_reset_password?(user)
20
+ token = user.generate_password_reset_token
21
+ RailsSimpleAuth.configuration.mailer.password_reset(user, token).deliver_later
22
+ end
23
+
24
+ redirect_to new_session_path, notice: "If an account exists with that email, password reset instructions have been sent."
25
+ end
26
+
27
+ def edit
28
+ end
29
+
30
+ def update
31
+ ActiveRecord::Base.transaction do
32
+ if @user.update(password_params)
33
+ @user.invalidate_all_sessions!
34
+ redirect_to new_session_path, notice: "Password has been reset. Please sign in with your new password."
35
+ else
36
+ render :edit, status: :unprocessable_content
37
+ raise ActiveRecord::Rollback
38
+ end
39
+ end
40
+ rescue ActiveRecord::StatementInvalid => e
41
+ Rails.logger.error(
42
+ "[RailsSimpleAuth] Session invalidation failed after password reset for user #{@user.id}: #{e.message}"
43
+ )
44
+ # Password was rolled back due to transaction, redirect with error
45
+ redirect_to new_password_path, alert: "Password reset failed. Please try again."
46
+ end
47
+
48
+ private
49
+
50
+ def set_user_from_token
51
+ @user = user_class.find_signed(params[:token], purpose: :password_reset)
52
+ redirect_to new_password_path, alert: "Invalid or expired password reset link." unless @user
53
+ end
54
+
55
+ def can_reset_password?(user)
56
+ return true unless RailsSimpleAuth.configuration.email_confirmation_enabled
57
+ return true unless user.respond_to?(:confirmed?)
58
+
59
+ user.confirmed?
60
+ end
61
+
62
+ def password_params
63
+ params.require(:user).permit(:password, :password_confirmation)
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsSimpleAuth
4
+ class RegistrationsController < BaseController
5
+ skip_before_action :require_authentication, only: %i[new create], raise: false
6
+
7
+ unless Rails.env.local?
8
+ rate_limit to: 5, within: 1.hour, by: -> { client_ip }, only: :create,
9
+ with: -> { redirect_to sign_up_path, alert: "Too many sign up attempts. Please try again later." }
10
+ end
11
+
12
+ def new
13
+ redirect_to resolve_path(:after_sign_in_path) if user_signed_in?
14
+ @user = user_class.new
15
+ end
16
+
17
+ def create
18
+ @user = user_class.new(registration_params)
19
+
20
+ if @user.save
21
+ after_successful_registration
22
+ else
23
+ render :new, status: :unprocessable_content
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def registration_params
30
+ params.require(:user).permit(:email_address, :password)
31
+ end
32
+
33
+ def after_successful_registration
34
+ if RailsSimpleAuth.configuration.email_confirmation_enabled
35
+ send_confirmation_email(@user)
36
+ run_after_sign_up_callback(@user)
37
+ redirect_to new_session_path, notice: "Account created! Please check your email to confirm your account."
38
+ else
39
+ create_session_for(@user)
40
+ run_after_sign_up_callback(@user)
41
+ redirect_to resolve_path(:after_sign_up_path), notice: "Account created successfully!"
42
+ end
43
+ end
44
+
45
+ def send_confirmation_email(user)
46
+ return unless user.respond_to?(:generate_confirmation_token)
47
+
48
+ token = user.generate_confirmation_token
49
+ RailsSimpleAuth.configuration.mailer.confirmation(user, token).deliver_later
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsSimpleAuth
4
+ class SessionsController < BaseController
5
+ skip_before_action :require_authentication,
6
+ only: %i[new create magic_link_form request_magic_link magic_link_login],
7
+ raise: false
8
+
9
+ unless Rails.env.local?
10
+ rate_limit to: 5, within: 15.minutes, by: -> { client_ip }, only: :create,
11
+ with: -> { redirect_to new_session_path, alert: "Too many login attempts. Please try again later." }
12
+
13
+ rate_limit to: 3, within: 10.minutes, by: -> { params[:email_address].to_s.downcase }, only: :request_magic_link,
14
+ with: -> { redirect_to new_session_path, alert: "Too many magic link requests. Please try again later." }
15
+
16
+ rate_limit to: 5, within: 15.minutes, by: -> { client_ip }, only: :magic_link_login,
17
+ with: -> { redirect_to new_session_path, alert: "Too many magic link attempts. Please try again later." }
18
+ end
19
+
20
+ def new
21
+ redirect_to resolve_path(:after_sign_in_path) if user_signed_in?
22
+ end
23
+
24
+ def create
25
+ user = user_class.find_by_email(params[:email_address]) || user_class.new(password: SecureRandom.hex(32))
26
+
27
+ if user.authenticate(params[:password]) && user.persisted?
28
+ if confirmation_required_for?(user)
29
+ @error_message = "Please confirm your email before signing in."
30
+ @previous_email = params[:email_address]
31
+ render :new, status: :unprocessable_content
32
+ else
33
+ sign_in_and_redirect(user)
34
+ end
35
+ else
36
+ Rails.logger.warn("Failed login attempt for email: #{params[:email_address]} from IP: #{client_ip}")
37
+ @error_message = "Invalid email or password"
38
+ @previous_email = params[:email_address]
39
+ render :new, status: :unprocessable_content
40
+ end
41
+ end
42
+
43
+ def destroy
44
+ user = current_user
45
+ destroy_current_session
46
+ run_after_sign_out_callback(user) if user
47
+ redirect_to resolve_path(:after_sign_out_path), notice: "Signed out successfully."
48
+ end
49
+
50
+ def magic_link_form
51
+ redirect_to resolve_path(:after_sign_in_path) if user_signed_in?
52
+ end
53
+
54
+ def request_magic_link
55
+ user = user_class.find_by_email(params[:email_address])
56
+
57
+ if user && user.respond_to?(:generate_magic_link_token)
58
+ token = user.generate_magic_link_token
59
+ RailsSimpleAuth.configuration.mailer.magic_link(user, token).deliver_later
60
+ end
61
+
62
+ redirect_to new_session_path, notice: "If an account exists with that email, a magic link has been sent."
63
+ end
64
+
65
+ def magic_link_login
66
+ user = user_class.find_signed(params[:token], purpose: :magic_link)
67
+
68
+ if user
69
+ user.confirm! if user.respond_to?(:confirm!) && user.respond_to?(:unconfirmed?) && user.unconfirmed?
70
+ sign_in_and_redirect(user)
71
+ else
72
+ redirect_to new_session_path, alert: "Invalid or expired magic link."
73
+ end
74
+ end
75
+
76
+ private
77
+
78
+ def confirmation_required_for?(user)
79
+ RailsSimpleAuth.configuration.email_confirmation_enabled &&
80
+ user.respond_to?(:unconfirmed?) &&
81
+ user.unconfirmed?
82
+ end
83
+
84
+ def sign_in_and_redirect(user)
85
+ create_session_for(user)
86
+ run_after_sign_in_callback(user)
87
+ redirect_to stored_location_or_default, notice: "Signed in successfully."
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsSimpleAuth
4
+ class AuthMailer < ActionMailer::Base
5
+ default from: -> { RailsSimpleAuth.configuration.mailer_sender }
6
+
7
+ def confirmation(user, token)
8
+ @user = user
9
+ @token = token
10
+ @confirmation_url = main_app.confirmation_url(token: token)
11
+
12
+ mail(
13
+ to: user.email_address,
14
+ subject: "Confirm your email"
15
+ )
16
+ end
17
+
18
+ def magic_link(user, token)
19
+ @user = user
20
+ @token = token
21
+ @magic_link_url = main_app.magic_link_url(token: token)
22
+
23
+ mail(
24
+ to: user.email_address,
25
+ subject: "Sign in to your account"
26
+ )
27
+ end
28
+
29
+ def password_reset(user, token)
30
+ @user = user
31
+ @token = token
32
+ @password_reset_url = main_app.edit_password_url(token: token)
33
+
34
+ mail(
35
+ to: user.email_address,
36
+ subject: "Reset your password"
37
+ )
38
+ end
39
+
40
+ private
41
+
42
+ def main_app
43
+ Rails.application.routes.url_helpers
44
+ end
45
+ end
46
+ end