mirror of
https://github.com/MetaFam/TheGame.git
synced 2026-04-24 03:00:09 -04:00
Architecture update (#7)
* Fix errors with profile_rank ENUM * Upgrade to Hasura 1.2.1 * Rename Profile to Account * Common JS package * rm app-react/lib/did * Better login management
This commit is contained in:
@@ -15,7 +15,7 @@
|
||||
"@types/react-dom": "^16.9.0",
|
||||
"@walletconnect/web3-provider": "^1.0.0-beta.47",
|
||||
"apollo-boost": "^0.4.7",
|
||||
"ethers": "^4.0.46",
|
||||
"ethers": "^4.0.47",
|
||||
"graphql": "^15.0.0",
|
||||
"graphql-codegen-hasura-core": "^4.8.4",
|
||||
"react": "^16.13.1",
|
||||
@@ -27,7 +27,8 @@
|
||||
"web3modal": "^1.3.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"start": "yarn start",
|
||||
"start:app": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject",
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import queries from '../graphql/queries';
|
||||
import { getSignerAddress } from '../lib/did';
|
||||
|
||||
const STORAGE_KEY = 'auth-token';
|
||||
|
||||
function getTokenFromStore() {
|
||||
export function getTokenFromStore() {
|
||||
return localStorage.getItem(STORAGE_KEY);
|
||||
}
|
||||
|
||||
@@ -15,58 +14,41 @@ function clearToken() {
|
||||
return localStorage.removeItem(STORAGE_KEY);
|
||||
}
|
||||
|
||||
export function loginLoading(client) {
|
||||
export function loginLoading(client, loading = true) {
|
||||
client.writeData({
|
||||
data: {
|
||||
authState: 'loading',
|
||||
authState: loading ? 'loading' : 'anonymous',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function login(client, token, ethAddress) {
|
||||
client.writeData({
|
||||
data: {
|
||||
authState: 'loading',
|
||||
authToken: token,
|
||||
},
|
||||
});
|
||||
setTokenInStore(token);
|
||||
loginLoading(client);
|
||||
|
||||
return client.query({
|
||||
query: queries.get_MyProfile,
|
||||
query: queries.get_MyAccount,
|
||||
variables: { eth_address: ethAddress }
|
||||
})
|
||||
.then(async res => {
|
||||
if(res.data.Profile.length === 0) {
|
||||
if(res.data.Account.length === 0) {
|
||||
throw new Error('Impossible to fetch player, not found.');
|
||||
}
|
||||
client.writeData({
|
||||
data: {
|
||||
authState: 'logged',
|
||||
playerId: res.data.Profile[0].Player.id,
|
||||
authToken: token,
|
||||
playerId: res.data.Account[0].Player.id,
|
||||
},
|
||||
});
|
||||
setTokenInStore(token);
|
||||
})
|
||||
.catch(async error => {
|
||||
client.writeData({
|
||||
data: {
|
||||
authState: 'error',
|
||||
authToken: null,
|
||||
},
|
||||
});
|
||||
logout();
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
export function logout() {
|
||||
export function logout(client) {
|
||||
clearToken();
|
||||
}
|
||||
|
||||
export function checkStoredAuth(client) {
|
||||
const token = getTokenFromStore();
|
||||
if(token) {
|
||||
const address = getSignerAddress(token);
|
||||
login(client, token, address).catch(console.error)
|
||||
}
|
||||
client.resetStore();
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@ import ApolloClient from 'apollo-boost';
|
||||
|
||||
import config from '../config';
|
||||
|
||||
import { localQueries, logout } from './index';
|
||||
import { checkStoredAuth } from './auth';
|
||||
import * as localQueries from './localQueries';
|
||||
import { logout } from './auth';
|
||||
|
||||
export function createApolloClient() {
|
||||
let client;
|
||||
@@ -31,7 +31,6 @@ export function createApolloClient() {
|
||||
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);
|
||||
@@ -52,7 +51,6 @@ export function createApolloClient() {
|
||||
client.writeData({ data: defaultClientState })
|
||||
});
|
||||
|
||||
checkStoredAuth(client);
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { createApolloClient } from './client';
|
||||
import { login, logout } from './auth';
|
||||
import * as localQueries from './localQueries';
|
||||
|
||||
export {
|
||||
localQueries, createApolloClient, login, logout,
|
||||
localQueries, createApolloClient,
|
||||
};
|
||||
|
||||
@@ -21,23 +21,11 @@ export const Login: React.FC = () => {
|
||||
return (
|
||||
<Box>Connected</Box>
|
||||
);
|
||||
} else if(data?.authState === 'error') {
|
||||
} else {
|
||||
return (
|
||||
<Box>
|
||||
Connection error
|
||||
Unknown state
|
||||
</Box>
|
||||
);
|
||||
} else if(data?.authState === 'anonymous') {
|
||||
return (
|
||||
<Box>
|
||||
<button onClick={connectWeb3}>Connect</button>
|
||||
</Box>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
Unknown state
|
||||
</Box>
|
||||
)
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, {createContext, useCallback, useState} from 'react';
|
||||
import React, {createContext, useCallback, useEffect, useState} from 'react';
|
||||
import WalletConnectProvider from '@walletconnect/web3-provider';
|
||||
import Web3Modal from 'web3modal';
|
||||
import Web3 from 'web3';
|
||||
@@ -7,8 +7,8 @@ import { AsyncSendable, Web3Provider } from 'ethers/providers';
|
||||
import {useApolloClient} from '@apollo/react-hooks';
|
||||
|
||||
import config from '../config';
|
||||
import {createToken} from '../lib/did';
|
||||
import {loginLoading, login} from '../apollo/auth';
|
||||
import { did } from '@the-game/utils';
|
||||
import {loginLoading, login, getTokenFromStore} from '../apollo/auth';
|
||||
|
||||
type Web3ContextType = {
|
||||
ethersProvider: Web3Provider | null,
|
||||
@@ -42,29 +42,44 @@ const Web3ContextProvider: React.FC = props => {
|
||||
|
||||
const [ethersProvider, setEthersProvider] = useState<Web3Provider | null>(null);
|
||||
|
||||
const connectDID = useCallback(async (ethersProvider: Web3Provider) => {
|
||||
|
||||
let token = getTokenFromStore();
|
||||
|
||||
if(!token) {
|
||||
token = await did.createToken(ethersProvider);
|
||||
}
|
||||
|
||||
const signer = ethersProvider.getSigner();
|
||||
const address = await signer.getAddress();
|
||||
await login(apolloClient, token, address);
|
||||
|
||||
}, [apolloClient]);
|
||||
|
||||
const connectWeb3 = useCallback(async () => {
|
||||
|
||||
loginLoading(apolloClient);
|
||||
|
||||
try {
|
||||
|
||||
loginLoading(apolloClient);
|
||||
|
||||
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();
|
||||
await connectDID(ethersProvider);
|
||||
|
||||
const token = await createToken(ethersProvider);
|
||||
console.log(token);
|
||||
|
||||
await login(apolloClient, token, address);
|
||||
setEthersProvider(ethersProvider);
|
||||
|
||||
} catch(error) {
|
||||
console.error('impossible to connect', error);
|
||||
} catch(e) {
|
||||
loginLoading(apolloClient, false);
|
||||
throw e;
|
||||
}
|
||||
}, [apolloClient]);
|
||||
}, [apolloClient, connectDID]);
|
||||
|
||||
useEffect(() => {
|
||||
if(web3Modal.cachedProvider) {
|
||||
connectWeb3().catch(console.error);
|
||||
}
|
||||
}, [connectWeb3]);
|
||||
|
||||
return (
|
||||
<Web3Context.Provider value={{ ethersProvider, connectWeb3 }}>
|
||||
|
||||
@@ -5,8 +5,8 @@ fragment PlayerFragment on Player {
|
||||
id
|
||||
}
|
||||
`;
|
||||
fragments.ProfileFragment = `
|
||||
fragment ProfileFragment on Profile {
|
||||
fragments.AccountFragment = `
|
||||
fragment AccountFragment on Account {
|
||||
identifier
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -19,7 +19,7 @@ export function useMyPlayer() {
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [playerId]);
|
||||
}, [getMyPlayer, playerId]);
|
||||
|
||||
return myPlayerQuery;
|
||||
}
|
||||
|
||||
@@ -24,22 +24,22 @@ query GetPlayer($player_id: uuid) {
|
||||
${fragments.PlayerFragment}
|
||||
`;
|
||||
|
||||
queries.get_MyProfile = gql`
|
||||
query GetMyProfile($eth_address: String) {
|
||||
Profile(
|
||||
queries.get_MyAccount = gql`
|
||||
query GetMyAccount($eth_address: String) {
|
||||
Account(
|
||||
where: {
|
||||
identifier: { _eq: $eth_address },
|
||||
type: { _eq: "ETHEREUM" }
|
||||
}
|
||||
) {
|
||||
...ProfileFragment
|
||||
...AccountFragment
|
||||
Player {
|
||||
...PlayerFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
${fragments.PlayerFragment}
|
||||
${fragments.ProfileFragment}
|
||||
${fragments.AccountFragment}
|
||||
`;
|
||||
|
||||
export default queries;
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react",
|
||||
"typeRoots": ["../../node_modules/@types", "./@types"]
|
||||
"typeRoots": ["./@types"]
|
||||
},
|
||||
"include": [
|
||||
"src",
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"start": "node ./dist/index.js",
|
||||
"start:backend": "yarn dev",
|
||||
"dev": "ts-node-dev --respawn --transpileOnly src/index.ts",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"build": "tsc"
|
||||
},
|
||||
@@ -14,7 +16,8 @@
|
||||
"body-parser": "^1.19.0",
|
||||
"ethers": "^4.0.46",
|
||||
"express": "^4.17.1",
|
||||
"node-fetch": "^2.6.0"
|
||||
"node-fetch": "^2.6.0",
|
||||
"ts-node-dev": "^1.0.0-pre.44"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.6",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export default {
|
||||
port: process.env.PORT || 3000,
|
||||
port: process.env.PORT || 4000,
|
||||
graphqlURL: process.env.GRAPHQL_URL || 'http://localhost:8080/v1/graphql',
|
||||
adminKey: process.env.HASURA_GRAPHQL_ADMIN_SECRET || 'metagame_secret',
|
||||
};
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { verifyToken } from './did';
|
||||
import { did } from '@the-game/utils';
|
||||
import { getPlayer } from './users';
|
||||
|
||||
const unauthorizedVariables = {
|
||||
@@ -24,7 +24,7 @@ const handler = async (req: Request, res: Response) => {
|
||||
}
|
||||
else {
|
||||
|
||||
const claim = verifyToken(token);
|
||||
const claim = did.verifyToken(token);
|
||||
if(!claim) {
|
||||
res.status(401).send();
|
||||
return;
|
||||
@@ -36,6 +36,7 @@ const handler = async (req: Request, res: Response) => {
|
||||
'X-Hasura-Role': 'player',
|
||||
'X-Hasura-User-Id': player.id,
|
||||
};
|
||||
|
||||
res.json(hasuraVariables);
|
||||
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import config from '../../config';
|
||||
|
||||
const getPlayerQuery = `
|
||||
query GetPlayerFromETH ($eth_address: String) {
|
||||
Profile(
|
||||
Account(
|
||||
where: {
|
||||
identifier: { _eq: $eth_address },
|
||||
type: { _eq: "ETHEREUM" }
|
||||
@@ -17,23 +17,15 @@ query GetPlayerFromETH ($eth_address: String) {
|
||||
}
|
||||
`;
|
||||
|
||||
const createPlayerMutation = `
|
||||
mutation CreatePlayer {
|
||||
insert_Player(objects: {}) {
|
||||
returning {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const createProfileMutation = `
|
||||
mutation CreateProfileFromETH ($player_id: uuid, $eth_address: String) {
|
||||
insert_Profile(
|
||||
mutation CreateAccountFromETH ($eth_address: String) {
|
||||
insert_Account(
|
||||
objects: {
|
||||
player_id: $player_id,
|
||||
type: "ETHEREUM",
|
||||
identifier: $eth_address
|
||||
Player: {
|
||||
data: {}
|
||||
}
|
||||
}) {
|
||||
returning {
|
||||
identifier
|
||||
@@ -65,17 +57,10 @@ interface IPlayer {
|
||||
}
|
||||
|
||||
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,
|
||||
const resProfile = await hasuraQuery(createProfileMutation, {
|
||||
eth_address: ethAddress,
|
||||
});
|
||||
|
||||
// TODO do it in only one query
|
||||
|
||||
return player;
|
||||
return resProfile.insert_Account.returning[0].Player;
|
||||
}
|
||||
|
||||
export async function getPlayer(ethAddress: string): Promise<IPlayer> {
|
||||
@@ -83,10 +68,9 @@ export async function getPlayer(ethAddress: string): Promise<IPlayer> {
|
||||
eth_address: ethAddress,
|
||||
});
|
||||
|
||||
let player = res.Profile[0]?.Player;
|
||||
let player = res.Account[0]?.Player;
|
||||
|
||||
if(!player) {
|
||||
// TODO if two requests sent at the same time, collision
|
||||
player = await createPlayer(ethAddress);
|
||||
}
|
||||
|
||||
|
||||
1
packages/utils/.gitignore
vendored
Normal file
1
packages/utils/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
dist
|
||||
1
packages/utils/@types/js-base64.d.ts
vendored
Normal file
1
packages/utils/@types/js-base64.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
declare module 'js-base64';
|
||||
19
packages/utils/package.json
Normal file
19
packages/utils/package.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "@the-game/utils",
|
||||
"version": "1.0.0",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start:app": "tsc -w",
|
||||
"start:backend": "tsc -w",
|
||||
"build": "tsc"
|
||||
},
|
||||
"dependencies": {
|
||||
"ethers": "^4.0.47",
|
||||
"js-base64": "^2.5.2",
|
||||
"typescript": "^3.8.3",
|
||||
"uuid": "^8.0.0"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { ethers } from "ethers";
|
||||
import { Web3Provider } from "ethers/providers";
|
||||
import { Base64 } from 'js-base64';
|
||||
|
||||
const tokenDuration = 1000 * 60 * 60 * 24 * 7; // 7 days
|
||||
|
||||
@@ -21,7 +22,7 @@ export async function createToken(provider: Web3Provider) {
|
||||
const serializedClaim = JSON.stringify(claim);
|
||||
const proof = await signer.signMessage(serializedClaim);
|
||||
|
||||
const DIDToken = btoa(JSON.stringify([proof, serializedClaim]));
|
||||
const DIDToken = Base64.encode(JSON.stringify([proof, serializedClaim]));
|
||||
|
||||
return DIDToken;
|
||||
}
|
||||
@@ -29,7 +30,7 @@ export async function createToken(provider: Web3Provider) {
|
||||
|
||||
export function getSignerAddress(token: string): any {
|
||||
try {
|
||||
const rawToken = atob(token);
|
||||
const rawToken = Base64.decode(token);
|
||||
const [proof, rawClaim] = JSON.parse(rawToken);
|
||||
const signerAddress = ethers.utils.verifyMessage(rawClaim, proof);
|
||||
return signerAddress;
|
||||
@@ -38,3 +39,20 @@ export function getSignerAddress(token: string): any {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function verifyToken(token: string): any {
|
||||
try {
|
||||
const rawToken = Base64.decode(token, 'base64');
|
||||
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;
|
||||
}
|
||||
}
|
||||
3
packages/utils/src/index.ts
Normal file
3
packages/utils/src/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import * as did from './did';
|
||||
|
||||
export { did };
|
||||
14
packages/utils/tsconfig.json
Normal file
14
packages/utils/tsconfig.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "./dist",
|
||||
"module": "CommonJS",
|
||||
"target": "ES5",
|
||||
"typeRoots": ["./@types"]
|
||||
},
|
||||
"include": [
|
||||
"./src",
|
||||
"./@types"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user