Base architecture (#1)

* Hasura and base models

* Added role public SELECT permissions

* Added role player UPDATE permissions

* basic backend api

* Added SELECT permissions for player

* Update backend to typescript

* init app-react

* Add apollo

* graphql-codegen not working well...

* Added web3 to web app

* connecting frontend with web3

* Auth webhook verifies eth signature

* Update frontend to fetch player_id
This commit is contained in:
Pacien Boisson
2020-04-16 10:20:15 +02:00
committed by GitHub
parent 30c2df6211
commit c684b9d836
66 changed files with 20070 additions and 0 deletions

6
.editorconfig Normal file
View File

@@ -0,0 +1,6 @@
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true

9
.env.sample Normal file
View File

@@ -0,0 +1,9 @@
COMPOSE_PROJECT_NAME=the-game
DATABASE_USER=metagame
DATABASE_PASSWORD=metagame_secret
DATABASE_NAME=thegame
HASURA_GRAPHQL_ADMIN_SECRET=metagame_secret
HASURA_PORT=8080

14
.gitignore vendored Normal file
View File

@@ -0,0 +1,14 @@
.DS_Store
.idea
node_modules
.yarn/*
!.yarn/releases
!.yarn/plugins
.pnp.*
.pnp.js
yarn-error.log
.env

View File

@@ -1,3 +1,36 @@
# The Game
Monorepo for the MetaGame applications, backend and databases.
## Development
### Bootstrap
```shell script
cp .env.sample .env
npm run docker:start
```
### Tooling
Start Hasura console
```shell script
npm run hasura:console
```
Hasura CLI example
```shell script
npm run hasura -- migrate squash 1586952135212
```
[Hasura CLI documentation](https://hasura.io/docs/1.0/graphql/manual/hasura-cli/index.html)
### Restart with fresh database
```shell script
npm run docker:clean
npm run docker:start:local
```

35
docker-compose.yml Normal file
View File

@@ -0,0 +1,35 @@
---
version: '3.6'
services:
hasura:
build: ./hasura
depends_on:
- database
ports:
- ${HASURA_PORT}:8080
environment:
PORT: 8080
DATABASE_URL: postgres://${DATABASE_USER}:${DATABASE_PASSWORD}@database:5432/${DATABASE_NAME}
HASURA_GRAPHQL_ADMIN_SECRET: ${HASURA_GRAPHQL_ADMIN_SECRET}
HASURA_GRAPHQL_AUTH_HOOK: http://backend:4000/auth-webhook
database:
image: postgres:12
volumes:
- database:/var/lib/postgresql/data
environment:
POSTGRES_USER: ${DATABASE_USER}
POSTGRES_PASSWORD: ${DATABASE_PASSWORD}
POSTGRES_DB: ${DATABASE_NAME}
backend:
build: ./packages/backend
environment:
PORT: 4000
GRAPHQL_URL: http://hasura:8080/v1/graphql
HASURA_GRAPHQL_ADMIN_SECRET: ${HASURA_GRAPHQL_ADMIN_SECRET}
volumes:
database:
...

19
hasura/Dockerfile Normal file
View File

@@ -0,0 +1,19 @@
FROM hasura/graphql-engine:v1.1.1.cli-migrations
## Default setup
ENV HASURA_GRAPHQL_ENABLE_TELEMETRY false
ENV HASURA_GRAPHQL_ENABLE_CONSOLE false
ENV HASURA_GRAPHQL_ENABLED_LOG_TYPES startup, http-log, webhook-log, websocket-log, query-log
## Migrations
COPY migrations /hasura-migrations
ENV HASURA_GRAPHQL_MIGRATIONS_DATABASE_ENV_VAR DATABASE_URL
## Execution
CMD graphql-engine \
--database-url $DATABASE_URL \
serve \
--server-port $PORT

2
hasura/config.yaml Normal file
View File

@@ -0,0 +1,2 @@
endpoint: http://localhost:8080
admin_secret: metagame_secret

View File

@@ -0,0 +1,99 @@
-- Enums
CREATE TYPE "Profile_Type" AS ENUM (
'ETHEREUM',
'DISCORD',
'GITHUB',
'DISCOURSE'
);
CREATE TYPE "Rank" AS ENUM (
'PLAYER',
'BRONZE',
'SILVER',
'GOLDEN',
'PLATINIUM',
'DIAMOND'
);
-- Tables
CREATE TABLE "Player" (
"id" uuid DEFAULT public.gen_random_uuid() NOT NULL,
"totalXp" numeric DEFAULT 0,
"rank" "Rank" NOT NULL DEFAULT 'PLAYER',
"links" json,
"sentences" json
);
CREATE TABLE "Profile" (
"player_id" uuid NOT NULL,
"identifier" text NOT NULL,
"linkToProof" text,
"type" "Profile_Type" NOT NULL
);
CREATE TABLE "Quest" (
"id" uuid DEFAULT public.gen_random_uuid() NOT NULL,
"name" text NOT NULL,
"description" text,
"url" text NOT NULL,
"xp" numeric NOT NULL DEFAULT 0
);
CREATE TABLE "Quest_Completed" (
"quest_id" uuid NOT NULL,
"player_id" uuid NOT NULL,
"time" timestamp
);
CREATE TABLE "XPInterval" (
"player_id" uuid NOT NULL,
"startTime" date,
"endTime" date,
"xp" numeric NOT NULL
);
CREATE TABLE "Guild" (
"id" uuid DEFAULT public.gen_random_uuid() NOT NULL,
"name" text
);
CREATE TABLE "Guild_Member" (
"guild_id" uuid NOT NULL,
"player_id" uuid NOT NULL
);
-- Primary keys
ALTER TABLE ONLY public."Player"
ADD CONSTRAINT "Player_pkey" PRIMARY KEY (id);
ALTER TABLE ONLY public."Quest"
ADD CONSTRAINT "Quest_pkey" PRIMARY KEY (id);
ALTER TABLE ONLY public."Guild"
ADD CONSTRAINT "Guild_pkey" PRIMARY KEY (id);
ALTER TABLE ONLY public."Quest_Completed"
ADD CONSTRAINT "Quest_Completed_pkey" PRIMARY KEY (quest_id, player_id);
ALTER TABLE ONLY public."Guild_Member"
ADD CONSTRAINT "Guild_Member_pkey" PRIMARY KEY (guild_id, player_id);
-- Uniques
ALTER TABLE ONLY public."Profile"
ADD CONSTRAINT "Profile_identifier_key" UNIQUE (identifier);
-- Foreign keys
ALTER TABLE "Profile" ADD CONSTRAINT "Profile_player_id_fkey" FOREIGN KEY ("player_id") REFERENCES "Player" ("id");
ALTER TABLE "Quest_Completed" ADD CONSTRAINT "Quest_Completed_player_id_fkey" FOREIGN KEY ("player_id") REFERENCES "Player" ("id");
ALTER TABLE "Quest_Completed" ADD CONSTRAINT "Quest_Completed_quest_id_fkey" FOREIGN KEY ("quest_id") REFERENCES "Quest" ("id");
ALTER TABLE "Guild_Member" ADD CONSTRAINT "Guild_Member_player_id_fkey" FOREIGN KEY ("player_id") REFERENCES "Player" ("id");
ALTER TABLE "Guild_Member" ADD CONSTRAINT "Guild_Member_guild_id_fkey" FOREIGN KEY ("guild_id") REFERENCES "Guild" ("id");
ALTER TABLE "XPInterval" ADD CONSTRAINT "XPInterval_player_id_fkey" FOREIGN KEY ("player_id") REFERENCES "Player" ("id");

View File

@@ -0,0 +1,92 @@
- args:
tables:
- array_relationships:
- name: Guild_Members
using:
foreign_key_constraint_on:
column: guild_id
table:
name: Guild_Member
schema: public
table:
name: Guild
schema: public
- object_relationships:
- name: Guild
using:
foreign_key_constraint_on: guild_id
- name: Player
using:
foreign_key_constraint_on: player_id
table:
name: Guild_Member
schema: public
- array_relationships:
- name: Guild_Members
using:
foreign_key_constraint_on:
column: player_id
table:
name: Guild_Member
schema: public
- name: Profiles
using:
foreign_key_constraint_on:
column: player_id
table:
name: Profile
schema: public
- name: Quest_Completeds
using:
foreign_key_constraint_on:
column: player_id
table:
name: Quest_Completed
schema: public
- name: XPIntervals
using:
foreign_key_constraint_on:
column: player_id
table:
name: XPInterval
schema: public
table:
name: Player
schema: public
- object_relationships:
- name: Player
using:
foreign_key_constraint_on: player_id
table:
name: Profile
schema: public
- array_relationships:
- name: Quest_Completeds
using:
foreign_key_constraint_on:
column: quest_id
table:
name: Quest_Completed
schema: public
table:
name: Quest
schema: public
- object_relationships:
- name: Player
using:
foreign_key_constraint_on: player_id
- name: Quest
using:
foreign_key_constraint_on: quest_id
table:
name: Quest_Completed
schema: public
- object_relationships:
- name: Player
using:
foreign_key_constraint_on: player_id
table:
name: XPInterval
schema: public
version: 2
type: replace_metadata

View File

@@ -0,0 +1,42 @@
- args:
role: public
table:
name: XPInterval
schema: public
type: drop_select_permission
- args:
role: public
table:
name: Quest_Completed
schema: public
type: drop_select_permission
- args:
role: public
table:
name: Quest
schema: public
type: drop_select_permission
- args:
role: public
table:
name: Profile
schema: public
type: drop_select_permission
- args:
role: public
table:
name: Guild_Member
schema: public
type: drop_select_permission
- args:
role: public
table:
name: Guild
schema: public
type: drop_select_permission
- args:
role: public
table:
name: Player
schema: public
type: drop_select_permission

View File

@@ -0,0 +1,109 @@
- args:
permission:
allow_aggregations: false
columns:
- id
- totalXp
- rank
- links
- sentences
computed_fields: []
filter: {}
limit: null
role: public
table:
name: Player
schema: public
type: create_select_permission
- args:
permission:
allow_aggregations: false
columns:
- id
- name
computed_fields: []
filter: {}
limit: null
role: public
table:
name: Guild
schema: public
type: create_select_permission
- args:
permission:
allow_aggregations: false
columns:
- guild_id
- player_id
computed_fields: []
filter: {}
limit: null
role: public
table:
name: Guild_Member
schema: public
type: create_select_permission
- args:
permission:
allow_aggregations: false
columns:
- player_id
- identifier
- linkToProof
- type
computed_fields: []
filter: {}
limit: null
role: public
table:
name: Profile
schema: public
type: create_select_permission
- args:
permission:
allow_aggregations: false
columns:
- id
- name
- description
- url
- xp
computed_fields: []
filter: {}
limit: null
role: public
table:
name: Quest
schema: public
type: create_select_permission
- args:
permission:
allow_aggregations: false
columns:
- quest_id
- player_id
- time
computed_fields: []
filter: {}
limit: null
role: public
table:
name: Quest_Completed
schema: public
type: create_select_permission
- args:
permission:
allow_aggregations: false
columns:
- player_id
- startTime
- endTime
- xp
computed_fields: []
filter: {}
limit: null
role: public
table:
name: XPInterval
schema: public
type: create_select_permission

View File

@@ -0,0 +1,48 @@
- args:
role: player
table:
name: XPInterval
schema: public
type: drop_select_permission
- args:
role: player
table:
name: Quest_Completed
schema: public
type: drop_select_permission
- args:
role: player
table:
name: Quest
schema: public
type: drop_select_permission
- args:
role: player
table:
name: Profile
schema: public
type: drop_select_permission
- args:
role: player
table:
name: Guild_Member
schema: public
type: drop_select_permission
- args:
role: player
table:
name: Guild
schema: public
type: drop_select_permission
- args:
role: player
table:
name: Player
schema: public
type: drop_select_permission
- args:
role: player
table:
name: Player
schema: public
type: drop_update_permission

View File

@@ -0,0 +1,125 @@
- args:
permission:
allow_aggregations: false
columns:
- id
- totalXp
- rank
- links
- sentences
computed_fields: []
filter: {}
limit: null
role: player
table:
name: Player
schema: public
type: create_select_permission
- args:
permission:
allow_aggregations: false
columns:
- id
- name
computed_fields: []
filter: {}
limit: null
role: player
table:
name: Guild
schema: public
type: create_select_permission
- args:
permission:
allow_aggregations: false
columns:
- guild_id
- player_id
computed_fields: []
filter: {}
limit: null
role: player
table:
name: Guild_Member
schema: public
type: create_select_permission
- args:
permission:
allow_aggregations: false
columns:
- player_id
- identifier
- linkToProof
- type
computed_fields: []
filter: {}
limit: null
role: player
table:
name: Profile
schema: public
type: create_select_permission
- args:
permission:
allow_aggregations: false
columns:
- id
- name
- description
- url
- xp
computed_fields: []
filter: {}
limit: null
role: player
table:
name: Quest
schema: public
type: create_select_permission
- args:
permission:
allow_aggregations: false
columns:
- quest_id
- player_id
- time
computed_fields: []
filter: {}
limit: null
role: player
table:
name: Quest_Completed
schema: public
type: create_select_permission
- args:
permission:
allow_aggregations: false
columns:
- player_id
- startTime
- endTime
- xp
computed_fields: []
filter: {}
limit: null
role: player
table:
name: XPInterval
schema: public
type: create_select_permission
- args:
permission:
columns:
- sentences
filter:
id:
_eq: X-Hasura-User-Id
localPresets:
- key: ""
value: ""
set: {}
role: player
table:
name: Player
schema: public
type: create_update_permission

95
package-lock.json generated Normal file
View File

@@ -0,0 +1,95 @@
{
"name": "@metafam/the-game",
"version": "0.1.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"ansi-styles": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
"requires": {
"color-convert": "^1.9.0"
}
},
"axios": {
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz",
"integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==",
"requires": {
"follow-redirects": "1.5.10"
}
},
"chalk": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
"requires": {
"ansi-styles": "^3.2.1",
"escape-string-regexp": "^1.0.5",
"supports-color": "^5.3.0"
}
},
"color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"requires": {
"color-name": "1.1.3"
}
},
"color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
},
"debug": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
"requires": {
"ms": "2.0.0"
}
},
"escape-string-regexp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
},
"follow-redirects": {
"version": "1.5.10",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz",
"integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==",
"requires": {
"debug": "=3.1.0"
}
},
"has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0="
},
"hasura-cli": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/hasura-cli/-/hasura-cli-1.1.1.tgz",
"integrity": "sha512-mnqLdVLozScAQG91e/OgcJODC+TftlW9NRlJKXwGuG0emfqu7mtbo3D03P0bSKTMOwm7jgYzAbkLjnHexVmdyQ==",
"requires": {
"axios": "^0.19.0",
"chalk": "^2.4.2"
}
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
"supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"requires": {
"has-flag": "^3.0.0"
}
}
}
}

18
package.json Normal file
View File

@@ -0,0 +1,18 @@
{
"name": "@metafam/the-game",
"version": "0.1.0",
"license": "MIT",
"scripts": {
"docker:start": "docker-compose up -d database && sleep 2 && docker-compose up --build -d",
"docker:stop": "docker-compose down",
"docker:clean": "docker-compose down -v",
"hasura": "hasura --project ./hasura",
"hasura:console": "npm run hasura console -- --no-browser",
"hasura:migrate:init": "npm run hasura migrate create \"init\" -- --from-server",
"hasura:migrate:apply": "npm run hasura migrate apply",
"test": "cd packages/tests && yarn test && cd ../.."
},
"dependencies": {
"hasura-cli": "1.1.1"
}
}

23
packages/app-react/.gitignore vendored Normal file
View File

@@ -0,0 +1,23 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

View File

@@ -0,0 +1,44 @@
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
## Available Scripts
In the project directory, you can run:
### `yarn start`
Runs the app in the development mode.<br />
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
The page will reload if you make edits.<br />
You will also see any lint errors in the console.
### `yarn test`
Launches the test runner in the interactive watch mode.<br />
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
### `yarn build`
Builds the app for production to the `build` folder.<br />
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.<br />
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### `yarn eject`
**Note: this is a one-way operation. Once you `eject`, you cant go back!**
If you arent satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point youre on your own.
You dont have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldnt feel obligated to use this feature. However we understand that this tool wouldnt be useful if you couldnt customize it when you are ready for it.
## Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).

View File

@@ -0,0 +1,50 @@
{
"name": "@the-game/app-react",
"version": "0.1.0",
"private": true,
"dependencies": {
"@apollo/client": "^3.0.0-beta.43",
"@apollo/react-hooks": "^3.1.5",
"@material-ui/core": "^4.9.10",
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.3.2",
"@testing-library/user-event": "^7.1.2",
"@types/jest": "^24.0.0",
"@types/node": "^12.0.0",
"@types/react": "^16.9.0",
"@types/react-dom": "^16.9.0",
"@walletconnect/web3-provider": "^1.0.0-beta.47",
"apollo-boost": "^0.4.7",
"ethers": "^4.0.46",
"graphql": "^15.0.0",
"graphql-codegen-hasura-core": "^4.8.4",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-scripts": "3.4.1",
"typescript": "~3.7.2",
"uuid": "^7.0.3",
"web3": "^1.2.6",
"web3modal": "^1.3.0"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": "react-app"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="The Meta Game"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>The Game</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

@@ -0,0 +1,25 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View File

@@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View File

@@ -0,0 +1,9 @@
import React from 'react';
import { render } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
const { getByText } = render(<App />);
const linkElement = getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});

View File

@@ -0,0 +1,23 @@
import React from 'react';
import { ApolloProvider } from '@apollo/react-hooks';
import { CssBaseline } from '@material-ui/core';
import { createApolloClient } from './apollo';
import Home from './containers/Home';
import Web3ContextProvider from './contexts/Web3';
const apolloClient = createApolloClient();
function App() {
return (
<ApolloProvider client={apolloClient}>
<Web3ContextProvider>
<CssBaseline/>
<Home/>
</Web3ContextProvider>
</ApolloProvider>
);
}
export default App;

View File

@@ -0,0 +1,72 @@
import queries from '../graphql/queries';
import { getSignerAddress } from '../lib/did';
const STORAGE_KEY = 'auth-token';
function getTokenFromStore() {
return localStorage.getItem(STORAGE_KEY);
}
function setTokenInStore(token) {
return localStorage.setItem(STORAGE_KEY, token);
}
function clearToken() {
return localStorage.removeItem(STORAGE_KEY);
}
export function loginLoading(client) {
client.writeData({
data: {
authState: 'loading',
},
});
}
export async function login(client, token, ethAddress) {
client.writeData({
data: {
authState: 'loading',
authToken: token,
},
});
setTokenInStore(token);
return client.query({
query: queries.get_MyProfile,
variables: { eth_address: ethAddress }
})
.then(async res => {
if(res.data.Profile.length === 0) {
throw new Error('Impossible to fetch player, not found.');
}
client.writeData({
data: {
authState: 'logged',
playerId: res.data.Profile[0].Player.id,
},
});
setTokenInStore(token);
})
.catch(async error => {
client.writeData({
data: {
authState: 'error',
authToken: null,
},
});
throw error;
});
}
export function logout() {
clearToken();
}
export function checkStoredAuth(client) {
const token = getTokenFromStore();
if(token) {
const address = getSignerAddress(token);
login(client, token, address).catch(console.error)
}
}

View File

@@ -0,0 +1,58 @@
import ApolloClient from 'apollo-boost';
import config from '../config';
import { localQueries, logout } from './index';
import { checkStoredAuth } from './auth';
export function createApolloClient() {
let client;
const defaultClientState = {
authState: 'anonymous',
authToken: null,
playerId: null,
};
async function authMiddleware(operation) {
const queryLogin = client.readQuery({ query: localQueries.get_authState });
if(queryLogin.authToken) {
operation.setContext({
headers: {
'authorization': `Bearer ${queryLogin.authToken}`,
},
});
}
}
function onErrorMiddleware({ networkError = {}, graphQLErrors = {}, operation }) {
if (networkError.statusCode === 401 || graphQLErrors[0]?.extensions?.code === 'invalid-jwt') {
console.error('Authentication error, login out');
logout(client);
client.resetStore();
}
else {
console.error('GraphQL request error:', networkError);
}
}
client = new ApolloClient({
uri: config.graphqlURL,
request: authMiddleware,
onError: onErrorMiddleware,
clientState: {
defaults: defaultClientState,
resolvers: {},
},
});
client.onResetStore(() => {
client.writeData({ data: defaultClientState })
});
checkStoredAuth(client);
return client;
}

View File

@@ -0,0 +1,7 @@
import { createApolloClient } from './client';
import { login, logout } from './auth';
import * as localQueries from './localQueries';
export {
localQueries, createApolloClient, login, logout,
};

View File

@@ -0,0 +1,9 @@
import { gql } from 'apollo-boost';
export const get_authState = gql`
query AuthState {
authState @client
authToken @client
playerId @client
}
`;

View File

@@ -0,0 +1,11 @@
import React from 'react';
import { Box } from '@material-ui/core';
export default function Player({ player }: { player: any }) {
return (
<Box>
{player.id}
</Box>
)
}

View File

@@ -0,0 +1,4 @@
export default {
graphqlURL: process.env.GRAPHQL_URL || 'http://localhost:8080/v1/graphql',
infuraId: process.env.INFURA_ID || '781d8466252d47508e177b8637b1c2fd',
};

View File

@@ -0,0 +1,21 @@
import React from 'react';
import { Box } from '@material-ui/core';
import { useQuery } from '@apollo/react-hooks';
import PlayerList from './PlayerList';
import Login from './Login';
import MyPlayer from './MyPlayer';
import {localQueries} from "../apollo";
export default function Home() {
const { data, loading } = useQuery(localQueries.get_authState);
return (
<Box>
<PlayerList/>
<Login/>
{!loading && data?.authState === 'logged' && <MyPlayer/>}
</Box>
);
}

View File

@@ -0,0 +1,39 @@
import React, {useContext} from 'react';
import { Box } from '@material-ui/core';
import { Web3Context } from '../contexts/Web3';
import {localQueries} from "../apollo";
import { useQuery } from '@apollo/react-hooks';
export default function Login() {
const { data, loading } = useQuery(localQueries.get_authState);
const { connectWeb3 } = useContext(Web3Context);
if(loading || data?.authState === 'loading') {
return (
<Box>
Connecting...
</Box>
);
} else if(data?.authState === 'logged') {
return (
<Box>Connected</Box>
);
} else if(data?.authState === 'error') {
return (
<Box>
Connection error
</Box>
);
} else if(data?.authState === 'anonymous') {
return (
<Box>
<button onClick={connectWeb3}>Connect</button>
</Box>
);
}
return 'Unknown state'
}

View File

@@ -0,0 +1,25 @@
import React from 'react';
import { Box } from '@material-ui/core';
import Player from '../components/Player';
import { useMyPlayer } from '../graphql/hooks';
export default function MyPlayer() {
const { data, called, loading, error } = useMyPlayer();
if(error) {
return <div>error</div>
}
if(loading || !called) {
return <div>loading</div>
}
const myPlayer = data.Player[0];
return (
<Box>
<h4>My player</h4>
<Player player={myPlayer} />
</Box>
)
}

View File

@@ -0,0 +1,25 @@
import React from 'react';
import { Box } from '@material-ui/core';
import {useQuery} from '@apollo/react-hooks';
import queries from '../graphql/queries';
import Player from '../components/Player';
export default function PlayerList() {
const { data, loading } = useQuery(queries.get_Player);
if(loading) {
return <div>loading</div>
}
return (
<Box>
<h4>Player list</h4>
{data.Player.map((player: any) =>
<Player key={player.id} player={player} />
)}
</Box>
)
}

View File

@@ -0,0 +1,71 @@
import React, {createContext, useCallback, useState} from 'react';
import WalletConnectProvider from '@walletconnect/web3-provider';
import Web3Modal from 'web3modal';
import Web3 from 'web3';
import { ethers } from 'ethers';
import {AsyncSendable} from 'ethers/providers';
import {useApolloClient} from '@apollo/react-hooks';
import config from '../config';
import {createToken} from '../lib/did';
import {loginLoading, login} from '../apollo/auth';
export const Web3Context = createContext({
ethersProvider: null,
connectWeb3: () => {},
});
const providerOptions = {
walletconnect: {
package: WalletConnectProvider,
options: {
infuraId: config.infuraId,
}
}
};
const web3Modal = new Web3Modal({
network: 'mainnet',
cacheProvider: true,
providerOptions,
});
const Web3ContextProvider = props => {
const apolloClient = useApolloClient();
const [ethersProvider, setEthersProvider] = useState(null);
const connectWeb3 = useCallback(async () => {
loginLoading(apolloClient);
try {
const provider = await web3Modal.connect();
const web3Provider = new Web3(provider);
const ethersProvider = new ethers.providers.Web3Provider(web3Provider.currentProvider as AsyncSendable);
const signer = ethersProvider.getSigner();
const address = await signer.getAddress();
const token = await createToken(ethersProvider);
console.log(token);
await login(apolloClient, token, address);
setEthersProvider(ethersProvider);
} catch(error) {
console.error('impossible to connect', error);
}
}, [apolloClient]);
return (
<Web3Context.Provider value={{ ethersProvider, connectWeb3 }}>
{props.children}
</Web3Context.Provider>
);
};
export default Web3ContextProvider;

View File

@@ -0,0 +1,14 @@
const fragments: any = {};
fragments.PlayerFragment = `
fragment PlayerFragment on Player {
id
}
`;
fragments.ProfileFragment = `
fragment ProfileFragment on Profile {
identifier
}
`;
export default fragments;

View File

@@ -0,0 +1,25 @@
import { useEffect } from 'react';
import { useQuery, useLazyQuery } from '@apollo/react-hooks';
import { localQueries } from '../apollo';
import queries from './queries';
export function useMyPlayer() {
const authStateQuery = useQuery(localQueries.get_authState);
const [getMyPlayer, myPlayerQuery] = useLazyQuery(queries.get_MyPlayer);
const playerId = authStateQuery.data?.playerId;
useEffect(() => {
if(playerId) {
getMyPlayer({
variables: {
player_id: playerId,
},
});
}
}, [playerId]);
return myPlayerQuery;
}

View File

@@ -0,0 +1,45 @@
import gql from 'graphql-tag';
import fragments from './fragments';
const queries: any = {};
queries.get_Player = gql`
query GetPlayer {
Player {
...PlayerFragment
}
}
${fragments.PlayerFragment}
`;
queries.get_MyPlayer = gql`
query GetPlayer($player_id: uuid) {
Player(
where: { id: { _eq: $player_id } }
) {
...PlayerFragment
}
}
${fragments.PlayerFragment}
`;
queries.get_MyProfile = gql`
query GetMyProfile($eth_address: String) {
Profile(
where: {
identifier: { _eq: $eth_address },
type: { _eq: "ETHEREUM" }
}
) {
...ProfileFragment
Player {
...PlayerFragment
}
}
}
${fragments.PlayerFragment}
${fragments.ProfileFragment}
`;
export default queries;

View File

@@ -0,0 +1,11 @@
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import * as serviceWorker from './serviceWorker';
ReactDOM.render(
<App />,
document.getElementById('root')
);
serviceWorker.unregister();

View File

@@ -0,0 +1,39 @@
import { v4 as uuidv4 } from 'uuid';
import { ethers } from "ethers";
const tokenDuration = 1000 * 60 * 60 * 24 * 7; // 7 days
export async function createToken(provider) {
const signer = provider.getSigner();
const address = await signer.getAddress();
const iat = +new Date();
const claim = {
iat: +new Date(),
exp: iat + tokenDuration,
iss: address,
aud: 'the-game',
tid: uuidv4(),
};
const serializedClaim = JSON.stringify(claim);
const proof = await signer.signMessage(serializedClaim);
const DIDToken = btoa(JSON.stringify([proof, serializedClaim]));
return DIDToken;
}
export function getSignerAddress(token: string): any {
try {
const rawToken = atob(token);
const [proof, rawClaim] = JSON.parse(rawToken);
const signerAddress = ethers.utils.verifyMessage(rawClaim, proof);
return signerAddress;
} catch (e) {
console.error('Token verification failed', e);
return null;
}
}

View File

@@ -0,0 +1 @@
/// <reference types="react-scripts" />

View File

@@ -0,0 +1,149 @@
// This optional code is used to register a service worker.
// register() is not called by default.
// This lets the app load faster on subsequent visits in production, and gives
// it offline capabilities. However, it also means that developers (and users)
// will only see deployed updates on subsequent visits to a page, after all the
// existing tabs open on the page have been closed, since previously cached
// resources are updated in the background.
// To learn more about the benefits of this model and instructions on how to
// opt-in, read https://bit.ly/CRA-PWA
const isLocalhost = Boolean(
window.location.hostname === 'localhost' ||
// [::1] is the IPv6 localhost address.
window.location.hostname === '[::1]' ||
// 127.0.0.0/8 are considered localhost for IPv4.
window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
)
);
type Config = {
onSuccess?: (registration: ServiceWorkerRegistration) => void;
onUpdate?: (registration: ServiceWorkerRegistration) => void;
};
export function register(config?: Config) {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(
process.env.PUBLIC_URL,
window.location.href
);
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
return;
}
window.addEventListener('load', () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
if (isLocalhost) {
// This is running on localhost. Let's check if a service worker still exists or not.
checkValidServiceWorker(swUrl, config);
// Add some additional logging to localhost, pointing developers to the
// service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => {
console.log(
'This web app is being served cache-first by a service ' +
'worker. To learn more, visit https://bit.ly/CRA-PWA'
);
});
} else {
// Is not localhost. Just register service worker
registerValidSW(swUrl, config);
}
});
}
}
function registerValidSW(swUrl: string, config?: Config) {
navigator.serviceWorker
.register(swUrl)
.then(registration => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
if (installingWorker == null) {
return;
}
installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// At this point, the updated precached content has been fetched,
// but the previous service worker will still serve the older
// content until all client tabs are closed.
console.log(
'New content is available and will be used when all ' +
'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
);
// Execute callback
if (config && config.onUpdate) {
config.onUpdate(registration);
}
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
console.log('Content is cached for offline use.');
// Execute callback
if (config && config.onSuccess) {
config.onSuccess(registration);
}
}
}
};
};
})
.catch(error => {
console.error('Error during service worker registration:', error);
});
}
function checkValidServiceWorker(swUrl: string, config?: Config) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl, {
headers: { 'Service-Worker': 'script' }
})
.then(response => {
// Ensure service worker exists, and that we really are getting a JS file.
const contentType = response.headers.get('content-type');
if (
response.status === 404 ||
(contentType != null && contentType.indexOf('javascript') === -1)
) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then(registration => {
registration.unregister().then(() => {
window.location.reload();
});
});
} else {
// Service worker found. Proceed as normal.
registerValidSW(swUrl, config);
}
})
.catch(() => {
console.log(
'No internet connection found. App is running in offline mode.'
);
});
}
export function unregister() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready
.then(registration => {
registration.unregister();
})
.catch(error => {
console.error(error.message);
});
}
}

View File

@@ -0,0 +1,5 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom/extend-expect';

View File

@@ -0,0 +1,25 @@
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": false,
"forceConsistentCasingInFileNames": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react"
},
"include": [
"src"
]
}

13187
packages/app-react/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,2 @@
node_modules
npm-debug.log

1
packages/backend/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
dist

View File

@@ -0,0 +1,13 @@
FROM node:12
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
CMD [ "npm", "start" ]

460
packages/backend/package-lock.json generated Normal file
View File

@@ -0,0 +1,460 @@
{
"name": "@the-game/backend",
"version": "0.1.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@types/body-parser": {
"version": "1.19.0",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.0.tgz",
"integrity": "sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ==",
"dev": true,
"requires": {
"@types/connect": "*",
"@types/node": "*"
}
},
"@types/connect": {
"version": "3.4.33",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.33.tgz",
"integrity": "sha512-2+FrkXY4zllzTNfJth7jOqEHC+enpLeGslEhpnTAkg21GkRrWV4SsAtqchtT4YS9/nODBU2/ZfsBY2X4J/dX7A==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/express": {
"version": "4.17.6",
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.6.tgz",
"integrity": "sha512-n/mr9tZI83kd4azlPG5y997C/M4DNABK9yErhFM6hKdym4kkmd9j0vtsJyjFIwfRBxtrxZtAfGZCNRIBMFLK5w==",
"dev": true,
"requires": {
"@types/body-parser": "*",
"@types/express-serve-static-core": "*",
"@types/qs": "*",
"@types/serve-static": "*"
}
},
"@types/express-serve-static-core": {
"version": "4.17.5",
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.5.tgz",
"integrity": "sha512-578YH5Lt88AKoADy0b2jQGwJtrBxezXtVe/MBqWXKZpqx91SnC0pVkVCcxcytz3lWW+cHBYDi3Ysh0WXc+rAYw==",
"dev": true,
"requires": {
"@types/node": "*",
"@types/range-parser": "*"
}
},
"@types/mime": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.1.tgz",
"integrity": "sha512-FwI9gX75FgVBJ7ywgnq/P7tw+/o1GUbtP0KzbtusLigAOgIgNISRK0ZPl4qertvXSIE8YbsVJueQ90cDt9YYyw==",
"dev": true
},
"@types/node": {
"version": "13.11.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-13.11.1.tgz",
"integrity": "sha512-eWQGP3qtxwL8FGneRrC5DwrJLGN4/dH1clNTuLfN81HCrxVtxRjygDTUoZJ5ASlDEeo0ppYFQjQIlXhtXpOn6g==",
"dev": true
},
"@types/qs": {
"version": "6.9.1",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.1.tgz",
"integrity": "sha512-lhbQXx9HKZAPgBkISrBcmAcMpZsmpe/Cd/hY7LGZS5OfkySUBItnPZHgQPssWYUET8elF+yCFBbP1Q0RZPTdaw==",
"dev": true
},
"@types/range-parser": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz",
"integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==",
"dev": true
},
"@types/serve-static": {
"version": "1.13.3",
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.3.tgz",
"integrity": "sha512-oprSwp094zOglVrXdlo/4bAHtKTAxX6VT8FOZlBKrmyLbNvE1zxZyJ6yikMVtHIvwP45+ZQGJn+FdXGKTozq0g==",
"dev": true,
"requires": {
"@types/express-serve-static-core": "*",
"@types/mime": "*"
}
},
"accepts": {
"version": "1.3.7",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
"integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==",
"requires": {
"mime-types": "~2.1.24",
"negotiator": "0.6.2"
}
},
"array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
},
"body-parser": {
"version": "1.19.0",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz",
"integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==",
"requires": {
"bytes": "3.1.0",
"content-type": "~1.0.4",
"debug": "2.6.9",
"depd": "~1.1.2",
"http-errors": "1.7.2",
"iconv-lite": "0.4.24",
"on-finished": "~2.3.0",
"qs": "6.7.0",
"raw-body": "2.4.0",
"type-is": "~1.6.17"
}
},
"bytes": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
"integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg=="
},
"content-disposition": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz",
"integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==",
"requires": {
"safe-buffer": "5.1.2"
}
},
"content-type": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
"integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA=="
},
"cookie": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz",
"integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg=="
},
"cookie-signature": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
"integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
},
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"requires": {
"ms": "2.0.0"
}
},
"depd": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
"integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak="
},
"destroy": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
"integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
},
"ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
},
"encodeurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k="
},
"escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
},
"esm": {
"version": "3.2.25",
"resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz",
"integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA=="
},
"etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
"integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc="
},
"express": {
"version": "4.17.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz",
"integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==",
"requires": {
"accepts": "~1.3.7",
"array-flatten": "1.1.1",
"body-parser": "1.19.0",
"content-disposition": "0.5.3",
"content-type": "~1.0.4",
"cookie": "0.4.0",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "~1.1.2",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"finalhandler": "~1.1.2",
"fresh": "0.5.2",
"merge-descriptors": "1.0.1",
"methods": "~1.1.2",
"on-finished": "~2.3.0",
"parseurl": "~1.3.3",
"path-to-regexp": "0.1.7",
"proxy-addr": "~2.0.5",
"qs": "6.7.0",
"range-parser": "~1.2.1",
"safe-buffer": "5.1.2",
"send": "0.17.1",
"serve-static": "1.14.1",
"setprototypeof": "1.1.1",
"statuses": "~1.5.0",
"type-is": "~1.6.18",
"utils-merge": "1.0.1",
"vary": "~1.1.2"
}
},
"finalhandler": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz",
"integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==",
"requires": {
"debug": "2.6.9",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"on-finished": "~2.3.0",
"parseurl": "~1.3.3",
"statuses": "~1.5.0",
"unpipe": "~1.0.0"
}
},
"forwarded": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
"integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ="
},
"fresh": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
"integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac="
},
"http-errors": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz",
"integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==",
"requires": {
"depd": "~1.1.2",
"inherits": "2.0.3",
"setprototypeof": "1.1.1",
"statuses": ">= 1.5.0 < 2",
"toidentifier": "1.0.0"
}
},
"iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"requires": {
"safer-buffer": ">= 2.1.2 < 3"
}
},
"inherits": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
},
"ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="
},
"media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g="
},
"merge-descriptors": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
"integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E="
},
"methods": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
"integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4="
},
"mime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="
},
"mime-db": {
"version": "1.43.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.43.0.tgz",
"integrity": "sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ=="
},
"mime-types": {
"version": "2.1.26",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.26.tgz",
"integrity": "sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ==",
"requires": {
"mime-db": "1.43.0"
}
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
"negotiator": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
"integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw=="
},
"on-finished": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
"integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
"requires": {
"ee-first": "1.1.1"
}
},
"parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="
},
"path-to-regexp": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
"integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
},
"proxy-addr": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz",
"integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==",
"requires": {
"forwarded": "~0.1.2",
"ipaddr.js": "1.9.1"
}
},
"qs": {
"version": "6.7.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
"integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ=="
},
"range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="
},
"raw-body": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz",
"integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==",
"requires": {
"bytes": "3.1.0",
"http-errors": "1.7.2",
"iconv-lite": "0.4.24",
"unpipe": "1.0.0"
}
},
"safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"send": {
"version": "0.17.1",
"resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz",
"integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==",
"requires": {
"debug": "2.6.9",
"depd": "~1.1.2",
"destroy": "~1.0.4",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"fresh": "0.5.2",
"http-errors": "~1.7.2",
"mime": "1.6.0",
"ms": "2.1.1",
"on-finished": "~2.3.0",
"range-parser": "~1.2.1",
"statuses": "~1.5.0"
},
"dependencies": {
"ms": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
"integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
}
}
},
"serve-static": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz",
"integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==",
"requires": {
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"parseurl": "~1.3.3",
"send": "0.17.1"
}
},
"setprototypeof": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
"integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw=="
},
"statuses": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
"integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow="
},
"toidentifier": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
"integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw=="
},
"type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
"requires": {
"media-typer": "0.3.0",
"mime-types": "~2.1.24"
}
},
"typescript": {
"version": "3.8.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.8.3.tgz",
"integrity": "sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==",
"dev": true
},
"unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw="
},
"utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
"integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
},
"vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
}
}
}

View File

@@ -0,0 +1,23 @@
{
"name": "@the-game/backend",
"version": "0.1.0",
"description": "",
"main": "dist/index.js",
"scripts": {
"start": "node ./dist/index.js",
"build": "tsc"
},
"author": "",
"license": "ISC",
"dependencies": {
"body-parser": "^1.19.0",
"ethers": "^4.0.46",
"express": "^4.17.1",
"node-fetch": "^2.6.0"
},
"devDependencies": {
"@types/express": "^4.17.6",
"@types/node": "^13.11.1",
"typescript": "^3.8.3"
}
}

View File

@@ -0,0 +1,5 @@
export default {
port: process.env.PORT || 3000,
graphqlURL: process.env.GRAPHQL_URL || 'http://localhost:8080/v1/graphql',
adminKey: process.env.HASURA_GRAPHQL_ADMIN_SECRET || 'metagame_secret',
};

View File

@@ -0,0 +1,18 @@
import { ethers } from "ethers";
export function verifyToken(token: string): any {
try {
const rawToken = Buffer.from(token, 'base64').toString();
const [proof, rawClaim] = JSON.parse(rawToken);
const claim = JSON.parse(rawClaim);
const signerAddress = ethers.utils.verifyMessage(rawClaim, proof);
if(signerAddress !== claim.iss) {
return null;
}
return claim;
} catch (e) {
console.error('Token verification failed', e);
return null;
}
}

View File

@@ -0,0 +1,45 @@
import { Request, Response } from 'express';
import { verifyToken } from './did';
import { getPlayer } from './users';
const unauthorizedVariables = {
'X-Hasura-Role': 'public',
};
function getHeaderToken(req: Request): string | null {
const authHeader = req.headers['authorization'];
if(!authHeader) return null;
const token = authHeader.replace('Bearer ', '');
if(token.length === 0) return null;
return token;
}
const handler = async (req: Request, res: Response) => {
const token = getHeaderToken(req);
if(!token) {
res.json(unauthorizedVariables);
return;
}
else {
const claim = verifyToken(token);
if(!claim) {
res.status(401).send();
return;
}
const player = await getPlayer(claim.iss);
const hasuraVariables = {
'X-Hasura-Role': 'player',
'X-Hasura-User-Id': player.id,
};
res.json(hasuraVariables);
}
};
export default handler;

View File

@@ -0,0 +1,94 @@
import fetch from 'node-fetch';
import config from '../../config';
const getPlayerQuery = `
query GetPlayerFromETH ($eth_address: String) {
Profile(
where: {
identifier: { _eq: $eth_address },
type: { _eq: "ETHEREUM" }
}
) {
Player {
id
}
}
}
`;
const createPlayerMutation = `
mutation CreatePlayer {
insert_Player(objects: {}) {
returning {
id
}
}
}
`;
const createProfileMutation = `
mutation CreateProfileFromETH ($player_id: uuid, $eth_address: String) {
insert_Profile(
objects: {
player_id: $player_id,
type: "ETHEREUM",
identifier: $eth_address
}) {
returning {
identifier
}
}
}
`;
async function hasuraQuery(query: string, qv: any = {}) {
const result = await fetch(config.graphqlURL, {
method: 'POST',
body: JSON.stringify({ query: query, variables: qv }),
headers: {
'Content-Type': 'application/json',
'x-hasura-access-key': config.adminKey,
},
});
const { errors, data } = await result.json();
if(errors) {
throw new Error(JSON.stringify(errors));
}
return data;
}
interface IPlayer {
id: string
}
export async function createPlayer(ethAddress: string): Promise<IPlayer> {
const resPlayer = await hasuraQuery(createPlayerMutation );
const player = resPlayer.insert_Player.returning[0];
await hasuraQuery(createProfileMutation, {
player_id: player.id,
eth_address: ethAddress,
});
// TODO do it in only one query
return player;
}
export async function getPlayer(ethAddress: string): Promise<IPlayer> {
const res = await hasuraQuery(getPlayerQuery, {
eth_address: ethAddress,
});
let player = res.Profile[0]?.Player;
if(!player) {
// TODO if two requests sent at the same time, collision
player = await createPlayer(ethAddress);
}
return player;
}

View File

@@ -0,0 +1,15 @@
import express from 'express';
import { asyncHandlerWrapper } from '../lib/apiHelpers';
import authHandler from './auth-webhook/handler';
const router = express.Router();
router.get('/', function (req, res) {
res.send('pong')
});
router.get('/auth-webhook', asyncHandlerWrapper(authHandler));
export default router;

View File

@@ -0,0 +1,19 @@
import express from 'express';
import bodyParser from 'body-parser';
import config from './config';
import routes from './handlers/routes';
import { errorMiddleware } from './lib/apiHelpers';
const app = express();
app.use(bodyParser.json());
app.use(routes);
app.use(errorMiddleware);
app.listen(config.port, function () {
console.log(`Listening on port ${config.port}`)
});

View File

@@ -0,0 +1,17 @@
import { Request, Response, NextFunction } from 'express';
export function asyncHandlerWrapper(middleware: any) {
if (middleware.length === 4) {
return function wrappedHandler(error: Error, req: Request, res: Response, next: NextFunction) {
middleware(error, req, res, next).catch(next);
};
}
return function wrappedHandler(req: Request, res: Response, next: NextFunction) {
middleware(req, res, next).catch(next);
};
}
export function errorMiddleware(error: Error, req: Request, res: Response, next: NextFunction) {
console.error(error);
res.status(500).send('Unexpected error');
}

View File

@@ -0,0 +1,11 @@
{
"compilerOptions": {
"target": "ES5",
"module": "commonjs",
"declaration": true,
"sourceMap": true,
"outDir": "./dist",
"strict": false,
"esModuleInterop": true
}
}

1
packages/graphql-codegen/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
autogen

View File

@@ -0,0 +1,7 @@
import gql from 'graphql-tag';
export const Player = gql`
fragment Player on Player {
id
}
`;

View File

@@ -0,0 +1,26 @@
schema:
- http://localhost:8080/v1/graphql
documents:
overwrite: true
config:
scalars:
DateTime: Date
JSON: "{ [key: string]: any }"
generates:
./autogen/hasura/gql.ts:
plugins:
- graphql-codegen-hasura-gql
documents:
- ./customFragments.ts
config:
reactApolloVersion: 3
typescriptCodegenOutputPath: ../
trimString:
withQueries: true
withSubscriptions: false
withInserts: false
withUpdates: false
withDeletes: false
./autogen/graphql.schema.json:
plugins:
- introspection

View File

@@ -0,0 +1,66 @@
schema:
- ./autogen/graphql.schema.json
overwrite: true
config:
scalars:
DateTime: Date
JSON: "{ [key: string]: any }"
generates:
./autogen/index.tsx:
documents:
- ./customFragments.ts
- ./autogen/hasura/gql.ts
plugins:
- typescript
- typescript-operations
- typescript-react-apollo
config:
reactApolloVersion: 3
withHooks: false
withHOC: false
withComponent: false
skipTypename: false
includeDirectives: true
./autogen/hasura/ts.ts:
documents:
- ./customFragments.ts
plugins:
- graphql-codegen-hasura-typescript
config:
reactApolloVersion: 3
typescriptCodegenOutputPath: "../"
trimString:
withClientAndCacheHelpers: true
withQueries: true
withSubscriptions: false
withInserts: false
withUpdates: false
withDeletes: false
./autogen/hasura/react.ts:
documents:
- ./customFragments.ts
plugins:
- graphql-codegen-hasura-react
config:
reactApolloVersion: 3
typescriptCodegenOutputPath: "../"
trimString:
withQueries: true
withSubscriptions: false
withInserts: false
withUpdates: false
withDeletes: false
./autogen/hasura/config.ts:
documents:
- ./customFragments.ts
plugins:
- graphql-codegen-hasura-client-config
config:
reactApolloVersion: 3
typescriptCodegenOutputPath: "../"
trimString:
withTypePolicies: true
withResolverTypes: true
withCombinedTypePolicyObject: false
withCacheRedirects: true
withCombinedCacheRedirectObject: true

View File

@@ -0,0 +1,22 @@
{
"name": "@the-game/graphql-codegen",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"scripts": {
"generate": "graphql-codegen --config=graphql-codegen-gql.yaml; graphql-codegen --config=graphql-codegen-typescript.yaml"
},
"dependencies": {
"@graphql-codegen/cli": "^1.13.2",
"@graphql-codegen/introspection": "^1.13.2",
"@graphql-codegen/typescript": "^1.13.2",
"@graphql-codegen/typescript-operations": "^1.13.2",
"@graphql-codegen/typescript-react-apollo": "^1.13.2",
"graphql-codegen-hasura-client-config": "^4.8.3",
"graphql-codegen-hasura-core": "^4.8.3",
"graphql-codegen-hasura-gql": "^4.8.3",
"graphql-codegen-hasura-react": "^4.8.3",
"graphql-codegen-hasura-shared": "^4.8.3",
"graphql-codegen-hasura-typescript": "^4.8.3"
}
}

File diff suppressed because it is too large Load Diff