rodauth-rails 0.5.0 → 0.6.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: f0d00b7ad2f6198fff3a5cc3c720c6f30d9296898e3fd764ddf7408e36232a6d
4
- data.tar.gz: 9ba008116fc5521c98ed62dda5b1f6b2eccd733d06ccb0a2c5b9b4db8df94539
3
+ metadata.gz: 00d7ab9dd749cbae17cddc2788005d8570d8d46f89f427ad624c7e61fe177665
4
+ data.tar.gz: d62aed32823b0be9c74d7281de80650b8faf600c01db22ad941a35f30bfeb002
5
5
  SHA512:
6
- metadata.gz: 848873e599cfb8dc8a5d274ac5fec5987cf4c895c77757a4b21529ddcd9a635ba8086c037f55c55ef097ae926549234e5abebb97a5300f0ead6118927d737d91
7
- data.tar.gz: aa75fb48217e79c40000cf1f226f36a65fdffdcb764233572ae45341b7ab9dd2f1d8f8470e593d8260495962b1b5c352e62d0d5e71fed0cb96891da93a0f344c
6
+ metadata.gz: 7ab0afe5e95fab1af706b64ef5c494252fac49481d49dad1c2ef3510c17ca96050c58bf0832a36af761673137f2fd63d7d49a692656bcf06748840de96f60875
7
+ data.tar.gz: 729bf3b5887647c23f4b11d821d3829c4b4d290c546d2f19ba6b393dd47bf0b357d128577e877ac3e0a4352972b79325d4217d62935d4a683e78ff8e910d13a2
@@ -1,3 +1,13 @@
1
+ ## 0.6.0 (2020-11-22)
2
+
3
+ * Add `Rodauth::Rails.rodauth` method for retrieving Rodauth instance outside of request context (@janko)
4
+
5
+ * Add default Action Dispatch response headers in Rodauth responses (@janko)
6
+
7
+ * Run controller rescue handlers around Rodauth actions (@janko)
8
+
9
+ * Run controller action callbacks around Rodauth actions (@janko)
10
+
1
11
  ## 0.5.0 (2020-11-16)
2
12
 
3
13
  * Support more Active Record adapters in `rodauth:install` generator (@janko)
data/README.md CHANGED
@@ -4,16 +4,22 @@ Provides Rails integration for the [Rodauth] authentication framework.
4
4
 
5
5
  ## Resources
6
6
 
