User-spezifische API-Key-Auswahl, globales is_active entfernt
Backend:
- is_active komplett entfernt, durch is_public (Admin-Toggle) ersetzt
- Neues selected_api_key_id auf users-Tabelle
- AnthropicService: globaler _token entfernt, apiToken pro Request
- Token-Durchreichung: StreamChat → ProcessStream → RunAgentChat →
RunPreparer/RunReview/RunFinalizer, ExecuteTool, send_to_session
- ChatEndpoint: User-Key aus DB laden, an StreamChat übergeben
- TelegramService: Key per bot.UserId auflösen
- CronjobService: Key per job.UserId auflösen
- Neue Endpoints: GET /api/api-keys/available, PUT /api/api-keys/select/{keyId},
GET /api/api-keys/selected, PUT /api/admin/api-keys/{keyId}/public,
POST /api/admin/api-keys/check-all
- Program.cs: AnthropicService ohne Token-Parameter
Frontend:
- API-Keys-Page: Public-Toggle (Globe/Lock) statt Activate/Deactivate
- Alle prüfen Button für Batch-Key-Check
- Chat: Key-Auswahl für alle User (nicht nur Admin)
- Dummy-Badge wenn kein Key ausgewählt
- Fallback-Badge bei Key ohne Usage-Daten
- Key-Switch-Modal: apiGetAvailableKeys + apiSelectApiKey
This commit is contained in:
1 parent
cc46cf2bbc
commit
dde0fede7d
18 files changed
+340
-193
No files matched your search
+116
-46
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Text.Json;
|
||||
@@ -15,13 +16,14 @@ public static class ApiKeysEndpoint
|
||||
{
|
||||
public static void MapApiKeysEndpoints(this WebApplication app)
|
||||
{
|
||||
// Admin-Endpoints
|
||||
app.MapGet("/api/admin/api-keys", HandleGetAll);
|
||||
app.MapPost("/api/admin/api-keys", HandleCreate);
|
||||
app.MapPut("/api/admin/api-keys/{keyId}", HandleUpdate);
|
||||
app.MapPut("/api/admin/api-keys/{keyId}/activate", HandleActivate);
|
||||
app.MapPut("/api/admin/api-keys/{keyId}/deactivate", HandleDeactivate);
|
||||
app.MapPut("/api/admin/api-keys/{keyId}/public", HandleTogglePublic);
|
||||
app.MapDelete("/api/admin/api-keys/{keyId}", HandleDelete);
|
||||
app.MapPost("/api/admin/api-keys/{keyId}/check", HandleCheck);
|
||||
app.MapPost("/api/admin/api-keys/check-all", HandleCheckAll);
|
||||
|
||||
// Scan-Endpoints
|
||||
app.MapPost("/api/admin/api-keys/scan", HandleScanTrigger);
|
||||
@@ -30,6 +32,11 @@ public static class ApiKeysEndpoint
|
||||
app.MapPost("/api/admin/api-keys/scanned/{id}/recheck", HandleRecheck);
|
||||
app.MapPost("/api/admin/api-keys/scanned/{id}/adopt", HandleAdopt);
|
||||
app.MapDelete("/api/admin/api-keys/scanned/{id}", HandleDeleteScanned);
|
||||
|
||||
// User-Endpoints (für alle authentifizierten User)
|
||||
app.MapGet("/api/api-keys/available", HandleGetAvailable);
|
||||
app.MapPut("/api/api-keys/select/{keyId}", HandleSelectKey);
|
||||
app.MapGet("/api/api-keys/selected", HandleGetSelected);
|
||||
}
|
||||
|
||||
private static async Task HandleGetAll(HttpContext ctx, DatabaseService db)
|
||||
@@ -43,7 +50,7 @@ public static class ApiKeysEndpoint
|
||||
k.Provider,
|
||||
k.Name,
|
||||
k.ApiKey,
|
||||
k.IsActive,
|
||||
k.IsPublic,
|
||||
k.Status,
|
||||
k.Plan,
|
||||
k.Usage7d,
|
||||
@@ -73,13 +80,15 @@ public static class ApiKeysEndpoint
|
||||
}
|
||||
|
||||
var now = DateTime.UtcNow.ToString("o");
|
||||
var isPublic = body.TryGetProperty("isPublic", out var pub) && pub.GetBoolean();
|
||||
|
||||
var record = new ApiKeyRecord
|
||||
{
|
||||
KeyId = Guid.NewGuid().ToString("N"),
|
||||
Provider = provider,
|
||||
Name = name,
|
||||
ApiKey = apiKey,
|
||||
IsActive = false,
|
||||
IsPublic = isPublic,
|
||||
CreatedAt = now,
|
||||
UpdatedAt = now
|
||||
};
|
||||
@@ -91,12 +100,12 @@ public static class ApiKeysEndpoint
|
||||
record.Provider,
|
||||
record.Name,
|
||||
record.ApiKey,
|
||||
record.IsActive,
|
||||
record.IsPublic,
|
||||
record.CreatedAt
|
||||
});
|
||||
}
|
||||
|
||||
private static async Task HandleUpdate(HttpContext ctx, DatabaseService db, AnthropicService anthropic, string keyId)
|
||||
private static async Task HandleUpdate(HttpContext ctx, DatabaseService db, string keyId)
|
||||
{
|
||||
if (!ctx.IsAdmin()) { ctx.Response.StatusCode = 403; return; }
|
||||
|
||||
@@ -116,15 +125,10 @@ public static class ApiKeysEndpoint
|
||||
name = existing.Name;
|
||||
|
||||
await db.UpdateApiKey(keyId, name, apiKey);
|
||||
|
||||
// Wenn aktiver Key geändert wird → Token im Service aktualisieren
|
||||
if (existing.IsActive && existing.Provider == "anthropic" && !string.IsNullOrWhiteSpace(apiKey))
|
||||
anthropic.UpdateToken(apiKey);
|
||||
|
||||
await ctx.Response.WriteAsJsonAsync(new { ok = true });
|
||||
}
|
||||
|
||||
private static async Task HandleActivate(HttpContext ctx, DatabaseService db, AnthropicService anthropic, string keyId)
|
||||
private static async Task HandleTogglePublic(HttpContext ctx, DatabaseService db, string keyId)
|
||||
{
|
||||
if (!ctx.IsAdmin()) { ctx.Response.StatusCode = 403; return; }
|
||||
|
||||
@@ -136,16 +140,12 @@ public static class ApiKeysEndpoint
|
||||
return;
|
||||
}
|
||||
|
||||
await db.SetActiveApiKey(keyId, existing.Provider);
|
||||
|
||||
// Token im laufenden Service aktualisieren
|
||||
if (existing.Provider == "anthropic")
|
||||
anthropic.UpdateToken(existing.ApiKey);
|
||||
|
||||
await ctx.Response.WriteAsJsonAsync(new { ok = true });
|
||||
var newPublic = !existing.IsPublic;
|
||||
await db.SetApiKeyPublic(keyId, newPublic);
|
||||
await ctx.Response.WriteAsJsonAsync(new { ok = true, isPublic = newPublic });
|
||||
}
|
||||
|
||||
private static async Task HandleDeactivate(HttpContext ctx, DatabaseService db, AnthropicService anthropic, string keyId)
|
||||
private static async Task HandleDelete(HttpContext ctx, DatabaseService db, string keyId)
|
||||
{
|
||||
if (!ctx.IsAdmin()) { ctx.Response.StatusCode = 403; return; }
|
||||
|
||||
@@ -157,31 +157,6 @@ public static class ApiKeysEndpoint
|
||||
return;
|
||||
}
|
||||
|
||||
await db.DeactivateApiKey(keyId);
|
||||
|
||||
// Token im laufenden Service leeren
|
||||
if (existing.Provider == "anthropic")
|
||||
anthropic.UpdateToken("");
|
||||
|
||||
await ctx.Response.WriteAsJsonAsync(new { ok = true });
|
||||
}
|
||||
|
||||
private static async Task HandleDelete(HttpContext ctx, DatabaseService db, AnthropicService anthropic, string keyId)
|
||||
{
|
||||
if (!ctx.IsAdmin()) { ctx.Response.StatusCode = 403; return; }
|
||||
|
||||
var existing = await db.GetApiKey(keyId);
|
||||
if (existing == null)
|
||||
{
|
||||
ctx.Response.StatusCode = 404;
|
||||
await ctx.Response.WriteAsJsonAsync(new { error = "Key nicht gefunden" });
|
||||
return;
|
||||
}
|
||||
|
||||
// Wenn aktiver Key gelöscht wird → Token leeren
|
||||
if (existing.IsActive && existing.Provider == "anthropic")
|
||||
anthropic.UpdateToken("");
|
||||
|
||||
await db.DeleteApiKey(keyId);
|
||||
await ctx.Response.WriteAsJsonAsync(new { ok = true });
|
||||
}
|
||||
@@ -226,6 +201,25 @@ public static class ApiKeysEndpoint
|
||||
});
|
||||
}
|
||||
|
||||
private static async Task HandleCheckAll(HttpContext ctx, IHttpClientFactory httpFactory, DatabaseService db)
|
||||
{
|
||||
if (!ctx.IsAdmin()) { ctx.Response.StatusCode = 403; return; }
|
||||
|
||||
var allKeys = await db.GetApiKeys();
|
||||
var anthropicKeys = allKeys.Where(k => k.Provider == "anthropic").ToList();
|
||||
|
||||
var results = new List<object>();
|
||||
foreach (var key in anthropicKeys)
|
||||
{
|
||||
var result = await AnthropicKeyChecker.CheckKeyAsync(httpFactory, key.ApiKey);
|
||||
var dbStatus = (result.Status == "ok" || result.Status == "usage_limited") ? "valid" : result.Status;
|
||||
await db.UpdateApiKeyMeta(key.KeyId, dbStatus, result.Plan, result.Usage7d, result.Usage5h);
|
||||
results.Add(new { key.KeyId, key.Name, status = dbStatus, result.Plan, result.Usage5h, result.Usage7d });
|
||||
}
|
||||
|
||||
await ctx.Response.WriteAsJsonAsync(new { checked_ = results.Count, results });
|
||||
}
|
||||
|
||||
// ── Scan-Endpoints ──
|
||||
|
||||
private static Task HandleScanTrigger(HttpContext ctx, KeyScannerService scanner)
|
||||
@@ -350,7 +344,7 @@ public static class ApiKeysEndpoint
|
||||
Provider = "anthropic",
|
||||
Name = title,
|
||||
ApiKey = sk.ApiKey,
|
||||
IsActive = false,
|
||||
IsPublic = false,
|
||||
Status = sk.Status,
|
||||
Plan = sk.Plan,
|
||||
Usage7d = sk.Usage7d,
|
||||
@@ -374,4 +368,80 @@ public static class ApiKeysEndpoint
|
||||
await ctx.Response.WriteAsJsonAsync(new { ok = true });
|
||||
}
|
||||
|
||||
// ── User-Endpoints ──
|
||||
|
||||
private static async Task HandleGetAvailable(HttpContext ctx, DatabaseService db)
|
||||
{
|
||||
var userId = ctx.GetUserId();
|
||||
if (userId == null) { ctx.Response.StatusCode = 401; return; }
|
||||
|
||||
var isAdmin = ctx.IsAdmin();
|
||||
var keys = isAdmin ? await db.GetApiKeys() : await db.GetPublicApiKeys();
|
||||
|
||||
var result = keys.Select(k => new
|
||||
{
|
||||
k.KeyId,
|
||||
k.Provider,
|
||||
k.Name,
|
||||
k.IsPublic,
|
||||
k.Status,
|
||||
k.Plan,
|
||||
k.Usage7d,
|
||||
k.Usage5h,
|
||||
k.CheckedAt
|
||||
});
|
||||
await ctx.Response.WriteAsJsonAsync(result);
|
||||
}
|
||||
|
||||
private static async Task HandleSelectKey(HttpContext ctx, DatabaseService db, string keyId)
|
||||
{
|
||||
var userId = ctx.GetUserId();
|
||||
if (userId == null) { ctx.Response.StatusCode = 401; return; }
|
||||
|
||||
var key = await db.GetApiKey(keyId);
|
||||
if (key == null)
|
||||
{
|
||||
ctx.Response.StatusCode = 404;
|
||||
await ctx.Response.WriteAsJsonAsync(new { error = "Key nicht gefunden" });
|
||||
return;
|
||||
}
|
||||
|
||||
// Nicht-Admins dürfen nur public Keys auswählen
|
||||
if (!ctx.IsAdmin() && !key.IsPublic)
|
||||
{
|
||||
ctx.Response.StatusCode = 403;
|
||||
await ctx.Response.WriteAsJsonAsync(new { error = "Key ist nicht öffentlich" });
|
||||
return;
|
||||
}
|
||||
|
||||
await db.SetUserSelectedApiKey(userId, keyId);
|
||||
await ctx.Response.WriteAsJsonAsync(new { ok = true, keyId, name = key.Name });
|
||||
}
|
||||
|
||||
private static async Task HandleGetSelected(HttpContext ctx, DatabaseService db)
|
||||
{
|
||||
var userId = ctx.GetUserId();
|
||||
if (userId == null) { ctx.Response.StatusCode = 401; return; }
|
||||
|
||||
var key = await db.GetUserSelectedApiKey(userId);
|
||||
if (key == null)
|
||||
{
|
||||
await ctx.Response.WriteAsJsonAsync(new { selected = false });
|
||||
return;
|
||||
}
|
||||
|
||||
await ctx.Response.WriteAsJsonAsync(new
|
||||
{
|
||||
selected = true,
|
||||
keyId = key.KeyId,
|
||||
provider = key.Provider,
|
||||
name = key.Name,
|
||||
isPublic = key.IsPublic,
|
||||
status = key.Status,
|
||||
plan = key.Plan,
|
||||
usage5h = key.Usage5h,
|
||||
usage7d = key.Usage7d
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -51,6 +51,16 @@ public static class ChatEndpoint
|
||||
}
|
||||
|
||||
var userId = req.HttpContext.GetUserId();
|
||||
|
||||
// API-Key des Users auflösen
|
||||
var userApiKey = await db.GetUserSelectedApiKey(userId);
|
||||
if (userApiKey == null)
|
||||
{
|
||||
res.StatusCode = 400;
|
||||
await res.WriteAsJsonAsync(new { error = "Kein API-Key ausgewählt. Bitte wähle einen Key aus." });
|
||||
return;
|
||||
}
|
||||
|
||||
var maxIterations = config.GetValue<int>("MaxIterations", 100);
|
||||
|
||||
var hasRemote = remoteMgr.GetBySessionId(sessionId) != null;
|
||||
@@ -142,7 +152,7 @@ public static class ChatEndpoint
|
||||
var preparerHistory = await chatService.BuildHistoryFromDb(sessionId, includeTag: "preparer", promptBlocks: ctx.PromptBlocks);
|
||||
|
||||
var preparerResult = await anthropic.RunPreparer(
|
||||
res, ctx.Preparer, preparerHistory, ctx.ChainId, ctx.Workspace, sessionId, cancellationToken);
|
||||
res, ctx.Preparer, preparerHistory, ctx.ChainId, ctx.Workspace, sessionId, userApiKey.ApiKey, cancellationToken);
|
||||
|
||||
if (!preparerResult.ShouldForward)
|
||||
{
|
||||
@@ -216,7 +226,7 @@ public static class ChatEndpoint
|
||||
else
|
||||
{
|
||||
// Standard: Anthropic
|
||||
await anthropic.StreamChat(res, ctx.ModelId, ctx.SystemPrompt, messages, ctx.ChainId, ctx.Workspace, sessionId, agentId: ctx.AgentId, userId: userId, useMemory: ctx.UseMemory, webSearchVersion: ctx.WebSearchVersion, maxIterations: ctx.MaxIterations, toolChoice: effectiveToolChoice, review: ctx.Review, finalizer: ctx.Finalizer, allowUserChoice: ctx.Agent?.AllowUserChoice ?? true, planMode: ctx.PlanMode, useAutoContext: ctx.Agent?.UseAutoContext ?? true, useGitHubSearch: ctx.Agent?.UseGitHubSearch ?? false, useSerperSearch: ctx.Agent?.UseSerperSearch ?? false, workCt: workCt, hardCt: hardCt, sseCt: sseCt, remoteMgr: remoteMgr);
|
||||
await anthropic.StreamChat(res, ctx.ModelId, ctx.SystemPrompt, messages, ctx.ChainId, ctx.Workspace, apiToken: userApiKey.ApiKey, sessionId: sessionId, agentId: ctx.AgentId, userId: userId, useMemory: ctx.UseMemory, webSearchVersion: ctx.WebSearchVersion, maxIterations: ctx.MaxIterations, toolChoice: effectiveToolChoice, review: ctx.Review, finalizer: ctx.Finalizer, allowUserChoice: ctx.Agent?.AllowUserChoice ?? true, planMode: ctx.PlanMode, useAutoContext: ctx.Agent?.UseAutoContext ?? true, useGitHubSearch: ctx.Agent?.UseGitHubSearch ?? false, useSerperSearch: ctx.Agent?.UseSerperSearch ?? false, workCt: workCt, hardCt: hardCt, sseCt: sseCt, remoteMgr: remoteMgr);
|
||||
}
|
||||
}
|
||||
finally
|
||||
|
||||
Reference in new issue
Block a user