From fa9cb523f14d4d017c3b8c8239d3c2096caabc87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Roycourt?= Date: Sun, 5 Apr 2020 22:06:53 +0200 Subject: [PATCH 1/9] feat(hasura): use webhook authentication --- .env.example | 2 +- docker-compose.yaml | 2 +- packages/app/components/profile/Profile.tsx | 14 ++++++++ packages/app/components/profile/index.ts | 1 + packages/app/components/profile/queries.ts | 9 ++++++ packages/app/lib/withApollo.tsx | 36 +++++++++++++++++++++ packages/app/next.config.js | 9 +++--- packages/app/package.json | 2 ++ packages/app/pages/profile.tsx | 10 ++++++ packages/app/server/index.ts | 11 ++----- packages/app/server/models/user.ts | 3 ++ packages/app/server/routes/auth.ts | 17 +++++++++- packages/app/server/session-store.ts | 12 +++++++ yarn.lock | 9 +++++- 14 files changed, 120 insertions(+), 17 deletions(-) create mode 100644 packages/app/components/profile/Profile.tsx create mode 100644 packages/app/components/profile/index.ts create mode 100644 packages/app/components/profile/queries.ts create mode 100644 packages/app/lib/withApollo.tsx create mode 100644 packages/app/pages/profile.tsx create mode 100644 packages/app/server/session-store.ts diff --git a/.env.example b/.env.example index 9983df70..6bc2fa4b 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,7 @@ NODE_ENV=development DOCKER_HOST_URL=http://host.docker.internal HASURA_SECRET=secret +HASURA_GRAPHQL_AUTH_HOOK=http://localhost:3000/hasura POSTGRES_USER=postgres POSTGRES_PASSWORD=test POSTGRES_DB=gameofblocks_dev @@ -15,5 +16,4 @@ AUTH0_CLIENT_SECRET=123456 AUTH0_CALLBACK_URL=http://localhost:3000/callback SENDGRID_API_KEY=123456 BASE_URL=http://localhost:3000 -HASURA_JWT_SECRET= GRAPHQL_SERVER_URI=http://localhost:5000 \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml index 67a84cb7..2ff020f9 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -28,7 +28,7 @@ services: HASURA_GRAPHQL_DATABASE_URL: postgres://$POSTGRES_USER:$POSTGRES_PASSWORD@db:5432/$POSTGRES_DB HASURA_GRAPHQL_ENABLE_CONSOLE: "true" HASURA_GRAPHQL_ADMIN_SECRET: $HASURA_SECRET - HASURA_GRAPHQL_JWT_SECRET: $HASURA_JWT_SECRET + HASURA_GRAPHQL_AUTH_HOOK: $HASURA_GRAPHQL_AUTH_HOOK volumes: gameofblocks-pgdata: diff --git a/packages/app/components/profile/Profile.tsx b/packages/app/components/profile/Profile.tsx new file mode 100644 index 00000000..c244a10f --- /dev/null +++ b/packages/app/components/profile/Profile.tsx @@ -0,0 +1,14 @@ +import React, { FunctionComponent } from 'react'; +import { useQuery } from '@apollo/react-hooks'; + +import { USER } from './queries'; + +export const Profile: FunctionComponent = () => { + const { data, error } = useQuery(USER); + if (error) { + return
{error.message}
; + } + return
{JSON.stringify(data)}
; +}; + +export default Profile; diff --git a/packages/app/components/profile/index.ts b/packages/app/components/profile/index.ts new file mode 100644 index 00000000..8668bd17 --- /dev/null +++ b/packages/app/components/profile/index.ts @@ -0,0 +1 @@ +export { Profile } from './Profile'; diff --git a/packages/app/components/profile/queries.ts b/packages/app/components/profile/queries.ts new file mode 100644 index 00000000..c0c3f2c1 --- /dev/null +++ b/packages/app/components/profile/queries.ts @@ -0,0 +1,9 @@ +import gql from 'graphql-tag'; + +export const USER = gql` + query user { + user { + id + } + } +`; diff --git a/packages/app/lib/withApollo.tsx b/packages/app/lib/withApollo.tsx new file mode 100644 index 00000000..c1c4cd58 --- /dev/null +++ b/packages/app/lib/withApollo.tsx @@ -0,0 +1,36 @@ +/* eslint-disable react/prop-types */ +/* eslint-disable react/jsx-props-no-spreading */ +import React from 'react'; +import withApollo from 'next-with-apollo'; +import { ApolloClient, InMemoryCache } from 'apollo-boost'; +import { ApolloProvider } from '@apollo/react-hooks'; +import { createHttpLink } from 'apollo-link-http'; +import fetch from 'cross-fetch'; + +const { GRAPHQL_SERVER_URI } = process.env; + +const httpLink = createHttpLink({ + credentials: 'include', + fetch, + uri: GRAPHQL_SERVER_URI, +}); + +export default withApollo( + ({ initialState }) => { + return new ApolloClient({ + link: httpLink, + cache: new InMemoryCache().restore(initialState || {}), + }); + }, + { + // eslint-disable-next-line react/prop-types + render: ({ Page, props }) => { + const { apollo } = props; + return ( + + + + ); + }, + } +); diff --git a/packages/app/next.config.js b/packages/app/next.config.js index dca74224..9aac2d3c 100644 --- a/packages/app/next.config.js +++ b/packages/app/next.config.js @@ -1,14 +1,13 @@ -require('dotenv').config(); - +const env = require('@gameofblocks/env'); const webpack = require('webpack'); module.exports = { publicRuntimeConfig: { - GRAPHQL_SERVER_URI: process.env.GRAPHQL_SERVER_URI, - NODE_ENV: process.env.NODE_ENV, + GRAPHQL_SERVER_URI: env.GRAPHQL_SERVER_URI, + NODE_ENV: env.NODE_ENV, }, webpack: (config) => { - config.plugins.push(new webpack.EnvironmentPlugin(process.env)); + config.plugins.push(new webpack.EnvironmentPlugin(env)); return config; }, }; diff --git a/packages/app/package.json b/packages/app/package.json index eb6f1b42..ef2764f6 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -12,6 +12,7 @@ "lint:fix": "yarn lint --fix" }, "dependencies": { + "@apollo/react-hooks": "^3.1.4", "@gameofblocks/env": "^1.0.0", "@rebass/forms": "4.0.6", "@rebass/preset": "4.0.5", @@ -36,6 +37,7 @@ "memorystore": "1.6.2", "next": "9.3.4", "next-cookies": "2.0.3", + "next-with-apollo": "^5.0.0", "passport": "0.4.1", "passport-auth0": "1.3.2", "pino": "6.1.1", diff --git a/packages/app/pages/profile.tsx b/packages/app/pages/profile.tsx new file mode 100644 index 00000000..4775d6ab --- /dev/null +++ b/packages/app/pages/profile.tsx @@ -0,0 +1,10 @@ +import React, { FunctionComponent } from 'react'; + +import { Profile } from '../components/profile'; +import withApollo from '../lib/withApollo'; + +const ProfilePage: FunctionComponent = () => { + return ; +}; + +export default withApollo(ProfilePage); diff --git a/packages/app/server/index.ts b/packages/app/server/index.ts index 7942c5ff..daae50c2 100644 --- a/packages/app/server/index.ts +++ b/packages/app/server/index.ts @@ -3,16 +3,16 @@ import nextJs from 'next'; import http from 'http'; import expressPinoLogger from 'express-pino-logger'; import uid from 'uid-safe'; -import session from 'express-session'; import cors from 'cors'; import passport from 'passport'; import Auth0Strategy from 'passport-auth0'; -import MemoryStore from 'memorystore'; import env from '@gameofblocks/env'; +import session from 'express-session'; import { logger } from '../utils/logger'; import { handleError } from '../utils/error-handler'; import authRoutes from './routes/auth'; +import store, { MAX_AGE } from './session-store'; const dev = env.NODE_ENV !== 'production'; const app = nextJs({ dev }); @@ -24,17 +24,12 @@ const port = process.env.APP_PORT || 3000; await app.prepare(); const server = express(); - const SessionMemoryStore = MemoryStore(session); - const MAX_AGE = 86400000; // prune expired entries every 24h - const sessionConfig = { secret: uid.sync(18), cookie: { maxAge: MAX_AGE, }, - store: new SessionMemoryStore({ - checkPeriod: MAX_AGE, - }), + store, resave: false, saveUninitialized: true, }; diff --git a/packages/app/server/models/user.ts b/packages/app/server/models/user.ts index 70a7a20a..c8fbcae6 100644 --- a/packages/app/server/models/user.ts +++ b/packages/app/server/models/user.ts @@ -9,6 +9,7 @@ import { UpdateLastLoginVariables, User, } from './types'; +import { logger } from '../../utils/logger'; async function find(authId: string): Promise { const { data } = await client.query({ @@ -47,8 +48,10 @@ export async function loginUser(userToTest: User): Promise { const { auth_id: authId } = userToTest; let user = await find(authId); if (!user) { + logger.info(`🚫 User ${authId} does not exist. User creation attempt...`); user = await create(userToTest); } else { + logger.info(`👋 User ${authId} logged in successfully`); await updateLastLogin(authId); } return user; diff --git a/packages/app/server/routes/auth.ts b/packages/app/server/routes/auth.ts index 6af0ac65..049b16e2 100644 --- a/packages/app/server/routes/auth.ts +++ b/packages/app/server/routes/auth.ts @@ -19,6 +19,20 @@ router.get( (req, res) => res.redirect('/') ); +router.get('/hasura', (req, res) => { + logger.info('🔒 Hasura webhook. Checking authentication...'); + + if (!req.isAuthenticated()) { + res.status(401); + } else { + res.status(200).json({ + 'X-Hasura-User-Id': '', + 'X-Hasura-Role': 'user', + }); + } + res.end(); +}); + router.get('/callback', (req, res, next) => { // eslint-disable-next-line consistent-return passport.authenticate('auth0', (error, user) => { @@ -31,7 +45,8 @@ router.get('/callback', (req, res, next) => { req.logIn(user, async (err) => { if (err) return next(err); - logger.info(user, 'Auhentication success'); + + console.log('=> session', req.session); const { id, diff --git a/packages/app/server/session-store.ts b/packages/app/server/session-store.ts new file mode 100644 index 00000000..003d34fd --- /dev/null +++ b/packages/app/server/session-store.ts @@ -0,0 +1,12 @@ +import MemoryStore from 'memorystore'; +import session from 'express-session'; + +export const MAX_AGE = 86400000; + +const SessionMemoryStore = MemoryStore(session); + +const store = new SessionMemoryStore({ + checkPeriod: MAX_AGE, +}); + +export default store; diff --git a/yarn.lock b/yarn.lock index 38e2586c..e4f971c2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6735,7 +6735,7 @@ isobject@^4.0.0: resolved "https://registry.yarnpkg.com/isobject/-/isobject-4.0.0.tgz#3f1c9155e73b192022a80819bacd0343711697b0" integrity sha512-S/2fF5wH8SJA/kmwr6HYhK/RI/OkhD84k8ntalo0iJjZikgq1XFvR5M8NPT1x5F7fBwCG3qHfnzeP/Vh/ZxCUA== -isomorphic-unfetch@3.0.0: +isomorphic-unfetch@3.0.0, isomorphic-unfetch@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/isomorphic-unfetch/-/isomorphic-unfetch-3.0.0.tgz#de6d80abde487b17de2c400a7ef9e5ecc2efb362" integrity sha512-V0tmJSYfkKokZ5mgl0cmfQMTb7MLHsBMngTkbLY0eXvKqiVRRoZP04Ly+KhKrJfKtzC9E6Pp15Jo+bwh7Vi2XQ== @@ -7644,6 +7644,13 @@ next-tick@~1.0.0: resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c" integrity sha1-yobR/ogoFpsBICCOPchCS524NCw= +next-with-apollo@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/next-with-apollo/-/next-with-apollo-5.0.0.tgz#022e58c1a5c2117a6190534271e297a7fa039319" + integrity sha512-JcMecKv6XtzsLf1hPKrbxM6g9sygnGdQugglmCDkfWhHZt1yziPa+g515qtw7in8A7Ed5W9PdaaaUn7CWWQ71w== + dependencies: + isomorphic-unfetch "^3.0.0" + next@9.3.4: version "9.3.4" resolved "https://registry.yarnpkg.com/next/-/next-9.3.4.tgz#7860d414ae01e2425bf8038277f1573f9d121b57" From 6bbbc1713e616492c01d44f982a3d15de103a13d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Roycourt?= Date: Mon, 6 Apr 2020 22:11:04 +0200 Subject: [PATCH 2/9] refactor(hasura): auth --- packages/app/lib/init-apollo.ts | 42 ++++++++++++++++++++++++++++++ packages/app/lib/withApollo.tsx | 36 ------------------------- packages/app/pages/_app.tsx | 23 +++++++++++----- packages/app/pages/profile.tsx | 3 +-- packages/app/server/routes/auth.ts | 7 +++-- yarn.lock | 2 +- 6 files changed, 63 insertions(+), 50 deletions(-) create mode 100644 packages/app/lib/init-apollo.ts delete mode 100644 packages/app/lib/withApollo.tsx diff --git a/packages/app/lib/init-apollo.ts b/packages/app/lib/init-apollo.ts new file mode 100644 index 00000000..48574401 --- /dev/null +++ b/packages/app/lib/init-apollo.ts @@ -0,0 +1,42 @@ +import { + ApolloClient, + NormalizedCacheObject, + InMemoryCache, +} from 'apollo-boost'; +import { createHttpLink } from 'apollo-link-http'; +import fetch from 'cross-fetch'; + +const { GRAPHQL_SERVER_URI } = process.env; + +let apolloClient = null; + +function create(initialState): ApolloClient { + const isBrowser = typeof window !== 'undefined'; + return new ApolloClient({ + connectToDevTools: isBrowser, + ssrMode: !isBrowser, + link: createHttpLink({ + fetch: !isBrowser && fetch, + uri: GRAPHQL_SERVER_URI, + credentials: 'include', + }), + cache: new InMemoryCache().restore(initialState || {}), + }); +} + +export function initApollo(initialState): ApolloClient { + // Make sure to create a new client for every server-side request so that data + // isn't shared between connections (which would be bad) + if (typeof window === 'undefined') { + return create(initialState); + } + + // Reuse client on the client-side + if (!apolloClient) { + apolloClient = create(initialState); + } + + return apolloClient; +} + +export default initApollo; diff --git a/packages/app/lib/withApollo.tsx b/packages/app/lib/withApollo.tsx deleted file mode 100644 index c1c4cd58..00000000 --- a/packages/app/lib/withApollo.tsx +++ /dev/null @@ -1,36 +0,0 @@ -/* eslint-disable react/prop-types */ -/* eslint-disable react/jsx-props-no-spreading */ -import React from 'react'; -import withApollo from 'next-with-apollo'; -import { ApolloClient, InMemoryCache } from 'apollo-boost'; -import { ApolloProvider } from '@apollo/react-hooks'; -import { createHttpLink } from 'apollo-link-http'; -import fetch from 'cross-fetch'; - -const { GRAPHQL_SERVER_URI } = process.env; - -const httpLink = createHttpLink({ - credentials: 'include', - fetch, - uri: GRAPHQL_SERVER_URI, -}); - -export default withApollo( - ({ initialState }) => { - return new ApolloClient({ - link: httpLink, - cache: new InMemoryCache().restore(initialState || {}), - }); - }, - { - // eslint-disable-next-line react/prop-types - render: ({ Page, props }) => { - const { apollo } = props; - return ( - - - - ); - }, - } -); diff --git a/packages/app/pages/_app.tsx b/packages/app/pages/_app.tsx index c097b702..88c9d1a4 100644 --- a/packages/app/pages/_app.tsx +++ b/packages/app/pages/_app.tsx @@ -1,13 +1,22 @@ /* eslint-disable react/jsx-props-no-spreading */ import React from 'react'; -import { AppProps } from 'next/app'; +import { AppProps, Container } from 'next/app'; import { NextPage } from 'next'; +import { ApolloProvider } from 'react-apollo'; +import withApollo, { WithApolloProps } from 'next-with-apollo'; +import { initApollo } from '../lib/init-apollo'; -const GameOfBlocks: NextPage = ({ - Component, - pageProps, -}: AppProps) => { - return ; +interface MainComponentProps extends WithApolloProps, AppProps {} + +const GameOfBlocks: NextPage = (props: MainComponentProps) => { + const { Component, pageProps, apollo } = props; + return ( + + + + + + ); }; -export default GameOfBlocks; +export default withApollo(initApollo)(GameOfBlocks); diff --git a/packages/app/pages/profile.tsx b/packages/app/pages/profile.tsx index 4775d6ab..0cae6375 100644 --- a/packages/app/pages/profile.tsx +++ b/packages/app/pages/profile.tsx @@ -1,10 +1,9 @@ import React, { FunctionComponent } from 'react'; import { Profile } from '../components/profile'; -import withApollo from '../lib/withApollo'; const ProfilePage: FunctionComponent = () => { return ; }; -export default withApollo(ProfilePage); +export default ProfilePage; diff --git a/packages/app/server/routes/auth.ts b/packages/app/server/routes/auth.ts index 049b16e2..25f2c775 100644 --- a/packages/app/server/routes/auth.ts +++ b/packages/app/server/routes/auth.ts @@ -20,11 +20,12 @@ router.get( ); router.get('/hasura', (req, res) => { - logger.info('🔒 Hasura webhook. Checking authentication...'); - + logger.info('🔒 Hasura webhook. Checking authentication with session id...'); if (!req.isAuthenticated()) { + logger.info('🚫 Authentication is rejected'); res.status(401); } else { + logger.info('✅ Authentication is successful'); res.status(200).json({ 'X-Hasura-User-Id': '', 'X-Hasura-Role': 'user', @@ -46,8 +47,6 @@ router.get('/callback', (req, res, next) => { req.logIn(user, async (err) => { if (err) return next(err); - console.log('=> session', req.session); - const { id, _json: { email, picture }, diff --git a/yarn.lock b/yarn.lock index e4f971c2..27aeb013 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3014,7 +3014,7 @@ anymatch@~3.1.1: normalize-path "^3.0.0" picomatch "^2.0.4" -apollo-boost@0.4.7: +apollo-boost@^0.4.7: version "0.4.7" resolved "https://registry.yarnpkg.com/apollo-boost/-/apollo-boost-0.4.7.tgz#b0680ab0893e3f8b1ab1058dcfa2b00cb6440d79" integrity sha512-jfc3aqO0vpCV+W662EOG5gq4AH94yIsvSgAUuDvS3o/Z+8Joqn4zGC9CgLCDHusK30mFgtsEgwEe0pZoedohsQ== From 2c8564a6e88273363e827a5c8930110697da0966 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Roycourt?= Date: Tue, 7 Apr 2020 01:18:59 +0200 Subject: [PATCH 3/9] feat(hasura): use webhook authentication (wip) --- packages/app/lib/init-apollo.ts | 33 +++++++++++++++++++++------ packages/app/pages/_app.tsx | 22 ++++++++++-------- packages/app/server/graphql/client.ts | 1 + packages/app/server/routes/auth.ts | 2 ++ 4 files changed, 42 insertions(+), 16 deletions(-) diff --git a/packages/app/lib/init-apollo.ts b/packages/app/lib/init-apollo.ts index 48574401..d4aaf8b7 100644 --- a/packages/app/lib/init-apollo.ts +++ b/packages/app/lib/init-apollo.ts @@ -4,36 +4,55 @@ import { InMemoryCache, } from 'apollo-boost'; import { createHttpLink } from 'apollo-link-http'; -import fetch from 'cross-fetch'; +import fetch from 'isomorphic-unfetch'; +import nextCookies from 'next-cookies'; +import { InitApolloOptions } from 'next-with-apollo'; const { GRAPHQL_SERVER_URI } = process.env; let apolloClient = null; -function create(initialState): ApolloClient { +function create( + options: InitApolloOptions +): ApolloClient { const isBrowser = typeof window !== 'undefined'; + + const enchancedFetch = (url, init): Promise => { + const cookies = nextCookies(options.ctx); + return fetch(url, { + ...init, + credentials: 'include', + headers: { + ...init.headers, + Cookie: `${cookies.toString()};`, + }, + }); + }; + return new ApolloClient({ connectToDevTools: isBrowser, ssrMode: !isBrowser, link: createHttpLink({ - fetch: !isBrowser && fetch, + fetch: !isBrowser ? enchancedFetch : fetch, uri: GRAPHQL_SERVER_URI, credentials: 'include', }), - cache: new InMemoryCache().restore(initialState || {}), + cache: new InMemoryCache().restore(options.initialState || {}), }); } -export function initApollo(initialState): ApolloClient { +export function initApollo( + options: InitApolloOptions +): ApolloClient { // Make sure to create a new client for every server-side request so that data // isn't shared between connections (which would be bad) if (typeof window === 'undefined') { - return create(initialState); + return create(options); } // Reuse client on the client-side if (!apolloClient) { - apolloClient = create(initialState); + apolloClient = create(options); } return apolloClient; diff --git a/packages/app/pages/_app.tsx b/packages/app/pages/_app.tsx index 88c9d1a4..465b1ce1 100644 --- a/packages/app/pages/_app.tsx +++ b/packages/app/pages/_app.tsx @@ -1,22 +1,26 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ /* eslint-disable react/jsx-props-no-spreading */ import React from 'react'; -import { AppProps, Container } from 'next/app'; -import { NextPage } from 'next'; +import { AppProps } from 'next/app'; import { ApolloProvider } from 'react-apollo'; import withApollo, { WithApolloProps } from 'next-with-apollo'; import { initApollo } from '../lib/init-apollo'; interface MainComponentProps extends WithApolloProps, AppProps {} -const GameOfBlocks: NextPage = (props: MainComponentProps) => { - const { Component, pageProps, apollo } = props; - return ( - +class GameOfBlocks extends React.Component { + static async getInitialProps() { + return {}; + } + + render() { + const { Component, pageProps, apollo } = this.props; + return ( - - ); -}; + ); + } +} export default withApollo(initApollo)(GameOfBlocks); diff --git a/packages/app/server/graphql/client.ts b/packages/app/server/graphql/client.ts index 71f7f641..bddc1425 100644 --- a/packages/app/server/graphql/client.ts +++ b/packages/app/server/graphql/client.ts @@ -6,6 +6,7 @@ const { GRAPHQL_SERVER_URI, HASURA_SECRET } = env; export const client = new ApolloClient({ uri: GRAPHQL_SERVER_URI, + headers: { 'x-hasura-admin-secret': HASURA_SECRET, }, diff --git a/packages/app/server/routes/auth.ts b/packages/app/server/routes/auth.ts index 25f2c775..cf8ab71b 100644 --- a/packages/app/server/routes/auth.ts +++ b/packages/app/server/routes/auth.ts @@ -20,6 +20,8 @@ router.get( ); router.get('/hasura', (req, res) => { + console.log(req.session); + logger.info('🔒 Hasura webhook. Checking authentication with session id...'); if (!req.isAuthenticated()) { logger.info('🚫 Authentication is rejected'); From dc00ec4917ec769fe3052512fc72d41872be622b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Roycourt?= Date: Tue, 7 Apr 2020 21:41:13 +0200 Subject: [PATCH 4/9] fix(apollo): forward headers.cookie with custom fetch --- packages/app/lib/init-apollo.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/app/lib/init-apollo.ts b/packages/app/lib/init-apollo.ts index d4aaf8b7..6cecee20 100644 --- a/packages/app/lib/init-apollo.ts +++ b/packages/app/lib/init-apollo.ts @@ -5,7 +5,6 @@ import { } from 'apollo-boost'; import { createHttpLink } from 'apollo-link-http'; import fetch from 'isomorphic-unfetch'; -import nextCookies from 'next-cookies'; import { InitApolloOptions } from 'next-with-apollo'; const { GRAPHQL_SERVER_URI } = process.env; @@ -17,16 +16,15 @@ function create( ): ApolloClient { const isBrowser = typeof window !== 'undefined'; - const enchancedFetch = (url, init): Promise => { - const cookies = nextCookies(options.ctx); - return fetch(url, { + const enchancedFetch = async (url, init): Promise => { + const response = await fetch(url, { ...init, - credentials: 'include', headers: { ...init.headers, - Cookie: `${cookies.toString()};`, + Cookie: options.ctx.req.headers.cookie, }, }); + return response; }; return new ApolloClient({ From 2685706008a84fcc49e6387274f256acba2ef82b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Roycourt?= Date: Tue, 7 Apr 2020 23:52:19 +0200 Subject: [PATCH 5/9] fix(server): auth0 signup --- packages/app/server/models/types.ts | 8 +++++--- packages/app/server/models/user.ts | 9 ++++++--- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/packages/app/server/models/types.ts b/packages/app/server/models/types.ts index 98fe5668..bbd231bf 100644 --- a/packages/app/server/models/types.ts +++ b/packages/app/server/models/types.ts @@ -17,9 +17,11 @@ export interface UpdateLastLoginVariables { lastLogin: string; } -export interface UserMutationResult { - affected_rows: number; - returning: User[]; +export interface InsertUserMutationResult { + insert_user: { + affected_rows: number; + returning: User[]; + }; } export interface User { diff --git a/packages/app/server/models/user.ts b/packages/app/server/models/user.ts index c8fbcae6..1f7fc72a 100644 --- a/packages/app/server/models/user.ts +++ b/packages/app/server/models/user.ts @@ -4,7 +4,7 @@ import { CREATE_USER, UPDATE_LAST_LOGIN } from '../graphql/mutations'; import { UserQueryResult, UserQueryVariables, - UserMutationResult, + InsertUserMutationResult, UserCreationVariables, UpdateLastLoginVariables, User, @@ -23,14 +23,17 @@ async function find(authId: string): Promise { async function create(userToCreate: User): Promise { const { email, picture, auth_id: authId } = userToCreate; const { data } = await client.mutate< - UserMutationResult, + InsertUserMutationResult, UserCreationVariables >({ mutation: CREATE_USER, variables: { email, picture, authId }, }); - const [user] = data.returning; + const { + insert_user: { returning }, + } = data; + const [user] = returning; return user; } From 1136a58650ae2d49c1f54d8f524adfdefc7541f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Roycourt?= Date: Wed, 8 Apr 2020 01:37:01 +0200 Subject: [PATCH 6/9] feat(login): modify profile component --- packages/app/components/profile/Profile.tsx | 31 +++++++++++++++++++-- packages/app/components/profile/queries.ts | 4 ++- packages/app/server/graphql/queries.ts | 1 + packages/app/server/models/types.ts | 7 +++++ packages/app/server/models/user.ts | 11 ++++---- packages/app/server/routes/auth.ts | 24 ++++++++++------ 6 files changed, 62 insertions(+), 16 deletions(-) diff --git a/packages/app/components/profile/Profile.tsx b/packages/app/components/profile/Profile.tsx index c244a10f..568f1942 100644 --- a/packages/app/components/profile/Profile.tsx +++ b/packages/app/components/profile/Profile.tsx @@ -3,12 +3,39 @@ import { useQuery } from '@apollo/react-hooks'; import { USER } from './queries'; +interface QueryProps { + user: { + picture: string; + email: string; + last_login: string; + }[]; +} + export const Profile: FunctionComponent = () => { - const { data, error } = useQuery(USER); + const { data, error, loading } = useQuery(USER); if (error) { return
{error.message}
; } - return
{JSON.stringify(data)}
; + + if (loading) { + return
Chargement...
; + } + + const [user] = data.user; + const lastLoginDate = new Date(user.last_login); + + return ( +
+ {user.picture && avatar} +
{`Email: ${user.email}`}
+
+ {`Last login: ${lastLoginDate.toLocaleDateString()} ${lastLoginDate.toLocaleTimeString()}`} +
+ + + +
+ ); }; export default Profile; diff --git a/packages/app/components/profile/queries.ts b/packages/app/components/profile/queries.ts index c0c3f2c1..1a525185 100644 --- a/packages/app/components/profile/queries.ts +++ b/packages/app/components/profile/queries.ts @@ -3,7 +3,9 @@ import gql from 'graphql-tag'; export const USER = gql` query user { user { - id + picture + email + last_login } } `; diff --git a/packages/app/server/graphql/queries.ts b/packages/app/server/graphql/queries.ts index f19e1996..0f45a6b1 100644 --- a/packages/app/server/graphql/queries.ts +++ b/packages/app/server/graphql/queries.ts @@ -3,6 +3,7 @@ import gql from 'graphql-tag'; export const GET_USER = gql` query user($authId: String!) { user(limit: 1, where: { auth_id: { _eq: $authId } }) { + id auth_id email picture diff --git a/packages/app/server/models/types.ts b/packages/app/server/models/types.ts index bbd231bf..30bbe370 100644 --- a/packages/app/server/models/types.ts +++ b/packages/app/server/models/types.ts @@ -24,7 +24,14 @@ export interface InsertUserMutationResult { }; } +export interface Auth0User { + auth_id: string; + email: string; + picture: string; +} + export interface User { + id: string; auth_id: string; email: string; picture: string; diff --git a/packages/app/server/models/user.ts b/packages/app/server/models/user.ts index 1f7fc72a..fe27b9ca 100644 --- a/packages/app/server/models/user.ts +++ b/packages/app/server/models/user.ts @@ -8,10 +8,11 @@ import { UserCreationVariables, UpdateLastLoginVariables, User, + Auth0User, } from './types'; import { logger } from '../../utils/logger'; -async function find(authId: string): Promise { +export async function find(authId: string): Promise { const { data } = await client.query({ query: GET_USER, variables: { authId }, @@ -20,7 +21,7 @@ async function find(authId: string): Promise { return data ? data.user[0] : null; } -async function create(userToCreate: User): Promise { +async function create(userToCreate: Auth0User): Promise { const { email, picture, auth_id: authId } = userToCreate; const { data } = await client.mutate< InsertUserMutationResult, @@ -47,12 +48,12 @@ async function updateLastLogin(authId: string): Promise { }); } -export async function loginUser(userToTest: User): Promise { - const { auth_id: authId } = userToTest; +export async function loginUser(auth0User: Auth0User): Promise { + const { auth_id: authId } = auth0User; let user = await find(authId); if (!user) { logger.info(`🚫 User ${authId} does not exist. User creation attempt...`); - user = await create(userToTest); + user = await create(auth0User); } else { logger.info(`👋 User ${authId} logged in successfully`); await updateLastLogin(authId); diff --git a/packages/app/server/routes/auth.ts b/packages/app/server/routes/auth.ts index cf8ab71b..7eac1a07 100644 --- a/packages/app/server/routes/auth.ts +++ b/packages/app/server/routes/auth.ts @@ -4,7 +4,7 @@ import bodyParser from 'body-parser'; import env from '@gameofblocks/env'; import { logger } from '../../utils/logger'; -import { loginUser } from '../models/user'; +import { loginUser, find } from '../models/user'; const router = Router(); router.use(bodyParser.json()); @@ -19,19 +19,27 @@ router.get( (req, res) => res.redirect('/') ); -router.get('/hasura', (req, res) => { - console.log(req.session); - +router.get('/hasura', async (req, res) => { logger.info('🔒 Hasura webhook. Checking authentication with session id...'); if (!req.isAuthenticated()) { logger.info('🚫 Authentication is rejected'); res.status(401); } else { logger.info('✅ Authentication is successful'); - res.status(200).json({ - 'X-Hasura-User-Id': '', - 'X-Hasura-Role': 'user', - }); + if (req.user) { + // TODO(remiroyc): find a better way to type req.user. + // @types/password defines an empty interface for User. + const { id } = req.user as { id: string }; + const user = await find(id); + res.status(200).json({ + 'X-Hasura-User-Id': user.id, + 'X-Hasura-Role': 'user', + }); + } else { + res.status(200).json({ + 'X-Hasura-Role': 'anonymous', + }); + } } res.end(); }); From b8dba41c2b6229cf3aeea949fe0b0d58951913c2 Mon Sep 17 00:00:00 2001 From: kwiss Date: Sat, 11 Apr 2020 11:42:54 +0200 Subject: [PATCH 7/9] feat(hasura-k8s): add auth webhook env --- .k8s/hasura/deployment.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.k8s/hasura/deployment.yaml b/.k8s/hasura/deployment.yaml index 70f8dcd0..7ffe74b0 100644 --- a/.k8s/hasura/deployment.yaml +++ b/.k8s/hasura/deployment.yaml @@ -37,6 +37,8 @@ spec: value: ${HASURA_SECRET} - name: HASURA_GRAPHQL_ENABLE_CONSOLE value: 'true' + - name: HASURA_GRAPHQL_AUTH_HOOK + value: gameofblocks-${CI_ENVIRONMENT_SLUG}/hasura ports: - name: http-metrics protocol: TCP From d469e4563789bdcde809bee6280245aeec6ff2a4 Mon Sep 17 00:00:00 2001 From: kwiss Date: Sat, 11 Apr 2020 11:57:00 +0200 Subject: [PATCH 8/9] chore(package): update yarn lock --- yarn.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yarn.lock b/yarn.lock index 27aeb013..e4f971c2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3014,7 +3014,7 @@ anymatch@~3.1.1: normalize-path "^3.0.0" picomatch "^2.0.4" -apollo-boost@^0.4.7: +apollo-boost@0.4.7: version "0.4.7" resolved "https://registry.yarnpkg.com/apollo-boost/-/apollo-boost-0.4.7.tgz#b0680ab0893e3f8b1ab1058dcfa2b00cb6440d79" integrity sha512-jfc3aqO0vpCV+W662EOG5gq4AH94yIsvSgAUuDvS3o/Z+8Joqn4zGC9CgLCDHusK30mFgtsEgwEe0pZoedohsQ== From ad7526954ccbf6a572ceadd1abd06d45a4575d85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Roycourt?= Date: Sat, 11 Apr 2020 13:58:16 +0200 Subject: [PATCH 9/9] fix(types): rename & move type definitions --- packages/app/components/profile/Profile.tsx | 12 ++---------- packages/app/components/profile/queries.ts | 8 ++++++++ packages/app/server/models/types.ts | 14 +++++++------- packages/app/server/models/user.ts | 8 ++++---- 4 files changed, 21 insertions(+), 21 deletions(-) diff --git a/packages/app/components/profile/Profile.tsx b/packages/app/components/profile/Profile.tsx index 568f1942..a2030663 100644 --- a/packages/app/components/profile/Profile.tsx +++ b/packages/app/components/profile/Profile.tsx @@ -1,18 +1,10 @@ import React, { FunctionComponent } from 'react'; import { useQuery } from '@apollo/react-hooks'; -import { USER } from './queries'; - -interface QueryProps { - user: { - picture: string; - email: string; - last_login: string; - }[]; -} +import { USER, UserQueryProps } from './queries'; export const Profile: FunctionComponent = () => { - const { data, error, loading } = useQuery(USER); + const { data, error, loading } = useQuery(USER); if (error) { return
{error.message}
; } diff --git a/packages/app/components/profile/queries.ts b/packages/app/components/profile/queries.ts index 1a525185..4f17f871 100644 --- a/packages/app/components/profile/queries.ts +++ b/packages/app/components/profile/queries.ts @@ -1,5 +1,13 @@ import gql from 'graphql-tag'; +export interface UserQueryProps { + user: { + picture: string; + email: string; + last_login: string; + }[]; +} + export const USER = gql` query user { user { diff --git a/packages/app/server/models/types.ts b/packages/app/server/models/types.ts index 30bbe370..88e39d50 100644 --- a/packages/app/server/models/types.ts +++ b/packages/app/server/models/types.ts @@ -6,24 +6,24 @@ export interface UserQueryVariables { authId: string; } -export interface UserCreationVariables { +export interface CreateUserVariables { email: string; picture: string; authId: string; } -export interface UpdateLastLoginVariables { - authId: string; - lastLogin: string; -} - -export interface InsertUserMutationResult { +export interface CreateUserMutationResult { insert_user: { affected_rows: number; returning: User[]; }; } +export interface UpdateLastLoginVariables { + authId: string; + lastLogin: string; +} + export interface Auth0User { auth_id: string; email: string; diff --git a/packages/app/server/models/user.ts b/packages/app/server/models/user.ts index fe27b9ca..d884c4ad 100644 --- a/packages/app/server/models/user.ts +++ b/packages/app/server/models/user.ts @@ -4,8 +4,8 @@ import { CREATE_USER, UPDATE_LAST_LOGIN } from '../graphql/mutations'; import { UserQueryResult, UserQueryVariables, - InsertUserMutationResult, - UserCreationVariables, + CreateUserMutationResult, + CreateUserVariables, UpdateLastLoginVariables, User, Auth0User, @@ -24,8 +24,8 @@ export async function find(authId: string): Promise { async function create(userToCreate: Auth0User): Promise { const { email, picture, auth_id: authId } = userToCreate; const { data } = await client.mutate< - InsertUserMutationResult, - UserCreationVariables + CreateUserMutationResult, + CreateUserVariables >({ mutation: CREATE_USER, variables: { email, picture, authId },