Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dev to Main v0.3.4 #305

Merged
merged 18 commits into from
Dec 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion packages/api/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,8 @@ NETWORK_CHAIN_WSS_URL = ""
DATABASE_URL = ""
DIRECT_URL = ""
CMC_API_KEY = ""
JWT_SECRET = ""
EE_AUTH_TOKEN = ""
SUPABASE_SERVICE_ROLE_KEY = ""
SUPABASE_FETCH_ALL_USERS_URL = "https://<SUPABASE_PROJECT_REF>.supabase.co/functions/v1/fetch-all-users"
SUPABASE_FETCH_ACCESS_LEVEL_URL = "https://<SUPABASE_PROJECT_REF>.supabase.co/functions/v1/fetch-access-level"
SUPABASE_POST_REQUESTS_URL = "https://<SUPABASE_PROJECT_REF>.supabase.co/functions/v1/post-requests"
3 changes: 3 additions & 0 deletions packages/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
"joi": "^17.12.2",
"jsonwebtoken": "^9.0.2",
"morgan": "~1.9.1",
"node-cache": "^5.1.2",
"node-cron": "^3.0.3",
"route-cache": "^0.7.0",
"viem": "^2.8.14",
"zod": "^3.23.4",
Expand All @@ -35,6 +37,7 @@
"@types/debug": "^4.1.12",
"@types/express": "^4.17.21",
"@types/morgan": "^1.9.9",
"@types/node-cron": "^3.0.11",
"@types/node": "^20.12.2",
"@types/route-cache": "^0.5.5",
"prisma": "^5.11.0",
Expand Down
3 changes: 3 additions & 0 deletions packages/api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import helmet from 'helmet'
import cors from 'cors'
import apiRouter from './routes'
import { EigenExplorerApiError, handleAndReturnErrorResponse } from './schema/errors'
import { startUserRequestsSync } from './utils/userRequestsSync'

const PORT = process.env.PORT ? Number.parseInt(process.env.PORT) : 3002

