From 4f3d917e0615dba1452b4c6c1651675da3d5a1f1 Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Fri, 6 Dec 2019 23:30:15 -0800 Subject: [PATCH] Complete rewrite of local state management. --- client/package.json | 3 +- client/src/App/App.container.jsx | 112 +++---------- client/src/App/App.container.jsx.bak | 78 +++++++++ client/src/App/App.js | 148 +++++++++++------- client/src/App/App.js.bak | 71 +++++++++ .../components/header/header.component.jsx | 8 +- .../sign-in-form/sign-in-form.component.jsx | 6 +- .../sign-out/sign-out.component.jsx | 8 +- client/src/graphql/client.js | 34 ++++ client/src/graphql/initial-state.js | 3 +- client/src/graphql/local.queries.js | 17 ++ client/src/graphql/metadata.queries.js | 9 +- client/src/graphql/resolvers.js | 15 +- client/src/index.js | 21 ++- .../src/pages/sign-in/sign-in.container.jsx | 25 --- client/src/pages/sign-in/sign-in.page.jsx | 12 +- client/yarn.lock | 13 ++ 17 files changed, 366 insertions(+), 217 deletions(-) create mode 100644 client/src/App/App.container.jsx.bak create mode 100644 client/src/App/App.js.bak create mode 100644 client/src/graphql/client.js create mode 100644 client/src/graphql/local.queries.js delete mode 100644 client/src/pages/sign-in/sign-in.container.jsx diff --git a/client/package.json b/client/package.json index 4ff4e45df..956f12bb9 100644 --- a/client/package.json +++ b/client/package.json @@ -4,9 +4,10 @@ "private": true, "license": "UNLICENSED", "dependencies": { - "@apollo/react-hooks": "^3.1.3", "antd": "^3.26.0", "apollo-boost": "^0.4.4", + "apollo-link-context": "^1.0.19", + "apollo-link-logger": "^1.2.3", "dotenv": "^8.2.0", "firebase": "^7.5.0", "graphql": "^14.5.8", diff --git a/client/src/App/App.container.jsx b/client/src/App/App.container.jsx index da9c2acad..c20d8f247 100644 --- a/client/src/App/App.container.jsx +++ b/client/src/App/App.container.jsx @@ -1,91 +1,29 @@ -//Baselined on https://blog.hasura.io/authentication-and-authorization-using-hasura-and-firebase/ +import React from "react"; +import { Mutation, Query } from "react-apollo"; +import { GET_CURRENT_USER, SET_CURRENT_USER } from "../graphql/local.queries"; -import React, { useState, useEffect } from "react"; -import firebase from "../firebase/firebase.utils"; import App from "./App"; - -import { gql } from "apollo-boost"; -import { HttpLink } from "apollo-link-http"; -import ApolloClient from "apollo-client"; -import { InMemoryCache } from "apollo-cache-inmemory"; - import Spin from "../components/loading-spinner/loading-spinner.component"; -const UPSERT_USER = gql` - mutation upsert_user($authEmail: String!, $authToken: String!) { - insert_users( - objects: [{ email: $authEmail, authid: $authToken }] - on_conflict: { constraint: users_pkey, update_columns: [authid] } - ) { - returning { - authid - } - } - } -`; - -const SET_CURRENT_USER = gql` - mutation SetCurrentUser($user: User!) { - setCurrentUser(user: $user) @client - } -`; - -const client = new ApolloClient({ - link: new HttpLink({ - uri: process.env.REACT_APP_GRAPHQL_ENDPOINT - }), - cache: new InMemoryCache() -}); - -export default function Auth() { - const [authState, setAuthState] = useState({ status: "loading" }); - - useEffect(() => { - return firebase.auth().onAuthStateChanged(async user => { - if (user) { - console.log("Current User:", user); - - client - .mutate({ - mutation: UPSERT_USER, - variables: { authEmail: user.email, authToken: user.uid } - }) - .then(r => console.log("Successful Upsert", r)) - .catch(error => console.log("Upsert error!!!!", error)); - - const token = await user.getIdToken(); - const idTokenResult = await user.getIdTokenResult(); - const hasuraClaim = - idTokenResult.claims["https://hasura.io/jwt/claims"]; - if (hasuraClaim) { - setAuthState({ status: "in", user, token }); - } else { - // Check if refresh is required. - const metadataRef = firebase - .database() - .ref("metadata/" + user.uid + "/refreshTime"); - metadataRef.on("value", async () => { - // Force refresh to pick up the latest custom claims changes. - const token = await user.getIdToken(true); - setAuthState({ status: "in", user, token }); - }); - } - } else { - setAuthState({ status: "out" }); - } - }); - }, []); - - let content; - if (authState.status === "loading") { - content = ; - } else { - content = ( - <> - - - ); - } - - return
{content}
; -} +export default () => { + return ( + + {({ loading, error, data: { currentUser } }) => { + if (loading) return ; + if (error) return error.message; + return ( + + {setCurrentUser => ( + { + setCurrentUser({ variables: { user } }); + }} + /> + )} + + ); + }} + + ); +}; diff --git a/client/src/App/App.container.jsx.bak b/client/src/App/App.container.jsx.bak new file mode 100644 index 000000000..50aa92e44 --- /dev/null +++ b/client/src/App/App.container.jsx.bak @@ -0,0 +1,78 @@ +//Baselined on https://blog.hasura.io/authentication-and-authorization-using-hasura-and-firebase/ + +import React, { useState, useEffect } from "react"; +import firebase from "../firebase/firebase.utils"; +import App from "./App"; + +import { gql } from "apollo-boost"; +import { HttpLink } from "apollo-link-http"; +import ApolloClient from "apollo-client"; +import { InMemoryCache } from "apollo-cache-inmemory"; + +import Spin from "../components/loading-spinner/loading-spinner.component"; + +const UPSERT_USER = gql` + mutation upsert_user($authEmail: String!, $authToken: String!) { + insert_users( + objects: [{ email: $authEmail, authid: $authToken }] + on_conflict: { constraint: users_pkey, update_columns: [authid] } + ) { + returning { + authid + } + } + } +`; + +export default function Auth() { + const [authState, setAuthState] = useState({ status: "loading" }); + + useEffect(() => { + return firebase.auth().onAuthStateChanged(async user => { + if (user) { + console.log("Current User:", user); + + client + .mutate({ + mutation: UPSERT_USER, + variables: { authEmail: user.email, authToken: user.uid } + }) + .then(r => console.log("Successful Upsert", r)) + .catch(error => console.log("Upsert error!!!!", error)); + + const token = await user.getIdToken(); + const idTokenResult = await user.getIdTokenResult(); + const hasuraClaim = + idTokenResult.claims["https://hasura.io/jwt/claims"]; + if (hasuraClaim) { + setAuthState({ status: "in", user, token }); + } else { + // Check if refresh is required. + const metadataRef = firebase + .database() + .ref("metadata/" + user.uid + "/refreshTime"); + metadataRef.on("value", async () => { + // Force refresh to pick up the latest custom claims changes. + const token = await user.getIdToken(true); + setAuthState({ status: "in", user, token }); + }); + } + } else { + setAuthState({ status: "out" }); + } + }); + }, []); + + let content; + if (authState.status === "loading") { + content = ; + } else { + content = ( + <> + + + ); + } + + return
{content}
; +} diff --git a/client/src/App/App.js b/client/src/App/App.js index 0d808c328..565448016 100644 --- a/client/src/App/App.js +++ b/client/src/App/App.js @@ -1,71 +1,105 @@ import React from "react"; -import { Switch, Route, Redirect } from "react-router"; -import { ApolloProvider } from "react-apollo"; -import { HttpLink } from "apollo-link-http"; -import ApolloClient from "apollo-client"; -import { InMemoryCache } from "apollo-cache-inmemory"; -import { resolvers, typeDefs } from "../graphql/resolvers"; -import initialState from "../graphql/initial-state"; -import { gql } from "apollo-boost"; -//Styling imports +import { Switch, Route } from "react-router-dom"; +import firebase from "../firebase/firebase.utils"; + import "./App.css"; //Component Imports import LandingPage from "../pages/landing/landing.page"; import Manage from "../pages/manage/manage.page"; - import PrivateRoute from "../utils/private-route"; -import SignInContainer from "../pages/sign-in/sign-in.container"; +import SignInPage from "../pages/sign-in/sign-in.page"; import Unauthorized from "../pages/unauthorized/unauthorized.component"; -const graphqlEndpoint = process.env.REACT_APP_GRAPHQL_ENDPOINT; +import { auth } from "../firebase/firebase.utils"; -export default function App({ authState }) { - const isIn = authState.status === "in"; - const headers = isIn ? { Authorization: `Bearer ${authState.token}` } : {}; - const httpLink = new HttpLink({ - uri: graphqlEndpoint, - headers - }); - const client = new ApolloClient({ - link: httpLink, - cache: new InMemoryCache(), - resolvers, - typeDefs - }); +class App extends React.Component { + unsubscribeFromAuth = null; - //Init local state. - client.writeData({ - data: { - ...initialState, - currentUser: { - __typename: null, - email: authState.user ? authState.user.email : null, - displayName: authState.user ? authState.user.displayName : null + componentDidMount() { + const { setCurrentUser } = this.props; + + this.unsubscribeFromAuth = auth.onAuthStateChanged(async user => { + console.log("Current User:", user); + if (user) { + // client + // .mutate({ + // mutation: UPSERT_USER, + // variables: { authEmail: user.email, authToken: user.uid } + // }) + // .then(r => console.log("Successful Upsert", r)) + // .catch(error => console.log("Upsert error!!!!", error)); + + const token = await user.getIdToken(); + const idTokenResult = await user.getIdTokenResult(); + const hasuraClaim = + idTokenResult.claims["https://hasura.io/jwt/claims"]; + if (hasuraClaim) { + setCurrentUser({ + email: user.email, + displayName: user.displayName, + token + }); + } else { + // Check if refresh is required. + const metadataRef = firebase + .database() + .ref("metadata/" + user.uid + "/refreshTime"); + metadataRef.on("value", async () => { + // Force refresh to pick up the latest custom claims changes. + const token = await user.getIdToken(true); + setCurrentUser({ + email: user.email, + displayName: user.displayName, + token + }); + }); + } + + //add the bearer token to the headers. + localStorage.setItem("token", token); + } else { + setCurrentUser({ + email: null, + displayName: null, + token: null + }); + localStorage.removeItem("token"); } - } - }); + }); + } - return ( - -
{authState.status}
- - - - - authState.status == "in" ? ( - - ) : ( - - ) - } - /> - {/* */} - - -
- ); + componentWillUnmount() { + this.unsubscribeFromAuth(); + } + + render() { + return ( +
+ + + + {/* + this.props.currentUser.email != null ? ( + + ) : ( + + ) + } + /> */} + + + +
+ ); + } } + +export default App; diff --git a/client/src/App/App.js.bak b/client/src/App/App.js.bak new file mode 100644 index 000000000..0d808c328 --- /dev/null +++ b/client/src/App/App.js.bak @@ -0,0 +1,71 @@ +import React from "react"; +import { Switch, Route, Redirect } from "react-router"; +import { ApolloProvider } from "react-apollo"; +import { HttpLink } from "apollo-link-http"; +import ApolloClient from "apollo-client"; +import { InMemoryCache } from "apollo-cache-inmemory"; +import { resolvers, typeDefs } from "../graphql/resolvers"; +import initialState from "../graphql/initial-state"; +import { gql } from "apollo-boost"; +//Styling imports +import "./App.css"; + +//Component Imports +import LandingPage from "../pages/landing/landing.page"; +import Manage from "../pages/manage/manage.page"; + +import PrivateRoute from "../utils/private-route"; +import SignInContainer from "../pages/sign-in/sign-in.container"; +import Unauthorized from "../pages/unauthorized/unauthorized.component"; + +const graphqlEndpoint = process.env.REACT_APP_GRAPHQL_ENDPOINT; + +export default function App({ authState }) { + const isIn = authState.status === "in"; + const headers = isIn ? { Authorization: `Bearer ${authState.token}` } : {}; + const httpLink = new HttpLink({ + uri: graphqlEndpoint, + headers + }); + const client = new ApolloClient({ + link: httpLink, + cache: new InMemoryCache(), + resolvers, + typeDefs + }); + + //Init local state. + client.writeData({ + data: { + ...initialState, + currentUser: { + __typename: null, + email: authState.user ? authState.user.email : null, + displayName: authState.user ? authState.user.displayName : null + } + } + }); + + return ( + +
{authState.status}
+ + + + + authState.status == "in" ? ( + + ) : ( + + ) + } + /> + {/* */} + + +
+ ); +} diff --git a/client/src/components/header/header.component.jsx b/client/src/components/header/header.component.jsx index 981f3411a..47f96917a 100644 --- a/client/src/components/header/header.component.jsx +++ b/client/src/components/header/header.component.jsx @@ -1,4 +1,4 @@ -import React, { Component, useState } from "react"; +import React from "react"; import { Link } from "react-router-dom"; import { Menu, Icon } from "antd"; import "./header.styles.scss"; @@ -7,9 +7,9 @@ import SignOut from "../sign-out/sign-out.component"; export default ({ selectedNavItem, navItems }) => { const handleClick = e => { console.log("click ", e); - this.setState({ - current: e.key - }); + // this.setState({ + // current: e.key + // }); }; return ( diff --git a/client/src/components/sign-in-form/sign-in-form.component.jsx b/client/src/components/sign-in-form/sign-in-form.component.jsx index 18bda3f60..393f3a875 100644 --- a/client/src/components/sign-in-form/sign-in-form.component.jsx +++ b/client/src/components/sign-in-form/sign-in-form.component.jsx @@ -65,11 +65,7 @@ class SignInForm extends React.Component { Log in - {errorMessage ? ( - - ) : ( - null - )} + {errorMessage ? : null} ); } diff --git a/client/src/components/sign-out/sign-out.component.jsx b/client/src/components/sign-out/sign-out.component.jsx index 1f3d6c670..a9ab53f81 100644 --- a/client/src/components/sign-out/sign-out.component.jsx +++ b/client/src/components/sign-out/sign-out.component.jsx @@ -1,13 +1,11 @@ -import React, { useState } from "react"; +import React from "react"; import firebase from "../../firebase/firebase.utils"; export default function SignOut() { - const [authState, setAuthState] = useState(); - const signOut = async () => { try { - const p = await firebase.auth().signOut(); - setAuthState({ status: "out" }); + await firebase.auth().signOut(); + console.log("Signin out!"); } catch (error) { console.log(error); } diff --git a/client/src/graphql/client.js b/client/src/graphql/client.js new file mode 100644 index 000000000..926f92cc1 --- /dev/null +++ b/client/src/graphql/client.js @@ -0,0 +1,34 @@ +import ApolloClient from "apollo-client"; +import { InMemoryCache } from "apollo-cache-inmemory"; +import { HttpLink } from "apollo-link-http"; +import { setContext } from "apollo-link-context"; +import { resolvers, typeDefs } from "./resolvers"; +import apolloLogger from "apollo-link-logger"; +import { ApolloLink } from "apollo-boost"; + +const httpLink = new HttpLink({ + uri: process.env.REACT_APP_GRAPHQL_ENDPOINT +}); + +const authLink = setContext((_, { headers }) => { + // get the authentication token from local storage if it exists + const token = localStorage.getItem("token"); + // return the headers to the context so httpLink can read them + if (token) { + return { + headers: { + ...headers, + authorization: token ? `Bearer ${token}` : "" + } + }; + } else { + return { headers }; + } +}); + +export const client = new ApolloClient({ + link: authLink.concat(httpLink), + cache: new InMemoryCache({ addTypename: false }), + typeDefs, + resolvers +}); diff --git a/client/src/graphql/initial-state.js b/client/src/graphql/initial-state.js index 513254aeb..9e117e1ae 100644 --- a/client/src/graphql/initial-state.js +++ b/client/src/graphql/initial-state.js @@ -1,7 +1,8 @@ export default { currentUser: { email: null, - displayName: null + displayName: null, + token: null }, selectedNavItem: "Home", recentItems: [] diff --git a/client/src/graphql/local.queries.js b/client/src/graphql/local.queries.js new file mode 100644 index 000000000..84bb26e6b --- /dev/null +++ b/client/src/graphql/local.queries.js @@ -0,0 +1,17 @@ +import { gql } from "apollo-boost"; + +export const SET_CURRENT_USER = gql` + mutation SetCurrentUser($user: User!) { + setCurrentUser(user: $user) @client { + email + } + } +`; + +export const GET_CURRENT_USER = gql` + { + currentUser @client { + email + } + } +`; diff --git a/client/src/graphql/metadata.queries.js b/client/src/graphql/metadata.queries.js index 12cefb86c..788dbe049 100644 --- a/client/src/graphql/metadata.queries.js +++ b/client/src/graphql/metadata.queries.js @@ -21,14 +21,7 @@ export const GET_SELECTED_NAV_ITEM = gql` } `; -export const GET_CURRENT_USER = gql` - query GetCurrentUser { - currentUser @client { - email - displayName - } - } -`; + // export const SET_CURRENT_USER = gql` // mutation SetCurrentUser($user User!){ diff --git a/client/src/graphql/resolvers.js b/client/src/graphql/resolvers.js index d961016d1..c6553ec60 100644 --- a/client/src/graphql/resolvers.js +++ b/client/src/graphql/resolvers.js @@ -1,20 +1,15 @@ import { gql } from "apollo-boost"; +import { GET_CURRENT_USER } from "./local.queries"; export const typeDefs = gql` extend type Mutation { SetCurrentUser(user: User!): User! } - type User { - displayName: String!, - email: String!, - photoUrl: String! - } -`; - -const GET_CURRENT_USER = gql` - { - currentUser @client + extend type User { + email: String! + displayName: String! + token: String! } `; diff --git a/client/src/index.js b/client/src/index.js index 459f19bc1..4de187462 100644 --- a/client/src/index.js +++ b/client/src/index.js @@ -5,14 +5,27 @@ import { BrowserRouter } from "react-router-dom"; import * as serviceWorker from "./serviceWorker"; import "./index.css"; -import { default as App } from "./App/App.container"; +import AppContainer from "./App/App.container"; + +import { ApolloProvider } from "react-apollo"; +import { client } from "./graphql/client"; +import initialState from "./graphql/initial-state"; require("dotenv").config(); +//Init local state. +client.writeData({ + data: { + ...initialState + } +}); + ReactDOM.render( - - - , + + + + + , document.getElementById("root") ); diff --git a/client/src/pages/sign-in/sign-in.container.jsx b/client/src/pages/sign-in/sign-in.container.jsx deleted file mode 100644 index 77adbb00f..000000000 --- a/client/src/pages/sign-in/sign-in.container.jsx +++ /dev/null @@ -1,25 +0,0 @@ -import React from "react"; -import { Query } from "react-apollo"; - -import { Alert } from "antd"; - -import Spin from "../../components/loading-spinner/loading-spinner.component"; -// import Skeleton from "../loading-skeleton/loading-skeleton.component"; - -import { GET_CURRENT_USER } from "../../graphql/metadata.queries"; -import SignInPage from "./sign-in.page"; - -const SignInContainer = () => { - return ( - - {({ loading, error, data: { currentUser } }) => { - if (loading) return ; - if (error) return ; - - return ; - }} - - ); -}; - -export default SignInContainer; diff --git a/client/src/pages/sign-in/sign-in.page.jsx b/client/src/pages/sign-in/sign-in.page.jsx index a58e0b408..a1f8c202c 100644 --- a/client/src/pages/sign-in/sign-in.page.jsx +++ b/client/src/pages/sign-in/sign-in.page.jsx @@ -1,14 +1,6 @@ import React from "react"; import SignInComponent from "../../components/sign-in-form/sign-in-form.component"; -import { Redirect } from "react-router-dom"; - - -export default ({signedIn}) => { - console.log(signedIn) - return ( -
- -
- ); +export default () => { + return ; }; diff --git a/client/yarn.lock b/client/yarn.lock index 1f630b970..74bfd73cd 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -2189,6 +2189,14 @@ apollo-client@^2.6.4: tslib "^1.9.3" zen-observable "^0.8.0" +apollo-link-context@^1.0.19: + version "1.0.19" + resolved "https://registry.yarnpkg.com/apollo-link-context/-/apollo-link-context-1.0.19.tgz#3c9ba5bf75ed5428567ce057b8837ef874a58987" + integrity sha512-TUi5TyufU84hEiGkpt+5gdH5HkB3Gx46npNfoxR4of3DKBCMuItGERt36RCaryGcU/C3u2zsICU3tJ+Z9LjFoQ== + dependencies: + apollo-link "^1.2.13" + tslib "^1.9.3" + apollo-link-error@^1.0.3: version "1.1.12" resolved "https://registry.yarnpkg.com/apollo-link-error/-/apollo-link-error-1.1.12.tgz#e24487bb3c30af0654047611cda87038afbacbf9" @@ -2216,6 +2224,11 @@ apollo-link-http@^1.3.1: apollo-link-http-common "^0.2.15" tslib "^1.9.3" +apollo-link-logger@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/apollo-link-logger/-/apollo-link-logger-1.2.3.tgz#1f3e6f7849ce7a7e3aa822141fe062cfa278b1e1" + integrity sha512-GaVwdHyXmawfvBlHfZkFkBHH3+YH7wibzSCc4/YpIbPVtbtZqi0Qop18w++jgpw385W083DMOdYe2eJsKkZdag== + apollo-link@^1.0.0, apollo-link@^1.0.6, apollo-link@^1.2.13: version "1.2.13" resolved "https://registry.yarnpkg.com/apollo-link/-/apollo-link-1.2.13.tgz#dff00fbf19dfcd90fddbc14b6a3f9a771acac6c4"