When a user submits a credit request, an email is sent to all configured NOTIFY_TO recipients with approve buttons (5M, 10M, 20M), a custom amount option, and a reject button. Each button is a signed HMAC-SHA256 webhook URL that expires after WEBHOOK_TTL_HOURS. Clicking approve from email processes the top-up identical to the admin dashboard — balance update, history log, request status change. Double-approval protection prevents the same link from being used twice. Portal approval still works alongside email approval. New dependencies: nodemailer New files: services/email.js, routes/webhooks.js Modified: server.js, routes/requests.js, .env.example Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
124 lines
4 KiB
JavaScript
124 lines
4 KiB
JavaScript
const { Router } = require('express');
|
|
const requestService = require('../services/requests');
|
|
const balanceService = require('../services/balance');
|
|
const history = require('../services/history');
|
|
const { sendApprovalEmail } = require('../services/email');
|
|
const { MongoClient } = require('mongodb');
|
|
|
|
const publicRouter = Router();
|
|
const adminRouter = Router();
|
|
|
|
// Public: submit a credit request (no auth)
|
|
publicRouter.post('/', (req, res) => {
|
|
try {
|
|
const { email, omgJobNumber } = req.body;
|
|
if (!email || !omgJobNumber) {
|
|
return res.status(400).json({ error: 'Email and OMG job number are required' });
|
|
}
|
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
if (!emailRegex.test(email)) {
|
|
return res.status(400).json({ error: 'Invalid email address' });
|
|
}
|
|
const request = requestService.createRequest(email, omgJobNumber);
|
|
sendApprovalEmail(request).catch(err =>
|
|
console.error('[email] Failed to send approval notification:', err)
|
|
);
|
|
res.json({ success: true, id: request.id });
|
|
} catch (err) {
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
// Admin: get all requests with optional status filter
|
|
adminRouter.get('/', (req, res) => {
|
|
try {
|
|
const { status } = req.query;
|
|
if (status === 'pending') return res.json({ requests: requestService.getPending() });
|
|
if (status === 'processed') return res.json({ requests: requestService.getProcessed() });
|
|
return res.json({ requests: requestService.getAll() });
|
|
} catch (err) {
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
// Admin: get pending count
|
|
adminRouter.get('/count', (req, res) => {
|
|
try {
|
|
const pending = requestService.getPending();
|
|
res.json({ count: pending.length });
|
|
} catch (err) {
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
// Admin: approve a request with a top-up amount
|
|
adminRouter.post('/:id/approve', async (req, res) => {
|
|
try {
|
|
const { amount } = req.body;
|
|
if (typeof amount !== 'number' || amount <= 0) {
|
|
return res.status(400).json({ error: 'Invalid amount' });
|
|
}
|
|
|
|
const requests = requestService.getAll();
|
|
const request = requests.find(r => r.id === req.params.id);
|
|
if (!request) {
|
|
return res.status(404).json({ error: 'Request not found' });
|
|
}
|
|
if (request.status !== 'pending') {
|
|
return res.status(400).json({ error: 'Request already processed' });
|
|
}
|
|
|
|
// Find user by email and top up
|
|
const MONGO_URI = process.env.MONGO_URI || 'mongodb://localhost:27017/LibreChat';
|
|
const client = new MongoClient(MONGO_URI);
|
|
await client.connect();
|
|
const db = client.db();
|
|
|
|
const user = await db.collection('users').findOne(
|
|
{ email: request.email.toLowerCase() },
|
|
{ projection: { _id: 1, email: 1 } },
|
|
);
|
|
|
|
if (!user) {
|
|
await client.close();
|
|
return res.status(404).json({ error: `No LibreChat user found with email: ${request.email}` });
|
|
}
|
|
|
|
const result = await balanceService.addBalance(db, user._id.toString(), amount);
|
|
await client.close();
|
|
|
|
history.log({
|
|
email: request.email,
|
|
action: 'approve',
|
|
amount,
|
|
source: 'request',
|
|
omgJobNumber: request.omgJobNumber,
|
|
balanceAfter: result.tokenCredits,
|
|
});
|
|
|
|
const updated = requestService.updateRequest(req.params.id, 'approved', amount);
|
|
res.json({ success: true, request: updated });
|
|
} catch (err) {
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
// Admin: reject a request
|
|
adminRouter.post('/:id/reject', (req, res) => {
|
|
try {
|
|
const requests = requestService.getAll();
|
|
const request = requests.find(r => r.id === req.params.id);
|
|
if (!request) {
|
|
return res.status(404).json({ error: 'Request not found' });
|
|
}
|
|
if (request.status !== 'pending') {
|
|
return res.status(400).json({ error: 'Request already processed' });
|
|
}
|
|
const updated = requestService.updateRequest(req.params.id, 'rejected', null);
|
|
res.json({ success: true, request: updated });
|
|
} catch (err) {
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
module.exports = { publicRouter, adminRouter };
|