Expand Down Expand Up @@ -50,4 +51,6 @@ app.use((err: Error, req: Request, res: Response) => {
// Start the server
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`)

startUserRequestsSync()
})
186 changes: 186 additions & 0 deletions packages/api/src/routes/auth/authController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
import type { Request, Response } from 'express'
import { handleAndReturnErrorResponse } from '../../schema/errors'
import { EthereumAddressSchema } from '../../schema/zod/schemas/base/ethereumAddress'
import { refreshAuthStore } from '../../utils/authMiddleware'
import { RegisterUserBodySchema, RequestHeadersSchema } from '../../schema/zod/schemas/auth'
import { verifyMessage } from 'viem'
import prisma from '../../utils/prismaClient'
import crypto from 'node:crypto'

/**
* Function for route /auth/users/:address/check-status
* Protected route, returns whether a given address is registered on EE, if they are an EL staker & if we track their rewards
*
* @param req
* @param res
* @returns
*/
export async function checkUserStatus(req: Request, res: Response) {
const headerCheck = RequestHeadersSchema.safeParse(req.headers)
if (!headerCheck.success) {
return handleAndReturnErrorResponse(req, res, headerCheck.error)
}

const paramCheck = EthereumAddressSchema.safeParse(req.params.address)
if (!paramCheck.success) {
return handleAndReturnErrorResponse(req, res, paramCheck.error)
}

try {
const apiToken = headerCheck.data['X-API-Token']
const authToken = process.env.EE_AUTH_TOKEN

if (!apiToken || apiToken !== authToken) {
throw new Error('Unauthorized access.')
}

const { address } = req.params

const [user, staker] = await Promise.all([
prisma.user.findUnique({
where: { address: address.toLowerCase() }
}),
prisma.staker.findUnique({
where: { address: address.toLowerCase() }
})
])

const isRegistered = !!user
const isStaker = !!staker
const isTracked = !!user?.isTracked

res.send({ isRegistered, isStaker, isTracked })
} catch (error) {
handleAndReturnErrorResponse(req, res, error)
}
}

/**
* Function for route /auth/users/:address/nonce
* Protected route, generates a nonce to be used by frontend for registering a new user via wallet
*
* @param req
* @param res
* @returns
*/
export async function generateNonce(req: Request, res: Response) {
const headerCheck = RequestHeadersSchema.safeParse(req.headers)
if (!headerCheck.success) {
return handleAndReturnErrorResponse(req, res, headerCheck.error)
}

try {
const apiToken = headerCheck.data['X-API-Token']
const authToken = process.env.EE_AUTH_TOKEN

if (!apiToken || apiToken !== authToken) {
throw new Error('Unauthorized access.')
}

const nonce = `0x${crypto.randomBytes(32).toString('hex')}`

res.send({ nonce })
} catch (error) {
handleAndReturnErrorResponse(req, res, error)
}
}

/**
* Function for route /auth/users/:address/register
* Protected route, adds an address to the User table if it doesn't exist
*
* @param req
* @param res
* @returns
*/
export async function registerUser(req: Request, res: Response) {
const headerCheck = RequestHeadersSchema.safeParse(req.headers)
if (!headerCheck.success) {
return handleAndReturnErrorResponse(req, res, headerCheck.error)
}

const paramCheck = EthereumAddressSchema.safeParse(req.params.address)
if (!paramCheck.success) {
return handleAndReturnErrorResponse(req, res, paramCheck.error)
}

const bodyCheck = RegisterUserBodySchema.safeParse(req.body)
if (!bodyCheck.success) {
return handleAndReturnErrorResponse(req, res, bodyCheck.error)
}

try {
const apiToken = headerCheck.data['X-API-Token']
const authToken = process.env.EE_AUTH_TOKEN

if (!apiToken || apiToken !== authToken) {
throw new Error('Unauthorized access.')
}

const { address } = req.params
const { signature, nonce } = bodyCheck.data

const message = `Welcome to EigenExplorer!\n\nPlease sign this message to verify your wallet ownership.\n\nNonce: ${nonce}`

const isValid = await verifyMessage({
address: address as `0x${string}`,
message,
signature: signature as `0x${string}`
})

if (!isValid) {
throw new Error('Invalid signature')
}

const existingUser = await prisma.user.findUnique({
where: { address: address.toLowerCase() }
})

if (!existingUser) {
await prisma.user.create({
data: {
address: address.toLowerCase(),
isTracked: false
}
})
}

res.send({ isNewUser: !existingUser })
} catch (error) {
handleAndReturnErrorResponse(req, res, error)
}
}

/**
* Protected route, refreshes the server's entire auth store. Called by Supabase edge fn signal-refresh.
* This function will fail if the caller does not use admin-level auth token
*
* @param req
* @param res
* @returns
*/
export async function signalRefreshAuthStore(req: Request, res: Response) {
const headerCheck = RequestHeadersSchema.safeParse(req.headers)
if (!headerCheck.success) {
return handleAndReturnErrorResponse(req, res, headerCheck.error)
}

try {
const apiToken = headerCheck.data['X-API-Token']
const authToken = process.env.EE_AUTH_TOKEN

if (!apiToken || apiToken !== authToken) {
throw new Error('Unauthorized access.')
}

const status = await refreshAuthStore()

if (!status) {
throw new Error('Refresh auth store failed.')
}

res.status(200).json({ message: 'Auth store refreshed.' })
} catch (error) {
handleAndReturnErrorResponse(req, res, error)
}
}
15 changes: 15 additions & 0 deletions packages/api/src/routes/auth/authRoutes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import express from 'express'
import routeCache from 'route-cache'
import { signalRefreshAuthStore } from './authController'
import { checkUserStatus, generateNonce, registerUser } from './authController'

const router = express.Router()

// API routes for /auth

router.get('/refresh-store', routeCache.cacheSeconds(5), signalRefreshAuthStore)
router.get('/users/:address/check-status', routeCache.cacheSeconds(30), checkUserStatus)
router.get('/users/:address/nonce', routeCache.cacheSeconds(10), generateNonce)
router.post('/users/:address/register', routeCache.cacheSeconds(10), registerUser)

export default router
Loading
Loading