From 1665e840498063b307d2e8a77f922a23393f5da5 Mon Sep 17 00:00:00 2001 From: Romil Chouhan Date: Wed, 22 Oct 2025 17:46:53 +0530 Subject: [PATCH] @Trading Platform @Static frontend @Working --- docker-compose.yml | 2 - frontend/package.json | 33 ++- frontend/public/index.html | 14 + frontend/public/manifest.json | 15 + frontend/public/robots.txt | 3 + frontend/src/App.css | 15 + frontend/src/App.js | 80 ++++++ frontend/src/components/common/Header.js | 88 ++++++ .../src/components/common/ProtectedRoute.js | 20 ++ frontend/src/components/common/Sidebar.js | 74 +++++ frontend/src/context/AuthContext.js | 89 ++++++ frontend/src/context/WebSocketContext.js | 126 +++++++++ frontend/src/index.css | 19 ++ frontend/src/index.js | 11 + frontend/src/pages/DashboardPage.js | 58 ++++ frontend/src/pages/LoginPage.js | 105 +++++++ frontend/src/pages/MarketDataPage.js | 23 ++ frontend/src/pages/OrderBookPage.js | 167 +++++++++++ frontend/src/pages/PortfolioPage.js | 57 ++++ frontend/src/pages/SettingsPage.js | 23 ++ frontend/src/services/api.js | 41 +++ frontend/src/services/authService.js | 35 +++ frontend/src/services/marketDataService.js | 23 ++ frontend/src/services/orderService.js | 41 +++ services/api-gateway/src/middleware/auth.js | 35 +++ .../src/middleware/errorHandler.js | 33 +++ services/api-gateway/src/server.js | 137 +++++++++ .../api-gateway/src/services/AuthService.js | 133 +++++++++ .../src/services/WebSocketManager.js | 188 +++++++++++++ services/market-data-service/package.json | 18 ++ .../src/controllers/MarketDataController.js | 120 ++++++++ services/market-data-service/src/server.js | 38 +++ .../src/services/MarketDataService.js | 147 ++++++++++ .../src/services/PriceService.js | 155 +++++++++++ services/orderbook-service/package.json | 19 ++ .../orderbook-service/src/OrderBookWrapper.js | 262 ++++++++++++++++++ .../src/controllers/MarketDataController.js | 60 ++++ .../src/controllers/OrderController.js | 108 ++++++++ services/orderbook-service/src/server.js | 37 +++ services/user-service/package.json | 18 ++ .../src/controllers/PortfolioController.js | 93 +++++++ .../src/controllers/UserController.js | 111 ++++++++ services/user-service/src/server.js | 40 +++ .../src/services/PortfolioService.js | 199 +++++++++++++ .../user-service/src/services/UserService.js | 144 ++++++++++ 45 files changed, 3251 insertions(+), 6 deletions(-) create mode 100644 frontend/public/index.html create mode 100644 frontend/public/manifest.json create mode 100644 frontend/public/robots.txt create mode 100644 frontend/src/App.css create mode 100644 frontend/src/App.js create mode 100644 frontend/src/components/common/Header.js create mode 100644 frontend/src/components/common/ProtectedRoute.js create mode 100644 frontend/src/components/common/Sidebar.js create mode 100644 frontend/src/context/AuthContext.js create mode 100644 frontend/src/context/WebSocketContext.js create mode 100644 frontend/src/index.css create mode 100644 frontend/src/index.js create mode 100644 frontend/src/pages/DashboardPage.js create mode 100644 frontend/src/pages/LoginPage.js create mode 100644 frontend/src/pages/MarketDataPage.js create mode 100644 frontend/src/pages/OrderBookPage.js create mode 100644 frontend/src/pages/PortfolioPage.js create mode 100644 frontend/src/pages/SettingsPage.js create mode 100644 frontend/src/services/api.js create mode 100644 frontend/src/services/authService.js create mode 100644 frontend/src/services/marketDataService.js create mode 100644 frontend/src/services/orderService.js create mode 100644 services/api-gateway/src/middleware/auth.js create mode 100644 services/api-gateway/src/middleware/errorHandler.js create mode 100644 services/api-gateway/src/server.js create mode 100644 services/api-gateway/src/services/AuthService.js create mode 100644 services/api-gateway/src/services/WebSocketManager.js create mode 100644 services/market-data-service/package.json create mode 100644 services/market-data-service/src/controllers/MarketDataController.js create mode 100644 services/market-data-service/src/server.js create mode 100644 services/market-data-service/src/services/MarketDataService.js create mode 100644 services/market-data-service/src/services/PriceService.js create mode 100644 services/orderbook-service/package.json create mode 100644 services/orderbook-service/src/OrderBookWrapper.js create mode 100644 services/orderbook-service/src/controllers/MarketDataController.js create mode 100644 services/orderbook-service/src/controllers/OrderController.js create mode 100644 services/orderbook-service/src/server.js create mode 100644 services/user-service/package.json create mode 100644 services/user-service/src/controllers/PortfolioController.js create mode 100644 services/user-service/src/controllers/UserController.js create mode 100644 services/user-service/src/server.js create mode 100644 services/user-service/src/services/PortfolioService.js create mode 100644 services/user-service/src/services/UserService.js diff --git a/docker-compose.yml b/docker-compose.yml index 4483d65..83e344a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,3 @@ -version: '3.8' - services: redis: image: redis:7-alpine diff --git a/frontend/package.json b/frontend/package.json index 3dfa28d..b172953 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -3,17 +3,42 @@ "version": "1.0.0", "private": true, "dependencies": { + "@testing-library/jest-dom": "^5.16.4", + "@testing-library/react": "^13.3.0", + "@testing-library/user-event": "^13.5.0", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-scripts": "5.0.1", "react-router-dom": "^6.3.0", + "react-scripts": "5.0.1", "axios": "^1.4.0", "@mui/material": "^5.14.1", - "@mui/icons-material": "^5.14.1" + "@mui/icons-material": "^5.14.1", + "@emotion/react": "^11.11.1", + "@emotion/styled": "^11.11.0", + "web-vitals": "^2.1.4" }, "scripts": { - "start": "rea[<35;82;26Mct-scripts start", + "start": "react-scripts start", "build": "react-scripts build", - "test": "react-scripts test" + "test": "react-scripts test", + "eject": "react-scripts eject" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] } } diff --git a/frontend/public/index.html b/frontend/public/index.html new file mode 100644 index 0000000..488a532 --- /dev/null +++ b/frontend/public/index.html @@ -0,0 +1,14 @@ + + + + + + + + Trading Platform + + + +
+ + diff --git a/frontend/public/manifest.json b/frontend/public/manifest.json new file mode 100644 index 0000000..f4bd73e --- /dev/null +++ b/frontend/public/manifest.json @@ -0,0 +1,15 @@ +{ + "short_name": "Trading", + "name": "Trading Platform", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#1976d2", + "background_color": "#0a0e27" +} diff --git a/frontend/public/robots.txt b/frontend/public/robots.txt new file mode 100644 index 0000000..3b39592 --- /dev/null +++ b/frontend/public/robots.txt @@ -0,0 +1,3 @@ +# Allow all robot +User-agent: * +Disallow: diff --git a/frontend/src/App.css b/frontend/src/App.css new file mode 100644 index 0000000..f25157f --- /dev/null +++ b/frontend/src/App.css @@ -0,0 +1,15 @@ +.App { + min-height: 100vh; + background-color: #0a0e27; +} + +.App-header { + background-color: #1a1a2e; + min-height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + font-size: calc(10px + 2vmin); + color: white; +} diff --git a/frontend/src/App.js b/frontend/src/App.js new file mode 100644 index 0000000..1b598ef --- /dev/null +++ b/frontend/src/App.js @@ -0,0 +1,80 @@ +import React from 'react'; +import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'; +import { ThemeProvider, createTheme } from '@mui/material/styles'; +import CssBaseline from '@mui/material/CssBaseline'; +import { AuthProvider } from './context/AuthContext'; +import { WebSocketProvider } from './context/WebSocketContext'; +import ProtectedRoute from './components/common/ProtectedRoute'; +import LoginPage from './pages/LoginPage'; +import DashboardPage from './pages/DashboardPage'; +import OrderBookPage from './pages/OrderBookPage'; +import PortfolioPage from './pages/PortfolioPage'; +import MarketDataPage from './pages/MarketDataPage'; +import SettingsPage from './pages/SettingsPage'; +import './App.css'; + +const darkTheme = createTheme({ + palette: { + mode: 'dark', + primary: { + main: '#1976d2', + }, + secondary: { + main: '#dc004e', + }, + background: { + default: '#0a0e27', + paper: '#1a1a2e', + }, + }, + typography: { + fontFamily: 'Roboto, Arial, sans-serif', + }, +}); + +function App() { + return ( + + + + + +
+ + } /> + + + + } /> + + + + } /> + + + + } /> + + + + } /> + + + + } /> + } /> + +
+
+
+
+
+ ); +} + +export default App; diff --git a/frontend/src/components/common/Header.js b/frontend/src/components/common/Header.js new file mode 100644 index 0000000..e41930e --- /dev/null +++ b/frontend/src/components/common/Header.js @@ -0,0 +1,88 @@ +import React from 'react'; +import { + AppBar, + Toolbar, + Typography, + IconButton, + Menu, + MenuItem, + Avatar, + Box +} from '@mui/material'; +import { AccountCircle, Notifications } from '@mui/icons-material'; +import { useAuth } from '../../context/AuthContext'; +import { useWebSocket } from '../../context/WebSocketContext'; + +const Header = () => { + const { user, logout } = useAuth(); + const { connected } = useWebSocket(); + const [anchorEl, setAnchorEl] = React.useState(null); + + const handleMenu = (event) => { + setAnchorEl(event.currentTarget); + }; + + const handleClose = () => { + setAnchorEl(null); + }; + + const handleLogout = () => { + handleClose(); + logout(); + }; + + return ( + + + + Trading Platform + + + + {/* Connection Status */} + + + + {connected ? 'Connected' : 'Disconnected'} + + + + {/* Notifications */} + + + + + {/* User Menu */} + + + {user?.firstName?.charAt(0) || 'U'} + + + + Profile + Settings + Logout + + + + + ); +}; + +export default Header; diff --git a/frontend/src/components/common/ProtectedRoute.js b/frontend/src/components/common/ProtectedRoute.js new file mode 100644 index 0000000..a67c212 --- /dev/null +++ b/frontend/src/components/common/ProtectedRoute.js @@ -0,0 +1,20 @@ +import React from 'react'; +import { Navigate } from 'react-router-dom'; +import { useAuth } from '../../context/AuthContext'; +import { CircularProgress, Box } from '@mui/material'; + +const ProtectedRoute = ({ children }) => { + const { isAuthenticated, loading } = useAuth(); + + if (loading) { + return ( + + + + ); + } + + return isAuthenticated ? children : ; +}; + +export default ProtectedRoute; diff --git a/frontend/src/components/common/Sidebar.js b/frontend/src/components/common/Sidebar.js new file mode 100644 index 0000000..fa465ff --- /dev/null +++ b/frontend/src/components/common/Sidebar.js @@ -0,0 +1,74 @@ +import React from 'react'; +import { useNavigate, useLocation } from 'react-router-dom'; +import { + Drawer, + List, + ListItem, + ListItemButton, + ListItemIcon, + ListItemText, + Box +} from '@mui/material'; +import { + Dashboard, + ShowChart, + AccountBalance, + TrendingUp, + Settings +} from '@mui/icons-material'; + +const Sidebar = () => { + const navigate = useNavigate(); + const location = useLocation(); + + const menuItems = [ + { text: 'Dashboard', icon: , path: '/' }, + { text: 'OrderBook', icon: , path: '/orderbook' }, + { text: 'Portfolio', icon: , path: '/portfolio' }, + { text: 'Market Data', icon: , path: '/market-data' }, + { text: 'Settings', icon: , path: '/settings' } + ]; + + return ( + + + + {menuItems.map((item) => ( + + navigate(item.path)} + sx={{ + '&.Mui-selected': { + bgcolor: 'rgba(25, 118, 210, 0.2)', + borderRight: '3px solid #1976d2' + } + }} + > + + {item.icon} + + + + + ))} + + + + ); +}; + +export default Sidebar; diff --git a/frontend/src/context/AuthContext.js b/frontend/src/context/AuthContext.js new file mode 100644 index 0000000..283e135 --- /dev/null +++ b/frontend/src/context/AuthContext.js @@ -0,0 +1,89 @@ +import React, { createContext, useContext, useState, useEffect } from 'react'; +import authService from '../services/authService'; + +const AuthContext = createContext(); + +export const useAuth = () => { + const context = useContext(AuthContext); + if (!context) { + throw new Error('useAuth must be used within an AuthProvider'); + } + return context; +}; + +export const AuthProvider = ({ children }) => { + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + const [isAuthenticated, setIsAuthenticated] = useState(false); + + useEffect(() => { + const initializeAuth = async () => { + const token = localStorage.getItem('token'); + if (token) { + try { + // Parse JWT to get user data (simple decode, don't use in production without validation) + const base64Url = token.split('.')[1]; + const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); + const jsonPayload = decodeURIComponent(atob(base64).split('').map(function(c) { + return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); + }).join('')); + + const userData = JSON.parse(jsonPayload); + setUser(userData); + setIsAuthenticated(true); + } catch (error) { + console.error('Token validation failed:', error); + localStorage.removeItem('token'); + } + } + setLoading(false); + }; + + initializeAuth(); + }, []); + + const login = async (email, password) => { + try { + const response = await authService.login(email, password); + if (response.success) { + localStorage.setItem('token', response.token); + setUser(response.user); + setIsAuthenticated(true); + return { success: true }; + } + return { success: false, message: response.message }; + } catch (error) { + return { success: false, message: 'Login failed' }; + } + }; + + const logout = () => { + localStorage.removeItem('token'); + setUser(null); + setIsAuthenticated(false); + }; + + const register = async (userData) => { + try { + const response = await authService.register(userData); + return response; + } catch (error) { + return { success: false, message: 'Registration failed' }; + } + }; + + const value = { + user, + loading, + isAuthenticated, + login, + logout, + register + }; + + return ( + + {children} + + ); +}; diff --git a/frontend/src/context/WebSocketContext.js b/frontend/src/context/WebSocketContext.js new file mode 100644 index 0000000..a96a6d2 --- /dev/null +++ b/frontend/src/context/WebSocketContext.js @@ -0,0 +1,126 @@ +import React, { createContext, useContext, useEffect, useState } from 'react'; +import { useAuth } from './AuthContext'; + +const WebSocketContext = createContext(); + +export const useWebSocket = () => { + const context = useContext(WebSocketContext); + if (!context) { + throw new Error('useWebSocket must be used within a WebSocketProvider'); + } + return context; +}; + +export const WebSocketProvider = ({ children }) => { + const [ws, setWs] = useState(null); + const [connected, setConnected] = useState(false); + const [marketData, setMarketData] = useState({}); + const [orderBookData, setOrderBookData] = useState({}); + const { isAuthenticated } = useAuth(); + + useEffect(() => { + if (!isAuthenticated) return; + + const wsUrl = process.env.REACT_APP_WS_URL || 'ws://localhost:8080'; + const websocket = new WebSocket(wsUrl); + + websocket.onopen = () => { + console.log('WebSocket connected'); + setConnected(true); + setWs(websocket); + }; + + websocket.onmessage = (event) => { + try { + const data = JSON.parse(event.data); + handleWebSocketMessage(data); + } catch (error) { + console.error('Error parsing WebSocket message:', error); + } + }; + + websocket.onclose = () => { + console.log('WebSocket disconnected'); + setConnected(false); + setWs(null); + }; + + websocket.onerror = (error) => { + console.error('WebSocket error:', error); + }; + + return () => { + if (websocket.readyState === WebSocket.OPEN) { + websocket.close(); + } + }; + }, [isAuthenticated]); + + const handleWebSocketMessage = (data) => { + switch (data.type) { + case 'broadcast': + if (data.channel === 'market_data_updates') { + handleMarketDataUpdate(data.data); + } + break; + case 'connection': + console.log('WebSocket connection confirmed:', data.clientId); + break; + case 'subscription_confirmed': + console.log('Subscribed to:', data.channel); + break; + default: + console.log('Unknown WebSocket message type:', data.type); + } + }; + + const handleMarketDataUpdate = (data) => { + if (data.type === 'price_update') { + setMarketData(prev => ({ + ...prev, + [data.symbol]: { + ...prev[data.symbol], + price: data.price, + timestamp: data.timestamp + } + })); + } else if (data.type === 'orderbook_update') { + setOrderBookData(prev => ({ + ...prev, + [data.symbol]: data.data + })); + } + }; + + const subscribe = (channel) => { + if (ws && connected) { + ws.send(JSON.stringify({ + type: 'subscribe', + channel + })); + } + }; + + const unsubscribe = (channel) => { + if (ws && connected) { + ws.send(JSON.stringify({ + type: 'unsubscribe', + channel + })); + } + }; + + const value = { + connected, + marketData, + orderBookData, + subscribe, + unsubscribe + }; + + return ( + + {children} + + ); +}; diff --git a/frontend/src/index.css b/frontend/src/index.css new file mode 100644 index 0000000..4f3d9b7 --- /dev/null +++ b/frontend/src/index.css @@ -0,0 +1,19 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + margin: 0; + font-family: 'Roboto', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', + monospace; +} diff --git a/frontend/src/index.js b/frontend/src/index.js new file mode 100644 index 0000000..0dd56bb --- /dev/null +++ b/frontend/src/index.js @@ -0,0 +1,11 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import './index.css'; +import App from './App'; + +const root = ReactDOM.createRoot(document.getElementById('root')); +root.render( + + + +); diff --git a/frontend/src/pages/DashboardPage.js b/frontend/src/pages/DashboardPage.js new file mode 100644 index 0000000..0a2b0c6 --- /dev/null +++ b/frontend/src/pages/DashboardPage.js @@ -0,0 +1,58 @@ +import React from 'react'; +import { Box, Grid, Paper, Typography } from '@mui/material'; +import Header from '../components/common/Header'; +import Sidebar from '../components/common/Sidebar'; +import { useAuth } from '../context/AuthContext'; + +const DashboardPage = () => { + const { user } = useAuth(); + + return ( + +
+ + + + + Welcome back, {user?.firstName || 'Trader'}! + + + + + + Portfolio Value + $125,000.50 + +2.5% Today + + + + + + Daily P&L + +$2,500.75 + +2.04% + + + + + + Active Orders + 12 + 3 Pending + + + + + + Quick Actions + + Navigate using the sidebar to access OrderBook, Portfolio, Market Data, and Settings. + + + + + + ); +}; + +export default DashboardPage; diff --git a/frontend/src/pages/LoginPage.js b/frontend/src/pages/LoginPage.js new file mode 100644 index 0000000..f5e61f0 --- /dev/null +++ b/frontend/src/pages/LoginPage.js @@ -0,0 +1,105 @@ +import React, { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { + Container, + Box, + TextField, + Button, + Typography, + Paper, + Alert +} from '@mui/material'; +import { useAuth } from '../context/AuthContext'; + +const LoginPage = () => { + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [error, setError] = useState(''); + const { login } = useAuth(); + const navigate = useNavigate(); + + const handleSubmit = async (e) => { + e.preventDefault(); + setError(''); + + const result = await login(email, password); + if (result.success) { + navigate('/'); + } else { + setError(result.message || 'Login failed'); + } + }; + + return ( + + + + + Trading Platform + + + Sign in to continue + + + {error && ( + + {error} + + )} + + + setEmail(e.target.value)} + /> + setPassword(e.target.value)} + /> + + + + + Demo Credentials: + + + Email: trader@example.com + + + Password: demo123 + + + + + + + ); +}; + +export default LoginPage; diff --git a/frontend/src/pages/MarketDataPage.js b/frontend/src/pages/MarketDataPage.js new file mode 100644 index 0000000..fc20c2f --- /dev/null +++ b/frontend/src/pages/MarketDataPage.js @@ -0,0 +1,23 @@ +import React from 'react'; +import { Box, Paper, Typography } from '@mui/material'; +import Header from '../components/common/Header'; +import Sidebar from '../components/common/Sidebar'; + +const MarketDataPage = () => { + return ( + +
+ + + + Market Data + + Real-time market data will be displayed here. + + + + + ); +}; + +export default MarketDataPage; diff --git a/frontend/src/pages/OrderBookPage.js b/frontend/src/pages/OrderBookPage.js new file mode 100644 index 0000000..b6b5cb5 --- /dev/null +++ b/frontend/src/pages/OrderBookPage.js @@ -0,0 +1,167 @@ +import React, { useState, useEffect } from 'react'; +import { Box, Paper, Typography, Grid, Button, TextField, MenuItem } from '@mui/material'; +import Header from '../components/common/Header'; +import Sidebar from '../components/common/Sidebar'; +import orderService from '../services/orderService'; +import { useAuth } from '../context/AuthContext'; + +const OrderBookPage = () => { + const [orderBook, setOrderBook] = useState({ bids: [], asks: [] }); + const [symbol, setSymbol] = useState('BTC/USD'); + const [side, setSide] = useState('BUY'); + const [quantity, setQuantity] = useState(''); + const [price, setPrice] = useState(''); + const [message, setMessage] = useState(''); + const { user } = useAuth(); + + useEffect(() => { + loadOrderBook(); + const interval = setInterval(loadOrderBook, 5000); + return () => clearInterval(interval); + }, [symbol]); + + const loadOrderBook = async () => { + try { + const response = await orderService.getOrderBook(symbol); + if (response.success) { + setOrderBook(response.data); + } + } catch (error) { + console.error('Failed to load order book:', error); + } + }; + + const handlePlaceOrder = async () => { + try { + const response = await orderService.placeOrder({ + symbol, + side, + quantity: parseFloat(quantity), + price: parseFloat(price), + type: 'LIMIT' + }); + + if (response.success) { + setMessage(`Order placed successfully! Order ID: ${response.orderId}`); + setQuantity(''); + setPrice(''); + loadOrderBook(); + } + } catch (error) { + setMessage('Failed to place order: ' + error.message); + } + }; + + return ( + +
+ + + + Order Book + + + {/* Order Book Display */} + + + {symbol} + + + {/* Bids */} + + BIDS + {orderBook.bids?.slice(0, 10).map((bid, idx) => ( + + {bid.price.toFixed(2)} + {bid.quantity.toFixed(4)} + + ))} + + + {/* Asks */} + + ASKS + {orderBook.asks?.slice(0, 10).map((ask, idx) => ( + + {ask.price.toFixed(2)} + {ask.quantity.toFixed(4)} + + ))} + + + + + + {/* Order Entry Form */} + + + Place Order + + setSymbol(e.target.value)} + margin="normal" + > + BTC/USD + ETH/USD + + + setSide(e.target.value)} + margin="normal" + > + BUY + SELL + + + setQuantity(e.target.value)} + margin="normal" + /> + + setPrice(e.target.value)} + margin="normal" + /> + + + + {message && ( + + {message} + + )} + + + + + + + ); +}; + +export default OrderBookPage; diff --git a/frontend/src/pages/PortfolioPage.js b/frontend/src/pages/PortfolioPage.js new file mode 100644 index 0000000..539059e --- /dev/null +++ b/frontend/src/pages/PortfolioPage.js @@ -0,0 +1,57 @@ +import React from 'react'; +import { Box, Paper, Typography, Table, TableBody, TableCell, TableContainer, TableHead, TableRow } from '@mui/material'; +import Header from '../components/common/Header'; +import Sidebar from '../components/common/Sidebar'; + +const PortfolioPage = () => { + const positions = [ + { symbol: 'BTC/USD', quantity: 2.5, avgPrice: 45000, currentPrice: 45250.50, pnl: 626.25 }, + { symbol: 'ETH/USD', quantity: 10.0, avgPrice: 3100, currentPrice: 3150.25, pnl: 502.50 }, + ]; + + return ( + +
+ + + + Portfolio + + + Total Portfolio Value: $125,000.50 + Daily P&L: +$2,500.75 (+2.04%) + + + + + + + Symbol + Quantity + Avg Price + Current Price + P&L + + + + {positions.map((position) => ( + + {position.symbol} + {position.quantity} + ${position.avgPrice.toFixed(2)} + ${position.currentPrice.toFixed(2)} + = 0 ? 'success.main' : 'error.main' }}> + ${position.pnl.toFixed(2)} + + + ))} + +
+
+
+
+ + ); +}; + +export default PortfolioPage; diff --git a/frontend/src/pages/SettingsPage.js b/frontend/src/pages/SettingsPage.js new file mode 100644 index 0000000..ed00c0d --- /dev/null +++ b/frontend/src/pages/SettingsPage.js @@ -0,0 +1,23 @@ +import React from 'react'; +import { Box, Paper, Typography } from '@mui/material'; +import Header from '../components/common/Header'; +import Sidebar from '../components/common/Sidebar'; + +const SettingsPage = () => { + return ( + +
+ + + + Settings + + User settings and preferences will be displayed here. + + + + + ); +}; + +export default SettingsPage; diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js new file mode 100644 index 0000000..f8dd1ed --- /dev/null +++ b/frontend/src/services/api.js @@ -0,0 +1,41 @@ +import axios from 'axios'; + +const API_BASE_URL = process.env.REACT_APP_API_URL || 'http://localhost:8080/api'; + +const api = axios.create({ + baseURL: API_BASE_URL, + timeout: 10000, + headers: { + 'Content-Type': 'application/json', + }, +}); + +// Request interceptor to add auth token +api.interceptors.request.use( + (config) => { + const token = localStorage.getItem('token'); + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; + }, + (error) => { + return Promise.reject(error); + } +); + +// Response interceptor to handle errors +api.interceptors.response.use( + (response) => { + return response; + }, + (error) => { + if (error.response?.status === 401) { + localStorage.removeItem('token'); + window.location.href = '/login'; + } + return Promise.reject(error); + } +); + +export default api; diff --git a/frontend/src/services/authService.js b/frontend/src/services/authService.js new file mode 100644 index 0000000..d2196b1 --- /dev/null +++ b/frontend/src/services/authService.js @@ -0,0 +1,35 @@ +import api from './api'; + +class AuthService { + async login(email, password) { + try { + const response = await api.post('/auth/login', { email, password }); + return response.data; + } catch (error) { + throw error.response?.data || { message: 'Login failed' }; + } + } + + async register(userData) { + try { + const response = await api.post('/auth/register', userData); + return response.data; + } catch (error) { + throw error.response?.data || { message: 'Registration failed' }; + } + } + + logout() { + localStorage.removeItem('token'); + } + + getToken() { + return localStorage.getItem('token'); + } + + isAuthenticated() { + return !!this.getToken(); + } +} + +export default new AuthService(); diff --git a/frontend/src/services/marketDataService.js b/frontend/src/services/marketDataService.js new file mode 100644 index 0000000..954e862 --- /dev/null +++ b/frontend/src/services/marketDataService.js @@ -0,0 +1,23 @@ +import api from './api'; + +class MarketDataService { + async getMarketData(symbol) { + try { + const response = await api.get(`/market-data/${symbol}`); + return response.data; + } catch (error) { + throw error.response?.data || { message: 'Failed to fetch market data' }; + } + } + + async getSymbols() { + try { + const response = await api.get('/symbols'); + return response.data; + } catch (error) { + throw error.response?.data || { message: 'Failed to fetch symbols' }; + } + } +} + +export default new MarketDataService(); diff --git a/frontend/src/services/orderService.js b/frontend/src/services/orderService.js new file mode 100644 index 0000000..f2024ef --- /dev/null +++ b/frontend/src/services/orderService.js @@ -0,0 +1,41 @@ +import api from './api'; + +class OrderService { + async placeOrder(orderData) { + try { + const response = await api.post('/orders', orderData); + return response.data; + } catch (error) { + throw error.response?.data || { message: 'Failed to place order' }; + } + } + + async cancelOrder(orderId) { + try { + const response = await api.delete(`/orders/${orderId}`); + return response.data; + } catch (error) { + throw error.response?.data || { message: 'Failed to cancel order' }; + } + } + + async getUserOrders(userId) { + try { + const response = await api.get(`/orders/user/${userId}`); + return response.data; + } catch (error) { + throw error.response?.data || { message: 'Failed to fetch orders' }; + } + } + + async getOrderBook(symbol) { + try { + const response = await api.get(`/orderbook/${symbol}`); + return response.data; + } catch (error) { + throw error.response?.data || { message: 'Failed to fetch order book' }; + } + } +} + +export default new OrderService(); diff --git a/services/api-gateway/src/middleware/auth.js b/services/api-gateway/src/middleware/auth.js new file mode 100644 index 0000000..ddaa3ed --- /dev/null +++ b/services/api-gateway/src/middleware/auth.js @@ -0,0 +1,35 @@ +const AuthService = require('../services/AuthService'); + +const authMiddleware = async (req, res, next) => { + try { + const authHeader = req.headers.authorization; + + if (!authHeader || !authHeader.startsWith('Bearer ')) { + return res.status(401).json({ + success: false, + message: 'No token provided' + }); + } + + const token = authHeader.substring(7); + const validation = await AuthService.validateToken(token); + + if (!validation.valid) { + return res.status(401).json({ + success: false, + message: validation.message + }); + } + + req.user = validation.user; + next(); + } catch (error) { + console.error('Auth middleware error:', error); + res.status(500).json({ + success: false, + message: 'Authentication error' + }); + } +}; + +module.exports = authMiddleware; diff --git a/services/api-gateway/src/middleware/errorHandler.js b/services/api-gateway/src/middleware/errorHandler.js new file mode 100644 index 0000000..251b0e0 --- /dev/null +++ b/services/api-gateway/src/middleware/errorHandler.js @@ -0,0 +1,33 @@ +const errorHandler = (err, req, res, next) => { + console.error('Error:', err); + + // Default error + let error = { ...err }; + error.message = err.message; + + // Mongoose bad ObjectId + if (err.name === 'CastError') { + const message = 'Resource not found'; + error = { message, statusCode: 404 }; + } + + // Mongoose duplicate key + if (err.code === 11000) { + const message = 'Duplicate field value entered'; + error = { message, statusCode: 400 }; + } + + // Mongoose validation error + if (err.name === 'ValidationError') { + const message = Object.values(err.errors).map(val => val.message).join(', '); + error = { message, statusCode: 400 }; + } + + res.status(error.statusCode || 500).json({ + success: false, + message: error.message || 'Server Error', + ...(process.env.NODE_ENV === 'development' && { stack: err.stack }) + }); +}; + +module.exports = errorHandler; diff --git a/services/api-gateway/src/server.js b/services/api-gateway/src/server.js new file mode 100644 index 0000000..cd5d26d --- /dev/null +++ b/services/api-gateway/src/server.js @@ -0,0 +1,137 @@ +const express = require('express'); +const http = require('http'); +const WebSocket = require('ws'); +const cors = require('cors'); +const helmet = require('helmet'); +const rateLimit = require('express-rate-limit'); +const { createProxyMiddleware } = require('http-proxy-middleware'); +const path = require('path'); +require('dotenv').config(); + +const AuthService = require('./services/AuthService'); +const WebSocketManager = require('./services/WebSocketManager'); +const authMiddleware = require('./middleware/auth'); +const errorHandler = require('./middleware/errorHandler'); + +const app = express(); +const server = http.createServer(app); + +// Security middleware +app.use(helmet()); +app.use(cors({ + origin: process.env.FRONTEND_URL || 'http://localhost:3000', + credentials: true +})); + +// Rate limiting +const limiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 100, + message: 'Too many requests from this IP' +}); +app.use('/api/', limiter); + +app.use(express.json({ limit: '10mb' })); +app.use(express.urlencoded({ extended: true })); + +// Serve static frontend files +app.use(express.static(path.join(__dirname, '../../../frontend/build'))); + +// Authentication routes +app.post('/api/auth/login', async (req, res) => { + try { + const { email, password } = req.body; + const result = await AuthService.login(email, password); + + if (result.success) { + res.json({ + success: true, + token: result.token, + user: result.user, + expiresIn: '24h' + }); + } else { + res.status(401).json({ + success: false, + message: 'Invalid credentials' + }); + } + } catch (error) { + res.status(500).json({ + success: false, + message: 'Authentication failed' + }); + } +}); + +app.post('/api/auth/register', async (req, res) => { + try { + const { email, password, firstName, lastName } = req.body; + const result = await AuthService.register(email, password, firstName, lastName); + + if (result.success) { + res.json({ + success: true, + message: 'User created successfully', + userId: result.userId + }); + } else { + res.status(400).json({ + success: false, + message: result.message + }); + } + } catch (error) { + res.status(500).json({ + success: false, + message: 'Registration failed' + }); + } +}); + +// Health check +app.get('/api/health', (req, res) => { + res.json({ + status: 'healthy', + timestamp: new Date().toISOString(), + uptime: process.uptime() + }); +}); + +// Protected API routes with microservice proxying +app.use('/api/orders', authMiddleware, createProxyMiddleware({ + target: process.env.ORDERBOOK_SERVICE_URL || 'http://localhost:8082', + changeOrigin: true, + pathRewrite: { '^/api/orders': '/orders' } +})); + +app.use('/api/market-data', authMiddleware, createProxyMiddleware({ + target: process.env.MARKET_DATA_SERVICE_URL || 'http://localhost:8083', + changeOrigin: true, + pathRewrite: { '^/api/market-data': '/market-data' } +})); + +app.use('/api/users', authMiddleware, createProxyMiddleware({ + target: process.env.USER_SERVICE_URL || 'http://localhost:8084', + changeOrigin: true, + pathRewrite: { '^/api/users': '/users' } +})); + +// Serve React app for all other routes +app.get('*', (req, res) => { + res.sendFile(path.join(__dirname, '../../../frontend/build/index.html')); +}); + +// Error handling +app.use(errorHandler); + +// WebSocket server for real-time data +const wss = new WebSocket.Server({ server }); +const wsManager = new WebSocketManager(wss); + +const PORT = process.env.PORT || 8080; +server.listen(PORT, () => { + console.log(`🚀 Trading Platform API Gateway running on port ${PORT}`); + console.log(`📱 Frontend available at: http://localhost:${PORT}`); + console.log(`🔌 WebSocket server ready for real-time data`); +}); diff --git a/services/api-gateway/src/services/AuthService.js b/services/api-gateway/src/services/AuthService.js new file mode 100644 index 0000000..cd2c450 --- /dev/null +++ b/services/api-gateway/src/services/AuthService.js @@ -0,0 +1,133 @@ +const jwt = require('jsonwebtoken'); +const bcrypt = require('bcryptjs'); +const redis = require('redis'); + +class AuthService { + constructor() { + this.redisClient = redis.createClient({ + url: process.env.REDIS_URL || 'redis://localhost:6379' + }); + this.redisClient.connect() + .then(() => console.log("Redis connected!")) + .catch(err => console.error("Redis connection error:", err)); + this.jwtSecret = process.env.JWT_SECRET || 'your-super-secret-key'; + + // Demo users for testing + this.demoUsers = [ + { + id: 1, + email: 'trader@example.com', + username: 'trader123', + password: bcrypt.hashSync('demo123', 10), + firstName: 'John', + lastName: 'Trader', + accountType: 'Premium', + joinDate: '2024-01-15' + }, + { + id: 2, + email: 'admin@example.com', + username: 'admin', + password: bcrypt.hashSync('admin123', 10), + firstName: 'Admin', + lastName: 'User', + accountType: 'Admin', + joinDate: '2023-12-01' + } + ]; + } + + async login(email, password) { + try { + const user = this.demoUsers.find(u => u.email === email || u.username === email); + + if (!user) { + return { success: false, message: 'User not found' }; + } + + const isValidPassword = bcrypt.compareSync(password, user.password); + if (!isValidPassword) { + return { success: false, message: 'Invalid password' }; + } + + const token = jwt.sign( + { + userId: user.id, + email: user.email, + accountType: user.accountType + }, + this.jwtSecret, + { expiresIn: '24h' } + ); + + await this.redisClient.setEx(`session:${user.id}`, 86400, JSON.stringify({ + userId: user.id, + email: user.email, + loginTime: new Date().toISOString() + })); + + const { password: _, ...userWithoutPassword } = user; + + return { + success: true, + token, + user: userWithoutPassword + }; + + } catch (error) { + console.error('Login error:', error); + return { success: false, message: 'Authentication failed' }; + } + } + + async register(email, password, firstName, lastName) { + try { + const existingUser = this.demoUsers.find(u => u.email === email); + if (existingUser) { + return { success: false, message: 'User already exists' }; + } + + const hashedPassword = bcrypt.hashSync(password, 10); + const newUser = { + id: this.demoUsers.length + 1, + email, + username: email.split('@')[0], + password: hashedPassword, + firstName, + lastName, + accountType: 'Standard', + joinDate: new Date().toISOString().split('T')[0] + }; + + this.demoUsers.push(newUser); + + return { + success: true, + userId: newUser.id, + message: 'User registered successfully' + }; + + } catch (error) { + console.error('Registration error:', error); + return { success: false, message: 'Registration failed' }; + } + } + + async validateToken(token) { + try { + const decoded = jwt.verify(token, this.jwtSecret); + const sessionData = await this.redisClient.get(`session:${decoded.userId}`); + + if (!sessionData) { + return { valid: false, message: 'Session expired' }; + } + + return { valid: true, user: decoded }; + + } catch (error) { + return { valid: false, message: 'Invalid token' }; + } + } +} + +module.exports = new AuthService(); diff --git a/services/api-gateway/src/services/WebSocketManager.js b/services/api-gateway/src/services/WebSocketManager.js new file mode 100644 index 0000000..a60ea9f --- /dev/null +++ b/services/api-gateway/src/services/WebSocketManager.js @@ -0,0 +1,188 @@ +const WebSocket = require('ws'); +const redis = require('redis'); + +class WebSocketManager { + constructor(wss) { + this.wss = wss; + this.clients = new Map(); + this.subscriptions = new Map(); + + this.redisSubscriber = redis.createClient({ + url: process.env.REDIS_URL || 'redis://localhost:6379' + }); + this.redisClient = redis.createClient({ url: process.env.REDIS_URL }); + this.redisClient.connect() + .then(() => console.log("Redis connected!")) + .catch(err => console.error("Redis connection error:", err)); + + this.setupWebSocketServer(); + this.setupRedisSubscriptions(); + } + + setupWebSocketServer() { + this.wss.on('connection', (ws, req) => { + const clientId = this.generateClientId(); + this.clients.set(clientId, { + ws, + subscriptions: new Set(), + lastPing: Date.now() + }); + + console.log(`WebSocket client connected: ${clientId}`); + + ws.on('message', (message) => { + this.handleMessage(clientId, message); + }); + + ws.on('close', () => { + this.handleDisconnect(clientId); + }); + + ws.on('pong', () => { + const client = this.clients.get(clientId); + if (client) { + client.lastPing = Date.now(); + } + }); + + // Send a welcome message + this.sendToClient(clientId, { + type: 'connection', + status: 'connected', + clientId + }); + }); + + // Heartbeat mechanism + setInterval(() => { + this.wss.clients.forEach((ws) => { + if (ws.readyState === WebSocket.OPEN) { + ws.ping(); + } + }); + }, 30000); + } + + setupRedisSubscriptions() { + // Subscribe to market data updates + this.redisSubscriber.subscribe('market_data_updates'); + this.redisSubscriber.subscribe('order_updates'); + this.redisSubscriber.subscribe('trade_updates'); + + this.redisSubscriber.on('message', (channel, message) => { + this.broadcastToSubscribers(channel, JSON.parse(message)); + }); + } + + handleMessage(clientId, message) { + try { + const data = JSON.parse(message); + + switch (data.type) { + case 'subscribe': + this.handleSubscription(clientId, data.channel); + break; + case 'unsubscribe': + this.handleUnsubscription(clientId, data.channel); + break; + case 'ping': + this.sendToClient(clientId, { type: 'pong' }); + break; + default: + console.log(`Unknown message type: ${data.type}`); + } + } catch (error) { + console.error('Error handling WebSocket message:', error); + } + } + + handleSubscription(clientId, channel) { + const client = this.clients.get(clientId); + if (client) { + client.subscriptions.add(channel); + + if (!this.subscriptions.has(channel)) { + this.subscriptions.set(channel, new Set()); + } + this.subscriptions.get(channel).add(clientId); + + this.sendToClient(clientId, { + type: 'subscription_confirmed', + channel + }); + + console.log(`Client ${clientId} subscribed to ${channel}`); + } + } + + handleUnsubscription(clientId, channel) { + const client = this.clients.get(clientId); + if (client) { + client.subscriptions.delete(channel); + + if (this.subscriptions.has(channel)) { + this.subscriptions.get(channel).delete(clientId); + + if (this.subscriptions.get(channel).size === 0) { + this.subscriptions.delete(channel); + } + } + + this.sendToClient(clientId, { + type: 'unsubscription_confirmed', + channel + }); + } + } + + handleDisconnect(clientId) { + const client = this.clients.get(clientId); + if (client) { + // Remove from all subscriptions + client.subscriptions.forEach(channel => { + if (this.subscriptions.has(channel)) { + this.subscriptions.get(channel).delete(clientId); + if (this.subscriptions.get(channel).size === 0) { + this.subscriptions.delete(channel); + } + } + }); + + this.clients.delete(clientId); + console.log(`WebSocket client disconnected: ${clientId}`); + } + } + + sendToClient(clientId, data) { + const client = this.clients.get(clientId); + if (client && client.ws.readyState === WebSocket.OPEN) { + client.ws.send(JSON.stringify(data)); + } + } + + broadcastToSubscribers(channel, data) { + if (this.subscriptions.has(channel)) { + const subscribers = this.subscriptions.get(channel); + subscribers.forEach(clientId => { + this.sendToClient(clientId, { + type: 'broadcast', + channel, + data + }); + }); + } + } + + broadcast(data) { + this.clients.forEach((client, clientId) => { + this.sendToClient(clientId, data); + }); + } + + generateClientId() { + return Math.random().toString(36).slice(2, 11); + } + +} + +module.exports = WebSocketManager; diff --git a/services/market-data-service/package.json b/services/market-data-service/package.json new file mode 100644 index 0000000..e402ddf --- /dev/null +++ b/services/market-data-service/package.json @@ -0,0 +1,18 @@ +{ + "name": "market-data-service", + "version": "1.0.0", + "description": "Market data and analytics microservice", + "main": "src/server.js", + "scripts": { + "start": "node src/server.js", + "dev": "nodemon src/server.js" + }, + "dependencies": { + "express": "^4.18.2", + "redis": "^4.6.7", + "dotenv": "^16.3.1" + }, + "devDependencies": { + "nodemon": "^3.0.1" + } +} diff --git a/services/market-data-service/src/controllers/MarketDataController.js b/services/market-data-service/src/controllers/MarketDataController.js new file mode 100644 index 0000000..3fe5b90 --- /dev/null +++ b/services/market-data-service/src/controllers/MarketDataController.js @@ -0,0 +1,120 @@ +class MarketDataController { + constructor(marketDataService) { + this.marketDataService = marketDataService; + } + + async getMarketData(req, res) { + try { + const { symbol } = req.params; + const marketData = await this.marketDataService.getMarketData(symbol); + + res.json({ + success: true, + data: marketData + }); + + } catch (error) { + console.error('Get market data error:', error); + res.status(500).json({ + success: false, + message: error.message + }); + } + } + + async getPriceHistory(req, res) { + try { + const { symbol } = req.params; + const interval = req.query.interval || '1h'; + const limit = parseInt(req.query.limit) || 100; + + const history = await this.marketDataService.getPriceHistory(symbol, interval, limit); + + res.json({ + success: true, + data: { + symbol, + interval, + history + } + }); + + } catch (error) { + console.error('Get price history error:', error); + res.status(500).json({ + success: false, + message: error.message + }); + } + } + + async getMarketStats(req, res) { + try { + const { symbol } = req.params; + const stats = await this.marketDataService.getMarketStats(symbol); + + res.json({ + success: true, + data: { + symbol, + ...stats + } + }); + + } catch (error) { + console.error('Get market stats error:', error); + res.status(500).json({ + success: false, + message: error.message + }); + } + } + + async getSymbols(req, res) { + try { + const symbols = await this.marketDataService.getSymbols(); + + res.json({ + success: true, + data: symbols + }); + + } catch (error) { + console.error('Get symbols error:', error); + res.status(500).json({ + success: false, + message: error.message + }); + } + } + + async getWatchlist(req, res) { + try { + const { userId } = req.params; + const requestingUserId = req.user?.userId; + + if (parseInt(userId) !== requestingUserId && req.user?.accountType !== 'Admin') { + return res.status(403).json({ + success: false, + message: 'Access denied' + }); + } + + const watchlist = await this.marketDataService.getWatchlist(userId); + + res.json({ + success: true, + data: watchlist + }); + + } catch (error) { + console.error('Get watchlist error:', error); + res.status(500).json({ + success: false, + message: error.message + }); + } + } +} + +module.exports = MarketDataController; diff --git a/services/market-data-service/src/server.js b/services/market-data-service/src/server.js new file mode 100644 index 0000000..5ff02b9 --- /dev/null +++ b/services/market-data-service/src/server.js @@ -0,0 +1,38 @@ +const express = require('express'); +const MarketDataController = require('./controllers/MarketDataController'); +const MarketDataService = require('./services/MarketDataService'); +const PriceService = require('./services/PriceService'); + +const app = express(); +app.use(express.json()); + +// Initialize services +const priceService = new PriceService(); +const marketDataService = new MarketDataService(priceService); + +// Initialize controller +const marketDataController = new MarketDataController(marketDataService); + +// Market data routes +app.get('/market-data/:symbol', (req, res) => marketDataController.getMarketData(req, res)); +app.get('/market-data/:symbol/history', (req, res) => marketDataController.getPriceHistory(req, res)); +app.get('/market-data/:symbol/stats', (req, res) => marketDataController.getMarketStats(req, res)); +app.get('/symbols', (req, res) => marketDataController.getSymbols(req, res)); +app.get('/watchlist/:userId', (req, res) => marketDataController.getWatchlist(req, res)); + +// Health check +app.get('/health', (req, res) => { + res.json({ + status: 'healthy', + service: 'market-data-service', + timestamp: new Date().toISOString() + }); +}); + +const PORT = process.env.PORT || 8083; +app.listen(PORT, () => { + console.log(`📈 Market Data Service running on port ${PORT}`); +}); + +// Start price simulation +priceService.startPriceSimulation(); diff --git a/services/market-data-service/src/services/MarketDataService.js b/services/market-data-service/src/services/MarketDataService.js new file mode 100644 index 0000000..9c1a833 --- /dev/null +++ b/services/market-data-service/src/services/MarketDataService.js @@ -0,0 +1,147 @@ +const redis = require('redis'); + +class MarketDataService { + constructor(priceService) { + this.priceService = priceService; + this.redisClient = redis.createClient({ + url: process.env.REDIS_URL || 'redis://localhost:6379' + }); + this.redisClient.connect(); + + this.symbols = [ + 'BTC/USD', 'ETH/USD', 'BNB/USD', 'ADA/USD', 'SOL/USD', + 'AAPL', 'GOOGL', 'MSFT', 'TSLA', 'AMZN' + ]; + + // Initialize market stats + this.initializeMarketStats(); + } + + async initializeMarketStats() { + const initialStats = { + 'BTC/USD': { + high24h: 46100.00, + low24h: 43800.00, + volume24h: 234567.89, + priceChange24h: 1250.75, + priceChangePercent24h: 2.84 + }, + 'ETH/USD': { + high24h: 3280.00, + low24h: 3050.00, + volume24h: 567890.12, + priceChange24h: -125.50, + priceChangePercent24h: -3.83 + }, + 'AAPL': { + high24h: 178.50, + low24h: 172.00, + volume24h: 89234567, + priceChange24h: 2.25, + priceChangePercent24h: 1.29 + } + }; + + for (const [symbol, stats] of Object.entries(initialStats)) { + await this.redisClient.setEx( + `market_stats:${symbol}`, + 3600, + JSON.stringify(stats) + ); + } + } + + async getMarketData(symbol) { + try { + const currentPrice = await this.priceService.getCurrentPrice(symbol); + const stats = await this.getMarketStats(symbol); + + return { + symbol, + price: currentPrice, + ...stats, + timestamp: Date.now() + }; + } catch (error) { + console.error('Get market data error:', error); + throw error; + } + } + + async getMarketStats(symbol) { + try { + const cached = await this.redisClient.get(`market_stats:${symbol}`); + if (cached) { + return JSON.parse(cached); + } + + // Default stats if not found + return { + high24h: 0, + low24h: 0, + volume24h: 0, + priceChange24h: 0, + priceChangePercent24h: 0 + }; + } catch (error) { + console.error('Get market stats error:', error); + throw error; + } + } + + async getPriceHistory(symbol, interval = '1h', limit = 100) { + try { + return await this.priceService.getPriceHistory(symbol, interval, limit); + } catch (error) { + console.error('Get price history error:', error); + throw error; + } + } + + async getSymbols() { + return this.symbols.map(symbol => ({ + symbol, + type: symbol.includes('/') ? 'crypto' : 'stock', + isActive: true + })); + } + + async getWatchlist(userId) { + try { + const watchlist = await this.redisClient.sMembers(`watchlist:${userId}`); + const watchlistData = []; + + for (const symbol of watchlist) { + const marketData = await this.getMarketData(symbol); + watchlistData.push(marketData); + } + + return watchlistData; + } catch (error) { + console.error('Get watchlist error:', error); + return []; + } + } + + async addToWatchlist(userId, symbol) { + try { + await this.redisClient.sAdd(`watchlist:${userId}`, symbol); + return true; + } catch (error) { + console.error('Add to watchlist error:', error); + return false; + } + } + + async removeFromWatchlist(userId, symbol) { + try { + await this.redisClient.sRem(`watchlist:${userId}`, symbol); + return true; + } catch (error) { + console.error('Remove from watchlist error:', error); + return false; + } + } +} + +module.exports = MarketDataService; diff --git a/services/market-data-service/src/services/PriceService.js b/services/market-data-service/src/services/PriceService.js new file mode 100644 index 0000000..6074d7a --- /dev/null +++ b/services/market-data-service/src/services/PriceService.js @@ -0,0 +1,155 @@ +const redis = require('redis'); + +class PriceService { + constructor() { + this.redisClient = redis.createClient({ + url: process.env.REDIS_URL || 'redis://localhost:6379' + }); + this.redisClient.connect(); + + // Initial prices + this.prices = new Map([ + ['BTC/USD', 45250.50], + ['ETH/USD', 3150.25], + ['BNB/USD', 320.75], + ['ADA/USD', 0.65], + ['SOL/USD', 98.50], + ['AAPL', 175.50], + ['GOOGL', 2750.25], + ['MSFT', 415.75], + ['TSLA', 245.30], + ['AMZN', 3420.80] + ]); + + this.priceHistory = new Map(); + this.isSimulating = false; + } + + async getCurrentPrice(symbol) { + try { + // Try cache first + const cached = await this.redisClient.get(`price:${symbol}`); + if (cached) { + return parseFloat(cached); + } + + // Get from memory + const price = this.prices.get(symbol) || 0; + + // Cache for 1 second + await this.redisClient.setEx(`price:${symbol}`, 1, price.toString()); + + return price; + } catch (error) { + console.error('Get current price error:', error); + return this.prices.get(symbol) || 0; + } + } + + async updatePrice(symbol, price) { + try { + this.prices.set(symbol, price); + + // Update cache + await this.redisClient.setEx(`price:${symbol}`, 1, price.toString()); + + // Store in history + await this.storePriceHistory(symbol, price); + + // Broadcast price update + await this.broadcastPriceUpdate(symbol, price); + + } catch (error) { + console.error('Update price error:', error); + } + } + + async storePriceHistory(symbol, price) { + try { + const timestamp = Date.now(); + const historyKey = `price_history:${symbol}`; + + // Store as sorted a set with timestamp as score + await this.redisClient.zAdd(historyKey, { + score: timestamp, + value: JSON.stringify({ price, timestamp }) + }); + + // Keep only the last 1000 entries + await this.redisClient.zRemRangeByRank(historyKey, 0, -1001); + + } catch (error) { + console.error('Store price history error:', error); + } + } + + async getPriceHistory(symbol, interval = '1h', limit = 100) { + try { + const historyKey = `price_history:${symbol}`; + const history = await this.redisClient.zRevRange(historyKey, 0, limit - 1, { + WITHSCORES: true + }); + + const formattedHistory = []; + for (let i = 0; i < history.length; i += 2) { + const data = JSON.parse(history[i]); + formattedHistory.push({ + timestamp: data.timestamp, + price: data.price, + date: new Date(data.timestamp).toISOString() + }); + } + + return formattedHistory; + } catch (error) { + console.error('Get price history error:', error); + return []; + } + } + + async broadcastPriceUpdate(symbol, price) { + try { + await this.redisClient.publish('market_data_updates', JSON.stringify({ + type: 'price_update', + symbol, + price, + timestamp: Date.now() + })); + } catch (error) { + console.error('Broadcast price update error:', error); + } + } + + startPriceSimulation() { + if (this.isSimulating) return; + + this.isSimulating = true; + console.log('Starting price simulation...'); + + setInterval(() => { + this.simulatePriceMovements(); + }, 1000); // Update every second + } + + simulatePriceMovements() { + for (const [symbol, currentPrice] of this.prices.entries()) { + // Generate random price movement (-2% to +2%) + const changePercent = (Math.random() - 0.5) * 0.04; + const newPrice = currentPrice * (1 + changePercent); + + // Round to appropriate decimal places + const roundedPrice = symbol.includes('/') + ? Math.round(newPrice * 100) / 100 // Crypto: 2 decimals + : Math.round(newPrice * 100) / 100; // Stocks: 2 decimals + + this.updatePrice(symbol, roundedPrice); + } + } + + stopPriceSimulation() { + this.isSimulating = false; + console.log('Price simulation stopped'); + } +} + +module.exports = PriceService; diff --git a/services/orderbook-service/package.json b/services/orderbook-service/package.json new file mode 100644 index 0000000..3b24b43 --- /dev/null +++ b/services/orderbook-service/package.json @@ -0,0 +1,19 @@ +{ + "name": "orderbook-service", + "version": "1.0.0", + "description": "C++ OrderBook microservice wrapper", + "main": "src/server.js", + "scripts": { + "start": "node src/server.js", + "dev": "nodemon src/server.js" + }, + "dependencies": { + "express": "^4.18.2", + "redis": "^4.6.7", + "uuid": "^9.0.0", + "dotenv": "^16.3.1" + }, + "devDependencies": { + "nodemon": "^3.0.1" + } +} diff --git a/services/orderbook-service/src/OrderBookWrapper.js b/services/orderbook-service/src/OrderBookWrapper.js new file mode 100644 index 0000000..14dce28 --- /dev/null +++ b/services/orderbook-service/src/OrderBookWrapper.js @@ -0,0 +1,262 @@ +const { spawn } = require('child_process'); +const redis = require('redis'); +const EventEmitter = require('events'); + +class OrderBookWrapper extends EventEmitter { + constructor() { + super(); + this.redisClient = redis.createClient({ + url: process.env.REDIS_URL || 'redis://localhost:6379' + }); + this.redisClient.connect(); + + this.isReady = false; + this.orderIdCounter = 1; + this.activeOrders = new Map(); + + // For demo purposes, we'll simulate the C++ OrderBook + // In production, this would spawn your actual C++ process + this.initializeSimulatedOrderBook(); + + setTimeout(() => { + this.isReady = true; + console.log('OrderBook service is ready'); + }, 2000); + } + + initializeSimulatedOrderBook() { + // Simulated market data for demo + this.orderBook = { + 'BTC/USD': { + bids: [ + { price: 45248.50, quantity: 2.5, orders: [] }, + { price: 45247.25, quantity: 1.8, orders: [] }, + { price: 45246.00, quantity: 3.2, orders: [] } + ], + asks: [ + { price: 45251.00, quantity: 1.7, orders: [] }, + { price: 45252.25, quantity: 2.3, orders: [] }, + { price: 45253.50, quantity: 1.5, orders: [] } + ] + } + }; + } + + async placeOrder(orderData) { + if (!this.isReady) { + throw new Error('OrderBook service not ready'); + } + + try { + const orderId = this.generateOrderId(); + const order = { + ...orderData, + orderId, + status: 'PENDING', + timestamp: new Date().toISOString() + }; + + // Store in active orders + this.activeOrders.set(orderId, order); + + // Cache in Redis + await this.redisClient.hSet( + `orders:active:${orderData.userId}`, + orderId, + JSON.stringify(order) + ); + + // Simulate order book update + this.simulateOrderBookUpdate(order); + + // Emit order update event + this.emit('orderUpdate', { + type: 'ORDER_PLACED', + order + }); + + return { + success: true, + orderId, + status: 'PENDING', + message: 'Order submitted successfully' + }; + + } catch (error) { + console.error('Place order error:', error); + return { + success: false, + message: 'Failed to place order' + }; + } + } + + async cancelOrder(orderId, userId) { + try { + const order = this.activeOrders.get(parseInt(orderId)); + if (!order || order.userId !== userId) { + return { + success: false, + message: 'Order not found or access denied' + }; + } + + // Remove from active orders + this.activeOrders.delete(parseInt(orderId)); + + // Update cache + await this.redisClient.hDel(`orders:active:${userId}`, orderId); + await this.redisClient.hSet( + `orders:cancelled:${userId}`, + orderId, + JSON.stringify({ + ...order, + status: 'CANCELLED', + cancelledAt: new Date().toISOString() + }) + ); + + // Emit order update event + this.emit('orderUpdate', { + type: 'ORDER_CANCELLED', + orderId, + userId + }); + + return { + success: true, + message: 'Order cancelled successfully' + }; + + } catch (error) { + console.error('Cancel order error:', error); + return { + success: false, + message: 'Failed to cancel order' + }; + } + } + + async getOrderBook(symbol = 'BTC/USD') { + try { + // Try to get from Redis cache first + const cached = await this.redisClient.get(`orderbook:${symbol}`); + if (cached) { + return JSON.parse(cached); + } + + // Return simulated data + const orderBookData = this.orderBook[symbol] || { + bids: [], + asks: [] + }; + + // Cache the result + await this.redisClient.setEx( + `orderbook:${symbol}`, + 30, + JSON.stringify({ + symbol, + timestamp: Date.now(), + ...orderBookData + }) + ); + + return { + symbol, + timestamp: Date.now(), + ...orderBookData + }; + + } catch (error) { + console.error('Get orderbook error:', error); + throw error; + } + } + + async getUserOrders(userId) { + try { + const activeOrders = await this.redisClient.hGetAll(`orders:active:${userId}`); + const cancelledOrders = await this.redisClient.hGetAll(`orders:cancelled:${userId}`); + + const orders = []; + + // Parse active orders + Object.values(activeOrders).forEach(orderStr => { + orders.push(JSON.parse(orderStr)); + }); + + // Parse cancelled orders + Object.values(cancelledOrders).forEach(orderStr => { + orders.push(JSON.parse(orderStr)); + }); + + return orders.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp)); + + } catch (error) { + console.error('Get user orders error:', error); + return []; + } + } + + simulateOrderBookUpdate(order) { + const symbol = order.symbol || 'BTC/USD'; + if (!this.orderBook[symbol]) { + this.orderBook[symbol] = { bids: [], asks: [] }; + } + + // Simulate adding order to the appropriate side + const side = order.side === 'BUY' ? 'bids' : 'asks'; + const priceLevel = this.orderBook[symbol][side].find(level => level.price === order.price); + + if (priceLevel) { + priceLevel.quantity += order.quantity; + priceLevel.orders.push(order); + } else { + this.orderBook[symbol][side].push({ + price: order.price, + quantity: order.quantity, + orders: [order] + }); + + // Sort bids descending, asks ascending + if (side === 'bids') { + this.orderBook[symbol][side].sort((a, b) => b.price - a.price); + } else { + this.orderBook[symbol][side].sort((a, b) => a.price - b.price); + } + } + + // Broadcast update + this.broadcastMarketUpdate(symbol); + } + + async broadcastMarketUpdate(symbol) { + const orderBookData = await this.getOrderBook(symbol); + + // Publish to Redis for WebSocket broadcast + this.redisClient.publish('market_data_updates', JSON.stringify({ + type: 'orderbook_update', + symbol, + data: orderBookData + })); + } + + generateOrderId() { + return this.orderIdCounter++; + } + + getBestBid(symbol = 'BTC/USD') { + const orderBook = this.orderBook[symbol]; + if (!orderBook || !orderBook.bids.length) return null; + return orderBook.bids[0].price; + } + + getBestAsk(symbol = 'BTC/USD') { + const orderBook = this.orderBook[symbol]; + if (!orderBook || !orderBook.asks.length) return null; + return orderBook.asks[0].price; + } +} + +module.exports = OrderBookWrapper; diff --git a/services/orderbook-service/src/controllers/MarketDataController.js b/services/orderbook-service/src/controllers/MarketDataController.js new file mode 100644 index 0000000..6837436 --- /dev/null +++ b/services/orderbook-service/src/controllers/MarketDataController.js @@ -0,0 +1,60 @@ +class MarketDataController { + constructor(orderBookWrapper) { + this.orderBookWrapper = orderBookWrapper; + } + + async getOrderBook(req, res) { + try { + const symbol = req.params.symbol || 'BTC/USD'; + const data = await this.orderBookWrapper.getOrderBook(symbol); + + res.json({ + success: true, + data + }); + + } catch (error) { + console.error('Get order book error:', error); + res.status(500).json({ + success: false, + message: error.message + }); + } + } + + async getMarketData(req, res) { + try { + const symbol = req.params.symbol; + const orderBookData = await this.orderBookWrapper.getOrderBook(symbol); + + const marketData = { + symbol, + bestBid: this.orderBookWrapper.getBestBid(symbol), + bestAsk: this.orderBookWrapper.getBestAsk(symbol), + spread: null, + lastPrice: null, + volume24h: 0, + priceChange24h: 0, + timestamp: Date.now() + }; + + if (marketData.bestBid && marketData.bestAsk) { + marketData.spread = marketData.bestAsk - marketData.bestBid; + } + + res.json({ + success: true, + data: marketData + }); + + } catch (error) { + console.error('Get market data error:', error); + res.status(500).json({ + success: false, + message: error.message + }); + } + } +} + +module.exports = MarketDataController; diff --git a/services/orderbook-service/src/controllers/OrderController.js b/services/orderbook-service/src/controllers/OrderController.js new file mode 100644 index 0000000..8e08a8a --- /dev/null +++ b/services/orderbook-service/src/controllers/OrderController.js @@ -0,0 +1,108 @@ +class OrderController { + constructor(orderBookWrapper) { + this.orderBookWrapper = orderBookWrapper; + } + + async placeOrder(req, res) { + try { + const orderData = { + userId: req.user?.userId || req.body.userId || 1, + symbol: req.body.symbol || 'BTC/USD', + side: req.body.side, + quantity: parseFloat(req.body.quantity), + price: parseFloat(req.body.price), + type: req.body.type || 'LIMIT' + }; + + // Validate order data + if (!orderData.side || !['BUY', 'SELL'].includes(orderData.side)) { + return res.status(400).json({ + success: false, + message: 'Invalid order side. Must be BUY or SELL' + }); + } + + if (!orderData.quantity || orderData.quantity <= 0) { + return res.status(400).json({ + success: false, + message: 'Invalid quantity. Must be greater than 0' + }); + } + + if (orderData.type === 'LIMIT' && (!orderData.price || orderData.price <= 0)) { + return res.status(400).json({ + success: false, + message: 'Invalid price for limit order' + }); + } + + const result = await this.orderBookWrapper.placeOrder(orderData); + + if (result.success) { + res.status(201).json(result); + } else { + res.status(400).json(result); + } + + } catch (error) { + console.error('Place order error:', error); + res.status(500).json({ + success: false, + message: error.message + }); + } + } + + async cancelOrder(req, res) { + try { + const { orderId } = req.params; + const userId = req.user?.userId || req.body.userId || 1; + + const result = await this.orderBookWrapper.cancelOrder(orderId, userId); + + if (result.success) { + res.json(result); + } else { + res.status(404).json(result); + } + + } catch (error) { + console.error('Cancel order error:', error); + res.status(500).json({ + success: false, + message: error.message + }); + } + } + + async getUserOrders(req, res) { + try { + const { userId } = req.params; + const requestingUserId = req.user?.userId || userId; + + // Users can only see their own orders (unless admin) + if (parseInt(userId) !== requestingUserId && req.user?.accountType !== 'Admin') { + return res.status(403).json({ + success: false, + message: 'Access denied' + }); + } + + const orders = await this.orderBookWrapper.getUserOrders(parseInt(userId)); + + res.json({ + success: true, + data: orders + }); + + } catch (error) { + console.error('Get user orders error:', error); + res.status(500).json({ + success: false, + message: error.message + }); + } + } +} + +module.exports = OrderController; diff --git a/services/orderbook-service/src/server.js b/services/orderbook-service/src/server.js new file mode 100644 index 0000000..2f9b0d9 --- /dev/null +++ b/services/orderbook-service/src/server.js @@ -0,0 +1,37 @@ +const express = require('express'); +const OrderBookWrapper = require('./OrderBookWrapper'); +const OrderController = require('./controllers/OrderController'); +const MarketDataController = require('./controllers/MarketDataController'); + +const app = express(); +app.use(express.json()); + +// Initialize OrderBook wrapper +const orderBookWrapper = new OrderBookWrapper(); + +// Inject OrderBook into controllers +const orderController = new OrderController(orderBookWrapper); +const marketDataController = new MarketDataController(orderBookWrapper); + +// Order management endpoints +app.post('/orders', (req, res) => orderController.placeOrder(req, res)); +app.delete('/orders/:orderId', (req, res) => orderController.cancelOrder(req, res)); +app.get('/orders/user/:userId', (req, res) => orderController.getUserOrders(req, res)); + +// Market data endpoints +app.get('/orderbook/:symbol?', (req, res) => marketDataController.getOrderBook(req, res)); +app.get('/market-data/:symbol', (req, res) => marketDataController.getMarketData(req, res)); + +// Health check +app.get('/health', (req, res) => { + res.json({ + status: 'healthy', + service: 'orderbook-service', + timestamp: new Date().toISOString() + }); +}); + +const PORT = process.env.PORT || 8082; +app.listen(PORT, () => { + console.log(`🔧 OrderBook Service running on port ${PORT}`); +}); diff --git a/services/user-service/package.json b/services/user-service/package.json new file mode 100644 index 0000000..b487b6e --- /dev/null +++ b/services/user-service/package.json @@ -0,0 +1,18 @@ +{ + "name": "user-service", + "version": "1.0.0", + "description": "User management microservice", + "main": "src/server.js", + "scripts": { + "start": "node src/server.js", + "dev": "nodemon src/server.js" + }, + "dependencies": { + "express": "^4.18.2", + "redis": "^4.6.7", + "dotenv": "^16.3.1" + }, + "devDependencies": { + "nodemon": "^3.0.1" + } +} diff --git a/services/user-service/src/controllers/PortfolioController.js b/services/user-service/src/controllers/PortfolioController.js new file mode 100644 index 0000000..e6537d6 --- /dev/null +++ b/services/user-service/src/controllers/PortfolioController.js @@ -0,0 +1,93 @@ +class PortfolioController { + constructor(portfolioService) { + this.portfolioService = portfolioService; + } + + async getPortfolio(req, res) { + try { + const { userId } = req.params; + const requestingUserId = req.user?.userId; + + if (parseInt(userId) !== requestingUserId && req.user?.accountType !== 'Admin') { + return res.status(403).json({ + success: false, + message: 'Access denied' + }); + } + + const portfolio = await this.portfolioService.getPortfolio(userId); + + res.json({ + success: true, + data: portfolio + }); + + } catch (error) { + console.error('Get portfolio error:', error); + res.status(500).json({ + success: false, + message: error.message + }); + } + } + + async getPositions(req, res) { + try { + const { userId } = req.params; + const requestingUserId = req.user?.userId; + + if (parseInt(userId) !== requestingUserId && req.user?.accountType !== 'Admin') { + return res.status(403).json({ + success: false, + message: 'Access denied' + }); + } + + const positions = await this.portfolioService.getPositions(userId); + + res.json({ + success: true, + data: positions + }); + + } catch (error) { + console.error('Get positions error:', error); + res.status(500).json({ + success: false, + message: error.message + }); + } + } + + async getTradeHistory(req, res) { + try { + const { userId } = req.params; + const requestingUserId = req.user?.userId; + const limit = parseInt(req.query.limit) || 50; + const offset = parseInt(req.query.offset) || 0; + + if (parseInt(userId) !== requestingUserId && req.user?.accountType !== 'Admin') { + return res.status(403).json({ + success: false, + message: 'Access denied' + }); + } + + const tradeHistory = await this.portfolioService.getTradeHistory(userId, limit, offset); + + res.json({ + success: true, + data: tradeHistory + }); + + } catch (error) { + console.error('Get trade history error:', error); + res.status(500).json({ + success: false, + message: error.message + }); + } + } +} + +module.exports = PortfolioController; diff --git a/services/user-service/src/controllers/UserController.js b/services/user-service/src/controllers/UserController.js new file mode 100644 index 0000000..c288353 --- /dev/null +++ b/services/user-service/src/controllers/UserController.js @@ -0,0 +1,111 @@ +class UserController { + constructor(userService) { + this.userService = userService; + } + + async getUser(req, res) { + try { + const { userId } = req.params; + const requestingUserId = req.user?.userId; + + // Users can only access their own data (unless admin) + if (parseInt(userId) !== requestingUserId && req.user?.accountType !== 'Admin') { + return res.status(403).json({ + success: false, + message: 'Access denied' + }); + } + + const user = await this.userService.getUser(userId); + if (!user) { + return res.status(404).json({ + success: false, + message: 'User not found' + }); + } + + res.json({ + success: true, + data: user + }); + + } catch (error) { + console.error('Get user error:', error); + res.status(500).json({ + success: false, + message: error.message + }); + } + } + + async updateUser(req, res) { + try { + const { userId } = req.params; + const requestingUserId = req.user?.userId; + + // Users can only update their own data + if (parseInt(userId) !== requestingUserId && req.user?.accountType !== 'Admin') { + return res.status(403).json({ + success: false, + message: 'Access denied' + }); + } + + const updatedUser = await this.userService.updateUser(userId, req.body); + if (!updatedUser) { + return res.status(404).json({ + success: false, + message: 'User not found' + }); + } + + res.json({ + success: true, + data: updatedUser + }); + + } catch (error) { + console.error('Update user error:', error); + res.status(500).json({ + success: false, + message: error.message + }); + } + } + + async getUserProfile(req, res) { + try { + const { userId } = req.params; + const requestingUserId = req.user?.userId; + + if (parseInt(userId) !== requestingUserId && req.user?.accountType !== 'Admin') { + return res.status(403).json({ + success: false, + message: 'Access denied' + }); + } + + const profile = await this.userService.getUserProfile(userId); + if (!profile) { + return res.status(404).json({ + success: false, + message: 'User not found' + }); + } + + res.json({ + success: true, + data: profile + }); + + } catch (error) { + console.error('Get user profile error:', error); + res.status(500).json({ + success: false, + message: error.message + }); + } + } +} + +module.exports = UserController; diff --git a/services/user-service/src/server.js b/services/user-service/src/server.js new file mode 100644 index 0000000..5d6753c --- /dev/null +++ b/services/user-service/src/server.js @@ -0,0 +1,40 @@ +const express = require('express'); +const UserController = require('./controllers/UserController'); +const PortfolioController = require('./controllers/PortfolioController'); +const UserService = require('./services/UserService'); +const PortfolioService = require('./services/PortfolioService'); + +const app = express(); +app.use(express.json()); + +// Initialize services +const userService = new UserService(); +const portfolioService = new PortfolioService(); + +// Initialize controllers +const userController = new UserController(userService); +const portfolioController = new PortfolioController(portfolioService); + +// User management routes +app.get('/users/:userId', (req, res) => userController.getUser(req, res)); +app.put('/users/:userId', (req, res) => userController.updateUser(req, res)); +app.get('/users/:userId/profile', (req, res) => userController.getUserProfile(req, res)); + +// Portfolio routes +app.get('/users/:userId/portfolio', (req, res) => portfolioController.getPortfolio(req, res)); +app.get('/users/:userId/positions', (req, res) => portfolioController.getPositions(req, res)); +app.get('/users/:userId/trades', (req, res) => portfolioController.getTradeHistory(req, res)); + +// Health check +app.get('/health', (req, res) => { + res.json({ + status: 'healthy', + service: 'user-service', + timestamp: new Date().toISOString() + }); +}); + +const PORT = process.env.PORT || 8084; +app.listen(PORT, () => { + console.log(`👥 User Service running on port ${PORT}`); +}); diff --git a/services/user-service/src/services/PortfolioService.js b/services/user-service/src/services/PortfolioService.js new file mode 100644 index 0000000..1972a66 --- /dev/null +++ b/services/user-service/src/services/PortfolioService.js @@ -0,0 +1,199 @@ +const redis = require('redis'); + +class PortfolioService { + constructor() { + this.redisClient = redis.createClient({ + url: process.env.REDIS_URL || 'redis://localhost:6379' + }); + this.redisClient.connect(); + + // Demo portfolio data + this.portfolios = new Map([ + [1, { + userId: 1, + totalValue: 125000.50, + cashBalance: 25000.00, + dailyPnL: 2500.75, + totalPnL: 25000.50, + positions: [ + { + symbol: 'BTC/USD', + quantity: 2.5, + avgPrice: 45000, + currentPrice: 45250.50, + marketValue: 113126.25, + unrealizedPnL: 626.25, + realizedPnL: 1250.00 + }, + { + symbol: 'ETH/USD', + quantity: 10.0, + avgPrice: 3100, + currentPrice: 3150.25, + marketValue: 31502.50, + unrealizedPnL: 502.50, + realizedPnL: 750.25 + } + ], + lastUpdated: new Date().toISOString() + }] + ]); + + // Demo trade history + this.tradeHistory = new Map([ + [1, [ + { + id: 1, + symbol: 'BTC/USD', + side: 'BUY', + quantity: 1.0, + price: 44000, + value: 44000, + fee: 44.00, + timestamp: '2024-01-15T10:30:00Z' + }, + { + id: 2, + symbol: 'BTC/USD', + side: 'BUY', + quantity: 1.5, + price: 46000, + value: 69000, + fee: 69.00, + timestamp: '2024-01-16T14:20:00Z' + }, + { + id: 3, + symbol: 'ETH/USD', + side: 'BUY', + quantity: 10.0, + price: 3100, + value: 31000, + fee: 31.00, + timestamp: '2024-01-17T09:15:00Z' + } + ]] + ]); + } + + async getPortfolio(userId) { + try { + // Try cache first + const cached = await this.redisClient.get(`portfolio:${userId}`); + if (cached) { + return JSON.parse(cached); + } + + const portfolio = this.portfolios.get(parseInt(userId)); + if (!portfolio) { + return { + userId: parseInt(userId), + totalValue: 0, + cashBalance: 0, + dailyPnL: 0, + totalPnL: 0, + positions: [], + lastUpdated: new Date().toISOString() + }; + } + + // Cache for 5 minutes + await this.redisClient.setEx(`portfolio:${userId}`, 300, JSON.stringify(portfolio)); + + return portfolio; + } catch (error) { + console.error('Get portfolio error:', error); + throw error; + } + } + + async getPositions(userId) { + try { + const portfolio = await this.getPortfolio(userId); + return portfolio.positions || []; + } catch (error) { + console.error('Get positions error:', error); + throw error; + } + } + + async getTradeHistory(userId, limit = 50, offset = 0) { + try { + const trades = this.tradeHistory.get(parseInt(userId)) || []; + + // Sort by timestamp descending + const sortedTrades = trades.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp)); + + // Apply pagination + const paginatedTrades = sortedTrades.slice(offset, offset + limit); + + return { + trades: paginatedTrades, + total: trades.length, + limit, + offset + }; + } catch (error) { + console.error('Get trade history error:', error); + throw error; + } + } + + async updatePosition(userId, symbol, quantity, price) { + try { + const portfolio = await this.getPortfolio(userId); + const existingPosition = portfolio.positions.find(p => p.symbol === symbol); + + if (existingPosition) { + // Update existing position + const totalQuantity = existingPosition.quantity + quantity; + const totalValue = (existingPosition.quantity * existingPosition.avgPrice) + (quantity * price); + + existingPosition.quantity = totalQuantity; + existingPosition.avgPrice = totalValue / totalQuantity; + } else { + // Add new position + portfolio.positions.push({ + symbol, + quantity, + avgPrice: price, + currentPrice: price, + marketValue: quantity * price, + unrealizedPnL: 0, + realizedPnL: 0 + }); + } + + portfolio.lastUpdated = new Date().toISOString(); + this.portfolios.set(parseInt(userId), portfolio); + + // Update cache + await this.redisClient.setEx(`portfolio:${userId}`, 300, JSON.stringify(portfolio)); + + return portfolio; + } catch (error) { + console.error('Update position error:', error); + throw error; + } + } + + async addTrade(userId, trade) { + try { + const trades = this.tradeHistory.get(parseInt(userId)) || []; + trades.push({ + ...trade, + id: trades.length + 1, + timestamp: new Date().toISOString() + }); + + this.tradeHistory.set(parseInt(userId), trades); + + return trade; + } catch (error) { + console.error('Add trade error:', error); + throw error; + } + } +} + +module.exports = PortfolioService; diff --git a/services/user-service/src/services/UserService.js b/services/user-service/src/services/UserService.js new file mode 100644 index 0000000..70555f0 --- /dev/null +++ b/services/user-service/src/services/UserService.js @@ -0,0 +1,144 @@ +const redis = require('redis'); + +class UserService { + constructor() { + this.redisClient = redis.createClient({ + url: process.env.REDIS_URL || 'redis://localhost:6379' + }); + this.redisClient.connect(); + + // Demo users data + this.users = new Map([ + [1, { + id: 1, + email: 'trader@example.com', + username: 'trader123', + firstName: 'John', + lastName: 'Trader', + accountType: 'Premium', + joinDate: '2024-01-15', + isActive: true, + preferences: { + currency: 'USD', + timezone: 'UTC', + notifications: { + email: true, + sms: false, + push: true + } + } + }], + [2, { + id: 2, + email: 'admin@example.com', + username: 'admin', + firstName: 'Admin', + lastName: 'User', + accountType: 'Admin', + joinDate: '2023-12-01', + isActive: true, + preferences: { + currency: 'USD', + timezone: 'UTC', + notifications: { + email: true, + sms: true, + push: true + } + } + }] + ]); + } + + async getUser(userId) { + try { + // Try cache first + const cached = await this.redisClient.get(`user:${userId}`); + if (cached) { + return JSON.parse(cached); + } + + // Get from "database" (demo data) + const user = this.users.get(parseInt(userId)); + if (!user) { + return null; + } + + // Cache for 1 hour + await this.redisClient.setEx(`user:${userId}`, 3600, JSON.stringify(user)); + + return user; + } catch (error) { + console.error('Get user error:', error); + throw error; + } + } + + async updateUser(userId, updateData) { + try { + const user = this.users.get(parseInt(userId)); + if (!user) { + return null; + } + + // Update user data + const updatedUser = { + ...user, + ...updateData, + id: user.id, // Prevent ID changes + updatedAt: new Date().toISOString() + }; + + this.users.set(parseInt(userId), updatedUser); + + // Update cache + await this.redisClient.setEx(`user:${userId}`, 3600, JSON.stringify(updatedUser)); + + return updatedUser; + } catch (error) { + console.error('Update user error:', error); + throw error; + } + } + + async getUserProfile(userId) { + try { + const user = await this.getUser(userId); + if (!user) { + return null; + } + + // Return profile without sensitive data + const { password, ...profile } = user; + return profile; + } catch (error) { + console.error('Get user profile error:', error); + throw error; + } + } + + async updateUserPreferences(userId, preferences) { + try { + const user = await this.getUser(userId); + if (!user) { + return null; + } + + const updatedUser = { + ...user, + preferences: { + ...user.preferences, + ...preferences + }, + updatedAt: new Date().toISOString() + }; + + return await this.updateUser(userId, updatedUser); + } catch (error) { + console.error('Update user preferences error:', error); + throw error; + } + } +} + +module.exports = UserService;