diff --git a/backend/src/index.ts b/backend/src/index.ts index f22da201..7420d155 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -23,7 +23,6 @@ Sentry.init({ } }); import cors from 'cors'; -import rateLimit from 'express-rate-limit'; import { tokenBucketRateLimit } from './middleware/rate-limit.js'; import compression from 'compression'; import { config } from './config.js'; @@ -57,16 +56,21 @@ import { pushRouter } from './routes/push.js'; import { ipAllowlistRouter } from './routes/ip-allowlist.js'; import { gdprRouter } from './routes/gdpr.js'; import { ipAllowlistMiddleware, initIpAllowlist } from './middleware/ip-allowlist.js'; +import { sessionsRouter } from './routes/sessions.js'; +import { sessionMiddleware } from './middleware/session.js'; import { notificationsRouter } from './routes/notifications.js'; import { auditRouter } from './routes/audit.js'; import { hedgingRouter } from './routes/hedging.js'; import { complianceRouter } from './routes/compliance.js'; +import { kybRouter } from './routes/kyb.js'; +import { batchRouter } from './routes/batch.js'; +import { relayerRouter } from './routes/relayer.js'; +import { paymentQueueRouter } from './routes/payment-queue.js'; import { disputeRoutes } from './disputes/index.js'; import { disputeService } from './disputes/disputeService.js'; import http from 'node:http'; import { attachWebSocketServer } from './websocket/server.js'; import { createWebSocketRouter } from './routes/websocket.js'; -import { complianceRouter } from './routes/compliance.js'; import { receiptsRouter } from './routes/receipts.js'; import { eventsRouter } from './routes/events.js'; import { threatDetectionRouter } from './routes/threat-detection.js'; @@ -84,6 +88,7 @@ import { tokenizationRouter } from './routes/tokenization.js'; import { startWebhookWorker, stopWebhookWorker } from './services/webhooks.js'; import { analyticsService } from './services/analytics.js'; import { createAnalyticsRouter } from './routes/analytics.js'; +import { paymentQueue } from './queue/payment-queue.js'; import './events/projections.js'; // Validate environment variables at startup @@ -193,6 +198,7 @@ app.use((req: Request, res: Response, next: NextFunction) => { }); app.use(slaTrackingMiddleware); +app.use(sessionMiddleware); app.use((req: Request, res: Response, next: NextFunction) => { if (req.method !== 'GET' && req.method !== 'HEAD') { @@ -235,7 +241,7 @@ apiV1Router.use('/portfolio', portfolioRouter); apiV1Router.use('/backup', backupRouter); apiV1Router.use('/ip-allowlist', ipAllowlistRouter); apiV1Router.use('/push', pushRouter); -// Rate limit analytics +apiV1Router.use('/sessions', sessionsRouter); apiV1Router.use('/rate-limit', rateLimitAnalyticsRouter); app.use('/api/v1', ipAllowlistMiddleware(), apiV1Router); @@ -244,6 +250,13 @@ app.use('/api/v1/notifications', notificationsRouter); app.use('/api/v1/audit', auditRouter); app.use('/api/v1/hedging', hedgingRouter); app.use('/api/v1/compliance', complianceRouter); +app.use('/api/v1/gdpr', gdprRouter); +app.use('/api/v1/escrow', escrowRouter); +app.use('/api/v1/multisig', multisigRouter); +app.use('/api/v1/webhooks', webhooksRouter); +app.use('/api/v1/fraud-detection', fraudDetectionRouter); +app.use('/api/v1/bridge', bridgeRouter); +app.use('/api/v1/tokenization', tokenizationRouter); // Payment receipt NFTs app.use('/api/v1/receipts', receiptsRouter); @@ -366,4 +379,4 @@ const shutdown = (signal: string) => { process.on('SIGTERM', () => shutdown('SIGTERM')); process.on('SIGINT', () => shutdown('SIGINT')); -export default app; \ No newline at end of file +export default app; diff --git a/backend/src/middleware/session.ts b/backend/src/middleware/session.ts new file mode 100644 index 00000000..885a36a7 --- /dev/null +++ b/backend/src/middleware/session.ts @@ -0,0 +1,39 @@ +import { Request, Response, NextFunction } from 'express'; +import { updateSessionActivity, getSession, checkSessionAnomaly } from '../services/session.js'; + +/** + * Middleware to track session activity and detect anomalies. + */ +export function sessionMiddleware(req: Request, res: Response, next: NextFunction) { + const sessionId = req.headers['x-session-id'] as string; + + if (sessionId) { + const session = getSession(sessionId); + + if (session) { + if (session.status === 'terminated') { + return res.status(401).json({ + error: { + code: 'SESSION_TERMINATED', + message: 'Your session has been terminated. Please log in again.', + status: 401 + } + }); + } + + const currentIp = (req.headers['x-forwarded-for'] as string) || req.socket.remoteAddress || '127.0.0.1'; + + // Update activity + updateSessionActivity(sessionId, currentIp); + + // Check for anomalies + const anomaly = checkSessionAnomaly(session, currentIp); + if (anomaly) { + console.warn(`[Session Anomaly] ${anomaly} for session ${sessionId} (User: ${session.userId})`); + res.setHeader('X-Session-Warning', anomaly); + } + } + } + + next(); +} diff --git a/backend/src/routes/sessions.ts b/backend/src/routes/sessions.ts new file mode 100644 index 00000000..a4c08baa --- /dev/null +++ b/backend/src/routes/sessions.ts @@ -0,0 +1,81 @@ +import { Router } from 'express'; +import { asyncHandler } from '../middleware/errorHandler.js'; +import { + getUserSessions, + terminateSession, + terminateOtherSessions, + getSessionHistory, + trustDevice, + createSession +} from '../services/session.js'; +import { AppError } from '../middleware/errorHandler.js'; + +export const sessionsRouter = Router(); + +// Mock user ID middleware for demo purposes +// In a real app, this would come from the auth middleware +const getUserId = (req: any) => req.headers['x-user-id'] || 'user_default'; + +// Get active sessions +sessionsRouter.get('/', asyncHandler(async (req, res) => { + const userId = getUserId(req); + const sessions = getUserSessions(userId); + res.json({ sessions }); +})); + +// Get session history +sessionsRouter.get('/history', asyncHandler(async (req, res) => { + const userId = getUserId(req); + const history = getSessionHistory(userId); + res.json({ history }); +})); + +// Create a new session (Mock login) +sessionsRouter.post('/login', asyncHandler(async (req, res) => { + const userId = getUserId(req); + const { deviceId, browser, os } = req.body; + + const ip = (req.headers['x-forwarded-for'] as string) || req.socket.remoteAddress || '127.0.0.1'; + + const session = createSession(userId, { + deviceId: deviceId || 'unknown', + browser: browser || req.headers['user-agent'] || 'unknown', + os: os || 'unknown', + ip + }); + + res.json({ session }); +})); + +// Terminate a specific session +sessionsRouter.delete('/:id', asyncHandler(async (req, res) => { + const id = req.params.id as string; + const success = terminateSession(id); + + if (!success) { + throw new AppError(404, 'Session not found', 'SESSION_NOT_FOUND'); + } + + res.json({ success: true }); +})); + +// Terminate all other sessions +sessionsRouter.delete('/others/:currentId', asyncHandler(async (req, res) => { + const userId = getUserId(req); + const currentId = req.params.currentId as string; + + const count = terminateOtherSessions(userId, currentId); + res.json({ success: true, terminatedCount: count }); +})); + +// Trust a device +sessionsRouter.post('/:id/trust', asyncHandler(async (req, res) => { + const id = req.params.id as string; + const success = trustDevice(id); + + if (!success) { + throw new AppError(404, 'Session not found', 'SESSION_NOT_FOUND'); + } + + res.json({ success: true }); +})); diff --git a/backend/src/services/session.ts b/backend/src/services/session.ts new file mode 100644 index 00000000..b82cef72 --- /dev/null +++ b/backend/src/services/session.ts @@ -0,0 +1,125 @@ +import { randomUUID } from 'node:crypto'; + +export interface Session { + id: string; + userId: string; + deviceId: string; + browser: string; + os: string; + ip: string; + lastActive: string; + isTrusted: boolean; + status: 'active' | 'terminated'; + createdAt: string; +} + +const sessions: Map = new Map(); +const MAX_CONCURRENT_SESSIONS = 5; + +/** + * Creates a new session for a user. + * If concurrent limits are exceeded, terminates the oldest session. + */ +export function createSession(userId: string, metadata: { deviceId: string; browser: string; os: string; ip: string }): Session { + const userSessions = getUserSessions(userId); + + if (userSessions.length >= MAX_CONCURRENT_SESSIONS) { + const oldest = userSessions.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime())[0]; + terminateSession(oldest.id); + } + + const session: Session = { + id: `sess_${randomUUID()}`, + userId, + ...metadata, + lastActive: new Date().toISOString(), + isTrusted: false, + status: 'active', + createdAt: new Date().toISOString(), + }; + + sessions.set(session.id, session); + return session; +} + +/** + * Gets all active sessions for a user. + */ +export function getUserSessions(userId: string): Session[] { + return Array.from(sessions.values()).filter(s => s.userId === userId && s.status === 'active'); +} + +/** + * Gets a specific session. + */ +export function getSession(sessionId: string): Session | undefined { + return sessions.get(sessionId); +} + +/** + * Terminates a specific session. + */ +export function terminateSession(sessionId: string): boolean { + const session = sessions.get(sessionId); + if (session) { + session.status = 'terminated'; + return true; + } + return false; +} + +/** + * Terminates all other sessions for a user except the current one. + */ +export function terminateOtherSessions(userId: string, currentSessionId: string): number { + let count = 0; + sessions.forEach(session => { + if (session.userId === userId && session.id !== currentSessionId && session.status === 'active') { + session.status = 'terminated'; + count++; + } + }); + return count; +} + +/** + * Updates the last active timestamp for a session. + */ +export function updateSessionActivity(sessionId: string, ip?: string): void { + const session = sessions.get(sessionId); + if (session && session.status === 'active') { + session.lastActive = new Date().toISOString(); + if (ip) session.ip = ip; + } +} + +/** + * Detects anomalies in session activity (e.g., sudden IP change). + */ +export function checkSessionAnomaly(session: Session, currentIp: string): string | null { + if (session.ip !== currentIp && !session.isTrusted) { + return 'IP_CHANGE_DETECTED'; + } + return null; +} + +/** + * Marks a device as trusted for a session. + */ +export function trustDevice(sessionId: string): boolean { + const session = sessions.get(sessionId); + if (session) { + session.isTrusted = true; + return true; + } + return false; +} + +/** + * Gets session history (including terminated ones). + */ +export function getSessionHistory(userId: string): Session[] { + return Array.from(sessions.values()) + .filter(s => s.userId === userId) + .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); +}