Andrew Swistak hace 5 años
padre
commit
9bd711ecec
Se han modificado 29 ficheros con 538 adiciones y 154 borrados
  1. 65 0
      app/controllers/api/auth_controller.rb
  2. 1 0
      app/controllers/concerns/cookie_based_csrf.rb
  3. 12 0
      app/controllers/concerns/user_authentication.rb
  4. 1 1
      app/controllers/omniauth_callbacks_controller.rb
  5. 81 0
      app/graphql/mutations/concerns/controller_methods.rb
  6. 23 25
      app/graphql/mutations/user/confirm_account.rb
  7. 31 34
      app/graphql/mutations/user/login.rb
  8. 5 19
      app/graphql/mutations/user/logout.rb
  9. 41 43
      app/graphql/mutations/user/sign_up.rb
  10. 23 10
      app/javascript/src/components/layout/ApplicationLayout.tsx
  11. 24 7
      app/javascript/src/components/pages/Login.tsx
  12. 30 0
      app/javascript/src/components/pages/Logout.tsx
  13. 2 2
      app/javascript/src/components/pages/Signup.tsx
  14. 25 3
      app/javascript/src/components/pages/__generated__/LoginMutation.graphql.ts
  15. 72 0
      app/javascript/src/components/pages/__generated__/LogoutMutation.graphql.ts
  16. 5 2
      app/javascript/src/context/User.tsx
  17. 3 1
      app/javascript/src/graphqlEnvironment.ts
  18. 2 0
      app/javascript/src/index.tsx
  19. 2 0
      app/views/layouts/application.html.haml
  20. 28 1
      config/application.rb
  21. 2 2
      config/initializers/secure_headers.rb
  22. 8 1
      config/webpack/environment.js
  23. 5 0
      db/migrate/20200118200407_remove_not_null_constraint_on_user_email.rb
  24. 1 1
      db/schema.graphql
  25. 2 2
      db/schema.rb
  26. 2 0
      lib/json_web_token.rb
  27. 32 0
      lib/selective_stack.rb
  28. 5 0
      spec/factories/users.rb
  29. 5 0
      spec/models/user_spec.rb

+ 65 - 0
app/controllers/api/auth_controller.rb

@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+class API::AuthController < DeviseTokenAuth::ApplicationController
+  attr_accessor :client_id, :token, :resource
+
+  def execute
+    # result = if params[:_json]
+    #  PokemonTradeSchema.multiplex(
+    #    params[:_json].map do |param|
+    #      {query: param[:query]}.merge(execute_params(param))
+    #    end,
+    #  )
+    # else
+    #  PokemonTradeSchema.execute(params[:query], execute_params(params))
+    # end
+    # render json: result unless performed?
+    result = PokemonTradeSchema.execute(
+      params[:query],
+      execute_params(params),
+    )
+
+    render json: result unless performed?
+  rescue StandardError => e
+    raise e unless Rails.env.development?
+
+    handle_error_in_development(e)
+  end
+
+  private
+
+  def execute_params(item)
+    {
+      operation_name: item[:operationName],
+      variables: ensure_hash(item[:variables]),
+      context: {controller: self},
+    }
+  end
+
+  def ensure_hash(ambiguous_param)
+    case ambiguous_param
+    when String
+      if ambiguous_param.present?
+        ensure_hash(JSON.parse(ambiguous_param))
+      else
+        {}
+      end
+    when Hash, ActionController::Parameters
+      ambiguous_param
+    when nil
+      {}
+    else
+      raise ArgumentError, "Unexpected parameter: #{ambiguous_param}"
+    end
+  end
+
+  def handle_error_in_development(err)
+    logger.error err.message
+    logger.error err.backtrace.join("\n")
+
+    render json: {
+      errors: [{message: err.message, backtrace: err.backtrace}],
+      data: {},
+    }, status: :internal_server_error
+  end
+end

+ 1 - 0
app/controllers/concerns/cookie_based_csrf.rb

@@ -12,6 +12,7 @@ module CookieBasedCsrf
     end
 
     def set_csrf_token
+      # If no JWT, reset CSRF tokens
       unless cookies[:jwt]
         cookies.delete(:_csrf_token)
         cookies.delete('x-csrf-token')

+ 12 - 0
app/controllers/concerns/user_authentication.rb

