searls-auth 0.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.
Files changed (33) hide show
  1. checksums.yaml +7 -0
  2. data/.standard.yml +3 -0
  3. data/CHANGELOG.md +5 -0
  4. data/LICENSE.txt +675 -0
  5. data/README.md +1 -0
  6. data/Rakefile +6 -0
  7. data/app/controllers/searls/auth/base_controller.rb +35 -0
  8. data/app/controllers/searls/auth/logins_controller.rb +44 -0
  9. data/app/controllers/searls/auth/registrations_controller.rb +35 -0
  10. data/app/controllers/searls/auth/verifications_controller.rb +64 -0
  11. data/app/helpers/searls/auth/application_helper.rb +82 -0
  12. data/app/javascript/controllers/searls_auth_login_controller.js +37 -0
  13. data/app/javascript/controllers/searls_auth_otp_controller.js +21 -0
  14. data/app/mailers/searls/auth/base_mailer.rb +8 -0
  15. data/app/mailers/searls/auth/login_link_mailer.rb +24 -0
  16. data/app/views/searls/auth/layouts/mailer.html.erb +54 -0
  17. data/app/views/searls/auth/login_link_mailer/login_link.html.erb +37 -0
  18. data/app/views/searls/auth/login_link_mailer/login_link.text.erb +20 -0
  19. data/app/views/searls/auth/logins/show.html.erb +20 -0
  20. data/app/views/searls/auth/registrations/show.html.erb +19 -0
  21. data/app/views/searls/auth/verifications/show.html.erb +23 -0
  22. data/config/importmap.rb +2 -0
  23. data/config/routes.rb +12 -0
  24. data/lib/searls/auth/authenticates_user.rb +32 -0
  25. data/lib/searls/auth/config.rb +47 -0
  26. data/lib/searls/auth/creates_user.rb +37 -0
  27. data/lib/searls/auth/emails_link.rb +15 -0
  28. data/lib/searls/auth/engine.rb +28 -0
  29. data/lib/searls/auth/railtie.rb +20 -0
  30. data/lib/searls/auth/resets_session.rb +13 -0
  31. data/lib/searls/auth/version.rb +5 -0
  32. data/lib/searls/auth.rb +63 -0
  33. metadata +87 -0
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ require "standard/rake"
4
+ require "tldr/rake"
5
+
6
+ task default: [:tldr, "standard:fix"]
@@ -0,0 +1,35 @@
1
+ require "securerandom"
2
+
3
+ module Searls
4
+ module Auth
5
+ class BaseController < ApplicationController # TODO should this be ActionController::Base? Trade-offs?
6
+ helper Rails.application.helpers
7
+ helper Rails.application.routes.url_helpers
8
+
9
+ protected
10
+
11
+ def searls_auth_config
12
+ Searls::Auth.config
13
+ end
14
+
15
+ def attach_short_code_to_session!(user)
16
+ session[:email_auth_short_code_user_id] = user.id
17
+ session[:email_auth_short_code] = SecureRandom.random_number(1000000).to_s.rjust(6, "0")
18
+ session[:email_auth_short_code_generated_at] = Time.current
19
+ end
20
+
21
+ def reset_expired_short_code
22
+ if session[:email_auth_short_code_generated_at].present? &&
23
+ Time.zone.parse(session[:email_auth_short_code_generated_at]) < Searls::Auth.config.token_expiry_minutes.minutes.ago
24
+ clear_short_code_from_session!
25
+ end
26
+ end
27
+
28
+ def clear_short_code_from_session!
29
+ session.delete(:email_auth_short_code_user_id)
30
+ session.delete(:email_auth_short_code_generated_at)
31
+ session.delete(:email_auth_short_code)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,44 @@
1
+ module Searls
2
+ module Auth
3
+ class LoginsController < BaseController
4
+ before_action :reset_expired_short_code
5
+
6
+ def show
7
+ render searls_auth_config.login_view, layout: searls_auth_config.layout
8
+ end
9
+
10
+ def create
11
+ user = searls_auth_config.user_finder_by_email.call(params[:email])
12
+
13
+ if user.present?
14
+ attach_short_code_to_session!(user)
15
+ EmailsLink.new.email(
16
+ user:,
17
+ redirect_path: params[:redirect_path],
18
+ redirect_subdomain: params[:redirect_subdomain],
19
+ short_code: session[:email_auth_short_code]
20
+ )
21
+ flash[:notice] = "Login details sent to #{params[:email]}"
22
+ redirect_to verify_path(
23
+ redirect_path: params[:redirect_path],
24
+ redirect_subdomain: params[:redirect_subdomain]
25
+ )
26
+ else
27
+ flash.now[:error] = "We don't know that email. <a href=\"#{register_path(
28
+ email: params[:email],
29
+ redirect_path: params[:redirect_path],
30
+ redirect_subdomain: params[:redirect_subdomain]
31
+ )}\">Sign up</a> instead?".html_safe
32
+ render searls_auth_config.login_view, layout: searls_auth_config.layout, status: :unprocessable_entity
33
+ end
34
+ end
35
+
36
+ def destroy
37
+ ResetsSession.new.reset(self, except_for: [:has_logged_in_before])
38
+
39
+ flash[:notice] = "You've been logged out."
40
+ redirect_to login_path
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,35 @@
1
+ module Searls
2
+ module Auth
3
+ class RegistrationsController < BaseController
4
+ def show
5
+ render searls_auth_config.register_view, layout: searls_auth_config.layout
6
+ end
7
+
8
+ def create
9
+ result = CreatesUser.new.call(params)
10
+
11
+ if result.success?
12
+ attach_short_code_to_session!(result.user)
13
+
14
+ redirect_params = {
15
+ redirect_path: searls_auth_config.resolve(
16
+ :redirect_path_after_register,
17
+ result.user, params, request, main_app
18
+ )
19
+ }
20
+
21
+ EmailsLink.new.email(
22
+ user: result.user,
23
+ short_code: session[:email_auth_short_code],
24
+ **redirect_params
25
+ )
26
+ flash[:notice] = "Verification email sent to #{params[:email]}"
27
+ redirect_to verify_path(**redirect_params)
28
+ else
29
+ flash.now[:error] = result.error_messages
30
+ render searls_auth_config.register_view, layout: searls_auth_config.layout, status: :unprocessable_entity
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,64 @@
1
+ module Searls
2
+ module Auth
3
+ class VerificationsController < BaseController
4
+ before_action :reset_expired_short_code
5
+
6
+ def show
7
+ render searls_auth_config.verify_view, layout: searls_auth_config.layout
8
+ end
9
+
10
+ def create
11
+ auth_method = params[:short_code].present? ? :short_code : :token
12
+ authenticator = AuthenticatesUser.new
13
+ result = case auth_method
14
+ when :short_code
15
+ authenticator.authenticate_by_short_code(params[:short_code], session)
16
+ when :token
17
+ authenticator.authenticate_by_token(params[:token])
18
+ end
19
+
20
+ if result.success?
21
+ session[:user_id] = result.user.id
22
+ session[:has_logged_in_before] = true
23
+ if params[:redirect_subdomain].present? && params[:redirect_subdomain] != request.subdomain
24
+ redirect_to generate_full_url(
25
+ params[:redirect_path],
26
+ params[:redirect_subdomain]
27
+ ), allow_other_host: true
28
+ elsif params[:redirect_path].present?
29
+ redirect_to params[:redirect_path]
30
+ else
31
+ redirect_to searls_auth_config.resolve(:default_redirect_path_after_login,
32
+ result.user, params, request, main_app)
33
+ end
34
+ elsif auth_method == :short_code
35
+ flash[:error] = "We weren't able to log you in with that code. Try again?"
36
+ render searls_auth_config.verify_view, layout: searls_auth_config.layout, status: :unprocessable_entity
37
+ else
38
+ flash[:error] = "We weren't able to log you in with that link. Try again?"
39
+ redirect_to login_path(
40
+ redirect_path: params[:redirect_path],
41
+ redirect_subdomain: params[:redirect_subdomain]
42
+ )
43
+ end
44
+ end
45
+
46
+ def destroy
47
+ ResetsSession.new.reset(self,
48
+ except_for: searls_auth_config.preserve_session_keys_after_logout)
49
+
50
+ flash[:notice] = "You've been logged out."
51
+ redirect_to login_path
52
+ end
53
+
54
+ private
55
+
56
+ def generate_full_url(path, subdomain)
57
+ port = request.port
58
+ port_string = (port == 80 || port == 443) ? "" : ":#{port}"
59
+
60
+ "#{request.protocol}#{subdomain}.#{request.domain}#{port_string}#{path}"
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,82 @@
1
+ module Searls
2
+ module Auth
3
+ class HelperMethods
4
+ def initialize(view_context)
5
+ @view_context = view_context
6
+ end
7
+
8
+ def routes
9
+ Searls::Auth::Engine.routes.url_helpers
10
+ end
11
+
12
+ def login_path(**kwargs)
13
+ routes.login_path(forwardable_params.merge(kwargs))
14
+ end
15
+
16
+ def login_url(**kwargs)
17
+ routes.login_url(forwardable_params.merge(kwargs))
18
+ end
19
+
20
+ def register_path(**kwargs)
21
+ routes.register_path(forwardable_params.merge(kwargs))
22
+ end
23
+
24
+ def register_url(**kwargs)
25
+ routes.register_url(forwardable_params.merge(kwargs))
26
+ end
27
+
28
+ def login_stimulus_controller
29
+ "searls-auth-login"
30
+ end
31
+
32
+ def email_field_stimulus_data
33
+ {
34
+ action: "change->searls-auth-login#updateEmail"
35
+ }
36
+ end
37
+
38
+ def otp_stimulus_controller
39
+ "searls-auth-otp"
40
+ end
41
+
42
+ def otp_field_stimulus_data
43
+ {
44
+ action: "paste->searls-auth-otp#pasted input->otp#caret click->searls-auth-otp#caret keydown->searls-auth-otp#caret keyup->searls-auth-otp#caret",
45
+ searls_auth_otp_target: "input"
46
+ }
47
+ end
48
+
49
+ def enable_turbo?
50
+ params[:redirect_subdomain].blank? || params[:redirect_subdomain] == request.subdomain
51
+ end
52
+
53
+ def attr_for(model, field_name)
54
+ model.attributes[field_name.to_s]
55
+ end
56
+
57
+ def rpad(s, spacer = " ", times = 1)
58
+ "#{s}#{spacer * times}"
59
+ end
60
+
61
+ private
62
+
63
+ def forwardable_params
64
+ {
65
+ email: params[:email],
66
+ redirect_path: params[:redirect_path],
67
+ redirect_subdomain: params[:redirect_subdomain]
68
+ }.compact_blank
69
+ end
70
+
71
+ def params
72
+ @view_context.params
73
+ end
74
+ end
75
+
76
+ module ApplicationHelper
77
+ def searls_auth_helper
78
+ @__searls_auth_helper ||= HelperMethods.new(self)
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,37 @@
1
+ import { Controller } from '@hotwired/stimulus'
2
+
3
+ export default class SearlsAuthLoginController extends Controller {
4
+ static values = {
5
+ email: String,
6
+ emailFieldName: {
7
+ type: String,
8
+ default: 'email'
9
+ },
10
+ timeZoneFieldName: {
11
+ type: String,
12
+ default: 'time_zone'
13
+ }
14
+ }
15
+
16
+ connect () {
17
+ this.emailValue = window.sessionStorage.getItem('__searls_auth_email') || undefined
18
+ if (this.emailValue) {
19
+ this.element.querySelector(`[name=${this.emailFieldNameValue}]`).value = this.emailValue
20
+ }
21
+
22
+ this.#setTimeZoneFieldMaybe()
23
+ }
24
+
25
+ updateEmail (e) {
26
+ debugger
27
+ this.emailValue = e.currentTarget.value
28
+ window.sessionStorage.setItem('__searls_auth_email', this.emailValue)
29
+ }
30
+
31
+ #setTimeZoneFieldMaybe () {
32
+ const hiddenField = this.element.querySelector(`[name=${this.timeZoneFieldNameValue}]`)
33
+ if (hiddenField) {
34
+ hiddenField.value ||= Intl.DateTimeFormat().resolvedOptions().timeZone
35
+ }
36
+ }
37
+ }
@@ -0,0 +1,21 @@
1
+ import { Controller } from '@hotwired/stimulus'
2
+
3
+ export default class SearlsAuthOtpController extends Controller {
4
+ static targets = ['input']
5
+
6
+ pasted (e) {
7
+ const content = e.clipboardData?.getData('text')
8
+ if (content && content.trim().match(/^\d{6}$/)) {
9
+ this.inputTarget.value = content.trim()
10
+ this.inputTarget.closest('form').requestSubmit()
11
+ }
12
+ }
13
+
14
+ // Maybe not every design needs this but mine does: hide the caret if it's after the end of the max length
15
+ caret () {
16
+ const shouldHideCaret = this.inputTarget.maxLength === this.inputTarget.selectionStart &&
17
+ this.inputTarget.maxLength === this.inputTarget.selectionEnd
18
+
19
+ this.inputTarget.style.caretColor = shouldHideCaret ? 'transparent' : ''
20
+ }
21
+ }
@@ -0,0 +1,8 @@
1
+ module Searls
2
+ module Auth
3
+ class BaseMailer < ApplicationMailer # TODO should this be ActionMailer::Base? Trade-offs?
4
+ helper Searls::Auth::ApplicationHelper
5
+ include Searls::Auth::ApplicationHelper
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,24 @@
1
+ module Searls
2
+ module Auth
3
+ class LoginLinkMailer < BaseMailer
4
+ def login_link
5
+ @config = Searls::Auth.config
6
+ @user = params[:user]
7
+ @token = params[:token]
8
+ @redirect_path = params[:redirect_path]
9
+ @redirect_subdomain = params[:redirect_subdomain]
10
+ @short_code = params[:short_code]
11
+
12
+ mail(
13
+ to: format_to(@user),
14
+ subject: "Your #{searls_auth_helper.rpad(@config.app_name)} login code is #{@short_code}",
15
+ template_path: @config.mail_login_template_path,
16
+ template_name: @config.mail_login_template_name
17
+ ) do |format|
18
+ format.html { render layout: @config.mail_layout }
19
+ format.text
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,54 @@
1
+ <!DOCTYPE html>
2
+ <html xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta http-equiv="x-ua-compatible" content="ie=edge">
6
+ <meta name="color-scheme" content="light">
7
+ <meta name="supported-color-schemes" content="light only">
8
+ <meta name="viewport" content="width=device-width, initial-scale=1">
9
+ <meta name="format-detection" content="telephone=no, date=no, address=no, email=no, url=no">
10
+ <meta name="x-apple-disable-message-reformatting">
11
+ <title><%= message.subject %></title>
12
+ </head>
13
+ <body style="background-color: <%= Searls::Auth.config.email_background_color %>;">
14
+ <div role="article" aria-roledescription="email" aria-label="<%= message.subject %>" lang="en">
15
+ <table border="0" cellpadding="0" cellspacing="0" style="font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background-color: <%= Searls::Auth.config.email_background_color %>; color: #111827;" width="100%">
16
+ <tr>
17
+ <td height="96" align="center">
18
+ <% if Searls::Auth.config.email_banner_image_path.present? %>
19
+ <% banner_img = image_tag(
20
+ email_image_data_url(Searls::Auth.config.email_banner_image_path),
21
+ style: "height: 48px;"
22
+ ) %>
23
+ <% if Searls::Auth.config.app_url.present? %>
24
+ <%= link_to Searls::Auth.config.app_url, style: "text-align: center;" do %>
25
+ <%= banner_img %>
26
+ <% end %>
27
+ <% else %>
28
+ <div style="text-align: center;">
29
+ <%= banner_img %>
30
+ </div>
31
+ <% end %>
32
+ <% end %>
33
+ </td>
34
+ </tr>
35
+ <tr>
36
+ <td align="center">
37
+ <table width="<%= content_for?(:width) ? yield(:width) : 480 %>" border="0" cellpadding="0" cellspacing="0" style="padding-left: 0.5rem; padding-right: 0.5rem; box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); border-radius: 0.75rem; background-color: white; width: 100%; max-width: <%= content_for?(:width) ? yield(:width) : 480 %>px;">
38
+ <tr>
39
+ <td>
40
+ <%= yield %>
41
+ </td>
42
+ </tr>
43
+ </table>
44
+ </td>
45
+ </tr>
46
+ <tr>
47
+ <td height="32">
48
+ <%= yield :footer %>
49
+ </td>
50
+ </tr>
51
+ </table>
52
+ </div>
53
+ </body>
54
+ </html>
@@ -0,0 +1,37 @@
1
+ <%= content_for :width, 480 %>
2
+ <p>
3
+ <% if (user_name = searls_auth_helper.attr_for(@user, @config.user_name_field)).present? %>
4
+ Hello, <strong><%= user_name %></strong>!
5
+ <% else %>
6
+ Hello!
7
+ <% end %>
8
+ </p>
9
+ <p style="margin-top: 1rem;">
10
+ You can click the button below to log in to your account:
11
+ <div style="margin-top: 2rem; text-align: center;">
12
+ <%= link_to verify_token_url({
13
+ token: @token,
14
+ redirect_path: @redirect_path,
15
+ redirect_subdomain: @redirect_subdomain
16
+ }.compact_blank) do %>
17
+ <span style="width: 70%; box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); display: inline-block; padding-left: 2rem; padding-right: 2rem; padding-top: 1rem; padding-bottom: 1rem; font-size: 1.5rem; font-weight: 700; border-radius: 0.75rem; background-color: <%= @config.email_button_color %>; color: white;">Log in</span>
18
+ <% end %>
19
+ <div style="margin-top: 0.5rem; font-size: 0.875rem; color: #6b7280;">
20
+ (This link will expire in <%= @config.token_expiry_minutes %> minutes.)
21
+ </div>
22
+ </div>
23
+ </p>
24
+ <p style="margin-top: 2rem;">
25
+ Alternatively, you can type or paste the following code into your browser:
26
+ <div style="margin-top: 2rem; text-align: center;">
27
+ <span id="one-time-code" style="width: 70%; display: inline-block; padding-left: 2rem; padding-right: 2rem; padding-top: 1rem; padding-bottom: 1rem; font-size: 1.5rem; font-weight: 700; border: 1px solid; border-radius: 0.75rem; background-color: #f3f4f6; border-color: #e5e7eb;">
28
+ <%= @short_code %>
29
+ </span>
30
+ <div style="margin-top: 0.5rem; font-size: 0.875rem; color: #6b7280;">
31
+ (So will this code.)
32
+ </div>
33
+ </div>
34
+ </p>
35
+ <p style="margin-top: 2rem; font-size: 0.875rem; color: #6b7280;">
36
+ p.s. Didn't try to log in? You can safely ignore this email. If this keeps happening, please <%= link_to "let us know", "mailto:#{@config.support_email_address}" %>.
37
+ </p>
@@ -0,0 +1,20 @@
1
+ <% if (user_name = searls_auth_helper.attr_for(@user, @config.user_name_field)).present? %>
2
+ Hi, <%= user_name %>!
3
+ <% else %>
4
+ Hello!
5
+ <% end %>
6
+
7
+ You can log in to your <%= searls_auth_helper.rpad(@config.app_name) %>account at this URL:
8
+
9
+ <%= verify_token_url({
10
+ token: @token,
11
+ redirect_path: @redirect_path,
12
+ redirect_subdomain: @redirect_subdomain
13
+ }.compact_blank) %>
14
+
15
+ Alternatively, you can type or paste the following code into your browser:
16
+
17
+ <%= @short_code %>
18
+
19
+ Didn't try to log in? You can safely ignore this email.<%= " If this keeps
20
+ happening, please let us know at #{@config.support_email_address}" if @config.support_email_address.present? %>
@@ -0,0 +1,20 @@
1
+ <h1>Log In</h1>
2
+
3
+ <div style="background: rgba(255,0,0, 0.1)">
4
+ <%= flash[:error] %>
5
+ </div>
6
+ <div style="background: rgba(0,0,255, 0.1)">
7
+ <%= flash[:notice] %>
8
+ </div>
9
+
10
+ <%= form_with url: login_path, method: :post, data: {controller: searls_auth_helper.login_stimulus_controller} do |f| %>
11
+ <%= f.hidden_field :redirect_path, value: params[:redirect_path] %>
12
+ <%= f.hidden_field :redirect_subdomain, value: params[:redirect_subdomain] %>
13
+ <%= f.email_field :email, required: true, value: params[:email], data: searls_auth_helper.email_field_stimulus_data %>
14
+ <%= f.submit "Log in" %>
15
+ <% end %>
16
+
17
+ <p>
18
+ Not registered yet? <%= link_to "Sign up", searls_auth_helper.register_path %> instead!
19
+ </p>
20
+
@@ -0,0 +1,19 @@
1
+ <h1>Create your account</h1>
2
+
3
+ <div style="background: rgba(255,0,0, 0.1)">
4
+ <%= flash[:error] %>
5
+ </div>
6
+ <div style="background: rgba(0,0,255, 0.1)">
7
+ <%= flash[:notice] %>
8
+ </div>
9
+
10
+ <%= form_with url: register_path, data: {controller: searls_auth_helper.login_stimulus_controller} do |f| %>
11
+ <%= f.hidden_field :redirect_path, value: params[:redirect_path] %>
12
+ <%= f.hidden_field :redirect_subdomain, value: params[:redirect_subdomain] %>
13
+ <%= f.email_field :email, value: params[:email], required: true, data: searls_auth_helper.email_field_stimulus_data %>
14
+ <%= f.submit "Register" %>
15
+ <% end %>
16
+
17
+ <p>
18
+ Already have an account? <%= link_to "Log in", searls_auth_helper.login_path %> instead.
19
+ </p>
@@ -0,0 +1,23 @@
1
+ <h1>Check your email!</h1>
2
+ <p>
3
+ In the next few moments, you should receive an email that will provide you
4
+ two ways to log in: a link and a six-digit code that you can enter below.
5
+ </p>
6
+ <%= form_with(url: verify_path, method: :post, data: {
7
+ # Don't use turbo on cross-domain redirects
8
+ turbo: searls_auth_helper.enable_turbo?
9
+ }) do |f| %>
10
+ <%= f.hidden_field :redirect_path, value: params[:redirect_path] %>
11
+ <%= f.hidden_field :redirect_subdomain, value: params[:redirect_subdomain] %>
12
+ <div data-controller="<%= searls_auth_helper.otp_stimulus_controller %>">
13
+ <%= f.text_field :short_code,
14
+ maxlength: 6,
15
+ inputmode: "numeric",
16
+ pattern: "\\d{6}",
17
+ autocomplete: "one-time-code",
18
+ title: "six-digit code that was emailed to you",
19
+ data: searls_auth_helper.otp_field_stimulus_data
20
+ %>
21
+ </div>
22
+ <%= f.submit "Log in" %>
23
+ <% end %>
@@ -0,0 +1,2 @@
1
+ pin "controllers/searls_auth_login_controller", to: "controllers/searls_auth_login_controller.js"
2
+ pin "controllers/searls_auth_otp_controller", to: "controllers/searls_auth_otp_controller.js"
data/config/routes.rb ADDED
@@ -0,0 +1,12 @@
1
+ Searls::Auth::Engine.routes.draw do
2
+ get "register", to: "registrations#show"
3
+ post "register", to: "registrations#create"
4
+
5
+ get "login", to: "logins#show"
6
+ post "login", to: "logins#create"
7
+ get "logout", to: "logins#destroy"
8
+
9
+ get "login/verify", to: "verifications#show", as: :verify
10
+ post "login/verify", to: "verifications#create"
11
+ get "login/verify_token", to: "verifications#create", as: :verify_token
12
+ end
@@ -0,0 +1,32 @@
1
+ module Searls
2
+ module Auth
3
+ class AuthenticatesUser
4
+ Result = Struct.new(:success?, :user, keyword_init: true)
5
+
6
+ def authenticate_by_short_code(short_code, session)
7
+ user = Searls::Auth.config.user_finder_by_id.call(session[:email_auth_short_code_user_id])
8
+
9
+ if session[:email_auth_short_code_generated_at].present? &&
10
+ Time.zone.parse(session[:email_auth_short_code_generated_at]) > Searls::Auth.config.token_expiry_minutes.minutes.ago &&
11
+ user.present? &&
12
+ short_code == session[:email_auth_short_code]
13
+ Searls::Auth.config.after_login_success&.call(user)
14
+ Result.new(success?: true, user: user)
15
+ else
16
+ Result.new(success?: false)
17
+ end
18
+ end
19
+
20
+ def authenticate_by_token(token)
21
+ user = Searls::Auth.config.user_finder_by_token.call(token)
22
+
23
+ if user.present?
24
+ Searls::Auth.config.after_login_success&.call(user)
25
+ Result.new(success?: true, user: user)
26
+ else
27
+ Result.new(success?: false)
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,47 @@
1
+ module Searls
2
+ module Auth
3
+ Config = Struct.new(
4
+ # Data setup
5
+ :user_finder_by_email, # proc (email)
6
+ :user_finder_by_id, # proc (id)
7
+ :user_finder_by_token, # proc (token)
8
+ :user_initializer, # proc (params)
9
+ :user_name_field, # string
10
+ :token_generator, # proc ()
11
+ :token_expiry_minutes, # integer
12
+ # Controller setup
13
+ :preserve_session_keys_after_logout, # array of symbols
14
+ # View setup
15
+ :layout, # string
16
+ :login_view, # string
17
+ :register_view, # string
18
+ :verify_view, # string
19
+ :mail_layout, # string
20
+ :mail_login_template_path, # string
21
+ :mail_login_template_name, # string
22
+ # Routing setup
23
+ :redirect_path_after_register, # string or proc, all new registrations redirect here
24
+ :default_redirect_path_after_login, # string or proc, only redirected here if redirect_path param not set
25
+ # Hook setup
26
+ :validate_registration, # proc (user, params, errors = []), must return an array of error messages where empty means valid
27
+ :after_login_success, # proc (user)
28
+ # Branding setup
29
+ :app_name, # string
30
+ :app_url, # string
31
+ :support_email_address, # string
32
+ :email_banner_image_path, # string
33
+ :email_background_color, # string
34
+ :email_button_color, # string
35
+ keyword_init: true
36
+ ) do
37
+ # Get values from values that might be procs
38
+ def resolve(option, *args)
39
+ if self[option].respond_to?(:call)
40
+ self[option].call(*args)
41
+ else
42
+ self[option]
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end