better_authy 0.1.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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +123 -0
- data/Rakefile +3 -0
- data/app/controllers/better_authy/base_controller.rb +39 -0
- data/app/controllers/better_authy/passwords_controller.rb +92 -0
- data/app/controllers/better_authy/registrations_controller.rb +27 -0
- data/app/controllers/better_authy/sessions_controller.rb +48 -0
- data/app/helpers/better_authy/application_helper.rb +7 -0
- data/app/mailers/better_authy/application_mailer.rb +13 -0
- data/app/mailers/better_authy/password_reset_mailer.rb +20 -0
- data/app/models/better_authy/forgot_password_form.rb +12 -0
- data/app/models/better_authy/reset_password_form.rb +15 -0
- data/app/models/better_authy/session_form.rb +15 -0
- data/app/views/better_authy/password_reset_mailer/reset_password_instructions.html.erb +13 -0
- data/app/views/better_authy/password_reset_mailer/reset_password_instructions.text.erb +9 -0
- data/app/views/better_authy/passwords/edit.html.erb +55 -0
- data/app/views/better_authy/passwords/new.html.erb +61 -0
- data/app/views/better_authy/registrations/new.html.erb +59 -0
- data/app/views/better_authy/sessions/new.html.erb +80 -0
- data/app/views/layouts/better_authy/application.html.erb +20 -0
- data/config/locales/en.yml +48 -0
- data/config/locales/it.yml +48 -0
- data/config/routes.rb +23 -0
- data/lib/better_authy/configuration.rb +43 -0
- data/lib/better_authy/controller_helpers.rb +97 -0
- data/lib/better_authy/engine.rb +11 -0
- data/lib/better_authy/errors.rb +6 -0
- data/lib/better_authy/model_extensions.rb +23 -0
- data/lib/better_authy/models/authenticable.rb +118 -0
- data/lib/better_authy/scope_configuration.rb +30 -0
- data/lib/better_authy/version.rb +5 -0
- data/lib/better_authy.rb +37 -0
- metadata +137 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
<% content_for :title, t("better_authy.sessions.title") %>
|
|
2
|
+
|
|
3
|
+
<div class="w-full max-w-md">
|
|
4
|
+
<% if flash[:alert].present? %>
|
|
5
|
+
<%= bui_action_messages([flash[:alert]],
|
|
6
|
+
variant: :danger,
|
|
7
|
+
style: :soft,
|
|
8
|
+
dismissible: true,
|
|
9
|
+
container_classes: "mb-6"
|
|
10
|
+
) %>
|
|
11
|
+
<% end %>
|
|
12
|
+
|
|
13
|
+
<% if flash[:notice].present? %>
|
|
14
|
+
<%= bui_action_messages([flash[:notice]],
|
|
15
|
+
variant: :success,
|
|
16
|
+
style: :soft,
|
|
17
|
+
dismissible: true,
|
|
18
|
+
container_classes: "mb-6"
|
|
19
|
+
) %>
|
|
20
|
+
<% end %>
|
|
21
|
+
|
|
22
|
+
<%= bui_card(
|
|
23
|
+
variant: :light,
|
|
24
|
+
style: :bordered,
|
|
25
|
+
size: :lg,
|
|
26
|
+
shadow: true
|
|
27
|
+
) do |card| %>
|
|
28
|
+
<% card.with_header do %>
|
|
29
|
+
<h1 class="text-2xl font-bold text-center text-grayscale-900">
|
|
30
|
+
<%= t("better_authy.sessions.title") %>
|
|
31
|
+
</h1>
|
|
32
|
+
<% end %>
|
|
33
|
+
|
|
34
|
+
<% card.with_body do %>
|
|
35
|
+
<%= form_with model: @session_form, scope: :session, url: request.path, builder: BetterUi::UiFormBuilder, class: "space-y-6" do |f| %>
|
|
36
|
+
<%= f.bui_text_input :email,
|
|
37
|
+
label: t("better_authy.attributes.email"),
|
|
38
|
+
placeholder: t("better_authy.placeholders.email"),
|
|
39
|
+
type: "email",
|
|
40
|
+
autocomplete: "email" %>
|
|
41
|
+
|
|
42
|
+
<%= f.bui_password_input :password,
|
|
43
|
+
label: t("better_authy.attributes.password"),
|
|
44
|
+
placeholder: t("better_authy.placeholders.password"),
|
|
45
|
+
autocomplete: "current-password" %>
|
|
46
|
+
|
|
47
|
+
<div class="flex items-center justify-between">
|
|
48
|
+
<%= f.bui_checkbox :remember_me,
|
|
49
|
+
label: t("better_authy.sessions.remember_me") %>
|
|
50
|
+
|
|
51
|
+
<%= link_to t("better_authy.sessions.forgot_password"),
|
|
52
|
+
send(:"new_#{params[:scope]}_password_path"),
|
|
53
|
+
class: "text-sm font-medium text-primary-600 hover:text-primary-500" %>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<div>
|
|
57
|
+
<%= bui_button(
|
|
58
|
+
variant: :primary,
|
|
59
|
+
style: :solid,
|
|
60
|
+
size: :lg,
|
|
61
|
+
type: :submit,
|
|
62
|
+
show_loader_on_click: true,
|
|
63
|
+
container_classes: "w-full"
|
|
64
|
+
) do %>
|
|
65
|
+
<%= t("better_authy.sessions.submit") %>
|
|
66
|
+
<% end %>
|
|
67
|
+
</div>
|
|
68
|
+
<% end %>
|
|
69
|
+
<% end %>
|
|
70
|
+
|
|
71
|
+
<% card.with_footer do %>
|
|
72
|
+
<p class="text-center text-sm text-grayscale-600">
|
|
73
|
+
<%= t("better_authy.sessions.no_account") %>
|
|
74
|
+
<%= link_to t("better_authy.sessions.signup_link"),
|
|
75
|
+
send(:"#{params[:scope]}_signup_path"),
|
|
76
|
+
class: "font-medium text-primary-600 hover:text-primary-500" %>
|
|
77
|
+
</p>
|
|
78
|
+
<% end %>
|
|
79
|
+
<% end %>
|
|
80
|
+
</div>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<title><%= content_for(:title) || "Authentication" %></title>
|
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
6
|
+
<%= csrf_meta_tags %>
|
|
7
|
+
<%= csp_meta_tag %>
|
|
8
|
+
|
|
9
|
+
<%= yield :head %>
|
|
10
|
+
|
|
11
|
+
<%= vite_stylesheet_link_tag "application.css" if defined?(vite_stylesheet_link_tag) %>
|
|
12
|
+
<%= vite_javascript_include_tag "application.js" if defined?(vite_javascript_include_tag) %>
|
|
13
|
+
</head>
|
|
14
|
+
|
|
15
|
+
<body class="bg-grayscale-50 min-h-screen">
|
|
16
|
+
<div class="flex min-h-screen flex-col items-center justify-center px-4 py-12">
|
|
17
|
+
<%= yield %>
|
|
18
|
+
</div>
|
|
19
|
+
</body>
|
|
20
|
+
</html>
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
en:
|
|
2
|
+
better_authy:
|
|
3
|
+
sessions:
|
|
4
|
+
title: "Sign In"
|
|
5
|
+
submit: "Sign In"
|
|
6
|
+
remember_me: "Remember me"
|
|
7
|
+
signed_in: "Signed in successfully."
|
|
8
|
+
signed_out: "Signed out successfully."
|
|
9
|
+
invalid_credentials: "Invalid email or password."
|
|
10
|
+
unauthenticated: "You need to sign in to continue."
|
|
11
|
+
no_account: "Don't have an account?"
|
|
12
|
+
signup_link: "Sign up"
|
|
13
|
+
forgot_password: "Forgot password?"
|
|
14
|
+
registrations:
|
|
15
|
+
title: "Sign Up"
|
|
16
|
+
submit: "Create account"
|
|
17
|
+
errors_title: "Please correct the following errors:"
|
|
18
|
+
signed_up: "Welcome! You have signed up successfully."
|
|
19
|
+
has_account: "Already have an account?"
|
|
20
|
+
login_link: "Sign in"
|
|
21
|
+
attributes:
|
|
22
|
+
email: "Email"
|
|
23
|
+
password: "Password"
|
|
24
|
+
password_confirmation: "Password confirmation"
|
|
25
|
+
placeholders:
|
|
26
|
+
email: "you@example.com"
|
|
27
|
+
password: "Enter your password"
|
|
28
|
+
password_confirmation: "Confirm your password"
|
|
29
|
+
hints:
|
|
30
|
+
password: "Minimum 8 characters"
|
|
31
|
+
passwords:
|
|
32
|
+
forgot_title: "Forgot Password"
|
|
33
|
+
forgot_instruction: "Enter your email address and we'll send you instructions to reset your password."
|
|
34
|
+
forgot_submit: "Send Reset Instructions"
|
|
35
|
+
remembered: "Remember your password?"
|
|
36
|
+
reset_title: "Reset Password"
|
|
37
|
+
reset_submit: "Reset Password"
|
|
38
|
+
send_instructions: "If your email address exists in our database, you will receive password reset instructions shortly."
|
|
39
|
+
updated: "Your password has been changed successfully. You can now sign in."
|
|
40
|
+
invalid_token: "The password reset link is invalid or has expired."
|
|
41
|
+
no_token: "No password reset token provided."
|
|
42
|
+
mailer:
|
|
43
|
+
subject: "Password Reset Instructions"
|
|
44
|
+
greeting: "Hello %{email}!"
|
|
45
|
+
instruction: "Someone has requested a link to change your password. You can do this through the link below."
|
|
46
|
+
action: "Change my password"
|
|
47
|
+
ignore: "If you didn't request this, please ignore this email."
|
|
48
|
+
expiration: "This link will expire in %{hours} hour(s)."
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
it:
|
|
2
|
+
better_authy:
|
|
3
|
+
sessions:
|
|
4
|
+
title: "Accedi"
|
|
5
|
+
submit: "Accedi"
|
|
6
|
+
remember_me: "Ricordami"
|
|
7
|
+
signed_in: "Accesso effettuato."
|
|
8
|
+
signed_out: "Disconnessione effettuata."
|
|
9
|
+
invalid_credentials: "Email o password non validi."
|
|
10
|
+
unauthenticated: "Devi effettuare l'accesso."
|
|
11
|
+
no_account: "Non hai un account?"
|
|
12
|
+
signup_link: "Registrati"
|
|
13
|
+
forgot_password: "Password dimenticata?"
|
|
14
|
+
registrations:
|
|
15
|
+
title: "Registrati"
|
|
16
|
+
submit: "Crea account"
|
|
17
|
+
errors_title: "Correggi i seguenti errori:"
|
|
18
|
+
signed_up: "Registrazione completata."
|
|
19
|
+
has_account: "Hai già un account?"
|
|
20
|
+
login_link: "Accedi"
|
|
21
|
+
attributes:
|
|
22
|
+
email: "Email"
|
|
23
|
+
password: "Password"
|
|
24
|
+
password_confirmation: "Conferma password"
|
|
25
|
+
placeholders:
|
|
26
|
+
email: "tu@esempio.com"
|
|
27
|
+
password: "Inserisci la password"
|
|
28
|
+
password_confirmation: "Conferma la password"
|
|
29
|
+
hints:
|
|
30
|
+
password: "Minimo 8 caratteri"
|
|
31
|
+
passwords:
|
|
32
|
+
forgot_title: "Password dimenticata"
|
|
33
|
+
forgot_instruction: "Inserisci il tuo indirizzo email e ti invieremo le istruzioni per reimpostare la password."
|
|
34
|
+
forgot_submit: "Invia istruzioni"
|
|
35
|
+
remembered: "Ricordi la password?"
|
|
36
|
+
reset_title: "Reimposta password"
|
|
37
|
+
reset_submit: "Reimposta password"
|
|
38
|
+
send_instructions: "Se il tuo indirizzo email esiste nel nostro database, riceverai le istruzioni per reimpostare la password a breve."
|
|
39
|
+
updated: "La tua password e' stata modificata con successo. Ora puoi accedere."
|
|
40
|
+
invalid_token: "Il link per reimpostare la password non e' valido o e' scaduto."
|
|
41
|
+
no_token: "Token per reimpostare la password non fornito."
|
|
42
|
+
mailer:
|
|
43
|
+
subject: "Istruzioni per reimpostare la password"
|
|
44
|
+
greeting: "Ciao %{email}!"
|
|
45
|
+
instruction: "Qualcuno ha richiesto un link per cambiare la tua password. Puoi farlo tramite il link qui sotto."
|
|
46
|
+
action: "Cambia la mia password"
|
|
47
|
+
ignore: "Se non hai richiesto tu questa operazione, ignora questa email."
|
|
48
|
+
expiration: "Questo link scadra' tra %{hours} ora/e."
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
BetterAuthy::Engine.routes.draw do
|
|
4
|
+
# Generate routes for each configured scope
|
|
5
|
+
# The defaults: { scope: scope_name } injects the scope into params
|
|
6
|
+
# so controllers don't need to parse the URL path
|
|
7
|
+
BetterAuthy.configuration.scopes.each_key do |scope_name|
|
|
8
|
+
scope scope_name.to_s, defaults: { scope: scope_name } do
|
|
9
|
+
get "login", to: "sessions#new", as: :"#{scope_name}_login"
|
|
10
|
+
post "login", to: "sessions#create"
|
|
11
|
+
delete "logout", to: "sessions#destroy", as: :"#{scope_name}_logout"
|
|
12
|
+
|
|
13
|
+
get "signup", to: "registrations#new", as: :"#{scope_name}_signup"
|
|
14
|
+
post "signup", to: "registrations#create"
|
|
15
|
+
|
|
16
|
+
# Password reset routes
|
|
17
|
+
get "password/new", to: "passwords#new", as: :"new_#{scope_name}_password"
|
|
18
|
+
post "password", to: "passwords#create", as: :"#{scope_name}_password"
|
|
19
|
+
get "password/edit", to: "passwords#edit", as: :"edit_#{scope_name}_password"
|
|
20
|
+
patch "password", to: "passwords#update"
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterAuthy
|
|
4
|
+
class Configuration
|
|
5
|
+
attr_reader :scopes, :cookie_config
|
|
6
|
+
|
|
7
|
+
def initialize
|
|
8
|
+
@scopes = {}
|
|
9
|
+
@cookie_config = {
|
|
10
|
+
secure: ENV.fetch("BETTER_AUTHY_SECURE_COOKIES", "false") == "true",
|
|
11
|
+
httponly: true,
|
|
12
|
+
same_site: :lax
|
|
13
|
+
}
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def cookie_config=(options)
|
|
17
|
+
@cookie_config = @cookie_config.merge(options)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def scope(name, &block)
|
|
21
|
+
raise ArgumentError, "scope requires a block" unless block_given?
|
|
22
|
+
|
|
23
|
+
name = name.to_sym
|
|
24
|
+
raise ConfigurationError, "Scope :#{name} is already registered" if @scopes.key?(name)
|
|
25
|
+
|
|
26
|
+
scope_config = ScopeConfiguration.new(name)
|
|
27
|
+
yield(scope_config)
|
|
28
|
+
@scopes[name] = scope_config
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def scope_for(name)
|
|
32
|
+
@scopes[name.to_sym]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def scope_for!(name)
|
|
36
|
+
scope_for(name) || raise(ConfigurationError, "Scope :#{name} is not registered")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def validate!
|
|
40
|
+
@scopes.each_value(&:validate!)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterAuthy
|
|
4
|
+
module ControllerHelpers
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
included do
|
|
8
|
+
# Define helper methods for each configured scope
|
|
9
|
+
BetterAuthy.configuration.scopes.each_key do |scope_name|
|
|
10
|
+
define_scope_helpers(scope_name)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
class_methods do
|
|
15
|
+
def define_scope_helpers(scope_name)
|
|
16
|
+
# current_{scope}
|
|
17
|
+
define_method(:"current_#{scope_name}") do
|
|
18
|
+
ivar = "@current_#{scope_name}"
|
|
19
|
+
return instance_variable_get(ivar) if instance_variable_defined?(ivar)
|
|
20
|
+
|
|
21
|
+
instance_variable_set(ivar, find_authenticated_resource(scope_name))
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# {scope}_signed_in?
|
|
25
|
+
define_method(:"#{scope_name}_signed_in?") do
|
|
26
|
+
send(:"current_#{scope_name}").present?
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# sign_in_{scope}
|
|
30
|
+
define_method(:"sign_in_#{scope_name}") do |resource, remember: false|
|
|
31
|
+
scope_config = BetterAuthy.scope_for!(scope_name)
|
|
32
|
+
|
|
33
|
+
reset_session
|
|
34
|
+
session[scope_config.session_key] = resource.id
|
|
35
|
+
resource.track_sign_in!(request)
|
|
36
|
+
|
|
37
|
+
if remember
|
|
38
|
+
token = resource.remember_me!
|
|
39
|
+
cookie_value = "#{resource.id}:#{token}"
|
|
40
|
+
cookies.encrypted[scope_config.remember_cookie] = {
|
|
41
|
+
value: cookie_value,
|
|
42
|
+
expires: scope_config.remember_for.from_now,
|
|
43
|
+
**BetterAuthy.configuration.cookie_config
|
|
44
|
+
}
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
instance_variable_set("@current_#{scope_name}", resource)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# sign_out_{scope}
|
|
51
|
+
define_method(:"sign_out_#{scope_name}") do
|
|
52
|
+
scope_config = BetterAuthy.scope_for!(scope_name)
|
|
53
|
+
current_resource = send(:"current_#{scope_name}")
|
|
54
|
+
|
|
55
|
+
current_resource&.forget_me!
|
|
56
|
+
|
|
57
|
+
session.delete(scope_config.session_key)
|
|
58
|
+
cookies.delete(scope_config.remember_cookie)
|
|
59
|
+
|
|
60
|
+
instance_variable_set("@current_#{scope_name}", nil)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# authenticate_{scope}!
|
|
64
|
+
define_method(:"authenticate_#{scope_name}!") do
|
|
65
|
+
return if send(:"#{scope_name}_signed_in?")
|
|
66
|
+
|
|
67
|
+
scope_config = BetterAuthy.scope_for!(scope_name)
|
|
68
|
+
redirect_to(scope_config.sign_in_path)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Register as helper methods
|
|
72
|
+
helper_method :"current_#{scope_name}", :"#{scope_name}_signed_in?"
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def find_authenticated_resource(scope_name)
|
|
79
|
+
scope_config = BetterAuthy.scope_for!(scope_name)
|
|
80
|
+
model_class = scope_config.model_class
|
|
81
|
+
|
|
82
|
+
# Try session first
|
|
83
|
+
if (resource_id = session[scope_config.session_key])
|
|
84
|
+
return model_class.find_by(id: resource_id)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Try remember cookie
|
|
88
|
+
if (cookie_value = cookies.encrypted[scope_config.remember_cookie])
|
|
89
|
+
resource_id, token = cookie_value.split(":", 2)
|
|
90
|
+
resource = model_class.find_by(id: resource_id)
|
|
91
|
+
return resource if resource&.remember_token_valid?(token)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
nil
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterAuthy
|
|
4
|
+
module ModelExtensions
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
class_methods do
|
|
8
|
+
def better_authy_authenticable(scope_name, **options)
|
|
9
|
+
# Store scope name and options on the class
|
|
10
|
+
class_attribute :authenticable_scope_name, default: scope_name
|
|
11
|
+
class_attribute :authenticable_options, default: options
|
|
12
|
+
|
|
13
|
+
# Include the authenticable concern (runs included do block)
|
|
14
|
+
include BetterAuthy::Models::Authenticable
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Extend ActiveRecord::Base when ActiveRecord is loaded
|
|
21
|
+
ActiveSupport.on_load(:active_record) do
|
|
22
|
+
include BetterAuthy::ModelExtensions
|
|
23
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterAuthy
|
|
4
|
+
module Models
|
|
5
|
+
module Authenticable
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
included do
|
|
9
|
+
# Configure has_secure_password
|
|
10
|
+
has_secure_password
|
|
11
|
+
|
|
12
|
+
# Email validations
|
|
13
|
+
validates :email,
|
|
14
|
+
presence: true,
|
|
15
|
+
uniqueness: { case_sensitive: false },
|
|
16
|
+
format: { with: URI::MailTo::EMAIL_REGEXP }
|
|
17
|
+
|
|
18
|
+
# Password validation (uses options from better_authy_authenticable call)
|
|
19
|
+
validates :password,
|
|
20
|
+
length: { minimum: authenticable_options.fetch(:password_minimum, 8) },
|
|
21
|
+
allow_nil: true
|
|
22
|
+
|
|
23
|
+
# Email normalization
|
|
24
|
+
normalizes :email, with: ->(email) { email.strip.downcase }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Returns the scope name for this model
|
|
28
|
+
def authenticable_scope
|
|
29
|
+
self.class.authenticable_scope_name
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Returns the scope configuration
|
|
33
|
+
def authenticable_scope_config
|
|
34
|
+
BetterAuthy.scope_for(authenticable_scope)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Generate remember token
|
|
38
|
+
def remember_me!
|
|
39
|
+
token = SecureRandom.urlsafe_base64(32)
|
|
40
|
+
update!(
|
|
41
|
+
remember_token_digest: BCrypt::Password.create(token),
|
|
42
|
+
remember_created_at: Time.current
|
|
43
|
+
)
|
|
44
|
+
token
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Clear remember token
|
|
48
|
+
def forget_me!
|
|
49
|
+
update!(
|
|
50
|
+
remember_token_digest: nil,
|
|
51
|
+
remember_created_at: nil
|
|
52
|
+
)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Validate remember token
|
|
56
|
+
def remember_token_valid?(token)
|
|
57
|
+
return false if remember_token_digest.blank?
|
|
58
|
+
return false if remember_created_at.blank?
|
|
59
|
+
return false if remember_created_at < authenticable_scope_config.remember_for.ago
|
|
60
|
+
|
|
61
|
+
BCrypt::Password.new(remember_token_digest).is_password?(token)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Track sign in
|
|
65
|
+
def track_sign_in!(request)
|
|
66
|
+
now = Time.current
|
|
67
|
+
update!(
|
|
68
|
+
sign_in_count: sign_in_count + 1,
|
|
69
|
+
last_sign_in_at: current_sign_in_at,
|
|
70
|
+
last_sign_in_ip: current_sign_in_ip,
|
|
71
|
+
current_sign_in_at: now,
|
|
72
|
+
current_sign_in_ip: request.remote_ip
|
|
73
|
+
)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Generate password reset token
|
|
77
|
+
def generate_password_reset_token!
|
|
78
|
+
token = SecureRandom.urlsafe_base64(32)
|
|
79
|
+
update!(
|
|
80
|
+
password_reset_token_digest: BCrypt::Password.create(token),
|
|
81
|
+
password_reset_sent_at: Time.current
|
|
82
|
+
)
|
|
83
|
+
token
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Validate password reset token
|
|
87
|
+
def password_reset_token_valid?(token)
|
|
88
|
+
return false if password_reset_token_digest.blank?
|
|
89
|
+
return false if password_reset_sent_at.blank?
|
|
90
|
+
return false if password_reset_sent_at < authenticable_scope_config.password_reset_within.ago
|
|
91
|
+
|
|
92
|
+
BCrypt::Password.new(password_reset_token_digest).is_password?(token)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Clear password reset token
|
|
96
|
+
def clear_password_reset_token!
|
|
97
|
+
update!(
|
|
98
|
+
password_reset_token_digest: nil,
|
|
99
|
+
password_reset_sent_at: nil
|
|
100
|
+
)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Reset password with confirmation
|
|
104
|
+
def reset_password!(new_password, new_password_confirmation)
|
|
105
|
+
self.password = new_password
|
|
106
|
+
self.password_confirmation = new_password_confirmation
|
|
107
|
+
|
|
108
|
+
if valid?
|
|
109
|
+
save!
|
|
110
|
+
clear_password_reset_token!
|
|
111
|
+
true
|
|
112
|
+
else
|
|
113
|
+
false
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterAuthy
|
|
4
|
+
class ScopeConfiguration
|
|
5
|
+
attr_reader :name
|
|
6
|
+
attr_accessor :model_name, :session_key, :remember_cookie, :remember_for,
|
|
7
|
+
:sign_in_path, :after_sign_in_path, :layout, :password_reset_within
|
|
8
|
+
|
|
9
|
+
def initialize(name)
|
|
10
|
+
@name = name.to_sym
|
|
11
|
+
@session_key = :"#{name}_id"
|
|
12
|
+
@remember_cookie = :"_remember_#{name}_token"
|
|
13
|
+
@remember_for = 2.weeks
|
|
14
|
+
@password_reset_within = 1.hour
|
|
15
|
+
@sign_in_path = "/auth/#{name}/login"
|
|
16
|
+
@after_sign_in_path = "/"
|
|
17
|
+
@layout = "better_authy/application"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def model_class
|
|
21
|
+
raise ConfigurationError, "model_name is required for scope :#{name}" if model_name.blank?
|
|
22
|
+
|
|
23
|
+
model_name.constantize
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def validate!
|
|
27
|
+
raise ConfigurationError, "model_name is required for scope :#{name}" if model_name.blank?
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
data/lib/better_authy.rb
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bcrypt"
|
|
4
|
+
require "better_ui"
|
|
5
|
+
|
|
6
|
+
require "better_authy/version"
|
|
7
|
+
require "better_authy/errors"
|
|
8
|
+
require "better_authy/scope_configuration"
|
|
9
|
+
require "better_authy/configuration"
|
|
10
|
+
require "better_authy/engine"
|
|
11
|
+
require "better_authy/models/authenticable"
|
|
12
|
+
require "better_authy/model_extensions"
|
|
13
|
+
require "better_authy/controller_helpers"
|
|
14
|
+
|
|
15
|
+
module BetterAuthy
|
|
16
|
+
class << self
|
|
17
|
+
def configure
|
|
18
|
+
yield(configuration)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def configuration
|
|
22
|
+
@configuration ||= Configuration.new
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def reset_configuration!
|
|
26
|
+
@configuration = Configuration.new
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def scope_for(name)
|
|
30
|
+
configuration.scope_for(name)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def scope_for!(name)
|
|
34
|
+
configuration.scope_for!(name)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|