Files
gott/Services/DatabaseService.Integrations.cs
MelekiandClaude Opus 4.6 4af9430466 feat: Claude CLI Pfad mit ask_user_choice, Plan-Modus und Tool-Konfiguration
- 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>
2026-03-17 22:25:39 +01:00

624 lines
27 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 });
}
}