Files
gott/Endpoints/ProfileEndpoint.cs
T
MelekiandClaude Opus 4.8 b23eba32b4 Let session owners reassign their session to another user
Previously a session could only be reassigned to another user via a
manual DB UPDATE. This adds it to the session edit UI for every owner of
a session (not just admins) - the ownership check in HandleUpdate
(existing.UserId == userId) already guarantees the caller owns the
session, so handing over one's own session is safe.

Backend:
- ProfileEndpoint: new GET /api/users returning a minimal user directory
  (userId/username/displayName only) for any logged-in user. The
  admin-gated /api/admin/users stays the source for sensitive fields
  (defaultPath/canUseCli/isAdmin).
- SessionEndpoint.HandleUpdate: validate the target user exists (400 if
  not) and pass allowUserReassign: true to UpdateSession.
- DatabaseService.UpdateSession: new allowUserReassign flag; only then is
  a non-empty userId applied as user_id (empty/null is ignored - a
  session must always have an owner).

Frontend:
- index.html: hidden #sessionOwnerRow with a user picker card in the
  session edit form (existing sessions only).
- cards.js: buildUserCard helper for the picker.
- session-page.js: initSessionUserPicker loads users via apiGetUsers(),
  preselects the current owner, and on an actual owner change asks for
  confirmation (irreversible - the session leaves the user's own list).
- api.js: apiGetUsers() -> /api/users.
- ARCHITECTURE.md: document /api/users.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 11:52:53 +02:00

118 lines
4.2 KiB
C#
Raw 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.Linq;
using System.Text.Json;
using Gott.Extensions;
using Gott.Services;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
namespace Gott.Endpoints;
public static class ProfileEndpoint
{
public static void MapProfileEndpoints(this WebApplication app)
{
app.MapGet("/api/profile", HandleGet);
app.MapPut("/api/profile", HandleUpdate);
app.MapPut("/api/profile/password", HandlePasswordChange);
app.MapGet("/api/users", HandleListDirectory);
}
/// <summary>
/// Minimales User-Verzeichnis für jeden eingeloggten Nutzer (z. B. um eine Session
/// einem anderen Benutzer zuzuweisen). Gibt bewusst NUR id/username/displayName zurück –
/// keine sensiblen Felder wie defaultPath/canUseCli/isAdmin (dafür gibt es /api/admin/users).
/// </summary>
private static async System.Threading.Tasks.Task HandleListDirectory(HttpContext ctx, DatabaseService db)
{
var users = await db.GetUsers();
var result = users.Select(u => new
{
userId = u.UserId,
username = u.Username,
displayName = u.DisplayName
});
await ctx.Response.WriteAsJsonAsync(result);
}
private static async System.Threading.Tasks.Task HandleGet(HttpContext ctx, DatabaseService db)
{
var userId = ctx.GetUserId();
var user = await db.GetUser(userId);
if (user == null)
{
ctx.Response.StatusCode = 404;
await ctx.Response.WriteAsJsonAsync(new { error = "User not found" });
return;
}
await ctx.Response.WriteAsJsonAsync(new
{
userId = user.UserId,
username = user.Username,
displayName = user.DisplayName,
persona = user.Persona,
defaultPath = user.DefaultPath,
isAdmin = user.IsAdmin
});
}
private static async System.Threading.Tasks.Task HandleUpdate(HttpContext ctx, DatabaseService db)
{
var userId = ctx.GetUserId();
var body = await JsonSerializer.DeserializeAsync<JsonElement>(ctx.Request.Body);
var displayName = body.TryGetProperty("displayName", out var dn) ? dn.GetString() : null;
var persona = body.TryGetProperty("persona", out var p) ? p.GetString() : null;
await db.UpdateUserProfile(userId, displayName, persona);
var user = await db.GetUser(userId);
await ctx.Response.WriteAsJsonAsync(new
{
userId = user.UserId,
username = user.Username,
displayName = user.DisplayName,
persona = user.Persona,
defaultPath = user.DefaultPath,
isAdmin = user.IsAdmin
});
}
private static async System.Threading.Tasks.Task HandlePasswordChange(HttpContext ctx, DatabaseService db, AuthService auth)
{
var userId = ctx.GetUserId();
var body = await JsonSerializer.DeserializeAsync<JsonElement>(ctx.Request.Body);
var currentPassword = body.TryGetProperty("currentPassword", out var cp) ? cp.GetString() : null;
var newPassword = body.TryGetProperty("newPassword", out var np) ? np.GetString() : null;
if (string.IsNullOrEmpty(currentPassword) || string.IsNullOrEmpty(newPassword))
{
ctx.Response.StatusCode = 400;
await ctx.Response.WriteAsJsonAsync(new { error = "Aktuelles und neues Passwort erforderlich" });
return;
}
if (newPassword.Length < 4)
{
ctx.Response.StatusCode = 400;
await ctx.Response.WriteAsJsonAsync(new { error = "Passwort muss mindestens 4 Zeichen lang sein" });
return;
}
var user = await db.GetUser(userId);
if (user == null || !auth.VerifyPassword(currentPassword, user.PasswordHash))
{
ctx.Response.StatusCode = 403;
await ctx.Response.WriteAsJsonAsync(new { error = "Aktuelles Passwort ist falsch" });
return;
}
var newHash = auth.HashPassword(newPassword);
await db.UpdateUserPassword(userId, newHash);
await ctx.Response.WriteAsJsonAsync(new { ok = true });
}
}