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 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,5 @@
1
+ module MagicLinks
2
+ class ApplicationRecord < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ end
5
+ 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,13 @@
1
+ require 'devise'
2
+
3
+ module MagicLinks
4
+ class Engine < ::Rails::Engine
5
+ isolate_namespace MagicLinks
6
+
7
+ config.generators do |g|
8
+ g.test_framework :rspec
9
+ g.fixture_replacement :factory_bot
10
+ g.factory_bot dir: 'spec/factories'
11
+ end
12
+ end
13
+ 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
@@ -0,0 +1,7 @@
1
+ module MagicLinks
2
+ VERSION = '1.0.1'
3
+
4
+ def self.version
5
+ VERSION
6
+ end
7
+ end
@@ -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: []