Skip to content

Commit

Permalink
support mongodb with user initilization
Browse files Browse the repository at this point in the history
  • Loading branch information
dan13ram committed Feb 13, 2023
1 parent a525d57 commit c3bd3df
Show file tree
Hide file tree
Showing 12 changed files with 453 additions and 13 deletions.
9 changes: 7 additions & 2 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,13 @@
"react/display-name": "off",
"react/no-unescaped-entities": "warn",
"react-hooks/rules-of-hooks": "error",
"no-console": "error",
"import/no-unresolved": "off"
"import/no-unresolved": "off",
"no-console": [
"error",
{
"allow": ["error"]
}
]
},
"globals": {
"React": "writable"
Expand Down
97 changes: 97 additions & 0 deletions lib/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { providers, utils } from 'ethers';
import { Base64 } from 'js-base64';
import { v4 as uuidv4 } from 'uuid';

const AUDIENCE = 'quest-chains-api';
const TOKEN_DURATION = 1000 * 60 * 60 * 24 * 7; // 7 days
const WELCOME_MESSAGE = `Welcome to Quest Chains Anon!\nPlease sign this message so we know it is you.\n\n`;

const verifySignature = (
address: string,
message: string,
signature: string,
): boolean => {
const recoveredAddress = utils.verifyMessage(message, signature);
return address.toLowerCase() === recoveredAddress.toLowerCase();
};

export const signMessage = async (
provider: providers.Web3Provider,
rawMessage: string,
): Promise<string> => {
const ethereum = provider.provider;
const signer = provider.getSigner();
const address = await signer.getAddress();
if (!ethereum.request) throw new Error('invalid ethereum provider');

let params = [rawMessage, address.toLowerCase()];
if (ethereum.isMetaMask) {
params = [params[1], params[0]];
}
const signature = await ethereum.request({
method: 'personal_sign',
params,
});
return signature;
};

type Claim = {
iat: number;
exp: number;
iss: string;
aud: string;
tid: string;
};

export const createToken = async (
provider: providers.Web3Provider,
): Promise<string> => {
const address = await provider.getSigner().getAddress();

const iat = new Date().getTime();

const claim: Claim = {
iat,
exp: iat + TOKEN_DURATION,
iss: address.toLowerCase(),
aud: AUDIENCE,
tid: uuidv4(),
};

const serializedClaim = JSON.stringify(claim);
const msgToSign = `${WELCOME_MESSAGE}${serializedClaim}`;
const proof = await signMessage(provider, msgToSign);

return Base64.encode(JSON.stringify([proof, serializedClaim]));
};

export const verifyToken = (token: string): string | null => {
try {
if (!token) return null;
const rawToken = Base64.decode(token);
const [proof, rawClaim] = JSON.parse(rawToken);
const claim: Claim = JSON.parse(rawClaim);
const address = claim.iss;

const msgToVerify = `${WELCOME_MESSAGE}${rawClaim}`;
const valid = verifySignature(address, msgToVerify, proof);
const expired = claim.exp < new Date().getTime();
const validAudience = claim.aud === AUDIENCE;

if (!valid) {
throw new Error('invalid signature');
}
if (expired) {
throw new Error('token expired');
}
if (!validAudience) {
throw new Error('invalid audience');
}
// Important, always keep address lowercase for comparisons
return address.toLowerCase();
} catch (e) {
// eslint-disable-next-line no-console
console.error('Token verification failed', e as Error);
return null;
}
};
56 changes: 56 additions & 0 deletions lib/mongodb/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { Db, MongoClient } from 'mongodb';

if (!process.env.MONGODB_URI) {
throw new Error('Invalid/Missing environment variable: "MONGODB_URI"');
}

if (!process.env.MONGODB_DATABASE) {
throw new Error('Invalid/Missing environment variable: "MONGODB_DATABASE"');
}

declare const global: {
_clientPromise: Promise<Db>;
};

const uri = process.env.MONGODB_URI;
const database = process.env.MONGODB_DATABASE;
const options = {};

let clientPromise: Promise<Db>;

export const initIndexes = async (client: Db): Promise<Db> => {
const usersCollection = client.collection('users');
await usersCollection.createIndex(
{ address: 1 },
{ unique: true, partialFilterExpression: { address: { $type: 'string' } } },
);
await usersCollection.createIndex(
{ email: 1 },
{ unique: true, partialFilterExpression: { email: { $type: 'string' } } },
);
return client;
};

const createClientPromise = async (): Promise<Db> => {
const client = new MongoClient(uri, options);
return client
.connect()
.then((client: MongoClient) => client.db(database))
.then(initIndexes);
};

if (process.env.NODE_ENV === 'development') {
// In development mode, use a global variable so that the value
// is preserved across module reloads caused by HMR (Hot Module Replacement).
if (!global._clientPromise) {
global._clientPromise = createClientPromise();
}
clientPromise = global._clientPromise;
} else {
// In production mode, it's best to not use a global variable.
clientPromise = createClientPromise();
}

// Export a module-scoped MongoClient promise. By doing this in a
// separate module, the client can be shared across functions.
export default clientPromise;
14 changes: 14 additions & 0 deletions lib/mongodb/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { WithId } from 'mongodb';

type Maybe<T> = T | null | undefined;

export type MongoUser = WithId<{
address: string;
createdAt: Date;
updatedAt: Date;
username: Maybe<string>;
email: Maybe<string>;
isAdmin: Maybe<boolean>;
profilePicture: Maybe<string>;
verified: Maybe<boolean>;
}>;
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
"fast-memoize": "^2.5.2",
"framer-motion": "^7.2.1",
"html2canvas": "^1.4.1",
"js-base64": "^3.7.5",
"mongodb": "^5.0.1",
"next": "12.2.5",
"react": "18.2.0",
"react-dom": "18.2.0",
Expand All @@ -44,6 +46,7 @@
"sass": "^1.54.8",
"three": "^0.144.0",
"urql": "^3.0.3",
"uuid": "^9.0.0",
"web3modal": "^1.9.9"
},
"devDependencies": {
Expand All @@ -53,6 +56,7 @@
"@types/react-scroll": "^1.8.4",
"@types/remove-markdown": "^0.3.1",
"@types/three": "^0.144.0",
"@types/uuid": "^9.0.0",
"@typescript-eslint/eslint-plugin": "^5.36.2",
"@typescript-eslint/parser": "^5.36.2",
"concurrently": "^7.3.0",
Expand Down
36 changes: 36 additions & 0 deletions pages/api/connect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { utils } from 'ethers';
import { ObjectId } from 'mongodb';
import { NextApiRequest, NextApiResponse } from 'next';

import { verifyToken } from '@/lib/auth';
import clientPromise from '@/lib/mongodb/client';

export const connect = async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method !== 'POST') return res.status(405).end();

