Skip to content
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
10 changes: 7 additions & 3 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/src'],
testMatch: ['**/__tests__/**/*.test.ts'],
roots: ['<rootDir>/src', '<rootDir>/tests'],
testMatch: ['**/?(*.)+(test).ts'],
setupFiles: ['<rootDir>/tests/setupEnv.ts'],
transform: {
'^.+\\.ts$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.test.json' }],
},
moduleFileExtensions: ['ts', 'js'],
collectCoverageFrom: [
'src/**/*.ts',
'!src/**/*.test.ts',
'!src/**/__tests__/**',
],
};
}
85 changes: 47 additions & 38 deletions package-lock.json

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

7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@
"jsonwebtoken": "^9.0.3",
"node-cron": "^4.2.1",
"twilio": "^4.11.0",
"winston": "^3.19.0"
"winston": "^3.19.0",
"zod": "^4.3.6"
},
"devDependencies": {
"@types/bcryptjs": "^2.4.6",
Expand All @@ -49,11 +50,11 @@
"@types/node": "^25.3.0",
"@types/node-cron": "^3.0.11",
"@types/twilio": "^3.19.3",
"@types/supertest": "^7.2.0",
"jest": "^30.2.0",
"nodemon": "^3.1.14",
"prisma": "^5.22.0",
"@types/supertest": "^6.0.2",
"supertest": "^6.3.3",
"supertest": "^7.2.2",
"ts-jest": "^29.4.6",
"ts-node": "^10.9.2",
"typescript": "^5.9.3"
Expand Down
44 changes: 19 additions & 25 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { config } from './config/env'
import { errorHandler } from './middleware/errorHandler'
import { requestLogger } from './middleware/logger'
import { rateLimiter } from './middleware/rateLimiter'
import { AuthMiddleware } from './middleware/authenticate'
import { logger } from './utils/logger'
import { startAgentLoop } from './agent/loop'
import { connectDb } from './db'
Expand All @@ -14,6 +13,11 @@ import healthRouter from './routes/health'
import agentRouter from './routes/agent'
import authRouter from './routes/auth'
import whatsappRouter from './routes/whatsapp'
import portfolioRouter from './routes/portfolio'
import transactionsRouter from './routes/transactions'
import protocolsRouter from './routes/protocols'
import depositRouter from './routes/deposit'
import withdrawRouter from './routes/withdraw'

const app = express()

Expand All @@ -32,49 +36,39 @@ app.use('/health', healthRouter)
app.use('/api/agent', agentRouter)
app.use('/api/auth', authRouter)
app.use('/api/whatsapp', whatsappRouter)

// Protected routes (require valid JWT)
// All routes mounted below this line are automatically protected.
app.use('/api/portfolio', AuthMiddleware.validateJwt)
app.use('/api/transactions', AuthMiddleware.validateJwt)
app.use('/api/deposit', AuthMiddleware.validateJwt)
app.use('/api/withdraw', AuthMiddleware.validateJwt)

// TODO: mount actual portfolio / transaction / deposit / withdraw routers here
// e.g. app.use('/api/portfolio', portfolioRouter)
app.use('/api/portfolio', portfolioRouter)
app.use('/api/transactions', transactionsRouter)
app.use('/api/protocols', protocolsRouter)
app.use('/api/deposit', depositRouter)
app.use('/api/withdraw', withdrawRouter)

// Global error handler — must always be last
app.use(errorHandler)

// Start server
async function main() {
// Database connectivity check
await connectDb()

// Background jobs
scheduleSessionCleanup()

// Start HTTP server
const server = app.listen(config.port, async () => {
app.listen(config.port, async () => {
logger.info(`NeuroWealth backend running on port ${config.port}`)
logger.info(`Environment: ${config.nodeEnv}`)
logger.info(`Network: ${config.stellar.network}`)

// Start autonomous agent loop

try {
await startAgentLoop()
} catch (error) {
logger.error('Failed to start agent loop', {
error: error instanceof Error ? error.message : 'Unknown error'
})
// Continue server operation even if agent fails to start
}
})
}

main().catch((error) => {
logger.error('[Startup] Unexpected error:', error)
process.exit(1)
})
if (require.main === module) {
main().catch((error) => {
logger.error('[Startup] Unexpected error:', error)
process.exit(1)
})
}

export default app
export default app
55 changes: 55 additions & 0 deletions src/middleware/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { NextFunction, Request, Response } from 'express'
import db from '../db'

function getBearerToken(req: Request): string | null {
const header = req.headers.authorization
if (!header) return null
const [scheme, token] = header.split(' ')
if (scheme !== 'Bearer' || !token) return null
return token
}

export async function requireAuth(
req: Request,
res: Response,
next: NextFunction,
) {
const token = getBearerToken(req)
if (!token) {
return res.status(401).json({ error: 'Unauthorized' })
}

const session = await db.session.findUnique({
where: { token },
include: { user: true },
})

if (!session || session.expiresAt < new Date() || !session.user.isActive) {
return res.status(401).json({ error: 'Unauthorized' })
}

req.auth = {
userId: session.userId,
sessionId: session.id,
walletAddress: session.walletAddress,
network: session.network,
}

return next()
}

export function enforceUserAccess(
req: Request,
res: Response,
next: NextFunction,
) {
const requestedUserId = (req.params.userId || req.body?.userId) as
| string
| undefined

if (!req.auth || (requestedUserId && req.auth.userId !== requestedUserId)) {
return res.status(401).json({ error: 'Unauthorized' })
}

return next()
}
Loading