Repository navigation
Expand file tree
/
Copy pathadmin-api.js
More file actions
326 lines (301 loc) · 12.7 KB
/
Copy pathadmin-api.js
File metadata and controls
326 lines (301 loc) · 12.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
const crypto = require('crypto');
const fs = require('fs');
const axios = require('axios');
const db = require('./db');
const { isBlockedURL, VALID_URL_PATTERN, getOllamaURL } = require('./utils');
const ADMIN_TOKEN = process.env.ADMIN_TOKEN || crypto.randomBytes(32).toString('hex');
if (process.env.ADMIN_TOKEN) {
console.log('ADMIN_TOKEN loaded from environment / .env file');
} else {
console.log('ADMIN_TOKEN env not set — generated random token (valid until restart)');
}
const SERVER_START = new Date().toISOString();
let configCache = { port: 3000, ollamaUrl: 'http://localhost:11434' };
function refreshConfigCache() {
try {
const portData = fs.readFileSync('port.conf', 'utf8').trim();
if (portData) configCache.port = parseInt(portData);
} catch {}
try {
const urlData = fs.readFileSync('ollamaURL.conf', 'utf8').trim();
if (urlData) configCache.ollamaUrl = urlData;
} catch {}
}
function authenticateAdmin(req, res, next) {
const token = req.headers['x-admin-token'];
if (!token) {
return res.status(401).json({ error: 'Admin token required via x-admin-token header' });
}
try {
if (!crypto.timingSafeEqual(Buffer.from(token), Buffer.from(ADMIN_TOKEN))) {
return res.status(401).json({ error: 'Invalid admin token' });
}
} catch {
return res.status(401).json({ error: 'Invalid admin token' });
}
next();
}
function setupAdminRoutes(app) {
app.use('/v1/admin', authenticateAdmin);
app.get('/v1/admin/health', async (req, res) => {
let ollamaReachable = false;
let ollamaError = null;
try {
const url = await getOllamaURL();
await axios.get(`${url}/api/tags`, { timeout: 5000 });
ollamaReachable = true;
} catch (e) {
ollamaError = e.code || e.message;
}
res.json({
status: ollamaReachable ? 'healthy' : 'degraded',
ollama: ollamaReachable ? 'reachable' : 'unreachable',
ollamaError,
serverStart: SERVER_START,
timestamp: new Date().toISOString()
});
});
app.get('/v1/admin/models', async (req, res) => {
try {
const url = await getOllamaURL();
const response = await axios.get(`${url}/api/tags`, { timeout: 5000 });
const models = (response.data.models || []).map((m, i) => ({
name: m.name,
size: m.size,
modifiedAt: m.modified_at,
details: m.details
}));
res.json(models);
} catch (e) {
if (e.code === 'ECONNREFUSED' || e.message?.includes('not found')) {
return res.json([]);
}
console.error('Failed to fetch models from Ollama:', e.message);
res.status(502).json({ error: 'Failed to fetch models from Ollama' });
}
});
app.get('/v1/admin/stats', async (req, res) => {
try {
const totalKeys = await db.get('SELECT COUNT(*) as count FROM apiKeys');
const activeKeys = await db.get('SELECT COUNT(*) as count FROM apiKeys WHERE active = 1');
const totalUsage = await db.get('SELECT COUNT(*) as count FROM apiUsage');
const recentUsage = await db.get("SELECT COUNT(*) as count FROM apiUsage WHERE timestamp >= datetime('now', '-24 hours')");
const uniqueKeysUsed = await db.get('SELECT COUNT(DISTINCT key) as count FROM apiUsage');
const errorCount = await db.get("SELECT COUNT(*) as count FROM apiUsage WHERE timestamp >= datetime('now', '-24 hours')");
const modelUsage = await db.all("SELECT model, COUNT(*) as count FROM apiUsage WHERE model IS NOT NULL AND model != '' GROUP BY model ORDER BY count DESC");
const totalModel = modelUsage.reduce((s, m) => s + m.count, 0);
const modelColors = [
'linear-gradient(90deg, #818cf8, #6366f1)',
'linear-gradient(90deg, #34d399, #10b981)',
'linear-gradient(90deg, #f472b6, #ec4899)',
'linear-gradient(90deg, #fbbf24, #f59e0b)',
'linear-gradient(90deg, #6b7280, #4b5563)',
'linear-gradient(90deg, #60a5fa, #3b82f6)',
'linear-gradient(90deg, #a78bfa, #8b5cf6)',
];
res.json({
totalRequests: totalUsage.count,
activeKeys: activeKeys.count,
totalKeys: totalKeys.count,
recentRequests: recentUsage.count,
uniqueKeysUsed: uniqueKeysUsed.count,
serverStart: SERVER_START,
errorRate: totalUsage.count > 0 ? ((errorCount.count / totalUsage.count) * 100).toFixed(2) : '0.00',
modelUsage: modelUsage.map((m, i) => ({
name: m.model,
usage: totalModel > 0 ? ((m.count / totalModel) * 100).toFixed(1) : '0',
count: m.count,
color: modelColors[i % modelColors.length]
}))
});
} catch (err) {
console.error('Admin API error:', err.message);
res.status(500).json({ error: 'Internal server error' });
}
});
app.get('/v1/admin/keys', async (req, res) => {
try {
const rows = await db.all(`
SELECT k.*, COALESCE(u.usage_count, 0) as usage_count
FROM apiKeys k
LEFT JOIN (SELECT key, COUNT(*) as usage_count FROM apiUsage GROUP BY key) u ON k.key = u.key
ORDER BY k.created_at DESC
`);
res.json(rows);
} catch (err) {
console.error('Admin API error:', err.message);
res.status(500).json({ error: 'Internal server error' });
}
});
app.get('/v1/admin/keys/:key', async (req, res) => {
try {
const row = await db.get('SELECT * FROM apiKeys WHERE key = ?', [req.params.key]);
if (!row) return res.status(404).json({ error: 'API key not found' });
res.json(row);
} catch (err) {
console.error('Admin API error:', err.message);
res.status(500).json({ error: 'Internal server error' });
}
});
app.post('/v1/admin/keys', async (req, res) => {
try {
const { description } = req.body || {};
const apiKey = crypto.randomBytes(20).toString('hex');
if (description) {
await db.run('INSERT INTO apiKeys(key, rate_limit, description) VALUES(?, 10, ?)', [apiKey, description]);
} else {
await db.run('INSERT INTO apiKeys(key, rate_limit) VALUES(?, 10)', [apiKey]);
}
const row = await db.get('SELECT * FROM apiKeys WHERE key = ?', [apiKey]);
res.status(201).json(row);
} catch (err) {
console.error('Admin API error:', err.message);
res.status(500).json({ error: 'Internal server error' });
}
});
app.delete('/v1/admin/keys/:key', async (req, res) => {
try {
const result = await db.run('DELETE FROM apiKeys WHERE key = ?', [req.params.key]);
if (result.changes === 0) return res.status(404).json({ error: 'API key not found' });
res.json({ message: 'API key removed' });
} catch (err) {
console.error('Admin API error:', err.message);
res.status(500).json({ error: 'Internal server error' });
}
});
app.put('/v1/admin/keys/:key/activate', async (req, res) => {
try {
const result = await db.run('UPDATE apiKeys SET active = 1 WHERE key = ?', [req.params.key]);
if (result.changes === 0) return res.status(404).json({ error: 'API key not found' });
res.json({ message: 'API key activated' });
} catch (err) {
console.error('Admin API error:', err.message);
res.status(500).json({ error: 'Internal server error' });
}
});
app.put('/v1/admin/keys/:key/deactivate', async (req, res) => {
try {
const result = await db.run('UPDATE apiKeys SET active = 0 WHERE key = ?', [req.params.key]);
if (result.changes === 0) return res.status(404).json({ error: 'API key not found' });
res.json({ message: 'API key deactivated' });
} catch (err) {
console.error('Admin API error:', err.message);
res.status(500).json({ error: 'Internal server error' });
}
});
app.put('/v1/admin/keys/:key/rate-limit', async (req, res) => {
try {
const { limit } = req.body;
if (!limit || isNaN(limit)) return res.status(400).json({ error: 'Rate limit number is required' });
const result = await db.run('UPDATE apiKeys SET rate_limit = ? WHERE key = ?', [parseInt(limit), req.params.key]);
if (result.changes === 0) return res.status(404).json({ error: 'API key not found' });
const row = await db.get('SELECT * FROM apiKeys WHERE key = ?', [req.params.key]);
res.json(row);
} catch (err) {
console.error('Admin API error:', err.message);
res.status(500).json({ error: 'Internal server error' });
}
});
app.put('/v1/admin/keys/:key/description', async (req, res) => {
try {
const { description } = req.body;
if (description === undefined) return res.status(400).json({ error: 'Description is required' });
const result = await db.run('UPDATE apiKeys SET description = ? WHERE key = ?', [description, req.params.key]);
if (result.changes === 0) return res.status(404).json({ error: 'API key not found' });
const row = await db.get('SELECT * FROM apiKeys WHERE key = ?', [req.params.key]);
res.json(row);
} catch (err) {
console.error('Admin API error:', err.message);
res.status(500).json({ error: 'Internal server error' });
}
});
app.post('/v1/admin/keys/:key/regenerate', async (req, res) => {
try {
const existing = await db.get('SELECT * FROM apiKeys WHERE key = ?', [req.params.key]);
if (!existing) return res.status(404).json({ error: 'API key not found' });
const newKey = crypto.randomBytes(20).toString('hex');
await db.run('BEGIN TRANSACTION');
await db.run('UPDATE apiKeys SET key = ? WHERE key = ?', [newKey, req.params.key]);
const row = await db.get('SELECT * FROM apiKeys WHERE key = ?', [newKey]);
await db.run('COMMIT');
res.json(row);
} catch (err) {
await db.run('ROLLBACK').catch(() => {});
console.error('Admin API error:', err.message);
res.status(500).json({ error: 'Internal server error' });
}
});
app.get('/v1/admin/activity', async (req, res) => {
try {
const rows = await db.all('SELECT * FROM apiUsage ORDER BY timestamp DESC LIMIT 50');
const enriched = await Promise.all(rows.map(async (row) => {
const keyInfo = await db.get('SELECT description FROM apiKeys WHERE key = ?', [row.key]);
return {
...row,
keyLabel: keyInfo?.description || row.key.substring(0, 8) + '...'
};
}));
res.json(enriched);
} catch (err) {
console.error('Admin API error:', err.message);
res.status(500).json({ error: 'Internal server error' });
}
});
app.get('/v1/admin/webhooks', async (req, res) => {
try {
const { key } = req.query;
let rows;
if (key) {
rows = await db.all('SELECT * FROM webhooks WHERE api_key = ?', [key]);
} else {
rows = await db.all('SELECT * FROM webhooks');
}
res.json(rows);
} catch (err) {
console.error('Admin API error:', err.message);
res.status(500).json({ error: 'Internal server error' });
}
});
app.post('/v1/admin/webhooks', async (req, res) => {
try {
const { url, apiKey } = req.body;
if (!url) return res.status(400).json({ error: 'Webhook URL is required' });
if (!apiKey) return res.status(400).json({ error: 'apiKey is required — webhooks must be associated with an API key' });
if (!VALID_URL_PATTERN.test(url)) {
return res.status(400).json({ error: 'Invalid webhook URL. Must be a valid http/https URL.' });
}
if (await isBlockedURL(url)) {
return res.status(400).json({ error: 'URL is not allowed (private/internal network addresses are blocked)' });
}
const keyExists = await db.get('SELECT key FROM apiKeys WHERE key = ?', [apiKey]);
if (!keyExists) return res.status(400).json({ error: 'API key not found' });
await db.run('INSERT INTO webhooks (url, api_key) VALUES (?, ?)', [url, apiKey]);
const rows = await db.all('SELECT * FROM webhooks ORDER BY id DESC LIMIT 1');
res.status(201).json(rows[0]);
} catch (err) {
console.error('Admin API error:', err.message);
res.status(500).json({ error: 'Internal server error' });
}
});
app.delete('/v1/admin/webhooks/:id', async (req, res) => {
try {
const result = await db.run('DELETE FROM webhooks WHERE id = ?', [req.params.id]);
if (result.changes === 0) return res.status(404).json({ error: 'Webhook not found' });
res.json({ message: 'Webhook deleted' });
} catch (err) {
console.error('Admin API error:', err.message);
res.status(500).json({ error: 'Internal server error' });
}
});
app.get('/v1/admin/config', async (req, res) => {
try {
refreshConfigCache();
res.json({ ...configCache, serverStart: SERVER_START });
} catch (err) {
console.error('Admin API error:', err.message);
res.status(500).json({ error: 'Internal server error' });
}
});
refreshConfigCache();
}
module.exports = { setupAdminRoutes, ADMIN_TOKEN };