فهرست منبع

Install devise, setup omniauth providers

Andrew Swistak 6 سال پیش
والد
کامیت
8083453f8e

+ 9 - 0
.env.template

@@ -17,6 +17,15 @@ POSTGRES_PASSWORD=postgres
 # will be created as `ENV['POSTGRES_DB'] + "_test"`
 POSTGRES_DB=pokemon_trade_dev
 
+REDDIT_OAUTH_KEY=
+REDDIT_OAUTH_SECRET=
+
+GOOGLE_OAUTH_CLIENT_ID=
+GOOGLE_OAUTH_CLIENT_SECRET=
+
+DISCORD_CLIENT_ID=
+DISCORD_CLIENT_SECRET=
+
 # If you want to use redis locally, provide the URL, e.g. redis://127.0.0.1:6379
 REDIS_URL=
 

+ 7 - 0
Gemfile

@@ -8,8 +8,13 @@ ruby '2.6.3'
 gem 'bootsnap', '>= 1.4.1', require: false
 gem 'listen', '>= 3.0.5', '< 3.2' # requirement of bootsnap
 
+gem 'devise'
 gem 'graphql'
 gem 'haml'
+gem 'omniauth'
+gem 'omniauth-discord'
+gem 'omniauth-google-oauth2'
+gem 'omniauth-oauth2'
 gem 'pg'
 gem 'puma', '~> 4.2'
 gem 'rails', '~> 6.0.0'
@@ -44,6 +49,8 @@ group :development, :test do
 end
 
 group :development do
+  gem 'letter_opener'
+
   # Access an interactive console on exception pages or by calling 'console'
   # anywhere in the code.
   gem 'web-console', '>= 4.0.1'

+ 49 - 0
Gemfile.lock

@@ -73,6 +73,7 @@ GEM
     addressable (2.7.0)
       public_suffix (>= 2.0.2, < 5.0)
     ast (2.4.0)
+    bcrypt (3.1.13)
     bindex (0.8.1)
     bootsnap (1.4.5)
       msgpack (~> 1.0)
@@ -96,6 +97,12 @@ GEM
     concurrent-ruby (1.1.5)
     crass (1.0.4)
     database_cleaner (1.7.0)
+    devise (4.7.1)
+      bcrypt (~> 3.0)
+      orm_adapter (~> 0.1)
+      railties (>= 4.1.0)
+      responders
+      warden (~> 1.2.3)
     diff-lcs (1.3)
     docile (1.3.2)
     domain_name (0.5.20190701)
@@ -112,6 +119,8 @@ GEM
       railties (>= 4.2.0)
     faker (2.6.0)
       i18n (>= 1.6, < 1.8)
+    faraday (0.17.0)
+      multipart-post (>= 1.2, < 3)
     ffi (1.11.1)
     globalid (0.4.2)
       activesupport (>= 4.2.0)
@@ -122,6 +131,7 @@ GEM
     haml (5.1.2)
       temple (>= 0.8.0)
       tilt
+    hashie (3.6.0)
     http-accept (1.7.0)
     http-cookie (1.0.3)
       domain_name (~> 0.5)
@@ -129,6 +139,11 @@ GEM
       concurrent-ruby (~> 1.0)
     jaro_winkler (1.5.3)
     json (2.2.0)
+    jwt (2.2.1)
+    launchy (2.4.3)
+      addressable (~> 2.3)
+    letter_opener (1.7.0)
+      launchy (~> 2.2)
     listen (3.1.5)
       rb-fsevent (~> 0.9, >= 0.9.4)
       rb-inotify (~> 0.9, >= 0.9.7)
@@ -149,10 +164,33 @@ GEM
     mini_portile2 (2.4.0)
     minitest (5.12.2)
     msgpack (1.3.1)
+    multi_json (1.14.1)
+    multi_xml (0.6.0)
+    multipart-post (2.1.1)
     netrc (0.11.0)
     nio4r (2.5.2)
     nokogiri (1.10.4)
       mini_portile2 (~> 2.4.0)