@@ -7,6 +7,8 @@ module UserAuthentication
 
   included do
     def current_user
+      return unless jwt
+
       @current_user ||=
         begin
           result = JsonWebToken.decode(jwt)
@@ -25,6 +27,16 @@ module UserAuthentication
     def logged_in?
       !current_user.nil?
     end
+
+    def login(user)
+      self.current_user = user
+    end
+
+    def logout
+      cookies.delete(:jwt)
+      cookies.delete(:_csrf_token)
+      cookies.delete('x-csrf-token')
+    end
   end
 
   private

+ 1 - 1
app/controllers/omniauth_callbacks_controller.rb

@@ -64,7 +64,7 @@ class OmniauthCallbacksController < ApplicationController
   def sign_in_and_redirect(user, *_args)
     # Ensure we have a new CSRF token now that user is signed in
     cookies.delete(:_csrf_token)
-    self.current_user = user
+    login(user)
     cookies['x-csrf-token'] = {
       value: form_authenticity_token,
       httponly: false,

+ 81 - 0
app/graphql/mutations/concerns/controller_methods.rb

@@ -0,0 +1,81 @@
+# frozen_string_literal: true
+
+module ControllerMethods
+  extend ActiveSupport::Concern
+
+  private
+
+  def raise_user_error(message)
+    # raise GraphqlDevise::UserError, message
+    raise message
+  end
+
+  def raise_user_error_list(message, errors:)
+    # raise GraphqlDevise::DetailedUserError.new(message, errors: errors)
+    raise message
+  end
+
+  def remove_resource
+    controller.resource = nil
+    controller.client_id = nil
+    controller.token = nil
+  end
+
+  def request
+    controller.request
+  end
+
+  def response
+    controller.response
+  end
+
+  def controller
+    context[:controller]
+  end
+
+  def resource_name
+    self.class.instance_variable_get(:@resource_name)
+  end
+
+  def resource_class
+    controller.send(:resource_class, resource_name)
+  end
+
+  def recoverable_enabled?
+    resource_class.devise_modules.include?(:recoverable)
+  end
+
+  def confirmable_enabled?
+    resource_class.devise_modules.include?(:confirmable)
+  end
+
+  def blacklisted_redirect_url?(redirect_url)
+    DeviseTokenAuth.redirect_whitelist && !DeviseTokenAuth::Url.whitelisted?(redirect_url)
+  end
+
+  def current_resource
+    @current_resource ||= controller.send(:set_user_by_token, resource_name)
+  end
+
+  def client
+    controller.token.client if controller.token.present?
+  end
+
+  def set_auth_headers(resource)
+    auth_headers = resource.create_new_auth_token
+    response.headers.merge!(auth_headers)
+  end
+
+  def client_and_token(token)
+    {client_id: token.client, token: token.token}
+  end
+
+  def redirect_headers(token_info, redirect_header_options)
+    controller.send(
+      :build_redirect_headers,
+      token_info.fetch(:token),
+      token_info.fetch(:client_id),
+      redirect_header_options,
+    )
+  end
+end

+ 23 - 25
app/graphql/mutations/user/confirm_account.rb

@@ -1,38 +1,36 @@
 # frozen_string_literal: true
 
 module Mutations
-  module User
-    class ConfirmAccount < Mutations::BaseMutation
-      # include ::ControllerMethods
+  class User::ConfirmAccount < Mutations::BaseMutation
+    include ControllerMethods
 
-      field :user, Types::UserType, null: true
+    field :user, Types::UserType, null: true
 
-      argument :confirmation_token, String, required: true
-      argument :redirect_url, String, required: true
+    argument :confirmation_token, String, required: true
+    argument :redirect_url, String, required: true
 
-      def resolve(confirmation_token:, redirect_url:)
-        user = User.confirm_by_token(confirmation_token)
+    def resolve(confirmation_token:, redirect_url:)
+      user = User.confirm_by_token(confirmation_token)
 
-        if user.errors.empty?
-          redirect_header_options = {account_confirmation_success: true}
+      if user.errors.empty?
+        redirect_header_options = {account_confirmation_success: true}
 
-          redirect_to_link = if controller.signed_in?(resource_name)
-            signed_in_resource.build_auth_url(
-              redirect_url,
-              redirect_headers(
-                client_and_token(controller.signed_in_resource.create_token),
-                redirect_header_options,
-              ),
-            )
-          else
-            DeviseTokenAuth::Url.generate(redirect_url, redirect_header_options)
-          end
-
-          controller.redirect_to(redirect_to_link)
-          {user: user}
+        redirect_to_link = if controller.signed_in?(resource_name)
+          signed_in_resource.build_auth_url(
+            redirect_url,
+            redirect_headers(
+              client_and_token(controller.signed_in_resource.create_token),
+              redirect_header_options,
+            ),
+          )
         else
-          raise_user_error('invalid confirmation token')
+          DeviseTokenAuth::Url.generate(redirect_url, redirect_header_options)
         end
+
+        controller.redirect_to(redirect_to_link)
+        {user: user}
+      else
+        raise_user_error('invalid confirmation token')
       end
     end
   end

+ 31 - 34
app/graphql/mutations/user/login.rb

@@ -1,54 +1,51 @@
 # frozen_string_literal: true
 
 module Mutations
-  module User
-    class Login < Mutations::BaseMutation
-      # include ::ControllerMethods
+  class User::Login < Mutations::BaseMutation
+    include ControllerMethods
 
-      field :user, Types::UserType, null: false
+    field :user, Types::UserType, null: false
 
-      argument :login, String, required: true
-      argument :password, String, required: true
+    argument :login, String, required: true
+    argument :password, String, required: true
 
-      def resolve(login:, password:)
-        user = User.find_for_database_authentication(login: login)
+    def resolve(login:, password:)
+      user = ::User.find_for_database_authentication(login: login)
 
-        if user && active_for_authentication?(user)
-          if invalid_for_authentication?(user, password)
-            raise_user_error('bad credentials')
-          end
+      if user # && active_for_authentication?(user)
+        raise_user_error('bad credentials') unless user.authenticate(password)
 
-          set_auth_headers(user)
-          controller.sign_in(:user, user, store: false, bypass: false)
+        # set_auth_headers(user)
+        context[:controller].login(user)
+        # controller.sign_in(:user, user, store: false, bypass: false)
 
-          {user: user}
-        elsif user && !active_for_authentication?(user)
-          if locked?(user)
-            raise_user_error('account locked')
-          else
-            raise_user_error('account not confirmed', email: user.email)
-          end
+        {user: user}
+      elsif user && !active_for_authentication?(user)
+        if locked?(user)
+          raise_user_error('account locked')
         else
-          raise_user_error('bad credentials given')
+          raise_user_error('account not confirmed', email: user.email)
         end
+      else
+        raise_user_error('bad credentials given')
       end
+    end
 
-      private
+    private
 
-      def invalid_for_authentication?(user, password)
-        valid_password = user.valid_password?(password)
+    def invalid_for_authentication?(user, password)
+      valid_password = user.valid_password?(password)
 
-        (user.respond_to?(:valid_for_authentication?) && !user.valid_for_authentication? { valid_password }) ||
-          !valid_password
-      end
+      (user.respond_to?(:valid_for_authentication?) && !user.valid_for_authentication? { valid_password }) ||
+        !valid_password
+    end
 
-      def active_for_authentication?(user)
-        !user.respond_to?(:active_for_authentication?) || user.active_for_authentication?
-      end
+    def active_for_authentication?(user)
+      !user.respond_to?(:active_for_authentication?) || user.active_for_authentication?
+    end
 
-      def locked?(user)
-        user.respond_to?(:locked_at) && user.locked_at
-      end
+    def locked?(user)
+      user.respond_to?(:locked_at) && user.locked_at
     end
   end
 end

+ 5 - 19
app/graphql/mutations/user/logout.rb

@@ -1,26 +1,12 @@
 # frozen_string_literal: true
 
 module Mutations
-  module User
-    class Logout < Mutations::BaseMutation
-      # include ::ControllerMethods
+  class User::Logout < Mutations::BaseMutation
+    field :success, Boolean, null: false
 
-      field :user, Types::UserType, null: false
-
-      def resolve
-        if current_user && client && current_user.tokens[client]
-          current_user.tokens.delete(client)
-          current_user.save!
-
-          remove_user
-
-          yield user if block_given?
-
-          {user: current_user}
-        else
-          raise_user_error('user not found')
-        end
-      end
+    def resolve(*)
+      context[:controller].logout
+      {success: true}
     end
   end
 end

+ 41 - 43
app/graphql/mutations/user/sign_up.rb

@@ -1,60 +1,58 @@
 # frozen_string_literal: true
 
 module Mutations
-  module User
-    class SignUp < Mutations::BaseMutation
-      # include ::ControllerMethods
+  class User::SignUp < Mutations::BaseMutation
+    include ControllerMethods
 
-      argument :username, String, required: true
-      argument :email, String, required: true
-      argument :password, String, required: true
-      argument :password_confirmation, String, required: true
-      argument :confirm_success_url, String, required: false
+    argument :username, String, required: true
+    argument :email, String, required: true
+    argument :password, String, required: true
+    argument :password_confirmation, String, required: true
+    argument :confirm_success_url, String, required: false
 
-      field :user, Types::UserType, null: false
+    field :user, Types::UserType, null: false
 
-      def resolve(confirm_success_url: nil, **attrs)
-        user = ::User.new(provider: :email, **attrs)
-        raise_user_error('failed to create user') if user.blank?
+    def resolve(confirm_success_url: nil, **attrs)
+      user = ::User.new(provider: :email, **attrs)
+      raise_user_error('failed to create user') if user.blank?
 
-        redirect_url = confirm_success_url \
-          || DeviseTokenAuth.default_confirm_success_url
+      redirect_url = confirm_success_url \
+        || DeviseTokenAuth.default_confirm_success_url
 
-        if blacklisted_redirect_url?(redirect_url)
-          raise_user_error('redirect url is not allowed')
-        end
+      if blacklisted_redirect_url?(redirect_url)
+        raise_user_error('redirect url is not allowed')
+      end
+
+      # user.skip_confirmation_notification!
 
-        # user.skip_confirmation_notification!
-
-        if user.save
-          unless user.confirmed?
-            # user.send_confirmation_instructions(
-            #  redirect_url: confirm_success_url,
-            #  template_path: ['mailer/user'],
-            # )
-          end
-
-          set_auth_headers(user) if user.active_for_authentication?
-
-          {user: user}
-        else
-          # clean_up_passwords(user)
-          raise_user_error_list(
-            'registration failed',
-            errors: user.errors.full_messages,
-          )
+      if user.save
+        unless user.confirmed?
+          # user.send_confirmation_instructions(
+          #  redirect_url: confirm_success_url,
+          #  template_path: ['mailer/user'],
+          # )
         end
-      end
 
-      private
+        set_auth_headers(user) if user.active_for_authentication?
 
-      def provider
-        :email
+        {user: user}
+      else
+        # clean_up_passwords(user)
+        raise_user_error_list(
+          'registration failed',
+          errors: user.errors.full_messages,
+        )
       end
+    end
 
-      def clean_up_passwords(user)
-        # controller.send(:clean_up_passwords, user)
-      end
+    private
+
+    def provider
+      :email
+    end
+
+    def clean_up_passwords(user)
+      # controller.send(:clean_up_passwords, user)
     end
   end
 end

+ 23 - 10
app/javascript/src/components/layout/ApplicationLayout.tsx

@@ -1,4 +1,4 @@
-import React from 'react';
+import React, {useContext} from 'react';
 import {Navbar, Nav} from 'react-bootstrap';
 import {Link} from 'found';
 
@@ -9,6 +9,8 @@ interface Props {
 }
 
 function ApplicationLayout({children}: Props): React.ReactElement {
+  const {user} = useContext(UserContext);
+
   return (
     <div>
       <nav>
@@ -23,20 +25,31 @@ function ApplicationLayout({children}: Props): React.ReactElement {
                 View Pokemon
               </Nav.Link>
             </Nav>
-            <UserContext.Consumer>
-              {user => user && user.email}
-            </UserContext.Consumer>
+            {user && user.email}
           </Navbar.Collapse>
         </Navbar>
       </nav>
 
       {children}
-      <p>
-        <Link to='/login'>Signin</Link>
-      </p>
-      <p>
-        <Link to='/signup'>Signup</Link>
-      </p>
+
+      {
+        user ? (
+          <p>
+            <Link to='/logout'>
+              Logout
+            </Link>
+          </p>
+        ) : (
+          <>
+            <p>
+              <Link to='/login'>Signin</Link>
+            </p>
+            <p>
+              <Link to='/signup'>Signup</Link>
+            </p>
+          </>
+        )
+      }
       <p>
         <Link to='/pokemon'>Show me the pokemon!</Link>
       </p>

+ 24 - 7
app/javascript/src/components/pages/Login.tsx

@@ -1,36 +1,50 @@
+import React, {useState, useContext} from 'react';
+import {useRouter} from 'found';
 import {commitMutation, graphql} from 'react-relay';
-import React, {useState} from 'react';
 import {Form, Row, Col, Button} from 'react-bootstrap';
 
-import {authEnvironment} from '../../graphqlEnvironment';
+import {UserContext} from '../../context/User';
+import graphqlEnvironment from '../../graphqlEnvironment';
+
+import {LoginMutationResponse as LoginResponse} from './__generated__/LoginMutation.graphql';
+import {User} from '../../context/User';
 
 const mutation = graphql`
   mutation LoginMutation($login: String!, $password: String!) {
     userLogin(login: $login, password: $password) {
       user {
+        iid
         username
+        email
       }
     }
   }
 `;
 
-const onSubmit = (login, password): void => {
+const onSubmit = (login, password, onCompleted): void => {
   const variables = {
     login,
     password,
   };
 
-  commitMutation(authEnvironment, {
+  commitMutation(graphqlEnvironment, {
     mutation,
     variables,
-    onCompleted: e => console.log(e),
-    onError: (err): void => console.error(err),
+    onCompleted: (e: LoginResponse) => onCompleted(e.userLogin.user),
+    onError: (err): void => console.error(err), //FIXME
   });
 };
 
 const Login: React.FC = () => {
   const [login, setLogin] = useState('');
   const [password, setPassword] = useState('');
+  const {setUser} = useContext(UserContext);
+  const {router} = useRouter();
+
+  const onLogin = (user: User): void => {
+    setUser(user);
+    router.push('/');
+  };
 
   return (
     <Form className="my-4" onSubmit={e => e.preventDefault()}>
@@ -53,7 +67,10 @@ const Login: React.FC = () => {
       </Form.Group>
       <Form.Group as={Row}>
         <Col sm={{span: 10, offset: 2}}>
-          <Button onClick={() => onSubmit(login, password)} type="submit">Login</Button>
+          <Button className="mr-2" onClick={() => onSubmit(login, password, onLogin)} type="submit">Login</Button>
+          <Button className="mx-2" href="/auth/discord"><span className="fab fa-discord" /></Button>
+          <Button className="mx-2" href="/auth/google"><span className="fab fa-google" /></Button>
+          <Button className="ml-2" href="/auth/reddit"><span className="fab fa-reddit" /></Button>
         </Col>
       </Form.Group>
     </Form>

+ 30 - 0
app/javascript/src/components/pages/Logout.tsx

@@ -0,0 +1,30 @@
+import React, {useContext} from 'react';
+import {commitMutation, graphql} from 'react-relay';
+
+import {UserContext} from '../../context/User';
+import graphqlEnvironment from '../../graphqlEnvironment';
+
+const mutation = graphql`
+  mutation LogoutMutation {
+    userLogout {
+      success
+    }
+  }
+`;
+
+const logout = (onLogout): void => {
+  commitMutation(graphqlEnvironment, {
+    mutation,
+    variables: {},
+    onCompleted: onLogout, // FIXME
+    onError: (err): void => console.error(err), //FIXME
+  });
+};
+
+const Logout: React.FC = () => {
+  const {setUser} = useContext(UserContext);
+  logout(() => setUser(null));
+  return <>You&apos;ve been logged out!</>;
+};
+
+export default Logout;

+ 2 - 2
app/javascript/src/components/pages/Signup.tsx

@@ -2,7 +2,7 @@ import {commitMutation, graphql} from 'react-relay';
 import React, {useState} from 'react';
 import {Form, Row, Col, Button} from 'react-bootstrap';
 
-import {authEnvironment} from '../../graphqlEnvironment';
+import graphqlEnvironment from '../../graphqlEnvironment';
 
 const mutation = graphql`
   mutation SignupMutation($username: String!, $email: String!, $password: String!, $passwordConfirmation: String!) {
@@ -22,7 +22,7 @@ const onSubmit = (username, email, password, passwordConfirmation): void => {
     passwordConfirmation,
   };
 
-  commitMutation(authEnvironment, {
+  commitMutation(graphqlEnvironment, {
     mutation,
     variables,
     onCompleted: () => {

+ 25 - 3
app/javascript/src/components/pages/__generated__/LoginMutation.graphql.ts

@@ -8,7 +8,9 @@ export type LoginMutationVariables = {
 export type LoginMutationResponse = {
     readonly userLogin: {
         readonly user: {
+            readonly iid: string;
             readonly username: string | null;
+            readonly email: string | null;
         };
     } | null;
 };
@@ -26,7 +28,9 @@ mutation LoginMutation(
 ) {
   userLogin(login: $login, password: $password) {
     user {
+      iid
       username
+      email
       id
     }
   }
@@ -61,11 +65,25 @@ v1 = [
   }
 ],
 v2 = {
+  "kind": "ScalarField",
+  "alias": null,
+  "name": "iid",
+  "args": null,
+  "storageKey": null
+},
+v3 = {
   "kind": "ScalarField",
   "alias": null,
   "name": "username",
   "args": null,
   "storageKey": null
+},
+v4 = {
+  "kind": "ScalarField",
+  "alias": null,
+  "name": "email",
+  "args": null,
+  "storageKey": null
 };
 return {
   "kind": "Request",
@@ -94,7 +112,9 @@ return {
             "concreteType": "User",
             "plural": false,
             "selections": [
-              (v2/*: any*/)
+              (v2/*: any*/),
+              (v3/*: any*/),
+              (v4/*: any*/)
             ]
           }
         ]
@@ -125,6 +145,8 @@ return {
             "plural": false,
             "selections": [
               (v2/*: any*/),
+              (v3/*: any*/),
+              (v4/*: any*/),
               {
                 "kind": "ScalarField",
                 "alias": null,
@@ -142,10 +164,10 @@ return {
     "operationKind": "mutation",
     "name": "LoginMutation",
     "id": null,
-    "text": "mutation LoginMutation(\n  $login: String!\n  $password: String!\n) {\n  userLogin(login: $login, password: $password) {\n    user {\n      username\n      id\n    }\n  }\n}\n",
+    "text": "mutation LoginMutation(\n  $login: String!\n  $password: String!\n) {\n  userLogin(login: $login, password: $password) {\n    user {\n      iid\n      username\n      email\n      id\n    }\n  }\n}\n",
     "metadata": {}
   }
 };
 })();
-(node as any).hash = '69f202bbc304aec0370d7c3d6a394bf3';
+(node as any).hash = 'b09cfc93f0bc74a76ff09884f6b2dfe1';
 export default node;

+ 72 - 0
app/javascript/src/components/pages/__generated__/LogoutMutation.graphql.ts

@@ -0,0 +1,72 @@
+/* tslint:disable */
+
+import { ConcreteRequest } from "relay-runtime";
+export type LogoutMutationVariables = {};
+export type LogoutMutationResponse = {
+    readonly userLogout: {
+        readonly success: boolean;
+    } | null;
+};
+export type LogoutMutation = {
+    readonly response: LogoutMutationResponse;
+    readonly variables: LogoutMutationVariables;
+};
+
+
+
+/*
+mutation LogoutMutation {
+  userLogout {
+    success
+  }
+}
+*/
+
+const node: ConcreteRequest = (function(){
+var v0 = [
+  {
+    "kind": "LinkedField",
+    "alias": null,
+    "name": "userLogout",
+    "storageKey": null,
+    "args": null,
+    "concreteType": "LogoutPayload",
+    "plural": false,
+    "selections": [
+      {
+        "kind": "ScalarField",
+        "alias": null,
+        "name": "success",
+        "args": null,
+        "storageKey": null
+      }
+    ]
+  }
+];
+return {
+  "kind": "Request",
+  "fragment": {
+    "kind": "Fragment",
+    "name": "LogoutMutation",
+    "type": "Mutation",
+    "metadata": null,
+    "argumentDefinitions": [],
+    "selections": (v0/*: any*/)
+  },
+  "operation": {
+    "kind": "Operation",
+    "name": "LogoutMutation",
+    "argumentDefinitions": [],
+    "selections": (v0/*: any*/)
+  },
+  "params": {
+    "operationKind": "mutation",
+    "name": "LogoutMutation",
+    "id": null,
+    "text": "mutation LogoutMutation {\n  userLogout {\n    success\n  }\n}\n",
+    "metadata": {}
+  }
+};
+})();
+(node as any).hash = '6e2adec762cb01ecf93e79615badc4c0';
+export default node;

+ 5 - 2
app/javascript/src/context/User.tsx

@@ -5,7 +5,10 @@ import graphqlEnvironment from '../graphqlEnvironment';
 import {User_QueryResponse as UserResponse} from './__generated__/User_Query.graphql';
 
 export type User = UserResponse['me'];
-export const UserContext = React.createContext<User | null>(null);
+type SetUser = {
+  setUser: (User) => void,
+}
+export const UserContext = React.createContext<{user: User | null} & SetUser>(null);
 
 const query = graphql`
   query User_Query {
@@ -31,5 +34,5 @@ export const UserProvider: React.FC<Props> = ({children}) => {
     })();
   }, []);
 
-  return <UserContext.Provider value={user}>{children}</UserContext.Provider>;
+  return <UserContext.Provider value={{user, setUser}}>{children}</UserContext.Provider>;
 };

+ 3 - 1
app/javascript/src/graphqlEnvironment.ts

@@ -33,6 +33,8 @@ export const fetchQuery: FetchFunction = (
     return new Promise(resolve => resolve(cachedData));
   }
 
+  const query = operation.text.replace(/\s+/g, ' ');
+
   return fetch('/api/graphql', {
     credentials: 'same-origin',
     method: 'POST',
@@ -41,7 +43,7 @@ export const fetchQuery: FetchFunction = (
       ...csrf.headers,
     },
     body: JSON.stringify({
-      query: operation.text,
+      query,
       variables,
     }),
   }).then(response => {

+ 2 - 0
app/javascript/src/index.tsx

@@ -16,6 +16,7 @@ import PokemonIndex from './components/pages/pokemon/Index';
 import PokemonCreate from './components/pages/pokemon/Create';
 import Signup from './components/pages/Signup';
 import Login from './components/pages/Login';
+import Logout from './components/pages/Logout';
 import {UserProvider} from './context/User';
 
 function App(): React.ReactElement {
@@ -40,6 +41,7 @@ function App(): React.ReactElement {
       />
 
       <Route Component={PokemonCreate} path='pokemon/create' />
+      <Route Component={Logout} path='logout' />
       <Route Component={Login} path='login' />
       <Route Component={Signup} path='signup' />
       <Route Component={NotFound} path='*' />

+ 2 - 0
app/views/layouts/application.html.haml

@@ -10,5 +10,7 @@
 
     = stylesheet_link_tag 'application'
     -#= stylesheet_pack_tag 'frontend'
+
+    %script{:crossorigin => "anonymous", :src => "https://kit.fontawesome.com/589a3d92cd.js"}
   %body
     = yield

+ 28 - 1
config/application.rb

@@ -2,7 +2,19 @@
 
 require_relative 'boot'
 
-require 'rails/all'
+require 'active_record/railtie'
+require 'active_storage/engine'
+require 'action_controller/railtie'
+require 'action_view/railtie'
+require 'action_mailer/railtie'
+require 'active_job/railtie'
+require 'action_cable/engine'
+require 'action_mailbox/engine'
+require 'action_text/engine'
+# require 'rails/test_unit/railtie'
+require 'sprockets/railtie'
+
+# require './lib/selective_stack'
 
 # Require the gems listed in Gemfile, including any gems
 # you've limited to :test, :development, or :production.
@@ -10,6 +22,21 @@ Bundler.require(*Rails.groups)
 
 module PokemonTrade
   class Application < Rails::Application
+    # We don't need sessions or anything of the like
+    # config.api_only = true
+
+    ## ...except for Cookies to securely store JWTs
+    # config.middleware.insert_after(
+    #  ActionDispatch::Callbacks,
+    #  ActionDispatch::Cookies,
+    # )
+
+    ## ...and conditionally for OmniAuth login, so selectively include it
+    # config.middleware.insert_after(
+    #  ActionDispatch::Cookies,
+    #  SelectiveStack,
+    # )
+
     # Initialize configuration defaults for originally generated Rails version.
     config.load_defaults 6.0
 

+ 2 - 2
config/initializers/secure_headers.rb

@@ -11,10 +11,10 @@ SecureHeaders::Configuration.default do |config|
   }
   config.csp = {
     # FIXME: only enable localhost for development
-    default_src: %w('self' http://localhost:3000 ws://localhost:3035 http://localhost:3035),
+    default_src: %w('self' http://localhost:3000 ws://localhost:3035 http://localhost:3035 https://kit-free.fontawesome.com),
 
     # FIXME: only enable unsafe-* for development
-    script_src: %w('self' 'unsafe-eval' 'unsafe-inline'),
+    script_src: %w('self' 'unsafe-eval' 'unsafe-inline' https://kit.fontawesome.com/589a3d92cd.js),
   }
 end
 

+ 8 - 1
config/webpack/environment.js

@@ -3,6 +3,13 @@ const PnpWebpackPlugin = require('pnp-webpack-plugin');
 
 const babelLoader = environment.loaders.get('babel');
 babelLoader.test = /\.(ts|tsx|js|jsx|mjs)?(\.erb)?$/;
-babelLoader.use.push({loader: 'ts-loader', options: PnpWebpackPlugin.tsLoaderOptions()});
+babelLoader.use.push({
+  loader: 'ts-loader',
+  options: PnpWebpackPlugin.tsLoaderOptions(),
+});
+const nodeLoader = environment.loaders.get('nodeModules');
+console.log(nodeLoader);
+console.log(nodeLoader.use[0].options);
+console.log(nodeLoader.use[0].options.presets[0][1]);
 
 module.exports = environment;

+ 5 - 0
db/migrate/20200118200407_remove_not_null_constraint_on_user_email.rb

@@ -0,0 +1,5 @@
+class RemoveNotNullConstraintOnUserEmail < ActiveRecord::Migration[6.0]
+  def change
+    change_column :users, :email, :string, null: true
+  end
+end

+ 1 - 1
db/schema.graphql

@@ -29,7 +29,7 @@ type LoginPayload {
 Autogenerated return type of Logout
 """
 type LogoutPayload {
-  user: User!
+  success: Boolean!
 }
 
 type Mutation {

+ 2 - 2
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: 2020_01_05_063306) do
+ActiveRecord::Schema.define(version: 2020_01_18_200407) do
 
   # These are extensions that must be enabled in order to support this database
   enable_extension "plpgsql"
@@ -26,7 +26,7 @@ ActiveRecord::Schema.define(version: 2020_01_05_063306) do
 
   create_table "users", force: :cascade do |t|
     t.string "username", default: "", null: false
-    t.string "email", default: "", null: false
+    t.string "email", default: ""
     t.string "password_digest", default: "", null: false
     t.string "reset_password_token"
     t.datetime "reset_password_sent_at"

+ 2 - 0
lib/json_web_token.rb

@@ -20,6 +20,8 @@ class JsonWebToken
   end
 
   def self.decode(token)
+    return if token.nil?
+
     JWT.decode(
       token,
       Rails.application.credentials.jwt_secret,

+ 32 - 0
lib/selective_stack.rb

@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+# Sessions are required for OmniAuth to work. This middleware will conditionally
+# enable sessions for the OAuth login flow by inserting middleware when
+# relevant
+# class SelectiveStack
+#  def initialize(app)
+#    @app = app
+#  end
+
+#  def call(env)
+#    if env['PATH_INFO'].start_with?('/auth/')
+#      session_stack.build(@app).call(env)
+#    else
+#      @app.call(env)
+#    end
+#  end
+
+#  private
+
+#  def session_stack
+#    @session_stack ||=
+#      ActionDispatch::MiddlewareStack.new.tap do |middleware|
+#        middleware.use(
+#          Rails.application.config.session_store,
+#          Rails.application.config.session_options,
+#        )
+#        middleware.use OmniAuth::Builder, &OmniAuthConfig
+#        middleware.use ActionDispatch::Flash
+#      end
+#  end
+# end

+ 5 - 0
spec/factories/users.rb

@@ -0,0 +1,5 @@
+FactoryBot.define do
+  factory :user do
+    
+  end
+end

+ 5 - 0
spec/models/user_spec.rb

@@ -0,0 +1,5 @@
+require 'rails_helper'
+
+RSpec.describe User, type: :model do
+  pending "add some examples to (or delete) #{__FILE__}"
+end