diff --git a/Endpoints/AdminEndpoint.cs b/Endpoints/AdminEndpoint.cs index 01f33b7..7e909a0 100644 --- a/Endpoints/AdminEndpoint.cs +++ b/Endpoints/AdminEndpoint.cs @@ -38,6 +38,8 @@ public static class AdminEndpoint displayName = u.DisplayName, isAdmin = u.IsAdmin, defaultPath = u.DefaultPath, + canCreateAgents = u.CanCreateAgents, + canUseCli = u.CanUseCli, createdAt = u.CreatedAt, updatedAt = u.UpdatedAt }); @@ -60,6 +62,8 @@ public static class AdminEndpoint var displayName = body.TryGetProperty("displayName", out var dn) ? dn.GetString() : null; var isAdmin = body.TryGetProperty("isAdmin", out var ia) && ia.GetBoolean(); var defaultPath = body.TryGetProperty("defaultPath", out var dp) ? dp.GetString() : null; + var canCreateAgents = !body.TryGetProperty("canCreateAgents", out var cca) || cca.GetBoolean(); + var canUseCli = !body.TryGetProperty("canUseCli", out var cuc) || cuc.GetBoolean(); if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password)) { @@ -84,7 +88,7 @@ public static class AdminEndpoint } var hash = auth.HashPassword(password); - var user = await db.CreateUser(username, hash, displayName, isAdmin, defaultPath); + var user = await db.CreateUser(username, hash, displayName, isAdmin, defaultPath, canCreateAgents, canUseCli); ctx.Response.StatusCode = 201; await ctx.Response.WriteAsJsonAsync(new @@ -94,6 +98,8 @@ public static class AdminEndpoint displayName = user.DisplayName, isAdmin = user.IsAdmin, defaultPath = user.DefaultPath, + canCreateAgents = user.CanCreateAgents, + canUseCli = user.CanUseCli, createdAt = user.CreatedAt }); } @@ -120,6 +126,8 @@ public static class AdminEndpoint var displayName = body.TryGetProperty("displayName", out var dn) ? dn.GetString() : user.DisplayName; var isAdmin = body.TryGetProperty("isAdmin", out var ia) ? ia.GetBoolean() : user.IsAdmin; var defaultPath = body.TryGetProperty("defaultPath", out var dp) ? dp.GetString() : user.DefaultPath; + var canCreateAgents = body.TryGetProperty("canCreateAgents", out var cca) ? cca.GetBoolean() : user.CanCreateAgents; + var canUseCli = body.TryGetProperty("canUseCli", out var cuc) ? cuc.GetBoolean() : user.CanUseCli; if (string.IsNullOrWhiteSpace(username)) { @@ -137,7 +145,7 @@ public static class AdminEndpoint return; } - await db.UpdateUserAdmin(userId, username, displayName, isAdmin, defaultPath); + await db.UpdateUserAdmin(userId, username, displayName, isAdmin, defaultPath, canCreateAgents, canUseCli); var updated = await db.GetUser(userId); await ctx.Response.WriteAsJsonAsync(new @@ -147,6 +155,8 @@ public static class AdminEndpoint displayName = updated.DisplayName, isAdmin = updated.IsAdmin, defaultPath = updated.DefaultPath, + canCreateAgents = updated.CanCreateAgents, + canUseCli = updated.CanUseCli, createdAt = updated.CreatedAt, updatedAt = updated.UpdatedAt }); diff --git a/Endpoints/AgentEndpoint.cs b/Endpoints/AgentEndpoint.cs index c8f52eb..f2ffda3 100644 --- a/Endpoints/AgentEndpoint.cs +++ b/Endpoints/AgentEndpoint.cs @@ -100,6 +100,14 @@ public static class AgentEndpoint private static async Task HandleCreate(HttpContext ctx, DatabaseService db, ImageService imageService, MemoryService memoryService) { var userId = ctx.GetUserId(); + var user = await db.GetUser(userId); + if (user != null && !user.CanCreateAgents && !user.IsAdmin) + { + ctx.Response.StatusCode = 403; + await ctx.Response.WriteAsJsonAsync(new { error = "Keine Berechtigung zum Erstellen von Agents" }); + return; + } + var body = await JsonSerializer.DeserializeAsync(ctx.Request.Body); var avatarRaw = body.TryGetProperty("avatar", out var av) ? av.GetString() : null; diff --git a/Endpoints/AuthEndpoint.cs b/Endpoints/AuthEndpoint.cs index da1101b..175ac6b 100644 --- a/Endpoints/AuthEndpoint.cs +++ b/Endpoints/AuthEndpoint.cs @@ -95,7 +95,9 @@ public static class AuthEndpoint username = user.Username, displayName = user.DisplayName, isAdmin = user.IsAdmin, - defaultPath = user.DefaultPath + defaultPath = user.DefaultPath, + canCreateAgents = user.CanCreateAgents, + canUseCli = user.CanUseCli }); } diff --git a/Endpoints/ChatEndpoint.cs b/Endpoints/ChatEndpoint.cs index 3a6e070..46905f5 100644 --- a/Endpoints/ChatEndpoint.cs +++ b/Endpoints/ChatEndpoint.cs @@ -243,6 +243,15 @@ public static class ChatEndpoint // Kein API-Key → Claude CLI als Fallback verwenden if (string.IsNullOrEmpty(anthropicToken)) { + // CLI-Berechtigung prüfen + var chatUser = await db.GetUser(userId); + if (chatUser != null && !chatUser.CanUseCli && !chatUser.IsAdmin) + { + await SseHelper.TryWriteSse(res, "error", new { error = "Kein API-Key zugewiesen und keine CLI-Berechtigung. Bitte einen Key zuweisen." }, sseCt); + await SseHelper.TryWriteSse(res, "done", new { chainId = ctx.ChainId }, sseCt); + return; + } + if (anthropic.IsCliAvailable) { await anthropic.StreamChatViaCli(res, ctx.ModelId, ctx.SystemPrompt, messages, ctx.ChainId, workspace: ctx.Workspace, allowUserChoice: ctx.Agent?.AllowUserChoice ?? true, planMode: body.PlanMode, webSearch: !string.IsNullOrEmpty(ctx.WebSearchVersion), sessionId: sessionId, agentId: ctx.AgentId, remoteMgr: remoteMgr, unityMcpUrl: ctx.Session?.UnityMcpUrl, unityMcpToken: ctx.Session?.UnityMcpToken, workCt: workCt, sseCt: sseCt); diff --git a/Models/UserRecord.cs b/Models/UserRecord.cs index aab5b66..a84f09f 100644 --- a/Models/UserRecord.cs +++ b/Models/UserRecord.cs @@ -14,6 +14,8 @@ public class UserRecord public string Settings { get; set; } public string DefaultPath { get; set; } public string SelectedApiKeyId { get; set; } + public bool CanCreateAgents { get; set; } + public bool CanUseCli { get; set; } public string GetEffectivePersona() { diff --git a/Services/DatabaseService.Agents.cs b/Services/DatabaseService.Agents.cs index d206ad5..4b03053 100644 --- a/Services/DatabaseService.Agents.cs +++ b/Services/DatabaseService.Agents.cs @@ -19,8 +19,8 @@ public partial class DatabaseService agent.CreatedAt = now; agent.UpdatedAt = now; await conn.ExecuteAsync(@" - INSERT INTO agents (agent_id, user_id, name, avatar, avatar_small, description, model, tool_choice, custom_commands, auto_commit, use_soul, use_identity, use_user_info, use_memory, use_auto_context, use_github_search, use_serper_search, use_web_search, web_search_version, allow_user_choice, task_instruction, avatar_scale, avatar_offset_x, avatar_offset_y, is_deleted, created_at, updated_at) - VALUES (@AgentId, @UserId, @Name, @Avatar, @AvatarSmall, @Description, @Model, @ToolChoice, @CustomCommands, @AutoCommit, @UseSoul, @UseIdentity, @UseUserInfo, @UseMemory, @UseAutoContext, @UseGitHubSearch, @UseSerperSearch, @UseWebSearch, @WebSearchVersion, @AllowUserChoice, @TaskInstruction, @AvatarScale, @AvatarOffsetX, @AvatarOffsetY, 0, @CreatedAt, @UpdatedAt) + INSERT INTO agents (agent_id, user_id, name, avatar, avatar_small, description, model, tool_choice, custom_commands, auto_commit, use_soul, use_identity, use_user_info, use_memory, use_auto_context, use_github_search, use_serper_search, use_web_search, web_search_version, allow_user_choice, task_instruction, avatar_scale, avatar_offset_x, avatar_offset_y, is_public, is_deleted, created_at, updated_at) + VALUES (@AgentId, @UserId, @Name, @Avatar, @AvatarSmall, @Description, @Model, @ToolChoice, @CustomCommands, @AutoCommit, @UseSoul, @UseIdentity, @UseUserInfo, @UseMemory, @UseAutoContext, @UseGitHubSearch, @UseSerperSearch, @UseWebSearch, @WebSearchVersion, @AllowUserChoice, @TaskInstruction, @AvatarScale, @AvatarOffsetX, @AvatarOffsetY, @IsPublic, 0, @CreatedAt, @UpdatedAt) ", agent); return agent; } diff --git a/Services/DatabaseService.Users.cs b/Services/DatabaseService.Users.cs index 89dc027..c65c372 100644 --- a/Services/DatabaseService.Users.cs +++ b/Services/DatabaseService.Users.cs @@ -159,24 +159,24 @@ public partial class DatabaseService new { userId, settings, now = DateTime.UtcNow.ToString("o") }); } - public async Task CreateUser(string username, string passwordHash, string displayName, bool isAdmin, string defaultPath = null) + public async Task CreateUser(string username, string passwordHash, string displayName, bool isAdmin, string defaultPath = null, bool canCreateAgents = true, bool canUseCli = true) { using var conn = CreateConnection(); var now = DateTime.UtcNow.ToString("o"); var userId = Guid.NewGuid().ToString("N"); await conn.ExecuteAsync(@" - INSERT INTO users (user_id, username, password_hash, display_name, is_admin, default_path, created_at, updated_at) - VALUES (@userId, @username, @passwordHash, @displayName, @isAdmin, @defaultPath, @now, @now)", - new { userId, username, passwordHash, displayName, isAdmin, defaultPath, now }); + INSERT INTO users (user_id, username, password_hash, display_name, is_admin, default_path, can_create_agents, can_use_cli, created_at, updated_at) + VALUES (@userId, @username, @passwordHash, @displayName, @isAdmin, @defaultPath, @canCreateAgents, @canUseCli, @now, @now)", + new { userId, username, passwordHash, displayName, isAdmin, defaultPath, canCreateAgents, canUseCli, now }); return await GetUser(userId); } - public async Task UpdateUserAdmin(string userId, string username, string displayName, bool isAdmin, string defaultPath) + public async Task UpdateUserAdmin(string userId, string username, string displayName, bool isAdmin, string defaultPath, bool canCreateAgents, bool canUseCli) { using var conn = CreateConnection(); await conn.ExecuteAsync( - "UPDATE users SET username = @username, display_name = @displayName, is_admin = @isAdmin, default_path = @defaultPath, updated_at = @now WHERE user_id = @userId", - new { userId, username, displayName, isAdmin, defaultPath, now = DateTime.UtcNow.ToString("o") }); + "UPDATE users SET username = @username, display_name = @displayName, is_admin = @isAdmin, default_path = @defaultPath, can_create_agents = @canCreateAgents, can_use_cli = @canUseCli, updated_at = @now WHERE user_id = @userId", + new { userId, username, displayName, isAdmin, defaultPath, canCreateAgents, canUseCli, now = DateTime.UtcNow.ToString("o") }); } public async Task DeleteUser(string userId) diff --git a/Services/DatabaseService.cs b/Services/DatabaseService.cs index 12b6ae9..e401a88 100644 --- a/Services/DatabaseService.cs +++ b/Services/DatabaseService.cs @@ -367,6 +367,10 @@ public partial class DatabaseService // users: ausgewählter API-Key (legacy, wird durch agent_api_keys ersetzt) TryAddColumn(conn, "users", "selected_api_key_id", "TEXT"); + // users: Berechtigungen (Default: true für Abwärtskompatibilität) + TryAddColumn(conn, "users", "can_create_agents", "INTEGER NOT NULL DEFAULT 1"); + TryAddColumn(conn, "users", "can_use_cli", "INTEGER NOT NULL DEFAULT 1"); + conn.Execute(@" CREATE TABLE IF NOT EXISTS agent_api_keys ( id INTEGER PRIMARY KEY AUTOINCREMENT, diff --git a/wwwroot/index.html b/wwwroot/index.html index b6193ae..a5d725c 100644 --- a/wwwroot/index.html +++ b/wwwroot/index.html @@ -677,6 +677,18 @@ Administrator +
+ +
+
+ +
diff --git a/wwwroot/js/pages/admin-page.js b/wwwroot/js/pages/admin-page.js index 3f3c8b3..34d787a 100644 --- a/wwwroot/js/pages/admin-page.js +++ b/wwwroot/js/pages/admin-page.js @@ -21,6 +21,8 @@ registerPage('admin', { var defaultPathInput = document.getElementById('adminDefaultPath'); var defaultPathBrowse = document.getElementById('adminDefaultPathBrowse'); var isAdminCheckbox = document.getElementById('adminIsAdmin'); + var canCreateAgentsCheckbox = document.getElementById('adminCanCreateAgents'); + var canUseCliCheckbox = document.getElementById('adminCanUseCli'); var saveBtn = document.getElementById('adminFormSaveBtn'); function renderUsers() { @@ -42,6 +44,10 @@ registerPage('admin', { var metaParts = ['@' + user.username]; if (user.isAdmin) metaParts.push('Administrator'); if (user.defaultPath) metaParts.push(user.defaultPath); + var perms = []; + if (user.canCreateAgents === false) perms.push('keine Agents'); + if (user.canUseCli === false) perms.push('keine CLI'); + if (perms.length) metaParts.push(perms.join(', ')); metaSpan.textContent = metaParts.join(' · '); info.appendChild(nameSpan); @@ -107,6 +113,8 @@ registerPage('admin', { passwordInput.required = false; defaultPathInput.value = user.defaultPath || ''; isAdminCheckbox.checked = user.isAdmin; + canCreateAgentsCheckbox.checked = user.canCreateAgents !== false; + canUseCliCheckbox.checked = user.canUseCli !== false; saveBtn.textContent = 'Speichern'; userForm.hidden = false; usernameInput.focus(); @@ -199,6 +207,8 @@ registerPage('admin', { passwordInput.required = true; defaultPathInput.value = ''; isAdminCheckbox.checked = false; + canCreateAgentsCheckbox.checked = true; + canUseCliCheckbox.checked = true; saveBtn.textContent = 'Erstellen'; userForm.hidden = false; usernameInput.focus(); @@ -212,7 +222,9 @@ registerPage('admin', { username: usernameInput.value.trim(), displayName: displayNameInput.value.trim(), defaultPath: defaultPathInput.value.trim() || null, - isAdmin: isAdminCheckbox.checked + isAdmin: isAdminCheckbox.checked, + canCreateAgents: canCreateAgentsCheckbox.checked, + canUseCli: canUseCliCheckbox.checked }; await apiAdminUpdateUser(editingUserId, data); @@ -234,7 +246,9 @@ registerPage('admin', { password: password, displayName: displayNameInput.value.trim(), defaultPath: defaultPathInput.value.trim() || null, - isAdmin: isAdminCheckbox.checked + isAdmin: isAdminCheckbox.checked, + canCreateAgents: canCreateAgentsCheckbox.checked, + canUseCli: canUseCliCheckbox.checked }); showToast('Benutzer erstellt'); diff --git a/wwwroot/js/pages/agent-page.js b/wwwroot/js/pages/agent-page.js index 9e096fa..3bdcaf8 100644 --- a/wwwroot/js/pages/agent-page.js +++ b/wwwroot/js/pages/agent-page.js @@ -9,6 +9,11 @@ registerPage('agent', { const isNew = !agentId; let agent = null; + if (isNew && currentUser && currentUser.canCreateAgents === false && !currentUser.isAdmin) { + showToast('Keine Berechtigung zum Erstellen von Agents', 'error'); + return navigateTo('dashboard'); + } + if (!isNew) { agent = await apiGetAgent(agentId); if (!agent) return navigateTo('dashboard'); diff --git a/wwwroot/js/pages/chat-page.js b/wwwroot/js/pages/chat-page.js index 6a15719..780141f 100644 --- a/wwwroot/js/pages/chat-page.js +++ b/wwwroot/js/pages/chat-page.js @@ -82,6 +82,16 @@ async function _initChatUsageBadge() { } } +async function _checkChatPermissions() { + if (!currentUser || currentUser.isAdmin) return; + if (currentUser.canUseCli !== false) return; // CLI erlaubt → kein Problem + try { + var selectedKey = await apiGetSelectedKey(getCurrentAgentId()); + if (selectedKey && selectedKey.selected) return; // Key vorhanden → kein Problem + addWarningBubble('Kein API-Key zugewiesen und keine CLI-Berechtigung. Nachrichten können nicht verarbeitet werden. Bitte wende dich an einen Admin.'); + } catch(e) { /* ignore */ } +} + async function _refreshChatUsageBadge() { if (!currentUser) return; var container = document.getElementById('chatUsageBadge'); @@ -619,6 +629,9 @@ registerPage('chat', { // Usage Badge laden (Admin only) _initChatUsageBadge(); + // Warnung anzeigen wenn kein Key und keine CLI-Berechtigung + _checkChatPermissions(); + // Nach Font-Loading nochmal scrollen (Schriftart kann Höhe verändern) document.fonts.ready.then(() => { requestAnimationFrame(() => scrollDown()); diff --git a/wwwroot/js/pages/dashboard.js b/wwwroot/js/pages/dashboard.js index 64c01aa..dc334fa 100644 --- a/wwwroot/js/pages/dashboard.js +++ b/wwwroot/js/pages/dashboard.js @@ -149,8 +149,13 @@ registerPage('dashboard', { agentGrid.appendChild(card); } - // Neuer Agent Button - document.getElementById('newAgentBtn').addEventListener('click', () => navigateTo('agent')); + // Neuer Agent Button (nur wenn Berechtigung vorhanden) + var newAgentBtn = document.getElementById('newAgentBtn'); + if (currentUser && currentUser.canCreateAgents === false && !currentUser.isAdmin) { + newAgentBtn.hidden = true; + } else { + newAgentBtn.addEventListener('click', () => navigateTo('agent')); + } // Cronjobs const cronjobGrid = document.getElementById('cronjobCards');