feat: User-Zuordnung für Sessions und Agents
- UserId-Feld zu SessionRecord und AgentRecord hinzugefügt - DB-Migration: user_id Spalte + bestehende Daten dem Admin zuweisen - Alle Endpoints filtern/prüfen nach UserId des eingeloggten Users - HttpContextExtensions.GetUserId() Helper für konsistenten Zugriff - Betrifft: Agent, Session, Chat, History, Workspace, Skill Endpoints - Sessions/Agents anderer User sind nicht sichtbar/zugreifbar
This commit is contained in:
1 parent
9a59ae686d
commit
7614141760
10 files changed
+123
-45
No files matched your search
@@ -1,5 +1,6 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Gott.Extensions;
|
||||||
using Gott.Models;
|
using Gott.Models;
|
||||||
using Gott.Services;
|
using Gott.Services;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
@@ -21,7 +22,8 @@ public static class AgentEndpoint
|
|||||||
|
|
||||||
private static async Task HandleList(HttpContext ctx, DatabaseService db)
|
private static async Task HandleList(HttpContext ctx, DatabaseService db)
|
||||||
{
|
{
|
||||||
var agents = await db.GetAgents();
|
var userId = ctx.GetUserId();
|
||||||
|
var agents = await db.GetAgents(userId);
|
||||||
await ctx.Response.WriteAsJsonAsync(agents);
|
await ctx.Response.WriteAsJsonAsync(agents);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,13 +33,14 @@ public static class AgentEndpoint
|
|||||||
if (string.IsNullOrEmpty(agentId)) { ctx.Response.StatusCode = 400; return; }
|
if (string.IsNullOrEmpty(agentId)) { ctx.Response.StatusCode = 400; return; }
|
||||||
|
|
||||||
var agent = await db.GetAgent(agentId);
|
var agent = await db.GetAgent(agentId);
|
||||||
if (agent == null) { ctx.Response.StatusCode = 404; return; }
|
if (agent == null || agent.UserId != ctx.GetUserId()) { ctx.Response.StatusCode = 404; return; }
|
||||||
|
|
||||||
await ctx.Response.WriteAsJsonAsync(agent);
|
await ctx.Response.WriteAsJsonAsync(agent);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task HandleCreate(HttpContext ctx, DatabaseService db, ImageService imageService, MemoryService memoryService)
|
private static async Task HandleCreate(HttpContext ctx, DatabaseService db, ImageService imageService, MemoryService memoryService)
|
||||||
{
|
{
|
||||||
|
var userId = ctx.GetUserId();
|
||||||
var body = await JsonSerializer.DeserializeAsync<JsonElement>(ctx.Request.Body);
|
var body = await JsonSerializer.DeserializeAsync<JsonElement>(ctx.Request.Body);
|
||||||
|
|
||||||
var avatarRaw = body.TryGetProperty("avatar", out var av) ? av.GetString() : null;
|
var avatarRaw = body.TryGetProperty("avatar", out var av) ? av.GetString() : null;
|
||||||
@@ -48,6 +51,7 @@ public static class AgentEndpoint
|
|||||||
|
|
||||||
var agent = new AgentRecord
|
var agent = new AgentRecord
|
||||||
{
|
{
|
||||||
|
UserId = userId,
|
||||||
Name = body.TryGetProperty("name", out var n) ? n.GetString() ?? "Neuer Agent" : "Neuer Agent",
|
Name = body.TryGetProperty("name", out var n) ? n.GetString() ?? "Neuer Agent" : "Neuer Agent",
|
||||||
Avatar = imageService.ResizeDataUrl(avatarRaw),
|
Avatar = imageService.ResizeDataUrl(avatarRaw),
|
||||||
Description = body.TryGetProperty("description", out var d) ? d.GetString() : null,
|
Description = body.TryGetProperty("description", out var d) ? d.GetString() : null,
|
||||||
@@ -68,11 +72,12 @@ public static class AgentEndpoint
|
|||||||
|
|
||||||
private static async Task HandleUpdate(HttpContext ctx, DatabaseService db, ImageService imageService, MemoryService memoryService)
|
private static async Task HandleUpdate(HttpContext ctx, DatabaseService db, ImageService imageService, MemoryService memoryService)
|
||||||
{
|
{
|
||||||
|
var userId = ctx.GetUserId();
|
||||||
var agentId = ctx.Request.RouteValues["agentId"]?.ToString();
|
var agentId = ctx.Request.RouteValues["agentId"]?.ToString();
|
||||||
if (string.IsNullOrEmpty(agentId)) { ctx.Response.StatusCode = 400; return; }
|
if (string.IsNullOrEmpty(agentId)) { ctx.Response.StatusCode = 400; return; }
|
||||||
|
|
||||||
var agent = await db.GetAgent(agentId);
|
var agent = await db.GetAgent(agentId);
|
||||||
if (agent == null) { ctx.Response.StatusCode = 404; return; }
|
if (agent == null || agent.UserId != userId) { ctx.Response.StatusCode = 404; return; }
|
||||||
|
|
||||||
var body = await JsonSerializer.DeserializeAsync<JsonElement>(ctx.Request.Body);
|
var body = await JsonSerializer.DeserializeAsync<JsonElement>(ctx.Request.Body);
|
||||||
|
|
||||||
@@ -97,10 +102,11 @@ public static class AgentEndpoint
|
|||||||
|
|
||||||
private static async Task HandleDelete(HttpContext ctx, DatabaseService db)
|
private static async Task HandleDelete(HttpContext ctx, DatabaseService db)
|
||||||
{
|
{
|
||||||
|
var userId = ctx.GetUserId();
|
||||||
var agentId = ctx.Request.RouteValues["agentId"]?.ToString();
|
var agentId = ctx.Request.RouteValues["agentId"]?.ToString();
|
||||||
if (string.IsNullOrEmpty(agentId)) { ctx.Response.StatusCode = 400; return; }
|
if (string.IsNullOrEmpty(agentId)) { ctx.Response.StatusCode = 400; return; }
|
||||||
|
|
||||||
await db.DeleteAgent(agentId);
|
await db.DeleteAgent(agentId, userId);
|
||||||
ctx.Response.StatusCode = 204;
|
ctx.Response.StatusCode = 204;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@ using System.Net.Http;
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Gott.Extensions;
|
||||||
using Gott.Models;
|
using Gott.Models;
|
||||||
using Gott.Services;
|
using Gott.Services;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
@@ -50,8 +51,9 @@ public static class ChatEndpoint
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Agent aus Session auflösen
|
// Agent aus Session auflösen
|
||||||
|
var userId = req.HttpContext.GetUserId();
|
||||||
var session = await db.GetSession(sessionId);
|
var session = await db.GetSession(sessionId);
|
||||||
if (session == null || string.IsNullOrEmpty(session.AgentId))
|
if (session == null || session.UserId != userId || string.IsNullOrEmpty(session.AgentId))
|
||||||
{
|
{
|
||||||
res.StatusCode = 400;
|
res.StatusCode = 400;
|
||||||
await res.WriteAsJsonAsync(new { error = "Kein Agent ausgewählt" });
|
await res.WriteAsJsonAsync(new { error = "Kein Agent ausgewählt" });
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Gott.Extensions;
|
||||||
using Gott.Models;
|
using Gott.Models;
|
||||||
using Gott.Services;
|
using Gott.Services;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
@@ -23,10 +24,17 @@ public static class HistoryEndpoint
|
|||||||
app.MapGet("/api/chains/{chainId}", HandleGetChain);
|
app.MapGet("/api/chains/{chainId}", HandleGetChain);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static async Task<bool> VerifySessionOwner(HttpContext ctx, DatabaseService db, string sessionId)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(sessionId)) return false;
|
||||||
|
var session = await db.GetSession(sessionId);
|
||||||
|
return session != null && session.UserId == ctx.GetUserId();
|
||||||
|
}
|
||||||
|
|
||||||
private static async Task HandleHistory(HttpContext ctx, DatabaseService db)
|
private static async Task HandleHistory(HttpContext ctx, DatabaseService db)
|
||||||
{
|
{
|
||||||
var sessionId = ctx.Request.Query["sessionId"].FirstOrDefault();
|
var sessionId = ctx.Request.Query["sessionId"].FirstOrDefault();
|
||||||
if (string.IsNullOrEmpty(sessionId))
|
if (string.IsNullOrEmpty(sessionId) || !await VerifySessionOwner(ctx, db, sessionId))
|
||||||
{
|
{
|
||||||
ctx.Response.StatusCode = 400;
|
ctx.Response.StatusCode = 400;
|
||||||
await ctx.Response.WriteAsJsonAsync(new { error = "sessionId required" });
|
await ctx.Response.WriteAsJsonAsync(new { error = "sessionId required" });
|
||||||
@@ -84,7 +92,7 @@ public static class HistoryEndpoint
|
|||||||
var afterStr = ctx.Request.Query["afterId"].FirstOrDefault();
|
var afterStr = ctx.Request.Query["afterId"].FirstOrDefault();
|
||||||
var afterId = long.TryParse(afterStr, out var a) ? a : 0;
|
var afterId = long.TryParse(afterStr, out var a) ? a : 0;
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(sessionId))
|
if (string.IsNullOrEmpty(sessionId) || !await VerifySessionOwner(ctx, db, sessionId))
|
||||||
{
|
{
|
||||||
ctx.Response.StatusCode = 400;
|
ctx.Response.StatusCode = 400;
|
||||||
return;
|
return;
|
||||||
@@ -97,7 +105,7 @@ public static class HistoryEndpoint
|
|||||||
private static async Task HandleArchive(HttpContext ctx, DatabaseService db)
|
private static async Task HandleArchive(HttpContext ctx, DatabaseService db)
|
||||||
{
|
{
|
||||||
var sessionId = ctx.Request.Query["sessionId"].FirstOrDefault();
|
var sessionId = ctx.Request.Query["sessionId"].FirstOrDefault();
|
||||||
if (string.IsNullOrEmpty(sessionId))
|
if (string.IsNullOrEmpty(sessionId) || !await VerifySessionOwner(ctx, db, sessionId))
|
||||||
{
|
{
|
||||||
ctx.Response.StatusCode = 400;
|
ctx.Response.StatusCode = 400;
|
||||||
return;
|
return;
|
||||||
@@ -115,7 +123,7 @@ public static class HistoryEndpoint
|
|||||||
private static async Task HandleListArchives(HttpContext ctx, DatabaseService db)
|
private static async Task HandleListArchives(HttpContext ctx, DatabaseService db)
|
||||||
{
|
{
|
||||||
var sessionId = ctx.Request.Query["sessionId"].FirstOrDefault();
|
var sessionId = ctx.Request.Query["sessionId"].FirstOrDefault();
|
||||||
if (string.IsNullOrEmpty(sessionId))
|
if (string.IsNullOrEmpty(sessionId) || !await VerifySessionOwner(ctx, db, sessionId))
|
||||||
{
|
{
|
||||||
ctx.Response.StatusCode = 400;
|
ctx.Response.StatusCode = 400;
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using System.Diagnostics;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Gott.Extensions;
|
||||||
using Gott.Services;
|
using Gott.Services;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
@@ -29,12 +30,14 @@ public static class SessionEndpoint
|
|||||||
|
|
||||||
private static async Task HandleList(HttpContext ctx, DatabaseService db)
|
private static async Task HandleList(HttpContext ctx, DatabaseService db)
|
||||||
{
|
{
|
||||||
var sessions = await db.GetSessionsWithStats();
|
var userId = ctx.GetUserId();
|
||||||
|
var sessions = await db.GetSessionsWithStats(userId);
|
||||||
await ctx.Response.WriteAsJsonAsync(sessions);
|
await ctx.Response.WriteAsJsonAsync(sessions);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task HandleGet(HttpContext ctx, DatabaseService db)
|
private static async Task HandleGet(HttpContext ctx, DatabaseService db)
|
||||||
{
|
{
|
||||||
|
var userId = ctx.GetUserId();
|
||||||
var sessionId = ctx.Request.RouteValues["sessionId"]?.ToString();
|
var sessionId = ctx.Request.RouteValues["sessionId"]?.ToString();
|
||||||
if (string.IsNullOrEmpty(sessionId))
|
if (string.IsNullOrEmpty(sessionId))
|
||||||
{
|
{
|
||||||
@@ -42,7 +45,7 @@ public static class SessionEndpoint
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var session = await db.GetSession(sessionId);
|
var session = await db.GetSession(sessionId);
|
||||||
if (session == null)
|
if (session == null || session.UserId != userId)
|
||||||
{
|
{
|
||||||
ctx.Response.StatusCode = 404;
|
ctx.Response.StatusCode = 404;
|
||||||
return;
|
return;
|
||||||
@@ -52,6 +55,7 @@ public static class SessionEndpoint
|
|||||||
|
|
||||||
private static async Task HandleCreate(HttpContext ctx, DatabaseService db, ImageService imageService)
|
private static async Task HandleCreate(HttpContext ctx, DatabaseService db, ImageService imageService)
|
||||||
{
|
{
|
||||||
|
var userId = ctx.GetUserId();
|
||||||
var body = await JsonSerializer.DeserializeAsync<JsonElement>(ctx.Request.Body);
|
var body = await JsonSerializer.DeserializeAsync<JsonElement>(ctx.Request.Body);
|
||||||
|
|
||||||
var name = body.TryGetProperty("name", out var n) ? n.GetString() : "Neue Session";
|
var name = body.TryGetProperty("name", out var n) ? n.GetString() : "Neue Session";
|
||||||
@@ -63,12 +67,13 @@ public static class SessionEndpoint
|
|||||||
var context = body.TryGetProperty("context", out var ctxEl) ? ctxEl.GetString() : null;
|
var context = body.TryGetProperty("context", out var ctxEl) ? ctxEl.GetString() : null;
|
||||||
var customCommands = body.TryGetProperty("customCommands", out var cc) && cc.ValueKind != JsonValueKind.Null ? cc.GetRawText() : null;
|
var customCommands = body.TryGetProperty("customCommands", out var cc) && cc.ValueKind != JsonValueKind.Null ? cc.GetRawText() : null;
|
||||||
|
|
||||||
var session = await db.CreateSession(name, description, image, agentId, workspace, context, customCommands);
|
var session = await db.CreateSession(userId, name, description, image, agentId, workspace, context, customCommands);
|
||||||
await ctx.Response.WriteAsJsonAsync(session);
|
await ctx.Response.WriteAsJsonAsync(session);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task HandleUpdate(HttpContext ctx, DatabaseService db, ImageService imageService)
|
private static async Task HandleUpdate(HttpContext ctx, DatabaseService db, ImageService imageService)
|
||||||
{
|
{
|
||||||
|
var userId = ctx.GetUserId();
|
||||||
var sessionId = ctx.Request.RouteValues["sessionId"]?.ToString();
|
var sessionId = ctx.Request.RouteValues["sessionId"]?.ToString();
|
||||||
if (string.IsNullOrEmpty(sessionId))
|
if (string.IsNullOrEmpty(sessionId))
|
||||||
{
|
{
|
||||||
@@ -76,6 +81,13 @@ public static class SessionEndpoint
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var existing = await db.GetSession(sessionId);
|
||||||
|
if (existing == null || existing.UserId != userId)
|
||||||
|
{
|
||||||
|
ctx.Response.StatusCode = 404;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var body = await JsonSerializer.DeserializeAsync<JsonElement>(ctx.Request.Body);
|
var body = await JsonSerializer.DeserializeAsync<JsonElement>(ctx.Request.Body);
|
||||||
await db.UpdateSession(sessionId, body, imageService);
|
await db.UpdateSession(sessionId, body, imageService);
|
||||||
|
|
||||||
@@ -85,15 +97,17 @@ public static class SessionEndpoint
|
|||||||
|
|
||||||
private static async Task HandleDelete(HttpContext ctx, DatabaseService db)
|
private static async Task HandleDelete(HttpContext ctx, DatabaseService db)
|
||||||
{
|
{
|
||||||
|
var userId = ctx.GetUserId();
|
||||||
var sessionId = ctx.Request.RouteValues["sessionId"]?.ToString();
|
var sessionId = ctx.Request.RouteValues["sessionId"]?.ToString();
|
||||||
if (string.IsNullOrEmpty(sessionId)) { ctx.Response.StatusCode = 400; return; }
|
if (string.IsNullOrEmpty(sessionId)) { ctx.Response.StatusCode = 400; return; }
|
||||||
|
|
||||||
await db.DeleteSession(sessionId);
|
await db.DeleteSession(sessionId, userId);
|
||||||
ctx.Response.StatusCode = 204;
|
ctx.Response.StatusCode = 204;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task HandleSystemPrompt(HttpContext ctx, DatabaseService db, MemoryService memoryService, SkillService skillService)
|
private static async Task HandleSystemPrompt(HttpContext ctx, DatabaseService db, MemoryService memoryService, SkillService skillService)
|
||||||
{
|
{
|
||||||
|
var userId = ctx.GetUserId();
|
||||||
var sessionId = ctx.Request.RouteValues["sessionId"]?.ToString();
|
var sessionId = ctx.Request.RouteValues["sessionId"]?.ToString();
|
||||||
if (string.IsNullOrEmpty(sessionId))
|
if (string.IsNullOrEmpty(sessionId))
|
||||||
{
|
{
|
||||||
@@ -102,7 +116,7 @@ public static class SessionEndpoint
|
|||||||
}
|
}
|
||||||
|
|
||||||
var session = await db.GetSession(sessionId);
|
var session = await db.GetSession(sessionId);
|
||||||
if (session == null)
|
if (session == null || session.UserId != userId)
|
||||||
{
|
{
|
||||||
ctx.Response.StatusCode = 404;
|
ctx.Response.StatusCode = 404;
|
||||||
return;
|
return;
|
||||||
@@ -135,11 +149,12 @@ public static class SessionEndpoint
|
|||||||
|
|
||||||
private static async Task HandleSystemPromptBlocks(HttpContext ctx, DatabaseService db, MemoryService memoryService, SkillService skillService)
|
private static async Task HandleSystemPromptBlocks(HttpContext ctx, DatabaseService db, MemoryService memoryService, SkillService skillService)
|
||||||
{
|
{
|
||||||
|
var userId = ctx.GetUserId();
|
||||||
var sessionId = ctx.Request.RouteValues["sessionId"]?.ToString();
|
var sessionId = ctx.Request.RouteValues["sessionId"]?.ToString();
|
||||||
if (string.IsNullOrEmpty(sessionId)) { ctx.Response.StatusCode = 400; return; }
|
if (string.IsNullOrEmpty(sessionId)) { ctx.Response.StatusCode = 400; return; }
|
||||||
|
|
||||||
var session = await db.GetSession(sessionId);
|
var session = await db.GetSession(sessionId);
|
||||||
if (session == null) { ctx.Response.StatusCode = 404; return; }
|
if (session == null || session.UserId != userId) { ctx.Response.StatusCode = 404; return; }
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(session.AgentId))
|
if (string.IsNullOrEmpty(session.AgentId))
|
||||||
{
|
{
|
||||||
@@ -165,8 +180,9 @@ public static class SessionEndpoint
|
|||||||
await ctx.Response.WriteAsJsonAsync(blocks.Select(b => new { b.Icon, b.Label, b.Content }));
|
await ctx.Response.WriteAsJsonAsync(blocks.Select(b => new { b.Icon, b.Label, b.Content }));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task HandleStreaming(HttpContext ctx, StreamingTracker streamingTracker)
|
private static async Task HandleStreaming(HttpContext ctx, DatabaseService db, StreamingTracker streamingTracker)
|
||||||
{
|
{
|
||||||
|
var userId = ctx.GetUserId();
|
||||||
var sessionId = ctx.Request.RouteValues["sessionId"]?.ToString();
|
var sessionId = ctx.Request.RouteValues["sessionId"]?.ToString();
|
||||||
if (string.IsNullOrEmpty(sessionId))
|
if (string.IsNullOrEmpty(sessionId))
|
||||||
{
|
{
|
||||||
@@ -174,6 +190,13 @@ public static class SessionEndpoint
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var session = await db.GetSession(sessionId);
|
||||||
|
if (session == null || session.UserId != userId)
|
||||||
|
{
|
||||||
|
ctx.Response.StatusCode = 404;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var isStreaming = streamingTracker.IsStreaming(sessionId);
|
var isStreaming = streamingTracker.IsStreaming(sessionId);
|
||||||
var chainId = streamingTracker.GetChainId(sessionId);
|
var chainId = streamingTracker.GetChainId(sessionId);
|
||||||
await ctx.Response.WriteAsJsonAsync(new { streaming = isStreaming, chainId });
|
await ctx.Response.WriteAsJsonAsync(new { streaming = isStreaming, chainId });
|
||||||
@@ -181,6 +204,7 @@ public static class SessionEndpoint
|
|||||||
|
|
||||||
private static async Task HandleExec(HttpContext ctx, DatabaseService db)
|
private static async Task HandleExec(HttpContext ctx, DatabaseService db)
|
||||||
{
|
{
|
||||||
|
var userId = ctx.GetUserId();
|
||||||
var sessionId = ctx.Request.RouteValues["sessionId"]?.ToString();
|
var sessionId = ctx.Request.RouteValues["sessionId"]?.ToString();
|
||||||
if (string.IsNullOrEmpty(sessionId))
|
if (string.IsNullOrEmpty(sessionId))
|
||||||
{
|
{
|
||||||
@@ -189,7 +213,7 @@ public static class SessionEndpoint
|
|||||||
}
|
}
|
||||||
|
|
||||||
var session = await db.GetSession(sessionId);
|
var session = await db.GetSession(sessionId);
|
||||||
if (session == null)
|
if (session == null || session.UserId != userId)
|
||||||
{
|
{
|
||||||
ctx.Response.StatusCode = 404;
|
ctx.Response.StatusCode = 404;
|
||||||
return;
|
return;
|
||||||
@@ -304,6 +328,7 @@ public static class SessionEndpoint
|
|||||||
|
|
||||||
private static async Task HandleGitStatus(HttpContext ctx, DatabaseService db)
|
private static async Task HandleGitStatus(HttpContext ctx, DatabaseService db)
|
||||||
{
|
{
|
||||||
|
var userId = ctx.GetUserId();
|
||||||
var sessionId = ctx.Request.RouteValues["sessionId"]?.ToString();
|
var sessionId = ctx.Request.RouteValues["sessionId"]?.ToString();
|
||||||
if (string.IsNullOrEmpty(sessionId))
|
if (string.IsNullOrEmpty(sessionId))
|
||||||
{
|
{
|
||||||
@@ -312,7 +337,7 @@ public static class SessionEndpoint
|
|||||||
}
|
}
|
||||||
|
|
||||||
var session = await db.GetSession(sessionId);
|
var session = await db.GetSession(sessionId);
|
||||||
if (session == null)
|
if (session == null || session.UserId != userId)
|
||||||
{
|
{
|
||||||
ctx.Response.StatusCode = 404;
|
ctx.Response.StatusCode = 404;
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using System.IO;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Gott.Extensions;
|
||||||
using Gott.Services;
|
using Gott.Services;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
@@ -50,10 +51,17 @@ public static class SkillEndpoint
|
|||||||
await ctx.Response.WriteAsJsonAsync(skills);
|
await ctx.Response.WriteAsJsonAsync(skills);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static async Task<bool> VerifyAgentOwner(HttpContext ctx, DatabaseService db, string agentId)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(agentId)) return false;
|
||||||
|
var agent = await db.GetAgent(agentId);
|
||||||
|
return agent != null && agent.UserId == ctx.GetUserId();
|
||||||
|
}
|
||||||
|
|
||||||
private static async Task HandleGetAgentSkills(HttpContext ctx, DatabaseService db, SkillService skillService)
|
private static async Task HandleGetAgentSkills(HttpContext ctx, DatabaseService db, SkillService skillService)
|
||||||
{
|
{
|
||||||
var agentId = ctx.Request.RouteValues["agentId"]?.ToString();
|
var agentId = ctx.Request.RouteValues["agentId"]?.ToString();
|
||||||
if (string.IsNullOrEmpty(agentId)) { ctx.Response.StatusCode = 400; return; }
|
if (string.IsNullOrEmpty(agentId) || !await VerifyAgentOwner(ctx, db, agentId)) { ctx.Response.StatusCode = 400; return; }
|
||||||
|
|
||||||
var slugs = await db.GetAgentSkills(agentId);
|
var slugs = await db.GetAgentSkills(agentId);
|
||||||
var allSkills = skillService.GetAllSkills();
|
var allSkills = skillService.GetAllSkills();
|
||||||
@@ -69,7 +77,7 @@ public static class SkillEndpoint
|
|||||||
private static async Task HandleSetAgentSkills(HttpContext ctx, DatabaseService db)
|
private static async Task HandleSetAgentSkills(HttpContext ctx, DatabaseService db)
|
||||||
{
|
{
|
||||||
var agentId = ctx.Request.RouteValues["agentId"]?.ToString();
|
var agentId = ctx.Request.RouteValues["agentId"]?.ToString();
|
||||||
if (string.IsNullOrEmpty(agentId)) { ctx.Response.StatusCode = 400; return; }
|
if (string.IsNullOrEmpty(agentId) || !await VerifyAgentOwner(ctx, db, agentId)) { ctx.Response.StatusCode = 400; return; }
|
||||||
|
|
||||||
var body = await JsonSerializer.DeserializeAsync<JsonElement>(ctx.Request.Body);
|
var body = await JsonSerializer.DeserializeAsync<JsonElement>(ctx.Request.Body);
|
||||||
var slugs = body.TryGetProperty("skills", out var arr)
|
var slugs = body.TryGetProperty("skills", out var arr)
|
||||||
@@ -86,7 +94,7 @@ public static class SkillEndpoint
|
|||||||
{
|
{
|
||||||
var agentId = ctx.Request.RouteValues["agentId"]?.ToString();
|
var agentId = ctx.Request.RouteValues["agentId"]?.ToString();
|
||||||
var slug = ctx.Request.RouteValues["slug"]?.ToString();
|
var slug = ctx.Request.RouteValues["slug"]?.ToString();
|
||||||
if (string.IsNullOrEmpty(agentId) || string.IsNullOrEmpty(slug)) { ctx.Response.StatusCode = 400; return; }
|
if (string.IsNullOrEmpty(agentId) || string.IsNullOrEmpty(slug) || !await VerifyAgentOwner(ctx, db, agentId)) { ctx.Response.StatusCode = 400; return; }
|
||||||
|
|
||||||
await db.AddAgentSkill(agentId, slug);
|
await db.AddAgentSkill(agentId, slug);
|
||||||
ctx.Response.StatusCode = 204;
|
ctx.Response.StatusCode = 204;
|
||||||
@@ -96,7 +104,7 @@ public static class SkillEndpoint
|
|||||||
{
|
{
|
||||||
var agentId = ctx.Request.RouteValues["agentId"]?.ToString();
|
var agentId = ctx.Request.RouteValues["agentId"]?.ToString();
|
||||||
var slug = ctx.Request.RouteValues["slug"]?.ToString();
|
var slug = ctx.Request.RouteValues["slug"]?.ToString();
|
||||||
if (string.IsNullOrEmpty(agentId) || string.IsNullOrEmpty(slug)) { ctx.Response.StatusCode = 400; return; }
|
if (string.IsNullOrEmpty(agentId) || string.IsNullOrEmpty(slug) || !await VerifyAgentOwner(ctx, db, agentId)) { ctx.Response.StatusCode = 400; return; }
|
||||||
|
|
||||||
await db.RemoveAgentSkill(agentId, slug);
|
await db.RemoveAgentSkill(agentId, slug);
|
||||||
ctx.Response.StatusCode = 204;
|
ctx.Response.StatusCode = 204;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Gott.Extensions;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Routing;
|
using Microsoft.AspNetCore.Routing;
|
||||||
@@ -54,7 +55,7 @@ public static class WorkspaceEndpoint
|
|||||||
// Workspace aus Session holen
|
// Workspace aus Session holen
|
||||||
var db = ctx.RequestServices.GetRequiredService<Gott.Services.DatabaseService>();
|
var db = ctx.RequestServices.GetRequiredService<Gott.Services.DatabaseService>();
|
||||||
var session = await db.GetSession(sessionId ?? "");
|
var session = await db.GetSession(sessionId ?? "");
|
||||||
if (session == null || string.IsNullOrEmpty(session.Workspace))
|
if (session == null || session.UserId != ctx.GetUserId() || string.IsNullOrEmpty(session.Workspace))
|
||||||
{
|
{
|
||||||
ctx.Response.StatusCode = 400;
|
ctx.Response.StatusCode = 400;
|
||||||
await ctx.Response.WriteAsJsonAsync(new { error = "Kein Workspace konfiguriert" });
|
await ctx.Response.WriteAsJsonAsync(new { error = "Kein Workspace konfiguriert" });
|
||||||
@@ -118,7 +119,7 @@ public static class WorkspaceEndpoint
|
|||||||
|
|
||||||
var db = ctx.RequestServices.GetRequiredService<Gott.Services.DatabaseService>();
|
var db = ctx.RequestServices.GetRequiredService<Gott.Services.DatabaseService>();
|
||||||
var session = await db.GetSession(sessionId ?? "");
|
var session = await db.GetSession(sessionId ?? "");
|
||||||
if (session == null || string.IsNullOrEmpty(session.Workspace))
|
if (session == null || session.UserId != ctx.GetUserId() || string.IsNullOrEmpty(session.Workspace))
|
||||||
{
|
{
|
||||||
ctx.Response.StatusCode = 400;
|
ctx.Response.StatusCode = 400;
|
||||||
await ctx.Response.WriteAsJsonAsync(new { error = "Kein Workspace konfiguriert" });
|
await ctx.Response.WriteAsJsonAsync(new { error = "Kein Workspace konfiguriert" });
|
||||||
@@ -176,7 +177,7 @@ public static class WorkspaceEndpoint
|
|||||||
|
|
||||||
var db = ctx.RequestServices.GetRequiredService<Gott.Services.DatabaseService>();
|
var db = ctx.RequestServices.GetRequiredService<Gott.Services.DatabaseService>();
|
||||||
var session = await db.GetSession(sessionId ?? "");
|
var session = await db.GetSession(sessionId ?? "");
|
||||||
if (session == null || string.IsNullOrEmpty(session.Workspace))
|
if (session == null || session.UserId != ctx.GetUserId() || string.IsNullOrEmpty(session.Workspace))
|
||||||
{
|
{
|
||||||
ctx.Response.StatusCode = 400;
|
ctx.Response.StatusCode = 400;
|
||||||
await ctx.Response.WriteAsJsonAsync(new { error = "Kein Workspace konfiguriert" });
|
await ctx.Response.WriteAsJsonAsync(new { error = "Kein Workspace konfiguriert" });
|
||||||
@@ -446,7 +447,7 @@ public static class WorkspaceEndpoint
|
|||||||
|
|
||||||
var db = ctx.RequestServices.GetRequiredService<Gott.Services.DatabaseService>();
|
var db = ctx.RequestServices.GetRequiredService<Gott.Services.DatabaseService>();
|
||||||
var session = await db.GetSession(sessionId ?? "");
|
var session = await db.GetSession(sessionId ?? "");
|
||||||
if (session == null || string.IsNullOrEmpty(session.Workspace))
|
if (session == null || session.UserId != ctx.GetUserId() || string.IsNullOrEmpty(session.Workspace))
|
||||||
{
|
{
|
||||||
ctx.Response.StatusCode = 400;
|
ctx.Response.StatusCode = 400;
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
|
namespace Gott.Extensions;
|
||||||
|
|
||||||
|
public static class HttpContextExtensions
|
||||||
|
{
|
||||||
|
public static string GetUserId(this HttpContext ctx)
|
||||||
|
{
|
||||||
|
return ctx.Items["UserId"] as string;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ public class AgentRecord
|
|||||||
public bool UseIdentity { get; set; } = true;
|
public bool UseIdentity { get; set; } = true;
|
||||||
public bool UseUserInfo { get; set; } = true;
|
public bool UseUserInfo { get; set; } = true;
|
||||||
public bool UseMemory { get; set; } = true;
|
public bool UseMemory { get; set; } = true;
|
||||||
|
public string UserId { get; set; }
|
||||||
public bool IsDeleted { get; set; }
|
public bool IsDeleted { get; set; }
|
||||||
public string CreatedAt { get; set; }
|
public string CreatedAt { get; set; }
|
||||||
public string UpdatedAt { get; set; }
|
public string UpdatedAt { get; set; }
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ public class SessionRecord
|
|||||||
public string Workspace { get; set; }
|
public string Workspace { get; set; }
|
||||||
public string Context { get; set; }
|
public string Context { get; set; }
|
||||||
public string CustomCommands { get; set; }
|
public string CustomCommands { get; set; }
|
||||||
|
public string UserId { get; set; }
|
||||||
public bool IsDeleted { get; set; }
|
public bool IsDeleted { get; set; }
|
||||||
public string CreatedAt { get; set; }
|
public string CreatedAt { get; set; }
|
||||||
public string UpdatedAt { get; set; }
|
public string UpdatedAt { get; set; }
|
||||||
|
|||||||
+34
-19
@@ -145,6 +145,10 @@ public class DatabaseService
|
|||||||
TryAddColumn(conn, "agents", "use_user_info", "INTEGER NOT NULL DEFAULT 1");
|
TryAddColumn(conn, "agents", "use_user_info", "INTEGER NOT NULL DEFAULT 1");
|
||||||
TryAddColumn(conn, "agents", "use_memory", "INTEGER NOT NULL DEFAULT 1");
|
TryAddColumn(conn, "agents", "use_memory", "INTEGER NOT NULL DEFAULT 1");
|
||||||
|
|
||||||
|
// User-Zuordnung
|
||||||
|
TryAddColumn(conn, "sessions", "user_id", "TEXT");
|
||||||
|
TryAddColumn(conn, "agents", "user_id", "TEXT");
|
||||||
|
|
||||||
conn.Execute(@"
|
conn.Execute(@"
|
||||||
CREATE TABLE IF NOT EXISTS agent_skills (
|
CREATE TABLE IF NOT EXISTS agent_skills (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
@@ -185,17 +189,17 @@ public class DatabaseService
|
|||||||
agent.CreatedAt = now;
|
agent.CreatedAt = now;
|
||||||
agent.UpdatedAt = now;
|
agent.UpdatedAt = now;
|
||||||
await conn.ExecuteAsync(@"
|
await conn.ExecuteAsync(@"
|
||||||
INSERT INTO agents (agent_id, name, avatar, description, model, tool_choice, custom_commands, auto_commit, use_soul, use_identity, use_user_info, use_memory, is_deleted, created_at, updated_at)
|
INSERT INTO agents (agent_id, user_id, name, avatar, description, model, tool_choice, custom_commands, auto_commit, use_soul, use_identity, use_user_info, use_memory, is_deleted, created_at, updated_at)
|
||||||
VALUES (@AgentId, @Name, @Avatar, @Description, @Model, @ToolChoice, @CustomCommands, @AutoCommit, @UseSoul, @UseIdentity, @UseUserInfo, @UseMemory, 0, @CreatedAt, @UpdatedAt)
|
VALUES (@AgentId, @UserId, @Name, @Avatar, @Description, @Model, @ToolChoice, @CustomCommands, @AutoCommit, @UseSoul, @UseIdentity, @UseUserInfo, @UseMemory, 0, @CreatedAt, @UpdatedAt)
|
||||||
", agent);
|
", agent);
|
||||||
return agent;
|
return agent;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<AgentRecord>> GetAgents()
|
public async Task<IEnumerable<AgentRecord>> GetAgents(string userId)
|
||||||
{
|
{
|
||||||
using var conn = CreateConnection();
|
using var conn = CreateConnection();
|
||||||
return await conn.QueryAsync<AgentRecord>(
|
return await conn.QueryAsync<AgentRecord>(
|
||||||
"SELECT * FROM agents WHERE is_deleted = 0 ORDER BY name");
|
"SELECT * FROM agents WHERE user_id = @userId AND is_deleted = 0 ORDER BY name", new { userId });
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<AgentRecord> GetAgent(string agentId)
|
public async Task<AgentRecord> GetAgent(string agentId)
|
||||||
@@ -215,27 +219,28 @@ public class DatabaseService
|
|||||||
custom_commands = @CustomCommands, auto_commit = @AutoCommit,
|
custom_commands = @CustomCommands, auto_commit = @AutoCommit,
|
||||||
use_soul = @UseSoul, use_identity = @UseIdentity, use_user_info = @UseUserInfo, use_memory = @UseMemory,
|
use_soul = @UseSoul, use_identity = @UseIdentity, use_user_info = @UseUserInfo, use_memory = @UseMemory,
|
||||||
updated_at = @UpdatedAt
|
updated_at = @UpdatedAt
|
||||||
WHERE agent_id = @AgentId
|
WHERE agent_id = @AgentId AND user_id = @UserId
|
||||||
", agent);
|
", agent);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task DeleteAgent(string agentId)
|
public async Task DeleteAgent(string agentId, string userId)
|
||||||
{
|
{
|
||||||
using var conn = CreateConnection();
|
using var conn = CreateConnection();
|
||||||
await conn.ExecuteAsync(
|
await conn.ExecuteAsync(
|
||||||
"UPDATE agents SET is_deleted = 1, updated_at = @now WHERE agent_id = @agentId",
|
"UPDATE agents SET is_deleted = 1, updated_at = @now WHERE agent_id = @agentId AND user_id = @userId",
|
||||||
new { agentId, now = DateTime.UtcNow.ToString("o") });
|
new { agentId, userId, now = DateTime.UtcNow.ToString("o") });
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Sessions ──
|
// ── Sessions ──
|
||||||
|
|
||||||
public async Task<SessionRecord> CreateSession(string name = null, string description = null, string image = null, string agentId = null, string workspace = null, string context = null, string customCommands = null)
|
public async Task<SessionRecord> CreateSession(string userId, string name = null, string description = null, string image = null, string agentId = null, string workspace = null, string context = null, string customCommands = null)
|
||||||
{
|
{
|
||||||
using var conn = CreateConnection();
|
using var conn = CreateConnection();
|
||||||
var now = DateTime.UtcNow.ToString("o");
|
var now = DateTime.UtcNow.ToString("o");
|
||||||
var session = new SessionRecord
|
var session = new SessionRecord
|
||||||
{
|
{
|
||||||
SessionId = Guid.NewGuid().ToString("N"),
|
SessionId = Guid.NewGuid().ToString("N"),
|
||||||
|
UserId = userId,
|
||||||
Name = name,
|
Name = name,
|
||||||
Description = description,
|
Description = description,
|
||||||
Image = image,
|
Image = image,
|
||||||
@@ -247,24 +252,24 @@ public class DatabaseService
|
|||||||
UpdatedAt = now
|
UpdatedAt = now
|
||||||
};
|
};
|
||||||
await conn.ExecuteAsync(@"
|
await conn.ExecuteAsync(@"
|
||||||
INSERT INTO sessions (session_id, name, description, image, agent_id, workspace, context, custom_commands, created_at, updated_at)
|
INSERT INTO sessions (session_id, user_id, name, description, image, agent_id, workspace, context, custom_commands, created_at, updated_at)
|
||||||
VALUES (@SessionId, @Name, @Description, @Image, @AgentId, @Workspace, @Context, @CustomCommands, @CreatedAt, @UpdatedAt)
|
VALUES (@SessionId, @UserId, @Name, @Description, @Image, @AgentId, @Workspace, @Context, @CustomCommands, @CreatedAt, @UpdatedAt)
|
||||||
", session);
|
", session);
|
||||||
return session;
|
return session;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<SessionRecord>> GetSessions()
|
public async Task<IEnumerable<SessionRecord>> GetSessions(string userId)
|
||||||
{
|
{
|
||||||
using var conn = CreateConnection();
|
using var conn = CreateConnection();
|
||||||
return await conn.QueryAsync<SessionRecord>(
|
return await conn.QueryAsync<SessionRecord>(
|
||||||
"SELECT * FROM sessions WHERE is_deleted = 0 ORDER BY updated_at DESC");
|
"SELECT * FROM sessions WHERE user_id = @userId AND is_deleted = 0 ORDER BY updated_at DESC", new { userId });
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<object>> GetSessionsWithStats()
|
public async Task<IEnumerable<object>> GetSessionsWithStats(string userId)
|
||||||
{
|
{
|
||||||
using var conn = CreateConnection();
|
using var conn = CreateConnection();
|
||||||
var sessions = await conn.QueryAsync<SessionRecord>(
|
var sessions = await conn.QueryAsync<SessionRecord>(
|
||||||
"SELECT * FROM sessions WHERE is_deleted = 0 ORDER BY updated_at DESC");
|
"SELECT * FROM sessions WHERE user_id = @userId AND is_deleted = 0 ORDER BY updated_at DESC", new { userId });
|
||||||
|
|
||||||
var results = new List<object>();
|
var results = new List<object>();
|
||||||
foreach (var s in sessions)
|
foreach (var s in sessions)
|
||||||
@@ -341,12 +346,12 @@ public class DatabaseService
|
|||||||
await conn.ExecuteAsync($"UPDATE sessions SET {string.Join(", ", sets)} WHERE session_id = @sessionId", p);
|
await conn.ExecuteAsync($"UPDATE sessions SET {string.Join(", ", sets)} WHERE session_id = @sessionId", p);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task DeleteSession(string sessionId)
|
public async Task DeleteSession(string sessionId, string userId)
|
||||||
{
|
{
|
||||||
using var conn = CreateConnection();
|
using var conn = CreateConnection();
|
||||||
await conn.ExecuteAsync(
|
await conn.ExecuteAsync(
|
||||||
"UPDATE sessions SET is_deleted = 1, updated_at = @now WHERE session_id = @sessionId",
|
"UPDATE sessions SET is_deleted = 1, updated_at = @now WHERE session_id = @sessionId AND user_id = @userId",
|
||||||
new { sessionId, now = DateTime.UtcNow.ToString("o") });
|
new { sessionId, userId, now = DateTime.UtcNow.ToString("o") });
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Archives ──
|
// ── Archives ──
|
||||||
@@ -657,21 +662,31 @@ public class DatabaseService
|
|||||||
{
|
{
|
||||||
using var conn = CreateConnection();
|
using var conn = CreateConnection();
|
||||||
var count = conn.ExecuteScalar<int>("SELECT COUNT(*) FROM users");
|
var count = conn.ExecuteScalar<int>("SELECT COUNT(*) FROM users");
|
||||||
|
string adminUserId;
|
||||||
if (count == 0)
|
if (count == 0)
|
||||||
{
|
{
|
||||||
|
adminUserId = Guid.NewGuid().ToString("N");
|
||||||
var now = DateTime.UtcNow.ToString("o");
|
var now = DateTime.UtcNow.ToString("o");
|
||||||
conn.Execute(@"
|
conn.Execute(@"
|
||||||
INSERT INTO users (user_id, username, password_hash, display_name, created_at, updated_at)
|
INSERT INTO users (user_id, username, password_hash, display_name, created_at, updated_at)
|
||||||
VALUES (@userId, @username, @hash, @displayName, @now, @now)",
|
VALUES (@userId, @username, @hash, @displayName, @now, @now)",
|
||||||
new
|
new
|
||||||
{
|
{
|
||||||
userId = Guid.NewGuid().ToString("N"),
|
userId = adminUserId,
|
||||||
username = "admin",
|
username = "admin",
|
||||||
hash = auth.HashPassword("admin"),
|
hash = auth.HashPassword("admin"),
|
||||||
displayName = "Admin",
|
displayName = "Admin",
|
||||||
now
|
now
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
adminUserId = conn.ExecuteScalar<string>("SELECT user_id FROM users ORDER BY id LIMIT 1");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bestehende Sessions/Agents ohne User dem ersten User zuweisen
|
||||||
|
conn.Execute("UPDATE sessions SET user_id = @userId WHERE user_id IS NULL", new { userId = adminUserId });
|
||||||
|
conn.Execute("UPDATE agents SET user_id = @userId WHERE user_id IS NULL", new { userId = adminUserId });
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<UserRecord> GetUserByUsername(string username)
|
public async Task<UserRecord> GetUserByUsername(string username)
|
||||||
|
|||||||
Reference in new issue
Block a user