Add admin token override feature for user accounts

- Add tokenOverride field to token usage bucket
- Create POST /api/admin/accounts/tokens endpoint for setting manual token limits
- Update admin accounts page to display token usage and override status
- Add 'Set Tokens' button to manually override user token limits for the month
- Override takes precedence over plan-based limits when set
This commit is contained in:
southseact-3d
2026-02-15 16:19:06 +00:00
parent 9973c3511c
commit dafd3c796d
3 changed files with 108 additions and 3 deletions

View File

@@ -6181,12 +6181,16 @@ function ensureTokenUsageBucket(userId) {
usage: 0,
addOns: 0,
paygBilled: 0,
tokenOverride: null,
};
} else {
const entry = tokenUsage[key];
entry.usage = typeof entry.usage === 'number' ? entry.usage : 0;
entry.addOns = typeof entry.addOns === 'number' ? entry.addOns : 0;
entry.paygBilled = typeof entry.paygBilled === 'number' ? entry.paygBilled : 0;
if (entry.tokenOverride !== null && typeof entry.tokenOverride !== 'number') {
entry.tokenOverride = null;
}
}
return tokenUsage[key];
}
@@ -7008,14 +7012,20 @@ function validateSubscriptionSelection(plan, billingCycle, currency) {
}
function getPlanTokenLimits(plan, userId) {
const bucket = ensureTokenUsageBucket(userId) || { addOns: 0, tokenOverride: null };
// Check for manual token override first
if (typeof bucket.tokenOverride === 'number' && bucket.tokenOverride >= 0) {
return Math.max(0, bucket.tokenOverride);
}
const normalized = normalizePlanSelection(plan) || DEFAULT_PLAN;
const base = (planTokenLimits && typeof planTokenLimits[normalized] === 'number') ? planTokenLimits[normalized] : (PLAN_TOKEN_LIMITS[DEFAULT_PLAN] || 0);
const bucket = ensureTokenUsageBucket(userId) || { addOns: 0 };
return Math.max(0, Number(base || 0) + Number(bucket.addOns || 0));
}
function getTokenUsageSummary(userId, plan) {
const bucket = ensureTokenUsageBucket(userId) || { usage: 0, addOns: 0 };
const bucket = ensureTokenUsageBucket(userId) || { usage: 0, addOns: 0, tokenOverride: null };
const limit = getPlanTokenLimits(plan, userId);
const used = Math.max(0, Number(bucket.usage || 0));
const remaining = limit > 0 ? Math.max(0, limit - used) : 0;
@@ -7027,6 +7037,7 @@ function getTokenUsageSummary(userId, plan) {
percent: limit > 0 ? Math.min(100, parseFloat(((used / limit) * 100).toFixed(1))) : 0,
addOn: Math.max(0, Number(bucket.addOns || 0)),
plan: normalizePlanSelection(plan) || DEFAULT_PLAN,
tokenOverride: bucket.tokenOverride !== undefined ? bucket.tokenOverride : null,
};
}
@@ -16005,6 +16016,39 @@ async function handleAdminAccountDelete(req, res) {
}
}
// Admin account token override endpoint
async function handleAdminAccountTokensPost(req, res) {
const session = requireAdminAuth(req, res);
if (!session) return;
try {
const body = await parseJsonBody(req);
const userId = body.userId;
const tokenAmount = body.tokens;
if (!userId) return sendJson(res, 400, { error: 'User ID is required' });
if (typeof tokenAmount !== 'number' || tokenAmount < 0 || !Number.isFinite(tokenAmount)) {
return sendJson(res, 400, { error: 'Invalid token amount. Must be a non-negative number.' });
}
const user = findUserById(userId);
if (!user) return sendJson(res, 404, { error: 'User not found' });
const bucket = ensureTokenUsageBucket(userId);
if (!bucket) return sendJson(res, 500, { error: 'Failed to access token usage data' });
// Set the token override
bucket.tokenOverride = Math.floor(tokenAmount);
await persistTokenUsage();
const accountData = await serializeAccount(user);
log('Admin set user token override', { userId, tokens: bucket.tokenOverride, admin: ADMIN_USER });
sendJson(res, 200, { ok: true, account: accountData, tokenOverride: bucket.tokenOverride });
} catch (error) {
sendJson(res, 400, { error: error.message || 'Unable to update token limit' });
}
}
// Affiliate management API endpoints
async function handleAdminAffiliatesList(req, res) {
const session = requireAdminAuth(req, res);
@@ -18583,6 +18627,7 @@ async function routeInternal(req, res, url, pathname) {
if (req.method === 'GET' && pathname === '/api/admin/env-config') return handleAdminEnvConfig(req, res);
if (req.method === 'GET' && pathname === '/api/admin/accounts') return handleAdminAccountsList(req, res);
if (req.method === 'POST' && pathname === '/api/admin/accounts/plan') return handleAdminAccountPlanUpdate(req, res);
if (req.method === 'POST' && pathname === '/api/admin/accounts/tokens') return handleAdminAccountTokensPost(req, res);
if (req.method === 'DELETE' && pathname === '/api/admin/accounts') return handleAdminAccountDelete(req, res);
if (req.method === 'GET' && pathname === '/api/admin/affiliates') return handleAdminAffiliatesList(req, res);
if (req.method === 'DELETE' && pathname === '/api/admin/affiliates') return handleAdminAffiliateDelete(req, res);