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
21 changes: 17 additions & 4 deletions backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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';
Expand All @@ -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
Expand Down Expand Up @@ -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') {
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -366,4 +379,4 @@ const shutdown = (signal: string) => {
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));

export default app;
export default app;
39 changes: 39 additions & 0 deletions backend/src/middleware/session.ts
Original file line number Diff line number Diff line change
@@ -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();
}
81 changes: 81 additions & 0 deletions backend/src/routes/sessions.ts
Original file line number Diff line number Diff line change
@@ -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 });
}));
125 changes: 125 additions & 0 deletions backend/src/services/session.ts
Original file line number Diff line number Diff line change
@@ -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<string, Session> = 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());
}
Loading