feat: User-Berechtigungen für Agent-Erstellung und CLI-Nutzung

- Neue Felder CanCreateAgents und CanUseCli im UserRecord (Default: true)
- DB-Migration mit Abwärtskompatibilität (bestehende User behalten alle Rechte)
- Admin-UI: Checkboxen für beide Berechtigungen pro User
- AgentEndpoint: Prüfung auf CanCreateAgents beim Erstellen
- ChatEndpoint: CLI-Fallback nur wenn CanUseCli erlaubt
- Frontend: Neuer-Agent-Button und Agent-Page bei fehlender Berechtigung gesperrt
- Chat: Proaktive Warnung wenn kein API-Key und keine CLI-Berechtigung
- Bug Fix: is_public fehlte im Agent-INSERT (Public Agents erst nach erneutem Speichern sichtbar)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
MelekiandClaude Opus 4.6 committed 2026-03-22 18:47:56 +01:00
1 parent 500c00f948
commit f03fbd298e
13 files changed
+100 -16

No files matched your search

+12 -2
View File
@@ -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
});
+8
View File
@@ -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<JsonElement>(ctx.Request.Body);
var avatarRaw = body.TryGetProperty("avatar", out var av) ? av.GetString() : null;
+3 -1
View File
@@ -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
});
}
+9
View File
@@ -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);
+2
View File
@@ -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()
{
+2 -2
View File
@@ -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;
}
+7 -7
View File
@@ -159,24 +159,24 @@ public partial class DatabaseService
new { userId, settings, now = DateTime.UtcNow.ToString("o") });
}
public async Task<UserRecord> CreateUser(string username, string passwordHash, string displayName, bool isAdmin, string defaultPath = null)
public async Task<UserRecord> 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)
+4
View File
@@ -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,
+12
View File
@@ -677,6 +677,18 @@
<span>Administrator</span>
</label>
</div>
<div class="admin-checkbox-row">
<label class="toggle-label">
<input type="checkbox" id="adminCanCreateAgents" checked>
<span>Agents erstellen</span>
</label>
</div>
<div class="admin-checkbox-row">
<label class="toggle-label">
<input type="checkbox" id="adminCanUseCli" checked>
<span>CLI verwenden (ohne API-Key)</span>
</label>
</div>
<div class="form-actions">
<button id="adminFormSaveBtn">Erstellen</button>
</div>
+16 -2
View File
@@ -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');
+5
View File
@@ -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');
+13
View File
@@ -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());
+7 -2
View File
@@ -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');