rails-auth-eassy 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/MIT-LICENSE +20 -0
- data/README.md +206 -0
- data/Rakefile +6 -0
- data/app/assets/stylesheets/rails/auth/application.css +15 -0
- data/app/controllers/concerns/rails/auth/authenticatable_controller.rb +114 -0
- data/app/controllers/rails/auth/application_controller.rb +9 -0
- data/app/controllers/rails/auth/confirmations_controller.rb +18 -0
- data/app/controllers/rails/auth/impersonations_controller.rb +46 -0
- data/app/controllers/rails/auth/mfa_controller.rb +26 -0
- data/app/controllers/rails/auth/otp_verifications_controller.rb +25 -0
- data/app/controllers/rails/auth/password_resets_controller.rb +51 -0
- data/app/controllers/rails/auth/profiles_controller.rb +24 -0
- data/app/controllers/rails/auth/registrations_controller.rb +27 -0
- data/app/controllers/rails/auth/security_controller.rb +27 -0
- data/app/controllers/rails/auth/sessions_controller.rb +69 -0
- data/app/controllers/rails/auth/unlocks_controller.rb +17 -0
- data/app/helpers/rails/auth/application_helper.rb +6 -0
- data/app/jobs/rails/auth/application_job.rb +6 -0
- data/app/mailers/rails/auth/application_mailer.rb +8 -0
- data/app/mailers/rails/auth/user_mailer.rb +20 -0
- data/app/models/concerns/rails/auth/authenticatable.rb +107 -0
- data/app/models/concerns/rails/auth/sessionable.rb +30 -0
- data/app/models/rails/auth/application_record.rb +7 -0
- data/app/models/rails/auth/current.rb +7 -0
- data/app/models/rails/auth/security_event.rb +25 -0
- data/app/views/layouts/rails/auth/application.html.erb +29 -0
- data/app/views/rails/auth/mfa/show.html.erb +24 -0
- data/app/views/rails/auth/otp_verifications/new.html.erb +13 -0
- data/app/views/rails/auth/password_resets/edit.html.erb +28 -0
- data/app/views/rails/auth/password_resets/new.html.erb +14 -0
- data/app/views/rails/auth/profiles/edit.html.erb +44 -0
- data/app/views/rails/auth/registrations/new.html.erb +40 -0
- data/app/views/rails/auth/security/sessions.html.erb +92 -0
- data/app/views/rails/auth/sessions/new.html.erb +20 -0
- data/app/views/rails/auth/user_mailer/confirmation_instructions.html.erb +5 -0
- data/app/views/rails/auth/user_mailer/password_reset.html.erb +8 -0
- data/app/views/rails/auth/user_mailer/password_reset.text.erb +8 -0
- data/app/views/rails/auth/user_mailer/unlock_instructions.html.erb +7 -0
- data/config/routes.rb +20 -0
- data/lib/generators/rails_auth/install/install_generator.rb +21 -0
- data/lib/generators/rails_auth/install/templates/rails_auth.rb +7 -0
- data/lib/generators/rails_auth/model/model_generator.rb +27 -0
- data/lib/generators/rails_auth/model/templates/create_rails_auth_tables.rb +60 -0
- data/lib/generators/rails_auth/model/templates/session.rb +3 -0
- data/lib/generators/rails_auth/model/templates/user.rb +3 -0
- data/lib/generators/rails_auth/views/views_generator.rb +13 -0
- data/lib/rails/auth/engine.rb +7 -0
- data/lib/rails/auth/version.rb +5 -0
- data/lib/rails/auth.rb +49 -0
- data/lib/tasks/rails/auth_tasks.rake +4 -0
- metadata +177 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
module Rails
|
|
2
|
+
module Auth
|
|
3
|
+
class SessionsController < ApplicationController
|
|
4
|
+
skip_before_action :authenticate_user!, only: [ :new, :create ]
|
|
5
|
+
|
|
6
|
+
def new
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def create
|
|
10
|
+
user = Rails::Auth.user_class.find_by(email: params[:email])
|
|
11
|
+
|
|
12
|
+
if user&.access_locked?
|
|
13
|
+
respond_to do |format|
|
|
14
|
+
format.html { redirect_to new_session_path, alert: "Your account is locked. Please check your email for unlock instructions." }
|
|
15
|
+
format.json { render json: { error: "Account locked" }, status: :locked }
|
|
16
|
+
end
|
|
17
|
+
return
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
if user&.authenticate(params[:password])
|
|
21
|
+
unless user.confirmed?
|
|
22
|
+
respond_to do |format|
|
|
23
|
+
format.html { redirect_to new_session_path, alert: "Please confirm your email address before signing in." }
|
|
24
|
+
format.json { render json: { error: "Email not confirmed" }, status: :unauthorized }
|
|
25
|
+
end
|
|
26
|
+
return
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
user.update(failed_attempts: 0) # Reset on success
|
|
30
|
+
|
|
31
|
+
if user.otp_enabled?
|
|
32
|
+
session[:otp_user_id] = user.id
|
|
33
|
+
respond_to do |format|
|
|
34
|
+
format.html { redirect_to new_otp_verification_path }
|
|
35
|
+
format.json { render json: { mfa_required: true }, status: :accepted }
|
|
36
|
+
end
|
|
37
|
+
else
|
|
38
|
+
sign_in(user)
|
|
39
|
+
respond_to do |format|
|
|
40
|
+
format.html { redirect_to main_app.root_path, notice: "Signed in successfully." }
|
|
41
|
+
format.json { render json: { token: Rails::Auth.encode_jwt(user_id: user.id), user: user.as_json(only: [ :id, :email, :role ]) } }
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
else
|
|
45
|
+
if user
|
|
46
|
+
user.increment_failed_attempts!
|
|
47
|
+
user.log_security_event!(:login_failed, request)
|
|
48
|
+
message = user.access_locked? ? "Account locked. Check your email." : "Invalid email or password."
|
|
49
|
+
else
|
|
50
|
+
message = "Invalid email or password."
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
respond_to do |format|
|
|
54
|
+
format.html do
|
|
55
|
+
flash.now[:alert] = message
|
|
56
|
+
render :new, status: :unprocessable_entity
|
|
57
|
+
end
|
|
58
|
+
format.json { render json: { error: message }, status: :unauthorized }
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def destroy
|
|
64
|
+
sign_out
|
|
65
|
+
redirect_to main_app.root_path, notice: "Signed out successfully."
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module Rails
|
|
2
|
+
module Auth
|
|
3
|
+
class UnlocksController < ApplicationController
|
|
4
|
+
skip_before_action :authenticate_user!
|
|
5
|
+
|
|
6
|
+
def show
|
|
7
|
+
user = Rails::Auth.user_class.find_by(unlock_token: params[:unlock_token])
|
|
8
|
+
if user
|
|
9
|
+
user.unlock_access!
|
|
10
|
+
redirect_to new_session_path, notice: "Your account has been unlocked. Please sign in."
|
|
11
|
+
else
|
|
12
|
+
redirect_to new_session_path, alert: "Invalid unlock token."
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module Rails
|
|
2
|
+
module Auth
|
|
3
|
+
class UserMailer < ApplicationMailer
|
|
4
|
+
def password_reset(user)
|
|
5
|
+
@user = user
|
|
6
|
+
mail to: user.email, subject: "Password Reset Instructions"
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def confirmation_instructions(user)
|
|
10
|
+
@user = user
|
|
11
|
+
mail to: user.email, subject: "Confirmation Instructions"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def unlock_instructions(user)
|
|
15
|
+
@user = user
|
|
16
|
+
mail to: user.email, subject: "Unlock Instructions"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
module Rails
|
|
2
|
+
module Auth
|
|
3
|
+
module Authenticatable
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
included do
|
|
6
|
+
has_secure_password
|
|
7
|
+
has_many :sessions, class_name: Rails::Auth.session_class_name, dependent: :destroy
|
|
8
|
+
has_many :security_events, class_name: "Rails::Auth::SecurityEvent", dependent: :destroy
|
|
9
|
+
|
|
10
|
+
validates :email, presence: true, uniqueness: true, format: { with: URI::MailTo::EMAIL_REGEXP }
|
|
11
|
+
|
|
12
|
+
validates :password, allow_nil: true, length: { minimum: 8 }
|
|
13
|
+
|
|
14
|
+
enum :role, { user: 0, moderator: 1, admin: 2 }
|
|
15
|
+
has_one_attached :avatar
|
|
16
|
+
|
|
17
|
+
before_create :generate_confirmation_token, unless: :confirmed?
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Confirmable
|
|
21
|
+
def confirm!
|
|
22
|
+
update!(confirmed_at: Time.current, confirmation_token: nil)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def confirmed?
|
|
26
|
+
confirmed_at.present?
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def generate_confirmation_token
|
|
30
|
+
self.confirmation_token = SecureRandom.hex(20)
|
|
31
|
+
self.confirmation_sent_at = Time.current
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def send_confirmation_instructions
|
|
35
|
+
generate_confirmation_token
|
|
36
|
+
save!
|
|
37
|
+
Rails::Auth::UserMailer.confirmation_instructions(self).deliver_now
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Lockable
|
|
41
|
+
def lock_access!
|
|
42
|
+
update!(locked_at: Time.current, unlock_token: SecureRandom.hex(20))
|
|
43
|
+
Rails::Auth::UserMailer.unlock_instructions(self).deliver_now
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def unlock_access!
|
|
47
|
+
update!(locked_at: nil, failed_attempts: 0, unlock_token: nil)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def access_locked?
|
|
51
|
+
locked_at.present? && locked_at > 1.hour.ago
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def increment_failed_attempts!
|
|
55
|
+
self.failed_attempts += 1
|
|
56
|
+
if failed_attempts >= 5
|
|
57
|
+
lock_access!
|
|
58
|
+
else
|
|
59
|
+
save!
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# MFA
|
|
64
|
+
def generate_otp_secret!
|
|
65
|
+
update!(otp_secret: ::ROTP::Base32.random)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def verify_otp(code)
|
|
69
|
+
return false unless otp_secret.present?
|
|
70
|
+
totp = ::ROTP::TOTP.new(otp_secret, issuer: "RailsAuth")
|
|
71
|
+
totp.verify(code, drift_behind: 15)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def otp_provisioning_uri
|
|
75
|
+
totp = ::ROTP::TOTP.new(otp_secret, issuer: "RailsAuth")
|
|
76
|
+
totp.provisioning_uri(email)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def log_security_event!(event_type, request = nil, details = {})
|
|
80
|
+
security_events.create!(
|
|
81
|
+
event_type: event_type,
|
|
82
|
+
ip_address: request&.remote_ip,
|
|
83
|
+
user_agent: request&.user_agent,
|
|
84
|
+
details: details
|
|
85
|
+
)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Password Reset
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def generate_password_reset_token!
|
|
92
|
+
update!(
|
|
93
|
+
reset_token: SecureRandom.hex(20),
|
|
94
|
+
reset_sent_at: Time.current
|
|
95
|
+
)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def password_reset_token_valid?
|
|
99
|
+
reset_sent_at.present? && reset_sent_at > 2.hours.ago
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def clear_password_reset_token!
|
|
103
|
+
update!(reset_token: nil, reset_sent_at: nil)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
module Rails
|
|
2
|
+
module Auth
|
|
3
|
+
module Sessionable
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
|
|
6
|
+
included do
|
|
7
|
+
belongs_to :user, class_name: Rails::Auth.user_class_name
|
|
8
|
+
|
|
9
|
+
before_create :generate_token
|
|
10
|
+
before_create :set_device_info
|
|
11
|
+
|
|
12
|
+
scope :active, -> { where("last_active_at > ?", 1.month.ago) }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def generate_token
|
|
18
|
+
self.token = SecureRandom.hex(32)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def set_device_info
|
|
22
|
+
if user_agent.present?
|
|
23
|
+
ua = UserAgent.parse(user_agent)
|
|
24
|
+
self.browser = "#{ua.browser} #{ua.version}"
|
|
25
|
+
self.os = ua.platform
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module Rails
|
|
2
|
+
module Auth
|
|
3
|
+
class SecurityEvent < ApplicationRecord
|
|
4
|
+
self.table_name = "security_events"
|
|
5
|
+
|
|
6
|
+
belongs_to :user, class_name: Rails::Auth.user_class_name
|
|
7
|
+
|
|
8
|
+
validates :event_type, presence: true
|
|
9
|
+
|
|
10
|
+
enum :event_type, {
|
|
11
|
+
login_success: "login_success",
|
|
12
|
+
login_failed: "login_failed",
|
|
13
|
+
logout: "logout",
|
|
14
|
+
password_changed: "password_changed",
|
|
15
|
+
password_reset_requested: "password_reset_requested",
|
|
16
|
+
mfa_enabled: "mfa_enabled",
|
|
17
|
+
mfa_disabled: "mfa_disabled",
|
|
18
|
+
account_locked: "account_locked",
|
|
19
|
+
account_unlocked: "account_unlocked",
|
|
20
|
+
impersonation_started: "impersonation_started",
|
|
21
|
+
impersonation_stopped: "impersonation_stopped"
|
|
22
|
+
}
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<title>Rails auth</title>
|
|
5
|
+
<%= csrf_meta_tags %>
|
|
6
|
+
<%= csp_meta_tag %>
|
|
7
|
+
|
|
8
|
+
<%= yield :head %>
|
|
9
|
+
|
|
10
|
+
<%= stylesheet_link_tag "rails/auth/application", media: "all" %>
|
|
11
|
+
</head>
|
|
12
|
+
<body>
|
|
13
|
+
<% if impersonating? %>
|
|
14
|
+
<div style="background: #ffcc00; padding: 10px; text-align: center;">
|
|
15
|
+
You are currently impersonating <strong><%= current_user.email %></strong>.
|
|
16
|
+
<%= button_to "Stop Impersonation", rails_auth.stop_impersonations_path, method: :delete, style: "margin-left: 10px;" %>
|
|
17
|
+
</div>
|
|
18
|
+
<% end %>
|
|
19
|
+
|
|
20
|
+
<% flash.each do |type, message| %>
|
|
21
|
+
<div class="flash flash-<%= type %>">
|
|
22
|
+
<%= message %>
|
|
23
|
+
</div>
|
|
24
|
+
<% end %>
|
|
25
|
+
|
|
26
|
+
<%= yield %>
|
|
27
|
+
|
|
28
|
+
</body>
|
|
29
|
+
</html>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<h2>Two-Factor Authentication (MFA)</h2>
|
|
2
|
+
|
|
3
|
+
<% if current_user.otp_enabled? %>
|
|
4
|
+
<p>Two-factor authentication is currently enabled.</p>
|
|
5
|
+
<%= button_to "Disable MFA", rails_auth.mfa_path, method: :delete, data: { confirm: "Are you sure?" } %>
|
|
6
|
+
<% else %>
|
|
7
|
+
<p>To enable two-factor authentication, scan the QR code below with your authenticator app (Google Authenticator, Authy, etc.):</p>
|
|
8
|
+
|
|
9
|
+
<div>
|
|
10
|
+
<%= @qrcode.as_svg(module_size: 4).html_safe %>
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
<p>After scanning, enter the 6-digit code from your app to verify:</p>
|
|
14
|
+
|
|
15
|
+
<%= form_with url: rails_auth.mfa_path do |f| %>
|
|
16
|
+
<div>
|
|
17
|
+
<%= f.label :otp_code, "Verification Code" %>
|
|
18
|
+
<%= f.text_field :otp_code, autocomplete: "one-time-code" %>
|
|
19
|
+
</div>
|
|
20
|
+
<%= f.submit "Verify and Enable MFA" %>
|
|
21
|
+
<% end %>
|
|
22
|
+
<% end %>
|
|
23
|
+
|
|
24
|
+
<%= link_to "Back to Security", rails_auth.security_sessions_path %>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<h2>Two-Factor Authentication</h2>
|
|
2
|
+
|
|
3
|
+
<p>Enter the 6-digit code from your authenticator app to complete sign in.</p>
|
|
4
|
+
|
|
5
|
+
<%= form_with url: rails_auth.otp_verification_path do |f| %>
|
|
6
|
+
<div>
|
|
7
|
+
<%= f.label :otp_code, "Verification Code" %>
|
|
8
|
+
<%= f.text_field :otp_code, autofocus: true, autocomplete: "one-time-code" %>
|
|
9
|
+
</div>
|
|
10
|
+
<%= f.submit "Verify and Sign In" %>
|
|
11
|
+
<% end %>
|
|
12
|
+
|
|
13
|
+
<%= link_to "Cancel", rails_auth.new_session_path %>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<h2>Reset Password</h2>
|
|
2
|
+
|
|
3
|
+
<%= form_with model: @user, url: rails_auth.password_reset_path(@user.reset_token), method: :patch do |f| %>
|
|
4
|
+
<% if @user.errors.any? %>
|
|
5
|
+
<div id="error_explanation">
|
|
6
|
+
<h2><%= pluralize(@user.errors.count, "error") %> prohibited this user from being saved:</h2>
|
|
7
|
+
<ul>
|
|
8
|
+
<% @user.errors.full_messages.each do |message| %>
|
|
9
|
+
<li><%= message %></li>
|
|
10
|
+
<% end %>
|
|
11
|
+
</ul>
|
|
12
|
+
</div>
|
|
13
|
+
<% end %>
|
|
14
|
+
|
|
15
|
+
<div>
|
|
16
|
+
<%= f.label :password %>
|
|
17
|
+
<%= f.password_field :password, autofocus: true, autocomplete: "new-password" %>
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<div>
|
|
21
|
+
<%= f.label :password_confirmation %>
|
|
22
|
+
<%= f.password_field :password_confirmation, autocomplete: "new-password" %>
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
<div>
|
|
26
|
+
<%= f.submit "Update Password" %>
|
|
27
|
+
</div>
|
|
28
|
+
<% end %>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<h2>Forgot Password</h2>
|
|
2
|
+
|
|
3
|
+
<%= form_with url: rails_auth.password_resets_path do |f| %>
|
|
4
|
+
<div>
|
|
5
|
+
<%= f.label :email %>
|
|
6
|
+
<%= f.email_field :email, autofocus: true, autocomplete: "email" %>
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
<div>
|
|
10
|
+
<%= f.submit "Send reset instructions" %>
|
|
11
|
+
</div>
|
|
12
|
+
<% end %>
|
|
13
|
+
|
|
14
|
+
<%= link_to "Back to Sign in", rails_auth.new_session_path %>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<h2>Edit Profile</h2>
|
|
2
|
+
|
|
3
|
+
<%= form_with model: @user, url: rails_auth.profile_path, method: :patch do |f| %>
|
|
4
|
+
<% if @user.errors.any? %>
|
|
5
|
+
<div id="error_explanation">
|
|
6
|
+
<h2><%= pluralize(@user.errors.count, "error") %> prohibited this profile from being saved:</h2>
|
|
7
|
+
<ul>
|
|
8
|
+
<% @user.errors.full_messages.each do |message| %>
|
|
9
|
+
<li><%= message %></li>
|
|
10
|
+
<% end %>
|
|
11
|
+
</ul>
|
|
12
|
+
</div>
|
|
13
|
+
<% end %>
|
|
14
|
+
|
|
15
|
+
<div>
|
|
16
|
+
<% if @user.avatar.attached? %>
|
|
17
|
+
<%= image_tag @user.avatar.variant(resize_to_limit: [100, 100]) %>
|
|
18
|
+
<% end %>
|
|
19
|
+
<%= f.label :avatar %>
|
|
20
|
+
<%= f.file_field :avatar %>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<div>
|
|
24
|
+
<%= f.label :email %>
|
|
25
|
+
<%= f.email_field :email, autofocus: true, autocomplete: "email" %>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<div>
|
|
29
|
+
<%= f.label :password %>
|
|
30
|
+
<i>(leave blank if you don't want to change it)</i>
|
|
31
|
+
<%= f.password_field :password, autocomplete: "new-password" %>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<div>
|
|
35
|
+
<%= f.label :password_confirmation %>
|
|
36
|
+
<%= f.password_field :password_confirmation, autocomplete: "new-password" %>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<div>
|
|
40
|
+
<%= f.submit "Update Profile" %>
|
|
41
|
+
</div>
|
|
42
|
+
<% end %>
|
|
43
|
+
|
|
44
|
+
<%= link_to "Back to Security", rails_auth.security_sessions_path %>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
<h2>Sign up</h2>
|
|
2
|
+
|
|
3
|
+
<%= form_with model: @user, url: rails_auth.registration_path do |f| %>
|
|
4
|
+
<% if @user.errors.any? %>
|
|
5
|
+
<div id="error_explanation">
|
|
6
|
+
<h2><%= pluralize(@user.errors.count, "error") %> prohibited this user from being saved:</h2>
|
|
7
|
+
<ul>
|
|
8
|
+
<% @user.errors.full_messages.each do |message| %>
|
|
9
|
+
<li><%= message %></li>
|
|
10
|
+
<% end %>
|
|
11
|
+
</ul>
|
|
12
|
+
</div>
|
|
13
|
+
<% end %>
|
|
14
|
+
|
|
15
|
+
<div>
|
|
16
|
+
<%= f.label :email %>
|
|
17
|
+
<%= f.email_field :email, autofocus: true, autocomplete: "email" %>
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<div>
|
|
21
|
+
<%= f.label :avatar %>
|
|
22
|
+
<%= f.file_field :avatar %>
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
<div>
|
|
26
|
+
<%= f.label :password %>
|
|
27
|
+
<%= f.password_field :password, autocomplete: "new-password" %>
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
<div>
|
|
31
|
+
<%= f.label :password_confirmation %>
|
|
32
|
+
<%= f.password_field :password_confirmation, autocomplete: "new-password" %>
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
<div>
|
|
36
|
+
<%= f.submit "Sign up" %>
|
|
37
|
+
</div>
|
|
38
|
+
<% end %>
|
|
39
|
+
|
|
40
|
+
<%= link_to "Sign in", rails_auth.new_session_path %>
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
<h2>Account Security</h2>
|
|
2
|
+
|
|
3
|
+
<div style="margin-bottom: 20px;">
|
|
4
|
+
<% if current_user.avatar.attached? %>
|
|
5
|
+
<%= image_tag current_user.avatar.variant(resize_to_limit: [100, 100]), style: "border-radius: 50%;" %>
|
|
6
|
+
<% else %>
|
|
7
|
+
<div style="width: 100px; height: 100px; background: #eee; border-radius: 50%; display: flex; align-items: center; justify-content: center;">
|
|
8
|
+
No Avatar
|
|
9
|
+
</div>
|
|
10
|
+
<% end %>
|
|
11
|
+
<p><strong><%= current_user.email %></strong> (<%= current_user.role.capitalize %>)</p>
|
|
12
|
+
<%= link_to "Edit Profile & Avatar", rails_auth.edit_profile_path %>
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
<hr>
|
|
16
|
+
|
|
17
|
+
<h2>Security Audit Log</h2>
|
|
18
|
+
|
|
19
|
+
<table>
|
|
20
|
+
<thead>
|
|
21
|
+
<tr>
|
|
22
|
+
<th>Event</th>
|
|
23
|
+
<th>IP Address</th>
|
|
24
|
+
<th>Date</th>
|
|
25
|
+
<th>Details</th>
|
|
26
|
+
</tr>
|
|
27
|
+
</thead>
|
|
28
|
+
<tbody>
|
|
29
|
+
<% current_user.security_events.order(created_at: :desc).limit(20).each do |event| %>
|
|
30
|
+
<tr>
|
|
31
|
+
<td><%= event.event_type.titleize %></td>
|
|
32
|
+
<td><%= event.ip_address %></td>
|
|
33
|
+
<td><%= event.created_at.strftime("%Y-%m-%d %H:%M:%S") %></td>
|
|
34
|
+
<td><small><%= event.details %></small></td>
|
|
35
|
+
</tr>
|
|
36
|
+
<% end %>
|
|
37
|
+
</tbody>
|
|
38
|
+
</table>
|
|
39
|
+
|
|
40
|
+
<hr>
|
|
41
|
+
|
|
42
|
+
<h2>Active Sessions</h2>
|
|
43
|
+
|
|
44
|
+
<%= button_to "Sign out of all devices", rails_auth.revoke_all_sessions_path, method: :delete, data: { confirm: "Are you sure? This will sign you out of all devices including this one." } %>
|
|
45
|
+
|
|
46
|
+
<hr>
|
|
47
|
+
|
|
48
|
+
<div>
|
|
49
|
+
<strong>Two-Factor Authentication (MFA)</strong>
|
|
50
|
+
<% if current_user.otp_enabled? %>
|
|
51
|
+
<span style="color: green;">Enabled</span>
|
|
52
|
+
<% else %>
|
|
53
|
+
<span style="color: red;">Disabled</span>
|
|
54
|
+
<% end %>
|
|
55
|
+
<%= link_to "Manage MFA", rails_auth.mfa_path %>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<hr>
|
|
59
|
+
|
|
60
|
+
<table>
|
|
61
|
+
<thead>
|
|
62
|
+
<tr>
|
|
63
|
+
<th>Device / Browser</th>
|
|
64
|
+
<th>IP Address</th>
|
|
65
|
+
<th>Last Active</th>
|
|
66
|
+
<th>Status</th>
|
|
67
|
+
<th>Action</th>
|
|
68
|
+
</tr>
|
|
69
|
+
</thead>
|
|
70
|
+
<tbody>
|
|
71
|
+
<% @sessions.each do |session| %>
|
|
72
|
+
<tr>
|
|
73
|
+
<td>
|
|
74
|
+
<strong><%= session.browser || "Unknown Browser" %></strong><br>
|
|
75
|
+
<small><%= session.os || "Unknown OS" %></small>
|
|
76
|
+
</td>
|
|
77
|
+
<td><%= session.ip_address %></td>
|
|
78
|
+
<td><%= time_ago_in_words(session.last_active_at) %> ago</td>
|
|
79
|
+
<td>
|
|
80
|
+
<% if session == current_session %>
|
|
81
|
+
<strong>Current</strong>
|
|
82
|
+
<% else %>
|
|
83
|
+
Active
|
|
84
|
+
<% end %>
|
|
85
|
+
</td>
|
|
86
|
+
<td>
|
|
87
|
+
<%= button_to "Revoke", rails_auth.revoke_session_path(session), method: :delete, data: { confirm: "Are you sure?" } %>
|
|
88
|
+
</td>
|
|
89
|
+
</tr>
|
|
90
|
+
<% end %>
|
|
91
|
+
</tbody>
|
|
92
|
+
</table>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<h2>Sign in</h2>
|
|
2
|
+
|
|
3
|
+
<%= form_with url: rails_auth.session_path do |f| %>
|
|
4
|
+
<div>
|
|
5
|
+
<%= f.label :email %>
|
|
6
|
+
<%= f.email_field :email, autofocus: true, autocomplete: "email" %>
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
<div>
|
|
10
|
+
<%= f.label :password %>
|
|
11
|
+
<%= f.password_field :password, autocomplete: "current-password" %>
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
<div>
|
|
15
|
+
<%= f.submit "Sign in" %>
|
|
16
|
+
<%= link_to "Forgot password?", rails_auth.new_password_reset_path %>
|
|
17
|
+
</div>
|
|
18
|
+
<% end %>
|
|
19
|
+
|
|
20
|
+
<%= link_to "Sign up", rails_auth.new_registration_path %>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<h1>Password Reset</h1>
|
|
2
|
+
|
|
3
|
+
<p>To reset your password, click the link below:</p>
|
|
4
|
+
|
|
5
|
+
<p><%= link_to "Reset Password", rails_auth.edit_password_reset_url(@user.reset_token) %></p>
|
|
6
|
+
|
|
7
|
+
<p>If you did not request a password reset, please ignore this email.</p>
|
|
8
|
+
<p>This link will expire in 2 hours.</p>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<h1>Hello <%= @user.email %>!</h1>
|
|
2
|
+
|
|
3
|
+
<p>Your account has been locked due to an excessive number of unsuccessful sign in attempts.</p>
|
|
4
|
+
|
|
5
|
+
<p>Click the link below to unlock your account:</p>
|
|
6
|
+
|
|
7
|
+
<p><%= link_to 'Unlock my account', rails_auth.unlock_url(unlock_token: @user.unlock_token) %></p>
|