├── .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 |
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 |
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 |
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 |
12 |
13 | Navigation:
14 |
15 |
16 | Login
17 | SignUp
18 | Dashboard
19 |
20 |
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
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 |
30 | Dashboard {" | "}
31 | User Area {" | "}
32 | {
35 | event.preventDefault()
36 | logout(() => navigate(`/dashboard/`))
37 | }}
38 | >
39 | log out
40 |
41 |
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 |
--------------------------------------------------------------------------------