passwordless 1.5.0 → 1.7.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: 472a9d736665b6a1e1a71bf5175214f3acc414e0c918a33825c98a1d7ac02624
4
- data.tar.gz: 9e12898751e19fc7104c62826eaf7c9ee4e17a14a12efc66419eab4850dae720
3
+ metadata.gz: 7e765c341b12f05bb0156802dae574f7a0db31288181d4f71779a6ca49e48cd6
4
+ data.tar.gz: 746f09e1aefaea54d0f926c9bff27548ba1f8a4fdeeea0cfbd87ddc4b22ad65f
5
5
  SHA512:
6
- metadata.gz: 58b5bac3c0260d68a86fc7d9c3258457405a3670e0f8333959aa1ee54774ab68774728522d2e40c5d3b67a430f3b952af8903bb5d29b4ee12b6a291e3415fd17
7
- data.tar.gz: 42252595f95f48896cd08ec1be77a28657553ae287cc52213bed5cc343c38121bf12c8aba1db8ea002237ea8b5c1d706a8fc8f10dc7b9522c72c80218e4d35a5
6
+ metadata.gz: 8792ad81a4efbd5a071bb8a119d3e1ad057f2112e2bebbb888bbfdc9a84b95a77e4e2cb68ffb957fecb98f75cf05efb6c17cbd5c97b67a41f1372257eeb1cae9
7
+ data.tar.gz: 763dbbba33a8568976b5a5df2c28f0d157dcf9fc92504bf9ca016e85f8239c59b1e6ae48f4fa4e2e5ab2391aba9239ce956f7706c5437e168c27756a80fc9f33
data/README.md CHANGED
@@ -8,7 +8,24 @@
8
8
 
9
9
  Add authentication to your Rails app without all the icky-ness of passwords. _Magic link_ authentication, if you will. We call it _passwordless_.
10
10
 
