feat: 初始提交(仅核心代码,已排除大文件)
This commit is contained in:
@@ -0,0 +1,300 @@
|
||||
const express = require('express')
|
||||
const Database = require('better-sqlite3')
|
||||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
const crypto = require('crypto')
|
||||
|
||||
const dataDir = path.join(process.cwd(), 'data')
|
||||
if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true })
|
||||
const dbPath = path.join(dataDir, 'rebalance.db')
|
||||
const db = new Database(dbPath)
|
||||
|
||||
// Initialize Tables
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS weeks (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
start_date TEXT NOT NULL,
|
||||
end_date TEXT NOT NULL,
|
||||
position_json TEXT NOT NULL,
|
||||
plan_json TEXT NOT NULL,
|
||||
asset_json TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
UNIQUE(start_date, end_date)
|
||||
);`)
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS excluded_stocks (
|
||||
code TEXT PRIMARY KEY,
|
||||
name TEXT
|
||||
);`)
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS fund_shares (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
start_date TEXT NOT NULL,
|
||||
end_date TEXT NOT NULL,
|
||||
data_json TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
UNIQUE(start_date, end_date)
|
||||
);`)
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS fund_summaries (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
start_date TEXT NOT NULL,
|
||||
end_date TEXT NOT NULL,
|
||||
currency TEXT,
|
||||
balance REAL,
|
||||
available REAL,
|
||||
reference_value REAL,
|
||||
asset_value REAL,
|
||||
profit REAL,
|
||||
day_profit REAL,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
UNIQUE(start_date, end_date)
|
||||
);`)
|
||||
|
||||
// Auth Helpers (Duplicated from index.js to avoid refactoring)
|
||||
const readCookieCfg = () => {
|
||||
const cfgPath = path.join(process.cwd(), 'public', 'tools', 'rebalance', 'assets', 'auth_config.json')
|
||||
let name = 'rebalance_gate'
|
||||
let maxAge = 90 * 24 * 3600 * 1000
|
||||
try {
|
||||
if (fs.existsSync(cfgPath)) {
|
||||
const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf-8'))
|
||||
if (cfg.cookieName) name = String(cfg.cookieName)
|
||||
const days = Number(cfg.max_age_days || 0)
|
||||
if (Number.isFinite(days) && days > 0) maxAge = Math.min(days, 365) * 24 * 3600 * 1000
|
||||
}
|
||||
} catch {}
|
||||
return { name, maxAge }
|
||||
}
|
||||
const parseCookie = (cookieStr) => {
|
||||
const out = {}
|
||||
String(cookieStr || '').split(/;\s*/).forEach(p => { const idx = p.indexOf('='); if (idx > 0) out[p.slice(0, idx)] = decodeURIComponent(p.slice(idx + 1)) })
|
||||
return out
|
||||
}
|
||||
|
||||
const checkAuth = (req, res, next) => {
|
||||
try {
|
||||
const cookies = parseCookie(req.headers.cookie || '')
|
||||
const { name, maxAge } = readCookieCfg()
|
||||
if (cookies[name] === '1') {
|
||||
res.cookie(name, '1', { httpOnly: true, sameSite: 'lax', maxAge })
|
||||
return next()
|
||||
}
|
||||
} catch {}
|
||||
return res.status(401).json({ error: 'unauthorized' })
|
||||
}
|
||||
|
||||
const bindRoutes = (app) => {
|
||||
// Rebalance API Routes
|
||||
// Note: These routes are mounted at root level by bindRoutes to match original paths /api/...
|
||||
// OR we can prefix them. The original app used /api/...
|
||||
// Since Toolbox uses /api for other things, we need to be careful.
|
||||
// Rebalance specific routes:
|
||||
// /api/weekly-data
|
||||
// /api/fund-shares
|
||||
// /api/settings/excluded-stocks
|
||||
|
||||
// Auth endpoints for Rebalance frontend compatibility
|
||||
app.get('/auth/handshake', (req, res) => {
|
||||
try {
|
||||
const cookies = parseCookie(req.headers.cookie || '')
|
||||
const { name } = readCookieCfg()
|
||||
if (cookies[name] === '1') return res.json({ ok: true })
|
||||
return res.status(401).json({ error: 'unauthorized' })
|
||||
} catch (e) {
|
||||
res.status(500).json({ ok: false, error: String(e.message || e) })
|
||||
}
|
||||
})
|
||||
|
||||
// We rely on Toolbox's main auth mechanism for setting the cookie.
|
||||
// But if the user hits this endpoint, we can just check if they are already auth'd.
|
||||
app.get('/auth/token', (req, res) => {
|
||||
// This is just a dummy to satisfy Rebalance frontend if it calls it.
|
||||
// In simplified mode, we expect the user to be logged in via Toolbox.
|
||||
const cookies = parseCookie(req.headers.cookie || '')
|
||||
const { name } = readCookieCfg()
|
||||
if (cookies[name] === '1') return res.json({ ok: true })
|
||||
return res.status(401).json({ error: 'unauthorized' })
|
||||
})
|
||||
|
||||
app.get('/api/weekly-data', checkAuth, (req, res) => {
|
||||
const { start, end } = req.query
|
||||
if (!start || !end) return res.status(400).json({ error: 'missing params' })
|
||||
try {
|
||||
const row = db.prepare('SELECT position_json, plan_json, asset_json FROM weeks WHERE start_date=? AND end_date=?').get(start, end)
|
||||
if (!row) return res.status(404).json({ error: 'not found' })
|
||||
res.json({
|
||||
positionData: JSON.parse(row.position_json),
|
||||
planData: JSON.parse(row.plan_json),
|
||||
assetData: JSON.parse(row.asset_json)
|
||||
})
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e && e.message ? e.message : 'db error' })
|
||||
}
|
||||
})
|
||||
|
||||
app.post('/api/weekly-data', checkAuth, (req, res) => {
|
||||
const { weekStart, weekEnd, positionData, planData, assetData, overwrite } = req.body
|
||||
if (!weekStart || !weekEnd || !Array.isArray(positionData) || !Array.isArray(planData) || !Array.isArray(assetData)) {
|
||||
return res.status(400).json({ error: 'invalid payload' })
|
||||
}
|
||||
const now = new Date().toISOString()
|
||||
try {
|
||||
const exists = db.prepare('SELECT id FROM weeks WHERE start_date=? AND end_date=?').get(weekStart, weekEnd)
|
||||
if (exists && !overwrite) return res.status(409).json({ error: 'exists' })
|
||||
if (exists && overwrite) {
|
||||
db.prepare('UPDATE weeks SET position_json=?, plan_json=?, asset_json=?, updated_at=? WHERE id=?')
|
||||
.run(JSON.stringify(positionData), JSON.stringify(planData), JSON.stringify(assetData), now, exists.id)
|
||||
return res.json({ status: 'updated' })
|
||||
}
|
||||
db.prepare('INSERT INTO weeks (start_date, end_date, position_json, plan_json, asset_json, created_at, updated_at) VALUES (?,?,?,?,?,?,?)')
|
||||
.run(weekStart, weekEnd, JSON.stringify(positionData), JSON.stringify(planData), JSON.stringify(assetData), now, now)
|
||||
res.json({ status: 'saved' })
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e && e.message ? e.message : 'db error' })
|
||||
}
|
||||
})
|
||||
|
||||
app.get('/api/fund-shares', checkAuth, (req, res) => {
|
||||
const { start, end } = req.query
|
||||
if (!start || !end) return res.status(400).json({ error: 'missing params' })
|
||||
try {
|
||||
const row = db.prepare('SELECT data_json FROM fund_shares WHERE start_date=? AND end_date=?').get(start, end)
|
||||
if (!row) return res.status(404).json({ error: 'not found' })
|
||||
|
||||
let payload = null
|
||||
try { payload = JSON.parse(row.data_json) } catch (e) { payload = null }
|
||||
let data = []
|
||||
let summaryJson = null
|
||||
if (Array.isArray(payload)) data = payload
|
||||
else if (payload && typeof payload === 'object') { data = payload.data || []; summaryJson = payload.summary || null }
|
||||
|
||||
let summary = null
|
||||
const srow = db.prepare('SELECT currency, balance, available, reference_value, asset_value, profit, day_profit FROM fund_summaries WHERE start_date=? AND end_date=?').get(start, end)
|
||||
if (srow) {
|
||||
summary = {
|
||||
币种: srow.currency || null,
|
||||
余额: typeof srow.balance === 'number' ? srow.balance : null,
|
||||
可用: typeof srow.available === 'number' ? srow.available : null,
|
||||
参考市值: typeof srow.reference_value === 'number' ? srow.reference_value : null,
|
||||
资产: typeof srow.asset_value === 'number' ? srow.asset_value : null,
|
||||
盈亏: typeof srow.profit === 'number' ? srow.profit : null,
|
||||
当日盈亏: typeof srow.day_profit === 'number' ? srow.day_profit : null
|
||||
}
|
||||
}
|
||||
if (!summary) summary = summaryJson || null
|
||||
res.json({ data, summary })
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e && e.message ? e.message : 'db error' })
|
||||
}
|
||||
})
|
||||
|
||||
app.post('/api/fund-shares', checkAuth, (req, res) => {
|
||||
const { weekStart, weekEnd, data, summary, overwrite } = req.body
|
||||
if (!weekStart || !weekEnd || !Array.isArray(data)) {
|
||||
return res.status(400).json({ error: 'invalid payload' })
|
||||
}
|
||||
const now = new Date().toISOString()
|
||||
try {
|
||||
const exists = db.prepare('SELECT id FROM fund_shares WHERE start_date=? AND end_date=?').get(weekStart, weekEnd)
|
||||
if (exists && !overwrite) return res.status(409).json({ error: 'exists' })
|
||||
|
||||
if (exists && overwrite) {
|
||||
db.prepare('UPDATE fund_shares SET data_json=?, updated_at=? WHERE id=?')
|
||||
.run(JSON.stringify(data), now, exists.id)
|
||||
|
||||
// upsert summary
|
||||
if (summary && typeof summary === 'object') {
|
||||
const sexists = db.prepare('SELECT id FROM fund_summaries WHERE start_date=? AND end_date=?').get(weekStart, weekEnd)
|
||||
const vals = [summary.币种 || null, summary.余额 ?? null, summary.可用 ?? null, summary.参考市值 ?? null, summary.资产 ?? null, summary.盈亏 ?? null, summary.当日盈亏 ?? null, now]
|
||||
if (sexists) {
|
||||
db.prepare('UPDATE fund_summaries SET currency=?, balance=?, available=?, reference_value=?, asset_value=?, profit=?, day_profit=?, updated_at=? WHERE id=?')
|
||||
.run(...vals, sexists.id)
|
||||
} else {
|
||||
db.prepare('INSERT INTO fund_summaries (start_date, end_date, currency, balance, available, reference_value, asset_value, profit, day_profit, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?)')
|
||||
.run(weekStart, weekEnd, ...vals, now)
|
||||
}
|
||||
}
|
||||
return res.json({ status: 'updated' })
|
||||
}
|
||||
|
||||
db.prepare('INSERT INTO fund_shares (start_date, end_date, data_json, created_at, updated_at) VALUES (?,?,?,?,?)')
|
||||
.run(weekStart, weekEnd, JSON.stringify(data), now, now)
|
||||
|
||||
if (summary && typeof summary === 'object') {
|
||||
db.prepare('INSERT INTO fund_summaries (start_date, end_date, currency, balance, available, reference_value, asset_value, profit, day_profit, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?)')
|
||||
.run(weekStart, weekEnd, summary.币种 || null, summary.余额 ?? null, summary.可用 ?? null, summary.参考市值 ?? null, summary.资产 ?? null, summary.盈亏 ?? null, summary.当日盈亏 ?? null, now, now)
|
||||
}
|
||||
res.json({ status: 'saved' })
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e && e.message ? e.message : 'db error' })
|
||||
}
|
||||
})
|
||||
|
||||
app.delete('/api/fund-shares', checkAuth, (req, res) => {
|
||||
const { start, end } = req.query
|
||||
let password = null
|
||||
try { password = (req.body && req.body.password) || null } catch (e) {}
|
||||
if (!start || !end) return res.status(400).json({ error: 'missing params' })
|
||||
if (password !== (process.env.SYSTEM_AUTH_PASS_93220 || '')) return res.status(403).json({ error: 'forbidden' })
|
||||
try {
|
||||
db.prepare('DELETE FROM fund_shares WHERE start_date=? AND end_date=?').run(start, end)
|
||||
res.json({ status: 'deleted' })
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e && e.message ? e.message : 'db error' })
|
||||
}
|
||||
})
|
||||
|
||||
app.post('/api/fund-shares/delete', checkAuth, (req, res) => {
|
||||
const { start, end, password } = req.body || {}
|
||||
if (!start || !end) return res.status(400).json({ error: 'missing params' })
|
||||
if (password !== (process.env.SYSTEM_AUTH_PASS_93220 || '')) return res.status(403).json({ error: 'forbidden' })
|
||||
try {
|
||||
db.prepare('DELETE FROM fund_shares WHERE start_date=? AND end_date=?').run(start, end)
|
||||
res.json({ status: 'deleted' })
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e && e.message ? e.message : 'db error' })
|
||||
}
|
||||
})
|
||||
|
||||
app.post('/api/fund-shares/clear-all', checkAuth, (req, res) => {
|
||||
const { password } = req.body || {}
|
||||
if (password !== (process.env.SYSTEM_AUTH_PASS_93220 || '')) return res.status(403).json({ error: 'forbidden' })
|
||||
try {
|
||||
db.exec('DELETE FROM fund_shares')
|
||||
db.exec('DELETE FROM fund_summaries')
|
||||
res.json({ status: 'all_deleted' })
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e && e.message ? e.message : 'db error' })
|
||||
}
|
||||
})
|
||||
|
||||
app.get('/api/settings/excluded-stocks', checkAuth, (req, res) => {
|
||||
try {
|
||||
const rows = db.prepare('SELECT code FROM excluded_stocks').all()
|
||||
res.json({ codes: rows.map((r) => r.code) })
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e && e.message ? e.message : 'db error' })
|
||||
}
|
||||
})
|
||||
|
||||
app.put('/api/settings/excluded-stocks', checkAuth, (req, res) => {
|
||||
const { codes } = req.body
|
||||
if (!Array.isArray(codes)) return res.status(400).json({ error: 'invalid payload' })
|
||||
try {
|
||||
db.transaction(() => {
|
||||
db.exec('DELETE FROM excluded_stocks')
|
||||
const stmt = db.prepare('INSERT OR REPLACE INTO excluded_stocks (code) VALUES (?)')
|
||||
codes.forEach((code) => {
|
||||
if (typeof code === 'string' && code.trim()) {
|
||||
stmt.run(code.trim())
|
||||
}
|
||||
})
|
||||
})()
|
||||
res.json({ status: 'saved', codes })
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: 'db error' })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = { bindRoutes }
|
||||
Reference in New Issue
Block a user