+    oauth2 (1.4.2)
+      faraday (>= 0.8, < 2.0)
+      jwt (>= 1.0, < 3.0)
+      multi_json (~> 1.3)
+      multi_xml (~> 0.5)
+      rack (>= 1.2, < 3)
+    omniauth (1.9.0)
+      hashie (>= 3.4.6, < 3.7.0)
+      rack (>= 1.6.2, < 3)
+    omniauth-discord (1.0.0)
+      omniauth
+      omniauth-oauth2
+    omniauth-google-oauth2 (0.8.0)
+      jwt (>= 2.0)
+      omniauth (>= 1.1.1)
+      omniauth-oauth2 (>= 1.6)
+    omniauth-oauth2 (1.6.0)
+      oauth2 (~> 1.1)
+      omniauth (~> 1.9)
+    orm_adapter (0.5.0)
     parallel (1.18.0)
     parser (2.6.5.0)
       ast (~> 2.4.0)
@@ -207,6 +245,9 @@ GEM
       ffi (~> 1.0)
     redis (4.1.3)
     regexp_parser (1.6.0)
+    responders (3.0.0)
+      actionpack (>= 5.0)
+      railties (>= 5.0)
     rest-client (2.1.0)
       http-accept (>= 1.7.0, < 2.0)
       http-cookie (>= 1.0.2, < 2.0)
@@ -279,6 +320,8 @@ GEM
       unf_ext
     unf_ext (0.0.7.6)
     unicode-display_width (1.6.0)
+    warden (1.2.8)
+      rack (>= 2.0.6)
     web-console (4.0.1)
       actionview (>= 6.0.0)
       activemodel (>= 6.0.0)
@@ -308,13 +351,19 @@ DEPENDENCIES
   bundle-audit
   capybara (>= 2.15)
   database_cleaner
+  devise
   dotenv-rails
   factory_bot_rails
   faker
   graphiql-rails
   graphql
   haml
+  letter_opener
   listen (>= 3.0.5, < 3.2)
+  omniauth
+  omniauth-discord
+  omniauth-google-oauth2
+  omniauth-oauth2
   pg
   pry
   pry-rails

+ 3 - 1
app/controllers/api/application_controller.rb

@@ -2,7 +2,9 @@
 
 require './lib/api_error/base_error'
 
-class API::ApplicationController < ActionController::API
+class API::ApplicationController < ApplicationController
+  protect_from_forgery with: :null_session
+
   before_action :set_default_response_format
 
   rescue_from StandardError, with: :handle_unhandled_api_error

+ 1 - 2
app/controllers/api/graphql_controller.rb

