google_sign_in 0.1.4 → 1.0.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.
Files changed (88) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +12 -0
  3. data/.travis.yml +18 -0
  4. data/Gemfile.lock +130 -10
  5. data/README.md +114 -47
  6. data/Rakefile +31 -1
  7. data/SECURITY.md +15 -0
  8. data/app/controllers/google_sign_in/authorizations_controller.rb +17 -0
  9. data/app/controllers/google_sign_in/base_controller.rb +15 -0
  10. data/app/controllers/google_sign_in/callbacks_controller.rb +27 -0
  11. data/app/helpers/google_sign_in/button_helper.rb +7 -0
  12. data/bin/rails +16 -0
  13. data/config/routes.rb +4 -0
  14. data/google_sign_in.gemspec +9 -6
  15. data/lib/google_sign_in.rb +9 -1
  16. data/lib/google_sign_in/engine.rb +28 -0
  17. data/lib/google_sign_in/identity.rb +10 -21
  18. data/lib/google_sign_in/redirect_protector.rb +25 -0
  19. data/test/certificate.pem +19 -0
  20. data/test/controllers/authorizations_controller_test.rb +26 -0
  21. data/test/controllers/callbacks_controller_test.rb +36 -0
  22. data/test/dummy/.ruby-version +1 -0
  23. data/test/dummy/Rakefile +6 -0
  24. data/test/dummy/app/assets/config/manifest.js +3 -0
  25. data/test/dummy/app/assets/images/.keep +0 -0
  26. data/test/dummy/app/assets/javascripts/application.js +15 -0
  27. data/test/dummy/app/assets/javascripts/cable.js +13 -0
  28. data/test/dummy/app/assets/javascripts/channels/.keep +0 -0
  29. data/test/dummy/app/assets/stylesheets/application.css +15 -0
  30. data/test/dummy/app/channels/application_cable/channel.rb +4 -0
  31. data/test/dummy/app/channels/application_cable/connection.rb +4 -0
  32. data/test/dummy/app/controllers/application_controller.rb +2 -0
  33. data/test/dummy/app/controllers/concerns/.keep +0 -0
  34. data/test/dummy/app/helpers/application_helper.rb +2 -0
  35. data/test/dummy/app/jobs/application_job.rb +2 -0
  36. data/test/dummy/app/mailers/application_mailer.rb +4 -0
  37. data/test/dummy/app/models/application_record.rb +3 -0
  38. data/test/dummy/app/models/concerns/.keep +0 -0
  39. data/test/dummy/app/views/layouts/application.html.erb +15 -0
  40. data/test/dummy/app/views/layouts/mailer.html.erb +13 -0
  41. data/test/dummy/app/views/layouts/mailer.text.erb +1 -0
  42. data/test/dummy/bin/bundle +3 -0
  43. data/test/dummy/bin/rails +4 -0
  44. data/test/dummy/bin/rake +4 -0
  45. data/test/dummy/bin/setup +36 -0
  46. data/test/dummy/bin/update +31 -0
  47. data/test/dummy/bin/yarn +11 -0
  48. data/test/dummy/config.ru +5 -0
  49. data/test/dummy/config/application.rb +20 -0
  50. data/test/dummy/config/boot.rb +5 -0
  51. data/test/dummy/config/cable.yml +10 -0
  52. data/test/dummy/config/database.yml +25 -0
  53. data/test/dummy/config/environment.rb +5 -0
  54. data/test/dummy/config/environments/development.rb +32 -0
  55. data/test/dummy/config/environments/production.rb +57 -0
  56. data/test/dummy/config/environments/test.rb +33 -0
  57. data/test/dummy/config/initializers/application_controller_renderer.rb +8 -0
  58. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  59. data/test/dummy/config/initializers/content_security_policy.rb +25 -0
  60. data/test/dummy/config/initializers/cookies_serializer.rb +5 -0
  61. data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  62. data/test/dummy/config/initializers/google_sign_in.rb +4 -0
  63. data/test/dummy/config/initializers/inflections.rb +16 -0
  64. data/test/dummy/config/initializers/mime_types.rb +4 -0
  65. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  66. data/test/dummy/config/locales/en.yml +33 -0
  67. data/test/dummy/config/puma.rb +34 -0
  68. data/test/dummy/config/routes.rb +2 -0
  69. data/test/dummy/config/spring.rb +6 -0
  70. data/test/dummy/config/storage.yml +34 -0
  71. data/test/dummy/lib/assets/.keep +0 -0
  72. data/test/dummy/log/.keep +0 -0
  73. data/test/dummy/package.json +5 -0
  74. data/test/dummy/public/404.html +67 -0
  75. data/test/dummy/public/422.html +67 -0
  76. data/test/dummy/public/500.html +66 -0
  77. data/test/dummy/public/apple-touch-icon-precomposed.png +0 -0
  78. data/test/dummy/public/apple-touch-icon.png +0 -0
  79. data/test/dummy/public/favicon.ico +0 -0
  80. data/test/helpers/button_helper_test.rb +36 -0
  81. data/test/key.pem +27 -0
  82. data/test/models/identity_test.rb +76 -0
  83. data/test/models/redirect_protector_test.rb +34 -0
  84. data/test/test_helper.rb +27 -3
  85. metadata +200 -10
  86. data/lib/google_sign_in/helper.rb +0 -76
  87. data/lib/google_sign_in/railtie.rb +0 -12
  88. data/test/identity_test.rb +0 -13
