Initial commit: ETF Trade Tracker with multi-user authentication
Features: - Multi-user authentication system with admin panel - SQLite database with user isolation - Trade entry, history, and portfolio tracking - Gains/losses calculation with price updates - Responsive design with sidebar navigation - Session-based authentication with bcrypt password hashing - Admin user management capabilities Default admin credentials: admin/admin123 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
commit
8a68a8bf86
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
node_modules/
|
||||
etf_trades.db
|
||||
*.log
|
||||
.env
|
||||
.DS_Store
|
||||
cookies.txt
|
||||
308
index.html
Normal file
308
index.html
Normal file
@ -0,0 +1,308 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>ETF Trade Tracker</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-container">
|
||||
<nav class="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<h2>ETF Tracker</h2>
|
||||
</div>
|
||||
<ul class="sidebar-menu">
|
||||
<li class="menu-item active" data-page="dashboard">
|
||||
<span class="menu-icon">📊</span>
|
||||
<span class="menu-text">Dashboard</span>
|
||||
</li>
|
||||
<li class="menu-item" data-page="portfolio">
|
||||
<span class="menu-icon">💼</span>
|
||||
<span class="menu-text">Portfolio</span>
|
||||
</li>
|
||||
<li class="menu-item" data-page="trade-history">
|
||||
<span class="menu-icon">📋</span>
|
||||
<span class="menu-text">Trade History</span>
|
||||
</li>
|
||||
<li class="menu-item" data-page="gains-losses">
|
||||
<span class="menu-icon">📈</span>
|
||||
<span class="menu-text">Gains/Losses</span>
|
||||
</li>
|
||||
<li class="menu-item menu-separator" data-page="add-trade">
|
||||
<span class="menu-icon">➕</span>
|
||||
<span class="menu-text">Add Trade</span>
|
||||
</li>
|
||||
<li class="menu-item admin-only" data-page="admin" style="display: none;">
|
||||
<span class="menu-icon">👥</span>
|
||||
<span class="menu-text">Admin Panel</span>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="sidebar-footer">
|
||||
<div class="user-info" id="user-info" style="display: none;">
|
||||
<span id="current-user">Not logged in</span>
|
||||
<button id="logout-btn" class="logout-btn">Logout</button>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="main-content">
|
||||
<header class="top-header">
|
||||
<button class="sidebar-toggle">☰</button>
|
||||
<h1 id="page-title">Dashboard</h1>
|
||||
</header>
|
||||
|
||||
<main class="content-area">
|
||||
<!-- Login Page -->
|
||||
<div id="login-page" class="page active">
|
||||
<div class="login-container">
|
||||
<h2>Login to ETF Tracker</h2>
|
||||
<form id="login-form" class="login-form">
|
||||
<div class="form-group">
|
||||
<label for="login-username">Username</label>
|
||||
<input type="text" id="login-username" required placeholder="Enter your username">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="login-password">Password</label>
|
||||
<input type="password" id="login-password" required placeholder="Enter your password">
|
||||
</div>
|
||||
<button type="submit" class="login-btn">Login</button>
|
||||
</form>
|
||||
<div class="login-info">
|
||||
<p>Default admin credentials:</p>
|
||||
<p><strong>Username:</strong> admin</p>
|
||||
<p><strong>Password:</strong> admin123</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dashboard Page -->
|
||||
<div id="dashboard-page" class="page">
|
||||
<div class="dashboard-grid">
|
||||
<div class="dashboard-card total-value">
|
||||
<h3>Current Portfolio Value</h3>
|
||||
<div class="metric-value" id="dashboard-current-value">€0.00</div>
|
||||
<div class="metric-detail" id="dashboard-last-updated">Using cost basis</div>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-card total-gains">
|
||||
<h3>Total Gains/Losses</h3>
|
||||
<div class="metric-value" id="dashboard-total-gains">€0.00</div>
|
||||
<div class="metric-change" id="dashboard-gains-percentage">0.0%</div>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-card monthly-investment">
|
||||
<h3>Monthly Investment</h3>
|
||||
<div class="metric-value" id="monthly-investment">€0.00</div>
|
||||
<div class="metric-detail" id="monthly-trades">0 trades</div>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-card yearly-investment">
|
||||
<h3>Yearly Investment</h3>
|
||||
<div class="metric-value" id="yearly-investment">€0.00</div>
|
||||
<div class="metric-detail" id="yearly-trades">0 trades</div>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-card total-shares">
|
||||
<h3>Total Shares</h3>
|
||||
<div class="metric-value" id="total-shares">0</div>
|
||||
<div class="metric-detail" id="unique-etfs">0 ETFs</div>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-card cost-basis">
|
||||
<h3>Total Investment</h3>
|
||||
<div class="metric-value" id="dashboard-cost-basis">€0.00</div>
|
||||
<div class="metric-detail" id="dashboard-avg-return">Avg return: 0.0%</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="etf-breakdown">
|
||||
<h3>ETF Breakdown</h3>
|
||||
<div id="etf-breakdown-list" class="breakdown-list">
|
||||
<p class="no-data">No ETF positions yet</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add Trade Page -->
|
||||
<div id="add-trade-page" class="page">
|
||||
<div class="trade-form-container">
|
||||
<h2>Add New Trade</h2>
|
||||
<form id="trade-form" class="trade-form">
|
||||
<div class="form-group">
|
||||
<label for="etf-symbol">ETF Symbol</label>
|
||||
<input type="text" id="etf-symbol" required placeholder="e.g., VWCE, SPY, QQQ">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="trade-type">Trade Type</label>
|
||||
<select id="trade-type" required>
|
||||
<option value="">Select trade type</option>
|
||||
<option value="buy">Buy</option>
|
||||
<option value="sell">Sell</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="shares">Number of Shares</label>
|
||||
<input type="number" id="shares" step="0.001" min="0" required placeholder="0.000">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="price">Price per Share</label>
|
||||
<input type="number" id="price" step="0.01" min="0" required placeholder="0.00">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="currency">Currency</label>
|
||||
<select id="currency" required>
|
||||
<option value="">Select currency</option>
|
||||
<option value="EUR">Euro (€)</option>
|
||||
<option value="USD">US Dollar ($)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="trade-date">Trade Date</label>
|
||||
<input type="date" id="trade-date" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="trade-time">Trade Time (24h format)</label>
|
||||
<input type="time" id="trade-time" required step="1">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="fees">Fees (optional)</label>
|
||||
<input type="number" id="fees" step="0.01" min="0" placeholder="0.00">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="notes">Notes (optional)</label>
|
||||
<textarea id="notes" rows="3" placeholder="Additional notes about this trade..."></textarea>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="submit-btn">Add Trade</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Trade History Page -->
|
||||
<div id="trade-history-page" class="page">
|
||||
<div class="trade-history-container">
|
||||
<h2>Trade History</h2>
|
||||
<div class="trades-controls">
|
||||
<button id="clear-trades" class="clear-btn">Clear All Trades</button>
|
||||
<button id="export-trades" class="export-btn">Export to JSON</button>
|
||||
</div>
|
||||
<div id="trades-list" class="trades-list">
|
||||
<p class="no-trades">No trades recorded yet. Add your first trade above!</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Portfolio Page -->
|
||||
<div id="portfolio-page" class="page">
|
||||
<div class="portfolio-container">
|
||||
<h2>Portfolio Summary</h2>
|
||||
<div id="portfolio-summary" class="portfolio-summary">
|
||||
<p class="no-data">No portfolio data yet</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Admin Panel Page -->
|
||||
<div id="admin-page" class="page">
|
||||
<div class="admin-container">
|
||||
<h2>Admin Panel - User Management</h2>
|
||||
|
||||
<div class="admin-actions">
|
||||
<h3>Create New User</h3>
|
||||
<form id="create-user-form" class="create-user-form">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="new-username">Username</label>
|
||||
<input type="text" id="new-username" required placeholder="Enter username">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="new-password">Password</label>
|
||||
<input type="password" id="new-password" required placeholder="Enter password">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="new-email">Email (optional)</label>
|
||||
<input type="email" id="new-email" placeholder="Enter email">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" id="new-is-admin"> Admin User
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="create-user-btn">Create User</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="users-list-section">
|
||||
<h3>Existing Users</h3>
|
||||
<div id="users-list" class="users-list">
|
||||
<p class="no-data">Loading users...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Gains/Losses Page -->
|
||||
<div id="gains-losses-page" class="page">
|
||||
<div class="gains-losses-container">
|
||||
<h2>Gains & Losses</h2>
|
||||
|
||||
<div class="performance-summary">
|
||||
<div class="performance-grid">
|
||||
<div class="performance-card total-gains">
|
||||
<h3>Total Gains/Losses</h3>
|
||||
<div class="performance-value" id="total-performance">€0.00</div>
|
||||
<div class="performance-percentage" id="total-percentage">0.0%</div>
|
||||
</div>
|
||||
|
||||
<div class="performance-card unrealized-gains">
|
||||
<h3>Unrealized P&L</h3>
|
||||
<div class="performance-value" id="unrealized-performance">€0.00</div>
|
||||
<div class="performance-percentage" id="unrealized-percentage">0.0%</div>
|
||||
</div>
|
||||
|
||||
<div class="performance-card current-value">
|
||||
<h3>Current Portfolio Value</h3>
|
||||
<div class="performance-value" id="current-portfolio-value">€0.00</div>
|
||||
<div class="performance-detail" id="last-updated">Not updated</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="price-updates-section">
|
||||
<h3>Update Current Prices</h3>
|
||||
<div class="price-update-controls">
|
||||
<button id="update-all-prices" class="update-all-btn">Update All Prices</button>
|
||||
<span class="update-info">Enter current market prices to calculate gains/losses</span>
|
||||
</div>
|
||||
<div id="price-update-list" class="price-update-list">
|
||||
<p class="no-data">No ETF positions to update</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gains-breakdown-section">
|
||||
<h3>Individual Performance</h3>
|
||||
<div id="gains-breakdown-list" class="gains-breakdown-list">
|
||||
<p class="no-data">Update prices to see performance breakdown</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
2842
package-lock.json
generated
Normal file
2842
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
23
package.json
Normal file
23
package.json
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "etf-trade-tracker",
|
||||
"version": "1.0.0",
|
||||
"description": "ETF Trade Tracker with SQLite storage",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"dev": "nodemon server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"sqlite3": "^5.1.6",
|
||||
"cors": "^2.8.5",
|
||||
"express-session": "^1.17.3",
|
||||
"bcrypt": "^5.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.0.1"
|
||||
},
|
||||
"keywords": ["etf", "trading", "portfolio", "sqlite"],
|
||||
"author": "",
|
||||
"license": "MIT"
|
||||
}
|
||||
469
server.js
Normal file
469
server.js
Normal file
@ -0,0 +1,469 @@
|
||||
const express = require('express');
|
||||
const sqlite3 = require('sqlite3').verbose();
|
||||
const cors = require('cors');
|
||||
const path = require('path');
|
||||
const session = require('express-session');
|
||||
const bcrypt = require('bcrypt');
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3000;
|
||||
|
||||
app.use(cors({
|
||||
origin: true,
|
||||
credentials: true
|
||||
}));
|
||||
app.use(express.json());
|
||||
app.use(session({
|
||||
secret: 'etf-tracker-secret-key',
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
cookie: {
|
||||
secure: false, // Set to true if using HTTPS
|
||||
maxAge: 24 * 60 * 60 * 1000 // 24 hours
|
||||
}
|
||||
}));
|
||||
app.use(express.static('.'));
|
||||
|
||||
const db = new sqlite3.Database('./etf_trades.db', (err) => {
|
||||
if (err) {
|
||||
console.error('Error opening database:', err.message);
|
||||
} else {
|
||||
console.log('Connected to SQLite database');
|
||||
initializeDatabase();
|
||||
}
|
||||
});
|
||||
|
||||
function initializeDatabase() {
|
||||
// Create users table
|
||||
const createUsersTableSQL = `
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT UNIQUE NOT NULL,
|
||||
password_hash TEXT NOT NULL,
|
||||
email TEXT UNIQUE,
|
||||
is_admin BOOLEAN DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
last_login DATETIME
|
||||
)
|
||||
`;
|
||||
|
||||
// Create trades table with user_id
|
||||
const createTradesTableSQL = `
|
||||
CREATE TABLE IF NOT EXISTS trades (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
etf_symbol TEXT NOT NULL,
|
||||
trade_type TEXT NOT NULL CHECK (trade_type IN ('buy', 'sell')),
|
||||
shares REAL NOT NULL CHECK (shares > 0),
|
||||
price REAL NOT NULL CHECK (price > 0),
|
||||
currency TEXT NOT NULL CHECK (currency IN ('EUR', 'USD')),
|
||||
trade_datetime TEXT NOT NULL,
|
||||
fees REAL DEFAULT 0 CHECK (fees >= 0),
|
||||
notes TEXT,
|
||||
total_value REAL NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users (id)
|
||||
)
|
||||
`;
|
||||
|
||||
db.run(createUsersTableSQL, (err) => {
|
||||
if (err) {
|
||||
console.error('Error creating users table:', err.message);
|
||||
} else {
|
||||
console.log('Users table ready');
|
||||
createDefaultAdmin();
|
||||
}
|
||||
});
|
||||
|
||||
db.run(createTradesTableSQL, (err) => {
|
||||
if (err) {
|
||||
console.error('Error creating trades table:', err.message);
|
||||
} else {
|
||||
console.log('Trades table ready');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function createDefaultAdmin() {
|
||||
const adminUsername = 'admin';
|
||||
const adminPassword = 'admin123';
|
||||
|
||||
db.get('SELECT id FROM users WHERE username = ?', [adminUsername], async (err, row) => {
|
||||
if (err) {
|
||||
console.error('Error checking for admin user:', err.message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!row) {
|
||||
try {
|
||||
const passwordHash = await bcrypt.hash(adminPassword, 10);
|
||||
db.run(
|
||||
'INSERT INTO users (username, password_hash, is_admin) VALUES (?, ?, ?)',
|
||||
[adminUsername, passwordHash, 1],
|
||||
function(err) {
|
||||
if (err) {
|
||||
console.error('Error creating admin user:', err.message);
|
||||
} else {
|
||||
console.log('Default admin user created (username: admin, password: admin123)');
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error hashing admin password:', error);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Middleware to check if user is authenticated
|
||||
function requireAuth(req, res, next) {
|
||||
if (req.session && req.session.userId) {
|
||||
next();
|
||||
} else {
|
||||
res.status(401).json({ error: 'Authentication required' });
|
||||
}
|
||||
}
|
||||
|
||||
// Middleware to check if user is admin
|
||||
function requireAdmin(req, res, next) {
|
||||
if (req.session && req.session.userId && req.session.isAdmin) {
|
||||
next();
|
||||
} else {
|
||||
res.status(403).json({ error: 'Admin privileges required' });
|
||||
}
|
||||
}
|
||||
|
||||
// Authentication endpoints
|
||||
app.post('/api/login', async (req, res) => {
|
||||
const { username, password } = req.body;
|
||||
|
||||
if (!username || !password) {
|
||||
return res.status(400).json({ error: 'Username and password required' });
|
||||
}
|
||||
|
||||
db.get('SELECT * FROM users WHERE username = ?', [username], async (err, user) => {
|
||||
if (err) {
|
||||
console.error('Login error:', err.message);
|
||||
return res.status(500).json({ error: 'Server error' });
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return res.status(401).json({ error: 'Invalid credentials' });
|
||||
}
|
||||
|
||||
try {
|
||||
const isValid = await bcrypt.compare(password, user.password_hash);
|
||||
if (!isValid) {
|
||||
return res.status(401).json({ error: 'Invalid credentials' });
|
||||
}
|
||||
|
||||
// Update last login
|
||||
db.run('UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE id = ?', [user.id]);
|
||||
|
||||
// Set session
|
||||
req.session.userId = user.id;
|
||||
req.session.username = user.username;
|
||||
req.session.isAdmin = user.is_admin;
|
||||
|
||||
res.json({
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
isAdmin: user.is_admin
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Password comparison error:', error);
|
||||
res.status(500).json({ error: 'Server error' });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
app.post('/api/logout', (req, res) => {
|
||||
req.session.destroy((err) => {
|
||||
if (err) {
|
||||
return res.status(500).json({ error: 'Could not log out' });
|
||||
}
|
||||
res.json({ message: 'Logged out successfully' });
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/api/me', requireAuth, (req, res) => {
|
||||
res.json({
|
||||
id: req.session.userId,
|
||||
username: req.session.username,
|
||||
isAdmin: req.session.isAdmin
|
||||
});
|
||||
});
|
||||
|
||||
// User management endpoints (admin only)
|
||||
app.get('/api/admin/users', requireAdmin, (req, res) => {
|
||||
const sql = 'SELECT id, username, email, is_admin, created_at, last_login FROM users ORDER BY created_at DESC';
|
||||
|
||||
db.all(sql, [], (err, rows) => {
|
||||
if (err) {
|
||||
console.error('Error fetching users:', err.message);
|
||||
res.status(500).json({ error: 'Failed to fetch users' });
|
||||
return;
|
||||
}
|
||||
|
||||
res.json(rows);
|
||||
});
|
||||
});
|
||||
|
||||
app.post('/api/admin/users', requireAdmin, async (req, res) => {
|
||||
const { username, password, email, isAdmin } = req.body;
|
||||
|
||||
if (!username || !password) {
|
||||
return res.status(400).json({ error: 'Username and password required' });
|
||||
}
|
||||
|
||||
try {
|
||||
const passwordHash = await bcrypt.hash(password, 10);
|
||||
const sql = 'INSERT INTO users (username, password_hash, email, is_admin) VALUES (?, ?, ?, ?)';
|
||||
|
||||
db.run(sql, [username, passwordHash, email || null, isAdmin ? 1 : 0], function(err) {
|
||||
if (err) {
|
||||
if (err.message.includes('UNIQUE constraint failed')) {
|
||||
res.status(400).json({ error: 'Username or email already exists' });
|
||||
} else {
|
||||
console.error('Error creating user:', err.message);
|
||||
res.status(500).json({ error: 'Failed to create user' });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(201).json({
|
||||
id: this.lastID,
|
||||
username,
|
||||
email,
|
||||
isAdmin: isAdmin || false
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Password hashing error:', error);
|
||||
res.status(500).json({ error: 'Server error' });
|
||||
}
|
||||
});
|
||||
|
||||
app.delete('/api/admin/users/:id', requireAdmin, (req, res) => {
|
||||
const userId = req.params.id;
|
||||
|
||||
if (!userId || isNaN(userId)) {
|
||||
return res.status(400).json({ error: 'Invalid user ID' });
|
||||
}
|
||||
|
||||
// Prevent deleting yourself
|
||||
if (parseInt(userId) === req.session.userId) {
|
||||
return res.status(400).json({ error: 'Cannot delete your own account' });
|
||||
}
|
||||
|
||||
db.run('DELETE FROM users WHERE id = ?', [userId], function(err) {
|
||||
if (err) {
|
||||
console.error('Error deleting user:', err.message);
|
||||
res.status(500).json({ error: 'Failed to delete user' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.changes === 0) {
|
||||
res.status(404).json({ error: 'User not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({ message: 'User deleted successfully' });
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/api/trades', requireAuth, (req, res) => {
|
||||
const sql = 'SELECT * FROM trades WHERE user_id = ? ORDER BY trade_datetime DESC, created_at DESC';
|
||||
|
||||
db.all(sql, [req.session.userId], (err, rows) => {
|
||||
if (err) {
|
||||
console.error('Error fetching trades:', err.message);
|
||||
res.status(500).json({ error: 'Failed to fetch trades' });
|
||||
return;
|
||||
}
|
||||
|
||||
const trades = rows.map(row => ({
|
||||
id: row.id.toString(),
|
||||
etfSymbol: row.etf_symbol,
|
||||
tradeType: row.trade_type,
|
||||
shares: row.shares,
|
||||
price: row.price,
|
||||
currency: row.currency,
|
||||
dateTime: row.trade_datetime,
|
||||
fees: row.fees,
|
||||
notes: row.notes,
|
||||
totalValue: row.total_value,
|
||||
timestamp: new Date(row.created_at).getTime()
|
||||
}));
|
||||
|
||||
res.json(trades);
|
||||
});
|
||||
});
|
||||
|
||||
app.post('/api/trades', requireAuth, (req, res) => {
|
||||
const {
|
||||
etfSymbol,
|
||||
tradeType,
|
||||
shares,
|
||||
price,
|
||||
currency,
|
||||
dateTime,
|
||||
fees = 0,
|
||||
notes = ''
|
||||
} = req.body;
|
||||
|
||||
if (!etfSymbol || !tradeType || !shares || !price || !currency || !dateTime) {
|
||||
return res.status(400).json({ error: 'Missing required fields' });
|
||||
}
|
||||
|
||||
if (!['buy', 'sell'].includes(tradeType)) {
|
||||
return res.status(400).json({ error: 'Invalid trade type' });
|
||||
}
|
||||
|
||||
if (!['EUR', 'USD'].includes(currency)) {
|
||||
return res.status(400).json({ error: 'Invalid currency' });
|
||||
}
|
||||
|
||||
if (shares <= 0 || price <= 0 || fees < 0) {
|
||||
return res.status(400).json({ error: 'Invalid numeric values' });
|
||||
}
|
||||
|
||||
const totalValue = (shares * price) + fees;
|
||||
|
||||
const sql = `
|
||||
INSERT INTO trades (user_id, etf_symbol, trade_type, shares, price, currency, trade_datetime, fees, notes, total_value)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`;
|
||||
|
||||
const params = [
|
||||
req.session.userId,
|
||||
etfSymbol.toUpperCase(),
|
||||
tradeType,
|
||||
shares,
|
||||
price,
|
||||
currency,
|
||||
dateTime,
|
||||
fees,
|
||||
notes,
|
||||
totalValue
|
||||
];
|
||||
|
||||
db.run(sql, params, function(err) {
|
||||
if (err) {
|
||||
console.error('Error inserting trade:', err.message);
|
||||
res.status(500).json({ error: 'Failed to add trade' });
|
||||
return;
|
||||
}
|
||||
|
||||
const newTrade = {
|
||||
id: this.lastID.toString(),
|
||||
etfSymbol: etfSymbol.toUpperCase(),
|
||||
tradeType,
|
||||
shares,
|
||||
price,
|
||||
currency,
|
||||
dateTime,
|
||||
fees,
|
||||
notes,
|
||||
totalValue,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
res.status(201).json(newTrade);
|
||||
});
|
||||
});
|
||||
|
||||
app.delete('/api/trades/:id', requireAuth, (req, res) => {
|
||||
const tradeId = req.params.id;
|
||||
|
||||
if (!tradeId || isNaN(tradeId)) {
|
||||
return res.status(400).json({ error: 'Invalid trade ID' });
|
||||
}
|
||||
|
||||
const sql = 'DELETE FROM trades WHERE id = ? AND user_id = ?';
|
||||
|
||||
db.run(sql, [tradeId, req.session.userId], function(err) {
|
||||
if (err) {
|
||||
console.error('Error deleting trade:', err.message);
|
||||
res.status(500).json({ error: 'Failed to delete trade' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.changes === 0) {
|
||||
res.status(404).json({ error: 'Trade not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({ message: 'Trade deleted successfully' });
|
||||
});
|
||||
});
|
||||
|
||||
app.delete('/api/trades', requireAuth, (req, res) => {
|
||||
const sql = 'DELETE FROM trades WHERE user_id = ?';
|
||||
|
||||
db.run(sql, [req.session.userId], function(err) {
|
||||
if (err) {
|
||||
console.error('Error clearing trades:', err.message);
|
||||
res.status(500).json({ error: 'Failed to clear trades' });
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({
|
||||
message: 'All trades cleared successfully',
|
||||
deletedCount: this.changes
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/api/portfolio-summary', requireAuth, (req, res) => {
|
||||
const sql = `
|
||||
SELECT
|
||||
etf_symbol,
|
||||
SUM(CASE WHEN trade_type = 'buy' THEN shares ELSE -shares END) as total_shares,
|
||||
SUM(CASE WHEN trade_type = 'buy' THEN total_value ELSE -total_value END) as total_invested,
|
||||
COUNT(*) as trade_count
|
||||
FROM trades
|
||||
WHERE user_id = ?
|
||||
GROUP BY etf_symbol
|
||||
HAVING total_shares > 0
|
||||
ORDER BY total_invested DESC
|
||||
`;
|
||||
|
||||
db.all(sql, [req.session.userId], (err, rows) => {
|
||||
if (err) {
|
||||
console.error('Error fetching portfolio summary:', err.message);
|
||||
res.status(500).json({ error: 'Failed to fetch portfolio summary' });
|
||||
return;
|
||||
}
|
||||
|
||||
const summary = rows.map(row => ({
|
||||
symbol: row.etf_symbol,
|
||||
totalShares: row.total_shares,
|
||||
totalInvested: row.total_invested,
|
||||
avgPrice: row.total_shares > 0 ? row.total_invested / row.total_shares : 0,
|
||||
trades: row.trade_count
|
||||
}));
|
||||
|
||||
res.json(summary);
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/', (req, res) => {
|
||||
res.sendFile(path.join(__dirname, 'index.html'));
|
||||
});
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
console.log('\nShutting down server...');
|
||||
db.close((err) => {
|
||||
if (err) {
|
||||
console.error('Error closing database:', err.message);
|
||||
} else {
|
||||
console.log('Database connection closed.');
|
||||
}
|
||||
process.exit(0);
|
||||
});
|
||||
});
|
||||
|
||||
app.listen(PORT, '0.0.0.0', () => {
|
||||
console.log(`Server running on http://0.0.0.0:${PORT}`);
|
||||
});
|
||||
1312
styles.css
Normal file
1312
styles.css
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user