librechat-balances/routes/api.js
DJP a9c514d5b1 Add history tracking and user history view
- All balance changes (top-ups, sets, bulk ops, request approvals)
  are now logged to data/history.json
- New "History" tab in admin sidebar
- Search by email to see all changes for a user with totals
- Partial name search shows matching users with summary stats
- Each entry shows action type, amount, source, OMG job #,
  resulting balance, and timestamp

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 13:33:08 -04:00

159 lines
4.4 KiB
JavaScript

const { Router } = require('express');
const balanceService = require('../services/balance');
const history = require('../services/history');
const router = Router();
router.get('/stats', async (req, res) => {
try {
const stats = await balanceService.getStats(req.db);
res.json(stats);
} catch (err) {
res.status(500).json({ error: err.message });
}
});
router.get('/balances', async (req, res) => {
try {
const { page = 1, limit = 50 } = req.query;
const result = await balanceService.getAllBalances(req.db, parseInt(page), parseInt(limit));
res.json(result);
} catch (err) {
res.status(500).json({ error: err.message });
}
});
router.get('/balances/low', async (req, res) => {
try {
const { threshold = 1000000 } = req.query;
const users = await balanceService.getLowBalances(req.db, parseInt(threshold));
res.json({ users });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
router.get('/balances/search', async (req, res) => {
try {
const { q } = req.query;
if (!q || q.length < 2) {
return res.json({ users: [] });
}
const users = await balanceService.searchUsers(req.db, q);
res.json({ users });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// History endpoints
router.get('/history', (req, res) => {
try {
const { email } = req.query;
if (email) {
const entries = history.getByEmail(email);
const totalAdded = entries
.filter(h => h.action === 'add' || h.action === 'approve')
.reduce((sum, h) => sum + (h.amount || 0), 0);
return res.json({ entries, totalAdded });
}
return res.json({ entries: history.getAll() });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
router.get('/history/search', (req, res) => {
try {
const { q } = req.query;
if (!q || q.length < 2) {
return res.json({ users: [] });
}
const users = history.searchByEmail(q);
return res.json({ users });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
router.post('/balances/bulk/add', async (req, res) => {
try {
const { amount } = req.body;
if (typeof amount !== 'number' || amount === 0) {
return res.status(400).json({ error: 'Invalid amount' });
}
const result = await balanceService.addToAll(req.db, amount);
history.log({ email: 'ALL_USERS', action: 'bulk_add', amount, source: 'admin' });
res.json(result);
} catch (err) {
res.status(500).json({ error: err.message });
}
});
router.post('/balances/bulk/set', async (req, res) => {
try {
const { amount } = req.body;
if (typeof amount !== 'number' || amount < 0) {
return res.status(400).json({ error: 'Invalid amount' });
}
const result = await balanceService.setAll(req.db, amount);
history.log({ email: 'ALL_USERS', action: 'bulk_set', amount, source: 'admin' });
res.json(result);
} catch (err) {
res.status(500).json({ error: err.message });
}
});
router.get('/balances/:userId', async (req, res) => {
try {
const balance = await balanceService.getUserBalance(req.db, req.params.userId);
if (!balance) {
return res.status(404).json({ error: 'User not found' });
}
res.json(balance);
} catch (err) {
res.status(500).json({ error: err.message });
}
});
router.post('/balances/:userId/set', async (req, res) => {
try {
const { amount } = req.body;
if (typeof amount !== 'number' || amount < 0) {
return res.status(400).json({ error: 'Invalid amount' });
}
const result = await balanceService.setBalance(req.db, req.params.userId, amount);
history.log({
email: result.email,
action: 'set',
amount,
source: 'admin',
balanceAfter: result.tokenCredits,
});
res.json(result);
} catch (err) {
res.status(500).json({ error: err.message });
}
});
router.post('/balances/:userId/add', async (req, res) => {
try {
const { amount } = req.body;
if (typeof amount !== 'number' || amount === 0) {
return res.status(400).json({ error: 'Invalid amount' });
}
const result = await balanceService.addBalance(req.db, req.params.userId, amount);
history.log({
email: result.email,
action: 'add',
amount,
source: 'admin',
balanceAfter: result.tokenCredits,
});
res.json(result);
} catch (err) {
res.status(500).json({ error: err.message });
}
});
module.exports = router;