7
+ Useful links:
8
+
7
9
  * [Rodauth documentation](http://rodauth.jeremyevans.net/documentation.html)
8
- * [rodauth-rails wiki](https://github.com/janko/rodauth-rails/wiki)
9
10
  * [Rails demo](https://github.com/janko/rodauth-demo-rails)
10
11
 
12
+ Articles:
13
+
14
+ * [Rodauth: A Refreshing Authentication Solution for Ruby](https://janko.io/rodauth-a-refreshing-authentication-solution-for-ruby/)
15
+ * [Adding Authentication in Rails 6 with Rodauth](https://janko.io/adding-authentication-in-rails-with-rodauth/)
16
+
11
17
  ## Installation
12
18
 
13
19
  Add the gem to your Gemfile:
14
20
 
15
21
  ```rb
16
- gem "rodauth-rails", "~> 0.4"
22
+ gem "rodauth-rails", "~> 0.6"
17
23
 
18
24
  # gem "jwt", require: false # for JWT feature
19
25
  # gem "rotp", require: false # for OTP feature
@@ -111,8 +117,9 @@ end
111
117
 
112
118
  ### Controller
113
119
 
114
- Your Rodauth app will by default use `RodauthController` for view rendering
115
- and CSRF protection.
120
+ Your Rodauth app will by default use `RodauthController` for view rendering,
121
+ CSRF protection, and running controller callbacks and rescue handlers around
122
+ Rodauth actions.
116
123
 
117
124
  ```rb
118
125
  # app/controllers/rodauth_controller.rb
@@ -131,9 +138,11 @@ class Account < ApplicationRecord
131
138
  end
132
139
  ```
133
140
 
134
- ## Getting started
141
+ ## Usage
135
142
 
136
- First, let's see what routes our Rodauth middleware will handle:
143
+ ### Routes
144
+
145
+ We can see the list of routes our Rodauth middleware handles:
137
146
 
138
147
  ```sh
139
148
  $ rails rodauth:routes
@@ -155,8 +164,8 @@ Routes handled by RodauthApp:
155
164
  /close-account rodauth.close_account_path
156
165
  ```
157
166
 
158
- We can use this information to add some basic authentication navigation links
159
- to our home page:
167
+ Using this information, we could add some basic authentication links to our
168
+ navigation header:
160
169
 
161
170
  ```erb
162
171
  <ul>
@@ -169,40 +178,45 @@ to our home page:
169
178
  </ul>
170
179
  ```
171
180
 
172
- These links are fully functional, feel free to visit them and interact with the
181
+ These routes are fully functional, feel free to visit them and interact with the
173
182
  pages. The templates that ship with Rodauth aim to provide a complete
174
183
  authentication experience, and the forms use [Bootstrap] markup.
175
184
 
176
- Let's also load the account record for authenticated requests and expose it via
177
- `#current_account`:
185
+ ### Current account
186
+
187
+ To be able to fetch currently authenticated account, let's define a
188
+ `#current_account` method that fetches the account id from session and
189
+ retrieves the corresponding account record:
178
190
 
179
191
  ```rb
180
192
  # app/controllers/application_controller.rb
181
193
  class ApplicationController < ActionController::Base
182
- before_action :load_account, if: -> { rodauth.authenticated? }
194
+ before_action :current_account, if: -> { rodauth.authenticated? }
183
195
 
184
196
  private
185
197
 
186
- def load_account
187
- @current_account = Account.find(rodauth.session_value)
198
+ def current_account
199
+ @current_account ||= Account.find(rodauth.session_value)
188
200
  rescue ActiveRecord::RecordNotFound
189
201
  rodauth.logout
190
202
  rodauth.login_required
191
203
  end
192
-
193
- attr_reader :current_account
194
204
  helper_method :current_account
195
205
  end
196
206
  ```
207
+
208
+ This allows us to access the current account in controllers and views:
209
+
197
210
  ```erb
198
211
  <p>Authenticated as: <%= current_account.email %></p>
199
212
  ```
200
213
 
201
214
  ### Requiring authentication
202
215
 
203
- Next, we'll likely want to require authentication for certain sections/pages of
204
- our app. We can do this in our Rodauth app's routing block, which helps keep
205
- the authentication logic encapsulated:
216
+ We'll likely want to require authentication for certain parts of our app,
217
+ redirecting the user to the login page if they're not logged in. We can do this
218
+ in our Rodauth app's routing block, which helps keep the authentication logic
219
+ encapsulated:
206
220
 
207
221
  ```rb
208
222
  # app/lib/rodauth_app.rb
@@ -260,9 +274,9 @@ end
260
274
 
261
275
  ### Views
262
276
 
263
- The templates built into Rodauth are useful when getting started, but at some
264
- point we'll probably want more control over the markup. For that we can run the
265
- following command:
277
+ The templates built into Rodauth are useful when getting started, but soon
278
+ you'll want to start editing the markup. You can run the following command to
279
+ copy Rodauth templates into your Rails app:
266
280
 
267
281
  ```sh
268
282
  $ rails generate rodauth:views
@@ -286,7 +300,7 @@ $ rails generate rodauth:views --all
286
300
  ```
287
301
 
288
302
  You can also tell the generator to create views into another directory (in this
289
- case make sure to rename the Rodauth controller accordingly).
303
+ case make sure to rename the Rodauth controller accordingly):
290
304
 
291
305
  ```sh
292
306
  # generates views into app/views/authentication
@@ -366,8 +380,8 @@ end
366
380
  ```
367
381
 
368
382
  You can then uncomment the lines in your Rodauth configuration to have it call
369
- your mailer. If you've enabled additional authentication features, make sure to
370
- override their `send_*_email` methods as well.
383
+ your mailer. If you've enabled additional authentication features that send
384
+ emails, make sure to override their `send_*_email` methods as well.
371
385
 
372
386
  ```rb
373
387
  # app/lib/rodauth_app.rb
@@ -408,10 +422,11 @@ end
408
422
 
409
423
  ### Migrations
410
424
 
411
- The install generator will have created some default tables, but you can use
412
- the migration generator to create tables for any additional Rodauth features:
425
+ The install generator will create a migration for tables used by the Rodauth
426
+ features enabled by default. For any additional features, you can use the
427
+ migration generator to create the corresponding tables:
413
428
 
414
- ```
429
+ ```sh
415
430
  $ rails generate rodauth:migration otp sms_codes recovery_codes
416
431
  ```
417
432
  ```rb
@@ -456,6 +471,25 @@ the configure method.
456
471
  Make sure to store the `jwt_secret` in a secure place, such as Rails
457
472
  credentials or environment variables.
458
473
 
474
+ ### Rodauth instance
475
+
476
+ In some cases you might need to use Rodauth more programmatically, and perform
477
+ Rodauth operations outside of the request context. rodauth-rails gives you the
478
+ ability to retrieve the Rodauth instance:
479
+
480
+ ```rb
481
+ rodauth = Rodauth::Rails.rodauth # or Rodauth::Rails.rodauth(:secondary)
482
+
483
+ rodauth.login_url #=> "https://example.com/login"
484
+ rodauth.account_from_login("user@example.com") # loads user by email
485
+ rodauth.password_match?("secret") #=> true
486
+ rodauth.setup_account_verification
487
+ rodauth.close_account
488
+ ```
489
+
490
+ This Rodauth instance will be initialized with basic Rack env that allows is it
491
+ to generate URLs, using `config.action_mailer.default_url_options` options.
492
+
459
493
  ## How it works
460
494
 
461
495
  ### Middleware
@@ -500,11 +534,12 @@ end
500
534
  The `Rodauth::Rails::App` class is a [Roda] subclass that provides Rails
501
535
  integration for Rodauth:
502
536
 
503
- * uses Rails' flash instead of Roda's
504
- * uses Rails' CSRF protection instead of Roda's
537
+ * uses Action Dispatch flash instead of Roda's
538
+ * uses Action Dispatch CSRF protection instead of Roda's
505
539
  * sets [HMAC] secret to Rails' secret key base
506
- * uses ActionController for rendering templates
507
- * uses ActionMailer for sending emails
540
+ * uses Action Controller for rendering templates
541
+ * runs Action Controller callbacks & rescue handlers around Rodauth actions
542
+ * uses Action Mailer for sending emails
508
543
 
509
544
  The `configure { ... }` method wraps configuring the Rodauth plugin, forwarding
510
545
  any additional [plugin options].
@@ -635,15 +670,11 @@ Rodauth method for creating database functions:
635
670
  # db/migrate/*_create_rodauth_database_functions.rb
636
671
  class CreateRodauthDatabaseFunctions < ActiveRecord::Migration
637
672
  def up
638
- # ...
639
673
  Rodauth.create_database_authentication_functions(DB)
640
- # ...
641
674
  end
642
675
 
643
676
  def down
644
- # ...
645
677
  Rodauth.drop_database_authentication_functions(DB)
646
- # ...
647
678
  end
648
679
  end
649
680
  ```
@@ -700,7 +731,6 @@ conduct](https://github.com/janko/rodauth-rails/blob/master/CODE_OF_CONDUCT.md).
700
731
 
701
732
  [Rodauth]: https://github.com/jeremyevans/rodauth
702
733
  [Sequel]: https://github.com/jeremyevans/sequel
703
- [rendering views outside of controllers]: https://blog.bigbinary.com/2016/01/08/rendering-views-outside-of-controllers-in-rails-5.html
704
734
  [feature documentation]: http://rodauth.jeremyevans.net/documentation.html
705
735
  [JWT feature]: http://rodauth.jeremyevans.net/rdoc/files/doc/jwt_rdoc.html
706
736
  [JWT gem]: https://github.com/jwt/ruby-jwt
@@ -9,14 +9,33 @@ module Rodauth
9
9
  # This allows the developer to avoid loading Rodauth at boot time.
10
10
  autoload :App, "rodauth/rails/app"
11
11
 
12
- def self.configure
13
- yield self
14
- end
15
-
16
12
  @app = nil
17
13
  @middleware = true
18
14
 
19
15
  class << self
16
+ def rodauth(name = nil)
17
+ url_options = ActionMailer::Base.default_url_options
18
+
19
+ scheme = url_options[:protocol] || "http"
20
+ port = url_options[:port]
21
+ port ||= Rack::Request::DEFAULT_PORTS[scheme] if Gem::Version.new(Rack.release) < Gem::Version.new("2.0")
22
+ host = url_options[:host]
23
+ host += ":#{port}" if port
24
+
25
+ rack_env = {
26
+ "HTTP_HOST" => host,
27
+ "rack.url_scheme" => scheme,
28
+ }
29
+
30
+ scope = app.new(rack_env)
31
+
32
+ scope.rodauth(name)
33
+ end
34
+
35
+ def configure
36
+ yield self
37
+ end
38
+
20
39
  attr_writer :app
21
40
  attr_writer :middleware
22
41
 
@@ -10,7 +10,7 @@ module Rodauth
10
10
 
11
11
  def self.configure(name = nil, **options, &block)
12
12
  unless options[:json] == :only
13
- require "rodauth/rails/app/flash"
13
+ require "rodauth/rails/flash"
14
14
  plugin Flash
15
15
  end
16
16
 
@@ -9,10 +9,11 @@ module Rodauth
9
9
  :rails_csrf_param,
10
10
  :rails_csrf_token,
11
11
  :rails_check_csrf!,
12
- :rails_controller_instance,
13
12
  :rails_controller,
14
13
  )
15
14
 
15
+ auth_cached_method :rails_controller_instance
16
+
16
17
  # Renders templates with layout. First tries to render a user-defined
17
18
  # template, otherwise falls back to Rodauth's template.
18
19
  def view(page, *)
@@ -28,6 +29,11 @@ module Rodauth
28
29
  super
29
30
  end
30
31
 
32
+ # Render Rails CSRF tags in Rodauth templates.
33
+ def csrf_tag(*)
34
+ rails_csrf_tag
35
+ end
36
+
31
37
  # Verify Rails' authenticity token.
32
38
  def check_csrf
33
39
  rails_check_csrf!
@@ -38,11 +44,6 @@ module Rodauth
38
44
  true
39
45
  end
40
46
 
41
- # Render Rails CSRF tags in Rodauth templates.
42
- def csrf_tag(*)
43
- rails_csrf_tag
44
- end
45
-
46
47
  # Default the flash error key to Rails' default :alert.
47
48
  def flash_error_key
48
49
  :alert
@@ -50,6 +51,59 @@ module Rodauth
50
51
 
51
52
  private
52
53
 
54
+ # Runs controller callbacks and rescue handlers around Rodauth actions.
55
+ def _around_rodauth(&block)
56
+ result = nil
57
+
58
+ rails_controller_rescue do
59
+ rails_controller_callbacks do
60
+ result = catch(:halt) { super(&block) }
61
+ end
62
+ end
63
+
64
+ if rails_controller_instance.performed?
65
+ rails_controller_response
66
+ else
67
+ result[1].merge!(rails_controller_instance.response.headers)
68
+ throw :halt, result
69
+ end
70
+ end
71
+
72
+ # Runs any #(before|around|after)_action controller callbacks.
73
+ def rails_controller_callbacks
74
+ # don't verify CSRF token as part of callbacks, Rodauth will do that
75
+ rails_controller_instance.allow_forgery_protection = false
76
+
77
+ rails_controller_instance.run_callbacks(:process_action) do
78
+ # turn the setting back to default so that form tags generate CSRF tags
79
+ rails_controller_instance.allow_forgery_protection = rails_controller.allow_forgery_protection
80
+
81
+ yield
82
+ end
83
+ end
84
+
85
+ # Runs any registered #rescue_from controller handlers.
86
+ def rails_controller_rescue
87
+ yield
88
+ rescue Exception => exception
89
+ rails_controller_instance.rescue_with_handler(exception) || raise
90
+
91
+ unless rails_controller_instance.performed?
92
+ raise Rodauth::Rails::Error, "rescue_from handler didn't write any response"
93
+ end
94
+ end
95
+
96
+ # Returns Roda response from controller response if set.
97
+ def rails_controller_response
98
+ controller_response = rails_controller_instance.response
99
+
100
+ response.status = controller_response.status
101
+ response.headers.merge! controller_response.headers
102
+ response.write controller_response.body
103
+
104
+ request.halt
105
+ end
106
+
53
107
  # Create emails with ActionMailer which uses configured delivery method.
54
108
  def create_email_to(to, subject, body)
55
109
  Mailer.create_email(to: to, from: email_from, subject: "#{email_subject_prefix}#{subject}", body: body)
@@ -64,11 +118,14 @@ module Rodauth
64
118
  def rails_render(*args)
65
119
  return if only_json?
66
120
 
67
- begin
68
- rails_controller_instance.render_to_string(*args)
69
- rescue ActionView::MissingTemplate
70
- nil
71
- end
121
+ rails_controller_instance.render_to_string(*args)
122
+ rescue ActionView::MissingTemplate
123
+ nil
124
+ end
125
+
126
+ # Calls the controller to verify the authenticity token.
127
+ def rails_check_csrf!
128
+ rails_controller_instance.send(:verify_authenticity_token)
72
129
  end
73
130
 
74
131
  # Hidden tag with Rails CSRF token inserted into Rodauth templates.
@@ -86,13 +143,8 @@ module Rodauth
86
143
  rails_controller_instance.send(:form_authenticity_token)
87
144
  end
88
145
 
89
- # Calls the controller to verify the authenticity token.
90
- def rails_check_csrf!
91
- rails_controller_instance.send(:verify_authenticity_token)
92
- end
93
-
94
146
  # Instances of the configured controller with current request's env hash.
95
- def rails_controller_instance
147
+ def _rails_controller_instance
96
148
  request = ActionDispatch::Request.new(scope.env)
97
149
  instance = rails_controller.new
98
150
 
@@ -0,0 +1,48 @@
1
+ module Rodauth
2
+ module Rails
3
+ # Roda plugin that sets up Rails flash integration.
4
+ module Flash
5
+ def self.load_dependencies(app)
6
+ app.plugin :hooks
7
+ end
8
+
9
+ def self.configure(app)
10
+ app.before { request.flash } # load flash
11
+ app.after { request.commit_flash } # save flash
12
+ end
13
+
14
+ module InstanceMethods
15
+ def flash
16
+ request.flash
17
+ end
18
+ end
19
+
20
+ module RequestMethods
21
+ # If the redirect would bubble up outside of the Roda app, the after
22
+ # hook would never get called, so we make sure to commit the flash.
23
+ def redirect(*)
24
+ commit_flash
25
+ super
26
+ end
27
+
28
+ def flash
29
+ rails_request.flash
30
+ end
31
+
32
+ def commit_flash
33
+ if ActionPack.version >= Gem::Version.new("5.0")
34
+ rails_request.commit_flash
35
+ else
36
+ # ActionPack 4.2 automatically commits flash
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ def rails_request
43
+ ActionDispatch::Request.new(env)
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -3,19 +3,16 @@ namespace :rodauth do
3
3
  app = Rodauth::Rails.app
4
4
 
5
5
  puts "Routes handled by #{app}:"
6
- puts
7
6
 
8
- app.opts[:rodauths].each do |rodauth_name, rodauth_class|
9
- route_names = rodauth_class.routes
10
- .map { |handle_method| handle_method.to_s.sub(/\Ahandle_/, "") }
11
- .uniq
7
+ app.opts[:rodauths].each_key do |rodauth_name|
8
+ rodauth = Rodauth::Rails.rodauth(rodauth_name)
12
9
 
13
- rodauth = rodauth_class.allocate
10
+ routes = rodauth.class.routes.map do |handle_method|
11
+ path_method = "#{handle_method.to_s.sub(/\Ahandle_/, "")}_path"
14
12
 
15
- routes = route_names.map do |name|
16
13
  [
17
- rodauth.public_send(:"#{name}_path"),
18
- "rodauth#{rodauth_name && "(:#{rodauth_name})"}.#{name}_path",
14
+ rodauth.public_send(path_method),
15
+ "rodauth#{rodauth_name && "(:#{rodauth_name})"}.#{path_method}",
19
16
  ]
20
17
  end
21
18
 
@@ -25,8 +22,7 @@ namespace :rodauth do
25
22
  "#{path.ljust(padding)} #{code}"
26
23
  end
27
24
 
28
- puts " #{route_lines.join("\n ")}"
29
- puts
25
+ puts "\n #{route_lines.join("\n ")}"
30
26
  end
31
27
  end
32
28
  end
@@ -1,5 +1,5 @@
1
1
  module Rodauth
2
2
  module Rails
3
- VERSION = "0.5.0"
3
+ VERSION = "0.6.0"
4
4
  end
5
5
  end
@@ -17,7 +17,7 @@ Gem::Specification.new do |spec|
17
17
  spec.require_paths = ["lib"]
18
18
 
19
19
  spec.add_dependency "railties", ">= 4.2", "< 7"
20
- spec.add_dependency "rodauth", "~> 2.1"
20
+ spec.add_dependency "rodauth", "~> 2.6"
21
21
  spec.add_dependency "sequel-activerecord_connection", "~> 1.1"
22
22
  spec.add_dependency "tilt"
23
23
  spec.add_dependency "bcrypt"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rodauth-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Janko Marohnić
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-11-16 00:00:00.000000000 Z
11
+ date: 2020-11-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: railties
@@ -36,14 +36,14 @@ dependencies:
36
36
  requirements:
37
37
  - - "~>"
38
38
  - !ruby/object:Gem::Version
39
- version: '2.1'
39
+ version: '2.6'
40
40
  type: :runtime
41
41
  prerelease: false
42
42
  version_requirements: !ruby/object:Gem::Requirement
43
43
  requirements:
44
44
  - - "~>"
45
45
  - !ruby/object:Gem::Version
46
- version: '2.1'
46
+ version: '2.6'
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: sequel-activerecord_connection
49
49
  requirement: !ruby/object:Gem::Requirement
@@ -190,9 +190,9 @@ files:
190
190
  - lib/rodauth/features/rails.rb
191
191
  - lib/rodauth/rails.rb
192
192
  - lib/rodauth/rails/app.rb
193
- - lib/rodauth/rails/app/flash.rb
194
193
  - lib/rodauth/rails/controller_methods.rb
195
194
  - lib/rodauth/rails/feature.rb
195
+ - lib/rodauth/rails/flash.rb
196
196
  - lib/rodauth/rails/middleware.rb
197
197
  - lib/rodauth/rails/railtie.rb
198
198
  - lib/rodauth/rails/tasks.rake
@@ -1,50 +0,0 @@
1
- module Rodauth
2
- module Rails
3
- class App
4
- # Sets up Rails' flash integration.
5
- module Flash
6
- def self.load_dependencies(app)
7
- app.plugin :hooks
8
- end
9
-
10
- def self.configure(app)
11
- app.before { request.flash } # load flash
12
- app.after { request.commit_flash } # save flash
13
- end
14
-
15
- module InstanceMethods
16
- def flash
17
- request.flash
18
- end
19
- end
20
-
21
- module RequestMethods
22
- # If the redirect would bubble up outside of the Roda app, the after
23
- # hook would never get called, so we make sure to commit the flash.
24
- def redirect(*)
25
- commit_flash
26
- super
27
- end
28
-
29
- def flash
30
- rails_request.flash
31
- end
32
-
33
- def commit_flash
34
- if ActionPack.version >= Gem::Version.new("5.0")
35
- rails_request.commit_flash
36
- else
37
- # ActionPack 4.2 automatically commits flash
38
- end
39
- end
40
-
41
- private
42
-
43
- def rails_request
44
- ActionDispatch::Request.new(env)
45
- end
46
- end
47
- end
48
- end
49
- end
50
- end