Files
gott/Endpoints/ApiKeysEndpoint.cs
T
Meleki dde0fede7d 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
2026-03-13 12:27:03 +01:00

448 lines
15 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;
using Gott.Extensions;
using Gott.Models;
using Gott.Services;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
namespace Gott.Endpoints;
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}/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);
app.MapGet("/api/admin/api-keys/scan/status", HandleScanStatus);
app.MapGet("/api/admin/api-keys/scanned", HandleGetScanned);
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)
{
if (!ctx.IsAdmin()) { ctx.Response.StatusCode = 403; return; }
var keys = await db.GetApiKeys();
var result = keys.Select(k => new
{
k.KeyId,
k.Provider,
k.Name,
k.ApiKey,
k.IsPublic,
k.Status,
k.Plan,
k.Usage7d,
k.Usage5h,
k.CheckedAt,
k.SourceUrl,
k.CreatedAt,
k.UpdatedAt
});
await ctx.Response.WriteAsJsonAsync(result);
}
private static async Task HandleCreate(HttpContext ctx, DatabaseService db)
{
if (!ctx.IsAdmin()) { ctx.Response.StatusCode = 403; return; }
var body = await JsonSerializer.DeserializeAsync<JsonElement>(ctx.Request.Body);
var provider = body.GetProperty("provider").GetString()?.Trim().ToLower() ?? "";
var name = body.GetProperty("name").GetString()?.Trim() ?? "";
var apiKey = body.GetProperty("apiKey").GetString()?.Trim() ?? "";
if (string.IsNullOrWhiteSpace(provider) || string.IsNullOrWhiteSpace(name) || string.IsNullOrWhiteSpace(apiKey))
{
ctx.Response.StatusCode = 400;
await ctx.Response.WriteAsJsonAsync(new { error = "Provider, Name und API-Key sind erforderlich" });
return;
}
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,
IsPublic = isPublic,
CreatedAt = now,
UpdatedAt = now
};
await db.CreateApiKey(record);
await ctx.Response.WriteAsJsonAsync(new
{
record.KeyId,
record.Provider,
record.Name,
record.ApiKey,
record.IsPublic,
record.CreatedAt
});
}
private static async Task HandleUpdate(HttpContext ctx, DatabaseService db, 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;
}
var body = await JsonSerializer.DeserializeAsync<JsonElement>(ctx.Request.Body);
var name = body.TryGetProperty("name", out var n) ? n.GetString()?.Trim() : null;
var apiKey = body.TryGetProperty("apiKey", out var k) ? k.GetString()?.Trim() : null;
if (string.IsNullOrWhiteSpace(name))
name = existing.Name;
await db.UpdateApiKey(keyId, name, apiKey);
await ctx.Response.WriteAsJsonAsync(new { ok = true });
}
private static async Task HandleTogglePublic(HttpContext ctx, DatabaseService db, 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;
}
var newPublic = !existing.IsPublic;
await db.SetApiKeyPublic(keyId, newPublic);
await ctx.Response.WriteAsJsonAsync(new { ok = true, isPublic = newPublic });
}
private static async Task HandleDelete(HttpContext ctx, DatabaseService db, 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;
}
await db.DeleteApiKey(keyId);
await ctx.Response.WriteAsJsonAsync(new { ok = true });
}
private static async Task HandleCheck(HttpContext ctx, IHttpClientFactory httpFactory, DatabaseService db, 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;
}
if (existing.Provider != "anthropic")
{
ctx.Response.StatusCode = 400;
await ctx.Response.WriteAsJsonAsync(new { error = "Check nur für Anthropic-Keys verfügbar" });
return;
}
var result = await AnthropicKeyChecker.CheckKeyAsync(httpFactory, existing.ApiKey);
var dbStatus = (result.Status == "ok" || result.Status == "usage_limited") ? "valid" : result.Status;
// Metadaten in DB speichern
await db.UpdateApiKeyMeta(keyId, dbStatus, result.Plan, result.Usage7d, result.Usage5h);
await ctx.Response.WriteAsJsonAsync(new
{
status = dbStatus,
message = result.Message,
plan = result.Plan,
result.IsSubscription,
result.Usage7d,
result.Usage5h,
result.Overage,
status7d = result.Status7d,
status5h = result.Status5h,
statusOverage = result.StatusOverage
});
}
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)
{
if (!ctx.IsAdmin()) { ctx.Response.StatusCode = 403; return Task.CompletedTask; }
if (scanner.IsRunning)
{
ctx.Response.StatusCode = 409;
return ctx.Response.WriteAsJsonAsync(new { error = "Scan läuft bereits" });
}
var mode = ctx.Request.Query["mode"].FirstOrDefault() ?? "full";
var validModes = new[] { "quick", "web", "full" };
if (!validModes.Contains(mode)) mode = "full";
scanner.TriggerScan(mode);
return ctx.Response.WriteAsJsonAsync(new { ok = true, message = $"{mode}-Scan gestartet" });
}
private static Task HandleScanStatus(HttpContext ctx, KeyScannerService scanner)
{
if (!ctx.IsAdmin()) { ctx.Response.StatusCode = 403; return Task.CompletedTask; }
return ctx.Response.WriteAsJsonAsync(new
{
running = scanner.IsRunning,
lastRun = scanner.LastRun,
lastResult = scanner.LastResult
});
}
private static async Task HandleGetScanned(HttpContext ctx, DatabaseService db)
{
if (!ctx.IsAdmin()) { ctx.Response.StatusCode = 403; return; }
var keys = await db.GetScannedKeys();
var result = keys.Select(k => new
{
k.ScannedKeyId,
k.Source,
k.Type,
k.SourceUrl,
k.Status,
k.Plan,
k.Usage7d,
k.Usage5h,
k.ScannedAt,
k.CheckedAt,
k.AdoptedApiKeyId
});
await ctx.Response.WriteAsJsonAsync(result);
}
private static async Task HandleRecheck(HttpContext ctx, DatabaseService db, KeyScannerService scanner, string id)
{
if (!ctx.IsAdmin()) { ctx.Response.StatusCode = 403; return; }
var sk = await db.GetScannedKey(id);
if (sk == null)
{
ctx.Response.StatusCode = 404;
await ctx.Response.WriteAsJsonAsync(new { error = "Key nicht gefunden" });
return;
}
await scanner.CheckAndUpdateKey(sk);
var updated = await db.GetScannedKey(id);
await ctx.Response.WriteAsJsonAsync(new
{
ok = true,
updated.Status,
updated.Plan,
updated.Usage7d,
updated.Usage5h,
updated.CheckedAt
});
}
private static async Task HandleAdopt(HttpContext ctx, DatabaseService db, string id)
{
if (!ctx.IsAdmin()) { ctx.Response.StatusCode = 403; return; }
var sk = await db.GetScannedKey(id);
if (sk == null)
{
ctx.Response.StatusCode = 404;
await ctx.Response.WriteAsJsonAsync(new { error = "Key nicht gefunden" });
return;
}
if (sk.AdoptedApiKeyId != null)
{
ctx.Response.StatusCode = 409;
await ctx.Response.WriteAsJsonAsync(new { error = "Key wurde bereits übernommen" });
return;
}
// Prüfen ob Key schon als API-Key existiert
if (await db.ApiKeyExistsInKeys(sk.ApiKey))
{
ctx.Response.StatusCode = 409;
await ctx.Response.WriteAsJsonAsync(new { error = "Key existiert bereits als API-Key" });
return;
}
// Name: user/repo aus URL extrahieren, SourceUrl: vollständige URL
var sourceUrl = !string.IsNullOrWhiteSpace(sk.SourceUrl) ? sk.SourceUrl : null;
var title = $"Scan: {sk.Plan ?? "Unknown"}";
if (sourceUrl != null)
{
var path = sourceUrl.Replace("https://github.com/", "");
var parts = path.Split('/');
if (parts.Length >= 2)
title = parts[0] + "/" + parts[1];
}
var now = DateTime.UtcNow.ToString("o");
var record = new ApiKeyRecord
{
KeyId = Guid.NewGuid().ToString("N"),
Provider = "anthropic",
Name = title,
ApiKey = sk.ApiKey,
IsPublic = false,
Status = sk.Status,
Plan = sk.Plan,
Usage7d = sk.Usage7d,
Usage5h = sk.Usage5h,
CheckedAt = sk.CheckedAt,
SourceUrl = sourceUrl,
CreatedAt = now,
UpdatedAt = now
};
var apiKeyId = await db.CreateApiKey(record);
await db.SetScannedKeyAdopted(id, apiKeyId);
await ctx.Response.WriteAsJsonAsync(new { ok = true, record.KeyId, record.Name });
}
private static async Task HandleDeleteScanned(HttpContext ctx, DatabaseService db, string id)
{
if (!ctx.IsAdmin()) { ctx.Response.StatusCode = 403; return; }
await db.DeleteScannedKey(id);
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
});
}
}