├── .editorconfig ├── .eslintrc.json ├── .gitignore ├── .prettierignore ├── .prettierrc ├── LICENSE ├── README.md ├── apollo ├── client.js ├── createPossibleTypes.js └── possibleTypes.json ├── backup ├── AuthContext.js ├── AuthProvider.js ├── login.js └── signup.js ├── gatsby-browser.js ├── gatsby-config.js ├── gatsby-node.js ├── gatsby-ssr.js ├── package-lock.json ├── package.json ├── src ├── components │ ├── Header.js │ ├── Layout.css │ ├── Layout.js │ ├── LoginForm.js │ ├── Navigation.js │ ├── PrivateRoute.js │ ├── ProgressLoader.css │ ├── ProgressLoader.js │ ├── Seo.js │ └── SignUpForm.js ├── dashboard │ ├── PageNotFound.js │ ├── UserArea.js │ └── index.js ├── hooks │ ├── useAuth.js │ ├── useFormFields.js │ ├── useLocalStorage.js │ ├── useNetwork.js │ └── useUser.js ├── images │ └── gatsby-icon.png ├── pages │ ├── 404.js │ ├── dashboard.js │ └── index.js └── services │ └── auth.js ├── wrapRootElement.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 2 8 | indent_style = space 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | max_line_length = 120 12 | 13 | [*.md] 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": [ 4 | "google", 5 | "eslint:recommended", 6 | "plugin:flowtype/recommended", 7 | "plugin:react/recommended", 8 | "prettier", 9 | "prettier/flowtype", 10 | "prettier/react" 11 | ], 12 | "plugins": ["flowtype", "prettier", "react", "filenames"], 13 | "parserOptions": { 14 | "ecmaVersion": 2016, 15 | "sourceType": "module", 16 | "ecmaFeatures": { 17 | "jsx": true 18 | } 19 | }, 20 | "env": { 21 | "browser": true, 22 | "es6": true, 23 | "node": true, 24 | "jest": true 25 | }, 26 | "globals": { 27 | "before": true, 28 | "after": true, 29 | "spyOn": true, 30 | "__PATH_PREFIX__": true, 31 | "__BASE_PATH__": true, 32 | "__ASSET_PREFIX__": true 33 | }, 34 | "rules": { 35 | "arrow-body-style": ["error", "as-needed", { "requireReturnForObjectLiteral": true }], 36 | "no-unused-expressions": [ 37 | "error", 38 | { 39 | "allowTaggedTemplates": true 40 | } 41 | ], 42 | "consistent-return": ["error"], 43 | "filenames/match-regex": ["error", "^[a-z-\\d\\.]+$", true], 44 | "no-console": "off", 45 | "no-inner-declarations": "off", 46 | "prettier/prettier": "error", 47 | "quotes": ["error", "backtick"], 48 | "react/display-name": "off", 49 | "react/jsx-key": "warn", 50 | "react/no-unescaped-entities": "off", 51 | "react/prop-types": "off", 52 | "require-jsdoc": "off", 53 | "valid-jsdoc": "off" 54 | }, 55 | "overrides": [ 56 | { 57 | "files": ["packages/**/gatsby-browser.js", "packages/gatsby/cache-dir/**/*"], 58 | "env": { 59 | "browser": true 60 | }, 61 | "globals": { 62 | "___loader": false, 63 | "___emitter": false 64 | } 65 | }, 66 | { 67 | "files": ["**/cypress/integration/**/*", "**/cypress/support/**/*"], 68 | "globals": { 69 | "cy": false, 70 | "Cypress": false 71 | } 72 | } 73 | ], 74 | "settings": { 75 | "react": { 76 | "version": "16.4.2" 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Environment Vars 2 | .env.development 3 | .env.production 4 | 5 | 6 | # Apollo GraphQL IntelliJ plugin 7 | 8 | .graphqlconfig 9 | apollo/schema.graphql 10 | 11 | # IDEs 12 | .idea 13 | 14 | # Logs 15 | logs 16 | *.log 17 | npm-debug.log* 18 | yarn-debug.log* 19 | yarn-error.log* 20 | 21 | # Runtime data 22 | pids 23 | *.pid 24 | *.seed 25 | *.pid.lock 26 | 27 | # Directory for instrumented libs generated by jscoverage/JSCover 28 | lib-cov 29 | 30 | # Coverage directory used by tools like istanbul 31 | coverage 32 | 33 | # nyc test coverage 34 | .nyc_output 35 | 36 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 37 | .grunt 38 | 39 | # Bower dependency directory (https://bower.io/) 40 | bower_components 41 | 42 | # node-waf configuration 43 | .lock-wscript 44 | 45 | # Compiled binary addons (http://nodejs.org/api/addons.html) 46 | build/Release 47 | 48 | # Dependency directories 49 | node_modules/ 50 | jspm_packages/ 51 | 52 | # Typescript v1 declaration files 53 | typings/ 54 | 55 | # Optional npm cache directory 56 | .npm 57 | 58 | # Optional eslint cache 59 | .eslintcache 60 | 61 | # Optional REPL history 62 | .node_repl_history 63 | 64 | # Output of 'npm pack' 65 | *.tgz 66 | 67 | # dotenv environment variable files 68 | .env* 69 | 70 | # gatsby files 71 | .cache/ 72 | public 73 | 74 | # Mac files 75 | .DS_Store 76 | 77 | # Yarn 78 | yarn-error.log 79 | .pnp/ 80 | .pnp.js 81 | # Yarn Integrity file 82 | .yarn-integrity 83 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .cache 2 | package.json 3 | package-lock.json 4 | public 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "endOfLine": "lf", 3 | "semi": false, 4 | "singleQuote": false, 5 | "tabWidth": 2, 6 | "trailingComma": "es5" 7 | } 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 NeverNull 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gatsby Apollo WPGraphQL JWT Starter 2 | 3 | This project aims to serve as a good starting point, to handle user registration and login with Apollo, WPGraphQL and WPGraphQL JWT Authentication. 4 | 5 | We gonna use the following libraries for now: 6 | 7 | - [WPGraphQL](https://github.com/wp-graphql/wp-graphql) - [[Docs](https://docs.wpgraphql.com/)] 8 | - [WPGraphQL JWT Authentication](https://github.com/wp-graphql/wp-graphql-jwt-authentication) - [[Docs](https://docs.wpgraphql.com/extensions/wpgraphql-jwt-authentication/)] 9 | - [Apollo Client 3](https://github.com/apollographql/apollo-client/tree/master) - `3.0.0-beta.31` - [[Docs](https://www.apollographql.com/docs/react/v3.0-beta)] 10 | 11 | 12 | It should work now with the latest versions of WPGraphQL. Apollo v3 might change from beta to a stable release. 13 | 14 | ## 🚀 Quick start 15 | 16 | ### WordPress 17 | 18 | 1. **Install plugins** 19 | 20 | Download the .zip files and install through the WordPress Admin or if you can run git, just run the following commands inside your `./wp-content/plugins/` folder: 21 | 22 | ``` 23 | git clone https://github.com/wp-graphql/wp-graphql.git 24 | git clone https://github.com/wp-graphql/wp-graphql-jwt-authentication.git 25 | ``` 26 | 27 | 2. **Check your permalinks** 28 | 29 | Make sure your graphql endpoint works as expected. 30 | See these docs: https://docs.wpgraphql.com/getting-started/install-and-activate/#verify-the-endpoint-works 31 | 32 | 3. **Define a secret** 33 | In your wp-config.php deinfe a secret. You can use WordPress Salt generator (https://api.wordpress.org/secret-key/1.1/salt/) to generate a Secret. 34 | ``` 35 | define( 'GRAPHQL_JWT_AUTH_SECRET_KEY', 'your-secret-token' ); 36 | ``` 37 | 38 | ### Gatsby 39 | 40 | 1. **Install modules** 41 | 42 | Run yarn to install packages. Also after the modules are installed it should run `createPossibleType.js` automatically on `postinstall`. 43 | 44 | ```shell 45 | yarn 46 | ``` 47 | 48 | Check if the file `./apollo/possibleTypes.json` has been created. 49 | 50 | 2. **Add .env.development** 51 | 52 | There is an `.env.development.example` which you can use and rename. Make sure you have a `.env.development` in your root folder. 53 | 54 | ```dotenv 55 | GRAPHQL_URL=http://your-domain/graphql 56 | ``` 57 | 58 | If you run `yarn run build` you need a `.env.production` in you root folder. Or you run it in your CI wit CI-Variables. 59 | 60 | 3. **Start developing** 61 | 62 | Navigate into your new site’s directory and start it up. 63 | 64 | ```shell 65 | yarn run develop 66 | ``` 67 | 68 | or 69 | 70 | ```shell 71 | yarn run cdev 72 | ``` 73 | 74 | `cdev` runs `gatsby clean` before running develop 75 | 76 | 4. **Open the source code and start editing!** 77 | 78 | Your site is now running at `http://localhost:8000`! 79 | 80 | _Note: You'll also see a second link: _`http://localhost:8000/___graphql`_. This is a tool you can use to experiment with querying your data. Learn more about using this tool in the [Gatsby tutorial](https://www.gatsbyjs.org/tutorial/part-five/#introducing-graphiql)._ 81 | 82 | Open the `my-default-starter` directory in your code editor of choice and edit `src/pages/index.js`. Save your changes and the browser will update in real time! 83 | 84 | 85 | -------------------------------------------------------------------------------- /apollo/client.js: -------------------------------------------------------------------------------- 1 | import { ApolloClient, ApolloLink, from, HttpLink, InMemoryCache } from "@apollo/client" 2 | import { TokenRefreshLink } from "apollo-link-token-refresh" 3 | import { onError } from "apollo-link-error" 4 | import fetch from "isomorphic-fetch" 5 | import uuid from "uuid" 6 | 7 | import possibleTypes from "./possibleTypes.json" 8 | import { 9 | deleteRefreshToken, 10 | getInMemoryAuthToken, 11 | getRefreshToken, 12 | isTokenExpired, 13 | logout, 14 | setAuthToken, 15 | } from "../src/services/auth" 16 | import { navigate } from "gatsby" 17 | 18 | 19 | const httpLink = new HttpLink( 20 | { 21 | uri: process.env.GRAPHQL_URL, 22 | fetch, 23 | // credentials: 'include', 24 | } 25 | ) 26 | 27 | const authMiddleware = new ApolloLink((operation, forward) => { 28 | // get the authentication token from local storage if it exists 29 | const token = getInMemoryAuthToken() 30 | 31 | operation.setContext({ 32 | headers: { 33 | Authorization: token ? `Bearer ${token}` : '', 34 | }, 35 | }) 36 | 37 | return forward(operation) 38 | }) 39 | 40 | const refreshTokenLink = new TokenRefreshLink({ 41 | accessTokenField: `refreshJwtAuthToken`, 42 | isTokenValidOrUndefined: () => { 43 | const token = getInMemoryAuthToken() 44 | return !token || (token && !isTokenExpired(token)) 45 | }, 46 | fetchAccessToken: () => { 47 | console.log("refreshTokenLink") 48 | // TODO: Check if refreshJwtAuthToken can return authExpiration 49 | const query = ` 50 | mutation RefreshJWTAuthToken($input: RefreshJwtAuthTokenInput!) { 51 | refreshJwtAuthToken(input: $input) { 52 | authToken 53 | } 54 | } 55 | ` 56 | return fetch(process.env.GRAPHQL_URL, { 57 | method: "POST", 58 | mode: "cors", 59 | // credentials: 'include', 60 | headers: { 61 | Accept: "application/json", 62 | "Content-Type": "application/json", 63 | }, 64 | body: JSON.stringify({ 65 | query, 66 | variables: { 67 | input: { 68 | clientMutationId: uuid(), 69 | jwtRefreshToken: getRefreshToken(), 70 | }, 71 | }, 72 | }), 73 | }) 74 | }, 75 | handleFetch: response => { 76 | if (response.errors && response.errors.length) return 77 | console.log("HandleFetch", response) 78 | setAuthToken(response.refreshJwtAuthToken.authToken) 79 | }, 80 | // handleResponse: (operation, accessTokenField) => response => { 81 | // console.log("HandleResponse:", response) 82 | // }, 83 | handleError: err => { 84 | console.error(err) 85 | deleteRefreshToken() 86 | }, 87 | }) 88 | 89 | const onErrorLink = onError(({ graphQLErrors, networkError }) => { 90 | if (graphQLErrors) { 91 | graphQLErrors.forEach(({ message, locations, path, extensions }) => { 92 | if (extensions && extensions.code === "invalid-jwt") { 93 | logout(() => navigate("/dashboard/")) 94 | } 95 | console.log(`[GraphQL error]:`, {Message: message, Location: locations, Path: path, Extension: extensions}) 96 | }) 97 | } 98 | 99 | if (networkError) { 100 | console.log(`[Network error]: ${networkError}`) 101 | } 102 | }) 103 | 104 | export const client = new ApolloClient({ 105 | link: from([ 106 | authMiddleware, 107 | onErrorLink, 108 | refreshTokenLink, 109 | httpLink 110 | ]), 111 | cache: new InMemoryCache({ possibleTypes }), 112 | }) 113 | -------------------------------------------------------------------------------- /apollo/createPossibleTypes.js: -------------------------------------------------------------------------------- 1 | let activeEnv = 2 | process.env.GATSBY_ACTIVE_ENV || process.env.NODE_ENV || "development" 3 | 4 | require("dotenv").config({ 5 | path: `.env.${activeEnv}`, 6 | }) 7 | 8 | const fetch = require('isomorphic-fetch'); 9 | const fs = require('fs'); 10 | 11 | const createPossibleTypes = () => { 12 | return new Promise((resolve) => { 13 | fetch(process.env.GRAPHQL_URL, { 14 | method: 'POST', 15 | headers: { 'Content-Type': 'application/json' }, 16 | body: JSON.stringify({ 17 | variables: {}, 18 | query: ` 19 | { 20 | __schema { 21 | types { 22 | kind 23 | name 24 | possibleTypes { 25 | name 26 | } 27 | } 28 | } 29 | } 30 | `, 31 | }), 32 | }).then(result => result.json()) 33 | .then(result => { 34 | const possibleTypes = {}; 35 | 36 | result.data.__schema.types.forEach(supertype => { 37 | if (supertype.possibleTypes) { 38 | possibleTypes[supertype.name] = 39 | supertype.possibleTypes.map(subtype => subtype.name); 40 | } 41 | }); 42 | 43 | fs.writeFile('./apollo/possibleTypes.json', JSON.stringify(possibleTypes), err => { 44 | if (err) { 45 | console.error('Error writing possibleTypes.json', err); 46 | } else { 47 | console.log('Fragment types successfully extracted!'); 48 | resolve() 49 | } 50 | }); 51 | }); 52 | }) 53 | } 54 | 55 | createPossibleTypes() 56 | -------------------------------------------------------------------------------- /apollo/possibleTypes.json: -------------------------------------------------------------------------------- 1 | {"Node":["Category","Taxonomy","PostType","Post","Page","User","Comment","CommentAuthor","MediaItem","Tag","UserRole","Menu","MenuItem","Plugin","Theme"],"PostObjectUnion":["Post","Page","MediaItem"],"CommentAuthorUnion":["User","CommentAuthor"],"TermObjectUnion":["Category","Tag"],"ContentRevisionUnion":["Post","Page"],"MenuItemObjectUnion":["Post","Page","Category","Tag","MenuItem"]} -------------------------------------------------------------------------------- /backup/AuthContext.js: -------------------------------------------------------------------------------- 1 | import {createContext} from 'react'; 2 | 3 | export const AuthContext = createContext(null); 4 | -------------------------------------------------------------------------------- /backup/AuthProvider.js: -------------------------------------------------------------------------------- 1 | import React, { useMemo, useState } from "react" 2 | import { AuthContext } from "./AuthContext" 3 | import { gql, useMutation } from "@apollo/client" 4 | import { setRefreshToken } from "../src/services/auth" 5 | import { navigate } from "gatsby" 6 | import uuid from "uuid" 7 | 8 | 9 | const LOGIN_USER = gql` 10 | mutation LoginUser($input: LoginInput!) { 11 | login(input: $input) { 12 | user { 13 | jwtAuthToken 14 | jwtRefreshToken 15 | jwtAuthExpiration 16 | username 17 | nicename 18 | } 19 | } 20 | } 21 | ` 22 | 23 | export const AuthProvider = ({ children }) => { 24 | const [loginUser, { data: loginData }] = useMutation(LOGIN_USER) 25 | const [userData, setUserData] = useState(null) 26 | 27 | const providerValue = useMemo(() => ( 28 | { 29 | loginUser: ({ username, password }) => loginUser({ 30 | variables: { 31 | input: { 32 | clientMutationId: uuid(), 33 | username: username, 34 | password: password, 35 | }, 36 | }, 37 | }).then((response) => { 38 | console.log("Response", response) 39 | // console.log("Data", data) 40 | const { login } = response.data 41 | const user = (login && login.user) ? login.user : {} 42 | 43 | setRefreshToken(user, () => navigate("/dashboard/")) 44 | }), 45 | loginData: loginData, 46 | userData: userData, 47 | setUserData: setUserData 48 | } 49 | ), [loginUser, loginData, userData, setUserData]) 50 | 51 | 52 | return ( 53 | 54 | {children} 55 | 56 | ) 57 | } 58 | -------------------------------------------------------------------------------- /backup/login.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | 3 | import Layout from "../src/components/Layout" 4 | import Seo from "../src/components/Seo" 5 | import LoginForm from "../src/components/LoginForm" 6 | import { navigate } from "gatsby" 7 | import { useAuth } from "../src/hooks/useAuth" 8 | 9 | const Login = () => { 10 | const auth = useAuth(); 11 | 12 | if (auth.isLoggedIn()) { 13 | navigate(`/dashboard/`, {replace: true}) 14 | return null 15 | } 16 | 17 | return ( 18 | 19 | 20 |

