- Ajout support custom API keys (Anthropic/OpenAI) dans localStorage - Backend utilise custom keys si fournis (pas de déduction rate limit) - Tentative fix rate limiter pour /api/llm/limit (skip globalLimiter) - Fix undefined/undefined dans compteur requêtes - Ajout error loop prevention (stop après 5 erreurs) - Reset quotidien à minuit pour compteur LLM Note: Problème 429 persiste, à débugger à la maison 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
341 lines
7.9 KiB
JavaScript
341 lines
7.9 KiB
JavaScript
const jwt = require('jsonwebtoken');
|
|
const bcrypt = require('bcryptjs');
|
|
const { v4: uuidv4 } = require('uuid');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const TOKENS_FILE = path.join(__dirname, 'data', 'tokens.json');
|
|
const JWT_SECRET = process.env.JWT_SECRET || 'confluent-secret-key-change-in-production';
|
|
|
|
// Structure des tokens
|
|
let tokens = {};
|
|
|
|
function loadTokens() {
|
|
try {
|
|
const dataDir = path.join(__dirname, 'data');
|
|
if (!fs.existsSync(dataDir)) {
|
|
fs.mkdirSync(dataDir, { recursive: true });
|
|
}
|
|
|
|
if (fs.existsSync(TOKENS_FILE)) {
|
|
return JSON.parse(fs.readFileSync(TOKENS_FILE, 'utf8'));
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading tokens:', error);
|
|
}
|
|
|
|
// Default: créer un token admin si aucun token n'existe
|
|
const defaultTokens = {
|
|
admin: {
|
|
id: 'admin',
|
|
name: 'Admin',
|
|
role: 'admin',
|
|
apiKey: uuidv4(),
|
|
createdAt: new Date().toISOString(),
|
|
active: true,
|
|
requestsToday: 0,
|
|
dailyLimit: -1, // illimité
|
|
// Tracking des tokens LLM
|
|
llmTokens: {
|
|
totalInput: 0,
|
|
totalOutput: 0,
|
|
today: {
|
|
input: 0,
|
|
output: 0,
|
|
date: new Date().toISOString().split('T')[0]
|
|
}
|
|
},
|
|
// Rate limiting LLM (illimité pour admin)
|
|
llmRequestsToday: 0,
|
|
llmDailyLimit: -1
|
|
}
|
|
};
|
|
|
|
saveTokens(defaultTokens);
|
|
console.log('🔑 Token admin créé:', defaultTokens.admin.apiKey);
|
|
return defaultTokens;
|
|
}
|
|
|
|
function saveTokens(tokensToSave = tokens) {
|
|
try {
|
|
fs.writeFileSync(TOKENS_FILE, JSON.stringify(tokensToSave, null, 2));
|
|
} catch (error) {
|
|
console.error('Error saving tokens:', error);
|
|
}
|
|
}
|
|
|
|
// Middleware d'authentification
|
|
function authenticate(req, res, next) {
|
|
// Routes publiques (GET seulement)
|
|
const publicRoutes = ['/api/lexique', '/api/stats', '/lexique'];
|
|
if (req.method === 'GET' && publicRoutes.some(route => req.path.startsWith(route))) {
|
|
return next();
|
|
}
|
|
|
|
const apiKey = req.headers['x-api-key'] || req.query.apiKey;
|
|
|
|
if (!apiKey) {
|
|
return res.status(401).json({ error: 'API key required' });
|
|
}
|
|
|
|
// Chercher le token
|
|
const token = Object.values(tokens).find(t => t.apiKey === apiKey);
|
|
|
|
if (!token) {
|
|
return res.status(401).json({ error: 'Invalid API key' });
|
|
}
|
|
|
|
if (!token.active) {
|
|
return res.status(403).json({ error: 'Token disabled' });
|
|
}
|
|
|
|
// Vérifier la limite quotidienne
|
|
const today = new Date().toISOString().split('T')[0];
|
|
const tokenToday = token.lastUsed?.split('T')[0];
|
|
|
|
if (tokenToday !== today) {
|
|
token.requestsToday = 0;
|
|
}
|
|
|
|
if (token.dailyLimit > 0 && token.requestsToday >= token.dailyLimit) {
|
|
return res.status(429).json({ error: 'Daily limit reached' });
|
|
}
|
|
|
|
// Mettre à jour les stats
|
|
token.requestsToday++;
|
|
token.lastUsed = new Date().toISOString();
|
|
saveTokens();
|
|
|
|
// Ajouter les infos au req
|
|
req.user = {
|
|
id: token.id,
|
|
name: token.name,
|
|
role: token.role
|
|
};
|
|
|
|
next();
|
|
}
|
|
|
|
// Middleware admin uniquement
|
|
function requireAdmin(req, res, next) {
|
|
if (!req.user || req.user.role !== 'admin') {
|
|
return res.status(403).json({ error: 'Admin access required' });
|
|
}
|
|
next();
|
|
}
|
|
|
|
// Créer un nouveau token
|
|
function createToken(name, role = 'user', dailyLimit = 100) {
|
|
const id = uuidv4();
|
|
const apiKey = uuidv4();
|
|
|
|
tokens[id] = {
|
|
id,
|
|
name,
|
|
role,
|
|
apiKey,
|
|
createdAt: new Date().toISOString(),
|
|
active: true,
|
|
requestsToday: 0,
|
|
dailyLimit,
|
|
// Tracking des tokens LLM
|
|
llmTokens: {
|
|
totalInput: 0,
|
|
totalOutput: 0,
|
|
today: {
|
|
input: 0,
|
|
output: 0,
|
|
date: new Date().toISOString().split('T')[0]
|
|
}
|
|
},
|
|
// Rate limiting LLM
|
|
llmRequestsToday: 0,
|
|
llmDailyLimit: 20
|
|
};
|
|
|
|
saveTokens();
|
|
return tokens[id];
|
|
}
|
|
|
|
// Lister tous les tokens
|
|
function listTokens() {
|
|
return Object.values(tokens).map(t => ({
|
|
id: t.id,
|
|
name: t.name,
|
|
role: t.role,
|
|
apiKey: t.apiKey.substring(0, 8) + '...',
|
|
createdAt: t.createdAt,
|
|
active: t.active,
|
|
requestsToday: t.requestsToday,
|
|
dailyLimit: t.dailyLimit,
|
|
lastUsed: t.lastUsed
|
|
}));
|
|
}
|
|
|
|
// Désactiver un token
|
|
function disableToken(id) {
|
|
if (tokens[id]) {
|
|
tokens[id].active = false;
|
|
saveTokens();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Réactiver un token
|
|
function enableToken(id) {
|
|
if (tokens[id]) {
|
|
tokens[id].active = true;
|
|
saveTokens();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Supprimer un token
|
|
function deleteToken(id) {
|
|
if (id === 'admin') {
|
|
return false; // Ne pas supprimer l'admin
|
|
}
|
|
if (tokens[id]) {
|
|
delete tokens[id];
|
|
saveTokens();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Stats globales
|
|
function getGlobalStats() {
|
|
const tokenList = Object.values(tokens);
|
|
return {
|
|
totalTokens: tokenList.length,
|
|
activeTokens: tokenList.filter(t => t.active).length,
|
|
totalRequestsToday: tokenList.reduce((sum, t) => sum + t.requestsToday, 0)
|
|
};
|
|
}
|
|
|
|
// Vérifier la limite de requêtes LLM
|
|
function checkLLMLimit(apiKey) {
|
|
const token = Object.values(tokens).find(t => t.apiKey === apiKey);
|
|
|
|
if (!token) return { allowed: false, error: 'Invalid API key' };
|
|
|
|
// Initialiser si n'existe pas
|
|
if (token.llmRequestsToday === undefined) {
|
|
token.llmRequestsToday = 0;
|
|
token.llmDailyLimit = token.role === 'admin' ? -1 : 20;
|
|
saveTokens(); // Sauvegarder l'initialisation
|
|
}
|
|
|
|
// Initialiser llmTokens.today.date si n'existe pas
|
|
if (!token.llmTokens) {
|
|
token.llmTokens = {
|
|
totalInput: 0,
|
|
totalOutput: 0,
|
|
today: {
|
|
input: 0,
|
|
output: 0,
|
|
date: new Date().toISOString().split('T')[0]
|
|
}
|
|
};
|
|
saveTokens();
|
|
}
|
|
|
|
const today = new Date().toISOString().split('T')[0];
|
|
|
|
// Reset si changement de jour
|
|
if (token.llmTokens.today.date !== today) {
|
|
token.llmRequestsToday = 0;
|
|
token.llmTokens.today = {
|
|
input: 0,
|
|
output: 0,
|
|
date: today
|
|
};
|
|
saveTokens();
|
|
}
|
|
|
|
// Vérifier la limite (-1 = illimité pour admin)
|
|
if (token.llmDailyLimit > 0 && token.llmRequestsToday >= token.llmDailyLimit) {
|
|
return {
|
|
allowed: false,
|
|
error: 'Daily LLM request limit reached',
|
|
limit: token.llmDailyLimit,
|
|
used: token.llmRequestsToday
|
|
};
|
|
}
|
|
|
|
return {
|
|
allowed: true,
|
|
remaining: token.llmDailyLimit > 0 ? token.llmDailyLimit - token.llmRequestsToday : -1,
|
|
limit: token.llmDailyLimit,
|
|
used: token.llmRequestsToday
|
|
};
|
|
}
|
|
|
|
// Tracker les tokens LLM utilisés
|
|
function trackLLMUsage(apiKey, inputTokens, outputTokens) {
|
|
const token = Object.values(tokens).find(t => t.apiKey === apiKey);
|
|
|
|
if (!token) return false;
|
|
|
|
// Initialiser la structure si elle n'existe pas (tokens existants)
|
|
if (!token.llmTokens) {
|
|
token.llmTokens = {
|
|
totalInput: 0,
|
|
totalOutput: 0,
|
|
today: {
|
|
input: 0,
|
|
output: 0,
|
|
date: new Date().toISOString().split('T')[0]
|
|
}
|
|
};
|
|
}
|
|
|
|
// Initialiser rate limiting LLM si n'existe pas
|
|
if (token.llmRequestsToday === undefined) {
|
|
token.llmRequestsToday = 0;
|
|
token.llmDailyLimit = token.role === 'admin' ? -1 : 20;
|
|
}
|
|
|
|
const today = new Date().toISOString().split('T')[0];
|
|
|
|
// Reset des compteurs quotidiens si changement de jour
|
|
if (token.llmTokens.today.date !== today) {
|
|
token.llmTokens.today = {
|
|
input: 0,
|
|
output: 0,
|
|
date: today
|
|
};
|
|
token.llmRequestsToday = 0; // Reset compteur requêtes LLM
|
|
}
|
|
|
|
// Incrémenter les compteurs
|
|
token.llmTokens.totalInput += inputTokens;
|
|
token.llmTokens.totalOutput += outputTokens;
|
|
token.llmTokens.today.input += inputTokens;
|
|
token.llmTokens.today.output += outputTokens;
|
|
token.llmRequestsToday++;
|
|
|
|
saveTokens();
|
|
return true;
|
|
}
|
|
|
|
// Charger les tokens au démarrage
|
|
tokens = loadTokens();
|
|
|
|
module.exports = {
|
|
authenticate,
|
|
requireAdmin,
|
|
createToken,
|
|
listTokens,
|
|
disableToken,
|
|
enableToken,
|
|
deleteToken,
|
|
getGlobalStats,
|
|
loadTokens,
|
|
trackLLMUsage,
|
|
checkLLMLimit,
|
|
tokens
|
|
};
|