feat(backend): Phase 2 - Redis client, P&L queries, type definitions
- Create shared/redis/index.ts: RedisManager with InMemoryFallback - Update admin.routes.ts: Real P&L queries + Redis health check - Create financial.types.ts: 12 enums + 3 interfaces for financial schema - Create llm.types.ts: 7 enums + 5 interfaces for LLM schema - Create audit.types.ts: 5 enums + 3 interfaces for audit schema - Create market-data.types.ts: 2 enums + 3 interfaces for market_data schema - Update shared/types/index.ts: barrel exports for new types - Add ioredis v5.9.2 dependency - Fix config/index.ts: correct DB credentials Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
86e6303847
commit
d07427aa63
@ -30,7 +30,7 @@ DB_HOST=localhost
|
|||||||
DB_PORT=5432
|
DB_PORT=5432
|
||||||
DB_NAME=trading_platform
|
DB_NAME=trading_platform
|
||||||
DB_USER=trading_user
|
DB_USER=trading_user
|
||||||
DB_PASSWORD=trading_dev_2025
|
DB_PASSWORD=trading_dev_2026
|
||||||
DB_SSL=false
|
DB_SSL=false
|
||||||
DB_POOL_MAX=20
|
DB_POOL_MAX=20
|
||||||
DB_IDLE_TIMEOUT=30000
|
DB_IDLE_TIMEOUT=30000
|
||||||
|
|||||||
93
package-lock.json
generated
93
package-lock.json
generated
@ -24,6 +24,7 @@
|
|||||||
"firebase-admin": "^13.6.0",
|
"firebase-admin": "^13.6.0",
|
||||||
"google-auth-library": "^9.4.1",
|
"google-auth-library": "^9.4.1",
|
||||||
"helmet": "^8.1.0",
|
"helmet": "^8.1.0",
|
||||||
|
"ioredis": "^5.9.2",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"morgan": "^1.10.0",
|
"morgan": "^1.10.0",
|
||||||
"nodemailer": "^7.0.11",
|
"nodemailer": "^7.0.11",
|
||||||
@ -54,6 +55,7 @@
|
|||||||
"@types/compression": "^1.7.5",
|
"@types/compression": "^1.7.5",
|
||||||
"@types/cors": "^2.8.17",
|
"@types/cors": "^2.8.17",
|
||||||
"@types/express": "^5.0.0",
|
"@types/express": "^5.0.0",
|
||||||
|
"@types/ioredis": "^4.28.10",
|
||||||
"@types/jest": "^30.0.0",
|
"@types/jest": "^30.0.0",
|
||||||
"@types/jsonwebtoken": "^9.0.5",
|
"@types/jsonwebtoken": "^9.0.5",
|
||||||
"@types/morgan": "^1.9.9",
|
"@types/morgan": "^1.9.9",
|
||||||
@ -2425,6 +2427,12 @@
|
|||||||
"url": "https://github.com/sponsors/nzakas"
|
"url": "https://github.com/sponsors/nzakas"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@ioredis/commands": {
|
||||||
|
"version": "1.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.5.0.tgz",
|
||||||
|
"integrity": "sha512-eUgLqrMf8nJkZxT24JvVRrQya1vZkQh8BBeYNwGDqa5I0VUi8ACx7uFvAaLxintokpTenkK6DASvo/bvNbBGow==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@isaacs/cliui": {
|
"node_modules/@isaacs/cliui": {
|
||||||
"version": "8.0.2",
|
"version": "8.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
||||||
@ -4023,6 +4031,16 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/ioredis": {
|
||||||
|
"version": "4.28.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/ioredis/-/ioredis-4.28.10.tgz",
|
||||||
|
"integrity": "sha512-69LyhUgrXdgcNDv7ogs1qXZomnfOEnSmrmMFqKgt1XMJxmoOSG/u3wYy13yACIfKuMJ8IhKgHafDO3sx19zVQQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/istanbul-lib-coverage": {
|
"node_modules/@types/istanbul-lib-coverage": {
|
||||||
"version": "2.0.6",
|
"version": "2.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz",
|
||||||
@ -6033,6 +6051,15 @@
|
|||||||
"node": ">=0.8"
|
"node": ">=0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/cluster-key-slot": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/co": {
|
"node_modules/co": {
|
||||||
"version": "4.6.0",
|
"version": "4.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
|
||||||
@ -6489,6 +6516,15 @@
|
|||||||
"node": ">=0.4.0"
|
"node": ">=0.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/denque": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/depd": {
|
"node_modules/depd": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||||
@ -8311,6 +8347,30 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ioredis": {
|
||||||
|
"version": "5.9.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.9.2.tgz",
|
||||||
|
"integrity": "sha512-tAAg/72/VxOUW7RQSX1pIxJVucYKcjFjfvj60L57jrZpYCHC3XN0WCQ3sNYL4Gmvv+7GPvTAjc+KSdeNuE8oWQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@ioredis/commands": "1.5.0",
|
||||||
|
"cluster-key-slot": "^1.1.0",
|
||||||
|
"debug": "^4.3.4",
|
||||||
|
"denque": "^2.1.0",
|
||||||
|
"lodash.defaults": "^4.2.0",
|
||||||
|
"lodash.isarguments": "^3.1.0",
|
||||||
|
"redis-errors": "^1.2.0",
|
||||||
|
"redis-parser": "^3.0.0",
|
||||||
|
"standard-as-callback": "^2.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.22.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/ioredis"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ipaddr.js": {
|
"node_modules/ipaddr.js": {
|
||||||
"version": "1.9.1",
|
"version": "1.9.1",
|
||||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||||
@ -9752,6 +9812,12 @@
|
|||||||
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
|
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/lodash.isarguments": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/lodash.isboolean": {
|
"node_modules/lodash.isboolean": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
|
||||||
@ -11389,6 +11455,27 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/redis-errors": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/redis-parser": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"redis-errors": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/regexp.prototype.flags": {
|
"node_modules/regexp.prototype.flags": {
|
||||||
"version": "1.5.4",
|
"version": "1.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz",
|
||||||
@ -11954,6 +12041,12 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/standard-as-callback": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/statuses": {
|
"node_modules/statuses": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
||||||
|
|||||||
@ -32,6 +32,7 @@
|
|||||||
"firebase-admin": "^13.6.0",
|
"firebase-admin": "^13.6.0",
|
||||||
"google-auth-library": "^9.4.1",
|
"google-auth-library": "^9.4.1",
|
||||||
"helmet": "^8.1.0",
|
"helmet": "^8.1.0",
|
||||||
|
"ioredis": "^5.9.2",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"morgan": "^1.10.0",
|
"morgan": "^1.10.0",
|
||||||
"nodemailer": "^7.0.11",
|
"nodemailer": "^7.0.11",
|
||||||
@ -62,6 +63,7 @@
|
|||||||
"@types/compression": "^1.7.5",
|
"@types/compression": "^1.7.5",
|
||||||
"@types/cors": "^2.8.17",
|
"@types/cors": "^2.8.17",
|
||||||
"@types/express": "^5.0.0",
|
"@types/express": "^5.0.0",
|
||||||
|
"@types/ioredis": "^4.28.10",
|
||||||
"@types/jest": "^30.0.0",
|
"@types/jest": "^30.0.0",
|
||||||
"@types/jsonwebtoken": "^9.0.5",
|
"@types/jsonwebtoken": "^9.0.5",
|
||||||
"@types/morgan": "^1.9.9",
|
"@types/morgan": "^1.9.9",
|
||||||
|
|||||||
@ -11,13 +11,13 @@ export const config = {
|
|||||||
name: 'Trading Platform',
|
name: 'Trading Platform',
|
||||||
version: '0.1.0',
|
version: '0.1.0',
|
||||||
env: process.env.NODE_ENV || 'development',
|
env: process.env.NODE_ENV || 'development',
|
||||||
port: parseInt(process.env.PORT || '3000', 10),
|
port: parseInt(process.env.PORT || '3081', 10),
|
||||||
frontendUrl: process.env.FRONTEND_URL || 'http://localhost:5173',
|
frontendUrl: process.env.FRONTEND_URL || 'http://localhost:3080',
|
||||||
apiUrl: process.env.API_URL || 'http://localhost:3000',
|
apiUrl: process.env.API_URL || 'http://localhost:3081',
|
||||||
},
|
},
|
||||||
|
|
||||||
cors: {
|
cors: {
|
||||||
origins: process.env.CORS_ORIGINS?.split(',') || ['http://localhost:5173'],
|
origins: process.env.CORS_ORIGINS?.split(',') || ['http://localhost:3080'],
|
||||||
},
|
},
|
||||||
|
|
||||||
jwt: {
|
jwt: {
|
||||||
@ -30,9 +30,9 @@ export const config = {
|
|||||||
database: {
|
database: {
|
||||||
host: process.env.DB_HOST || 'localhost',
|
host: process.env.DB_HOST || 'localhost',
|
||||||
port: parseInt(process.env.DB_PORT || '5432', 10),
|
port: parseInt(process.env.DB_PORT || '5432', 10),
|
||||||
name: process.env.DB_NAME || 'trading',
|
name: process.env.DB_NAME || 'trading_platform',
|
||||||
user: process.env.DB_USER || 'postgres',
|
user: process.env.DB_USER || 'trading_user',
|
||||||
password: process.env.DB_PASSWORD || 'postgres',
|
password: process.env.DB_PASSWORD || 'trading_dev_2026',
|
||||||
ssl: process.env.DB_SSL === 'true',
|
ssl: process.env.DB_SSL === 'true',
|
||||||
poolMax: parseInt(process.env.DB_POOL_MAX || '20', 10),
|
poolMax: parseInt(process.env.DB_POOL_MAX || '20', 10),
|
||||||
idleTimeout: parseInt(process.env.DB_IDLE_TIMEOUT || '30000', 10),
|
idleTimeout: parseInt(process.env.DB_IDLE_TIMEOUT || '30000', 10),
|
||||||
@ -44,7 +44,7 @@ export const config = {
|
|||||||
host: process.env.REDIS_HOST || 'localhost',
|
host: process.env.REDIS_HOST || 'localhost',
|
||||||
port: parseInt(process.env.REDIS_PORT || '6379', 10),
|
port: parseInt(process.env.REDIS_PORT || '6379', 10),
|
||||||
password: process.env.REDIS_PASSWORD,
|
password: process.env.REDIS_PASSWORD,
|
||||||
db: parseInt(process.env.REDIS_DB || '0', 10),
|
db: parseInt(process.env.REDIS_DB || '1', 10),
|
||||||
},
|
},
|
||||||
|
|
||||||
stripe: {
|
stripe: {
|
||||||
|
|||||||
@ -1,10 +1,13 @@
|
|||||||
/**
|
/**
|
||||||
* Admin Routes
|
* Admin Routes
|
||||||
* Admin-only endpoints for dashboard, user management, system health, and audit logs
|
* Admin-only endpoints for dashboard, user management, system health, and audit logs
|
||||||
|
* Wired to real database queries (trading_platform)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Router, Request, Response, NextFunction } from 'express';
|
import { Router, Request, Response, NextFunction } from 'express';
|
||||||
import { mlEngineClient, tradingAgentsClient } from '../../shared/clients/index.js';
|
import { mlEngineClient, tradingAgentsClient } from '../../shared/clients/index.js';
|
||||||
|
import { db } from '../../shared/database/index.js';
|
||||||
|
import { redis } from '../../shared/redis/index.js';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
@ -14,66 +17,105 @@ const router = Router();
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /api/v1/admin/dashboard
|
* GET /api/v1/admin/dashboard
|
||||||
* Get dashboard statistics
|
* Get dashboard statistics from real database
|
||||||
*/
|
*/
|
||||||
router.get('/dashboard', async (req: Request, res: Response, next: NextFunction) => {
|
router.get('/dashboard', async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
// Mock stats for development - replace with actual DB queries in production
|
// Run all stats queries in parallel
|
||||||
const stats = {
|
const [
|
||||||
|
usersResult,
|
||||||
|
activeUsersResult,
|
||||||
|
newUsersWeekResult,
|
||||||
|
newUsersMonthResult,
|
||||||
|
totalTradesResult,
|
||||||
|
tradesTodayResult,
|
||||||
|
winningTradesResult,
|
||||||
|
totalModelsResult,
|
||||||
|
activeModelsResult,
|
||||||
|
predictionsTodayResult,
|
||||||
|
overallAccuracyResult,
|
||||||
|
signalsTodayResult,
|
||||||
|
pnlTodayResult,
|
||||||
|
pnlWeekResult,
|
||||||
|
pnlMonthResult,
|
||||||
|
avgPnlResult,
|
||||||
|
] = await Promise.all([
|
||||||
|
db.query<{ count: string }>('SELECT COUNT(*) as count FROM auth.users'),
|
||||||
|
db.query<{ count: string }>("SELECT COUNT(*) as count FROM auth.users WHERE status = 'active'"),
|
||||||
|
db.query<{ count: string }>("SELECT COUNT(*) as count FROM auth.users WHERE created_at >= NOW() - INTERVAL '7 days'"),
|
||||||
|
db.query<{ count: string }>("SELECT COUNT(*) as count FROM auth.users WHERE created_at >= NOW() - INTERVAL '30 days'"),
|
||||||
|
db.query<{ count: string }>('SELECT COUNT(*) as count FROM trading.trades'),
|
||||||
|
db.query<{ count: string }>("SELECT COUNT(*) as count FROM trading.trades WHERE DATE(executed_at) = CURRENT_DATE"),
|
||||||
|
db.query<{ count: string }>("SELECT COUNT(*) as count FROM trading.signals WHERE actual_outcome = 'hit_target'"),
|
||||||
|
db.query<{ count: string }>('SELECT COUNT(*) as count FROM ml.models'),
|
||||||
|
db.query<{ count: string }>("SELECT COUNT(*) as count FROM ml.models WHERE status = 'production'"),
|
||||||
|
db.query<{ count: string }>("SELECT COUNT(*) as count FROM ml.predictions WHERE DATE(created_at) = CURRENT_DATE"),
|
||||||
|
db.query<{ avg: string | null }>("SELECT AVG(overall_accuracy) as avg FROM ml.models WHERE status = 'production'"),
|
||||||
|
db.query<{ count: string }>("SELECT COUNT(*) as count FROM trading.signals WHERE DATE(created_at) = CURRENT_DATE"),
|
||||||
|
// P&L from closed positions
|
||||||
|
db.query<{ total: string | null }>(
|
||||||
|
"SELECT COALESCE(SUM(realized_pnl), 0) as total FROM trading.positions WHERE status = 'closed' AND DATE(closed_at) = CURRENT_DATE"
|
||||||
|
),
|
||||||
|
db.query<{ total: string | null }>(
|
||||||
|
"SELECT COALESCE(SUM(realized_pnl), 0) as total FROM trading.positions WHERE status = 'closed' AND closed_at >= NOW() - INTERVAL '7 days'"
|
||||||
|
),
|
||||||
|
db.query<{ total: string | null }>(
|
||||||
|
"SELECT COALESCE(SUM(realized_pnl), 0) as total FROM trading.positions WHERE status = 'closed' AND closed_at >= NOW() - INTERVAL '30 days'"
|
||||||
|
),
|
||||||
|
db.query<{ avg: string | null }>(
|
||||||
|
"SELECT AVG(realized_pnl) as avg FROM trading.positions WHERE status = 'closed' AND realized_pnl != 0"
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const totalUsers = parseInt(usersResult.rows[0]?.count || '0', 10);
|
||||||
|
const activeUsers = parseInt(activeUsersResult.rows[0]?.count || '0', 10);
|
||||||
|
const newUsersWeek = parseInt(newUsersWeekResult.rows[0]?.count || '0', 10);
|
||||||
|
const newUsersMonth = parseInt(newUsersMonthResult.rows[0]?.count || '0', 10);
|
||||||
|
const totalTrades = parseInt(totalTradesResult.rows[0]?.count || '0', 10);
|
||||||
|
const tradesToday = parseInt(tradesTodayResult.rows[0]?.count || '0', 10);
|
||||||
|
const winningTrades = parseInt(winningTradesResult.rows[0]?.count || '0', 10);
|
||||||
|
const totalModels = parseInt(totalModelsResult.rows[0]?.count || '0', 10);
|
||||||
|
const activeModels = parseInt(activeModelsResult.rows[0]?.count || '0', 10);
|
||||||
|
const predictionsToday = parseInt(predictionsTodayResult.rows[0]?.count || '0', 10);
|
||||||
|
const overallAccuracy = parseFloat(overallAccuracyResult.rows[0]?.avg || '0');
|
||||||
|
const signalsToday = parseInt(signalsTodayResult.rows[0]?.count || '0', 10);
|
||||||
|
const pnlToday = parseFloat(pnlTodayResult.rows[0]?.total || '0');
|
||||||
|
const pnlWeek = parseFloat(pnlWeekResult.rows[0]?.total || '0');
|
||||||
|
const pnlMonth = parseFloat(pnlMonthResult.rows[0]?.total || '0');
|
||||||
|
const avgPnl = parseFloat(avgPnlResult.rows[0]?.avg || '0');
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
total_models: totalModels,
|
||||||
|
active_models: activeModels,
|
||||||
|
total_predictions_today: predictionsToday,
|
||||||
|
total_predictions_week: predictionsToday * 7, // Approximate
|
||||||
|
overall_accuracy: overallAccuracy,
|
||||||
|
total_agents: 3, // Atlas, Orion, Nova - static config
|
||||||
|
active_agents: 0, // Query external service if needed
|
||||||
|
total_signals_today: signalsToday,
|
||||||
|
total_pnl_today: pnlToday,
|
||||||
|
total_pnl_week: pnlWeek,
|
||||||
|
total_pnl_month: pnlMonth,
|
||||||
|
system_health: 'healthy',
|
||||||
users: {
|
users: {
|
||||||
total_users: 150,
|
total_users: totalUsers,
|
||||||
active_users: 142,
|
active_users: activeUsers,
|
||||||
new_users_week: 12,
|
new_users_week: newUsersWeek,
|
||||||
new_users_month: 45,
|
new_users_month: newUsersMonth,
|
||||||
},
|
},
|
||||||
trading: {
|
trading: {
|
||||||
total_trades: 1256,
|
total_trades: totalTrades,
|
||||||
trades_today: 48,
|
trades_today: tradesToday,
|
||||||
winning_trades: 723,
|
winning_trades: winningTrades,
|
||||||
avg_pnl: 125.50,
|
avg_pnl: avgPnl,
|
||||||
},
|
|
||||||
models: {
|
|
||||||
total_models: 6,
|
|
||||||
active_models: 5,
|
|
||||||
predictions_today: 1247,
|
|
||||||
overall_accuracy: 0.68,
|
|
||||||
},
|
|
||||||
agents: {
|
|
||||||
total_agents: 3,
|
|
||||||
active_agents: 1,
|
|
||||||
signals_today: 24,
|
|
||||||
},
|
|
||||||
pnl: {
|
|
||||||
today: 1250.75,
|
|
||||||
week: 8456.32,
|
|
||||||
month: 32145.89,
|
|
||||||
},
|
},
|
||||||
system: {
|
system: {
|
||||||
uptime: process.uptime(),
|
uptime: process.uptime(),
|
||||||
memory: process.memoryUsage(),
|
memory: process.memoryUsage(),
|
||||||
version: process.env.npm_package_version || '1.0.0',
|
version: process.env.npm_package_version || '1.0.0',
|
||||||
},
|
},
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
};
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
data: {
|
|
||||||
total_models: stats.models.total_models,
|
|
||||||
active_models: stats.models.active_models,
|
|
||||||
total_predictions_today: stats.models.predictions_today,
|
|
||||||
total_predictions_week: stats.models.predictions_today * 7,
|
|
||||||
overall_accuracy: stats.models.overall_accuracy,
|
|
||||||
total_agents: stats.agents.total_agents,
|
|
||||||
active_agents: stats.agents.active_agents,
|
|
||||||
total_signals_today: stats.agents.signals_today,
|
|
||||||
total_pnl_today: stats.pnl.today,
|
|
||||||
total_pnl_week: stats.pnl.week,
|
|
||||||
total_pnl_month: stats.pnl.month,
|
|
||||||
system_health: 'healthy',
|
|
||||||
users: stats.users,
|
|
||||||
trading: stats.trading,
|
|
||||||
system: stats.system,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -87,10 +129,22 @@ router.get('/dashboard', async (req: Request, res: Response, next: NextFunction)
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /api/v1/admin/system/health
|
* GET /api/v1/admin/system/health
|
||||||
* Get system-wide health status
|
* Get system-wide health status with real checks
|
||||||
*/
|
*/
|
||||||
router.get('/system/health', async (req: Request, res: Response, next: NextFunction) => {
|
router.get('/system/health', async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
|
// Check Database
|
||||||
|
let dbHealth = 'unknown';
|
||||||
|
let dbLatency = 0;
|
||||||
|
try {
|
||||||
|
const dbStart = Date.now();
|
||||||
|
const healthy = await db.healthCheck();
|
||||||
|
dbLatency = Date.now() - dbStart;
|
||||||
|
dbHealth = healthy ? 'healthy' : 'unhealthy';
|
||||||
|
} catch {
|
||||||
|
dbHealth = 'unhealthy';
|
||||||
|
}
|
||||||
|
|
||||||
// Check ML Engine
|
// Check ML Engine
|
||||||
let mlHealth = 'unknown';
|
let mlHealth = 'unknown';
|
||||||
let mlLatency = 0;
|
let mlLatency = 0;
|
||||||
@ -115,14 +169,32 @@ router.get('/system/health', async (req: Request, res: Response, next: NextFunct
|
|||||||
agentsHealth = 'unhealthy';
|
agentsHealth = 'unhealthy';
|
||||||
}
|
}
|
||||||
|
|
||||||
const overallHealth = (mlHealth === 'healthy' && agentsHealth === 'healthy') ? 'healthy' : 'degraded';
|
// Check Redis
|
||||||
|
let redisHealth = 'unknown';
|
||||||
|
let redisLatency = 0;
|
||||||
|
let redisType = 'unknown';
|
||||||
|
try {
|
||||||
|
const redisCheck = await redis.healthCheck();
|
||||||
|
redisHealth = redisCheck.status;
|
||||||
|
redisLatency = redisCheck.latency;
|
||||||
|
redisType = redisCheck.type;
|
||||||
|
} catch {
|
||||||
|
redisHealth = 'unhealthy';
|
||||||
|
}
|
||||||
|
|
||||||
|
const allHealthy = dbHealth === 'healthy' && mlHealth === 'healthy' && agentsHealth === 'healthy' && redisHealth === 'healthy';
|
||||||
|
const overallHealth = allHealthy ? 'healthy' : (dbHealth === 'healthy' ? 'degraded' : 'unhealthy');
|
||||||
|
|
||||||
|
const poolStatus = db.getPoolStatus();
|
||||||
|
const memUsage = process.memoryUsage();
|
||||||
|
|
||||||
const health = {
|
const health = {
|
||||||
status: overallHealth,
|
status: overallHealth,
|
||||||
services: {
|
services: {
|
||||||
database: {
|
database: {
|
||||||
status: 'healthy', // Mock for now - add actual DB check
|
status: dbHealth,
|
||||||
latency: 5,
|
latency: dbLatency,
|
||||||
|
pool: poolStatus,
|
||||||
},
|
},
|
||||||
mlEngine: {
|
mlEngine: {
|
||||||
status: mlHealth,
|
status: mlHealth,
|
||||||
@ -133,16 +205,17 @@ router.get('/system/health', async (req: Request, res: Response, next: NextFunct
|
|||||||
latency: agentsLatency,
|
latency: agentsLatency,
|
||||||
},
|
},
|
||||||
redis: {
|
redis: {
|
||||||
status: 'healthy', // Mock for now
|
status: redisHealth,
|
||||||
latency: 2,
|
latency: redisLatency,
|
||||||
|
type: redisType,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
system: {
|
system: {
|
||||||
uptime: process.uptime(),
|
uptime: process.uptime(),
|
||||||
memory: {
|
memory: {
|
||||||
used: process.memoryUsage().heapUsed,
|
used: memUsage.heapUsed,
|
||||||
total: process.memoryUsage().heapTotal,
|
total: memUsage.heapTotal,
|
||||||
percentage: (process.memoryUsage().heapUsed / process.memoryUsage().heapTotal) * 100,
|
percentage: Math.round((memUsage.heapUsed / memUsage.heapTotal) * 100),
|
||||||
},
|
},
|
||||||
cpu: process.cpuUsage(),
|
cpu: process.cpuUsage(),
|
||||||
},
|
},
|
||||||
@ -164,68 +237,91 @@ router.get('/system/health', async (req: Request, res: Response, next: NextFunct
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /api/v1/admin/users
|
* GET /api/v1/admin/users
|
||||||
* List all users with filters and pagination
|
* List all users with filters and pagination from real database
|
||||||
*/
|
*/
|
||||||
router.get('/users', async (req: Request, res: Response, next: NextFunction) => {
|
router.get('/users', async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const { page = 1, limit = 20, status, role, search } = req.query;
|
const page = Math.max(1, parseInt(req.query.page as string, 10) || 1);
|
||||||
|
const limit = Math.min(100, Math.max(1, parseInt(req.query.limit as string, 10) || 20));
|
||||||
|
const offset = (page - 1) * limit;
|
||||||
|
const { status, role, search } = req.query;
|
||||||
|
|
||||||
// Mock users data for development
|
// Build dynamic query
|
||||||
const mockUsers = [
|
const conditions: string[] = [];
|
||||||
{
|
const params: (string | number)[] = [];
|
||||||
id: '1',
|
let paramIdx = 1;
|
||||||
email: 'admin@trading.local',
|
|
||||||
role: 'admin',
|
|
||||||
status: 'active',
|
|
||||||
created_at: new Date().toISOString(),
|
|
||||||
full_name: 'Admin Trading Platform',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '2',
|
|
||||||
email: 'trader1@example.com',
|
|
||||||
role: 'premium',
|
|
||||||
status: 'active',
|
|
||||||
created_at: new Date().toISOString(),
|
|
||||||
full_name: 'Trader One',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '3',
|
|
||||||
email: 'trader2@example.com',
|
|
||||||
role: 'user',
|
|
||||||
status: 'active',
|
|
||||||
created_at: new Date().toISOString(),
|
|
||||||
full_name: 'Trader Two',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
let filteredUsers = mockUsers;
|
if (status && typeof status === 'string') {
|
||||||
|
conditions.push(`u.status = $${paramIdx}::auth.user_status`);
|
||||||
if (status) {
|
params.push(status);
|
||||||
filteredUsers = filteredUsers.filter(u => u.status === status);
|
paramIdx++;
|
||||||
}
|
}
|
||||||
if (role) {
|
|
||||||
filteredUsers = filteredUsers.filter(u => u.role === role);
|
if (role && typeof role === 'string') {
|
||||||
|
conditions.push(`u.role = $${paramIdx}::auth.user_role`);
|
||||||
|
params.push(role);
|
||||||
|
paramIdx++;
|
||||||
}
|
}
|
||||||
if (search) {
|
|
||||||
const searchLower = (search as string).toLowerCase();
|
if (search && typeof search === 'string') {
|
||||||
filteredUsers = filteredUsers.filter(u =>
|
conditions.push(`(
|
||||||
u.email.toLowerCase().includes(searchLower) ||
|
u.email ILIKE $${paramIdx}
|
||||||
u.full_name.toLowerCase().includes(searchLower)
|
OR up.first_name ILIKE $${paramIdx}
|
||||||
|
OR up.last_name ILIKE $${paramIdx}
|
||||||
|
OR up.display_name ILIKE $${paramIdx}
|
||||||
|
)`);
|
||||||
|
params.push(`%${search}%`);
|
||||||
|
paramIdx++;
|
||||||
|
}
|
||||||
|
|
||||||
|
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
||||||
|
|
||||||
|
// Count total
|
||||||
|
const countResult = await db.query<{ count: string }>(
|
||||||
|
`SELECT COUNT(*) as count
|
||||||
|
FROM auth.users u
|
||||||
|
LEFT JOIN auth.user_profiles up ON u.id = up.user_id
|
||||||
|
${whereClause}`,
|
||||||
|
params
|
||||||
);
|
);
|
||||||
}
|
const total = parseInt(countResult.rows[0]?.count || '0', 10);
|
||||||
|
|
||||||
const total = filteredUsers.length;
|
// Fetch users
|
||||||
const start = (Number(page) - 1) * Number(limit);
|
const usersResult = await db.query(
|
||||||
const paginatedUsers = filteredUsers.slice(start, start + Number(limit));
|
`SELECT
|
||||||
|
u.id, u.email, u.role::text, u.status::text, u.email_verified,
|
||||||
|
u.mfa_enabled, u.last_login_at, u.created_at, u.updated_at,
|
||||||
|
up.first_name, up.last_name, up.display_name, up.avatar_url
|
||||||
|
FROM auth.users u
|
||||||
|
LEFT JOIN auth.user_profiles up ON u.id = up.user_id
|
||||||
|
${whereClause}
|
||||||
|
ORDER BY u.created_at DESC
|
||||||
|
LIMIT $${paramIdx} OFFSET $${paramIdx + 1}`,
|
||||||
|
[...params, limit, offset]
|
||||||
|
);
|
||||||
|
|
||||||
|
const users = usersResult.rows.map((row: any) => ({
|
||||||
|
id: row.id,
|
||||||
|
email: row.email,
|
||||||
|
role: row.role,
|
||||||
|
status: row.status,
|
||||||
|
email_verified: row.email_verified,
|
||||||
|
mfa_enabled: row.mfa_enabled,
|
||||||
|
last_login_at: row.last_login_at,
|
||||||
|
created_at: row.created_at,
|
||||||
|
updated_at: row.updated_at,
|
||||||
|
full_name: [row.first_name, row.last_name].filter(Boolean).join(' ') || row.display_name || row.email,
|
||||||
|
avatar_url: row.avatar_url,
|
||||||
|
}));
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: paginatedUsers,
|
data: users,
|
||||||
meta: {
|
meta: {
|
||||||
total,
|
total,
|
||||||
page: Number(page),
|
page,
|
||||||
limit: Number(limit),
|
limit,
|
||||||
totalPages: Math.ceil(total / Number(limit)),
|
totalPages: Math.ceil(total / limit),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -235,23 +331,61 @@ router.get('/users', async (req: Request, res: Response, next: NextFunction) =>
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /api/v1/admin/users/:id
|
* GET /api/v1/admin/users/:id
|
||||||
* Get user details by ID
|
* Get user details by ID from real database
|
||||||
*/
|
*/
|
||||||
router.get('/users/:id', async (req: Request, res: Response, next: NextFunction) => {
|
router.get('/users/:id', async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
|
|
||||||
// Mock user data
|
const result = await db.query(
|
||||||
|
`SELECT
|
||||||
|
u.id, u.email, u.role::text, u.status::text, u.email_verified,
|
||||||
|
u.mfa_enabled, u.mfa_method::text, u.phone_number, u.phone_verified,
|
||||||
|
u.last_login_at, u.last_login_ip, u.failed_login_attempts,
|
||||||
|
u.suspended_at, u.suspended_reason, u.created_at, u.updated_at,
|
||||||
|
up.first_name, up.last_name, up.display_name, up.avatar_url,
|
||||||
|
up.bio, up.language, up.timezone, up.country_code
|
||||||
|
FROM auth.users u
|
||||||
|
LEFT JOIN auth.user_profiles up ON u.id = up.user_id
|
||||||
|
WHERE u.id = $1`,
|
||||||
|
[id]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.rows.length === 0) {
|
||||||
|
res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
error: { message: 'User not found', code: 'NOT_FOUND' },
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const row = result.rows[0] as any;
|
||||||
const user = {
|
const user = {
|
||||||
id,
|
id: row.id,
|
||||||
email: 'admin@trading.local',
|
email: row.email,
|
||||||
role: 'admin',
|
role: row.role,
|
||||||
status: 'active',
|
status: row.status,
|
||||||
created_at: new Date().toISOString(),
|
email_verified: row.email_verified,
|
||||||
full_name: 'Admin Trading Platform',
|
mfa_enabled: row.mfa_enabled,
|
||||||
avatar_url: null,
|
mfa_method: row.mfa_method,
|
||||||
bio: 'Platform administrator',
|
phone_number: row.phone_number,
|
||||||
location: 'Remote',
|
phone_verified: row.phone_verified,
|
||||||
|
last_login_at: row.last_login_at,
|
||||||
|
last_login_ip: row.last_login_ip?.toString() || null,
|
||||||
|
failed_login_attempts: row.failed_login_attempts,
|
||||||
|
suspended_at: row.suspended_at,
|
||||||
|
suspended_reason: row.suspended_reason,
|
||||||
|
created_at: row.created_at,
|
||||||
|
updated_at: row.updated_at,
|
||||||
|
full_name: [row.first_name, row.last_name].filter(Boolean).join(' ') || row.display_name || row.email,
|
||||||
|
first_name: row.first_name,
|
||||||
|
last_name: row.last_name,
|
||||||
|
display_name: row.display_name,
|
||||||
|
avatar_url: row.avatar_url,
|
||||||
|
bio: row.bio,
|
||||||
|
language: row.language,
|
||||||
|
timezone: row.timezone,
|
||||||
|
country_code: row.country_code,
|
||||||
};
|
};
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
@ -265,29 +399,46 @@ router.get('/users/:id', async (req: Request, res: Response, next: NextFunction)
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* PATCH /api/v1/admin/users/:id/status
|
* PATCH /api/v1/admin/users/:id/status
|
||||||
* Update user status
|
* Update user status in real database
|
||||||
*/
|
*/
|
||||||
router.patch('/users/:id/status', async (req: Request, res: Response, next: NextFunction) => {
|
router.patch('/users/:id/status', async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const { status, reason } = req.body;
|
const { status, reason } = req.body;
|
||||||
|
|
||||||
if (!['active', 'suspended', 'banned'].includes(status)) {
|
const validStatuses = ['active', 'suspended', 'deactivated', 'banned'];
|
||||||
|
if (!validStatuses.includes(status)) {
|
||||||
res.status(400).json({
|
res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: { message: 'Invalid status value', code: 'VALIDATION_ERROR' },
|
error: { message: `Invalid status. Must be one of: ${validStatuses.join(', ')}`, code: 'VALIDATION_ERROR' },
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const suspendedAt = (status === 'suspended' || status === 'banned') ? 'NOW()' : 'NULL';
|
||||||
|
|
||||||
|
const result = await db.query(
|
||||||
|
`UPDATE auth.users
|
||||||
|
SET status = $1::auth.user_status,
|
||||||
|
suspended_at = ${suspendedAt},
|
||||||
|
suspended_reason = $2,
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE id = $3
|
||||||
|
RETURNING id, status::text, suspended_at, suspended_reason, updated_at`,
|
||||||
|
[status, reason || null, id]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.rows.length === 0) {
|
||||||
|
res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
error: { message: 'User not found', code: 'NOT_FOUND' },
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mock update - replace with actual DB update
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: {
|
data: result.rows[0],
|
||||||
id,
|
|
||||||
status,
|
|
||||||
updated_at: new Date().toISOString(),
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
@ -296,29 +447,41 @@ router.patch('/users/:id/status', async (req: Request, res: Response, next: Next
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* PATCH /api/v1/admin/users/:id/role
|
* PATCH /api/v1/admin/users/:id/role
|
||||||
* Update user role
|
* Update user role in real database
|
||||||
*/
|
*/
|
||||||
router.patch('/users/:id/role', async (req: Request, res: Response, next: NextFunction) => {
|
router.patch('/users/:id/role', async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const { role } = req.body;
|
const { role } = req.body;
|
||||||
|
|
||||||
if (!['user', 'premium', 'admin'].includes(role)) {
|
const validRoles = ['user', 'trader', 'analyst', 'admin', 'super_admin'];
|
||||||
|
if (!validRoles.includes(role)) {
|
||||||
res.status(400).json({
|
res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: { message: 'Invalid role value', code: 'VALIDATION_ERROR' },
|
error: { message: `Invalid role. Must be one of: ${validRoles.join(', ')}`, code: 'VALIDATION_ERROR' },
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await db.query(
|
||||||
|
`UPDATE auth.users
|
||||||
|
SET role = $1::auth.user_role, updated_at = NOW()
|
||||||
|
WHERE id = $2
|
||||||
|
RETURNING id, role::text, updated_at`,
|
||||||
|
[role, id]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.rows.length === 0) {
|
||||||
|
res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
error: { message: 'User not found', code: 'NOT_FOUND' },
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mock update - replace with actual DB update
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: {
|
data: result.rows[0],
|
||||||
id,
|
|
||||||
role,
|
|
||||||
updated_at: new Date().toISOString(),
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
@ -331,64 +494,100 @@ router.patch('/users/:id/role', async (req: Request, res: Response, next: NextFu
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /api/v1/admin/audit/logs
|
* GET /api/v1/admin/audit/logs
|
||||||
* Get audit logs with filters
|
* Get audit logs with filters from real database
|
||||||
*/
|
*/
|
||||||
router.get('/audit/logs', async (req: Request, res: Response, next: NextFunction) => {
|
router.get('/audit/logs', async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const { page = 1, limit = 50, userId, action, startDate, endDate } = req.query;
|
const page = Math.max(1, parseInt(req.query.page as string, 10) || 1);
|
||||||
|
const limit = Math.min(100, Math.max(1, parseInt(req.query.limit as string, 10) || 50));
|
||||||
|
const offset = (page - 1) * limit;
|
||||||
|
const { userId, action, startDate, endDate, severity } = req.query;
|
||||||
|
|
||||||
// Mock audit logs
|
const conditions: string[] = [];
|
||||||
const mockLogs = [
|
const params: (string | number)[] = [];
|
||||||
{
|
let paramIdx = 1;
|
||||||
id: '1',
|
|
||||||
user_id: '1',
|
|
||||||
action: 'LOGIN',
|
|
||||||
resource: 'auth',
|
|
||||||
details: { ip: '192.168.1.1' },
|
|
||||||
ip_address: '192.168.1.1',
|
|
||||||
created_at: new Date().toISOString(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '2',
|
|
||||||
user_id: '1',
|
|
||||||
action: 'UPDATE_SETTINGS',
|
|
||||||
resource: 'users',
|
|
||||||
details: { theme: 'dark' },
|
|
||||||
ip_address: '192.168.1.1',
|
|
||||||
created_at: new Date(Date.now() - 3600000).toISOString(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '3',
|
|
||||||
user_id: '1',
|
|
||||||
action: 'CREATE_SIGNAL',
|
|
||||||
resource: 'trading',
|
|
||||||
details: { symbol: 'XAUUSD', direction: 'long' },
|
|
||||||
ip_address: '192.168.1.1',
|
|
||||||
created_at: new Date(Date.now() - 7200000).toISOString(),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
let filteredLogs = mockLogs;
|
if (userId && typeof userId === 'string') {
|
||||||
|
conditions.push(`al.user_id = $${paramIdx}`);
|
||||||
if (userId) {
|
params.push(userId);
|
||||||
filteredLogs = filteredLogs.filter(l => l.user_id === userId);
|
paramIdx++;
|
||||||
}
|
|
||||||
if (action) {
|
|
||||||
filteredLogs = filteredLogs.filter(l => l.action === action);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const total = filteredLogs.length;
|
if (action && typeof action === 'string') {
|
||||||
const start = (Number(page) - 1) * Number(limit);
|
conditions.push(`al.action = $${paramIdx}`);
|
||||||
const paginatedLogs = filteredLogs.slice(start, start + Number(limit));
|
params.push(action);
|
||||||
|
paramIdx++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (severity && typeof severity === 'string') {
|
||||||
|
conditions.push(`al.severity = $${paramIdx}::audit.event_severity`);
|
||||||
|
params.push(severity);
|
||||||
|
paramIdx++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startDate && typeof startDate === 'string') {
|
||||||
|
conditions.push(`al.created_at >= $${paramIdx}`);
|
||||||
|
params.push(startDate);
|
||||||
|
paramIdx++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (endDate && typeof endDate === 'string') {
|
||||||
|
conditions.push(`al.created_at <= $${paramIdx}`);
|
||||||
|
params.push(endDate);
|
||||||
|
paramIdx++;
|
||||||
|
}
|
||||||
|
|
||||||
|
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
||||||
|
|
||||||
|
// Count total
|
||||||
|
const countResult = await db.query<{ count: string }>(
|
||||||
|
`SELECT COUNT(*) as count FROM audit.audit_logs al ${whereClause}`,
|
||||||
|
params
|
||||||
|
);
|
||||||
|
const total = parseInt(countResult.rows[0]?.count || '0', 10);
|
||||||
|
|
||||||
|
// Fetch logs
|
||||||
|
const logsResult = await db.query(
|
||||||
|
`SELECT
|
||||||
|
al.id, al.event_type::text, al.event_status::text, al.severity::text,
|
||||||
|
al.user_id, al.session_id, al.ip_address, al.user_agent,
|
||||||
|
al.resource_type::text, al.resource_id, al.resource_name,
|
||||||
|
al.action, al.description, al.old_values, al.new_values,
|
||||||
|
al.metadata, al.created_at,
|
||||||
|
u.email as user_email
|
||||||
|
FROM audit.audit_logs al
|
||||||
|
LEFT JOIN auth.users u ON al.user_id = u.id
|
||||||
|
${whereClause}
|
||||||
|
ORDER BY al.created_at DESC
|
||||||
|
LIMIT $${paramIdx} OFFSET $${paramIdx + 1}`,
|
||||||
|
[...params, limit, offset]
|
||||||
|
);
|
||||||
|
|
||||||
|
const logs = logsResult.rows.map((row: any) => ({
|
||||||
|
id: row.id,
|
||||||
|
event_type: row.event_type,
|
||||||
|
event_status: row.event_status,
|
||||||
|
severity: row.severity,
|
||||||
|
user_id: row.user_id,
|
||||||
|
user_email: row.user_email,
|
||||||
|
ip_address: row.ip_address?.toString() || null,
|
||||||
|
resource_type: row.resource_type,
|
||||||
|
resource_id: row.resource_id,
|
||||||
|
resource_name: row.resource_name,
|
||||||
|
action: row.action,
|
||||||
|
description: row.description,
|
||||||
|
details: { ...row.old_values, ...row.new_values, ...row.metadata },
|
||||||
|
created_at: row.created_at,
|
||||||
|
}));
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: paginatedLogs,
|
data: logs,
|
||||||
meta: {
|
meta: {
|
||||||
total,
|
total,
|
||||||
page: Number(page),
|
page,
|
||||||
limit: Number(limit),
|
limit,
|
||||||
totalPages: Math.ceil(total / Number(limit)),
|
totalPages: Math.ceil(total / limit),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -397,29 +596,57 @@ router.get('/audit/logs', async (req: Request, res: Response, next: NextFunction
|
|||||||
});
|
});
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Stats Endpoint (for admin dashboard widget)
|
// Stats Endpoint
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /api/v1/admin/stats
|
* GET /api/v1/admin/stats
|
||||||
* Get admin stats (alias for dashboard endpoint)
|
* Get admin stats from real database
|
||||||
*/
|
*/
|
||||||
router.get('/stats', async (req: Request, res: Response, next: NextFunction) => {
|
router.get('/stats', async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
|
const [
|
||||||
|
modelsResult,
|
||||||
|
activeModelsResult,
|
||||||
|
predsTodayResult,
|
||||||
|
predsWeekResult,
|
||||||
|
accuracyResult,
|
||||||
|
signalsTodayResult,
|
||||||
|
statsPnlTodayResult,
|
||||||
|
statsPnlWeekResult,
|
||||||
|
statsPnlMonthResult,
|
||||||
|
] = await Promise.all([
|
||||||
|
db.query<{ count: string }>('SELECT COUNT(*) as count FROM ml.models'),
|
||||||
|
db.query<{ count: string }>("SELECT COUNT(*) as count FROM ml.models WHERE status = 'production'"),
|
||||||
|
db.query<{ count: string }>("SELECT COUNT(*) as count FROM ml.predictions WHERE DATE(created_at) = CURRENT_DATE"),
|
||||||
|
db.query<{ count: string }>("SELECT COUNT(*) as count FROM ml.predictions WHERE created_at >= NOW() - INTERVAL '7 days'"),
|
||||||
|
db.query<{ avg: string | null }>("SELECT AVG(overall_accuracy) as avg FROM ml.models WHERE status = 'production'"),
|
||||||
|
db.query<{ count: string }>("SELECT COUNT(*) as count FROM trading.signals WHERE DATE(created_at) = CURRENT_DATE"),
|
||||||
|
db.query<{ total: string | null }>(
|
||||||
|
"SELECT COALESCE(SUM(realized_pnl), 0) as total FROM trading.positions WHERE status = 'closed' AND DATE(closed_at) = CURRENT_DATE"
|
||||||
|
),
|
||||||
|
db.query<{ total: string | null }>(
|
||||||
|
"SELECT COALESCE(SUM(realized_pnl), 0) as total FROM trading.positions WHERE status = 'closed' AND closed_at >= NOW() - INTERVAL '7 days'"
|
||||||
|
),
|
||||||
|
db.query<{ total: string | null }>(
|
||||||
|
"SELECT COALESCE(SUM(realized_pnl), 0) as total FROM trading.positions WHERE status = 'closed' AND closed_at >= NOW() - INTERVAL '30 days'"
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: {
|
data: {
|
||||||
total_models: 6,
|
total_models: parseInt(modelsResult.rows[0]?.count || '0', 10),
|
||||||
active_models: 5,
|
active_models: parseInt(activeModelsResult.rows[0]?.count || '0', 10),
|
||||||
total_predictions_today: 1247,
|
total_predictions_today: parseInt(predsTodayResult.rows[0]?.count || '0', 10),
|
||||||
total_predictions_week: 8729,
|
total_predictions_week: parseInt(predsWeekResult.rows[0]?.count || '0', 10),
|
||||||
overall_accuracy: 0.68,
|
overall_accuracy: parseFloat(accuracyResult.rows[0]?.avg || '0'),
|
||||||
total_agents: 3,
|
total_agents: 3,
|
||||||
active_agents: 1,
|
active_agents: 0,
|
||||||
total_signals_today: 24,
|
total_signals_today: parseInt(signalsTodayResult.rows[0]?.count || '0', 10),
|
||||||
total_pnl_today: 1250.75,
|
total_pnl_today: parseFloat(statsPnlTodayResult.rows[0]?.total || '0'),
|
||||||
total_pnl_week: 8456.32,
|
total_pnl_week: parseFloat(statsPnlWeekResult.rows[0]?.total || '0'),
|
||||||
total_pnl_month: 32145.89,
|
total_pnl_month: parseFloat(statsPnlMonthResult.rows[0]?.total || '0'),
|
||||||
system_health: 'healthy',
|
system_health: 'healthy',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
168
src/modules/llm/types/llm.types.ts
Normal file
168
src/modules/llm/types/llm.types.ts
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
/**
|
||||||
|
* LLM Types
|
||||||
|
* TypeScript interfaces matching llm.* DDL schema
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Enums (from llm.00-enums.sql)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export type MessageRole = 'user' | 'assistant' | 'system' | 'tool';
|
||||||
|
export type ConversationStatus = 'active' | 'archived' | 'deleted';
|
||||||
|
export type ConversationType = 'general' | 'trading_advice' | 'education' | 'market_analysis' | 'support' | 'onboarding';
|
||||||
|
export type CommunicationTone = 'casual' | 'professional' | 'technical';
|
||||||
|
export type VerbosityLevel = 'brief' | 'normal' | 'detailed';
|
||||||
|
export type AlertFrequency = 'low' | 'normal' | 'high';
|
||||||
|
export type MemoryType = 'fact' | 'preference' | 'context' | 'goal' | 'constraint';
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Conversation (from llm.conversations)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export interface Conversation {
|
||||||
|
id: string;
|
||||||
|
userId: string;
|
||||||
|
title?: string;
|
||||||
|
conversationType: ConversationType;
|
||||||
|
status: ConversationStatus;
|
||||||
|
summary?: string;
|
||||||
|
totalMessages: number;
|
||||||
|
totalTokensUsed: number;
|
||||||
|
tags: string[];
|
||||||
|
relatedSymbols: string[];
|
||||||
|
relatedTopics: string[];
|
||||||
|
startedAt: Date;
|
||||||
|
lastMessageAt?: Date;
|
||||||
|
archivedAt?: Date;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Message (from llm.messages)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export interface Message {
|
||||||
|
id: string;
|
||||||
|
conversationId: string;
|
||||||
|
role: MessageRole;
|
||||||
|
content: string;
|
||||||
|
modelName?: string;
|
||||||
|
promptTokens?: number;
|
||||||
|
completionTokens?: number;
|
||||||
|
totalTokens?: number;
|
||||||
|
contextUsed?: Record<string, unknown>;
|
||||||
|
toolCalls?: ToolCall[];
|
||||||
|
toolResults?: Record<string, unknown>;
|
||||||
|
responseTimeMs?: number;
|
||||||
|
temperature?: number;
|
||||||
|
userRating?: number;
|
||||||
|
userFeedback?: string;
|
||||||
|
referencesSymbols: string[];
|
||||||
|
referencesConcepts: string[];
|
||||||
|
metadata?: Record<string, unknown>;
|
||||||
|
createdAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ToolCall {
|
||||||
|
tool: string;
|
||||||
|
params: Record<string, unknown>;
|
||||||
|
result?: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// User Preferences (from llm.user_preferences)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export interface UserPreferences {
|
||||||
|
id: string;
|
||||||
|
userId: string;
|
||||||
|
communicationTone: CommunicationTone;
|
||||||
|
verbosityLevel: VerbosityLevel;
|
||||||
|
alertFrequency: AlertFrequency;
|
||||||
|
preferredLanguage: string;
|
||||||
|
preferredSymbols: string[];
|
||||||
|
preferredTimeframes: string[];
|
||||||
|
tradingExperience: string;
|
||||||
|
riskTolerance: string;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// User Memory (from llm.user_memory)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export interface UserMemory {
|
||||||
|
id: string;
|
||||||
|
userId: string;
|
||||||
|
memoryType: MemoryType;
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
confidence: number;
|
||||||
|
sourceConversationId?: string;
|
||||||
|
expiresAt?: Date;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Embedding (from llm.embeddings)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export interface Embedding {
|
||||||
|
id: string;
|
||||||
|
sourceType: string;
|
||||||
|
sourceId: string;
|
||||||
|
content: string;
|
||||||
|
embedding: number[];
|
||||||
|
modelName: string;
|
||||||
|
dimensions: number;
|
||||||
|
metadata?: Record<string, unknown>;
|
||||||
|
createdAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Row types (snake_case from DB)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export interface ConversationRow {
|
||||||
|
id: string;
|
||||||
|
user_id: string;
|
||||||
|
title: string | null;
|
||||||
|
conversation_type: string;
|
||||||
|
status: string;
|
||||||
|
summary: string | null;
|
||||||
|
total_messages: number;
|
||||||
|
total_tokens_used: number;
|
||||||
|
tags: string[];
|
||||||
|
related_symbols: string[];
|
||||||
|
related_topics: string[];
|
||||||
|
started_at: Date;
|
||||||
|
last_message_at: Date | null;
|
||||||
|
archived_at: Date | null;
|
||||||
|
created_at: Date;
|
||||||
|
updated_at: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MessageRow {
|
||||||
|
id: string;
|
||||||
|
conversation_id: string;
|
||||||
|
role: string;
|
||||||
|
content: string;
|
||||||
|
model_name: string | null;
|
||||||
|
prompt_tokens: number | null;
|
||||||
|
completion_tokens: number | null;
|
||||||
|
total_tokens: number | null;
|
||||||
|
context_used: Record<string, unknown> | null;
|
||||||
|
tool_calls: Record<string, unknown> | null;
|
||||||
|
tool_results: Record<string, unknown> | null;
|
||||||
|
response_time_ms: number | null;
|
||||||
|
temperature: number | null;
|
||||||
|
user_rating: number | null;
|
||||||
|
user_feedback: string | null;
|
||||||
|
references_symbols: string[];
|
||||||
|
references_concepts: string[];
|
||||||
|
metadata: Record<string, unknown> | null;
|
||||||
|
created_at: Date;
|
||||||
|
}
|
||||||
173
src/modules/payments/types/financial.types.ts
Normal file
173
src/modules/payments/types/financial.types.ts
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
/**
|
||||||
|
* Financial Types
|
||||||
|
* TypeScript interfaces matching financial.* DDL schema
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Enums (from financial.00-enums.sql)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export type WalletType = 'trading' | 'investment' | 'earnings' | 'referral';
|
||||||
|
export type WalletStatus = 'active' | 'frozen' | 'closed';
|
||||||
|
export type TransactionType = 'deposit' | 'withdrawal' | 'transfer_in' | 'transfer_out' | 'fee' | 'refund' | 'earning' | 'distribution' | 'bonus';
|
||||||
|
export type TransactionStatus = 'pending' | 'processing' | 'completed' | 'failed' | 'cancelled' | 'reversed';
|
||||||
|
export type SubscriptionPlan = 'free' | 'basic' | 'pro' | 'premium' | 'enterprise';
|
||||||
|
export type SubscriptionStatus = 'active' | 'past_due' | 'cancelled' | 'incomplete' | 'trialing' | 'unpaid' | 'paused';
|
||||||
|
export type CurrencyCode = 'USD' | 'MXN' | 'EUR';
|
||||||
|
export type PaymentMethodType = 'card' | 'bank_transfer' | 'wire' | 'crypto' | 'paypal' | 'stripe';
|
||||||
|
export type PaymentStatus = 'pending' | 'processing' | 'succeeded' | 'failed' | 'cancelled' | 'refunded';
|
||||||
|
export type InvoiceType = 'subscription' | 'one_time' | 'usage';
|
||||||
|
export type InvoiceStatus = 'draft' | 'open' | 'paid' | 'void' | 'uncollectible';
|
||||||
|
export type WalletAuditAction = 'created' | 'balance_updated' | 'status_changed' | 'limit_changed' | 'frozen' | 'unfrozen' | 'closed';
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Wallet (from financial.wallets)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export interface Wallet {
|
||||||
|
id: string;
|
||||||
|
userId: string;
|
||||||
|
walletType: WalletType;
|
||||||
|
status: WalletStatus;
|
||||||
|
balance: number;
|
||||||
|
availableBalance: number;
|
||||||
|
pendingBalance: number;
|
||||||
|
currency: CurrencyCode;
|
||||||
|
stripeAccountId?: string;
|
||||||
|
stripeCustomerId?: string;
|
||||||
|
dailyWithdrawalLimit?: number;
|
||||||
|
monthlyWithdrawalLimit?: number;
|
||||||
|
minBalance: number;
|
||||||
|
lastTransactionAt?: Date;
|
||||||
|
totalDeposits: number;
|
||||||
|
totalWithdrawals: number;
|
||||||
|
metadata: Record<string, unknown>;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
closedAt?: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Wallet Transaction (from financial.wallet_transactions)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export interface WalletTransaction {
|
||||||
|
id: string;
|
||||||
|
walletId: string;
|
||||||
|
transactionType: TransactionType;
|
||||||
|
status: TransactionStatus;
|
||||||
|
amount: number;
|
||||||
|
fee: number;
|
||||||
|
netAmount: number;
|
||||||
|
currency: CurrencyCode;
|
||||||
|
balanceBefore?: number;
|
||||||
|
balanceAfter?: number;
|
||||||
|
stripePaymentIntentId?: string;
|
||||||
|
stripeTransferId?: string;
|
||||||
|
stripeChargeId?: string;
|
||||||
|
referenceId?: string;
|
||||||
|
destinationWalletId?: string;
|
||||||
|
relatedTransactionId?: string;
|
||||||
|
description?: string;
|
||||||
|
notes?: string;
|
||||||
|
metadata: Record<string, unknown>;
|
||||||
|
processedAt?: Date;
|
||||||
|
completedAt?: Date;
|
||||||
|
failedAt?: Date;
|
||||||
|
failedReason?: string;
|
||||||
|
idempotencyKey?: string;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Subscription (from financial.subscriptions)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export interface Subscription {
|
||||||
|
id: string;
|
||||||
|
userId: string;
|
||||||
|
plan: SubscriptionPlan;
|
||||||
|
status: SubscriptionStatus;
|
||||||
|
stripeSubscriptionId?: string;
|
||||||
|
stripeCustomerId?: string;
|
||||||
|
stripePriceId?: string;
|
||||||
|
stripeProductId?: string;
|
||||||
|
price: number;
|
||||||
|
currency: CurrencyCode;
|
||||||
|
billingInterval: 'month' | 'year';
|
||||||
|
currentPeriodStart?: Date;
|
||||||
|
currentPeriodEnd?: Date;
|
||||||
|
trialStart?: Date;
|
||||||
|
trialEnd?: Date;
|
||||||
|
cancelledAt?: Date;
|
||||||
|
cancelAtPeriodEnd: boolean;
|
||||||
|
cancellationReason?: string;
|
||||||
|
cancellationFeedback?: Record<string, unknown>;
|
||||||
|
previousPlan?: SubscriptionPlan;
|
||||||
|
scheduledPlan?: SubscriptionPlan;
|
||||||
|
scheduledPlanEffectiveAt?: Date;
|
||||||
|
lastPaymentAt?: Date;
|
||||||
|
nextPaymentAt?: Date;
|
||||||
|
failedPaymentCount: number;
|
||||||
|
metadata: Record<string, unknown>;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
endedAt?: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Row types (snake_case from DB)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export interface WalletRow {
|
||||||
|
id: string;
|
||||||
|
user_id: string;
|
||||||
|
wallet_type: string;
|
||||||
|
status: string;
|
||||||
|
balance: string;
|
||||||
|
available_balance: string;
|
||||||
|
pending_balance: string;
|
||||||
|
currency: string;
|
||||||
|
stripe_account_id: string | null;
|
||||||
|
stripe_customer_id: string | null;
|
||||||
|
daily_withdrawal_limit: string | null;
|
||||||
|
monthly_withdrawal_limit: string | null;
|
||||||
|
min_balance: string;
|
||||||
|
last_transaction_at: Date | null;
|
||||||
|
total_deposits: string;
|
||||||
|
total_withdrawals: string;
|
||||||
|
metadata: Record<string, unknown>;
|
||||||
|
created_at: Date;
|
||||||
|
updated_at: Date;
|
||||||
|
closed_at: Date | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WalletTransactionRow {
|
||||||
|
id: string;
|
||||||
|
wallet_id: string;
|
||||||
|
transaction_type: string;
|
||||||
|
status: string;
|
||||||
|
amount: string;
|
||||||
|
fee: string;
|
||||||
|
net_amount: string;
|
||||||
|
currency: string;
|
||||||
|
balance_before: string | null;
|
||||||
|
balance_after: string | null;
|
||||||
|
stripe_payment_intent_id: string | null;
|
||||||
|
stripe_transfer_id: string | null;
|
||||||
|
stripe_charge_id: string | null;
|
||||||
|
reference_id: string | null;
|
||||||
|
destination_wallet_id: string | null;
|
||||||
|
related_transaction_id: string | null;
|
||||||
|
description: string | null;
|
||||||
|
notes: string | null;
|
||||||
|
metadata: Record<string, unknown>;
|
||||||
|
processed_at: Date | null;
|
||||||
|
completed_at: Date | null;
|
||||||
|
failed_at: Date | null;
|
||||||
|
failed_reason: string | null;
|
||||||
|
idempotency_key: string | null;
|
||||||
|
created_at: Date;
|
||||||
|
updated_at: Date;
|
||||||
|
}
|
||||||
137
src/shared/redis/index.ts
Normal file
137
src/shared/redis/index.ts
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
/**
|
||||||
|
* Trading Platform - Redis Connection
|
||||||
|
* Shared Redis client with health check support.
|
||||||
|
* Falls back to in-memory if ioredis is not installed.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { config } from '../../config';
|
||||||
|
import { logger } from '../utils/logger';
|
||||||
|
|
||||||
|
interface RedisClientLike {
|
||||||
|
get(key: string): Promise<string | null>;
|
||||||
|
set(key: string, value: string, ...args: unknown[]): Promise<unknown>;
|
||||||
|
setex(key: string, seconds: number, value: string): Promise<unknown>;
|
||||||
|
del(...keys: string[]): Promise<unknown>;
|
||||||
|
ping(): Promise<string>;
|
||||||
|
quit(): Promise<unknown>;
|
||||||
|
status?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class RedisManager {
|
||||||
|
private client: RedisClientLike | null = null;
|
||||||
|
private isConnected = false;
|
||||||
|
private usesFallback = false;
|
||||||
|
|
||||||
|
async getClient(): Promise<RedisClientLike> {
|
||||||
|
if (this.client) return this.client;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||||
|
const Redis = require('ioredis');
|
||||||
|
const redisClient = new Redis({
|
||||||
|
host: config.redis.host,
|
||||||
|
port: config.redis.port,
|
||||||
|
password: config.redis.password || undefined,
|
||||||
|
db: config.redis.db,
|
||||||
|
lazyConnect: true,
|
||||||
|
maxRetriesPerRequest: 3,
|
||||||
|
retryStrategy(times: number) {
|
||||||
|
if (times > 3) return null;
|
||||||
|
return Math.min(times * 200, 2000);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await redisClient.connect();
|
||||||
|
this.client = redisClient as RedisClientLike;
|
||||||
|
this.isConnected = true;
|
||||||
|
this.usesFallback = false;
|
||||||
|
logger.info('Redis connected', { host: config.redis.host, port: config.redis.port, db: config.redis.db });
|
||||||
|
return this.client;
|
||||||
|
} catch {
|
||||||
|
logger.warn('Redis not available, using in-memory fallback');
|
||||||
|
this.client = new InMemoryFallback();
|
||||||
|
this.isConnected = true;
|
||||||
|
this.usesFallback = true;
|
||||||
|
return this.client;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async healthCheck(): Promise<{ status: string; latency: number; type: string }> {
|
||||||
|
const start = Date.now();
|
||||||
|
try {
|
||||||
|
const client = await this.getClient();
|
||||||
|
const pong = await client.ping();
|
||||||
|
const latency = Date.now() - start;
|
||||||
|
return {
|
||||||
|
status: pong === 'PONG' ? 'healthy' : 'degraded',
|
||||||
|
latency,
|
||||||
|
type: this.usesFallback ? 'memory' : 'redis',
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return {
|
||||||
|
status: 'unhealthy',
|
||||||
|
latency: Date.now() - start,
|
||||||
|
type: this.usesFallback ? 'memory' : 'redis',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getConnectionInfo(): { connected: boolean; type: string } {
|
||||||
|
return {
|
||||||
|
connected: this.isConnected,
|
||||||
|
type: this.usesFallback ? 'memory' : 'redis',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async close(): Promise<void> {
|
||||||
|
if (this.client && !this.usesFallback) {
|
||||||
|
await this.client.quit();
|
||||||
|
logger.info('Redis connection closed');
|
||||||
|
}
|
||||||
|
this.client = null;
|
||||||
|
this.isConnected = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class InMemoryFallback implements RedisClientLike {
|
||||||
|
private store = new Map<string, { value: string; expiresAt?: number }>();
|
||||||
|
|
||||||
|
async get(key: string): Promise<string | null> {
|
||||||
|
const entry = this.store.get(key);
|
||||||
|
if (!entry) return null;
|
||||||
|
if (entry.expiresAt && Date.now() > entry.expiresAt) {
|
||||||
|
this.store.delete(key);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return entry.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
async set(key: string, value: string): Promise<string> {
|
||||||
|
this.store.set(key, { value });
|
||||||
|
return 'OK';
|
||||||
|
}
|
||||||
|
|
||||||
|
async setex(key: string, seconds: number, value: string): Promise<string> {
|
||||||
|
this.store.set(key, { value, expiresAt: Date.now() + seconds * 1000 });
|
||||||
|
return 'OK';
|
||||||
|
}
|
||||||
|
|
||||||
|
async del(...keys: string[]): Promise<number> {
|
||||||
|
let count = 0;
|
||||||
|
for (const key of keys) {
|
||||||
|
if (this.store.delete(key)) count++;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
async ping(): Promise<string> {
|
||||||
|
return 'PONG';
|
||||||
|
}
|
||||||
|
|
||||||
|
async quit(): Promise<string> {
|
||||||
|
this.store.clear();
|
||||||
|
return 'OK';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const redis = new RedisManager();
|
||||||
121
src/shared/types/audit.types.ts
Normal file
121
src/shared/types/audit.types.ts
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
/**
|
||||||
|
* Audit Types
|
||||||
|
* TypeScript interfaces matching audit.* DDL schema
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Enums (from audit.00-enums.sql)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export type AuditEventType = 'create' | 'read' | 'update' | 'delete' | 'login' | 'logout' | 'permission_change' | 'config_change' | 'export' | 'import';
|
||||||
|
export type EventSeverity = 'debug' | 'info' | 'warning' | 'error' | 'critical';
|
||||||
|
export type SecurityEventCategory = 'authentication' | 'authorization' | 'data_access' | 'configuration' | 'suspicious_activity' | 'compliance';
|
||||||
|
export type EventStatus = 'success' | 'failure' | 'blocked' | 'pending_review';
|
||||||
|
export type ResourceType = 'user' | 'account' | 'transaction' | 'order' | 'position' | 'bot' | 'subscription' | 'payment' | 'course' | 'model' | 'system_config';
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Audit Log (from audit.audit_logs)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export interface AuditLog {
|
||||||
|
id: string;
|
||||||
|
eventType: AuditEventType;
|
||||||
|
eventStatus: EventStatus;
|
||||||
|
severity: EventSeverity;
|
||||||
|
userId?: string;
|
||||||
|
sessionId?: string;
|
||||||
|
ipAddress?: string;
|
||||||
|
userAgent?: string;
|
||||||
|
resourceType: ResourceType;
|
||||||
|
resourceId?: string;
|
||||||
|
resourceName?: string;
|
||||||
|
action: string;
|
||||||
|
description?: string;
|
||||||
|
oldValues?: Record<string, unknown>;
|
||||||
|
newValues?: Record<string, unknown>;
|
||||||
|
metadata: Record<string, unknown>;
|
||||||
|
requestId?: string;
|
||||||
|
correlationId?: string;
|
||||||
|
serviceName?: string;
|
||||||
|
createdAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Security Event (from audit.security_events)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export interface SecurityEvent {
|
||||||
|
id: string;
|
||||||
|
userId?: string;
|
||||||
|
category: SecurityEventCategory;
|
||||||
|
severity: EventSeverity;
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
ipAddress?: string;
|
||||||
|
userAgent?: string;
|
||||||
|
geoLocation?: Record<string, unknown>;
|
||||||
|
metadata: Record<string, unknown>;
|
||||||
|
resolved: boolean;
|
||||||
|
resolvedAt?: Date;
|
||||||
|
resolvedBy?: string;
|
||||||
|
createdAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Trading Audit (from audit.trading_audit)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export interface TradingAudit {
|
||||||
|
id: string;
|
||||||
|
userId: string;
|
||||||
|
action: string;
|
||||||
|
entityType: string;
|
||||||
|
entityId: string;
|
||||||
|
details: Record<string, unknown>;
|
||||||
|
ipAddress?: string;
|
||||||
|
createdAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Row types (snake_case from DB)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export interface AuditLogRow {
|
||||||
|
id: string;
|
||||||
|
event_type: string;
|
||||||
|
event_status: string;
|
||||||
|
severity: string;
|
||||||
|
user_id: string | null;
|
||||||
|
session_id: string | null;
|
||||||
|
ip_address: string | null;
|
||||||
|
user_agent: string | null;
|
||||||
|
resource_type: string;
|
||||||
|
resource_id: string | null;
|
||||||
|
resource_name: string | null;
|
||||||
|
action: string;
|
||||||
|
description: string | null;
|
||||||
|
old_values: Record<string, unknown> | null;
|
||||||
|
new_values: Record<string, unknown> | null;
|
||||||
|
metadata: Record<string, unknown>;
|
||||||
|
request_id: string | null;
|
||||||
|
correlation_id: string | null;
|
||||||
|
service_name: string | null;
|
||||||
|
created_at: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SecurityEventRow {
|
||||||
|
id: string;
|
||||||
|
user_id: string | null;
|
||||||
|
category: string;
|
||||||
|
severity: string;
|
||||||
|
title: string;
|
||||||
|
description: string | null;
|
||||||
|
ip_address: string | null;
|
||||||
|
user_agent: string | null;
|
||||||
|
geo_location: Record<string, unknown> | null;
|
||||||
|
metadata: Record<string, unknown>;
|
||||||
|
resolved: boolean;
|
||||||
|
resolved_at: Date | null;
|
||||||
|
resolved_by: string | null;
|
||||||
|
created_at: Date;
|
||||||
|
}
|
||||||
@ -3,3 +3,5 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export * from './common.types';
|
export * from './common.types';
|
||||||
|
export * from './audit.types';
|
||||||
|
export * from './market-data.types';
|
||||||
|
|||||||
127
src/shared/types/market-data.types.ts
Normal file
127
src/shared/types/market-data.types.ts
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
/**
|
||||||
|
* Market Data Types
|
||||||
|
* TypeScript interfaces matching market_data.* DDL schema
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Enums (from market_data.00-enums.sql)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export type AssetType = 'forex' | 'crypto' | 'commodity' | 'index' | 'stock';
|
||||||
|
export type Timeframe = '1m' | '5m' | '15m' | '30m' | '1h' | '4h' | '1d' | '1w';
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Ticker (from market_data.tickers)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export interface Ticker {
|
||||||
|
id: string;
|
||||||
|
symbol: string;
|
||||||
|
name: string;
|
||||||
|
assetType: AssetType;
|
||||||
|
exchange?: string;
|
||||||
|
baseCurrency: string;
|
||||||
|
quoteCurrency: string;
|
||||||
|
pipSize: number;
|
||||||
|
lotSize: number;
|
||||||
|
minLotSize: number;
|
||||||
|
maxLotSize: number;
|
||||||
|
isActive: boolean;
|
||||||
|
metadata: Record<string, unknown>;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// OHLCV Data (from market_data.ohlcv_data)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export interface OhlcvData {
|
||||||
|
id: string;
|
||||||
|
tickerId: string;
|
||||||
|
timeframe: Timeframe;
|
||||||
|
openTime: Date;
|
||||||
|
closeTime: Date;
|
||||||
|
open: number;
|
||||||
|
high: number;
|
||||||
|
low: number;
|
||||||
|
close: number;
|
||||||
|
volume: number;
|
||||||
|
quoteVolume?: number;
|
||||||
|
numberOfTrades?: number;
|
||||||
|
metadata?: Record<string, unknown>;
|
||||||
|
createdAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Market Snapshot (from market_data.market_snapshots)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export interface MarketSnapshot {
|
||||||
|
id: string;
|
||||||
|
tickerId: string;
|
||||||
|
bid: number;
|
||||||
|
ask: number;
|
||||||
|
last: number;
|
||||||
|
volume24h: number;
|
||||||
|
change24h: number;
|
||||||
|
changePercent24h: number;
|
||||||
|
high24h: number;
|
||||||
|
low24h: number;
|
||||||
|
metadata?: Record<string, unknown>;
|
||||||
|
capturedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Row types (snake_case from DB)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export interface TickerRow {
|
||||||
|
id: string;
|
||||||
|
symbol: string;
|
||||||
|
name: string;
|
||||||
|
asset_type: string;
|
||||||
|
exchange: string | null;
|
||||||
|
base_currency: string;
|
||||||
|
quote_currency: string;
|
||||||
|
pip_size: string;
|
||||||
|
lot_size: string;
|
||||||
|
min_lot_size: string;
|
||||||
|
max_lot_size: string;
|
||||||
|
is_active: boolean;
|
||||||
|
metadata: Record<string, unknown>;
|
||||||
|
created_at: Date;
|
||||||
|
updated_at: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OhlcvDataRow {
|
||||||
|
id: string;
|
||||||
|
ticker_id: string;
|
||||||
|
timeframe: string;
|
||||||
|
open_time: Date;
|
||||||
|
close_time: Date;
|
||||||
|
open: string;
|
||||||
|
high: string;
|
||||||
|
low: string;
|
||||||
|
close: string;
|
||||||
|
volume: string;
|
||||||
|
quote_volume: string | null;
|
||||||
|
number_of_trades: number | null;
|
||||||
|
metadata: Record<string, unknown> | null;
|
||||||
|
created_at: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MarketSnapshotRow {
|
||||||
|
id: string;
|
||||||
|
ticker_id: string;
|
||||||
|
bid: string;
|
||||||
|
ask: string;
|
||||||
|
last: string;
|
||||||
|
volume_24h: string;
|
||||||
|
change_24h: string;
|
||||||
|
change_percent_24h: string;
|
||||||
|
high_24h: string;
|
||||||
|
low_24h: string;
|
||||||
|
metadata: Record<string, unknown> | null;
|
||||||
|
captured_at: Date;
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user