feat: API Key pro Agent statt pro User

- Neue Tabelle agent_api_keys (user_id, agent_id, key_id)
- Migration: bestehende selected_api_key_id auf alle Agents verteilt
- Backend: GetAgentApiKeyRecord(userId, agentId) in Chat, Telegram, Cronjobs
- Endpoints: select/selected mit ?agentId Query-Parameter
- Frontend: currentAgentId aus Session, alle Key-Calls agent-aware
- DeleteApiKey bereinigt agent_api_keys statt users.selected_api_key_id
This commit is contained in:
Meleki committed 2026-03-14 21:41:27 +01:00
1 parent b04b1a2cf9
commit 1effc6d00f
10 files changed
+128 -43

No files matched your search

+6 -6
View File
@@ -86,10 +86,10 @@ public class CronjobService : BackgroundService
if (ctx == null)
throw new Exception("Session oder Agent nicht gefunden / nicht konfiguriert");
// API-Key des Cronjob-Owners auflösen
var userApiKey = await _db.GetUserSelectedApiKey(job.UserId);
// API-Key des Cronjob-Owners für diesen Agent auflösen
var userApiKey = await _db.GetAgentApiKeyRecord(job.UserId, ctx.AgentId);
if (userApiKey == null)
throw new Exception($"Kein API-Key für User {job.UserId} ausgewählt");
throw new Exception($"Kein API-Key für Agent {ctx.AgentId} zugewiesen (User {job.UserId})");
ctx.ApiToken = userApiKey.ApiKey;
// History aus DB laden
@@ -140,11 +140,11 @@ public class CronjobService : BackgroundService
return;
}
// API-Key des Cronjob-Owners auflösen
var notifyApiKey = await _db.GetUserSelectedApiKey(job.UserId);
// API-Key des Cronjob-Owners für Notify-Agent auflösen
var notifyApiKey = await _db.GetAgentApiKeyRecord(job.UserId, notifyCtx.AgentId);
if (notifyApiKey == null)
{
_logger.LogWarning("Kein API-Key für User {UserId} (Cronjob '{Name}' Notification)", job.UserId, job.Name);
_logger.LogWarning("Kein API-Key für Agent {AgentId} (Cronjob '{Name}' Notification)", notifyCtx.AgentId, job.Name);
return;
}
notifyCtx.ApiToken = notifyApiKey.ApiKey;
+2 -2
View File
@@ -427,9 +427,9 @@ public partial class DatabaseService
await conn.ExecuteAsync(
"UPDATE scanned_keys SET adopted_api_key_id = NULL WHERE adopted_api_key_id = @keyId",
new { keyId });
// User die diesen Key ausgewählt haben zurücksetzen
// Agent-Key-Zuordnungen die auf diesen Key verweisen löschen
await conn.ExecuteAsync(
"UPDATE users SET selected_api_key_id = NULL WHERE selected_api_key_id = @keyId",
"DELETE FROM agent_api_keys WHERE key_id = @keyId",
new { keyId });
await conn.ExecuteAsync("DELETE FROM api_keys WHERE key_id = @keyId", new { keyId });
}
+23 -12
View File
@@ -187,32 +187,43 @@ public partial class DatabaseService
await conn.ExecuteAsync("DELETE FROM users WHERE user_id = @userId", new { userId });
}
// ── User API-Key Auswahl ──
// ── Agent API-Key Zuordnung ──
public async Task SetUserSelectedApiKey(string userId, string keyId)
public async Task SetAgentApiKey(string userId, string agentId, string keyId)
{
using var conn = CreateConnection();
await conn.ExecuteAsync(
"UPDATE users SET selected_api_key_id = @keyId, updated_at = @now WHERE user_id = @userId",
new { userId, keyId = (object)keyId ?? DBNull.Value, now = DateTime.UtcNow.ToString("o") });
if (string.IsNullOrEmpty(keyId))
{
await conn.ExecuteAsync(
"DELETE FROM agent_api_keys WHERE user_id = @userId AND agent_id = @agentId",
new { userId, agentId });
}
else
{
await conn.ExecuteAsync(@"
INSERT INTO agent_api_keys (user_id, agent_id, key_id)
VALUES (@userId, @agentId, @keyId)
ON CONFLICT(user_id, agent_id) DO UPDATE SET key_id = @keyId",
new { userId, agentId, keyId });
}
}
public async Task<string> GetUserSelectedApiKeyId(string userId)
public async Task<string> GetAgentApiKeyId(string userId, string agentId)
{
using var conn = CreateConnection();
return await conn.QuerySingleOrDefaultAsync<string>(
"SELECT selected_api_key_id FROM users WHERE user_id = @userId",
new { userId });
"SELECT key_id FROM agent_api_keys WHERE user_id = @userId AND agent_id = @agentId",
new { userId, agentId });
}
public async Task<ApiKeyRecord> GetUserSelectedApiKey(string userId)
public async Task<ApiKeyRecord> GetAgentApiKeyRecord(string userId, string agentId)
{
using var conn = CreateConnection();
return await conn.QuerySingleOrDefaultAsync<ApiKeyRecord>(@"
SELECT k.* FROM api_keys k
INNER JOIN users u ON u.selected_api_key_id = k.key_id
WHERE u.user_id = @userId",
new { userId });
INNER JOIN agent_api_keys a ON a.key_id = k.key_id
WHERE a.user_id = @userId AND a.agent_id = @agentId",
new { userId, agentId });
}
/// <summary>
+52 -1
View File
@@ -357,9 +357,23 @@ public partial class DatabaseService
TryAddColumn(conn, "api_keys", "source_url", "TEXT");
TryAddColumn(conn, "api_keys", "is_public", "INTEGER NOT NULL DEFAULT 0");
// users: ausgewählter API-Key
// users: ausgewählter API-Key (legacy, wird durch agent_api_keys ersetzt)
TryAddColumn(conn, "users", "selected_api_key_id", "TEXT");
conn.Execute(@"
CREATE TABLE IF NOT EXISTS agent_api_keys (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
agent_id TEXT NOT NULL,
key_id TEXT NOT NULL,
UNIQUE(user_id, agent_id)
);
CREATE INDEX IF NOT EXISTS idx_agent_api_keys_user ON agent_api_keys(user_id);
");
// Migration: selected_api_key_id → agent_api_keys (einmalig)
MigrateSelectedApiKeysToAgentKeys(conn);
// Migration: Bestehende PNG/JPEG-Avatare/Images zu WebP konvertieren
MigrateImagesToWebP(conn);
@@ -367,6 +381,43 @@ public partial class DatabaseService
MigrateImageThumbnails(conn);
}
private void MigrateSelectedApiKeysToAgentKeys(SqliteConnection conn)
{
// Nur migrieren wenn agent_api_keys leer ist und users mit selected_api_key_id existieren
var count = conn.ExecuteScalar<int>("SELECT COUNT(*) FROM agent_api_keys");
if (count > 0) return;
var usersWithKeys = conn.Query<(string UserId, string SelectedApiKeyId)>(
"SELECT user_id, selected_api_key_id FROM users WHERE selected_api_key_id IS NOT NULL AND selected_api_key_id != ''")
.ToList();
foreach (var (userId, keyId) in usersWithKeys)
{
// Alle Agents des Users + öffentliche Agents
var agentIds = conn.Query<string>(
"SELECT agent_id FROM agents WHERE user_id = @userId OR is_public = 1",
new { userId }).ToList();
// Alle Sessions des Users mit Agent → deren Agents auch berücksichtigen
var sessionAgentIds = conn.Query<string>(
"SELECT DISTINCT agent_id FROM sessions WHERE user_id = @userId AND agent_id IS NOT NULL AND agent_id != ''",
new { userId }).ToList();
var allAgentIds = agentIds.Union(sessionAgentIds).Distinct().ToList();
foreach (var agentId in allAgentIds)
{
try
{
conn.Execute(
"INSERT OR IGNORE INTO agent_api_keys (user_id, agent_id, key_id) VALUES (@userId, @agentId, @keyId)",
new { userId, agentId, keyId });
}
catch { /* UNIQUE constraint – already exists */ }
}
}
}
private void MigrateImagesToWebP(SqliteConnection conn)
{
const int webPQuality = 80;
+3 -3
View File
@@ -168,11 +168,11 @@ public class TelegramService
return;
}
// API-Key des Bot-Owners auflösen
var userApiKey = await _db.GetUserSelectedApiKey(botUserId);
// API-Key des Bot-Owners für diesen Agent auflösen
var userApiKey = await _db.GetAgentApiKeyRecord(botUserId, ctx.AgentId);
if (userApiKey == null)
{
await SendMessage(chatId, botToken, "❌ Kein API-Key ausgewählt. Der Bot-Besitzer muss im Web-UI einen Key auswählen.");
await SendMessage(chatId, botToken, "❌ Kein API-Key für diesen Agent zugewiesen. Der Bot-Besitzer muss im Web-UI einen Key zuweisen.");
return;
}
ctx.ApiToken = userApiKey.ApiKey;