@@ -0,0 +1,15 @@
1
+ # Google-Sign in for Rails: Security
2
+
3
+ Security is of utmost importance in an authentication library like Google Sign-In for Rails. We at Basecamp use this plugin in our own apps, so we have a vested interest in investigating and mitigating all reported vulnerabilities. We welcome responsible security reviews and reports from our peers in the open-source software community and strive to acknowledge such valuable contributions.
4
+
5
+
6
+ ## Reporting a vulnerability
7
+
8
+ Send urgent or sensitive reports to **<security@basecamp.com>**. If necessary, use our [public key] to protect your message and provide us with a secure way to respond. We’ll get back to you as soon as we can—usually within one business day. Please follow up or [ping us on Twitter][twitter] if you don’t hear back. For non-urgent or non-sensitive requests, please contact our [support team][support].
9
+
10
+ Read more about our security response policy [on our website][policy].
11
+
12
+ [public key]: https://basecamp.com/about/policies/security/Basecamp-security.pub
13
+ [twitter]: https://twitter.com/basecamp
14
+ [support]: https://basecamp.com/support
15
+ [policy]: https://basecamp.com/about/policies/security/response
@@ -0,0 +1,17 @@
1
+ require 'securerandom'
2
+
3
+ class GoogleSignIn::AuthorizationsController < GoogleSignIn::BaseController
4
+ def create
5
+ redirect_to login_url(scope: 'openid profile email', state: state),
6
+ flash: { proceed_to: params.require(:proceed_to), state: state }
7
+ end
8
+
9
+ private
10
+ def login_url(**params)
11
+ client.auth_code.authorize_url(prompt: 'login', **params)
12
+ end
13
+
14
+ def state
15
+ @state ||= SecureRandom.base64(16)
16
+ end
17
+ end
@@ -0,0 +1,15 @@
1
+ require 'oauth2'
2
+
3
+ class GoogleSignIn::BaseController < ActionController::Base
4
+ protect_from_forgery with: :exception
5
+
6
+ private
7
+ def client
8
+ @client ||= OAuth2::Client.new \
9
+ GoogleSignIn.client_id,
10
+ GoogleSignIn.client_secret,
11
+ authorize_url: 'https://accounts.google.com/o/oauth2/auth',
12
+ token_url: 'https://www.googleapis.com/oauth2/v3/token',
13
+ redirect_uri: callback_url
14
+ end
15
+ end
@@ -0,0 +1,27 @@
1
+ require_dependency 'google_sign_in/redirect_protector'
2
+
3
+ class GoogleSignIn::CallbacksController < GoogleSignIn::BaseController
4
+ def show
5
+ if valid_request?
6
+ redirect_to proceed_to_url, flash: { google_sign_in_token: id_token }
7
+ else
8
+ head :unprocessable_entity
9
+ end
10
+ rescue GoogleSignIn::RedirectProtector::Violation => error
11
+ logger.error error.message
12
+ head :bad_request
13
+ end
14
+
15
+ private
16
+ def valid_request?
17
+ flash[:state].present? && params.require(:state) == flash[:state]
18
+ end
19
+
20
+ def proceed_to_url
21
+ flash[:proceed_to].tap { |url| GoogleSignIn::RedirectProtector.ensure_same_origin(url, request.url) }
22
+ end
23
+
24
+ def id_token
25
+ client.auth_code.get_token(params.require(:code))['id_token']
26
+ end
27
+ end
@@ -0,0 +1,7 @@
1
+ module GoogleSignIn::ButtonHelper
2
+ def google_sign_in_button(text = nil, proceed_to:, **options, &block)
3
+ form_with url: google_sign_in.authorization_path do
4
+ hidden_field_tag(:proceed_to, proceed_to, id: nil) + button_tag(text, name: nil, **options, &block)
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env ruby
2
+ # This command will automatically be run when you run "rails" with Rails gems
3
+ # installed from the root of your application.
4
+
5
+ ENGINE_ROOT = File.expand_path('..', __dir__)
6
+ ENGINE_PATH = File.expand_path('../lib/blorgh/engine', __dir__)
7
+ APP_PATH = File.expand_path('../test/dummy/config/application', __dir__)
8
+
9
+ # Set up gems listed in the Gemfile.
10
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
11
+ require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
12
+
13
+ require 'rails'
14
+ require 'action_controller/railtie'
15
+ require 'rails/test_unit/railtie'
16
+ require 'rails/engine/commands'
@@ -0,0 +1,4 @@
1
+ GoogleSignIn::Engine.routes.draw do
2
+ resource :authorization, only: :create
3
+ resource :callback, only: :show
4
+ end
@@ -1,19 +1,22 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = 'google_sign_in'
3
- s.version = '0.1.4'
4
- s.authors = 'David Heinemeier Hansson'
5
- s.email = 'david@basecamp.com'
3
+ s.version = '1.0.0'
4
+ s.authors = ['David Heinemeier Hansson', 'George Claghorn']
5
+ s.email = ['david@basecamp.com', 'george@basecamp.com']
6
6
  s.summary = 'Sign in (or up) with Google for Rails applications'