11
- ---
11
+ - [Installation](#installation)
12
+ - [Upgrading](#upgrading)
13
+ - [Usage](#usage)
14
+ - [Getting the current user, restricting access, the usual](#getting-the-current-user-restricting-access-the-usual)
15
+ - [Providing your own templates](#providing-your-own-templates)
16
+ - [Registering new users](#registering-new-users)
17
+ - [URLs and links](#urls-and-links)
18
+ - [Route constraints](#route-constraints)
19
+ - [Configuration](#configuration)
20
+ - [Delivery method](#delivery-method)
21
+ - [Token generation](#token-generation)
22
+ - [Timeout and Expiry](#timeout-and-expiry)
23
+ - [Redirection after sign-in](#redirection-after-sign-in)
24
+ - [Looking up the user](#looking-up-the-user)
25
+ - [Test helpers](#test-helpers)
26
+ - [Security considerations](#security-considerations)
27
+ - [Alternatives](#alternatives)
28
+ - [License](#license)
12
29
 
13
30
  ## Installation
14
31
 
@@ -149,6 +166,35 @@ config.action_mailer.default_url_options = {host: "www.example.com"}
149
166
  routes.default_url_options[:host] ||= "www.example.com"
150
167
  ```
151
168
 
169
+ ### Route constraints
170
+
171
+ With [constraints](https://guides.rubyonrails.org/routing.html#request-based-constraints) you can restrict access to certain routes.
172
+ Passwordless provides `Passwordless::Constraint` and it's negative counterpart `Passwordless::NotConstraint` for this purpose.
173
+
174
+ To limit a route to only authenticated `User`s:
175
+
176
+ ```ruby
177
+ constraints Passwordless::Constraint.new(User) do
178
+ # ...
179
+ end
180
+ ```
181
+
182
+ The constraint takes a second `if:` argument, that expects a block and is passed the `authenticatable` record, (ie. `User`):
183
+
184
+ ```ruby
185
+ constraints Passwordless::Constraint.new(User, if: -> (user) { user.email.include?("john") }) do
186
+ # ...
187
+ end
188
+ ```
189
+
190
+ The negated version has the same API but with the opposite result, ie. ensuring authenticated user **don't** have access:
191
+
192
+ ```ruby
193
+ constraints Passwordless::NotConstraint.new(User) do
194
+ get("/no-users-allowed", to: "secrets#index")
195
+ end
196
+ ```
197
+
152
198
  ## Configuration
153
199
 
154
200
  To customize Passwordless, create a file `config/initializers/passwordless.rb`.
@@ -160,7 +206,7 @@ Passwordless.configure do |config|
160
206
  config.default_from_address = "CHANGE_ME@example.com"
161
207
  config.parent_controller = "ApplicationController"
162
208
  config.parent_mailer = "ActionMailer::Base"
163
- config.restrict_token_reuse = false # Can a token/link be used multiple times?
209
+ config.restrict_token_reuse = true # Can a token/link be used multiple times?
164
210
  config.token_generator = Passwordless::ShortTokenGenerator.new # Used to generate magic link tokens.
165
211
 
166
212
  config.expires_at = lambda { 1.year.from_now } # How long until a signed in session expires.
@@ -30,19 +30,20 @@ module Passwordless
30
30
  Passwordless.context.path_for(
31
31
  @session,
32
32
  id: @session.to_param,
33
- action: "show"
33
+ action: "show",
34
+ **default_url_options
34
35
  ),
35
36
  flash: {notice: I18n.t("passwordless.sessions.create.email_sent")}
36
37
  )
37
38
  else
38
- flash[:error] = I18n.t("passwordless.sessions.create.error")
39
+ flash.alert = I18n.t("passwordless.sessions.create.error")
39
40
  render(:new, status: :unprocessable_entity)
40
41
  end
41
42
 
42
43
  rescue ActiveRecord::RecordNotFound
43
44
  @session = Session.new
44
45
 
45
- flash[:error] = I18n.t("passwordless.sessions.create.not_found")
46
+ flash.alert = I18n.t("passwordless.sessions.create.not_found")
46
47
  render(:new, status: :not_found)
47
48
  end
48
49
 
@@ -155,15 +156,15 @@ module Passwordless
155
156
  **redirect_to_options
156
157
  )
157
158
  else
158
- flash[:error] = I18n.t("passwordless.sessions.errors.invalid_token")
159
+ flash.alert = I18n.t("passwordless.sessions.errors.invalid_token")
159
160
  render(status: :forbidden, action: "show")
160
161
  end
161
162
 
162
163
  rescue Errors::TokenAlreadyClaimedError
163
- flash[:error] = I18n.t("passwordless.sessions.errors.token_claimed")
164
+ flash.alert = I18n.t("passwordless.sessions.errors.token_claimed")
164
165
  redirect_to(passwordless_failure_redirect_path, status: :see_other, **redirect_to_options)
165
166
  rescue Errors::SessionTimedOutError
166
- flash[:error] = I18n.t("passwordless.sessions.errors.session_expired")
167
+ flash.alert = I18n.t("passwordless.sessions.errors.session_expired")
167
168
  redirect_to(passwordless_failure_redirect_path, status: :see_other, **redirect_to_options)
168
169
  end
169
170
 
@@ -9,7 +9,8 @@ module Passwordless
9
9
  belongs_to(
10
10
  :authenticatable,
11
11
  polymorphic: true,
12
- inverse_of: :passwordless_sessions
12
+ inverse_of: :passwordless_sessions,
13
+ autosave: false
13
14
  )
14
15
 
15
16
  validates(
@@ -1,5 +1,5 @@
1
1
  <%= form_with(model: @session, url: url_for(action: 'update'), scope: 'passwordless', method: 'patch', data: { turbo: false }) do |f| %>
2
- <%= f.label :token %>
2
+ <%= f.label :token, t(".token") %>
3
3
  <%= f.text_field :token,
4
4
  required: true,
5
5
  autofocus: true,
@@ -12,6 +12,7 @@ en:
12
12
  not_found: "We couldn't find a user with that email address"
13
13
  error: "An error occured"
14
14
  show:
15
+ token: "Token"
15
16
  confirm: "Confirm"
16
17
  errors:
17
18
  invalid_token: "Token is invalid"
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class CreatePasswordlessSessions < ActiveRecord::Migration[5.1]
3
+ class CreatePasswordlessSessions < ActiveRecord::Migration[6.0]
4
4
  def change
5
5
  create_table(:passwordless_sessions) do |t|
6
6
  t.belongs_to(
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "passwordless/controller_helpers"
4
+
5
+ module Passwordless
6
+ # A class the constraint routes to authenticated records
7
+ class Constraint
8
+ include ControllerHelpers
9
+
10
+ attr_reader :authenticatable_type, :predicate, :session
11
+
12
+ # @param [Class] authenticatable_type Authenticatable class
13
+ # @option options [Proc] :if A lambda that takes an authenticatable and returns a boolean
14
+ def initialize(authenticatable_type, **options)
15
+ @authenticatable_type = authenticatable_type
16
+ # `if' is a keyword but so we do this instead of keyword arguments
17
+ @predicate = options.fetch(:if) { -> (_) { true } }
18
+ end
19
+
20
+ def matches?(request)
21
+ # used in authenticate_by_session
22
+ @session = request.session
23
+ authenticatable = authenticate_by_session(authenticatable_type)
24
+ !!(authenticatable && predicate.call(authenticatable))
25
+ end
26
+ end
27
+
28
+ # A class the constraint routes to NOT authenticated records
29
+ class ConstraintNot < Constraint
30
+ # @param [Class] authenticatable_type Authenticatable class
31
+ # @option options [Proc] :if A lambda that takes an authenticatable and returns a boolean
32
+ def initialize(authenticatable_type, **options)
33
+ super
34
+ end
35
+
36
+ def matches?(request)
37
+ !super
38
+ end
39
+ end
40
+ end
@@ -54,7 +54,7 @@ module Passwordless
54
54
  end
55
55
 
56
56
  # Signs in session
57
- # @param authenticatable [Passwordless::Session] Instance of {Passwordless::Session}
57
+ # @param passwordless_session [Passwordless::Session] Instance of {Passwordless::Session}
58
58
  # to sign in
59
59
  # @return [ActiveRecord::Base] the record that is passed in.
60
60
  def sign_in(passwordless_session)
@@ -11,9 +11,5 @@ module Passwordless
11
11
  ActionDispatch::Routing::Mapper.include RouterHelpers
12
12
  ActiveRecord::Base.extend ModelHelpers
13
13
  end
14
-
15
- config.before_initialize do |app|
16
- app.config.i18n.load_path += Dir[Engine.root.join("config", "locales", "*.yml")]
17
- end
18
14
  end
19
15
  end
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Passwordless
4
4
  # :nodoc:
5
- VERSION = "1.5.0"
5
+ VERSION = "1.7.0"
6
6
  end
data/lib/passwordless.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  require "active_support"
4
4
  require "passwordless/config"
5
5
  require "passwordless/context"
6
+ require "passwordless/constraint"
6
7
  require "passwordless/errors"
7
8
  require "passwordless/engine"
8
9
  require "passwordless/token_digest"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: passwordless
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.5.0
4
+ version: 1.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mikkel Malmberg
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-03-11 00:00:00.000000000 Z
11
+ date: 2024-05-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -62,6 +62,7 @@ files:
62
62
  - lib/generators/passwordless/views_generator.rb
63
63
  - lib/passwordless.rb
64
64
  - lib/passwordless/config.rb
65
+ - lib/passwordless/constraint.rb
65
66
  - lib/passwordless/context.rb
66
67
  - lib/passwordless/controller_helpers.rb
67
68
  - lib/passwordless/engine.rb
@@ -91,7 +92,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
91
92
  - !ruby/object:Gem::Version
92
93
  version: '0'
93
94
  requirements: []
94
- rubygems_version: 3.5.6
95
+ rubygems_version: 3.5.10
95
96
  signing_key:
96
97
  specification_version: 4
97
98
  summary: Add authentication to your app without all the ickyness of passwords.