@@ -6,8 +6,7 @@ class API::GraphqlController < API::ApplicationController
     query = params[:query]
     operation_name = params[:operationName]
     context = {
-      # Query context goes here, for example:
-      # current_user: current_user,
+      current_user: current_user,
     }
 
     render json: PokemonTradeSchema.execute(

+ 1 - 0
app/controllers/application_controller.rb

@@ -1,4 +1,5 @@
 # frozen_string_literal: true
 
 class ApplicationController < ActionController::Base
+  protect_from_forgery with: :exception
 end

+ 74 - 0
app/controllers/users/omniauth_callbacks_controller.rb

@@ -0,0 +1,74 @@
+# frozen_string_literal: true
+
+class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
+  # You should configure your model like this:
+  # devise :omniauthable, omniauth_providers: [:twitter]
+  def google_oauth2
+    @user = User.from_omniauth(request.env['omniauth.auth'])
+
+    if @user.persisted?
+      # this will throw if @user is not activated
+      sign_in_and_redirect @user, event: :authentication
+      if is_navigational_format?
+        set_flash_message(:notice, :success, kind: 'Google')
+      end
+    else
+      session['devise.google_oauth2_data'] = request.env['omniauth.auth']
+      redirect_to new_user_registration_url
+    end
+  end
+
+  def reddit
+    @user = User.from_omniauth(request.env['omniauth.auth'])
+
+    if @user.persisted?
+      # this will throw if @user is not activated
+      sign_in_and_redirect @user, event: :authentication
+      if is_navigational_format?
+        set_flash_message(:notice, :success, kind: 'Reddit')
+      end
+    else
+      session['devise.reddit_data'] = request.env['omniauth.auth']
+      redirect_to new_user_registration_url
+    end
+  end
+
+  def discord
+    @user = User.from_omniauth(request.env['omniauth.auth'])
+
+    if @user.persisted?
+      # this will throw if @user is not activated
+      sign_in_and_redirect @user, event: :authentication
+      if is_navigational_format?
+        set_flash_message(:notice, :success, kind: 'Discord')
+      end
+    else
+      session['devise.discord_data'] = request.env['omniauth.auth']
+      redirect_to new_user_registration_url
+    end
+  end
+
+  # You should also create an action method in this controller like this:
+  # def twitter
+  # end
+
+  # More info at:
+  # https://github.com/plataformatec/devise#omniauth
+
+  # GET|POST /resource/auth/twitter
+  # def passthru
+  #   super
+  # end
+
+  # GET|POST /users/auth/twitter/callback
+  # def failure
+  #   super
+  # end
+
+  # protected
+
+  # The path used when OmniAuth fails
+  # def after_omniauth_failure_path_for(scope)
+  #   super(scope)
+  # end
+end

+ 22 - 0
app/models/user.rb

@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+class User < ApplicationRecord
+  devise :database_authenticatable, :lockable, :recoverable, :rememberable,
+         :trackable, :validatable, :confirmable, :registerable
+
+  devise :omniauthable, omniauth_providers: %i[reddit google_oauth2 discord]
+
+  # Note: devise :validatable above adds validations for :email and :password
+  # validates :username, presence: true, length: {maximum: 128}
+  validates :email, confirmation: true
+
+  def self.from_omniauth(auth)
+    where(provider: auth.provider, uid: auth.uid).first_or_create do |user|
+      user.email = auth.info.email
+      user.password = Devise.friendly_token[0, 20]
+
+      # Reddit, Google, and Discord both have email verification already
+      user.skip_confirmation!
+    end
+  end
+end

+ 8 - 0
config/environments/development.rb

@@ -45,6 +45,14 @@ Rails.application.configure do
 
   config.action_mailer.perform_caching = false
 
+  config.action_mailer.default_url_options = {
+    host: 'localhost',
+    port: ENV['WEB_HOST_PORT'],
+  }
+
+  config.action_mailer.delivery_method = :letter_opener
+  config.action_mailer.perform_deliveries = true
+
   # Print deprecation notices to the Rails logger.
   config.active_support.deprecation = :log
 

+ 329 - 0
config/initializers/devise.rb

@@ -0,0 +1,329 @@
+# frozen_string_literal: true
+
+require './lib/omni_auth/strategies/reddit'
+
+# Use this hook to configure devise mailer, warden hooks and so forth.
+# Many of these configuration options can be set straight in your model.
+Devise.setup do |config|
+  # The secret key used by Devise. Devise uses this key to generate
+  # random tokens. Changing this key will render invalid all existing
+  # confirmation, reset password and unlock tokens in the database.
+  # Devise will use the `secret_key_base` as its `secret_key`
+  # by default. You can change it below and use your own secret key.
+  # config.secret_key = '8caa33dfbc71eedf4f4f1b6d7966f73840d73d20fa13cf47f5a166f6e90041a6813372a50ca939c25c72a67d99e96f51c04bbe1857fbbc248476af72aafa2a01'
+
+  # ==> Controller configuration
+  # Configure the parent class to the devise controllers.
+  # config.parent_controller = 'DeviseController'
+
+  # ==> Mailer Configuration
+  # Configure the e-mail address which will be shown in Devise::Mailer,
+  # note that it will be overwritten if you use your own mailer class
+  # with default "from" parameter.
+
+  # FIXME: change to env var or other
+  config.mailer_sender = 'please-change-me-at-config-initializers-devise@example.com'
+
+  # Configure the class responsible to send e-mails.
+  # config.mailer = 'Devise::Mailer'
+
+  # Configure the parent class responsible to send e-mails.
+  # config.parent_mailer = 'ActionMailer::Base'
+
+  # ==> ORM configuration
+  # Load and configure the ORM. Supports :active_record (default) and
+  # :mongoid (bson_ext recommended) by default. Other ORMs may be
+  # available as additional gems.
+  require 'devise/orm/active_record'
+
+  # ==> Configuration for any authentication mechanism
+  # Configure which keys are used when authenticating a user. The default is
+  # just :email. You can configure it to use [:username, :subdomain], so for
+  # authenticating a user, both parameters are required. Remember that those
+  # parameters are used only when authenticating and not when retrieving from
+  # session. If you need permissions, you should implement that in a before
+  # filter.  You can also supply a hash where the value is a boolean determining
+  # whether or not authentication should be aborted when the value is not
+  # present.
+  config.authentication_keys = [:username]
+
+  # Configure parameters from the request object used for authentication. Each
+  # entry given should be a request method and it will automatically be passed
+  # to the find_for_authentication method and considered in your model lookup.
+  # For instance, if you set :request_keys to [:subdomain], :subdomain will be
+  # used on authentication.  The same considerations mentioned for
+  # authentication_keys also apply to request_keys.
+  # config.request_keys = []
+
+  # Configure which authentication keys should be case-insensitive.
+  # These keys will be downcased upon creating or modifying a user and when used
+  # to authenticate or find a user. Default is :email.
+  config.case_insensitive_keys = %i[username email email_confirmation]
+
+  # Configure which authentication keys should have whitespace stripped.  These
+  # keys will have whitespace before and after removed upon creating or
+  # modifying a user and when used to authenticate or find a user. Default is
+  # :email.
+  config.strip_whitespace_keys = %i[username email]
+
+  # Tell if authentication through request.params is enabled. True by default.
+  # It can be set to an array that will enable params authentication only for
+  # the given strategies, for example, `config.params_authenticatable =
+  # [:database]` will enable it only for database (email + password)
+  # authentication.
+  # config.params_authenticatable = true
+
+  # Tell if authentication through HTTP Auth is enabled. False by default.  It
+  # can be set to an array that will enable http authentication only for the
+  # given strategies, for example, `config.http_authenticatable = [:database]`
+  # will enable it only for database authentication. The supported strategies
+  # are: :database      = Support basic authentication with authentication key +
+  # password config.http_authenticatable = false
+
+  # If 401 status code should be returned for AJAX requests. True by default.
+  # config.http_authenticatable_on_xhr = true
+
+  # The realm used in Http Basic Authentication. 'Application' by default.
+  # config.http_authentication_realm = 'Application'
+
+  # It will change confirmation, password recovery and other workflows
+  # to behave the same regardless if the e-mail provided was right or wrong.
+  # Does not affect registerable.
+  config.paranoid = true
+
+  # By default Devise will store the user in session. You can skip storage for
+  # particular strategies by setting this option.
+  # Notice that if you are skipping storage for all authentication paths, you
+  # may want to disable generating routes to Devise's sessions controller by
+  # passing skip: :sessions to `devise_for` in your config/routes.rb
+  config.skip_session_storage = [:http_auth]
+
+  # By default, Devise cleans up the CSRF token on authentication to
+  # avoid CSRF token fixation attacks. This means that, when using AJAX
+  # requests for sign in and sign up, you need to get a new CSRF token
+  # from the server. You can disable this option at your own risk.
+  # config.clean_up_csrf_token_on_authentication = true
+
+  # When false, Devise will not attempt to reload routes on eager load.
+  # This can reduce the time taken to boot the app but if your application
+  # requires the Devise mappings to be loaded during boot time the application
+  # won't boot properly.
+  # config.reload_routes = true
+
+  # ==> Configuration for :database_authenticatable
+  # For bcrypt, this is the cost for hashing the password and defaults to 11. If
+  # using other algorithms, it sets how many times you want the password to be
+  # hashed.
+  #
+  # Limiting the stretches to just one in testing will increase the performance
+  # of your test suite dramatically. However, it is STRONGLY RECOMMENDED to not
+  # use a value less than 10 in other environments. Note that, for bcrypt (the
+  # default algorithm), the cost increases exponentially with the number of
+  # stretches (e.g.  a value of 20 is already extremely slow: approx. 60 seconds
+  # for 1 calculation).
+  config.stretches = Rails.env.test? ? 1 : 11
+
+  # Set up a pepper to generate the hashed password.
+  # config.pepper = '51526ffa94bb35de720f3acebdf749bd07d86ffef03bf637f07022806dff74e6045d94ec7912548e53dce24829e8cfb5ed042a08128ea2592a8542d9f5e8f0e5'
+
+  # Send a notification to the original email when the user's email is changed.
+  config.send_email_changed_notification = true
+
+  # Send a notification email when the user's password is changed.
+  config.send_password_change_notification = true
+
+  # ==> Configuration for :confirmable
+  # A period that the user is allowed to access the website even without
+  # confirming their account. For instance, if set to 2.days, the user will be
+  # able to access the website for two days without confirming their account,
+  # access will be blocked just in the third day.
+  # You can also set it to nil, which will allow the user to access the website
+  # without confirming their account.
+  # Default is 0.days, meaning the user cannot access the website without
+  # confirming their account.
+  # config.allow_unconfirmed_access_for = 2.days
+
+  # A period that the user is allowed to confirm their account before their
+  # token becomes invalid. For example, if set to 3.days, the user can confirm
+  # their account within 3 days after the mail was sent, but on the fourth day
+  # their account can't be confirmed with the token any more.
+  # Default is nil, meaning there is no restriction on how long a user can take
+  # before confirming their account.
+  # config.confirm_within = 3.days
+
+  # If true, requires any email changes to be confirmed (exactly the same way as
+  # initial account confirmation) to be applied. Requires additional
+  # unconfirmed_email db field (see migrations). Until confirmed, new email is
+  # stored in unconfirmed_email column, and copied to email column on successful
+  # confirmation.
+  config.reconfirmable = true
+
+  # Defines which key will be used when confirming an account
+  # config.confirmation_keys = [:email]
+
+  # ==> Configuration for :rememberable
+  # The time the user will be remembered without asking for credentials again.
+  # config.remember_for = 2.weeks
+
+  # Invalidates all the remember me tokens when the user signs out.
+  config.expire_all_remember_me_on_sign_out = true
+
+  # If true, extends the user's remember period when remembered via cookie.
+  # config.extend_remember_period = false
+
+  # Options to be passed to the created cookie. For instance, you can set
+  # secure: true in order to force SSL only cookies.
+  # config.rememberable_options = {}
+
+  # ==> Configuration for :validatable
+  # Range for password length.
+  config.password_length = 8..128
+
+  # Email regex used to validate email formats. It simply asserts that
+  # one (and only one) @ exists in the given string. This is mainly
+  # to give user feedback and not to assert the e-mail validity.
+  config.email_regexp = /\A[^@\s]+@[^@\s]+\z/
+
+  # ==> Configuration for :timeoutable
+  # The time you want to timeout the user session without activity. After this
+  # time the user will be asked for credentials again. Default is 30 minutes.
+  # config.timeout_in = 30.minutes
+
+  # ==> Configuration for :lockable
+  # Defines which strategy will be used to lock an account.
+  # :failed_attempts = Locks an account after a number of failed attempts to sign in.
+  # :none            = No lock strategy. You should handle locking by yourself.
+  config.lock_strategy = :failed_attempts
+
+  # Defines which key will be used when locking and unlocking an account
+  config.unlock_keys = [:email]
+
+  # Defines which strategy will be used to unlock an account.
+  # :email = Sends an unlock link to the user email
+  # :time  = Re-enables login after a certain amount of time (see :unlock_in below)
+  # :both  = Enables both strategies
+  # :none  = No unlock strategy. You should handle unlocking by yourself.
+  config.unlock_strategy = :both
+
+  # Number of authentication tries before locking an account if lock_strategy
+  # is failed attempts.
+  config.maximum_attempts = 10
+
+  # Time interval to unlock the account if :time is enabled as unlock_strategy.
+  config.unlock_in = 10.minutes
+
+  # Warn on the last attempt before the account is locked.
+  # config.last_attempt_warning = true
+
+  # ==> Configuration for :recoverable
+  #
+  # Defines which key will be used when recovering the password for an account
+  config.reset_password_keys = [:email]
+
+  # Time interval you can reset your password with a reset password key.
+  # Don't put a too small interval or your users won't have the time to
+  # change their passwords.
+  config.reset_password_within = 6.hours
+
+  # When set to false, does not sign a user in automatically after their
+  # password is reset. Defaults to true, so a user is signed in automatically
+  # after a reset.
+  config.sign_in_after_reset_password = false
+
+  # ==> Configuration for :encryptable
+  # Allow you to use another hashing or encryption algorithm besides bcrypt
+  # (default).  You can use :sha1, :sha512 or algorithms from others
+  # authentication tools as :clearance_sha1, :authlogic_sha512 (then you should
+  # set stretches above to 20 for default behavior) and
+  # :restful_authentication_sha1 (then you should set stretches to 10, and copy
+  # REST_AUTH_SITE_KEY to pepper).
+  #
+  # Require the `devise-encryptable` gem when using anything other than bcrypt
+  # config.encryptor = :sha512
+
+  # ==> Scopes configuration
+  # Turn scoped views on. Before rendering "sessions/new", it will first check
+  # for "users/sessions/new". It's turned off by default because it's slower if
+  # you are using only default views.  config.scoped_views = false
+
+  # Configure the default scope given to Warden. By default it's the first
+  # devise role declared in your routes (usually :user).
+  # config.default_scope = :user
+
+  # Set this configuration to false if you want /users/sign_out to sign out
+  # only the current scope. By default, Devise signs out all scopes.
+  # config.sign_out_all_scopes = true
+
+  # ==> Navigation configuration
+  # Lists the formats that should be treated as navigational. Formats like
+  # :html, should redirect to the sign in page when the user does not have
+  # access, but formats like :xml or :json, should return 401.
+  #
+  # If you have any extra navigational formats, like :iphone or :mobile, you
+  # should add them to the navigational formats lists.
+  #
+  # The "*/*" below is required to match Internet Explorer requests.
+  # config.navigational_formats = ['*/*', :html]
+
+  # The default HTTP method used to sign out a resource. Default is :delete.
+  config.sign_out_via = :delete
+
+  # ==> OmniAuth
+  # Add a new OmniAuth provider. Check the wiki for more information on setting
+  # up on your models and hooks.
+  config.omniauth :reddit,
+                  ENV['REDDIT_OAUTH_CLIENT_ID'],
+                  ENV['REDDIT_OAUTH_SECRET'],
+                  scope: 'identity',
+                  duration: 'temporary'
+
+  config.omniauth :google_oauth2,
+                  ENV['GOOGLE_OAUTH_CLIENT_ID'],
+                  ENV['GOOGLE_OAUTH_CLIENT_SECRET'],
+                  scope: 'email'
+
+  config.omniauth :discord,
+                  ENV['DISCORD_CLIENT_ID'],
+                  ENV['DISCORD_CLIENT_SECRET'],
+                  scope: 'email identify'
+
+  # ==> Warden configuration
+  # If you want to use other strategies, that are not supported by Devise, or
+  # change the failure app, you can configure them inside the config.warden
+  # block.
+  #
+  # config.warden do |manager|
+  #   manager.intercept_401 = false
+  #   manager.default_strategies(scope: :user).unshift :some_external_strategy
+  # end
+
+  # ==> Mountable engine configurations
+  # When using Devise inside an engine, let's call it `MyEngine`, and this
+  # engine is mountable, there are some extra configurations to be taken into
+  # account.  The following options are available, assuming the engine is
+  # mounted as:
+  #
+  #     mount MyEngine, at: '/my_engine'
+  #
+  # The router that invoked `devise_for`, in the example above, would be:
+  # config.router_name = :my_engine
+  #
+  # When using OmniAuth, Devise cannot automatically set OmniAuth path,
+  # so you need to do it manually. For the users scope, it would be:
+  # config.omniauth_path_prefix = '/my_engine/users/auth'
+
+  # ==> Turbolinks configuration
+  # If your app is using Turbolinks, Turbolinks::Controller needs to be included
+  # to make redirection work correctly:
+  #
+  # ActiveSupport.on_load(:devise_failure_app) do
+  #   include Turbolinks::Controller
+  # end
+
+  # ==> Configuration for :registerable
+
+  # When set to false, does not sign a user in automatically after their
+  # password is changed. Defaults to true, so a user is signed in automatically
+  # after changing a password.
+  # config.sign_in_after_change_password = true
+end

+ 3 - 0
config/initializers/omniauth.rb

@@ -0,0 +1,3 @@
+# frozen_string_literal: true
+
+OmniAuth.config.logger = Rails.logger

+ 65 - 0
config/locales/devise.en.yml

@@ -0,0 +1,65 @@
+# Additional translations at https://github.com/plataformatec/devise/wiki/I18n
+
+en:
+  devise:
+    confirmations:
+      confirmed: "Your email address has been successfully confirmed."
+      send_instructions: "You will receive an email with instructions for how to confirm your email address in a few minutes."
+      send_paranoid_instructions: "If your email address exists in our database, you will receive an email with instructions for how to confirm your email address in a few minutes."
+    failure:
+      already_authenticated: "You are already signed in."
+      inactive: "Your account is not activated yet."
+      invalid: "Invalid %{authentication_keys} or password."
+      locked: "Your account is locked."
+      last_attempt: "You have one more attempt before your account is locked."
+      not_found_in_database: "Invalid %{authentication_keys} or password."
+      timeout: "Your session expired. Please sign in again to continue."
+      unauthenticated: "You need to sign in or sign up before continuing."
+      unconfirmed: "You have to confirm your email address before continuing."
+    mailer:
+      confirmation_instructions:
+        subject: "Confirmation instructions"
+      reset_password_instructions:
+        subject: "Reset password instructions"
+      unlock_instructions:
+        subject: "Unlock instructions"
+      email_changed:
+        subject: "Email Changed"
+      password_change:
+        subject: "Password Changed"
+    omniauth_callbacks:
+      failure: "Could not authenticate you from %{kind} because \"%{reason}\"."
+      success: "Successfully authenticated from %{kind} account."
+    passwords:
+      no_token: "You can't access this page without coming from a password reset email. If you do come from a password reset email, please make sure you used the full URL provided."
+      send_instructions: "You will receive an email with instructions on how to reset your password in a few minutes."
+      send_paranoid_instructions: "If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes."
+      updated: "Your password has been changed successfully. You are now signed in."
+      updated_not_active: "Your password has been changed successfully."
+    registrations:
+      destroyed: "Bye! Your account has been successfully cancelled. We hope to see you again soon."
+      signed_up: "Welcome! You have signed up successfully."
+      signed_up_but_inactive: "You have signed up successfully. However, we could not sign you in because your account is not yet activated."
+      signed_up_but_locked: "You have signed up successfully. However, we could not sign you in because your account is locked."
+      signed_up_but_unconfirmed: "A message with a confirmation link has been sent to your email address. Please follow the link to activate your account."
+      update_needs_confirmation: "You updated your account successfully, but we need to verify your new email address. Please check your email and follow the confirmation link to confirm your new email address."
+      updated: "Your account has been updated successfully."
+      updated_but_not_signed_in: "Your account has been updated successfully, but since your password was changed, you need to sign in again"
+    sessions:
+      signed_in: "Signed in successfully."
+      signed_out: "Signed out successfully."
+      already_signed_out: "Signed out successfully."
+    unlocks:
+      send_instructions: "You will receive an email with instructions for how to unlock your account in a few minutes."
+      send_paranoid_instructions: "If your account exists, you will receive an email with instructions for how to unlock it in a few minutes."
+      unlocked: "Your account has been unlocked successfully. Please sign in to continue."
+  errors:
+    messages:
+      already_confirmed: "was already confirmed, please try signing in"
+      confirmation_period_expired: "needs to be confirmed within %{period}, please request a new one"
+      expired: "has expired, please request a new one"
+      not_found: "not found"
+      not_locked: "was not locked"
+      not_saved:
+        one: "1 error prohibited this %{resource} from being saved:"
+        other: "%{count} errors prohibited this %{resource} from being saved:"

+ 3 - 0
config/routes.rb

@@ -7,6 +7,9 @@ Rails.application.routes.draw do
           graphql_path: '/api/graphql'
   end
 
+  devise_for :users,
+             controllers: {omniauth_callbacks: 'users/omniauth_callbacks'}
+
   root to: 'welcome#root'
 
   namespace :api, defaults: {formats: :json} do

+ 50 - 0
db/migrate/20191018075555_devise_create_users.rb

@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+class DeviseCreateUsers < ActiveRecord::Migration[6.0]
+  def change
+    create_table :users do |t|
+      ## Database authenticatable
+      t.string :username
+      t.string :email,              null: false, default: ''
+      t.string :encrypted_password, null: false, default: ''
+
+      ## Recoverable
+      t.string   :reset_password_token
+      t.datetime :reset_password_sent_at
+
+      ## Rememberable
+      t.datetime :remember_created_at
+
+      ## Trackable
+      t.integer  :sign_in_count, default: 0, null: false
+      t.datetime :current_sign_in_at
+      t.datetime :last_sign_in_at
+      t.inet     :current_sign_in_ip
+      t.inet     :last_sign_in_ip
+
+      ## Confirmable
+      t.string   :confirmation_token
+      t.datetime :confirmed_at
+      t.datetime :confirmation_sent_at
+      t.string   :unconfirmed_email # Only if using reconfirmable
+
+      ## Lockable
+      # Only if lock strategy is :failed_attempts
+      t.integer  :failed_attempts, default: 0, null: false
+      # Only if unlock strategy is :email or :both
+      t.string   :unlock_token
+      t.datetime :locked_at
+
+      ## Omniauthable
+      t.string :provider
+      t.string :uid
+
+      t.timestamps null: false
+    end
+
+    add_index :users, :email,                unique: true
+    add_index :users, :reset_password_token, unique: true
+    add_index :users, :confirmation_token,   unique: true
+    add_index :users, :unlock_token,         unique: true
+  end
+end

+ 30 - 1
db/schema.rb

@@ -10,7 +10,7 @@
 #
 # It's strongly recommended that you check this file into your version control system.
 
-ActiveRecord::Schema.define(version: 2018_12_28_215350) do
+ActiveRecord::Schema.define(version: 2019_10_18_075555) do
 
   # These are extensions that must be enabled in order to support this database
   enable_extension "plpgsql"
@@ -24,4 +24,33 @@ ActiveRecord::Schema.define(version: 2018_12_28_215350) do
     t.datetime "updated_at", null: false
   end
 
+  create_table "users", force: :cascade do |t|
+    t.string "username", default: "", null: false
+    t.string "email", default: "", null: false
+    t.string "encrypted_password", default: "", null: false
+    t.string "reset_password_token"
+    t.datetime "reset_password_sent_at"
+    t.datetime "remember_created_at"
+    t.integer "sign_in_count", default: 0, null: false
+    t.datetime "current_sign_in_at"
+    t.datetime "last_sign_in_at"
+    t.inet "current_sign_in_ip"
+    t.inet "last_sign_in_ip"
+    t.string "confirmation_token"
+    t.datetime "confirmed_at"
+    t.datetime "confirmation_sent_at"
+    t.string "unconfirmed_email"
+    t.integer "failed_attempts", default: 0, null: false
+    t.string "unlock_token"
+    t.datetime "locked_at"
+    t.string "provider"
+    t.string "uid"
+    t.datetime "created_at", precision: 6, null: false
+    t.datetime "updated_at", precision: 6, null: false
+    t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true
+    t.index ["email"], name: "index_users_on_email", unique: true
+    t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
+    t.index ["unlock_token"], name: "index_users_on_unlock_token", unique: true
+  end
+
 end

+ 63 - 0
lib/omni_auth/strategies/reddit.rb

@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+module OmniAuth
+  module Strategies
+    class Reddit < OmniAuth::Strategies::OAuth2
+      option :name, 'reddit'
+      option :authorize_options, %i[scope duration]
+
+      option :client_options,
+             site: 'https://oauth.reddit.com',
+             token_url: 'https://www.reddit.com/api/v1/access_token'
+
+      uid { raw_info['id'] }
+
+      info do
+        {
+          name: raw_info['name'],
+        }
+      end
+
+      extra do
+        {'raw_info' => raw_info}
+      end
+
+      def raw_info
+        @raw_info ||= access_token.get('/api/v1/me').parsed || {}
+      end
+
+      def build_access_token
+        options.token_params[:headers] = {'Authorization' => basic_auth_header}
+        super
+      end
+
+      def basic_auth_header
+        'Basic ' + Base64.strict_encode64(
+          "#{options[:client_id]}:#{options[:client_secret]}",
+        )
+      end
+
+      MOBILE_USER_AGENTS = 'webos|ipod|iphone|mobile'
+
+      def request_phase
+        options[:client_options].authorize_url =
+          if mobile_request?
+            'https://www.reddit.com/api/v1/authorize.compact'
+          else
+            'https://www.reddit.com/api/v1/authorize'
+          end
+
+        super
+      end
+
+      def mobile_request?
+        ua = Rack::Request.new(@env).user_agent.to_s
+        ua.downcase =~ Regexp.new(MOBILE_USER_AGENTS)
+      end
+
+      def callback_url
+        options[:redirect_uri] || (full_host + script_name + callback_path)
+      end
+    end
+  end
+end