rails_authentication 0.3.0 → 0.4.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 +4 -4
- data/README.md +24 -10
- data/lib/generators/authentication/authentication_generator.rb +16 -0
- data/lib/generators/authentication/features/ott.rb +30 -0
- data/lib/generators/authentication/templates/app/controllers/otts_controller.rb.tt +95 -0
- data/lib/generators/authentication/templates/app/mailers/otts_mailer.rb.tt +6 -0
- data/lib/generators/authentication/templates/app/models/concerns/ott_concern.rb.tt +50 -0
- data/lib/generators/authentication/templates/app/views/otts/edit.html.erb.tt +79 -0
- data/lib/generators/authentication/templates/app/views/otts_mailer/ott.html.erb.tt +8 -0
- data/lib/generators/authentication/templates/app/views/otts_mailer/ott.text.erb.tt +6 -0
- data/lib/generators/authentication/templates/app/views/sessions/new.html.erb.tt +16 -0
- data/lib/generators/authentication/templates/db/migrate/add_ott_to_users.rb.tt +7 -0
- data/lib/rails_authentication/version.rb +1 -1
- metadata +9 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f5c0f93e4dc6c9eb2562c7804920c82cff3a5a7d23d15758aaee3d63cdb60d99
|
|
4
|
+
data.tar.gz: 60ee05fafe8ffd4d9f56c10aa57c9ea5d347b801e94d3651ba2b04b258545a13
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bfd4e4526f964ef0ec6f0004b85061b64b86e6885c4e383adae282396b769f50e300de8e5424bd9892b59d5bae499d428be4719c6d8cdab0ec07fd76b64f5f3a
|
|
7
|
+
data.tar.gz: d4c2cb6365fd97356977944566aa332772c751061fcf8b1ad4b9e57b6d4eb7f0cd65ee497493d79286b1efb8bf85b897933e635b914e38a8994e419af61c1961
|
data/README.md
CHANGED
|
@@ -18,6 +18,7 @@ gem extends that same command to also install:
|
|
|
18
18
|
| **Lockable** | Locks the account after 5 failed attempts; unlock via email or automatically after 1 hour |
|
|
19
19
|
| **Invitable** | Invite users by email; blocks sign-in until they accept by choosing their own password |
|
|
20
20
|
| **MagicLink** (opt-in) | Passwordless magic link sign-in via email — DB-backed, single-use, expiring tokens |
|
|
21
|
+
| **Ott** (opt-in) | Passwordless sign-in via an emailed 6-digit one-time code — replaces the password sign-in form |
|
|
21
22
|
|
|
22
23
|
Everything is **generated into your app** as plain, readable code — controllers, views, mailers,
|
|
23
24
|
migrations, and one model concern per feature. There is no runtime dependency on this gem: after
|
|
@@ -28,7 +29,7 @@ This gem is meant to be installed temporarily. Install it long enough to run the
|
|
|
28
29
|
## Installation
|
|
29
30
|
|
|
30
31
|
Requires Rails >= 8.0 and, for the email-driven features (Confirmable, Recoverable, Lockable,
|
|
31
|
-
Invitable, MagicLink), Action Mailer.
|
|
32
|
+
Invitable, MagicLink, Ott), Action Mailer.
|
|
32
33
|
|
|
33
34
|
```ruby
|
|
34
35
|
# Gemfile
|
|
@@ -56,24 +57,37 @@ Available flags: `--skip-confirmable`, `--skip-recoverable`, `--skip-registerabl
|
|
|
56
57
|
`--skip-lockable`, `--skip-invitable`, and `--reconfirmable` (Confirmable: postpone email address
|
|
57
58
|
changes until the new address is confirmed, via an `unconfirmed_email` column).
|
|
58
59
|
|
|
59
|
-
MagicLink
|
|
60
|
+
MagicLink and Ott are **opt-in** features — they change the sign-in UX, so you have to ask for
|
|
61
|
+
them:
|
|
60
62
|
|
|
61
63
|
```sh
|
|
62
64
|
bin/rails generate authentication --magic-link
|
|
65
|
+
bin/rails generate authentication --ott
|
|
63
66
|
```
|
|
64
67
|
|
|
65
|
-
|
|
66
|
-
emailed link signs the user in directly; tokens are DB-backed, single-use, and expire
|
|
67
|
-
20 minutes (`MagicLinkConcern::MAGIC_LINK_EXPIRES_IN`).
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
MagicLink adds a "Sign in with magic link" link to the sign-in page, leading to an email-only
|
|
69
|
+
form. The emailed link signs the user in directly; tokens are DB-backed, single-use, and expire
|
|
70
|
+
after 20 minutes (`MagicLinkConcern::MAGIC_LINK_EXPIRES_IN`).
|
|
71
|
+
|
|
72
|
+
Ott replaces the sign-in form: the user enters only their email address, receives a 6-digit code
|
|
73
|
+
(`OttConcern::OTT_CODE_LENGTH`) by email, and enters it on a second screen — one single-digit
|
|
74
|
+
input per digit with auto-advance and paste/autofill support (plain inline JavaScript, no
|
|
75
|
+
framework required). A "Sign in with password
|
|
76
|
+
instead" link (`/session/new?with_password=1`) toggles back to the classic password form at
|
|
77
|
+
runtime. Codes are DB-backed, single-use, expire after 10 minutes (`OttConcern::OTT_EXPIRES_IN`),
|
|
78
|
+
and are voided after 5 wrong entries (`OttConcern::OTT_MAX_ATTEMPTS`). The password machinery
|
|
79
|
+
(SessionsController#create, Recoverable, Registerable) stays generated and functional — only the
|
|
80
|
+
sign-in UI changes.
|
|
81
|
+
|
|
82
|
+
Both flows honor the other enabled features: locked, unconfirmed, or invitation-pending accounts
|
|
83
|
+
still can't sign in, and Trackable records the attempt.
|
|
70
84
|
|
|
71
85
|
Each feature adds a single `include <Feature>Concern` line to `app/models/user.rb`; all of its
|
|
72
86
|
model behavior lives in `app/models/concerns/<feature>_concern.rb`. Tunables are plain constants in
|
|
73
87
|
the generated concerns — e.g. `TimeoutableConcern::TIMEOUT_IN`, `LockableConcern::MAXIMUM_ATTEMPTS`,
|
|
74
88
|
`LockableConcern::UNLOCK_IN`, `ConfirmableConcern::CONFIRMATION_TOKEN_EXPIRES_IN`,
|
|
75
|
-
`ValidatableConcern::PASSWORD_LENGTH`, `ValidatableConcern::PASSWORD_MINIMUM_COMPLEXITY
|
|
76
|
-
there. Concerns keep the model clean.
|
|
89
|
+
`ValidatableConcern::PASSWORD_LENGTH`, `ValidatableConcern::PASSWORD_MINIMUM_COMPLEXITY`,
|
|
90
|
+
`OttConcern::OTT_CODE_LENGTH` — edit them there. Concerns keep the model clean.
|
|
77
91
|
|
|
78
92
|
### Generated routes
|
|
79
93
|
|
|
@@ -83,6 +97,7 @@ resources :confirmations, only: %i[ new create show ], param: :token
|
|
|
83
97
|
resources :unlocks, only: %i[ new create show ], param: :token
|
|
84
98
|
resources :invitations, only: %i[ new create edit update ], param: :token
|
|
85
99
|
resources :magic_links, only: %i[ new create show ], param: :token # with --magic-link
|
|
100
|
+
resource :ott, only: %i[ create edit update ] # with --ott
|
|
86
101
|
resource :session # from the base generator
|
|
87
102
|
resources :passwords, param: :token # from the base generator
|
|
88
103
|
```
|
|
@@ -101,7 +116,6 @@ resources :passwords, param: :token # from the base generator
|
|
|
101
116
|
|
|
102
117
|
## Future Plans
|
|
103
118
|
|
|
104
|
-
- OTT (Email code)
|
|
105
119
|
- Passkey
|
|
106
120
|
|
|
107
121
|
## Development
|
|
@@ -12,6 +12,7 @@ require_relative "features/validatable"
|
|
|
12
12
|
require_relative "features/lockable"
|
|
13
13
|
require_relative "features/invitable"
|
|
14
14
|
require_relative "features/magic_link"
|
|
15
|
+
require_relative "features/ott"
|
|
15
16
|
|
|
16
17
|
module RailsAuthentication
|
|
17
18
|
module Generators
|
|
@@ -44,6 +45,7 @@ module RailsAuthentication
|
|
|
44
45
|
include Features::Lockable
|
|
45
46
|
include Features::Invitable
|
|
46
47
|
include Features::MagicLink
|
|
48
|
+
include Features::Ott
|
|
47
49
|
|
|
48
50
|
source_root File.expand_path("templates", __dir__)
|
|
49
51
|
|
|
@@ -60,6 +62,12 @@ module RailsAuthentication
|
|
|
60
62
|
class_option :magic_link, type: :boolean, default: false,
|
|
61
63
|
desc: "Add passwordless magic link sign-in (opt-in)"
|
|
62
64
|
|
|
65
|
+
# Also opt-in: replaces the sign-in form with an email-only form that
|
|
66
|
+
# emails a 6-digit one-time code, entered on a second screen. Password
|
|
67
|
+
# machinery stays generated and functional.
|
|
68
|
+
class_option :ott, type: :boolean, default: false,
|
|
69
|
+
desc: "Add one-time token (emailed 6-digit code) sign-in (opt-in)"
|
|
70
|
+
|
|
63
71
|
def install_base_authentication
|
|
64
72
|
say "Running Rails' built-in authentication generator", :green
|
|
65
73
|
Rails::Generators.invoke("rails:authentication", [], behavior: behavior, destination_root: destination_root)
|
|
@@ -105,6 +113,10 @@ module RailsAuthentication
|
|
|
105
113
|
generate_magic_link if magic_link?
|
|
106
114
|
end
|
|
107
115
|
|
|
116
|
+
def install_ott
|
|
117
|
+
generate_ott if ott?
|
|
118
|
+
end
|
|
119
|
+
|
|
108
120
|
# Runs after every feature install so the blank line separates the concern
|
|
109
121
|
# includes (if any) from the rest of the class body, no matter which features
|
|
110
122
|
# are enabled.
|
|
@@ -135,6 +147,10 @@ module RailsAuthentication
|
|
|
135
147
|
options[:magic_link]
|
|
136
148
|
end
|
|
137
149
|
|
|
150
|
+
def ott?
|
|
151
|
+
options[:ott]
|
|
152
|
+
end
|
|
153
|
+
|
|
138
154
|
def include_concern_in_user(concern)
|
|
139
155
|
inject_into_class "app/models/user.rb", "User", " include #{concern}\n"
|
|
140
156
|
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsAuthentication
|
|
4
|
+
module Generators
|
|
5
|
+
module Features
|
|
6
|
+
# Opt-in (--ott): one-time token sign-in. The sign-in page becomes an
|
|
7
|
+
# email-only form; a 6-digit code is emailed and entered on a second
|
|
8
|
+
# screen. Codes are DB-backed, single-use, expire after 10 minutes, and
|
|
9
|
+
# are voided after 5 wrong attempts. Password machinery (Recoverable,
|
|
10
|
+
# Registerable, SessionsController#create) is left intact.
|
|
11
|
+
module Ott
|
|
12
|
+
private
|
|
13
|
+
def generate_ott
|
|
14
|
+
template "app/models/concerns/ott_concern.rb"
|
|
15
|
+
include_concern_in_user "OttConcern"
|
|
16
|
+
migration_template "db/migrate/add_ott_to_users.rb", "db/migrate/add_ott_to_users.rb"
|
|
17
|
+
template "app/controllers/otts_controller.rb"
|
|
18
|
+
template "app/views/otts/edit.html.erb"
|
|
19
|
+
route "resource :ott, only: %i[ create edit update ]"
|
|
20
|
+
|
|
21
|
+
if defined?(ActionMailer::Railtie)
|
|
22
|
+
template "app/mailers/otts_mailer.rb"
|
|
23
|
+
template "app/views/otts_mailer/ott.html.erb"
|
|
24
|
+
template "app/views/otts_mailer/ott.text.erb"
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
class OttsController < ApplicationController
|
|
2
|
+
allow_unauthenticated_access
|
|
3
|
+
rate_limit to: 10, within: 3.minutes, only: %i[ create update ], with: -> { redirect_to new_session_path, alert: "Try again later." }
|
|
4
|
+
|
|
5
|
+
def create
|
|
6
|
+
email_address = params[:email_address].presence || session[:ott_email_address]
|
|
7
|
+
user = User.find_by(email_address: email_address)
|
|
8
|
+
user&.send_ott
|
|
9
|
+
session[:ott_email_address] = email_address
|
|
10
|
+
|
|
11
|
+
redirect_to edit_ott_path, notice: "We've emailed you a sign-in code (if a user with that email address exists)."
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def edit
|
|
15
|
+
redirect_to new_session_path if session[:ott_email_address].blank?
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def update
|
|
19
|
+
user = User.find_by(email_address: session[:ott_email_address])
|
|
20
|
+
|
|
21
|
+
case user && user.verify_ott(params[:code])
|
|
22
|
+
when nil, :expired
|
|
23
|
+
redirect_to new_session_path, alert: "Your sign-in code is invalid or has expired. Request a new one."
|
|
24
|
+
when :exhausted
|
|
25
|
+
<% if trackable? -%>
|
|
26
|
+
record_authentication_attempt(user, success: false, failure_reason: "ott_attempts_exhausted")
|
|
27
|
+
<% end -%>
|
|
28
|
+
redirect_to new_session_path, alert: "Too many incorrect attempts. Request a new code."
|
|
29
|
+
when :invalid
|
|
30
|
+
<% if trackable? -%>
|
|
31
|
+
record_authentication_attempt(user, success: false, failure_reason: "invalid_ott")
|
|
32
|
+
<% end -%>
|
|
33
|
+
redirect_to edit_ott_path, alert: "Incorrect code. Try again."
|
|
34
|
+
else
|
|
35
|
+
complete_ott_sign_in(user)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def complete_ott_sign_in(user)
|
|
42
|
+
<% if lockable? -%>
|
|
43
|
+
if user.locked?
|
|
44
|
+
<% if trackable? -%>
|
|
45
|
+
record_authentication_attempt(user, success: false, failure_reason: "locked")
|
|
46
|
+
<% end -%>
|
|
47
|
+
return redirect_to new_session_path, alert: "Your account is locked. Check your email for unlock instructions."
|
|
48
|
+
end
|
|
49
|
+
<% end -%>
|
|
50
|
+
<% if confirmable? -%>
|
|
51
|
+
unless user.confirmed?
|
|
52
|
+
<% if trackable? -%>
|
|
53
|
+
record_authentication_attempt(user, success: false, failure_reason: "unconfirmed")
|
|
54
|
+
<% end -%>
|
|
55
|
+
return redirect_to new_session_path, alert: "You must confirm your email address before signing in."
|
|
56
|
+
end
|
|
57
|
+
<% end -%>
|
|
58
|
+
<% if invitable? -%>
|
|
59
|
+
if user.invitation_pending?
|
|
60
|
+
<% if trackable? -%>
|
|
61
|
+
record_authentication_attempt(user, success: false, failure_reason: "invitation_pending")
|
|
62
|
+
<% end -%>
|
|
63
|
+
return redirect_to new_session_path, alert: "You must accept your invitation before signing in."
|
|
64
|
+
end
|
|
65
|
+
<% end -%>
|
|
66
|
+
# Add your own authentication restrictions specific to your application
|
|
67
|
+
# if user.deleted_at.present?
|
|
68
|
+
<% if trackable? -%>
|
|
69
|
+
# record_authentication_attempt(user, success: false, failure_reason: "soft_deleted")
|
|
70
|
+
<% end -%>
|
|
71
|
+
# return redirect_to new_session_path, alert: "Your account is not available."
|
|
72
|
+
# end
|
|
73
|
+
|
|
74
|
+
user.consume_ott!
|
|
75
|
+
session.delete(:ott_email_address)
|
|
76
|
+
<% if lockable? -%>
|
|
77
|
+
user.reset_failed_attempts!
|
|
78
|
+
<% end -%>
|
|
79
|
+
<% if trackable? -%>
|
|
80
|
+
record_authentication_attempt(user, success: true)
|
|
81
|
+
<% end -%>
|
|
82
|
+
<% if rememberable? -%>
|
|
83
|
+
start_new_session_for user, remember: params[:remember_me] == "1"
|
|
84
|
+
<% else -%>
|
|
85
|
+
start_new_session_for user
|
|
86
|
+
<% end -%>
|
|
87
|
+
redirect_to after_authentication_url
|
|
88
|
+
end
|
|
89
|
+
<% if trackable? -%>
|
|
90
|
+
|
|
91
|
+
def record_authentication_attempt(user, success:, failure_reason: nil)
|
|
92
|
+
UserAuth.record(user, request, success: success, failure_reason: failure_reason)
|
|
93
|
+
end
|
|
94
|
+
<% end -%>
|
|
95
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
module OttConcern
|
|
2
|
+
extend ActiveSupport::Concern
|
|
3
|
+
|
|
4
|
+
OTT_EXPIRES_IN = 10.minutes
|
|
5
|
+
OTT_MAX_ATTEMPTS = 5
|
|
6
|
+
OTT_CODE_LENGTH = 6
|
|
7
|
+
|
|
8
|
+
def send_ott
|
|
9
|
+
generate_ott!
|
|
10
|
+
OttsMailer.ott(self).deliver_later
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def generate_ott!
|
|
14
|
+
update!(ott_code: build_ott_code, ott_sent_at: Time.current, ott_attempts: 0)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Returns :valid, :invalid, :exhausted, or :expired. Codes are single-use:
|
|
18
|
+
# consumed on successful sign-in, and voided once OTT_MAX_ATTEMPTS wrong
|
|
19
|
+
# codes have been entered.
|
|
20
|
+
def verify_ott(code)
|
|
21
|
+
return :expired unless ott_active?
|
|
22
|
+
|
|
23
|
+
if ActiveSupport::SecurityUtils.secure_compare(ott_code, code.to_s)
|
|
24
|
+
:valid
|
|
25
|
+
else
|
|
26
|
+
increment!(:ott_attempts)
|
|
27
|
+
|
|
28
|
+
if ott_attempts >= OTT_MAX_ATTEMPTS
|
|
29
|
+
consume_ott!
|
|
30
|
+
:exhausted
|
|
31
|
+
else
|
|
32
|
+
:invalid
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def consume_ott!
|
|
38
|
+
update!(ott_code: nil, ott_sent_at: nil, ott_attempts: 0)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
protected
|
|
42
|
+
|
|
43
|
+
def ott_active?
|
|
44
|
+
ott_code.present? && ott_sent_at&.after?(OTT_EXPIRES_IN.ago)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def build_ott_code
|
|
48
|
+
SecureRandom.random_number(10**OTT_CODE_LENGTH).to_s.rjust(OTT_CODE_LENGTH, "0")
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
<h1>Enter your sign-in code</h1>
|
|
2
|
+
|
|
3
|
+
<%%= tag.div(flash[:alert], style: "color:red") if flash[:alert] %>
|
|
4
|
+
<%%= tag.div(flash[:notice], style: "color:green") if flash[:notice] %>
|
|
5
|
+
|
|
6
|
+
<p>
|
|
7
|
+
We emailed a <%%= OttConcern::OTT_CODE_LENGTH %>-digit code to <%%= session[:ott_email_address] %>.
|
|
8
|
+
It expires in <%%= distance_of_time_in_words(0, OttConcern::OTT_EXPIRES_IN) %>.
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
<%%= form_with url: ott_path, method: :patch do |form| %>
|
|
12
|
+
<%%= form.hidden_field :code, id: "ott-code" %>
|
|
13
|
+
<div id="ott-digits">
|
|
14
|
+
<%% OttConcern::OTT_CODE_LENGTH.times do |i| %>
|
|
15
|
+
<input type="text" inputmode="numeric" pattern="[0-9]*" maxlength="<%%= OttConcern::OTT_CODE_LENGTH %>" required
|
|
16
|
+
autocomplete="<%%= i.zero? ? "one-time-code" : "off" %>"
|
|
17
|
+
aria-label="Digit <%%= i + 1 %>" <%%= "autofocus" if i.zero? %>
|
|
18
|
+
style="width:2em;text-align:center;font-size:1.5em">
|
|
19
|
+
<%% end %>
|
|
20
|
+
</div>
|
|
21
|
+
<br>
|
|
22
|
+
<% if rememberable? -%>
|
|
23
|
+
<%%= form.check_box :remember_me %> <%%= form.label :remember_me, "Remember me" %><br>
|
|
24
|
+
<% end -%>
|
|
25
|
+
<%%= form.submit "Sign in" %>
|
|
26
|
+
<%% end %>
|
|
27
|
+
<br>
|
|
28
|
+
|
|
29
|
+
<%%= button_to "Resend code", ott_path, method: :post %>
|
|
30
|
+
|
|
31
|
+
<script>
|
|
32
|
+
(function() {
|
|
33
|
+
var boxes = Array.prototype.slice.call(document.querySelectorAll("#ott-digits input"));
|
|
34
|
+
var hidden = document.getElementById("ott-code");
|
|
35
|
+
|
|
36
|
+
function sync() {
|
|
37
|
+
hidden.value = boxes.map(function(box) { return box.value; }).join("");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Distributes a string of digits across the boxes, starting at `start`.
|
|
41
|
+
function fill(text, start) {
|
|
42
|
+
var digits = text.replace(/\D/g, "").slice(0, boxes.length - start).split("");
|
|
43
|
+
digits.forEach(function(digit, i) { boxes[start + i].value = digit; });
|
|
44
|
+
boxes[Math.min(start + digits.length, boxes.length - 1)].focus();
|
|
45
|
+
sync();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
boxes.forEach(function(box, i) {
|
|
49
|
+
box.addEventListener("input", function() {
|
|
50
|
+
// A multi-character value means a paste or OS one-time-code autofill.
|
|
51
|
+
if (box.value.length > 1) return fill(box.value, i);
|
|
52
|
+
|
|
53
|
+
box.value = box.value.replace(/\D/g, "");
|
|
54
|
+
if (box.value && i < boxes.length - 1) boxes[i + 1].focus();
|
|
55
|
+
sync();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
box.addEventListener("keydown", function(event) {
|
|
59
|
+
if (event.key === "Backspace" && !box.value && i > 0) {
|
|
60
|
+
event.preventDefault();
|
|
61
|
+
boxes[i - 1].value = "";
|
|
62
|
+
boxes[i - 1].focus();
|
|
63
|
+
sync();
|
|
64
|
+
} else if (event.key === "ArrowLeft" && i > 0) {
|
|
65
|
+
event.preventDefault();
|
|
66
|
+
boxes[i - 1].focus();
|
|
67
|
+
} else if (event.key === "ArrowRight" && i < boxes.length - 1) {
|
|
68
|
+
event.preventDefault();
|
|
69
|
+
boxes[i + 1].focus();
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
box.addEventListener("paste", function(event) {
|
|
74
|
+
event.preventDefault();
|
|
75
|
+
fill((event.clipboardData || window.clipboardData).getData("text"), 0);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
})();
|
|
79
|
+
</script>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<p>Your sign-in code is:</p>
|
|
2
|
+
|
|
3
|
+
<p><strong><%%= @user.ott_code %></strong></p>
|
|
4
|
+
|
|
5
|
+
<p>
|
|
6
|
+
This code will expire in <%%= distance_of_time_in_words(0, OttConcern::OTT_EXPIRES_IN) %> and can only be used once.
|
|
7
|
+
If you didn't request it, you can ignore this email.
|
|
8
|
+
</p>
|
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
<%%= tag.div(flash[:alert], style: "color:red") if flash[:alert] %>
|
|
2
2
|
<%%= tag.div(flash[:notice], style: "color:green") if flash[:notice] %>
|
|
3
3
|
|
|
4
|
+
<% if ott? -%>
|
|
5
|
+
<%% if params[:with_password].blank? %>
|
|
6
|
+
<%%= form_with url: ott_path do |form| %>
|
|
7
|
+
<%%= form.email_field :email_address, required: true, autofocus: true, autocomplete: "username", placeholder: "Enter your email address", value: params[:email_address] %><br>
|
|
8
|
+
<%%= form.submit "Email me a sign-in code" %>
|
|
9
|
+
<%% end %>
|
|
10
|
+
<br>
|
|
11
|
+
|
|
12
|
+
<br><%%= link_to "Sign in with password instead", new_session_path(with_password: 1) %>
|
|
13
|
+
<%% else %>
|
|
14
|
+
<% end -%>
|
|
4
15
|
<%%= form_with url: session_path do |form| %>
|
|
5
16
|
<%%= form.email_field :email_address, required: true, autofocus: true, autocomplete: "username", placeholder: "Enter your email address", value: params[:email_address] %><br>
|
|
6
17
|
<%%= form.password_field :password, required: true, autocomplete: "current-password", placeholder: "Enter your password", maxlength: 72 %><br>
|
|
@@ -10,6 +21,11 @@
|
|
|
10
21
|
<%%= form.submit "Sign in" %>
|
|
11
22
|
<%% end %>
|
|
12
23
|
<br>
|
|
24
|
+
<% if ott? -%>
|
|
25
|
+
|
|
26
|
+
<br><%%= link_to "Sign in with a one-time code instead", new_session_path %>
|
|
27
|
+
<%% end %>
|
|
28
|
+
<% end -%>
|
|
13
29
|
|
|
14
30
|
<% if magic_link? -%>
|
|
15
31
|
<br><%%= link_to "Sign in with magic link", new_magic_link_path %>
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rails_authentication
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- David Freerksen
|
|
@@ -38,6 +38,7 @@ files:
|
|
|
38
38
|
- lib/generators/authentication/features/invitable.rb
|
|
39
39
|
- lib/generators/authentication/features/lockable.rb
|
|
40
40
|
- lib/generators/authentication/features/magic_link.rb
|
|
41
|
+
- lib/generators/authentication/features/ott.rb
|
|
41
42
|
- lib/generators/authentication/features/recoverable.rb
|
|
42
43
|
- lib/generators/authentication/features/registerable.rb
|
|
43
44
|
- lib/generators/authentication/features/rememberable.rb
|
|
@@ -48,6 +49,7 @@ files:
|
|
|
48
49
|
- lib/generators/authentication/templates/app/controllers/confirmations_controller.rb.tt
|
|
49
50
|
- lib/generators/authentication/templates/app/controllers/invitations_controller.rb.tt
|
|
50
51
|
- lib/generators/authentication/templates/app/controllers/magic_links_controller.rb.tt
|
|
52
|
+
- lib/generators/authentication/templates/app/controllers/otts_controller.rb.tt
|
|
51
53
|
- lib/generators/authentication/templates/app/controllers/passwords_controller.rb.tt
|
|
52
54
|
- lib/generators/authentication/templates/app/controllers/registrations_controller.rb.tt
|
|
53
55
|
- lib/generators/authentication/templates/app/controllers/sessions_controller.rb.tt
|
|
@@ -55,12 +57,14 @@ files:
|
|
|
55
57
|
- lib/generators/authentication/templates/app/mailers/confirmations_mailer.rb.tt
|
|
56
58
|
- lib/generators/authentication/templates/app/mailers/invitations_mailer.rb.tt
|
|
57
59
|
- lib/generators/authentication/templates/app/mailers/magic_links_mailer.rb.tt
|
|
60
|
+
- lib/generators/authentication/templates/app/mailers/otts_mailer.rb.tt
|
|
58
61
|
- lib/generators/authentication/templates/app/mailers/passwords_mailer.rb.tt
|
|
59
62
|
- lib/generators/authentication/templates/app/mailers/unlocks_mailer.rb.tt
|
|
60
63
|
- lib/generators/authentication/templates/app/models/concerns/confirmable_concern.rb.tt
|
|
61
64
|
- lib/generators/authentication/templates/app/models/concerns/invitable_concern.rb.tt
|
|
62
65
|
- lib/generators/authentication/templates/app/models/concerns/lockable_concern.rb.tt
|
|
63
66
|
- lib/generators/authentication/templates/app/models/concerns/magic_link_concern.rb.tt
|
|
67
|
+
- lib/generators/authentication/templates/app/models/concerns/ott_concern.rb.tt
|
|
64
68
|
- lib/generators/authentication/templates/app/models/concerns/recoverable_concern.rb.tt
|
|
65
69
|
- lib/generators/authentication/templates/app/models/concerns/rememberable_concern.rb.tt
|
|
66
70
|
- lib/generators/authentication/templates/app/models/concerns/timeoutable_concern.rb.tt
|
|
@@ -77,6 +81,9 @@ files:
|
|
|
77
81
|
- lib/generators/authentication/templates/app/views/magic_links/new.html.erb.tt
|
|
78
82
|
- lib/generators/authentication/templates/app/views/magic_links_mailer/magic_link.html.erb.tt
|
|
79
83
|
- lib/generators/authentication/templates/app/views/magic_links_mailer/magic_link.text.erb.tt
|
|
84
|
+
- lib/generators/authentication/templates/app/views/otts/edit.html.erb.tt
|
|
85
|
+
- lib/generators/authentication/templates/app/views/otts_mailer/ott.html.erb.tt
|
|
86
|
+
- lib/generators/authentication/templates/app/views/otts_mailer/ott.text.erb.tt
|
|
80
87
|
- lib/generators/authentication/templates/app/views/passwords_mailer/reset.html.erb.tt
|
|
81
88
|
- lib/generators/authentication/templates/app/views/passwords_mailer/reset.text.erb.tt
|
|
82
89
|
- lib/generators/authentication/templates/app/views/registrations/edit.html.erb.tt
|
|
@@ -89,6 +96,7 @@ files:
|
|
|
89
96
|
- lib/generators/authentication/templates/db/migrate/add_invitable_to_users.rb.tt
|
|
90
97
|
- lib/generators/authentication/templates/db/migrate/add_lockable_to_users.rb.tt
|
|
91
98
|
- lib/generators/authentication/templates/db/migrate/add_magic_link_to_users.rb.tt
|
|
99
|
+
- lib/generators/authentication/templates/db/migrate/add_ott_to_users.rb.tt
|
|
92
100
|
- lib/generators/authentication/templates/db/migrate/add_recoverable_to_users.rb.tt
|
|
93
101
|
- lib/generators/authentication/templates/db/migrate/add_rememberable_to_users.rb.tt
|
|
94
102
|
- lib/generators/authentication/templates/db/migrate/create_user_auths.rb.tt
|