multi_session 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +110 -0
- data/Rakefile +12 -0
- data/lib/generators/multi_session/install_generator.rb +10 -0
- data/lib/generators/templates/multi_session.rb +7 -0
- data/lib/multi_session/helper.rb +21 -0
- data/lib/multi_session/railtie.rb +7 -0
- data/lib/multi_session/session.rb +59 -0
- data/lib/multi_session/version.rb +3 -0
- data/lib/multi_session.rb +15 -0
- data/spec/dummy/Rakefile +6 -0
- data/spec/dummy/app/assets/config/manifest.js +2 -0
- data/spec/dummy/app/assets/javascripts/application.js +14 -0
- data/spec/dummy/app/assets/stylesheets/application.css +15 -0
- data/spec/dummy/app/controllers/application_controller.rb +2 -0
- data/spec/dummy/app/controllers/multi_session_test_controller.rb +18 -0
- data/spec/dummy/app/helpers/application_helper.rb +2 -0
- data/spec/dummy/app/jobs/application_job.rb +2 -0
- data/spec/dummy/app/views/layouts/application.html.erb +14 -0
- data/spec/dummy/app/views/multi_session_test/some_action.html.erb +1 -0
- data/spec/dummy/bin/bundle +3 -0
- data/spec/dummy/bin/rails +4 -0
- data/spec/dummy/bin/rake +4 -0
- data/spec/dummy/bin/setup +25 -0
- data/spec/dummy/bin/update +25 -0
- data/spec/dummy/config/application.rb +29 -0
- data/spec/dummy/config/boot.rb +5 -0
- data/spec/dummy/config/credentials.yml.enc +1 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +40 -0
- data/spec/dummy/config/environments/production.rb +68 -0
- data/spec/dummy/config/environments/test.rb +36 -0
- data/spec/dummy/config/initializers/application_controller_renderer.rb +8 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/content_security_policy.rb +25 -0
- data/spec/dummy/config/initializers/cookies_serializer.rb +5 -0
- data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/spec/dummy/config/initializers/inflections.rb +16 -0
- data/spec/dummy/config/initializers/mime_types.rb +4 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +9 -0
- data/spec/dummy/config/locales/en.yml +33 -0
- data/spec/dummy/config/master.key +1 -0
- data/spec/dummy/config/puma.rb +34 -0
- data/spec/dummy/config/routes.rb +5 -0
- data/spec/dummy/config/spring.rb +6 -0
- data/spec/dummy/config.ru +5 -0
- data/spec/dummy/log/development.log +0 -0
- data/spec/dummy/log/test.log +396 -0
- data/spec/dummy/public/404.html +67 -0
- data/spec/dummy/public/422.html +67 -0
- data/spec/dummy/public/500.html +66 -0
- data/spec/dummy/public/apple-touch-icon-precomposed.png +0 -0
- data/spec/dummy/public/apple-touch-icon.png +0 -0
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/requests/multi_session_spec.rb +40 -0
- data/spec/spec_helper.rb +19 -0
- metadata +188 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: e401b25f019731faaad3230a51406a7853c071837f9a134c7779bf2f1130841f
|
4
|
+
data.tar.gz: ef26f4a723075fcdb0b19406a787a8bd6fabd84e8fb4fac417c96a1be3c3d281
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 19f01b7549b920e6d25e6a072fabd846c80d6cd8f4ed60bb48ce75e88a58363abfc72ee9a8d42ba40aa62b02f01572294b67aac68f0eccb0711121d4711d9ad6
|
7
|
+
data.tar.gz: b2f5cd824bbc7e84310f352c08ce36ecb8450711ec5605846dbccc31e4b5c450d4bac6df72873afbc367d3284c7f0282f9004c9a3fb3b008d3d0d280b40f0c21
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2018 Sean Huber
|
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,110 @@
|
|
1
|
+
[![Gem Version](https://badge.fury.io/rb/multi_session.svg)](https://badge.fury.io/rb/multi_session)
|
2
|
+
[![Build Status](https://travis-ci.org/seanhuber/multi_session.svg?branch=master)](https://travis-ci.org/seanhuber/multi_session)
|
3
|
+
[![Coverage Status](https://coveralls.io/repos/github/seanhuber/multi_session/badge.svg?branch=master)](https://coveralls.io/github/seanhuber/multi_session?branch=master)
|
4
|
+
|
5
|
+
multi_session
|
6
|
+
==============
|
7
|
+
|
8
|
+
`multi_session` is a Railtie that extends rails to provide the ability to have multiple sessions per user via encrypted cookies.
|
9
|
+
|
10
|
+
## Motivation
|
11
|
+
|
12
|
+
Rails comes with a `session` helper method for storing user session information across HTTP requests. The default approach is to store that information in an encrypted cookie that gets passed as a header in each HTTP request/response. That cookie is encrypted/decrypted using the `secret_key_base` defined in `config/secrets.yml` (or `config/credentials.yml.enc` as of Rails version 5.2).
|
13
|
+
|
14
|
+
Rails' `session` is secure and works great but what if you'd like to share that session cookie with multiple Rails applications hosted across different subdomains? Various blogs and stackoverflow questions on the matter suggest sharing the same `secret_key_base` value amongst the multiple Rails applications. But what if these apps only want to share part of the session information? Or what if there are security concerns because `secret_key_base` is used for more than just encrypting session cookies? This `multi_session` railtie provides a helper method (named `multi_session`) similar to `session` except that it permits you to create multiple encrypted session cookies per user, each with their own secret key, giving you the flexibility to choose which session components could be shared with other Rails (and non-Rails) web applications.
|
15
|
+
|
16
|
+
## Usage
|
17
|
+
|
18
|
+
`multi_session` is a helper method that gets added to your `ApplicationController` and is therefore accessible by all your controllers that subclass `ApplicationController`, as well all view templates. To create and read new sessions, use bracket notation (`[]` and `[]=`) like you would with `Hash` or hash-like objects.
|
19
|
+
|
20
|
+
For example:
|
21
|
+
|
22
|
+
```ruby
|
23
|
+
# app/controllers/some_controller.rb
|
24
|
+
|
25
|
+
class SomeController < ApplicationController
|
26
|
+
def my_action
|
27
|
+
@user = User.find(params.require(:id))
|
28
|
+
multi_session[:global_user_session] = {
|
29
|
+
'user_id' => @user.id,
|
30
|
+
'email' => @user.email,
|
31
|
+
'name' => @user.full_name
|
32
|
+
}
|
33
|
+
multi_session[:user_preferences] = {
|
34
|
+
'enable_push_notifications' => true,
|
35
|
+
'something_else' => false
|
36
|
+
}
|
37
|
+
end
|
38
|
+
|
39
|
+
def another_action
|
40
|
+
@user = User.find(multi_session[:global_user_session]['user_id'])
|
41
|
+
end
|
42
|
+
end
|
43
|
+
```
|
44
|
+
|
45
|
+
In the example above, multi_session would create 2 encrypted cookies, one named `"global_user_session"` and the other named `"user_preferences"`. The key_base used to encrypt/decrypt these cookies would need to be defined in `config/secrets.yml` or `config/credentials.yml.enc` under a key named `:multi_session_keys`. Example:
|
46
|
+
|
47
|
+
```yaml
|
48
|
+
# config/credentials.yml.enc
|
49
|
+
secret_key_base: # generated by Rails
|
50
|
+
|
51
|
+
multi_session_keys: # use `rake secret` to generate custom keys
|
52
|
+
global_user_session: # insert a new secret here
|
53
|
+
user_preferences: # insert a different secret here
|
54
|
+
```
|
55
|
+
|
56
|
+
## Installation
|
57
|
+
|
58
|
+
Add this line to your application's Gemfile:
|
59
|
+
|
60
|
+
```ruby
|
61
|
+
gem 'multi_session', '~> 1.1'
|
62
|
+
```
|
63
|
+
|
64
|
+
## Configuration
|
65
|
+
|
66
|
+
For the current version `multi_session`, there these are the configuration values that can optionally be set:
|
67
|
+
|
68
|
+
| Config option | Type | Description |
|
69
|
+
|---------------------------------------|-------------------------|-------------------------------------------------------|
|
70
|
+
| `expires` | ActiveSupport::Duration | expiration period for `multi_session` cookies/values |
|
71
|
+
| `authenticated_encrypted_cookie_salt` | String | Salt used to derive key for GCM encryption |
|
72
|
+
|
73
|
+
|
74
|
+
To configure `multi_session`, first generate an initializer using the built-in rails generator:
|
75
|
+
|
76
|
+
```
|
77
|
+
rails g multi_session:install
|
78
|
+
```
|
79
|
+
|
80
|
+
Then open and edit `config/initializers/multi_session.rb`:
|
81
|
+
|
82
|
+
```ruby
|
83
|
+
# config/initializers/multi_session.rb
|
84
|
+
|
85
|
+
MultiSession.setup do |config|
|
86
|
+
# Uncomment to force multi_session cookies to expire after a period of time
|
87
|
+
config.expires = 30.minutes
|
88
|
+
|
89
|
+
# Salt used to derive key for GCM encryption. Default value is 'multi session authenticated encrypted cookie'
|
90
|
+
config.authenticated_encrypted_cookie_salt = 'my multi session salt value'
|
91
|
+
end
|
92
|
+
```
|
93
|
+
|
94
|
+
## Security
|
95
|
+
|
96
|
+
`multi_session` does not introduce any novel security mechanisms. Encryptions/decryption is done using `ActiveSupport::MessageEncryptor` in the same manner that Rails encrypts the `session` cookie in Rails 5.2 (see https://github.com/rails/rails/blob/5fb4703471ffb11dab9aa3855daeef9f592f6388/actionpack/lib/action_dispatch/middleware/cookies.rb).
|
97
|
+
|
98
|
+
The default cipher for encrypting messages in Rails 5.2 is `AES-256-GCM`, which is the same as what `multi_session` uses.
|
99
|
+
|
100
|
+
There are many good writeups on the web (such as https://guides.rubyonrails.org/security.html, https://www.justinweiss.com/articles/how-rails-sessions-work/, and https://www.theodinproject.com/courses/ruby-on-rails/lessons/sessions-cookies-and-authentication) that go into detail of how Rails addresses session security concerns and I encourage all app developers to spend some time educating themselves on how Rails sessions work.
|
101
|
+
|
102
|
+
The implementation of `multi_session` is actually quite simple because it leans heavily on existing Rails functionality. The nuts and bolts are primarily coded in `lib/multi_session/session.rb` (https://github.com/seanhuber/multi_session/blob/b0211d714d995dc01eb817e52f5dc78e52120bf0/lib/multi_session/session.rb). It's a small file, please do your own code-audit before using this gem. :wink:
|
103
|
+
|
104
|
+
## Contributing
|
105
|
+
|
106
|
+
Pull requests and issue reports are welcome!
|
107
|
+
|
108
|
+
## License
|
109
|
+
|
110
|
+
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,12 @@
|
|
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 'bundler/gem_tasks'
|
8
|
+
require 'rspec/core/rake_task'
|
9
|
+
|
10
|
+
RSpec::Core::RakeTask.new(:spec)
|
11
|
+
|
12
|
+
task default: :spec
|
@@ -0,0 +1,10 @@
|
|
1
|
+
module MultiSession
|
2
|
+
class InstallGenerator < Rails::Generators::Base
|
3
|
+
source_root File.expand_path('../../templates', __FILE__)
|
4
|
+
|
5
|
+
desc 'creates a config/initializers/multi_session.rb file for configuring multi_session'
|
6
|
+
def install
|
7
|
+
template 'multi_session.rb', 'config/initializers/multi_session.rb'
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,7 @@
|
|
1
|
+
MultiSession.setup do |config|
|
2
|
+
# Uncomment to force multi_session cookies to expire after a period of time
|
3
|
+
# config.expires = 30.minutes
|
4
|
+
|
5
|
+
# Salt used to derive key for GCM encryption. Default value is 'multi session authenticated encrypted cookie'
|
6
|
+
# config.authenticated_encrypted_cookie_salt = 'multi session authenticated encrypted cookie'
|
7
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module MultiSession
|
2
|
+
module Helper
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
helper_method :multi_session if defined?(helper_method)
|
7
|
+
after_action :slide_multi_session
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def multi_session
|
13
|
+
Session.new cookies
|
14
|
+
end
|
15
|
+
|
16
|
+
def slide_multi_session
|
17
|
+
return unless MultiSession.expires.present?
|
18
|
+
Session.new(cookies).update_expiration
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module MultiSession
|
2
|
+
class Session
|
3
|
+
def initialize cookies
|
4
|
+
@cookies = cookies
|
5
|
+
end
|
6
|
+
|
7
|
+
def [](key)
|
8
|
+
return nil unless @cookies[key.to_s].present?
|
9
|
+
session = ActiveSupport::JSON.decode encryptor(key).decrypt_and_verify(@cookies[key])
|
10
|
+
session['value'] # TODO: add ability to let developer retrieve the session_id
|
11
|
+
end
|
12
|
+
|
13
|
+
def []=(key, value)
|
14
|
+
previous_session = self[key]
|
15
|
+
session_id = if previous_session && previous_session['session_id'].present?
|
16
|
+
previous_session['session_id']
|
17
|
+
else
|
18
|
+
SecureRandom.hex(16).encode Encoding::UTF_8
|
19
|
+
end
|
20
|
+
|
21
|
+
new_session = {
|
22
|
+
'session_id' => session_id,
|
23
|
+
'value' => value
|
24
|
+
}
|
25
|
+
expiry_options = MultiSession.expires.present? ? {expires_at: Time.now + MultiSession.expires} : {}
|
26
|
+
encrypted_and_signed_value = encryptor(key).encrypt_and_sign ActiveSupport::JSON.encode(new_session), expiry_options
|
27
|
+
|
28
|
+
raise ActionDispatch::Cookies::CookieOverflow if encrypted_and_signed_value.bytesize > ActionDispatch::Cookies::MAX_COOKIE_SIZE
|
29
|
+
|
30
|
+
@cookies[key.to_s] = {value: encrypted_and_signed_value}.merge(MultiSession.expires.present? ? {expires: MultiSession.expires} : {})
|
31
|
+
nil
|
32
|
+
end
|
33
|
+
|
34
|
+
def clear
|
35
|
+
@cookies.clear
|
36
|
+
end
|
37
|
+
|
38
|
+
def update_expiration
|
39
|
+
Rails.application.credentials[:multi_session_keys].each_key do |key|
|
40
|
+
self[key] = self[key] # decrypt and re-encrypt to force expires_at to update
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def encryptor key
|
47
|
+
secret_key_base = Rails.application.credentials[:multi_session_keys][key.to_sym]
|
48
|
+
raise ArgumentError.new("Rails.application.credentials[:multi_session_keys][:'#{key}'] has not been set.") unless secret_key_base.present?
|
49
|
+
|
50
|
+
encrypted_cookie_cipher = 'aes-256-gcm'
|
51
|
+
key_generator = ActiveSupport::CachingKeyGenerator.new ActiveSupport::KeyGenerator.new(secret_key_base, iterations: 1000)
|
52
|
+
key_len = ActiveSupport::MessageEncryptor.key_len encrypted_cookie_cipher
|
53
|
+
salt = 'authenticated encrypted cookie'
|
54
|
+
secret = key_generator.generate_key(MultiSession.authenticated_encrypted_cookie_salt, key_len)
|
55
|
+
|
56
|
+
ActiveSupport::MessageEncryptor.new secret, cipher: encrypted_cookie_cipher, serializer: ActiveSupport::MessageEncryptor::NullSerializer
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'multi_session/helper'
|
2
|
+
require 'multi_session/railtie'
|
3
|
+
require 'multi_session/session'
|
4
|
+
|
5
|
+
module MultiSession
|
6
|
+
mattr_accessor :authenticated_encrypted_cookie_salt
|
7
|
+
@@authenticated_encrypted_cookie_salt = 'multi session authenticated encrypted cookie'
|
8
|
+
|
9
|
+
mattr_accessor :expires
|
10
|
+
@@expires = nil
|
11
|
+
|
12
|
+
def self.setup
|
13
|
+
yield self
|
14
|
+
end
|
15
|
+
end
|
data/spec/dummy/Rakefile
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
// This is a manifest file that'll be compiled into application.js, which will include all the files
|
2
|
+
// listed below.
|
3
|
+
//
|
4
|
+
// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
|
5
|
+
// or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path.
|
6
|
+
//
|
7
|
+
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
|
8
|
+
// compiled file. JavaScript code in this file should be added after the last require_* statement.
|
9
|
+
//
|
10
|
+
// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
|
11
|
+
// about supported directives.
|
12
|
+
//
|
13
|
+
//= require rails-ujs
|
14
|
+
//= require_tree .
|
@@ -0,0 +1,15 @@
|
|
1
|
+
/*
|
2
|
+
* This is a manifest file that'll be compiled into application.css, which will include all the files
|
3
|
+
* listed below.
|
4
|
+
*
|
5
|
+
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
|
6
|
+
* or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
|
7
|
+
*
|
8
|
+
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
|
9
|
+
* compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
|
10
|
+
* files in this directory. Styles in this file should be added after the last require_* statement.
|
11
|
+
* It is generally better to create a new file per style scope.
|
12
|
+
*
|
13
|
+
*= require_tree .
|
14
|
+
*= require_self
|
15
|
+
*/
|
@@ -0,0 +1,18 @@
|
|
1
|
+
class MultiSessionTestController < ApplicationController
|
2
|
+
def some_action
|
3
|
+
end
|
4
|
+
|
5
|
+
def encrypt_multi_sessions
|
6
|
+
params.require(:session_values).permit!.each do |session_key, value|
|
7
|
+
multi_session[session_key] = value
|
8
|
+
end
|
9
|
+
head :ok
|
10
|
+
end
|
11
|
+
|
12
|
+
def decrypt_multi_sessions
|
13
|
+
session_values = params.require(:session_keys).map do |key|
|
14
|
+
"#{key}-#{multi_session[key]}"
|
15
|
+
end.join(',')
|
16
|
+
render inline: session_values
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
<h1>hello world!</h1>
|
data/spec/dummy/bin/rake
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'fileutils'
|
3
|
+
include FileUtils
|
4
|
+
|
5
|
+
# path to your application root.
|
6
|
+
APP_ROOT = File.expand_path('..', __dir__)
|
7
|
+
|
8
|
+
def system!(*args)
|
9
|
+
system(*args) || abort("\n== Command #{args} failed ==")
|
10
|
+
end
|
11
|
+
|
12
|
+
chdir APP_ROOT do
|
13
|
+
# This script is a starting point to setup your application.
|
14
|
+
# Add necessary setup steps to this file.
|
15
|
+
|
16
|
+
puts '== Installing dependencies =='
|
17
|
+
system! 'gem install bundler --conservative'
|
18
|
+
system('bundle check') || system!('bundle install')
|
19
|
+
|
20
|
+
puts "\n== Removing old logs and tempfiles =="
|
21
|
+
system! 'bin/rails log:clear tmp:clear'
|
22
|
+
|
23
|
+
puts "\n== Restarting application server =="
|
24
|
+
system! 'bin/rails restart'
|
25
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'fileutils'
|
3
|
+
include FileUtils
|
4
|
+
|
5
|
+
# path to your application root.
|
6
|
+
APP_ROOT = File.expand_path('..', __dir__)
|
7
|
+
|
8
|
+
def system!(*args)
|
9
|
+
system(*args) || abort("\n== Command #{args} failed ==")
|
10
|
+
end
|
11
|
+
|
12
|
+
chdir APP_ROOT do
|
13
|
+
# This script is a way to update your development environment automatically.
|
14
|
+
# Add necessary update steps to this file.
|
15
|
+
|
16
|
+
puts '== Installing dependencies =='
|
17
|
+
system! 'gem install bundler --conservative'
|
18
|
+
system('bundle check') || system!('bundle install')
|
19
|
+
|
20
|
+
puts "\n== Removing old logs and tempfiles =="
|
21
|
+
system! 'bin/rails log:clear tmp:clear'
|
22
|
+
|
23
|
+
puts "\n== Restarting application server =="
|
24
|
+
system! 'bin/rails restart'
|
25
|
+
end
|