- ClaudeCliService: Marker-basierte ask_user_choice Erkennung im Stream-Parser - AnthropicService: StreamChatViaCli mit allowUserChoice, planMode, webSearch - BuildHistoryContext erweitert für tool_use/tool_result (Choice-Antworten) - Tool-Use Records werden in DB persistiert (Poll-Kompatibilität) - CLI Tools konfigurierbar: Workspace/Plan-Modus/WebSearch/kein Workspace - OAuth DB-Felder für zukünftige Token-Verwaltung Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
624 lines
27 KiB
C#
624 lines
27 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.IO;
|
||
using System.Linq;
|
||
using System.Threading.Tasks;
|
||
using Dapper;
|
||
using Gott.Models;
|
||
using Microsoft.Data.Sqlite;
|
||
|
||
namespace Gott.Services;
|
||
|
||
public partial class DatabaseService
|
||
{
|
||
// ── OpenClaw Import ──
|
||
|
||
public async Task CreateChainForImport(string chainId, string sessionId, string agentId, string model, int inputTokens, int outputTokens, int cacheCreationTokens, int cacheReadTokens, string createdAt)
|
||
{
|
||
using var conn = CreateConnection();
|
||
await conn.ExecuteAsync(@"
|
||
INSERT INTO chains (chain_id, session_id, agent_id, model, input_tokens, output_tokens, cache_creation_tokens, cache_read_tokens, iterations, stop_reason, duration_ms, plan_mode, created_at)
|
||
VALUES (@chainId, @sessionId, @agentId, @model, @inputTokens, @outputTokens, @cacheCreationTokens, @cacheReadTokens, 0, 'end_turn', 0, 0, @createdAt)
|
||
", new { chainId, sessionId, agentId, model, inputTokens, outputTokens, cacheCreationTokens, cacheReadTokens, createdAt });
|
||
}
|
||
|
||
public async Task CreateMessageForImport(string chainId, string role, string type, string content, string createdAt, string tag)
|
||
{
|
||
using var conn = CreateConnection();
|
||
await conn.ExecuteAsync(@"
|
||
INSERT INTO messages (chain_id, role, type, content, is_error, iteration, attachment_count, tag, created_at)
|
||
VALUES (@chainId, @role, @type, @content, 0, 0, 0, @tag, @createdAt)
|
||
", new { chainId, role, type, content, tag, createdAt });
|
||
}
|
||
|
||
public async Task DeleteOpenClawImport(string sessionId)
|
||
{
|
||
using var conn = CreateConnection();
|
||
using var tx = conn.BeginTransaction();
|
||
|
||
// Get all chain_ids that belong to this session and have imported messages
|
||
var importedChainIds = (await conn.QueryAsync<string>(@"
|
||
SELECT DISTINCT c.chain_id
|
||
FROM chains c
|
||
INNER JOIN messages m ON m.chain_id = c.chain_id
|
||
WHERE c.session_id = @sessionId AND m.tag = 'openclaw-import'
|
||
", new { sessionId }, tx)).ToList();
|
||
|
||
if (importedChainIds.Count > 0)
|
||
{
|
||
// Delete messages for these chains
|
||
await conn.ExecuteAsync(
|
||
"DELETE FROM messages WHERE chain_id IN @chainIds",
|
||
new { chainIds = importedChainIds }, tx);
|
||
|
||
// Delete the chains themselves
|
||
await conn.ExecuteAsync(
|
||
"DELETE FROM chains WHERE chain_id IN @chainIds",
|
||
new { chainIds = importedChainIds }, tx);
|
||
}
|
||
|
||
tx.Commit();
|
||
}
|
||
|
||
|
||
// ── Telegram Bots ──
|
||
|
||
private void MigrateTelegramBotsFromSessions(SqliteConnection conn)
|
||
{
|
||
// Prüfe ob es Sessions mit bot_token gibt, die noch kein telegram_bot_id haben
|
||
var sessions = conn.Query<dynamic>(
|
||
"SELECT session_id, user_id, name, telegram_bot_token, telegram_webhook_secret FROM sessions WHERE telegram_bot_token IS NOT NULL AND telegram_bot_token != '' AND (telegram_bot_id IS NULL OR telegram_bot_id = '')").ToList();
|
||
|
||
if (sessions.Count == 0) return;
|
||
|
||
var now = DateTime.UtcNow.ToString("o");
|
||
foreach (var s in sessions)
|
||
{
|
||
string token = s.telegram_bot_token;
|
||
string userId = s.user_id ?? "";
|
||
string sessionName = s.name ?? "Unbenannt";
|
||
string secret = s.telegram_webhook_secret ?? Guid.NewGuid().ToString("N");
|
||
|
||
// Prüfe ob für diesen Token bereits ein Bot existiert
|
||
var existing = conn.QuerySingleOrDefault<dynamic>(
|
||
"SELECT bot_id FROM telegram_bots WHERE bot_token = @token", new { token });
|
||
|
||
string botId;
|
||
if (existing != null)
|
||
{
|
||
botId = existing.bot_id;
|
||
}
|
||
else
|
||
{
|
||
botId = Guid.NewGuid().ToString();
|
||
conn.Execute(@"
|
||
INSERT INTO telegram_bots (bot_id, user_id, name, bot_token, webhook_secret, created_at, updated_at)
|
||
VALUES (@botId, @userId, @name, @token, @secret, @now, @now)",
|
||
new { botId, userId, name = $"Bot von {sessionName}", token, secret, now });
|
||
}
|
||
|
||
conn.Execute(
|
||
"UPDATE sessions SET telegram_bot_id = @botId, updated_at = @now WHERE session_id = @sessionId",
|
||
new { botId, sessionId = (string)s.session_id, now });
|
||
}
|
||
}
|
||
|
||
public async Task<IEnumerable<TelegramBotRecord>> GetTelegramBots(string userId)
|
||
{
|
||
using var conn = CreateConnection();
|
||
return await conn.QueryAsync<TelegramBotRecord>(
|
||
"SELECT * FROM telegram_bots WHERE user_id = @userId ORDER BY created_at DESC",
|
||
new { userId });
|
||
}
|
||
|
||
public async Task<IEnumerable<TelegramBotRecord>> GetAvailableTelegramBots(string userId)
|
||
{
|
||
using var conn = CreateConnection();
|
||
return await conn.QueryAsync<TelegramBotRecord>(
|
||
"SELECT * FROM telegram_bots WHERE user_id = @userId OR is_public = 1 ORDER BY created_at DESC",
|
||
new { userId });
|
||
}
|
||
|
||
public async Task<TelegramBotRecord> GetTelegramBot(string botId)
|
||
{
|
||
using var conn = CreateConnection();
|
||
return await conn.QuerySingleOrDefaultAsync<TelegramBotRecord>(
|
||
"SELECT * FROM telegram_bots WHERE bot_id = @botId",
|
||
new { botId });
|
||
}
|
||
|
||
public async Task<TelegramBotRecord> CreateTelegramBot(string userId, string name, string botToken)
|
||
{
|
||
using var conn = CreateConnection();
|
||
var botId = Guid.NewGuid().ToString();
|
||
var secret = Guid.NewGuid().ToString("N");
|
||
var now = DateTime.UtcNow.ToString("o");
|
||
await conn.ExecuteAsync(@"
|
||
INSERT INTO telegram_bots (bot_id, user_id, name, bot_token, webhook_secret, created_at, updated_at)
|
||
VALUES (@botId, @userId, @name, @botToken, @secret, @now, @now)",
|
||
new { botId, userId, name, botToken, secret, now });
|
||
return await GetTelegramBot(botId);
|
||
}
|
||
|
||
public async Task UpdateTelegramBot(string botId, string name, string botToken, bool isPublic)
|
||
{
|
||
using var conn = CreateConnection();
|
||
var now = DateTime.UtcNow.ToString("o");
|
||
await conn.ExecuteAsync(@"
|
||
UPDATE telegram_bots SET name = @name, bot_token = @botToken, is_public = @isPublic, updated_at = @now
|
||
WHERE bot_id = @botId",
|
||
new { botId, name, botToken, isPublic, now });
|
||
}
|
||
|
||
public async Task DeleteTelegramBot(string botId)
|
||
{
|
||
using var conn = CreateConnection();
|
||
var now = DateTime.UtcNow.ToString("o");
|
||
// Erst alle Session-Verknüpfungen entfernen
|
||
await conn.ExecuteAsync(
|
||
"UPDATE sessions SET telegram_bot_id = NULL, updated_at = @now WHERE telegram_bot_id = @botId",
|
||
new { botId, now });
|
||
await conn.ExecuteAsync("DELETE FROM telegram_bots WHERE bot_id = @botId", new { botId });
|
||
}
|
||
|
||
public async Task<int> GetTelegramBotSessionCount(string botId)
|
||
{
|
||
using var conn = CreateConnection();
|
||
return await conn.ExecuteScalarAsync<int>(
|
||
"SELECT COUNT(*) FROM sessions WHERE telegram_bot_id = @botId AND is_deleted = 0",
|
||
new { botId });
|
||
}
|
||
|
||
public async Task SetSessionTelegramBot(string sessionId, string botId)
|
||
{
|
||
using var conn = CreateConnection();
|
||
var now = DateTime.UtcNow.ToString("o");
|
||
await conn.ExecuteAsync(
|
||
"UPDATE sessions SET telegram_bot_id = @botId, updated_at = @now WHERE session_id = @sessionId",
|
||
new { sessionId, botId = (object)botId ?? DBNull.Value, now });
|
||
}
|
||
|
||
public async Task UpdateSessionTelegramChat(string sessionId, string chatId)
|
||
{
|
||
using var conn = CreateConnection();
|
||
var now = DateTime.UtcNow.ToString("o");
|
||
await conn.ExecuteAsync(@"
|
||
UPDATE sessions SET telegram_chat_id = @chatId, updated_at = @now
|
||
WHERE session_id = @sessionId",
|
||
new { sessionId, chatId, now });
|
||
}
|
||
|
||
public async Task<IEnumerable<SessionRecord>> GetSessionsByTelegramBot(string botId)
|
||
{
|
||
using var conn = CreateConnection();
|
||
return await conn.QueryAsync<SessionRecord>(
|
||
"SELECT * FROM sessions WHERE telegram_bot_id = @botId AND is_deleted = 0",
|
||
new { botId });
|
||
}
|
||
|
||
public async Task<SessionRecord> FindSessionForTelegramChat(string botId, long chatId)
|
||
{
|
||
using var conn = CreateConnection();
|
||
var chatIdStr = chatId.ToString();
|
||
return await conn.QuerySingleOrDefaultAsync<SessionRecord>(
|
||
"SELECT * FROM sessions WHERE telegram_bot_id = @botId AND telegram_chat_id = @chatIdStr AND is_deleted = 0",
|
||
new { botId, chatIdStr });
|
||
}
|
||
|
||
public async Task<IEnumerable<SessionRecord>> FindUnlinkedSessionsForBot(string botId)
|
||
{
|
||
using var conn = CreateConnection();
|
||
return await conn.QueryAsync<SessionRecord>(
|
||
"SELECT * FROM sessions WHERE telegram_bot_id = @botId AND (telegram_chat_id IS NULL OR telegram_chat_id = '') AND is_deleted = 0",
|
||
new { botId });
|
||
}
|
||
|
||
// ── Cronjobs ──
|
||
|
||
public async Task<CronjobRecord> CreateCronjob(CronjobRecord cronjob)
|
||
{
|
||
using var conn = CreateConnection();
|
||
var now = DateTime.UtcNow.ToString("o");
|
||
cronjob.CronjobId = Guid.NewGuid().ToString("N");
|
||
cronjob.CreatedAt = now;
|
||
cronjob.UpdatedAt = now;
|
||
await conn.ExecuteAsync(@"
|
||
INSERT INTO cronjobs (cronjob_id, user_id, session_id, name, cron_expression, prompt, notify_session_id, is_enabled, next_run_at, created_at, updated_at)
|
||
VALUES (@CronjobId, @UserId, @SessionId, @Name, @CronExpression, @Prompt, @NotifySessionId, @IsEnabled, @NextRunAt, @CreatedAt, @UpdatedAt)
|
||
", cronjob);
|
||
return cronjob;
|
||
}
|
||
|
||
public async Task<IEnumerable<CronjobRecord>> GetCronjobs(string userId)
|
||
{
|
||
using var conn = CreateConnection();
|
||
return await conn.QueryAsync<CronjobRecord>(@"
|
||
SELECT c.*, s.name AS session_name, ns.name AS notify_session_name
|
||
FROM cronjobs c
|
||
LEFT JOIN sessions s ON c.session_id = s.session_id
|
||
LEFT JOIN sessions ns ON c.notify_session_id = ns.session_id
|
||
WHERE c.user_id = @userId
|
||
ORDER BY c.name",
|
||
new { userId });
|
||
}
|
||
|
||
public async Task<CronjobRecord> GetCronjob(string cronjobId)
|
||
{
|
||
using var conn = CreateConnection();
|
||
return await conn.QuerySingleOrDefaultAsync<CronjobRecord>(@"
|
||
SELECT c.*, s.name AS session_name, ns.name AS notify_session_name
|
||
FROM cronjobs c
|
||
LEFT JOIN sessions s ON c.session_id = s.session_id
|
||
LEFT JOIN sessions ns ON c.notify_session_id = ns.session_id
|
||
WHERE c.cronjob_id = @cronjobId",
|
||
new { cronjobId });
|
||
}
|
||
|
||
public async Task UpdateCronjob(CronjobRecord cronjob)
|
||
{
|
||
using var conn = CreateConnection();
|
||
cronjob.UpdatedAt = DateTime.UtcNow.ToString("o");
|
||
await conn.ExecuteAsync(@"
|
||
UPDATE cronjobs SET name = @Name, session_id = @SessionId, cron_expression = @CronExpression,
|
||
prompt = @Prompt, notify_session_id = @NotifySessionId, is_enabled = @IsEnabled, next_run_at = @NextRunAt, updated_at = @UpdatedAt
|
||
WHERE cronjob_id = @CronjobId AND user_id = @UserId",
|
||
cronjob);
|
||
}
|
||
|
||
public async Task DeleteCronjob(string cronjobId, string userId)
|
||
{
|
||
using var conn = CreateConnection();
|
||
await conn.ExecuteAsync(
|
||
"DELETE FROM cronjobs WHERE cronjob_id = @cronjobId AND user_id = @userId",
|
||
new { cronjobId, userId });
|
||
}
|
||
|
||
public async Task UpdateCronjobRun(string cronjobId, string status, string error, string nextRunAt)
|
||
{
|
||
using var conn = CreateConnection();
|
||
await conn.ExecuteAsync(@"
|
||
UPDATE cronjobs SET last_run_at = @now, last_run_status = @status,
|
||
last_run_error = @error, next_run_at = @nextRunAt, updated_at = @now
|
||
WHERE cronjob_id = @cronjobId",
|
||
new { cronjobId, status, error, nextRunAt, now = DateTime.UtcNow.ToString("o") });
|
||
}
|
||
|
||
public async Task<IEnumerable<CronjobRecord>> GetDueCronjobs()
|
||
{
|
||
using var conn = CreateConnection();
|
||
var now = DateTime.UtcNow.ToString("o");
|
||
return await conn.QueryAsync<CronjobRecord>(@"
|
||
SELECT c.*, s.name AS session_name, ns.name AS notify_session_name
|
||
FROM cronjobs c
|
||
LEFT JOIN sessions s ON c.session_id = s.session_id
|
||
LEFT JOIN sessions ns ON c.notify_session_id = ns.session_id
|
||
WHERE c.is_enabled = 1 AND c.next_run_at IS NOT NULL AND c.next_run_at <= @now",
|
||
new { now });
|
||
}
|
||
|
||
// ── API Keys ──
|
||
|
||
private void MigrateAnthropicTokenFromConfig(SqliteConnection conn)
|
||
{
|
||
// Nur migrieren wenn noch keine Anthropic-Keys existieren
|
||
var count = conn.ExecuteScalar<int>("SELECT COUNT(*) FROM api_keys WHERE provider = 'anthropic'");
|
||
if (count > 0) return;
|
||
|
||
// Token aus appsettings.Production.json lesen
|
||
var configPath = Path.Combine(Path.GetDirectoryName(_connectionString.Replace("Data Source=", "")) ?? "", "..", "appsettings.Production.json");
|
||
// Normaler Pfad: neben der DB-Datei im übergeordneten Ordner
|
||
var basePath = Path.GetDirectoryName(_connectionString.Replace("Data Source=", ""));
|
||
if (basePath != null)
|
||
{
|
||
var prodPath = Path.Combine(basePath, "..", "appsettings.Production.json");
|
||
if (File.Exists(prodPath))
|
||
{
|
||
try
|
||
{
|
||
var json = File.ReadAllText(prodPath);
|
||
using var doc = System.Text.Json.JsonDocument.Parse(json);
|
||
if (doc.RootElement.TryGetProperty("AnthropicToken", out var tokenEl))
|
||
{
|
||
var token = tokenEl.GetString();
|
||
if (!string.IsNullOrWhiteSpace(token))
|
||
{
|
||
var now = DateTime.UtcNow.ToString("o");
|
||
conn.Execute(@"
|
||
INSERT INTO api_keys (key_id, provider, name, api_key, is_public, created_at, updated_at)
|
||
VALUES (@keyId, 'anthropic', 'Migriert aus Config', @token, 1, @now, @now)",
|
||
new { keyId = Guid.NewGuid().ToString("N"), token, now });
|
||
Console.WriteLine("[Migration] AnthropicToken aus appsettings.Production.json → api_keys migriert");
|
||
}
|
||
}
|
||
}
|
||
catch { /* Config nicht lesbar – ignorieren */ }
|
||
}
|
||
}
|
||
}
|
||
|
||
public async Task<IEnumerable<ApiKeyRecord>> GetApiKeys()
|
||
{
|
||
using var conn = CreateConnection();
|
||
return await conn.QueryAsync<ApiKeyRecord>(
|
||
"SELECT * FROM api_keys ORDER BY provider, name");
|
||
}
|
||
|
||
public async Task<IEnumerable<ApiKeyRecord>> GetApiKeysByProvider(string provider)
|
||
{
|
||
using var conn = CreateConnection();
|
||
return await conn.QueryAsync<ApiKeyRecord>(
|
||
"SELECT * FROM api_keys WHERE provider = @provider ORDER BY name",
|
||
new { provider });
|
||
}
|
||
|
||
public async Task<IEnumerable<ApiKeyRecord>> GetPublicApiKeys()
|
||
{
|
||
using var conn = CreateConnection();
|
||
return await conn.QueryAsync<ApiKeyRecord>(
|
||
"SELECT * FROM api_keys WHERE is_public = 1 ORDER BY provider, name");
|
||
}
|
||
|
||
public async Task<ApiKeyRecord> GetApiKey(string keyId)
|
||
{
|
||
using var conn = CreateConnection();
|
||
return await conn.QuerySingleOrDefaultAsync<ApiKeyRecord>(
|
||
"SELECT * FROM api_keys WHERE key_id = @keyId",
|
||
new { keyId });
|
||
}
|
||
|
||
|
||
public async Task<string> CreateApiKey(ApiKeyRecord record)
|
||
{
|
||
using var conn = CreateConnection();
|
||
await conn.ExecuteAsync(@"
|
||
INSERT INTO api_keys (key_id, provider, name, api_key, is_public, status, plan, usage_7d, usage_5h, checked_at, source_url, created_at, updated_at)
|
||
VALUES (@KeyId, @Provider, @Name, @ApiKey, @IsPublic, @Status, @Plan, @Usage7d, @Usage5h, @CheckedAt, @SourceUrl, @CreatedAt, @UpdatedAt)",
|
||
record);
|
||
return record.KeyId;
|
||
}
|
||
|
||
public async Task UpdateApiKey(string keyId, string name, string apiKey = null)
|
||
{
|
||
using var conn = CreateConnection();
|
||
if (!string.IsNullOrWhiteSpace(apiKey))
|
||
{
|
||
await conn.ExecuteAsync(
|
||
"UPDATE api_keys SET name = @name, api_key = @apiKey, updated_at = @now WHERE key_id = @keyId",
|
||
new { keyId, name, apiKey, now = DateTime.UtcNow.ToString("o") });
|
||
}
|
||
else
|
||
{
|
||
await conn.ExecuteAsync(
|
||
"UPDATE api_keys SET name = @name, updated_at = @now WHERE key_id = @keyId",
|
||
new { keyId, name, now = DateTime.UtcNow.ToString("o") });
|
||
}
|
||
}
|
||
|
||
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 UpdateApiKeyMetaByToken(string apiKey, 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 api_key = @apiKey",
|
||
new { apiKey, status, plan, usage7d, usage5h, now = DateTime.UtcNow.ToString("o") });
|
||
}
|
||
|
||
public async Task SetApiKeyPublic(string keyId, bool isPublic)
|
||
{
|
||
using var conn = CreateConnection();
|
||
await conn.ExecuteAsync(
|
||
"UPDATE api_keys SET is_public = @isPublic, updated_at = @now WHERE key_id = @keyId",
|
||
new { keyId, isPublic, now = DateTime.UtcNow.ToString("o") });
|
||
}
|
||
|
||
public async Task DeleteApiKey(string keyId)
|
||
{
|
||
using var conn = CreateConnection();
|
||
// Scanned-Keys die auf diesen API-Key verweisen zurücksetzen
|
||
await conn.ExecuteAsync(
|
||
"UPDATE scanned_keys SET adopted_api_key_id = NULL WHERE adopted_api_key_id = @keyId",
|
||
new { keyId });
|
||
// Agent-Key-Zuordnungen die auf diesen Key verweisen löschen
|
||
await conn.ExecuteAsync(
|
||
"DELETE FROM agent_api_keys WHERE key_id = @keyId",
|
||
new { keyId });
|
||
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, source_url, type, status, plan, usage_7d, usage_5h, scanned_at, checked_at)
|
||
VALUES (@ScannedKeyId, @ApiKey, @Source, @SourceUrl, @Type, @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<IEnumerable<ScannedKeyRecord>> GetScannedKeysByType(string type)
|
||
{
|
||
using var conn = CreateConnection();
|
||
return await conn.QueryAsync<ScannedKeyRecord>(
|
||
"SELECT * FROM scanned_keys WHERE deleted = 0 AND type = @type ORDER BY scanned_at DESC",
|
||
new { type });
|
||
}
|
||
|
||
public async Task<IEnumerable<dynamic>> GetScannedKeysSummary()
|
||
{
|
||
using var conn = CreateConnection();
|
||
return await conn.QueryAsync(
|
||
@"SELECT type,
|
||
COUNT(*) as total,
|
||
SUM(CASE WHEN adopted_api_key_id IS NOT NULL THEN 1 ELSE 0 END) as adopted,
|
||
SUM(CASE WHEN status = 'valid' AND adopted_api_key_id IS NULL THEN 1 ELSE 0 END) as valid,
|
||
SUM(CASE WHEN status = 'invalid' THEN 1 ELSE 0 END) as invalid,
|
||
SUM(CASE WHEN status = 'unchecked' THEN 1 ELSE 0 END) as unchecked
|
||
FROM scanned_keys WHERE deleted = 0
|
||
GROUP BY type ORDER BY total 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, string 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 });
|
||
}
|
||
|
||
// ── Scanner Logs ──
|
||
|
||
public async Task CreateScannerLog(string type, string message, string scanner = "event", string details = null)
|
||
{
|
||
using var conn = CreateConnection();
|
||
// Sicherstellen dass die details-Spalte existiert (Migration)
|
||
try
|
||
{
|
||
await conn.ExecuteAsync(@"ALTER TABLE scanner_logs ADD COLUMN details TEXT");
|
||
}
|
||
catch { /* Spalte existiert bereits */ }
|
||
|
||
await conn.ExecuteAsync(@"
|
||
INSERT INTO scanner_logs (timestamp, type, message, scanner, details)
|
||
VALUES (@timestamp, @type, @message, @scanner, @details)",
|
||
new { timestamp = DateTime.UtcNow.ToString("o"), type, message, scanner, details });
|
||
}
|
||
|
||
public async Task<IEnumerable<dynamic>> GetScannerLogs(int limit = 200, int offset = 0, string type = null, string scanner = null)
|
||
{
|
||
using var conn = CreateConnection();
|
||
var where = "WHERE 1=1";
|
||
if (!string.IsNullOrEmpty(type)) where += " AND type = @type";
|
||
if (!string.IsNullOrEmpty(scanner)) where += " AND scanner = @scanner";
|
||
|
||
return await conn.QueryAsync($@"
|
||
SELECT id, timestamp, type, message, scanner, details
|
||
FROM scanner_logs {where}
|
||
ORDER BY id DESC
|
||
LIMIT @limit OFFSET @offset",
|
||
new { limit, offset, type, scanner });
|
||
}
|
||
|
||
public async Task<int> GetScannerLogCount(string type = null, string scanner = null)
|
||
{
|
||
using var conn = CreateConnection();
|
||
var where = "WHERE 1=1";
|
||
if (!string.IsNullOrEmpty(type)) where += " AND type = @type";
|
||
if (!string.IsNullOrEmpty(scanner)) where += " AND scanner = @scanner";
|
||
|
||
return await conn.ExecuteScalarAsync<int>(
|
||
$"SELECT COUNT(*) FROM scanner_logs {where}",
|
||
new { type, scanner });
|
||
}
|
||
|
||
// ── OAuth Tokens ──
|
||
|
||
/// <summary>
|
||
/// Erstellt einen neuen API-Key-Eintrag für OAuth-Tokens.
|
||
/// </summary>
|
||
public async Task<string> CreateOAuthApiKey(string provider, string name, string accessToken, string refreshToken, DateTime expiresAt, string oauthProvider)
|
||
{
|
||
using var conn = CreateConnection();
|
||
var keyId = Guid.NewGuid().ToString("N");
|
||
var now = DateTime.UtcNow.ToString("o");
|
||
await conn.ExecuteAsync(@"
|
||
INSERT INTO api_keys (key_id, provider, name, api_key, is_public, status, plan, refresh_token, access_token_expires_at, oauth_provider, created_at, updated_at)
|
||
VALUES (@keyId, @provider, @name, @accessToken, 0, 'valid', NULL, @refreshToken, @expiresAt, @oauthProvider, @now, @now)",
|
||
new { keyId, provider, name, accessToken, refreshToken, expiresAt = expiresAt.ToString("o"), oauthProvider, now });
|
||
return keyId;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Aktualisiert Access-Token und Ablaufzeit eines OAuth-Keys.
|
||
/// </summary>
|
||
public async Task UpdateOAuthTokens(string keyId, string newAccessToken, DateTime newExpiresAt)
|
||
{
|
||
using var conn = CreateConnection();
|
||
var now = DateTime.UtcNow.ToString("o");
|
||
await conn.ExecuteAsync(@"
|
||
UPDATE api_keys SET api_key = @newAccessToken, access_token_expires_at = @expiresAt, updated_at = @now
|
||
WHERE key_id = @keyId",
|
||
new { keyId, newAccessToken, expiresAt = newExpiresAt.ToString("o"), now });
|
||
}
|
||
|
||
/// <summary>
|
||
/// Holt alle OAuth-Keys die bald ablaufen (innerhalb von refreshBeforeMinutes).
|
||
/// </summary>
|
||
public async Task<IEnumerable<ApiKeyRecord>> GetOAuthKeysNeedingRefresh(int refreshBeforeMinutes = 60)
|
||
{
|
||
using var conn = CreateConnection();
|
||
var threshold = DateTime.UtcNow.AddMinutes(refreshBeforeMinutes).ToString("o");
|
||
return await conn.QueryAsync<ApiKeyRecord>(@"
|
||
SELECT * FROM api_keys
|
||
WHERE refresh_token IS NOT NULL AND refresh_token != ''
|
||
AND access_token_expires_at IS NOT NULL AND access_token_expires_at != ''
|
||
AND access_token_expires_at < @threshold",
|
||
new { threshold });
|
||
}
|
||
|
||
/// <summary>
|
||
/// Setzt den Status eines API-Keys auf "expired".
|
||
/// </summary>
|
||
public async Task SetApiKeyExpired(string keyId)
|
||
{
|
||
using var conn = CreateConnection();
|
||
var now = DateTime.UtcNow.ToString("o");
|
||
await conn.ExecuteAsync(
|
||
"UPDATE api_keys SET status = 'expired', updated_at = @now WHERE key_id = @keyId",
|
||
new { keyId, now });
|
||
}
|
||
}
|