Login

21 | 22 |
23 | ) 24 | } 25 | 26 | export default Login 27 | -------------------------------------------------------------------------------- /backup/signup.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | 3 | import Seo from "../src/components/Seo" 4 | import SignUpForm from "../src/components/SignUpForm" 5 | import Layout from "../src/components/Layout" 6 | import { navigate } from "gatsby" 7 | import { useAuth } from "../src/hooks/useAuth" 8 | 9 | const SignUp = () => { 10 | const auth = useAuth(); 11 | 12 | if (auth.isLoggedIn()) { 13 | navigate(`/dashboard/`, {replace: true}) 14 | return null 15 | } 16 | 17 | return ( 18 | 19 | 20 |

Sign Up

21 | 22 |
23 | ) 24 | } 25 | 26 | export default SignUp 27 | -------------------------------------------------------------------------------- /gatsby-browser.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Implement Gatsby's Browser APIs in this file. 3 | * 4 | * See: https://www.gatsbyjs.org/docs/browser-apis/ 5 | */ 6 | 7 | // You can delete this file if you're not using it 8 | export {wrapRootElement} from './wrapRootElement'; 9 | -------------------------------------------------------------------------------- /gatsby-config.js: -------------------------------------------------------------------------------- 1 | let activeEnv = 2 | process.env.GATSBY_ACTIVE_ENV || process.env.NODE_ENV || "development" 3 | 4 | console.log(`Using environment config: '${activeEnv}'`) 5 | 6 | require("dotenv").config({ 7 | path: `.env.${activeEnv}`, 8 | }) 9 | 10 | module.exports = { 11 | siteMetadata: { 12 | title: `Gatsby Apollo WPGraphQL JWT Starter`, 13 | description: `This project aims to serve as a good starting point, to handle user registration and login with Apollo, WPGraphQL and WPGraphQL JWT Authentication.`, 14 | author: `@NeverNull`, 15 | }, 16 | plugins: [ 17 | `gatsby-plugin-react-helmet`, 18 | { 19 | resolve: `gatsby-source-filesystem`, 20 | options: { 21 | name: `images`, 22 | path: `${__dirname}/src/images`, 23 | }, 24 | }, 25 | `gatsby-transformer-sharp`, 26 | `gatsby-plugin-sharp`, 27 | { 28 | resolve: `gatsby-plugin-manifest`, 29 | options: { 30 | name: `gatsby-starter-default`, 31 | short_name: `starter`, 32 | start_url: `/`, 33 | background_color: `#663399`, 34 | theme_color: `#663399`, 35 | display: `minimal-ui`, 36 | icon: `src/images/gatsby-icon.png`, // This path is relative to the root of the site. 37 | }, 38 | }, 39 | { 40 | resolve: "gatsby-source-graphql", 41 | options: { 42 | typeName: "WPGraphQL", 43 | fieldName: "wpgraphql", 44 | url: `${process.env.GRAPHQL_URL}`, 45 | refetchInterval: 60, 46 | }, 47 | }, 48 | ], 49 | } 50 | -------------------------------------------------------------------------------- /gatsby-node.js: -------------------------------------------------------------------------------- 1 | 2 | exports.onCreatePage = async ({ page, actions }) => { 3 | const { createPage } = actions 4 | 5 | // Only update the `/app` page. 6 | if (page.path.match(/^\/dashboard/)) { 7 | // page.matchPath is a special key that's used for matching pages 8 | // with corresponding routes only on the client. 9 | page.matchPath = "/dashboard/*" 10 | 11 | // Update the page. 12 | await createPage(page) 13 | } 14 | } 15 | 16 | -------------------------------------------------------------------------------- /gatsby-ssr.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Implement Gatsby's SSR (Server Side Rendering) APIs in this file. 3 | * 4 | * See: https://www.gatsbyjs.org/docs/ssr-apis/ 5 | */ 6 | 7 | // You can delete this file if you're not using it 8 | export {wrapRootElement} from './wrapRootElement'; 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gatsby-apollo-wpgraphql-jwt-starter", 3 | "private": true, 4 | "description": "This project aims to serve as a good starting point, to handle user registration and login with Apollo, WPGraphQL and WPGraphQL JWT Authentication.", 5 | "version": "0.1.0", 6 | "author": { 7 | "name": "NeverNull GmbH", 8 | "email": "info@nevernull.io", 9 | "url": "https://www.nevernull.io/" 10 | }, 11 | "homepage": "https://github.com/NeverNull/gatsby-apollo-wpgraphql-jwt-starter#readme", 12 | "dependencies": { 13 | "@apollo/client": "^3.0.0-beta.31", 14 | "@reach/router": "^1.2.1", 15 | "apollo-link-error": "^1.1.12", 16 | "apollo-link-token-refresh": "^0.2.7", 17 | "dotenv": "^8.2.0", 18 | "gatsby": "^2.19.7", 19 | "gatsby-image": "^2.2.39", 20 | "gatsby-plugin-manifest": "^2.2.39", 21 | "gatsby-plugin-offline": "^3.0.32", 22 | "gatsby-plugin-react-helmet": "^3.1.21", 23 | "gatsby-plugin-sharp": "^2.4.3", 24 | "gatsby-source-filesystem": "^2.1.46", 25 | "gatsby-source-graphql": "^2.1.32", 26 | "gatsby-transformer-sharp": "^2.3.13", 27 | "isomorphic-fetch": "^2.2.1", 28 | "js-cookie": "^2.2.1", 29 | "jwt-decode": "^2.2.0", 30 | "prop-types": "^15.7.2", 31 | "react": "^16.12.0", 32 | "react-dom": "^16.12.0", 33 | "react-helmet": "^5.2.1", 34 | "uuid": "^3.4.0" 35 | }, 36 | "devDependencies": { 37 | "prettier": "^1.19.1" 38 | }, 39 | "keywords": [ 40 | "gatsby" 41 | ], 42 | "license": "MIT", 43 | "scripts": { 44 | "build": "gatsby build", 45 | "develop": "gatsby develop", 46 | "develop:expose": "gatsby develop -H 0.0.0.0", 47 | "cdev": "npm run clean && npm run develop", 48 | "cdev:expose": "npm run clean && npm run develop:expose", 49 | "createTypes": "node ./apollo/createPossibleTypes.js", 50 | "postinstall": "npm run createTypes", 51 | "format": "prettier --write \"**/*.{js,jsx,json,md}\"", 52 | "start": "npm run develop", 53 | "serve": "gatsby serve", 54 | "clean": "gatsby clean", 55 | "test": "echo \"Write tests! -> https://gatsby.dev/unit-testing\" && exit 1" 56 | }, 57 | "repository": { 58 | "type": "git", 59 | "url": "https://github.com/gatsbyjs/gatsby-starter-default" 60 | }, 61 | "bugs": { 62 | "url": "https://github.com/gatsbyjs/gatsby/issues" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/components/Header.js: -------------------------------------------------------------------------------- 1 | import { Link } from "gatsby" 2 | import PropTypes from "prop-types" 3 | import React from "react" 4 | 5 | const Header = ({ siteTitle }) => ( 6 |
12 |
19 |

20 | 27 | {siteTitle} 28 | 29 |

30 |
31 |
32 | ) 33 | 34 | Header.propTypes = { 35 | siteTitle: PropTypes.string, 36 | } 37 | 38 | Header.defaultProps = { 39 | siteTitle: ``, 40 | } 41 | 42 | export default Header 43 | -------------------------------------------------------------------------------- /src/components/Layout.css: -------------------------------------------------------------------------------- 1 | html { 2 | font-family: sans-serif; 3 | -ms-text-size-adjust: 100%; 4 | -webkit-text-size-adjust: 100%; 5 | } 6 | body { 7 | margin: 0; 8 | -webkit-font-smoothing: antialiased; 9 | -moz-osx-font-smoothing: grayscale; 10 | } 11 | article, 12 | aside, 13 | details, 14 | figcaption, 15 | figure, 16 | footer, 17 | header, 18 | main, 19 | menu, 20 | nav, 21 | section, 22 | summary { 23 | display: block; 24 | } 25 | audio, 26 | canvas, 27 | progress, 28 | video { 29 | display: inline-block; 30 | } 31 | audio:not([controls]) { 32 | display: none; 33 | height: 0; 34 | } 35 | progress { 36 | vertical-align: baseline; 37 | } 38 | [hidden], 39 | template { 40 | display: none; 41 | } 42 | a { 43 | background-color: transparent; 44 | -webkit-text-decoration-skip: objects; 45 | } 46 | a:active, 47 | a:hover { 48 | outline-width: 0; 49 | } 50 | abbr[title] { 51 | border-bottom: none; 52 | text-decoration: underline; 53 | text-decoration: underline dotted; 54 | } 55 | b, 56 | strong { 57 | font-weight: inherit; 58 | font-weight: bolder; 59 | } 60 | dfn { 61 | font-style: italic; 62 | } 63 | h1 { 64 | font-size: 2em; 65 | margin: 0.67em 0; 66 | } 67 | mark { 68 | background-color: #ff0; 69 | color: #000; 70 | } 71 | small { 72 | font-size: 80%; 73 | } 74 | sub, 75 | sup { 76 | font-size: 75%; 77 | line-height: 0; 78 | position: relative; 79 | vertical-align: baseline; 80 | } 81 | sub { 82 | bottom: -0.25em; 83 | } 84 | sup { 85 | top: -0.5em; 86 | } 87 | img { 88 | border-style: none; 89 | } 90 | svg:not(:root) { 91 | overflow: hidden; 92 | } 93 | code, 94 | kbd, 95 | pre, 96 | samp { 97 | font-family: monospace, monospace; 98 | font-size: 1em; 99 | } 100 | figure { 101 | margin: 1em 40px; 102 | } 103 | hr { 104 | box-sizing: content-box; 105 | height: 0; 106 | overflow: visible; 107 | } 108 | button, 109 | input, 110 | optgroup, 111 | select, 112 | textarea { 113 | font: inherit; 114 | margin: 0; 115 | } 116 | optgroup { 117 | font-weight: 700; 118 | } 119 | button, 120 | input { 121 | overflow: visible; 122 | } 123 | button, 124 | select { 125 | text-transform: none; 126 | } 127 | [type="reset"], 128 | [type="submit"], 129 | button, 130 | html [type="button"] { 131 | -webkit-appearance: button; 132 | } 133 | [type="button"]::-moz-focus-inner, 134 | [type="reset"]::-moz-focus-inner, 135 | [type="submit"]::-moz-focus-inner, 136 | button::-moz-focus-inner { 137 | border-style: none; 138 | padding: 0; 139 | } 140 | [type="button"]:-moz-focusring, 141 | [type="reset"]:-moz-focusring, 142 | [type="submit"]:-moz-focusring, 143 | button:-moz-focusring { 144 | outline: 1px dotted ButtonText; 145 | } 146 | fieldset { 147 | border: 1px solid silver; 148 | margin: 0 2px; 149 | padding: 0.35em 0.625em 0.75em; 150 | } 151 | legend { 152 | box-sizing: border-box; 153 | color: inherit; 154 | display: table; 155 | max-width: 100%; 156 | padding: 0; 157 | white-space: normal; 158 | } 159 | textarea { 160 | overflow: auto; 161 | } 162 | [type="checkbox"], 163 | [type="radio"] { 164 | box-sizing: border-box; 165 | padding: 0; 166 | } 167 | [type="number"]::-webkit-inner-spin-button, 168 | [type="number"]::-webkit-outer-spin-button { 169 | height: auto; 170 | } 171 | [type="search"] { 172 | -webkit-appearance: textfield; 173 | outline-offset: -2px; 174 | } 175 | [type="search"]::-webkit-search-cancel-button, 176 | [type="search"]::-webkit-search-decoration { 177 | -webkit-appearance: none; 178 | } 179 | ::-webkit-input-placeholder { 180 | color: inherit; 181 | opacity: 0.54; 182 | } 183 | ::-webkit-file-upload-button { 184 | -webkit-appearance: button; 185 | font: inherit; 186 | } 187 | html { 188 | font: 112.5%/1.45em georgia, serif; 189 | box-sizing: border-box; 190 | overflow-y: scroll; 191 | } 192 | * { 193 | box-sizing: inherit; 194 | } 195 | *:before { 196 | box-sizing: inherit; 197 | } 198 | *:after { 199 | box-sizing: inherit; 200 | } 201 | body { 202 | color: hsla(0, 0%, 0%, 0.8); 203 | font-family: georgia, serif; 204 | font-weight: normal; 205 | word-wrap: break-word; 206 | font-kerning: normal; 207 | -moz-font-feature-settings: "kern", "liga", "clig", "calt"; 208 | -ms-font-feature-settings: "kern", "liga", "clig", "calt"; 209 | -webkit-font-feature-settings: "kern", "liga", "clig", "calt"; 210 | font-feature-settings: "kern", "liga", "clig", "calt"; 211 | } 212 | img { 213 | max-width: 100%; 214 | margin-left: 0; 215 | margin-right: 0; 216 | margin-top: 0; 217 | padding-bottom: 0; 218 | padding-left: 0; 219 | padding-right: 0; 220 | padding-top: 0; 221 | margin-bottom: 1.45rem; 222 | } 223 | h1 { 224 | margin-left: 0; 225 | margin-right: 0; 226 | margin-top: 0; 227 | padding-bottom: 0; 228 | padding-left: 0; 229 | padding-right: 0; 230 | padding-top: 0; 231 | margin-bottom: 1.45rem; 232 | color: inherit; 233 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 234 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 235 | font-weight: bold; 236 | text-rendering: optimizeLegibility; 237 | font-size: 2.25rem; 238 | line-height: 1.1; 239 | } 240 | h2 { 241 | margin-left: 0; 242 | margin-right: 0; 243 | margin-top: 0; 244 | padding-bottom: 0; 245 | padding-left: 0; 246 | padding-right: 0; 247 | padding-top: 0; 248 | margin-bottom: 1.45rem; 249 | color: inherit; 250 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 251 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 252 | font-weight: bold; 253 | text-rendering: optimizeLegibility; 254 | font-size: 1.62671rem; 255 | line-height: 1.1; 256 | } 257 | h3 { 258 | margin-left: 0; 259 | margin-right: 0; 260 | margin-top: 0; 261 | padding-bottom: 0; 262 | padding-left: 0; 263 | padding-right: 0; 264 | padding-top: 0; 265 | margin-bottom: 1.45rem; 266 | color: inherit; 267 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 268 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 269 | font-weight: bold; 270 | text-rendering: optimizeLegibility; 271 | font-size: 1.38316rem; 272 | line-height: 1.1; 273 | } 274 | h4 { 275 | margin-left: 0; 276 | margin-right: 0; 277 | margin-top: 0; 278 | padding-bottom: 0; 279 | padding-left: 0; 280 | padding-right: 0; 281 | padding-top: 0; 282 | margin-bottom: 1.45rem; 283 | color: inherit; 284 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 285 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 286 | font-weight: bold; 287 | text-rendering: optimizeLegibility; 288 | font-size: 1rem; 289 | line-height: 1.1; 290 | } 291 | h5 { 292 | margin-left: 0; 293 | margin-right: 0; 294 | margin-top: 0; 295 | padding-bottom: 0; 296 | padding-left: 0; 297 | padding-right: 0; 298 | padding-top: 0; 299 | margin-bottom: 1.45rem; 300 | color: inherit; 301 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 302 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 303 | font-weight: bold; 304 | text-rendering: optimizeLegibility; 305 | font-size: 0.85028rem; 306 | line-height: 1.1; 307 | } 308 | h6 { 309 | margin-left: 0; 310 | margin-right: 0; 311 | margin-top: 0; 312 | padding-bottom: 0; 313 | padding-left: 0; 314 | padding-right: 0; 315 | padding-top: 0; 316 | margin-bottom: 1.45rem; 317 | color: inherit; 318 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 319 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 320 | font-weight: bold; 321 | text-rendering: optimizeLegibility; 322 | font-size: 0.78405rem; 323 | line-height: 1.1; 324 | } 325 | hgroup { 326 | margin-left: 0; 327 | margin-right: 0; 328 | margin-top: 0; 329 | padding-bottom: 0; 330 | padding-left: 0; 331 | padding-right: 0; 332 | padding-top: 0; 333 | margin-bottom: 1.45rem; 334 | } 335 | ul { 336 | margin-left: 1.45rem; 337 | margin-right: 0; 338 | margin-top: 0; 339 | padding-bottom: 0; 340 | padding-left: 0; 341 | padding-right: 0; 342 | padding-top: 0; 343 | margin-bottom: 1.45rem; 344 | list-style-position: outside; 345 | list-style-image: none; 346 | } 347 | ol { 348 | margin-left: 1.45rem; 349 | margin-right: 0; 350 | margin-top: 0; 351 | padding-bottom: 0; 352 | padding-left: 0; 353 | padding-right: 0; 354 | padding-top: 0; 355 | margin-bottom: 1.45rem; 356 | list-style-position: outside; 357 | list-style-image: none; 358 | } 359 | dl { 360 | margin-left: 0; 361 | margin-right: 0; 362 | margin-top: 0; 363 | padding-bottom: 0; 364 | padding-left: 0; 365 | padding-right: 0; 366 | padding-top: 0; 367 | margin-bottom: 1.45rem; 368 | } 369 | dd { 370 | margin-left: 0; 371 | margin-right: 0; 372 | margin-top: 0; 373 | padding-bottom: 0; 374 | padding-left: 0; 375 | padding-right: 0; 376 | padding-top: 0; 377 | margin-bottom: 1.45rem; 378 | } 379 | p { 380 | margin-left: 0; 381 | margin-right: 0; 382 | margin-top: 0; 383 | padding-bottom: 0; 384 | padding-left: 0; 385 | padding-right: 0; 386 | padding-top: 0; 387 | margin-bottom: 1.45rem; 388 | } 389 | figure { 390 | margin-left: 0; 391 | margin-right: 0; 392 | margin-top: 0; 393 | padding-bottom: 0; 394 | padding-left: 0; 395 | padding-right: 0; 396 | padding-top: 0; 397 | margin-bottom: 1.45rem; 398 | } 399 | pre { 400 | margin-left: 0; 401 | margin-right: 0; 402 | margin-top: 0; 403 | margin-bottom: 1.45rem; 404 | font-size: 0.85rem; 405 | line-height: 1.42; 406 | background: hsla(0, 0%, 0%, 0.04); 407 | border-radius: 3px; 408 | overflow: auto; 409 | word-wrap: normal; 410 | padding: 1.45rem; 411 | } 412 | table { 413 | margin-left: 0; 414 | margin-right: 0; 415 | margin-top: 0; 416 | padding-bottom: 0; 417 | padding-left: 0; 418 | padding-right: 0; 419 | padding-top: 0; 420 | margin-bottom: 1.45rem; 421 | font-size: 1rem; 422 | line-height: 1.45rem; 423 | border-collapse: collapse; 424 | width: 100%; 425 | } 426 | fieldset { 427 | margin-left: 0; 428 | margin-right: 0; 429 | margin-top: 0; 430 | padding-bottom: 0; 431 | padding-left: 0; 432 | padding-right: 0; 433 | padding-top: 0; 434 | margin-bottom: 1.45rem; 435 | } 436 | blockquote { 437 | margin-left: 1.45rem; 438 | margin-right: 1.45rem; 439 | margin-top: 0; 440 | padding-bottom: 0; 441 | padding-left: 0; 442 | padding-right: 0; 443 | padding-top: 0; 444 | margin-bottom: 1.45rem; 445 | } 446 | form { 447 | margin-left: 0; 448 | margin-right: 0; 449 | margin-top: 0; 450 | padding-bottom: 0; 451 | padding-left: 0; 452 | padding-right: 0; 453 | padding-top: 0; 454 | margin-bottom: 1.45rem; 455 | } 456 | noscript { 457 | margin-left: 0; 458 | margin-right: 0; 459 | margin-top: 0; 460 | padding-bottom: 0; 461 | padding-left: 0; 462 | padding-right: 0; 463 | padding-top: 0; 464 | margin-bottom: 1.45rem; 465 | } 466 | iframe { 467 | margin-left: 0; 468 | margin-right: 0; 469 | margin-top: 0; 470 | padding-bottom: 0; 471 | padding-left: 0; 472 | padding-right: 0; 473 | padding-top: 0; 474 | margin-bottom: 1.45rem; 475 | } 476 | hr { 477 | margin-left: 0; 478 | margin-right: 0; 479 | margin-top: 0; 480 | padding-bottom: 0; 481 | padding-left: 0; 482 | padding-right: 0; 483 | padding-top: 0; 484 | margin-bottom: calc(1.45rem - 1px); 485 | background: hsla(0, 0%, 0%, 0.2); 486 | border: none; 487 | height: 1px; 488 | } 489 | address { 490 | margin-left: 0; 491 | margin-right: 0; 492 | margin-top: 0; 493 | padding-bottom: 0; 494 | padding-left: 0; 495 | padding-right: 0; 496 | padding-top: 0; 497 | margin-bottom: 1.45rem; 498 | } 499 | b { 500 | font-weight: bold; 501 | } 502 | strong { 503 | font-weight: bold; 504 | } 505 | dt { 506 | font-weight: bold; 507 | } 508 | th { 509 | font-weight: bold; 510 | } 511 | li { 512 | margin-bottom: calc(1.45rem / 2); 513 | } 514 | ol li { 515 | padding-left: 0; 516 | } 517 | ul li { 518 | padding-left: 0; 519 | } 520 | li > ol { 521 | margin-left: 1.45rem; 522 | margin-bottom: calc(1.45rem / 2); 523 | margin-top: calc(1.45rem / 2); 524 | } 525 | li > ul { 526 | margin-left: 1.45rem; 527 | margin-bottom: calc(1.45rem / 2); 528 | margin-top: calc(1.45rem / 2); 529 | } 530 | blockquote *:last-child { 531 | margin-bottom: 0; 532 | } 533 | li *:last-child { 534 | margin-bottom: 0; 535 | } 536 | p *:last-child { 537 | margin-bottom: 0; 538 | } 539 | li > p { 540 | margin-bottom: calc(1.45rem / 2); 541 | } 542 | code { 543 | font-size: 0.85rem; 544 | line-height: 1.45rem; 545 | } 546 | kbd { 547 | font-size: 0.85rem; 548 | line-height: 1.45rem; 549 | } 550 | samp { 551 | font-size: 0.85rem; 552 | line-height: 1.45rem; 553 | } 554 | abbr { 555 | border-bottom: 1px dotted hsla(0, 0%, 0%, 0.5); 556 | cursor: help; 557 | } 558 | acronym { 559 | border-bottom: 1px dotted hsla(0, 0%, 0%, 0.5); 560 | cursor: help; 561 | } 562 | abbr[title] { 563 | border-bottom: 1px dotted hsla(0, 0%, 0%, 0.5); 564 | cursor: help; 565 | text-decoration: none; 566 | } 567 | thead { 568 | text-align: left; 569 | } 570 | td, 571 | th { 572 | text-align: left; 573 | border-bottom: 1px solid hsla(0, 0%, 0%, 0.12); 574 | font-feature-settings: "tnum"; 575 | -moz-font-feature-settings: "tnum"; 576 | -ms-font-feature-settings: "tnum"; 577 | -webkit-font-feature-settings: "tnum"; 578 | padding-left: 0.96667rem; 579 | padding-right: 0.96667rem; 580 | padding-top: 0.725rem; 581 | padding-bottom: calc(0.725rem - 1px); 582 | } 583 | th:first-child, 584 | td:first-child { 585 | padding-left: 0; 586 | } 587 | th:last-child, 588 | td:last-child { 589 | padding-right: 0; 590 | } 591 | tt, 592 | code { 593 | background-color: hsla(0, 0%, 0%, 0.04); 594 | border-radius: 3px; 595 | font-family: "SFMono-Regular", Consolas, "Roboto Mono", "Droid Sans Mono", 596 | "Liberation Mono", Menlo, Courier, monospace; 597 | padding: 0; 598 | padding-top: 0.2em; 599 | padding-bottom: 0.2em; 600 | } 601 | pre code { 602 | background: none; 603 | line-height: 1.42; 604 | } 605 | code:before, 606 | code:after, 607 | tt:before, 608 | tt:after { 609 | letter-spacing: -0.2em; 610 | content: " "; 611 | } 612 | pre code:before, 613 | pre code:after, 614 | pre tt:before, 615 | pre tt:after { 616 | content: ""; 617 | } 618 | @media only screen and (max-width: 480px) { 619 | html { 620 | font-size: 100%; 621 | } 622 | } 623 | -------------------------------------------------------------------------------- /src/components/Layout.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Layout component that queries for data 3 | * with Gatsby's useStaticQuery component 4 | * 5 | * See: https://www.gatsbyjs.org/docs/use-static-query/ 6 | */ 7 | 8 | import React from "react" 9 | import PropTypes from "prop-types" 10 | import { useStaticQuery, graphql } from "gatsby" 11 | 12 | import Header from "./Header" 13 | import "./Layout.css" 14 | import Navigation from "./Navigation" 15 | 16 | const Layout = ({ children }) => { 17 | const data = useStaticQuery(graphql` 18 | query SiteTitleQuery { 19 | site { 20 | siteMetadata { 21 | title 22 | } 23 | } 24 | } 25 | `) 26 | 27 | return ( 28 | <> 29 |
30 |
37 | 38 |
{children}
39 |
40 | 41 |
42 |
43 | 44 | ) 45 | } 46 | 47 | Layout.propTypes = { 48 | children: PropTypes.node.isRequired, 49 | } 50 | 51 | export default Layout 52 | -------------------------------------------------------------------------------- /src/components/LoginForm.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react" 2 | import useFormFields from "../hooks/useFormFields" 3 | import { gql, useMutation } from "@apollo/client" 4 | import uuid from "uuid/v4" 5 | import { setAuthToken, setRefreshToken } from "../services/auth" 6 | import { navigate } from "gatsby" 7 | import { useAuth } from "../hooks/useAuth" 8 | 9 | const LOGIN_USER = gql` 10 | mutation LoginUser($input: LoginInput!) { 11 | login(input: $input) { 12 | authToken 13 | refreshToken 14 | } 15 | } 16 | ` 17 | 18 | const labelStyle = { 19 | marginTop: 16 20 | } 21 | 22 | const LoginForm = () => { 23 | const auth = useAuth(); 24 | 25 | useEffect(() => { 26 | 27 | if (auth.isLoggedIn()) { 28 | navigate(`/dashboard/`, {replace: true}) 29 | } 30 | }, [auth]) 31 | 32 | const [loginUser, { data: loginData }] = useMutation(LOGIN_USER) 33 | 34 | const [fields, handleFieldChange] = useFormFields({ 35 | username: "", 36 | password: "", 37 | }) 38 | 39 | const handleSubmit = (e) => { 40 | e.preventDefault() 41 | 42 | loginUser({ 43 | variables: { 44 | input: { 45 | clientMutationId: uuid(), 46 | username: fields.username, 47 | password: fields.password, 48 | }, 49 | }, 50 | }).then((response) => { 51 | // console.log("Response", response) 52 | const { login } = response.data 53 | 54 | if(login) { 55 | setAuthToken(login.authToken) 56 | setRefreshToken(login.refreshToken, () => navigate("/dashboard/")) 57 | } 58 | }) 59 | } 60 | 61 | return ( 62 |
64 |
65 | 66 | 68 | 69 | 70 | 72 |
73 | 74 | 75 |
76 | ) 77 | } 78 | 79 | 80 | export default LoginForm 81 | -------------------------------------------------------------------------------- /src/components/Navigation.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { Link } from "gatsby" 3 | 4 | const style = { 5 | display: 'inline-block', 6 | marginRight: 16 7 | } 8 | 9 | const Navigation = () => { 10 | return ( 11 | 21 | ) 22 | } 23 | 24 | export default Navigation 25 | -------------------------------------------------------------------------------- /src/components/PrivateRoute.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { navigate } from "gatsby" 3 | import { isBrowser } from "../services/auth" 4 | import { useAuth } from "../hooks/useAuth" 5 | 6 | const PrivateRoute = ({ component: Component, location, ...rest }) => { 7 | const auth = useAuth() 8 | 9 | if (!auth.isLoggedIn() && isBrowser && window.location.pathname !== `/login/`) { 10 | navigate(`/dashboard/login/`, {replace: true}) 11 | return null 12 | } 13 | 14 | return 15 | } 16 | export default PrivateRoute 17 | -------------------------------------------------------------------------------- /src/components/ProgressLoader.css: -------------------------------------------------------------------------------- 1 | /* Loader 5 */ 2 | .loader-5 { 3 | height: 32px; 4 | width: 32px; 5 | -webkit-animation: loader-5-1 2s cubic-bezier(0.770, 0.000, 0.175, 1.000) infinite; 6 | animation: loader-5-1 2s cubic-bezier(0.770, 0.000, 0.175, 1.000) infinite; 7 | } 8 | @-webkit-keyframes loader-5-1 { 9 | 0% { -webkit-transform: rotate(0deg); } 10 | 100% { -webkit-transform: rotate(360deg); } 11 | } 12 | @keyframes loader-5-1 { 13 | 0% { transform: rotate(0deg); } 14 | 100% { transform: rotate(360deg); } 15 | } 16 | .loader-5::before { 17 | content: ""; 18 | display: block; 19 | position: absolute; 20 | top: 0; left: 0; 21 | bottom: 0; right: auto; 22 | margin: auto; 23 | width: 8px; 24 | height: 8px; 25 | background: rebeccapurple; 26 | border-radius: 50%; 27 | -webkit-animation: loader-5-2 2s cubic-bezier(0.770, 0.000, 0.175, 1.000) infinite; 28 | animation: loader-5-2 2s cubic-bezier(0.770, 0.000, 0.175, 1.000) infinite; 29 | } 30 | @-webkit-keyframes loader-5-2 { 31 | 0% { -webkit-transform: translate3d(0, 0, 0) scale(1); } 32 | 50% { -webkit-transform: translate3d(24px, 0, 0) scale(.5); } 33 | 100% { -webkit-transform: translate3d(0, 0, 0) scale(1); } 34 | } 35 | @keyframes loader-5-2 { 36 | 0% { transform: translate3d(0, 0, 0) scale(1); } 37 | 50% { transform: translate3d(24px, 0, 0) scale(.5); } 38 | 100% { transform: translate3d(0, 0, 0) scale(1); } 39 | } 40 | .loader-5::after { 41 | content: ""; 42 | display: block; 43 | position: absolute; 44 | top: 0; left: auto; 45 | bottom: 0; right: 0; 46 | margin: auto; 47 | width: 8px; 48 | height: 8px; 49 | background: rebeccapurple; 50 | border-radius: 50%; 51 | -webkit-animation: loader-5-3 2s cubic-bezier(0.770, 0.000, 0.175, 1.000) infinite; 52 | animation: loader-5-3 2s cubic-bezier(0.770, 0.000, 0.175, 1.000) infinite; 53 | } 54 | @-webkit-keyframes loader-5-3 { 55 | 0% { -webkit-transform: translate3d(0, 0, 0) scale(1); } 56 | 50% { -webkit-transform: translate3d(-24px, 0, 0) scale(.5); } 57 | 100% { -webkit-transform: translate3d(0, 0, 0) scale(1); } 58 | } 59 | @keyframes loader-5-3 { 60 | 0% { transform: translate3d(0, 0, 0) scale(1); } 61 | 50% { transform: translate3d(-24px, 0, 0) scale(.5); } 62 | 100% { transform: translate3d(0, 0, 0) scale(1); } 63 | } 64 | .loader-5 span { 65 | display: block; 66 | position: absolute; 67 | top: 0; left: 0; 68 | bottom: 0; right: 0; 69 | margin: auto; 70 | height: 32px; 71 | width: 32px; 72 | } 73 | .loader-5 span::before { 74 | content: ""; 75 | display: block; 76 | position: absolute; 77 | top: 0; left: 0; 78 | bottom: auto; right: 0; 79 | margin: auto; 80 | width: 8px; 81 | height: 8px; 82 | background: rebeccapurple; 83 | border-radius: 50%; 84 | -webkit-animation: loader-5-4 2s cubic-bezier(0.770, 0.000, 0.175, 1.000) infinite; 85 | animation: loader-5-4 2s cubic-bezier(0.770, 0.000, 0.175, 1.000) infinite; 86 | } 87 | @-webkit-keyframes loader-5-4 { 88 | 0% { -webkit-transform: translate3d(0, 0, 0) scale(1); } 89 | 50% { -webkit-transform: translate3d(0, 24px, 0) scale(.5); } 90 | 100% { -webkit-transform: translate3d(0, 0, 0) scale(1); } 91 | } 92 | @keyframes loader-5-4 { 93 | 0% { transform: translate3d(0, 0, 0) scale(1); } 94 | 50% { transform: translate3d(0, 24px, 0) scale(.5); } 95 | 100% { transform: translate3d(0, 0, 0) scale(1); } 96 | } 97 | .loader-5 span::after { 98 | content: ""; 99 | display: block; 100 | position: absolute; 101 | top: auto; left: 0; 102 | bottom: 0; right: 0; 103 | margin: auto; 104 | width: 8px; 105 | height: 8px; 106 | background: rebeccapurple; 107 | border-radius: 50%; 108 | -webkit-animation: loader-5-5 2s cubic-bezier(0.770, 0.000, 0.175, 1.000) infinite; 109 | animation: loader-5-5 2s cubic-bezier(0.770, 0.000, 0.175, 1.000) infinite; 110 | } 111 | @-webkit-keyframes loader-5-5 { 112 | 0% { -webkit-transform: translate3d(0, 0, 0) scale(1); } 113 | 50% { -webkit-transform: translate3d(0, -24px, 0) scale(.5); } 114 | 100% { -webkit-transform: translate3d(0, 0, 0) scale(1); } 115 | } 116 | @keyframes loader-5-5 { 117 | 0% { transform: translate3d(0, 0, 0) scale(1); } 118 | 50% { transform: translate3d(0, -24px, 0) scale(.5); } 119 | 100% { transform: translate3d(0, 0, 0) scale(1); } 120 | } 121 | -------------------------------------------------------------------------------- /src/components/ProgressLoader.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | 3 | import "./ProgressLoader.css" 4 | 5 | // Just a simple CSS base ProgressLoader as an example. 6 | // Used this example from CodePen: https://codepen.io/rbv912/pen/dYbqLQ 7 | const ProgressLoader = ({}) => { 8 | return ( 9 |
10 | ) 11 | } 12 | 13 | 14 | export default ProgressLoader 15 | -------------------------------------------------------------------------------- /src/components/Seo.js: -------------------------------------------------------------------------------- 1 | /** 2 | * SEO component that queries for data with 3 | * Gatsby's useStaticQuery React hook 4 | * 5 | * See: https://www.gatsbyjs.org/docs/use-static-query/ 6 | */ 7 | 8 | import React from "react" 9 | import PropTypes from "prop-types" 10 | import Helmet from "react-helmet" 11 | import { useStaticQuery, graphql } from "gatsby" 12 | 13 | function Seo({ description, lang, meta, title }) { 14 | const { site } = useStaticQuery( 15 | graphql` 16 | query { 17 | site { 18 | siteMetadata { 19 | title 20 | description 21 | author 22 | } 23 | } 24 | } 25 | ` 26 | ) 27 | 28 | const metaDescription = description || site.siteMetadata.description 29 | 30 | return ( 31 | 72 | ) 73 | } 74 | 75 | Seo.defaultProps = { 76 | lang: `en`, 77 | meta: [], 78 | description: ``, 79 | } 80 | 81 | Seo.propTypes = { 82 | description: PropTypes.string, 83 | lang: PropTypes.string, 84 | meta: PropTypes.arrayOf(PropTypes.object), 85 | title: PropTypes.string.isRequired, 86 | } 87 | 88 | export default Seo 89 | -------------------------------------------------------------------------------- /src/components/SignUpForm.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react" 2 | import { gql, useMutation } from "@apollo/client" 3 | import useFormFields from "../hooks/useFormFields" 4 | import uuid from "uuid" 5 | import { setAuthToken, setRefreshToken } from "../services/auth" 6 | import { useAuth } from "../hooks/useAuth" 7 | import { navigate } from "gatsby" 8 | 9 | 10 | const REGISTER_USER = gql` 11 | mutation RegisterUser($input: RegisterUserInput!) { 12 | registerUser(input: $input) { 13 | user { 14 | jwtAuthToken 15 | jwtRefreshToken 16 | } 17 | } 18 | } 19 | ` 20 | 21 | const labelStyle = { 22 | marginTop: 16 23 | } 24 | 25 | const SignUpForm = () => { 26 | const auth = useAuth(); 27 | 28 | useEffect(() => { 29 | 30 | if (auth.isLoggedIn()) { 31 | navigate(`/dashboard/`, {replace: true}) 32 | } 33 | }, [auth]) 34 | 35 | 36 | const [fields, handleFieldChange] = useFormFields({ 37 | email: "", 38 | firstName: "", 39 | lastName: "", 40 | password: "", 41 | }) 42 | const [isLoading, setIsLoading] = useState(false) 43 | const [registerUser, { data }] = useMutation(REGISTER_USER) 44 | 45 | const handleSubmit = async (event) => { 46 | event.preventDefault() 47 | setIsLoading(true) 48 | 49 | await registerUser({ 50 | variables: { 51 | input: { 52 | clientMutationId: uuid(), 53 | username: fields.email, 54 | firstName: fields.firstName || null, 55 | lastName: fields.lastName || null, 56 | email: fields.email, 57 | password: fields.password, 58 | }, 59 | }, 60 | }).then((response) => { 61 | const { registerUser } = response.data 62 | 63 | if(registerUser && registerUser.user) { 64 | setAuthToken(response.data.registerUser.user.jwtAuthToken) 65 | setRefreshToken(response.data.registerUser.user.jwtRefreshToken, () => navigate("/dashboard/")) 66 | } 67 | 68 | setIsLoading(false) 69 | }) 70 | } 71 | 72 | return
73 |
74 | 75 | 76 | 78 | 79 | 80 | 82 | 83 | 84 | 86 | 87 | 88 | 90 |
91 | 92 | 93 | {isLoading ?

is Loading ...

: null} 94 | 95 |
96 | } 97 | 98 | export default SignUpForm 99 | -------------------------------------------------------------------------------- /src/dashboard/PageNotFound.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react" 2 | import { navigate } from "gatsby" 3 | 4 | const PageNotFound = () => { 5 | useEffect(() => { 6 | navigate(`/dashboard/`, {replace: true}) 7 | }, []) 8 | 9 | return
Page not found
10 | } 11 | 12 | export default PageNotFound 13 | -------------------------------------------------------------------------------- /src/dashboard/UserArea.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | 3 | const UserArea = () => { 4 | 5 | return ( 6 |
7 | This is the UserArea 8 |
9 | ) 10 | } 11 | export default UserArea 12 | -------------------------------------------------------------------------------- /src/dashboard/index.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react" 2 | import Seo from "../components/Seo" 3 | import { gql, useQuery } from "@apollo/client" 4 | import { useUser } from "../hooks/useUser" 5 | import ProgressLoader from "../components/ProgressLoader" 6 | 7 | 8 | const GET_USER = gql` 9 | { 10 | viewer { 11 | firstName 12 | lastName 13 | email 14 | username 15 | } 16 | } 17 | ` 18 | 19 | const DashboardIndex = () => { 20 | const { data, loading, error, refetch } = useQuery(GET_USER) 21 | const [user, setUser] = useUser(); 22 | 23 | 24 | useEffect(() => { 25 | if (data) { 26 | setUser({ 27 | ...user, 28 | username: data.viewer.username, 29 | firstName: data.viewer.firstName, 30 | lastName: data.viewer.lastName, 31 | email: data.viewer.email, 32 | }) 33 | } 34 | 35 | }, [data]) 36 | 37 | if (loading) return 38 | 39 | return ( 40 | <> 41 | 42 |

