File: helpers.rb

package info (click to toggle)
ruby-devise 4.8.1-1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm
  • size: 752 kB
  • sloc: ruby: 3,877; sh: 24; makefile: 11
file content (293 lines) | stat: -rw-r--r-- 11,619 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
# frozen_string_literal: true

module Devise
  module Controllers
    # Those helpers are convenience methods added to ApplicationController.
    module Helpers
      extend ActiveSupport::Concern
      include Devise::Controllers::SignInOut
      include Devise::Controllers::StoreLocation

      included do
        if respond_to?(:helper_method)
          helper_method :warden, :signed_in?, :devise_controller?
        end
      end

      module ClassMethods
        # Define authentication filters and accessor helpers for a group of mappings.
        # These methods are useful when you are working with multiple mappings that
        # share some functionality. They are pretty much the same as the ones
        # defined for normal mappings.
        #
        # Example:
        #
        #   inside BlogsController (or any other controller, it doesn't matter which):
        #     devise_group :blogger, contains: [:user, :admin]
        #
        #   Generated methods:
        #     authenticate_blogger!  # Redirects unless user or admin are signed in
        #     blogger_signed_in?     # Checks whether there is either a user or an admin signed in
        #     current_blogger        # Currently signed in user or admin
        #     current_bloggers       # Currently signed in user and admin
        #
        #   Use:
        #     before_action :authenticate_blogger!              # Redirects unless either a user or an admin are authenticated
        #     before_action ->{ authenticate_blogger! :admin }  # Redirects to the admin login page
        #     current_blogger :user                             # Preferably returns a User if one is signed in
        #
        def devise_group(group_name, opts = {})
          mappings = "[#{ opts[:contains].map { |m| ":#{m}" }.join(',') }]"

          class_eval <<-METHODS, __FILE__, __LINE__ + 1
            def authenticate_#{group_name}!(favorite = nil, opts = {})
              unless #{group_name}_signed_in?
                mappings = #{mappings}
                mappings.unshift mappings.delete(favorite.to_sym) if favorite
                mappings.each do |mapping|
                  opts[:scope] = mapping
                  warden.authenticate!(opts) if !devise_controller? || opts.delete(:force)
                end
              end
            end

            def #{group_name}_signed_in?
              #{mappings}.any? do |mapping|
                warden.authenticate?(scope: mapping)
              end
            end

            def current_#{group_name}(favorite = nil)
              mappings = #{mappings}
              mappings.unshift mappings.delete(favorite.to_sym) if favorite
              mappings.each do |mapping|
                current = warden.authenticate(scope: mapping)
                return current if current
              end
              nil
            end

            def current_#{group_name.to_s.pluralize}
              #{mappings}.map do |mapping|
                warden.authenticate(scope: mapping)
              end.compact
            end

            if respond_to?(:helper_method)
              helper_method "current_#{group_name}", "current_#{group_name.to_s.pluralize}", "#{group_name}_signed_in?"
            end
          METHODS
        end

        def log_process_action(payload)
          payload[:status] ||= 401 unless payload[:exception]
          super
        end
      end

      # Define authentication filters and accessor helpers based on mappings.
      # These filters should be used inside the controllers as before_actions,
      # so you can control the scope of the user who should be signed in to
      # access that specific controller/action.
      # Example:
      #
      #   Roles:
      #     User
      #     Admin
      #
      #   Generated methods:
      #     authenticate_user!  # Signs user in or redirect
      #     authenticate_admin! # Signs admin in or redirect
      #     user_signed_in?     # Checks whether there is a user signed in or not
      #     admin_signed_in?    # Checks whether there is an admin signed in or not
      #     current_user        # Current signed in user
      #     current_admin       # Current signed in admin
      #     user_session        # Session data available only to the user scope
      #     admin_session       # Session data available only to the admin scope
      #
      #   Use:
      #     before_action :authenticate_user!  # Tell devise to use :user map
      #     before_action :authenticate_admin! # Tell devise to use :admin map
      #
      def self.define_helpers(mapping) #:nodoc:
        mapping = mapping.name

        class_eval <<-METHODS, __FILE__, __LINE__ + 1
          def authenticate_#{mapping}!(opts = {})
            opts[:scope] = :#{mapping}
            warden.authenticate!(opts) if !devise_controller? || opts.delete(:force)
          end

          def #{mapping}_signed_in?
            !!current_#{mapping}
          end

          def current_#{mapping}
            @current_#{mapping} ||= warden.authenticate(scope: :#{mapping})
          end

          def #{mapping}_session
            current_#{mapping} && warden.session(:#{mapping})
          end
        METHODS

        ActiveSupport.on_load(:action_controller) do
          if respond_to?(:helper_method)
            helper_method "current_#{mapping}", "#{mapping}_signed_in?", "#{mapping}_session"
          end
        end
      end

      # The main accessor for the warden proxy instance
      def warden
        request.env['warden'] or raise MissingWarden
      end

      # Return true if it's a devise_controller. false to all controllers unless
      # the controllers defined inside devise. Useful if you want to apply a before
      # filter to all controllers, except the ones in devise:
      #
      #   before_action :my_filter, unless: :devise_controller?
      def devise_controller?
        is_a?(::DeviseController)
      end

      # Set up a param sanitizer to filter parameters using strong_parameters. See
      # lib/devise/parameter_sanitizer.rb for more info. Override this
      # method in your application controller to use your own parameter sanitizer.
      def devise_parameter_sanitizer
        @devise_parameter_sanitizer ||= Devise::ParameterSanitizer.new(resource_class, resource_name, params)
      end

      # Tell warden that params authentication is allowed for that specific page.
      def allow_params_authentication!
        request.env["devise.allow_params_authentication"] = true
      end

      # The scope root url to be used when they're signed in. By default, it first
      # tries to find a resource_root_path, otherwise it uses the root_path.
      def signed_in_root_path(resource_or_scope)
        scope = Devise::Mapping.find_scope!(resource_or_scope)
        router_name = Devise.mappings[scope].router_name

        home_path = "#{scope}_root_path"

        context = router_name ? send(router_name) : self

        if context.respond_to?(home_path, true)
          context.send(home_path)
        elsif context.respond_to?(:root_path)
          context.root_path
        elsif respond_to?(:root_path)
          root_path
        else
          "/"
        end
      end

      # The default url to be used after signing in. This is used by all Devise
      # controllers and you can overwrite it in your ApplicationController to
      # provide a custom hook for a custom resource.
      #
      # By default, it first tries to find a valid resource_return_to key in the
      # session, then it fallbacks to resource_root_path, otherwise it uses the
      # root path. For a user scope, you can define the default url in
      # the following way:
      #
      #   get '/users' => 'users#index', as: :user_root # creates user_root_path
      #
      #   namespace :user do
      #     root 'users#index' # creates user_root_path
      #   end
      #
      # If the resource root path is not defined, root_path is used. However,
      # if this default is not enough, you can customize it, for example:
      #
      #   def after_sign_in_path_for(resource)
      #     stored_location_for(resource) ||
      #       if resource.is_a?(User) && resource.can_publish?
      #         publisher_url
      #       else
      #         super
      #       end
      #   end
      #
      def after_sign_in_path_for(resource_or_scope)
        stored_location_for(resource_or_scope) || signed_in_root_path(resource_or_scope)
      end

      # Method used by sessions controller to sign out a user. You can overwrite
      # it in your ApplicationController to provide a custom hook for a custom
      # scope. Notice that differently from +after_sign_in_path_for+ this method
      # receives a symbol with the scope, and not the resource.
      #
      # By default it is the root_path.
      def after_sign_out_path_for(resource_or_scope)
        scope = Devise::Mapping.find_scope!(resource_or_scope)
        router_name = Devise.mappings[scope].router_name
        context = router_name ? send(router_name) : self
        context.respond_to?(:root_path) ? context.root_path : "/"
      end

      # Sign in a user and tries to redirect first to the stored location and
      # then to the url specified by after_sign_in_path_for. It accepts the same
      # parameters as the sign_in method.
      def sign_in_and_redirect(resource_or_scope, *args)
        options  = args.extract_options!
        scope    = Devise::Mapping.find_scope!(resource_or_scope)
        resource = args.last || resource_or_scope
        sign_in(scope, resource, options)
        redirect_to after_sign_in_path_for(resource)
      end

      # Sign out a user and tries to redirect to the url specified by
      # after_sign_out_path_for.
      def sign_out_and_redirect(resource_or_scope)
        scope = Devise::Mapping.find_scope!(resource_or_scope)
        redirect_path = after_sign_out_path_for(scope)
        Devise.sign_out_all_scopes ? sign_out : sign_out(scope)
        redirect_to redirect_path
      end

      # Overwrite Rails' handle unverified request to sign out all scopes,
      # clear run strategies and remove cached variables.
      def handle_unverified_request
        super # call the default behavior which resets/nullifies/raises
        request.env["devise.skip_storage"] = true
        sign_out_all_scopes(false)
      end

      def request_format
        @request_format ||= request.format.try(:ref)
      end

      def is_navigational_format?
        Devise.navigational_formats.include?(request_format)
      end

      # Check if flash messages should be emitted. Default is to do it on
      # navigational formats
      def is_flashing_format?
        request.respond_to?(:flash) && is_navigational_format?
      end

      private

      def expire_data_after_sign_out!
        Devise.mappings.each { |_,m| instance_variable_set("@current_#{m.name}", nil) }
        super
      end
    end
  end

  class MissingWarden < StandardError
    def initialize
      super "Devise could not find the `Warden::Proxy` instance on your request environment.\n" + \
        "Make sure that your application is loading Devise and Warden as expected and that " + \
        "the `Warden::Manager` middleware is present in your middleware stack.\n" + \
        "If you are seeing this on one of your tests, ensure that your tests are either " + \
        "executing the Rails middleware stack or that your tests are using the `Devise::Test::ControllerHelpers` " + \
        "module to inject the `request.env['warden']` object for you."
    end
  end
end