devise-passwordless 0.1.0 → 0.6.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: '08511edac883e2b94811e349d41ce761a7d5771e55b628450bd1e02d835f0374'
4
- data.tar.gz: 9d0f442ffed9f59818a370c8bdc815f362367bd24a6c09fc66b0097009992cd9
3
+ metadata.gz: c015970325061c0bd86aac15d0a2a90e200cd036cbe85249503d16881fb07d9a
4
+ data.tar.gz: 562b24b072376547b0e4bb6d503924dc58ad6d404a8e01397751af52e7a318cd
5
5
  SHA512:
6
- metadata.gz: 405e6a4ee5dbb3c66dcd079a92642e01dfb10b336d5e220f8a05f0814a2ac50ef47e78aa13e734b4aec4ce2891eb8cde900854984c3c84193afb1ff05e5b8481
7
- data.tar.gz: 933e273120b795b3b1eb1bb96a4672c9f3da7645ed7df55046eddc57197307afd9aadee5612fb047980cceed3fc5c2348f5388bb5b19df87191e9ee57e2fdb0f
6
+ metadata.gz: f9bd36c841f32e9e017e0ae672636ef0e2708064b7d10b9e001af148f3e6fbeee3b3ee117a21ee1947c3d91f7a036b9e33079f28f15d582681d75bdac11d995d
7
+ data.tar.gz: ab507245047decac1f9fbeb871dc54e7bef7e056c5c065550dcdedb1082ecf7db3c0caf911346b83ac93b636b2512b4735bdecf7754b9079327427b4160854f6
data/.gitignore CHANGED
@@ -5,6 +5,7 @@
5
5
  /doc/
6
6
  /pkg/
7
7
  /spec/reports/
8
+ /spec/tmp
8
9
  /tmp/
9
10
  Gemfile.lock
10
11
 
data/Gemfile CHANGED
@@ -4,3 +4,12 @@ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
4
 
5
5
  # Specify your gem's dependencies in devise-passwordless.gemspec
6
6
  gemspec
7
+
8
+ gem "rake", "~> 10.0"
9
+
10
+ group :test do
11
+ gem "rspec", "~> 3.0"
12
+ gem "generator_spec"
13
+ gem "timecop"
14
+ gem "pry"
15
+ end
data/README.md CHANGED
@@ -1,50 +1,223 @@
1
1
  # Devise::Passwordless
2
2
 