7
7
  s.homepage = 'https://github.com/basecamp/google_sign_in'
8
8
  s.license = 'MIT'
9
9
 
10
10
  s.required_ruby_version = '>= 1.9.3'
11
11
 
12
- s.add_dependency 'activesupport', '>= 5.1'
12
+ s.add_dependency 'rails', '>= 5.2.0'
13
13
  s.add_dependency 'google-id-token', '>= 1.4.0'
14
+ s.add_dependency 'oauth2', '>= 1.4.0'
14
15
 
15
16
  s.add_development_dependency 'bundler', '~> 1.15'
17
+ s.add_development_dependency 'jwt'
18
+ s.add_development_dependency 'webmock'
16
19
 
17
- s.files = `git ls-files`.split("\n")
18
- s.test_files = `git ls-files -- test/*`.split("\n")
20
+ s.files = `git ls-files`.split("\n")
21
+ s.test_files = `git ls-files -- test/*`.split("\n")
19
22
  end
@@ -1,2 +1,10 @@
1
+ require 'active_support'
2
+ require 'active_support/rails'
3
+
4
+ module GoogleSignIn
5
+ mattr_accessor :client_id
6
+ mattr_accessor :client_secret
7
+ end
8
+
1
9
  require 'google_sign_in/identity'
2
- require 'google_sign_in/railtie' if defined?(Rails)
10
+ require 'google_sign_in/engine' if defined?(Rails)
@@ -0,0 +1,28 @@
1
+ require 'rails/engine'
2
+
3
+ module GoogleSignIn
4
+ class Engine < ::Rails::Engine
5
+ isolate_namespace GoogleSignIn
6
+
7
+ config.google_sign_in = ActiveSupport::OrderedOptions.new
8
+
9
+ initializer 'google_sign_in.config' do |app|
10
+ config.after_initialize do
11
+ GoogleSignIn.client_id = config.google_sign_in.client_id || app.credentials.dig(:google_sign_in, :client_id)
12
+ GoogleSignIn.client_secret = config.google_sign_in.client_secret || app.credentials.dig(:google_sign_in, :client_secret)
13
+ end
14
+ end
15
+
16
+ initializer 'google_sign_in.helpers' do
17
+ ActiveSupport.on_load :action_controller do
18
+ ActionController::Base.helper GoogleSignIn::Engine.helpers
19
+ end
20
+ end
21
+
22
+ initializer 'google_sign_in.mount' do |app|
23
+ app.routes.append do
24
+ mount GoogleSignIn::Engine, at: app.config.google_sign_in.root || 'google_sign_in'
25
+ end
26
+ end
27
+ end
28
+ end
@@ -1,21 +1,15 @@
1
1
  require 'google-id-token'