const token = req.headers.authorization?.split('Bearer')?.pop()?.trim();

if (!token) return res.status(401).end();

const address = verifyToken(token);

if (!utils.isAddress) return res.status(401).end();

const client = await clientPromise;

const usersCollection = client.collection('users');

const user = await usersCollection.findOne({ address });
if (user) {
return res.status(200).json(user);
}
const newUser = {
createdAt: new Date(),
updatedAt: new Date(),
address,
};
const { insertedId } = await usersCollection.insertOne(newUser);
return res.status(200).json({ _id: new ObjectId(insertedId), ...newUser });
};

export default connect;
3 changes: 3 additions & 0 deletions sample.env
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
NEXT_PUBLIC_SUPPORTED_NETWORKS=0x5
NEXT_PUBLIC_INFURA_ID=9aa3d95b3bc440fa88ea12eaa4456161
NEXT_PUBLIC_API_URL=https://api.questchains.xyz

MONGODB_URI=
MONGODB_DATABASE=
41 changes: 41 additions & 0 deletions utils/fetchWithHeaders.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { getFromStorage, STORAGE_KEYS } from '@/utils/storageHelpers';

export const RequestTypes = {
Put: 'PUT',
Delete: 'DELETE',
Post: 'POST',
Patch: 'PATCH',
Get: 'GET',
};

const defaultHeaders = {
Accept: 'application/json',
'Content-Type': 'application/json',
};

export const fetchWithHeaders = async (
url: string,
method = RequestTypes.Get,
data = {},
headers = {},
): Promise<Response> => {
headers = { ...headers, ...defaultHeaders };

const authToken = getFromStorage(STORAGE_KEYS.AUTH_TOKEN);
if (authToken) {
headers = { ...headers, Authorization: authToken };
}

const request = {
method,
headers,
body:
method === RequestTypes.Get
? null
: data instanceof FormData
? data
: JSON.stringify(data),
};

return fetch(url, request);
};
8 changes: 8 additions & 0 deletions utils/storageHelpers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
export const STORAGE_KEYS = {
IPFS_GATEWAY: 'quest-chains-ipfs-gateway',
AUTH_TOKEN: 'quest-chains-auth-token',
};

export const removeFromStorage = (key: string): void => {
if (typeof window === 'undefined') {
return;
}
window.localStorage.removeItem(key);
};

export const getFromStorage = (key: string): string | null => {
Expand Down
Loading

0 comments on commit c3bd3df

Please sign in to comment.