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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c71c1a30969abed6c581c67e3447c1968176e6e25c218894e64d5317472b873e
4
- data.tar.gz: 1f65cac490fae4c9f575d907bf70d4d059afdb1bcb5e8a56474b741b4eac4f3e
3
+ metadata.gz: f5c0f93e4dc6c9eb2562c7804920c82cff3a5a7d23d15758aaee3d63cdb60d99
4
+ data.tar.gz: 60ee05fafe8ffd4d9f56c10aa57c9ea5d347b801e94d3651ba2b04b258545a13
5
5
  SHA512:
6
- metadata.gz: 5469da8db1ff529c5dd079b37a3499b1bc08b657560bdcd037607c407d6be292a306bcd146bcc99f30243435824a6fa9a58cb965a80edc0d7fb30435dcdc1cc3
7
- data.tar.gz: '0693170a1f4bdebe7ec41bfe6088ddbbc436225aa3a09b8d3216af9cf800bd36d461a9618fd022e0b45b1662c82382b13a0bf4cc10e715a9e7247fcd1d77fb18'
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 is the one **opt-in** featureit changes the sign-in UX, so you have to ask for it:
60
+ MagicLink and Ott are **opt-in** featuresthey 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
- It adds a "Sign in with magic link" link to the sign-in page, leading to an email-only form. The
66
- emailed link signs the user in directly; tokens are DB-backed, single-use, and expire after
67
- 20 minutes (`MagicLinkConcern::MAGIC_LINK_EXPIRES_IN`). The magic link flow honors the other
68
- enabled features: locked, unconfirmed, or invitation-pending accounts still can't sign in, and
69
- Trackable records the attempt.
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` — edit them
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,6 @@
1
+ class OttsMailer < ApplicationMailer
2
+ def ott(user)
3
+ @user = user
4
+ mail subject: "Your sign-in code", to: user.email_address
5
+ end
6
+ 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>
@@ -0,0 +1,6 @@
1
+ Your sign-in code is:
2
+
3
+ <%%= @user.ott_code %>
4
+
5
+ This code will expire in <%%= distance_of_time_in_words(0, OttConcern::OTT_EXPIRES_IN) %> and can only be used once.
6
+ If you didn't request it, you can ignore this email.
@@ -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 %>
@@ -0,0 +1,7 @@
1
+ class AddOttToUsers < ActiveRecord::Migration<%= migration_version %>
2
+ def change
3
+ add_column :users, :ott_code, :string
4
+ add_column :users, :ott_sent_at, :datetime
5
+ add_column :users, :ott_attempts, :integer, default: 0, null: false
6
+ end
7
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RailsAuthentication
4
- VERSION = "0.3.0"
4
+ VERSION = "0.4.0"
5
5
  end
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.3.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