2
- require 'active_support/core_ext/class/attribute'
3
- require 'active_support/core_ext/numeric/time'
2
+ require 'active_support/core_ext/module/delegation'
4
3
 
5
4
  module GoogleSignIn
6
5
  class Identity
7
- class_attribute :client_id
6
+ class ValidationError < StandardError; end
8
7
 
9
- class_attribute :token_expiry
10
- self.token_expiry = 5.minutes
11
-
12
- class_attribute :logger
13
- self.logger = defined?(Rails) ? Rails.logger : Logger.new(STDOUT)
8
+ class_attribute :validator, default: GoogleIDToken::Validator.new
14
9
 
15
10
  def initialize(token)
16
11
  ensure_client_id_present
17
12
  set_extracted_payload(token)
18
- ensure_proper_audience
19
13
  end
20
14
 
21
15
  def user_id
@@ -31,7 +25,7 @@ module GoogleSignIn
31
25
  end
32
26
 
33
27
  def email_verified?
34
- @payload["email_verified"] == "true"
28
+ @payload["email_verified"] == true
35
29
  end
36
30
 
37
31
  def avatar_url
@@ -43,6 +37,8 @@ module GoogleSignIn
43
37
  end
44
38
 
45
39
  private
40
+ delegate :client_id, to: GoogleSignIn
41
+
46
42
  def ensure_client_id_present
47
43
  if client_id.blank?
48
44
  raise ArgumentError, "GoogleSignIn.client_id must be set to validate identity"
@@ -50,16 +46,9 @@ module GoogleSignIn
50
46
  end
51
47
 
52
48
  def set_extracted_payload(token)
53
- @payload = GoogleIDToken::Validator.new(expiry: token_expiry).check(token, client_id)
54
- rescue GoogleIDToken::ValidationError => e
55
- logger.error "Google token failed to validate (#{e.message})"
56
- @payload = {}
57
- end
58
-
59
- def ensure_proper_audience
60
- unless @payload["aud"].include?(client_id)
61
- raise "Failed to locate the client_id #{client_id} in the authorized audience (#{@payload["aud"]})"
62
- end
49
+ @payload = validator.check(token, client_id)
50
+ rescue GoogleIDToken::ValidationError => error
51
+ raise ValidationError, error.message
63
52
  end
64
53
  end
