-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
support mongodb with user initilization
- Loading branch information
Showing
12 changed files
with
453 additions
and
13 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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>; | ||
}>; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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= |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.