3
- A passwordless login strategy for [Devise][]
3
+ A passwordless a.k.a. "magic link" login strategy for [Devise][]
4
+
5
+ ## Features
6
+
7
+ * No special database migrations needed - magic links are stateless encrypted tokens
8
+ * Magic links are sent from your app - not a mounted Rails engine - so path and URL helpers work as expected
9
+ * [Supports multiple user (resource) types](#multiple-user-resource-types)
10
+ * All the goodness of Devise!
4
11
 
5
12
  ## Installation
6
13
 
7
- You should already have Devise installed. Then add this gem:
14
+ First, install and set up [Devise][].
15
+
16
+ Then add this gem to your application's Gemfile:
8
17
 
9
18
  ```ruby
10
19
  gem "devise-passwordless"
11
20
  ```
12
21
 
13
- Then run the generator to automatically update your Devise initializer:
22
+ And then execute:
23
+
24
+ ```
25
+ $ bundle install
26
+ ```
27
+
28
+ Finally, run the install generator:
14
29
 
15
30
  ```
16
- rails g devise:passwordless:install
31
+ $ rails g devise:passwordless:install
32
+ ```
33
+
34
+ See the [customization section](#customization) for details on what gets installed and how to configure and customize.
35
+
36
+ ## Usage
37
+
38
+ This gem adds a `:magic_link_authenticatable` strategy that can be used in your Devise models for passwordless authentication. This strategy plays well with most other Devise strategies (see [*notes on other Devise strategies*](#notes-on-other-devise-strategies)).
39
+
40
+ For example, if your Devise model is User, enable the strategy like this:
41
+
42
+ ```ruby
43
+ # app/models/user.rb
44
+ class User < ApplicationRecord
45
+ devise :magic_link_authenticatable #, :registerable, :rememberable, ...
46
+ end
47
+ ```
48
+
49
+ Then, you'll need to set up your Devise routes like so to use the passwordless controllers to modify Devise's default session create logic and to handle processing magic links:
50
+
51
+ ```ruby
52
+ # config/routes.rb
53
+ Rails.application.routes.draw do
54
+ devise_for :users,
55
+ controllers: { sessions: "devise/passwordless/sessions" }
56
+ devise_scope :user do
57
+ get "/users/magic_link",
58
+ to: "devise/passwordless/magic_links#show",
59
+ as: "users_magic_link"
60
+ end
61
+ end
62
+ ```
63
+
64
+ Finally, you'll want to update Devise's generated views to remove references to passwords, since you don't need them any more!
65
+
66
+ These files/directories can be deleted entirely:
67
+
68
+ ```
69
+ app/views/devise/passwords
70
+ app/views/devise/mailer/password_change.html.erb
71
+ app/views/devise/mailer/reset_password_instructions.html.erb
72
+ ```
73
+
74
+ And these should be edited to remove password references:
75
+
76
+ * `app/views/devise/registrations/new.html.erb`
77
+ * Delete fields `:password` and `:password_confirmation`
78
+ * `app/views/devise/registrations/edit.html.erb`
79
+ * Delete fields `:password`, `:password_confirmation`, `:current_password`
80
+ * `app/views/devise/sessions/new.html.erb`
81
+ * Delete field `:password`
82
+
83
+ #### Manually sending magic links
84
+
85
+ You can very easily send a magic link at any point like so:
86
+
87
+ ```ruby
88
+ remember_me = true
89
+ User.send_magic_link(remember_me)
90
+ ```
91
+
92
+ ## Customization
93
+
94
+ Configuration options are stored in Devise's initializer at `config/initializers/devise.rb`:
95
+
96
+ ```ruby
97
+ # ==> Configuration for :magic_link_authenticatable
98
+
99
+ # Need to use a custom Devise mailer in order to send magic links
100
+ require "devise/passwordless/mailer"
101
+ config.mailer = "Devise::Passwordless::Mailer"
102
+
103
+ # Time period after a magic login link is sent out that it will be valid for.
104
+ # config.passwordless_login_within = 20.minutes
105
+
106
+ # The secret key used to generate passwordless login tokens. The default value
107
+ # is nil, which means defer to Devise's `secret_key` config value. Changing this
108
+ # key will render invalid all existing passwordless login tokens. You can
109
+ # generate your own secret value with e.g. `rake secret`
110
+ # config.passwordless_secret_key = nil
111
+
112
+ # When using the :trackable module, set to true to consider magic link tokens
113
+ # generated before the user's current sign in time to be expired. In other words,
114
+ # each time you sign in, all existing magic links will be considered invalid.
115
+ # config.passwordless_expire_old_tokens_on_sign_in = false
17
116
  ```
18
117
 
19
- Merge these YAML values into your `devise.en.yml` file:
118
+ To customize the magic link email subject line and other status and error messages, modify these values in `config/locales/devise.en.yml`:
20
119
 
21
120
  ```yaml
22
121
  en:
23
122
  devise:
123
+ passwordless:
124
+ not_found_in_database: "Could not find a user for that email address"
125
+ magic_link_sent: "A login link has been sent to your email address. Please follow the link to log in to your account."
24
126
  failure:
25
- passwordless_invalid: "Invalid or expired login link."
127
+ magic_link_invalid: "Invalid or expired login link."
26
128
  mailer:
27
- passwordless_link:
28
- subject: "Here's your magic link"
129
+ magic_link:
130
+ subject: "Here's your magic login link"
29
131
  ```
30
132
 
31
- ## Usage
133
+ To customize the magic link email body, edit `app/views/devise/mailer/magic_link.html.erb`
134
+
135
+ ### Multiple user (resource) types
32
136
 
33
- This gem adds an `:email_authenticatable` strategy that can be used in your Devise models for passwordless authentication. This strategy plays well with most other Devise strategies.
137
+ Devise supports multiple resource types, so we do too.
34
138
 
35
- For example, for a User model, you could do this (other strategies optional and not an exhaustive list):
139
+ For example, if you have a User and Admin model, enable the `:magic_link_authenticatable` strategy for each:
36
140
 
37
141
  ```ruby
142
+ # app/models/user.rb
38
143
  class User < ApplicationRecord
39
- devise :email_authenticatable,
40
- :registerable,
41
- :rememberable,
42
- :validatable,
43
- :confirmable
144
+ devise :magic_link_authenticatable # , :registerable, :rememberable, ...
145
+ end
146
+
147
+ # app/models/admin.rb
148
+ class Admin < ApplicationRecord
149
+ devise :magic_link_authenticatable # , :registerable, :rememberable, ...
44
150
  end
45
151
  ```
46
152
 
47
- **Note** if using the `:rememberable` strategy for "remember me" functionality, you'll need to add a `remember_token` column to your resource, as there is no password salt to use for validating cookies:
153
+ Then just set up your routes like this:
154
+
155
+ ```ruby
156
+ # config/routes.rb
157
+ Rails.application.routes.draw do
158
+ devise_for :users,
159
+ controllers: { sessions: "devise/passwordless/sessions" }
160
+ devise_scope :user do
161
+ get "/users/magic_link",
162
+ to: "devise/passwordless/magic_links#show",
163
+ as: "users_magic_link"
164
+ end
165
+ devise_for :admins,
166
+ controllers: { sessions: "devise/passwordless/sessions" }
167
+ devise_scope :admin do
168
+ get "/admins/magic_link",
169
+ to: "devise/passwordless/magic_links#show",
170
+ as: "admins_magic_link"
171
+ end
172
+ end
173
+ ```
174
+
175
+ And that's it!
176
+
177
+ Messaging can be customized per-resource using [Devise's usual I18n support][devise-i18n]:
178
+
179
+ ```yaml
180
+ en:
181
+ devise:
182
+ passwordless:
183
+ user:
184
+ not_found_in_database: "Could not find a USER for that email address"
185
+ magic_link_sent: "A USER login link has been sent to your email address. Please follow the link to log in to your account."
186
+ admin:
187
+ not_found_in_database: "Could not find an ADMIN for that email address"
188
+ magic_link_sent: "An ADMIN login link has been sent to your email address. Please follow the link to log in to your account."
189
+ failure:
190
+ user:
191
+ magic_link_invalid: "Invalid or expired USER login link."
192
+ admin:
193
+ magic_link_invalid: "Invalid or expired ADMIN login link."
194
+ mailer:
195
+ magic_link:
196
+ user_subject: "Here's your USER magic login link ✨"
197
+ admin_subject: "Here's your ADMIN magic login link ✨"
198
+ ```
199
+
200
+ #### Scoped views
201
+
202
+ If you have multiple Devise models, some that are passwordless and some that aren't, you will probably want to enable [Devise's `scoped_views` setting](https://henrytabima.github.io/rails-setup/docs/devise/configuring-views) so that the models have different signup and login pages (since some models will need password fields and others won't).
203
+
204
+ If you need to generate fresh Devise views for your models, you can do so like so:
205
+
206
+ ```
207
+ $ rails generate devise:views users
208
+ $ rails generate devise:views admins
209
+ ```
210
+
211
+ Which will generate the whole set of Devise views under these paths:
212
+
213
+ ```
214
+ app/views/users/
215
+ app/views/admins/
216
+ ```
217
+
218
+ ### Notes on other Devise strategies
219
+
220
+ If using the `:rememberable` strategy for "remember me" functionality, you'll need to add a `remember_token` column to your resource, as by default that strategy assumes you're using a password auth strategy and relies on comparing the password's salt to validate cookies:
48
221
 
49
222
  ```ruby
50
223
  change_table :users do |t|
@@ -52,12 +225,18 @@ change_table :users do |t|
52
225
  end
53
226
  ```
54
227
 
55
- **Note** if using the `:confirmable` strategy, you may want to override the default Devise behavior of requiring a fresh login after email confirmation (e.g. [this](https://stackoverflow.com/a/39010334/215168) or [this](https://stackoverflow.com/a/25865526/215168) approach). Otherwise, users will have to get a fresh login link after confirming their email, which makes no sense if they just confirmed they own the email address.
228
+ If using the `:confirmable` strategy, you may want to override the default Devise behavior of requiring a fresh login after email confirmation (e.g. [this](https://stackoverflow.com/a/39010334/215168) or [this](https://stackoverflow.com/a/25865526/215168) approach). Otherwise, users will have to get a fresh login link after confirming their email, which makes little sense if they just confirmed they own the email address.
229
+
230
+ ## Alternatives
231
+
232
+ Other Ruby libraries that offer passwordless authentication:
56
233
 
57
- ## Configuration
234
+ * [passwordless](https://github.com/mikker/passwordless)
235
+ * [magic-link](https://github.com/dvanderbeek/magic-link)
58
236
 
59
237
  ## License
60
238
 
61
239
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
62
240
 
63
241
  [Devise]: https://github.com/heartcombo/devise
242
+ [devise-i18n]: https://github.com/heartcombo/devise#i18n
@@ -35,11 +35,7 @@ Gem::Specification.new do |spec|
35
35
  spec.bindir = "exe"
36
36
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
37
37
  spec.require_paths = ["lib"]
38
+ spec.required_ruby_version = ">= 2.1.0"
38
39
 
39
40
  spec.add_dependency "devise"
40
- spec.add_dependency "rails"
41
-
42
- spec.add_development_dependency "bundler", "~> 1.17"
43
- spec.add_development_dependency "rake", "~> 10.0"
44
- spec.add_development_dependency "rspec", "~> 3.0"
45
41
  end
@@ -1,8 +1,8 @@
1
- require 'devise/strategies/email_authenticatable'
1
+ require 'devise/strategies/magic_link_authenticatable'
2
2
 
3
3
  module Devise
4
4
  module Models
5
- module EmailAuthenticatable
5
+ module MagicLinkAuthenticatable
6
6
  extend ActiveSupport::Concern
7
7
 
8
8
  def password_required?
@@ -14,31 +14,40 @@ module Devise
14
14
  nil
15
15
  end
16
16
 
17
+ def send_magic_link(remember_me)
18
+ token = Devise::Passwordless::LoginToken.encode(self)
19
+ send_devise_notification(:magic_link, token, remember_me, {})
20
+ end
21
+
17
22
  # A callback initiated after successfully authenticating. This can be
18
23
  # used to insert your own logic that is only run after the user successfully
19
24
  # authenticates.
20
25
  #
21
26
  # Example:
22
27
  #
23
- # def after_passwordless_authentication
28
+ # def after_magic_link_authentication
24
29
  # self.update_attribute(:invite_code, nil)
25
30
  # end
26
31
  #
27
- def after_passwordless_authentication
32
+ def after_magic_link_authentication
28
33
  end
29
34
 
30
35
  protected
31
36
 
32
37
  module ClassMethods
33
38
  # We assume this method already gets the sanitized values from the
34
- # EmailAuthenticatable strategy. If you are using this method on
39
+ # MagicLinkAuthenticatable strategy. If you are using this method on
35
40
  # your own, be sure to sanitize the conditions hash to only include
36
41
  # the proper fields.
37
- def find_for_email_authentication(conditions)
42
+ def find_for_magic_link_authentication(conditions)
38
43
  find_for_authentication(conditions)
39
44
  end
40
45
 
41
- Devise::Models.config(self, :passwordless_login_within, :passwordless_secret_key)
46
+ Devise::Models.config(self,
47
+ :passwordless_login_within,
48
+ :passwordless_secret_key,
49
+ :passwordless_expire_old_tokens_on_sign_in
50
+ )
42
51
  end
43
52
  end
44
53
  end
@@ -50,4 +59,7 @@ module Devise
50
59
 
51
60
  mattr_accessor :passwordless_secret_key
52
61
  @@passwordless_secret_key = nil
62
+
63
+ mattr_accessor :passwordless_expire_old_tokens_on_sign_in
64
+ @@passwordless_expire_old_tokens_on_sign_in = false
53
65
  end
@@ -1,3 +1,3 @@
1
1
  require "devise/passwordless/version"
2
- require "devise/models/email_authenticatable"
2
+ require "devise/models/magic_link_authenticatable"
3
3
  require "generators/devise/passwordless/install_generator"
@@ -54,4 +54,4 @@ module Devise::Passwordless
54
54
  end
55
55
  end
56
56
  end
57
- end
57
+ end
@@ -1,9 +1,11 @@
1
- if defined?(Devise::Mailer)
2
- Devise::Mailer.class_eval do
3
- def passwordless_link(record, remember_me, opts = {})
1
+ # frozen_string_literal: true
2
+
3
+ module Devise::Passwordless
4
+ class Mailer < Devise::Mailer
5
+ def magic_link(record, token, remember_me, opts = {})
6
+ @token = token
4
7
  @remember_me = remember_me
5
- @token = Devise::Passwordless::LoginToken.encode(record)
6
- devise_mail(record, :passwordless_link, opts)
8
+ devise_mail(record, :magic_link, opts)
7
9
  end
8
10
  end
9
11
  end
@@ -1,5 +1,5 @@
1
1
  module Devise
2
2
  module Passwordless
3
- VERSION = "0.1.0"
3
+ VERSION = "0.6.0"
4
4
  end
5
5
  end
@@ -6,7 +6,7 @@ require "devise/passwordless/login_token"
6
6
 
7
7
  module Devise
8
8
  module Strategies
9
- class EmailAuthenticatable < Authenticatable
9
+ class MagicLinkAuthenticatable < Authenticatable
10
10
  #undef :password
11
11
  #undef :password=
12
12
  attr_accessor :token
@@ -20,21 +20,31 @@ module Devise
20
20
  end
21
21
 
22
22
  def authenticate!
23
- data = begin
24
- x = Devise::Passwordless::LoginToken.decode(self.token)
25
- x["data"]
23
+ begin
24
+ data = Devise::Passwordless::LoginToken.decode(self.token)
26
25
  rescue Devise::Passwordless::LoginToken::InvalidOrExpiredTokenError
27
- fail!(:passwordless_invalid)
26
+ fail!(:magic_link_invalid)
28
27
  return
29
28
  end
30
29
 
31
- resource = mapping.to.find_by(id: data["resource"]["key"])
30
+ resource = mapping.to.find_by(id: data["data"]["resource"]["key"])
31
+
32
+ if resource && Devise.passwordless_expire_old_tokens_on_sign_in
33
+ if (last_login = resource.try(:current_sign_in_at))
34
+ token_created_at = ActiveSupport::TimeZone["UTC"].at(data["created_at"])
35
+ if token_created_at < last_login
36
+ fail!(:magic_link_invalid)
37
+ return
38
+ end
39
+ end
40
+ end
41
+
32
42
  if validate(resource)
33
43
  remember_me(resource)
34
- resource.after_passwordless_authentication
44
+ resource.after_magic_link_authentication
35
45
  success!(resource)
36
46
  else
37
- fail!(:passwordless_invalid)
47
+ fail!(:magic_link_invalid)
38
48
  end
39
49
  end
40
50
 
@@ -52,12 +62,11 @@ module Devise
52
62
  end
53
63
  end
54
64
 
55
- Warden::Strategies.add(:email_authenticatable, Devise::Strategies::EmailAuthenticatable)
65
+ Warden::Strategies.add(:magic_link_authenticatable, Devise::Strategies::MagicLinkAuthenticatable)
56
66
 
57
- Devise.add_module(:email_authenticatable, {
67
+ Devise.add_module(:magic_link_authenticatable, {
58
68
  strategy: true,
59
69
  controller: :sessions,
60
- model: "devise/models/email_authenticatable",
61
- #route: { email_authenticatable: [nil, :new, :edit] }
62
- route: :session
70
+ route: :session,
71
+ model: "devise/models/magic_link_authenticatable",
63
72
  })
@@ -1,41 +1,128 @@
1
+ require "psych"
1
2
  require "rails/generators"
2
3
  require "yaml"
3
4
 
4
5
  module Devise::Passwordless
5
- module Generators
6
- class InstallGenerator < ::Rails::Generators::Base
7
- desc "Updates the Devise initializer to add passwordless config options"
6
+ module Generators # :nodoc:
7
+ class InstallGenerator < ::Rails::Generators::Base # :nodoc:
8
+ desc "Creates default install and config files for the Devise :magic_link_authenticatable strategy"
9
+
10
+ def self.default_generator_root
11
+ File.dirname(__FILE__)
12
+ end
13
+
14
+ def create_sessions_controller
15
+ template "sessions_controller.rb.erb", "app/controllers/devise/passwordless/sessions_controller.rb"
16
+ end
17
+
18
+ def create_magic_links_controller
19
+ template "magic_links_controller.rb.erb", "app/controllers/devise/passwordless/magic_links_controller.rb"
20
+ end
8
21
 
9
22
  def update_devise_initializer
10
23
  inject_into_file 'config/initializers/devise.rb', before: /^end$/ do <<~'CONFIG'.indent(2)
11
24
 
12
- # ==> Configuration for :email_authenticatable
25
+ # ==> Configuration for :magic_link_authenticatable
26
+
27
+ # Need to use a custom Devise mailer in order to send magic links
28
+ require "devise/passwordless/mailer"
29
+ config.mailer = "Devise::Passwordless::Mailer"
13
30
 
14
31
  # Time period after a magic login link is sent out that it will be valid for.
15
32
  # config.passwordless_login_within = 20.minutes
16
-
17
- # The secret key used to generate passwordless login tokens. The default
18
- # value is nil, which means defer to Devise's `secret_key` config value.
19
- # Changing this key will render invalid all existing passwordless login
20
- # tokens. You can generate your own value with e.g. `rake secret`
33
+
34
+ # The secret key used to generate passwordless login tokens. The default value
35
+ # is nil, which means defer to Devise's `secret_key` config value. Changing this
36
+ # key will render invalid all existing passwordless login tokens. You can
37
+ # generate your own secret value with e.g. `rake secret`
21
38
  # config.passwordless_secret_key = nil
39
+
40
+ # When using the :trackable module, set to true to consider magic link tokens
41
+ # generated before the user's current sign in time to be expired. In other words,
42
+ # each time you sign in, all existing magic links will be considered invalid.
43
+ # config.passwordless_expire_old_tokens_on_sign_in = false
22
44
  CONFIG
23
45
  end
24
46
  end
25
47
 
48
+ def add_mailer_view
49
+ create_file "app/views/devise/mailer/magic_link.html.erb" do <<~'FILE'
50
+ <p>Hello <%= @resource.email %>!</p>
51
+
52
+ <p>You can login using the link below:</p>
53
+
54
+ <p><%= link_to "Log in to my account", send("#{@scope_name.to_s.pluralize}_magic_link_url", Hash[@scope_name, {email: @resource.email, token: @token, remember_me: @remember_me}]) %></p>
55
+
56
+ <p>Note that the link will expire in <%= Devise.passwordless_login_within.inspect %>.</p>
57
+ FILE
58
+ end
59
+ end
60
+
26
61
  def update_devise_yaml
27
62
  devise_yaml = "config/locales/devise.en.yml"
63
+ existing_config = {}
28
64
  begin
29
- config = YAML.load_file(devise_yaml)
65
+ in_root do
66
+ existing_config = YAML.load_file(devise_yaml)
67
+ end
30
68
  rescue Errno::ENOENT
31
- STDERR.puts "Couldn't find devise.en.yml - skipping patch"
69
+ say_status :skip, devise_yaml, :yellow
32
70
  return
33
71
  end
34
- config["en"]["devise"]["failure"]["passwordless_invalid"] = "Invalid or expired login link."
35
- config["en"]["devise"]["mailer"]["passwordless_link"] = {subject: "Your login link"}
36
- File.open(devise_yaml, "w") do |f|
37
- f.write(config.to_yaml)
72
+ default_config = {
73
+ en: {
74
+ devise: {
75
+ passwordless: {
76
+ not_found_in_database: "Could not find a user for that email address",
77
+ magic_link_sent: "A login link has been sent to your email address. Please follow the link to log in to your account.",
78
+ },
79
+ failure: {
80
+ magic_link_invalid: "Invalid or expired login link.",
81
+ },
82
+ mailer: {
83
+ magic_link: {
84
+ subject: "Here's your magic login link ✨",
85
+ },
86
+ }
87
+ }
88
+ }
89
+ }
90
+ merged_config = existing_config.deep_merge(default_config.deep_stringify_keys)
91
+ if existing_config.to_yaml == merged_config.to_yaml
92
+ say_status :identical, devise_yaml, :blue
93
+ else
94
+ in_root do
95
+ File.open(devise_yaml, "w") do |f|
96
+ f.write(force_double_quote_yaml(merged_config.to_yaml))
97
+ end
98
+ end
99
+ say_status :insert, devise_yaml, :green
100
+ end
101
+ end
102
+
103
+ private
104
+
105
+ # https://github.com/ruby/psych/issues/322#issuecomment-328408276
106
+ def force_double_quote_yaml(yaml_str)
107
+ ast = Psych.parse_stream(yaml_str)
108
+
109
+ # First pass, quote everything
110
+ ast.grep(Psych::Nodes::Scalar).each do |node|
111
+ node.plain = false
112
+ node.quoted = true
113
+ node.style = Psych::Nodes::Scalar::DOUBLE_QUOTED
114
+ end
115
+
116
+ # Second pass, unquote keys
117
+ ast.grep(Psych::Nodes::Mapping).each do |node|
118
+ node.children.each_slice(2) do |k, _|
119
+ k.plain = true
120
+ k.quoted = false
121
+ k.style = Psych::Nodes::Scalar::ANY
122
+ end
38
123
  end
124
+
125
+ ast.yaml(nil, {line_width: -1})
39
126
  end
40
127
  end
41
128
  end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ <% module_namespacing do -%>
4
+ class Devise::Passwordless::MagicLinksController < DeviseController
5
+ prepend_before_action :require_no_authentication, only: :show
6
+ prepend_before_action :allow_params_authentication!, only: :show
7
+ prepend_before_action(only: [:show]) { request.env["devise.skip_timeout"] = true }
8
+
9
+ def show
10
+ self.resource = warden.authenticate!(auth_options)
11
+ set_flash_message!(:notice, :signed_in)
12
+ sign_in(resource_name, resource)
13
+ yield resource if block_given?
14
+ redirect_to after_sign_in_path_for(resource)
15
+ end
16
+
17
+ protected
18
+
19
+ def auth_options
20
+ mapping = Devise.mappings[resource_name]
21
+ { scope: resource_name, recall: "#{mapping.controllers[:sessions]}#new" }
22
+ end
23
+
24
+ def translation_scope
25
+ "devise.sessions"
26
+ end
27
+
28
+ private
29
+
30
+ def create_params
31
+ resource_params.permit(:email, :remember_me)
32
+ end
33
+ end
34
+ <% end -%>
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ <% module_namespacing do -%>
4
+ class Devise::Passwordless::SessionsController < Devise::SessionsController
5
+ def create
6
+ self.resource = resource_class.find_by(email: create_params[:email])
7
+ if self.resource
8
+ resource.send_magic_link(create_params[:remember_me])
9
+ set_flash_message(:notice, :magic_link_sent, now: true)
10
+ else
11
+ set_flash_message(:alert, :not_found_in_database, now: true)
12
+ end
13
+
14
+ self.resource = resource_class.new(create_params)
15
+ render :new
16
+ end
17
+
18
+ protected
19
+
20
+ def translation_scope
21
+ if action_name == "create"
22
+ "devise.passwordless"
23
+ else
24
+ super
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def create_params
31
+ resource_params.permit(:email, :remember_me)
32
+ end
33
+ end
34
+ <% end -%>
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: devise-passwordless
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Abe Voelker
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-11-09 00:00:00.000000000 Z
11
+ date: 2021-04-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: devise
@@ -24,62 +24,6 @@ dependencies:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0'
27
- - !ruby/object:Gem::Dependency
28
- name: rails
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - ">="
32
- - !ruby/object:Gem::Version
33
- version: '0'
34
- type: :runtime
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - ">="
39
- - !ruby/object:Gem::Version
40
- version: '0'
41
- - !ruby/object:Gem::Dependency
42
- name: bundler
43
- requirement: !ruby/object:Gem::Requirement
44
- requirements:
45
- - - "~>"
46
- - !ruby/object:Gem::Version
47
- version: '1.17'
48
- type: :development
49
- prerelease: false
50
- version_requirements: !ruby/object:Gem::Requirement
51
- requirements:
52
- - - "~>"
53
- - !ruby/object:Gem::Version
54
- version: '1.17'
55
- - !ruby/object:Gem::Dependency
56
- name: rake
57
- requirement: !ruby/object:Gem::Requirement
58
- requirements:
59
- - - "~>"
60
- - !ruby/object:Gem::Version
61
- version: '10.0'
62
- type: :development
63
- prerelease: false
64
- version_requirements: !ruby/object:Gem::Requirement
65
- requirements:
66
- - - "~>"
67
- - !ruby/object:Gem::Version
68
- version: '10.0'
69
- - !ruby/object:Gem::Dependency
70
- name: rspec
71
- requirement: !ruby/object:Gem::Requirement
72
- requirements:
73
- - - "~>"
74
- - !ruby/object:Gem::Version
75
- version: '3.0'
76
- type: :development
77
- prerelease: false
78
- version_requirements: !ruby/object:Gem::Requirement
79
- requirements:
80
- - - "~>"
81
- - !ruby/object:Gem::Version
82
- version: '3.0'
83
27
  description:
84
28
  email:
85
29
  - _@abevoelker.com
@@ -97,13 +41,15 @@ files:
97
41
  - bin/console
98
42
  - bin/setup
99
43
  - devise-passwordless.gemspec
100
- - lib/devise/models/email_authenticatable.rb
44
+ - lib/devise/models/magic_link_authenticatable.rb
101
45
  - lib/devise/passwordless.rb
102
46
  - lib/devise/passwordless/login_token.rb
103
47
  - lib/devise/passwordless/mailer.rb
104
48
  - lib/devise/passwordless/version.rb
105
- - lib/devise/strategies/email_authenticatable.rb
49
+ - lib/devise/strategies/magic_link_authenticatable.rb
106
50
  - lib/generators/devise/passwordless/install_generator.rb
51
+ - lib/generators/devise/passwordless/templates/magic_links_controller.rb.erb
52
+ - lib/generators/devise/passwordless/templates/sessions_controller.rb.erb
107
53
  homepage: https://github.com/abevoelker/devise-passwordless
108
54
  licenses:
109
55
  - MIT
@@ -118,14 +64,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
118
64
  requirements:
119
65
  - - ">="
120
66
  - !ruby/object:Gem::Version
121
- version: '0'
67
+ version: 2.1.0
122
68
  required_rubygems_version: !ruby/object:Gem::Requirement
123
69
  requirements:
124
70
  - - ">="
125
71
  - !ruby/object:Gem::Version
126
72
  version: '0'
127
73
  requirements: []
128
- rubygems_version: 3.0.3
74
+ rubygems_version: 3.1.2
129
75
  signing_key:
130
76
  specification_version: 4
131
77
  summary: Passwordless (email-only) login strategy for Devise