65
- end
54
+ end
@@ -0,0 +1,25 @@
1
+ require 'uri'
2
+
3
+ module GoogleSignIn
4
+ module RedirectProtector
5
+ extend self
6
+
7
+ class Violation < StandardError; end
8
+
9
+ QUALIFIED_URL_PATTERN = /\A#{URI::DEFAULT_PARSER.make_regexp}\z/
10
+
11
+ def ensure_same_origin(target, source)
12
+ if target =~ QUALIFIED_URL_PATTERN && origin_of(target) != origin_of(source)
13
+ raise Violation, "Redirect target #{target} does not have same origin as request (expected #{origin_of(source)})"
14
+ end
15
+ end
16
+
17
+ private
18
+ def origin_of(url)
19
+ uri = URI(url)
20
+ "#{uri.scheme}://#{uri.host}:#{uri.port}"
21
+ rescue ArgumentError
22
+ nil
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,19 @@
1
+ -----BEGIN CERTIFICATE-----
2
+ MIIDETCCAfmgAwIBAQIBADANBgkqhkiG9w0BAQUFADAvMS0wKwYDVQQDDCRnb29n
3
+ bGUtc2lnbi1pbi1mb3ItcmFpbHMuZXhhbXBsZS5jb20wHhcNMTgwOTAyMjI0MTQw
4
+ WhcNMjMwOTAyMjI0MTQwWjAvMS0wKwYDVQQDDCRnb29nbGUtc2lnbi1pbi1mb3It
5
+ cmFpbHMuZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
6
+ AQCtnO1OcLbdxj4f6I/aUMJkCJfrDNvp0v2ljUJoaq6hqWPmoZgcl92njBvt91np
7
+ JPfGaCy0ZYLfizUNBRKkfo6u3MXvubktYqk3SVCejy0TUE11PwRx1u/x0c2rCTa6
8
+ Y7ppAoO9Ur3yoccDmkceP8MpofHWetrdaxyhktlqy6gpM7V+kjj+anySQk4XqDJl
9
+ F4FXHt82HMNK3xbjXJyEoyMudGUISBDn/rG8b3LxEKawUiLVCI54g3+L/Oi4nZCE
10
+ XNCd/mvWpVFoPpFQGMoKW3S9KxFowvfkDSxWYwgYnWnsO0ueXS+WYML88KO1Qf7V
11
+ mEZ91u7w1/EiMVxiuczxycSfAgMBAAGjODA2MAwGA1UdEwEB/wQCMAAwDgYDVR0P
12
+ AQH/BAQDAgeAMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMCMA0GCSqGSIb3DQEBBQUA
13
+ A4IBAQCVXynTCZhpbINC4j1prGaPfY6mRkCZzcRpQFim4C6hJtYSRn57qjpmYWG3
14
+ eVc3ElfNUAWgC3trACjN3hDqKv0/hH9TGTY9iFhc747L/VSaKWzH/uWewj1qTwsX
15
+ dUEFxZILAWvAMBNUT060t8bt+pSFc2h4fHsftOqFLfkFUcCr22QsWyueXzWZyDeZ
16
+ XWFGtD+WOR5SC4mIY359e75/vZsJymzZIfM+pfcaHnXtXez9SeLM81rvnRdR1b+H
17
+ /S0LT0dPRkXSvC2HRPwzHxVctNrDoaxON+OIMgd4lHAFs6doVoYmnprzO69+IBUK
18
+ s0LBENxbn2rd7IEl6EaC91cXCl3y
19
+ -----END CERTIFICATE-----
@@ -0,0 +1,26 @@
1
+ require 'test_helper'
2
+
3
+ class GoogleSignIn::AuthorizationsControllerTest < ActionDispatch::IntegrationTest
4
+ test "redirecting to Google for authorization" do
5
+ post google_sign_in.authorization_url, params: { proceed_to: 'http://www.example.com/login' }
6
+ assert_response :redirect
7
+ assert_match 'https://accounts.google.com/o/oauth2/auth', response.location
8
+
9
+ params = extract_query_params_from(response.location)
10
+ assert_equal FAKE_GOOGLE_CLIENT_ID, params[:client_id]
11
+ assert_equal 'login', params[:prompt]
12
+ assert_equal 'code', params[:response_type]
13
+ assert_equal 'http://www.example.com/google_sign_in/callback', params[:redirect_uri]
14
+ assert_equal 'openid profile email', params[:scope]
15
+ assert_match /[A-Za-z0-9+\/]{22}==/, params[:state]
16
+
17
+ assert_equal 'http://www.example.com/login', flash[:proceed_to]
18
+ assert_equal params[:state], flash[:state]
19
+ end
20
+
21
+ private
22
+ def extract_query_params_from(url)
23
+ query = URI(url).query
24
+ Rack::Utils.parse_query(query).symbolize_keys
25
+ end
26
+ end
@@ -0,0 +1,36 @@
1
+ require 'test_helper'
2
+
3
+ class GoogleSignIn::CallbacksControllerTest < ActionDispatch::IntegrationTest
4
+ test "receiving an authorization code" do
5
+ post google_sign_in.authorization_url, params: { proceed_to: 'http://www.example.com/login' }
6
+ assert_response :redirect
7
+
8
+ stub_token_request code: '4/SgCpHSVW5-Cy', access_token: 'ya29.GlwIBo', id_token: 'eyJhbGciOiJSUzI'
9
+
10
+ get google_sign_in.callback_url(code: '4/SgCpHSVW5-Cy', state: flash[:state])
11
+ assert_redirected_to 'http://www.example.com/login'
12
+ assert_equal 'eyJhbGciOiJSUzI', flash[:google_sign_in_token]
13
+ end
14
+
15
+ test "protecting against CSRF" do
16
+ get google_sign_in.callback_url(code: '4/SgCpHSVW5-Cy', state: 'invalid')
17
+ assert_response :unprocessable_entity
18
+ end
19
+
20
+ test "protecting against open redirects" do
21
+ post google_sign_in.authorization_url, params: { proceed_to: 'http://malicious.example.com/login' }
22
+ assert_response :redirect
23
+
24
+ get google_sign_in.callback_url(code: '4/SgCpHSVW5-Cy', state: flash[:state])
25
+ assert_response :bad_request
26
+ end
27
+
28
+ private
29
+ def stub_token_request(code:, **params)
30
+ stub_request(:post, 'https://www.googleapis.com/oauth2/v3/token').
31
+ with(body: { grant_type: 'authorization_code', code: code,
32
+ client_id: FAKE_GOOGLE_CLIENT_ID, client_secret: FAKE_GOOGLE_CLIENT_SECRET,
33
+ redirect_uri: 'http://www.example.com/google_sign_in/callback' }).
34
+ to_return(status: 200, headers: { 'Content-Type' => 'application/json' }, body: JSON.generate(params))
35
+ end
36
+ end
@@ -0,0 +1 @@
1
+ 2.5.0
@@ -0,0 +1,6 @@
1
+ # Add your own tasks in files placed in lib/tasks ending in .rake,
2
+ # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
3
+
4
+ require_relative 'config/application'
5
+
6
+ Rails.application.load_tasks
@@ -0,0 +1,3 @@
1
+ //= link_tree ../images
2
+ //= link_directory ../javascripts .js
3
+ //= link_directory ../stylesheets .css
File without changes
@@ -0,0 +1,15 @@
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 activestorage
15
+ //= require_tree .
@@ -0,0 +1,13 @@
1
+ // Action Cable provides the framework to deal with WebSockets in Rails.
2
+ // You can generate new channels where WebSocket features live using the `rails generate channel` command.
3
+ //
4
+ //= require action_cable
5
+ //= require_self
6
+ //= require_tree ./channels
7
+
8
+ (function() {
9
+ this.App || (this.App = {});
10
+
11
+ App.cable = ActionCable.createConsumer();
12
+
13
+ }).call(this);
@@ -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,4 @@
1
+ module ApplicationCable
2
+ class Channel < ActionCable::Channel::Base
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module ApplicationCable
2
+ class Connection < ActionCable::Connection::Base
3
+ end
4
+ end
@@ -0,0 +1,2 @@
1
+ class ApplicationController < ActionController::Base
2
+ end