magic_links 1.0.1
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 +98 -0
- data/Rakefile +22 -0
- data/app/helpers/magic_links/url_helper.rb +27 -0
- data/app/models/magic_links/application_record.rb +5 -0
- data/app/models/magic_links/magic_token.rb +110 -0
- data/db/migrate/20211215150823_create_magic_links_magic_tokens.rb +15 -0
- data/lib/magic_links/engine.rb +13 -0
- data/lib/magic_links/middleware/magic_token_redirect.rb +78 -0
- data/lib/magic_links/rails.rb +23 -0
- data/lib/magic_links/strategies/magic_token_authentication.rb +70 -0
- data/lib/magic_links/template.rb +67 -0
- data/lib/magic_links/templates.rb +32 -0
- data/lib/magic_links/version.rb +7 -0
- data/lib/magic_links.rb +12 -0
- metadata +146 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: b6939cd65d873662c0c1101a1073e1d9dcfef386af25be14aa8d8232a5b293e7
|
4
|
+
data.tar.gz: 437b07e49b311be7721c3e3882275893ce3fe1ce2587105665b0a9e827c8f655
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 0146bba9e94182eb77b5bc001abfb0edcd232ea07fea288868fbdb63c4279676084147abe4da32b91257f91ff0b263b086095d068efdcf72399c934e37525bd5
|
7
|
+
data.tar.gz: 8b5d6a7bfec01316fa1f232d4239838b95a737f2d062c22af3a7ecc5be1b294be90794ee78d3f81ba02cb0631a9afb8f0d57e2e26d38e99de75a8157db455cf8
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2021 wozza35
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
# MagicLinks
|
2
|
+
|
3
|
+
Send 'magic links' to your users that grants them authorized access to your application without the need for them to be signed in.
|
4
|
+
|
5
|
+
#### Features:
|
6
|
+
|
7
|
+
- Grant authorized access to a subset of controllers and actions only
|
8
|
+
- Set an expiry time on the magic link
|
9
|
+
- Quick and easy to implement via the use of magic link 'templates' and inbuilt url helpers
|
10
|
+
- Negligible performance overhead
|
11
|
+
|
12
|
+
## Requirements
|
13
|
+
This gem assumes you are using Devise for authentication, and already have it installed and configured. If you have not done so, then follow their setup instructions here: https://github.com/heartcombo/devise#getting-started
|
14
|
+
|
15
|
+
It also requires that your application is using cookies.
|
16
|
+
|
17
|
+
## Installation
|
18
|
+
|
19
|
+
1. Add this line to your application's Gemfile:
|
20
|
+
|
21
|
+
```ruby
|
22
|
+
gem 'magic_links'
|
23
|
+
```
|
24
|
+
|
25
|
+
2. Install the gem
|
26
|
+
```bash
|
27
|
+
$ bundle install
|
28
|
+
```
|
29
|
+
|
30
|
+
3. Copy across the magic link migrations and run them
|
31
|
+
```bash
|
32
|
+
rails magic_links:install:migrations
|
33
|
+
rails db:migrate
|
34
|
+
```
|
35
|
+
|
36
|
+
4. Add the magic_link authentication strategy to your Devise configuration. For example, to enable magic_link authentication for 'users':
|
37
|
+
|
38
|
+
#### /config/initializers/devise.rb:
|
39
|
+
```ruby
|
40
|
+
config.warden do |manager|
|
41
|
+
# adds magic_token_authentication before the devise defaults
|
42
|
+
manager.default_strategies(scope: :user).unshift :magic_token_authentication
|
43
|
+
end
|
44
|
+
```
|
45
|
+
|
46
|
+
### Usage
|
47
|
+
|
48
|
+
To start creating magic links, you first need to specify one or more 'templates'. It is recommended that you do this by creating a magic_links initializer:
|
49
|
+
|
50
|
+
#### /config/initializers/magic_links.rb:
|
51
|
+
Example:
|
52
|
+
```ruby
|
53
|
+
# This will enable the helper:
|
54
|
+
# `magic_link_for(current_user, :order_tracking, '/orders/12345/tracking')`, which will return a relative path, or
|
55
|
+
# `magic_url_for(current_user, :order_tracking, '/orders/12345/tracking')`, which will return a a full URL.
|
56
|
+
# the resulting path/URL (e.g. `/ot/abcd12345`) will redirect to `/orders/12345/tracking`,
|
57
|
+
# authenticating `current_user` to perform any actions permitted in the `action_scope`.
|
58
|
+
# In this case, the user can call the 'show' and 'edit' actions on the 'Orders::TrackingController' and the 'dashboard'
|
59
|
+
# action on 'CustomersController'
|
60
|
+
|
61
|
+
Rails.application.config.to_prepare do
|
62
|
+
MagicLinks.add_template(
|
63
|
+
name: :order_tracking,
|
64
|
+
pattern: '/ot/:token',
|
65
|
+
action_scope: {'orders/tracking': [:show, :edit], customers: :dashboard},
|
66
|
+
strength: :mild # mild (8 char strength), moderate (16), or strong (32)
|
67
|
+
)
|
68
|
+
end
|
69
|
+
```
|
70
|
+
|
71
|
+
The templates can then be used as arguments to the url helpers. For example, to generate a magic link that can be sent
|
72
|
+
to a user:
|
73
|
+
|
74
|
+
```ruby
|
75
|
+
magic_link_for(current_user, :order_tracking, order_tracking_path, expiry: 1.week.from_now)
|
76
|
+
# will output '/ot/abcd1234'
|
77
|
+
```
|
78
|
+
|
79
|
+
Note: If a user attempts to perform an action that isn't part of the magic token's scope, they will receive a 401 and,
|
80
|
+
with Devise's default behavior will be redirected to a sign in page.
|
81
|
+
|
82
|
+
### Magic Links Helper
|
83
|
+
By default, the magic_links helper is included in `ActionController`. If you would like to use the magic_links helpers
|
84
|
+
anywhere else (e.g. in a mailer) then you can simply include the helper manually.
|
85
|
+
e.g:
|
86
|
+
```ruby
|
87
|
+
module UserMailer
|
88
|
+
include MagicLinks::UrlHelper
|
89
|
+
...
|
90
|
+
end
|
91
|
+
```
|
92
|
+
|
93
|
+
### Default url options
|
94
|
+
When using the `magic_url_for` helper you'll need to specify default_url_options for your development and testing
|
95
|
+
environments.
|
96
|
+
|
97
|
+
## License
|
98
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
begin
|
2
|
+
require 'bundler/setup'
|
3
|
+
rescue LoadError
|
4
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'rdoc/task'
|
8
|
+
|
9
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
10
|
+
rdoc.rdoc_dir = 'rdoc'
|
11
|
+
rdoc.title = 'MagicLinks'
|
12
|
+
rdoc.options << '--line-numbers'
|
13
|
+
rdoc.rdoc_files.include('README.md')
|
14
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
15
|
+
end
|
16
|
+
|
17
|
+
APP_RAKEFILE = File.expand_path("spec/dummy/Rakefile", __dir__)
|
18
|
+
load 'rails/tasks/engine.rake'
|
19
|
+
|
20
|
+
load 'rails/tasks/statistics.rake'
|
21
|
+
|
22
|
+
require 'bundler/gem_tasks'
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module MagicLinks
|
2
|
+
module UrlHelper
|
3
|
+
class << self
|
4
|
+
def magic_link_for(user, template_name, path, expiry = nil)
|
5
|
+
template = MagicLinks::Templates.find(template_name)
|
6
|
+
raise ArgumentError, 'Template not found' unless template.present?
|
7
|
+
|
8
|
+
template.magic_link_for(user, path, expiry)
|
9
|
+
end
|
10
|
+
|
11
|
+
def magic_url_for(user, template_name, path, expiry = nil)
|
12
|
+
template = MagicLinks::Templates.find(template_name)
|
13
|
+
raise ArgumentError, 'Template not found' unless template.present?
|
14
|
+
|
15
|
+
template.magic_url_for(user, path, expiry)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def magic_link_for(user, template_name, path, expiry = nil)
|
20
|
+
MagicLinks::UrlHelper.magic_link_for(user, template_name, path, expiry)
|
21
|
+
end
|
22
|
+
|
23
|
+
def magic_url_for(user, template_name, path, expiry = nil)
|
24
|
+
MagicLinks::UrlHelper.magic_url_for(user, template_name, path, expiry)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
module MagicLinks
|
2
|
+
class MagicToken < ApplicationRecord
|
3
|
+
belongs_to :magic_token_authenticatable, polymorphic: true
|
4
|
+
|
5
|
+
validates :token, presence: true
|
6
|
+
validates :target_path, presence: true
|
7
|
+
validates :action_scope, presence: true
|
8
|
+
validate :token_expiry
|
9
|
+
|
10
|
+
after_initialize :ensure_token
|
11
|
+
before_create :ensure_unique_token
|
12
|
+
|
13
|
+
TOKEN_STRENGTHS = {
|
14
|
+
mild: 8,
|
15
|
+
moderate: 16,
|
16
|
+
strong: 32,
|
17
|
+
}.with_indifferent_access.freeze
|
18
|
+
|
19
|
+
class << self
|
20
|
+
TOKEN_STRENGTHS.each_key do |strength|
|
21
|
+
define_method(strength) do |authenticatable, target_path, action_scope|
|
22
|
+
MagicToken.new(target_path: target_path, action_scope: action_scope).tap do |token|
|
23
|
+
token.magic_token_authenticatable = authenticatable
|
24
|
+
token.send(:strength=, strength)
|
25
|
+
token.save!
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def for(authenticatable:, target_path:, action_scope:, strength:, expiry: nil)
|
31
|
+
send(strength, authenticatable, target_path, action_scope).tap do |token|
|
32
|
+
token.expire_in(expiry) if expiry.present?
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def expired?
|
38
|
+
expires_at&.past?
|
39
|
+
end
|
40
|
+
|
41
|
+
def expire_in(duration)
|
42
|
+
update_attribute :expires_at, (Time.zone.now + duration)
|
43
|
+
self
|
44
|
+
end
|
45
|
+
|
46
|
+
def mild?
|
47
|
+
strength == :mild
|
48
|
+
end
|
49
|
+
|
50
|
+
def moderate?
|
51
|
+
strength == :moderate
|
52
|
+
end
|
53
|
+
|
54
|
+
def strong?
|
55
|
+
strength == :strong
|
56
|
+
end
|
57
|
+
|
58
|
+
def scope
|
59
|
+
return unless magic_token_authenticatable.present?
|
60
|
+
|
61
|
+
magic_token_authenticatable.model_name.singular.to_sym
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def token_expiry
|
67
|
+
return unless expired?
|
68
|
+
|
69
|
+
errors.add(:base, 'Token has expired')
|
70
|
+
end
|
71
|
+
|
72
|
+
def strength=(val)
|
73
|
+
self.token = generate_token(val)
|
74
|
+
end
|
75
|
+
|
76
|
+
def strength
|
77
|
+
token_strength || :moderate
|
78
|
+
end
|
79
|
+
|
80
|
+
def token_strength
|
81
|
+
return unless token
|
82
|
+
|
83
|
+
case token.length
|
84
|
+
when 32..64
|
85
|
+
:strong
|
86
|
+
when 16..31
|
87
|
+
:moderate
|
88
|
+
else
|
89
|
+
:mild
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def ensure_token
|
94
|
+
self.token ||= generate_token(strength)
|
95
|
+
end
|
96
|
+
|
97
|
+
def generate_token(strength)
|
98
|
+
Devise.friendly_token(TOKEN_STRENGTHS[strength])
|
99
|
+
end
|
100
|
+
|
101
|
+
def ensure_unique_token
|
102
|
+
self.token ||= generate_token(strength)
|
103
|
+
loop do
|
104
|
+
return unless MagicToken.where(token: token).first
|
105
|
+
|
106
|
+
self.token = generate_token(strength)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class CreateMagicLinksMagicTokens < ActiveRecord::Migration[5.2]
|
2
|
+
def change
|
3
|
+
create_table :magic_links_magic_tokens do |t|
|
4
|
+
t.string :token, null: false
|
5
|
+
t.string :target_path, null: false
|
6
|
+
t.json :action_scope, null: false
|
7
|
+
t.datetime :expires_at
|
8
|
+
t.references :magic_token_authenticatable, polymorphic: true, index: {name: 'index_magic_tokens_on_magic_token_authenticatable'}
|
9
|
+
|
10
|
+
t.timestamps
|
11
|
+
end
|
12
|
+
|
13
|
+
add_index :magic_links_magic_tokens, :token, unique: true
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module MagicLinks
|
2
|
+
module Middleware
|
3
|
+
class MagicTokenRedirect
|
4
|
+
def initialize(app)
|
5
|
+
@app = app
|
6
|
+
end
|
7
|
+
|
8
|
+
def call(env)
|
9
|
+
Handler.new(env).handle || app.call(env)
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
attr_reader :app
|
15
|
+
|
16
|
+
class Handler
|
17
|
+
def initialize(env)
|
18
|
+
@request = ActionDispatch::Request.new(env)
|
19
|
+
end
|
20
|
+
|
21
|
+
def handle
|
22
|
+
return unless redirect_request?
|
23
|
+
return root unless magic_token.present?
|
24
|
+
|
25
|
+
cookies.signed[magic_token_key] = magic_token.token if scope
|
26
|
+
respond_with_redirect magic_token.target_path
|
27
|
+
end
|
28
|
+
|
29
|
+
def root
|
30
|
+
respond_with_redirect '/', 'to the home page (token not found)'
|
31
|
+
end
|
32
|
+
|
33
|
+
attr_reader :request
|
34
|
+
|
35
|
+
def path
|
36
|
+
request.path
|
37
|
+
end
|
38
|
+
|
39
|
+
def redirect_request?
|
40
|
+
Templates.match?(path)
|
41
|
+
end
|
42
|
+
|
43
|
+
def magic_token
|
44
|
+
return unless token
|
45
|
+
|
46
|
+
@magic_token ||= MagicToken.find_by(token: token)
|
47
|
+
end
|
48
|
+
|
49
|
+
def token
|
50
|
+
@token ||= Templates.token_for(path)
|
51
|
+
end
|
52
|
+
|
53
|
+
def scope
|
54
|
+
magic_token&.scope
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def respond_with(status, headers, body)
|
60
|
+
ActionDispatch::Response.new(status, headers, body).to_a
|
61
|
+
end
|
62
|
+
|
63
|
+
def respond_with_redirect(path, path_desc = '')
|
64
|
+
body = %(You are being redirected <a href="#{path}">#{path_desc.present? ? path_desc : path}</a>)
|
65
|
+
ActionDispatch::Response.new(302, {'Location' => path}, body).to_a
|
66
|
+
end
|
67
|
+
|
68
|
+
def magic_token_key
|
69
|
+
"#{scope}_magic_token"
|
70
|
+
end
|
71
|
+
|
72
|
+
def cookies
|
73
|
+
request.cookie_jar
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module MagicLinks
|
2
|
+
class Engine < ::Rails::Engine
|
3
|
+
|
4
|
+
initializer 'magic_links.url_helpers' do
|
5
|
+
ActiveSupport.on_load(:action_controller) do
|
6
|
+
include MagicLinks::UrlHelper
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
initializer 'magic_links.middleware_redirect', before: :build_middleware_stack do |app|
|
11
|
+
app.config.middleware.insert_after ActionDispatch::Cookies, MagicLinks::Middleware::MagicTokenRedirect
|
12
|
+
end
|
13
|
+
|
14
|
+
initializer 'magic_links.devise_strategy' do
|
15
|
+
Warden::Strategies.add(:magic_token_authentication, MagicLinks::Strategies::MagicTokenAuthentication)
|
16
|
+
Devise.setup do |config|
|
17
|
+
config.warden do |manager|
|
18
|
+
manager.default_strategies(scope: :user).unshift :magic_token_authentication
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module MagicLinks
|
3
|
+
module Strategies
|
4
|
+
class MagicTokenAuthentication < Devise::Strategies::Base
|
5
|
+
def valid?
|
6
|
+
magic_token_cookie.present?
|
7
|
+
end
|
8
|
+
|
9
|
+
def authenticate!
|
10
|
+
return unless magic_token.present? && magic_token.valid?
|
11
|
+
return unless valid_devise_mapping?
|
12
|
+
return unless permitted_action?
|
13
|
+
|
14
|
+
success!(resource)
|
15
|
+
end
|
16
|
+
|
17
|
+
def store?
|
18
|
+
false
|
19
|
+
end
|
20
|
+
|
21
|
+
def clean_up_csrf?
|
22
|
+
false
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def valid_devise_mapping?
|
28
|
+
mapping.to == resource.class
|
29
|
+
end
|
30
|
+
|
31
|
+
def permitted_action?
|
32
|
+
return false unless permitted_controller?
|
33
|
+
|
34
|
+
Array(action_scope[controller]).include? action
|
35
|
+
end
|
36
|
+
|
37
|
+
def permitted_controller?
|
38
|
+
action_scope.keys.include? controller
|
39
|
+
end
|
40
|
+
|
41
|
+
def action_scope
|
42
|
+
magic_token.action_scope.with_indifferent_access
|
43
|
+
end
|
44
|
+
|
45
|
+
def resource
|
46
|
+
magic_token.magic_token_authenticatable
|
47
|
+
end
|
48
|
+
|
49
|
+
def magic_token
|
50
|
+
@magic_token ||= MagicToken.find_by token: magic_token_cookie
|
51
|
+
end
|
52
|
+
|
53
|
+
def magic_token_key
|
54
|
+
"#{scope}_magic_token"
|
55
|
+
end
|
56
|
+
|
57
|
+
def magic_token_cookie
|
58
|
+
@magic_token_cookie ||= cookies.signed[magic_token_key]
|
59
|
+
end
|
60
|
+
|
61
|
+
def controller
|
62
|
+
params[:controller]
|
63
|
+
end
|
64
|
+
|
65
|
+
def action
|
66
|
+
params[:action]
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
module MagicLinks
|
2
|
+
class Template
|
3
|
+
VALID_TEMPLATE_PATTERN = %r{^/([A-Za-z0-9_\-]+)/(:token)$}.freeze
|
4
|
+
|
5
|
+
attr_reader :name, :pattern, :action_scope, :strength, :expiry
|
6
|
+
|
7
|
+
def initialize(pattern:, action_scope:, strength:, expiry:)
|
8
|
+
raise ArgumentError, "Pattern must be of the form '/xyz/:token'" unless VALID_TEMPLATE_PATTERN.match?(pattern)
|
9
|
+
|
10
|
+
@pattern = pattern
|
11
|
+
@action_scope = action_scope
|
12
|
+
@strength = strength
|
13
|
+
@expiry = expiry
|
14
|
+
end
|
15
|
+
|
16
|
+
def match?(path)
|
17
|
+
matcher.match?(path)
|
18
|
+
end
|
19
|
+
|
20
|
+
def token_for(path)
|
21
|
+
matcher.match(path)&.captures&.first
|
22
|
+
end
|
23
|
+
|
24
|
+
def magic_link_for(user, path, expiry = nil)
|
25
|
+
expiry ||= self.expiry
|
26
|
+
magic_token = magic_token_for(user, path, expiry)
|
27
|
+
pattern.sub(':token', magic_token.token)
|
28
|
+
end
|
29
|
+
|
30
|
+
def magic_url_for(user, path, expiry = nil)
|
31
|
+
url_for(magic_link_for(user, path, expiry))
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def url_for(path)
|
37
|
+
ActionDispatch::Http::URL.url_for(default_url_options.merge(path: path))
|
38
|
+
end
|
39
|
+
|
40
|
+
def default_url_options
|
41
|
+
Rails.application.routes.default_url_options
|
42
|
+
end
|
43
|
+
|
44
|
+
def magic_token_for(user, path, expiry)
|
45
|
+
MagicToken.for(magic_token_params_for(user, path, expiry))
|
46
|
+
end
|
47
|
+
|
48
|
+
def magic_token_params_for(user, path, expiry)
|
49
|
+
{
|
50
|
+
authenticatable: user,
|
51
|
+
target_path: path,
|
52
|
+
action_scope: action_scope,
|
53
|
+
strength: strength,
|
54
|
+
expiry: expiry
|
55
|
+
}
|
56
|
+
end
|
57
|
+
|
58
|
+
def matcher
|
59
|
+
@matcher ||= build_matcher
|
60
|
+
end
|
61
|
+
|
62
|
+
def build_matcher
|
63
|
+
sections = VALID_TEMPLATE_PATTERN.match(pattern).captures
|
64
|
+
%r{^/#{sections[0]}/([A-Za-z0-9_\-]+)$}
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module MagicLinks
|
2
|
+
module Templates
|
3
|
+
class << self
|
4
|
+
def add(name:, pattern:, action_scope:, strength:, expiry: nil)
|
5
|
+
templates[name] = Template.new(pattern: pattern,
|
6
|
+
action_scope: action_scope,
|
7
|
+
strength: strength,
|
8
|
+
expiry: expiry)
|
9
|
+
end
|
10
|
+
|
11
|
+
def find(name)
|
12
|
+
templates[name]
|
13
|
+
end
|
14
|
+
|
15
|
+
def match?(path)
|
16
|
+
templates.values.any? do |template|
|
17
|
+
template.match? path
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def token_for(path)
|
22
|
+
templates.values.find { |template| template.match?(path) }&.token_for(path)
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def templates
|
28
|
+
@templates ||= {}
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
data/lib/magic_links.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'magic_links/engine'
|
2
|
+
require 'magic_links/template'
|
3
|
+
require 'magic_links/templates'
|
4
|
+
require 'magic_links/middleware/magic_token_redirect'
|
5
|
+
require 'magic_links/strategies/magic_token_authentication'
|
6
|
+
require 'magic_links/rails'
|
7
|
+
|
8
|
+
module MagicLinks
|
9
|
+
def self.add_template(*args)
|
10
|
+
MagicLinks::Templates.add(*args)
|
11
|
+
end
|
12
|
+
end
|
metadata
ADDED
@@ -0,0 +1,146 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: magic_links
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- James wozniak
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2022-06-24 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rails
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '5.2'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '5.2'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: devise
|
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: sqlite3
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec-rails
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: factory_bot_rails
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: shoulda-matchers
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
description: Manages the creation and use of 'magic' tokens. These can be used to
|
98
|
+
provide authenticated access to a subset of controller actions, avoiding the need
|
99
|
+
for users to be signed in.
|
100
|
+
email:
|
101
|
+
- wozza35@hotmail.com
|
102
|
+
executables: []
|
103
|
+
extensions: []
|
104
|
+
extra_rdoc_files: []
|
105
|
+
files:
|
106
|
+
- MIT-LICENSE
|
107
|
+
- README.md
|
108
|
+
- Rakefile
|
109
|
+
- app/helpers/magic_links/url_helper.rb
|
110
|
+
- app/models/magic_links/application_record.rb
|
111
|
+
- app/models/magic_links/magic_token.rb
|
112
|
+
- db/migrate/20211215150823_create_magic_links_magic_tokens.rb
|
113
|
+
- lib/magic_links.rb
|
114
|
+
- lib/magic_links/engine.rb
|
115
|
+
- lib/magic_links/middleware/magic_token_redirect.rb
|
116
|
+
- lib/magic_links/rails.rb
|
117
|
+
- lib/magic_links/strategies/magic_token_authentication.rb
|
118
|
+
- lib/magic_links/template.rb
|
119
|
+
- lib/magic_links/templates.rb
|
120
|
+
- lib/magic_links/version.rb
|
121
|
+
homepage: https://github.com/ClickMechanic/magic_links
|
122
|
+
licenses:
|
123
|
+
- MIT
|
124
|
+
metadata:
|
125
|
+
allowed_push_host: https://rubygems.org
|
126
|
+
source_code_uri: https://github.com/ClickMechanic/magic_links
|
127
|
+
post_install_message:
|
128
|
+
rdoc_options: []
|
129
|
+
require_paths:
|
130
|
+
- lib
|
131
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
132
|
+
requirements:
|
133
|
+
- - ">="
|
134
|
+
- !ruby/object:Gem::Version
|
135
|
+
version: '0'
|
136
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
137
|
+
requirements:
|
138
|
+
- - ">="
|
139
|
+
- !ruby/object:Gem::Version
|
140
|
+
version: '0'
|
141
|
+
requirements: []
|
142
|
+
rubygems_version: 3.1.6
|
143
|
+
signing_key:
|
144
|
+
specification_version: 4
|
145
|
+
summary: Token based authentication
|
146
|
+
test_files: []
|