Dashboard Index

43 | 44 |
45 | Hello {data && user && (user.firstName ? user.firstName + " " + user.lastName : user.email)} 46 |
47 | 48 | ) 49 | } 50 | 51 | export default DashboardIndex 52 | -------------------------------------------------------------------------------- /src/hooks/useAuth.js: -------------------------------------------------------------------------------- 1 | import React, { createContext, useContext, useEffect } from "react" 2 | import { 3 | getInMemoryAuthToken, 4 | getRefreshToken, 5 | isLoggedOut, 6 | isTokenExpired, 7 | LOGGED_OUT_KEY, 8 | logout, 9 | setAuthToken, 10 | } from "../services/auth" 11 | import { gql, useMutation } from "@apollo/client" 12 | import useNetwork from "./useNetwork" 13 | import { navigate } from "gatsby" 14 | import uuid from "uuid" 15 | 16 | const AuthContext = createContext(null) 17 | 18 | export const AuthProvider = ({ children }) => ( 19 | 20 | {children} 21 | 22 | ) 23 | 24 | export const useAuth = () => useContext(AuthContext) 25 | 26 | const syncLoginStatus = (event) => { 27 | if (event.key === LOGGED_OUT_KEY && isLoggedOut()) { 28 | logout(navigate("/dashboard/")) 29 | } 30 | } 31 | 32 | const REFRESH_TOKEN = gql` 33 | mutation LoginUser($input: RefreshJwtAuthTokenInput!) { 34 | refreshJwtAuthToken(input: $input) { 35 | authToken 36 | } 37 | } 38 | ` 39 | 40 | const useProvideAuth = () => { 41 | const [refreshToken, { data }] = useMutation(REFRESH_TOKEN) 42 | 43 | const isOnline = useNetwork() 44 | 45 | // const isAuthTokenExpired = () => 46 | // getInMemoryAuthToken().authToken && isTokenExpired(getInMemoryAuthToken().authToken) 47 | 48 | const isLoggedIn = () => 49 | getInMemoryAuthToken() && !isTokenExpired(getInMemoryAuthToken()) 50 | 51 | useEffect(() => { 52 | // TODO: This should only happen in one place. Either we implement an 53 | // interval here, or we use the apollo-link-token-refresh in client.js 54 | if (isOnline && getRefreshToken()) { 55 | refreshToken({ 56 | variables: { 57 | input: { 58 | clientMutationId: uuid(), 59 | jwtRefreshToken: getRefreshToken(), 60 | }, 61 | }, 62 | }).then((response) => { 63 | console.log("silentRefresh", response) 64 | const token = response.data.refreshJwtAuthToken ? response.data.refreshJwtAuthToken.authToken : null 65 | if (token) { 66 | setAuthToken(response.data.refreshJwtAuthToken.authToken) 67 | } 68 | }) 69 | } 70 | }, [refreshToken]) 71 | 72 | 73 | /** 74 | * Make sure, User is logged out on all Tabs 75 | */ 76 | useEffect(() => { 77 | window.addEventListener("storage", syncLoginStatus) 78 | 79 | return () => { 80 | window.removeEventListener("storage", syncLoginStatus) 81 | } 82 | }) 83 | 84 | // TODO: this still needs to be implemented properly 85 | // useEffect(() => { 86 | // console.log("useEffect called") 87 | // if (isAuthTokenExpired() && isOnline) { 88 | // console.log("Triggered Token Refresh") 89 | // /** 90 | // * Execute an arbitrary query to trigger auth token refresh, 91 | // * then sync local storage with auth context. 92 | // */ 93 | // client 94 | // .query({ 95 | // query: gql` 96 | // { 97 | // generalSettings { 98 | // title 99 | // } 100 | // } 101 | // `, 102 | // }) 103 | // } 104 | // }) 105 | 106 | return { 107 | isLoggedIn, 108 | // user: token ? token.user : null, 109 | } 110 | } 111 | 112 | -------------------------------------------------------------------------------- /src/hooks/useFormFields.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | 3 | const useFormFields = (initialState) => { 4 | const [fields, setValues] = useState(initialState); 5 | 6 | return [ 7 | fields, 8 | function(event) { 9 | setValues({ 10 | ...fields, 11 | [event.target.name]: event.target.value 12 | }); 13 | } 14 | ]; 15 | } 16 | 17 | export default useFormFields 18 | -------------------------------------------------------------------------------- /src/hooks/useLocalStorage.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react" 2 | 3 | /** 4 | * @param {string} key The key to the value in local storage. 5 | * @param {*} initialValue The initial value. 6 | */ 7 | export const useLocalStorage = (key, initialValue) => { 8 | // State to store our value. 9 | // Pass initial state function to useState so logic is only executed once. 10 | const [storedValue, setStoredValue] = useState(() => { 11 | try { 12 | const item = 13 | typeof window !== `undefined` ? window.localStorage.getItem(key) : null 14 | 15 | return item ? JSON.parse(item) : initialValue 16 | } catch (error) { 17 | console.log(error) 18 | return initialValue 19 | } 20 | }) 21 | 22 | // Return a wrapped version of useState's setter function that 23 | // persists the new value to localStorage. 24 | const setValue = value => { 25 | try { 26 | // Allow value to be a function so we have same API as useState. 27 | const valueToStore = 28 | value instanceof Function ? value(storedValue) : value 29 | 30 | setStoredValue(valueToStore) 31 | 32 | if (typeof window !== `undefined`) { 33 | window.localStorage.setItem(key, JSON.stringify(valueToStore)) 34 | } 35 | } catch (error) { 36 | console.log(error) 37 | } 38 | } 39 | 40 | return [storedValue, setValue] 41 | } 42 | -------------------------------------------------------------------------------- /src/hooks/useNetwork.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react" 2 | 3 | const useNetwork = () => { 4 | const initialValue = 5 | typeof window !== "undefined" ? window.navigator.onLine : true 6 | const [isOnline, setNetwork] = useState(initialValue) 7 | const updateNetwork = () => setNetwork(window.navigator.onLine) 8 | 9 | useEffect(() => { 10 | window.addEventListener("offline", updateNetwork) 11 | window.addEventListener("online", updateNetwork) 12 | return () => { 13 | window.removeEventListener("offline", updateNetwork) 14 | window.removeEventListener("online", updateNetwork) 15 | } 16 | }) 17 | 18 | return isOnline 19 | } 20 | 21 | export default useNetwork 22 | -------------------------------------------------------------------------------- /src/hooks/useUser.js: -------------------------------------------------------------------------------- 1 | import React, { createContext, useContext, useState } from "react" 2 | 3 | const UserContext = createContext(null) 4 | 5 | export const UserProvider = ({ children }) => ( 6 | 7 | {children} 8 | 9 | ) 10 | 11 | export const useUser = () => useContext(UserContext) 12 | 13 | 14 | const useProvideUser = () => { 15 | const [user, setUser] = useState({ 16 | username: null, 17 | firstName: null, 18 | lastName: null, 19 | email: null, 20 | }) 21 | 22 | return [ 23 | user, 24 | setUser, 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /src/images/gatsby-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NeverNull/gatsby-apollo-wpgraphql-jwt-starter/1f4bc6fe03fce813b5291044c9bf7efae22212ff/src/images/gatsby-icon.png -------------------------------------------------------------------------------- /src/pages/404.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | 3 | import Layout from "../components/Layout" 4 | import Seo from "../components/Seo" 5 | 6 | const NotFoundPage = () => ( 7 | 8 | 9 |

NOT FOUND

10 |

You just hit a route that doesn't exist... the sadness.

11 |
12 | ) 13 | 14 | export default NotFoundPage 15 | -------------------------------------------------------------------------------- /src/pages/dashboard.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { Router } from "@reach/router" 3 | import PrivateRoute from "../components/PrivateRoute" 4 | import UserArea from "../dashboard/UserArea" 5 | import Layout from "../components/Layout" 6 | import DashboardIndex from "../dashboard" 7 | import { Link, navigate } from "gatsby" 8 | import { logout } from "../services/auth" 9 | 10 | import LoginForm from "../components/LoginForm" 11 | import SignUpForm from "../components/SignUpForm" 12 | import { useAuth } from "../hooks/useAuth" 13 | import PageNotFound from "../dashboard/PageNotFound" 14 | import { UserProvider } from "../hooks/useUser" 15 | 16 | // TODO: somehow make sure, the Dashboard is not flicking when trying to access it without being logged in 17 | 18 | const Dashboard = () => { 19 | const auth = useAuth() 20 | return ( 21 | 22 | 23 |
24 | 25 | { 26 | auth.isLoggedIn() && 27 | <> 28 |

Dashboard

29 | 42 | 43 | } 44 | 45 | {/* This also defines the space, where the routes get rendered */} 46 |
47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 |
57 | 58 |
59 | ) 60 | } 61 | 62 | export default Dashboard 63 | -------------------------------------------------------------------------------- /src/pages/index.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | 3 | import Layout from "../components/Layout" 4 | import Seo from "../components/Seo" 5 | 6 | const IndexPage = () => ( 7 | 8 | 9 |

Gatsby Apollo WPGraphQL JWT Starter

10 | 11 |

This project aims to serve as a good starting point, to handle user registration and login with Apollo, WPGraphQL and 12 | WPGraphQL JWT Authentication.

13 | 14 | Find more info here: https://github.com/NeverNull/gatsby-apollo-wpgraphql-jwt-starter 15 | 16 |
17 | ) 18 | 19 | export default IndexPage 20 | -------------------------------------------------------------------------------- /src/services/auth.js: -------------------------------------------------------------------------------- 1 | import decode from "jwt-decode" 2 | 3 | 4 | let inMemoryAuthTokenDefault = { 5 | authToken: null, 6 | authExpiration: null, 7 | } 8 | 9 | let inMemoryAuthToken = inMemoryAuthTokenDefault 10 | 11 | 12 | // Local Storage Key 13 | export const REFRESH_TOKEN_KEY = `REFRESH_TOKEN` 14 | export const LOGGED_OUT_KEY = `LOGGED_OUT_TIME` 15 | 16 | 17 | // Helper 18 | export const isBrowser = typeof window !== `undefined` 19 | 20 | // TODO: Check if these work as expected 21 | export const isTokenExpired = authToken => { 22 | return authToken ? Date.now() - decode(authToken).exp < 1000 : true 23 | } 24 | 25 | export const isLoggedOut = () => { 26 | const loggedOutTime = getLoggedOutTime() 27 | return loggedOutTime && loggedOutTime <= Date.now() 28 | } 29 | 30 | 31 | // Methods 32 | 33 | export const deleteRefreshToken = () => { 34 | if (!isBrowser) return null 35 | localStorage.removeItem(REFRESH_TOKEN_KEY) 36 | } 37 | 38 | export const logout = (callback) => { 39 | inMemoryAuthToken = inMemoryAuthTokenDefault 40 | deleteRefreshToken() 41 | setLoggedOutTime() 42 | 43 | if (callback) { 44 | callback() 45 | } 46 | } 47 | 48 | 49 | // Setter 50 | 51 | export const setAuthToken = (authToken) => { 52 | if (!isBrowser) return 53 | if (!authToken) { 54 | console.log("[setAuthToken]", `Auth Token or Auth Expiration shouldn't be ${authToken}.`) 55 | return 56 | } 57 | inMemoryAuthToken = {authToken, authExpiration: decode(authToken).exp} 58 | } 59 | 60 | export const setRefreshToken = (refreshToken, callback) => { 61 | if (!isBrowser) return 62 | if (!refreshToken) { 63 | // console.log("[setRefreshToken]", `Refresh token shouldn't be ${refreshToken}.`) 64 | return 65 | } 66 | 67 | localStorage.setItem(REFRESH_TOKEN_KEY, JSON.stringify(refreshToken)) 68 | localStorage.removeItem(LOGGED_OUT_KEY) 69 | 70 | // console.log("setRefreshToken", inMemoryAuthToken) 71 | 72 | if (callback) { 73 | callback() 74 | } 75 | } 76 | 77 | export const setLoggedOutTime = () => { 78 | if (!isBrowser) return 79 | localStorage.setItem(LOGGED_OUT_KEY, JSON.stringify(Date.now())) 80 | } 81 | 82 | 83 | // Getter 84 | 85 | export const getInMemoryAuthToken = () => { 86 | if (!isBrowser) return null 87 | return inMemoryAuthToken.authToken 88 | } 89 | 90 | export const getAuthTokenExpiration = () => { 91 | if (!isBrowser) return null 92 | return inMemoryAuthToken.authExpiration 93 | } 94 | 95 | export const getRefreshToken = () => { 96 | if (!isBrowser) return null 97 | return JSON.parse(localStorage.getItem(REFRESH_TOKEN_KEY)) 98 | } 99 | 100 | export const getLoggedOutTime = () => { 101 | if (!isBrowser) return null 102 | return JSON.parse(localStorage.getItem(LOGGED_OUT_KEY)) 103 | } 104 | 105 | 106 | 107 | 108 | -------------------------------------------------------------------------------- /wrapRootElement.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { client } from "./apollo/client" 3 | 4 | import { AuthProvider } from "./src/hooks/useAuth" 5 | import { ApolloProvider } from "@apollo/client" 6 | 7 | // eslint-disable-next-line react/display-name,react/prop-types 8 | export const wrapRootElement = ({element}) => { 9 | return 10 | 11 | {element} 12 | 13 | 14 | } 15 | --------------------------------------------------------------------------------