Files
gott/Endpoints/ApiKeysEndpoint.cs
T
Meleki 22d966b830 feat: Serper.dev Google-Dork-Integration für Key-Scanner
- ScanSerper() durchsucht Google via Serper.dev nach geleakten API-Keys
- Exakte Phrase-Suche pro KeyPattern, bis zu 100 Ergebnisse pro Query
- Keys aus Snippets und Titles via Regex extrahiert
- Source-Feld (github/serper/gist/pastebin) in ScannedKeyRecord + DB-Migration
- RunScan() kombiniert GitHub + Serper mit Deduplizierung
- Service startet auch wenn nur SerperApiKey konfiguriert ist
- Frontend: Source-Icons und verkürzte URLs für Nicht-GitHub-Quellen
- Config: SerperApiKey in appsettings.json
2026-03-13 06:26:43 +01:00

373 lines
13 KiB
C#

using System;
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)
{
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.MapDelete("/api/admin/api-keys/{keyId}", HandleDelete);
app.MapPost("/api/admin/api-keys/{keyId}/check", HandleCheck);
// 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);
}
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.IsActive,
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 record = new ApiKeyRecord
{
KeyId = Guid.NewGuid().ToString("N"),
Provider = provider,
Name = name,
ApiKey = apiKey,
IsActive = false,
CreatedAt = now,
UpdatedAt = now
};
await db.CreateApiKey(record);
await ctx.Response.WriteAsJsonAsync(new
{
record.KeyId,
record.Provider,
record.Name,
record.ApiKey,
record.IsActive,
record.CreatedAt
});
}
private static async Task HandleUpdate(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;
}
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);
// 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)
{
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.SetActiveApiKey(keyId, existing.Provider);
// Token im laufenden Service aktualisieren
if (existing.Provider == "anthropic")
anthropic.UpdateToken(existing.ApiKey);
await ctx.Response.WriteAsJsonAsync(new { ok = true });
}
private static async Task HandleDeactivate(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;
}
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 });
}
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
});
}
// ── 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" });
}
scanner.TriggerScan();
return ctx.Response.WriteAsJsonAsync(new { ok = true, message = "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.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,
IsActive = 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 });
}
}