Refine admin workflows and team access

This commit is contained in:
AzuTear
2026-06-26 18:09:29 +02:00
parent b7804e10ea
commit 8b69dfbafb
71 changed files with 5066 additions and 751 deletions
+67 -10
View File
@@ -13,6 +13,7 @@ namespace Backend.Endpoints;
public static class AdminTeamEndpoints
{
private const int MinPasswordLength = 10;
private static readonly TimeSpan OnlineActivityWindow = TimeSpan.FromMinutes(5);
private const string PersonalOwnerLogin = "jayuhime";
private static readonly string[] PersonalCreatorLogins = ["sleepy_bao"];
@@ -79,11 +80,21 @@ public static class AdminTeamEndpoints
.OrderByDescending(item => item.Role == AdminRoles.Owner)
.ThenBy(item => item.Role)
.ThenBy(item => item.DisplayName)
.Select(item => ToMemberDto(item))
.ToArrayAsync();
var sessionKeys = members
.SelectMany(MemberSessionKeys)
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToArray();
var sessions = sessionKeys.Length == 0
? []
: await db.UserSessions
.AsNoTracking()
.Where(item => sessionKeys.Contains(item.TwitchUserId))
.ToArrayAsync();
var now = DateTimeOffset.UtcNow;
return Results.Ok(new AdminTeamResponse(
members,
members.Select(member => ToMemberDto(member, sessions, now)),
await BuildRoleDtosAsync(db),
PermissionCatalog));
}
@@ -322,10 +333,15 @@ public static class AdminTeamEndpoints
IAdminAuditService adminAuditService)
{
var session = AdminEndpointConventions.CurrentSession(context);
if (!AdminRoles.IsPrivilegedFullControlRole(session.Role))
{
return Results.Json(new { message = "Rollenberechtigungen können nur von Owner oder Creator geändert werden." }, statusCode: StatusCodes.Status403Forbidden);
}
var allowedPermissions = AdminPermissionCatalog.AllPermissionKeys.ToHashSet(StringComparer.OrdinalIgnoreCase);
var allowedRoles = DefaultRoles.Select(item => item.Key).ToHashSet(StringComparer.OrdinalIgnoreCase);
var existingRows = await db.TeamRolePermissions.ToDictionaryAsync(item => item.Role, StringComparer.OrdinalIgnoreCase, context.RequestAborted);
var roleChanges = new List<object>();
foreach (var roleUpdate in request.Roles ?? [])
{
@@ -339,14 +355,18 @@ public static class AdminTeamEndpoints
.Select(item => item.Trim())
.Where(item => allowedPermissions.Contains(item))
.Distinct(StringComparer.OrdinalIgnoreCase)
.OrderBy(item => item)
.ToArray();
permissionKeys = AdminPermissionCatalog.NormalizePermissionKeys(role, permissionKeys);
if (AdminRoles.IsPrivilegedFullControlRole(role) && permissionKeys.Length != AdminPermissionCatalog.AllPermissionKeys.Length)
{
return Results.BadRequest(new { message = "Owner und Creator müssen alle Berechtigungen behalten." });
}
var beforePermissions = existingRows.TryGetValue(role, out var existingRow)
? AdminPermissionCatalog.NormalizePermissionKeys(role, ReadPermissionKeys(existingRow.PermissionsJson, AdminPermissionCatalog.DefaultPermissionKeys(role)))
: AdminPermissionCatalog.NormalizePermissionKeys(role, AdminPermissionCatalog.DefaultPermissionKeys(role));
if (!existingRows.TryGetValue(role, out var row))
{
row = new TeamRolePermission { Role = role };
@@ -356,6 +376,18 @@ public static class AdminTeamEndpoints
row.PermissionsJson = JsonSerializer.Serialize(permissionKeys);
row.UpdatedAt = DateTimeOffset.UtcNow;
row.UpdatedByTwitchId = session.TwitchUserId;
if (!beforePermissions.SequenceEqual(permissionKeys, StringComparer.OrdinalIgnoreCase))
{
roleChanges.Add(new
{
role,
before = beforePermissions,
after = permissionKeys,
added = permissionKeys.Except(beforePermissions, StringComparer.OrdinalIgnoreCase).ToArray(),
removed = beforePermissions.Except(permissionKeys, StringComparer.OrdinalIgnoreCase).ToArray(),
});
}
}
adminAuditService.AddEntry(
@@ -364,7 +396,7 @@ public static class AdminTeamEndpoints
"team-role",
"matrix",
"Team-Rollenberechtigungen wurden gespeichert.",
new { roleCount = request.Roles?.Length ?? 0 },
new { roleCount = request.Roles?.Length ?? 0, changes = roleChanges },
RequestMetadataReader.Read(context));
await db.SaveChangesAsync(context.RequestAborted);
@@ -384,18 +416,30 @@ public static class AdminTeamEndpoints
? ReadPermissionKeys(json, role.PermissionKeys)
: role.PermissionKeys;
if (AdminRoles.IsPrivilegedFullControlRole(role.Key))
{
permissions = AdminPermissionCatalog.AllPermissionKeys;
}
permissions = AdminPermissionCatalog.NormalizePermissionKeys(role.Key, permissions);
return role with { PermissionKeys = permissions };
})
.ToArray();
}
private static AdminTeamMemberDto ToMemberDto(TeamMember member) =>
new(
private static AdminTeamMemberDto ToMemberDto(
TeamMember member,
IReadOnlyCollection<UserSession>? sessions = null,
DateTimeOffset? now = null)
{
var memberSessions = sessions?
.Where(session => MemberSessionKeys(member).Contains(session.TwitchUserId, StringComparer.OrdinalIgnoreCase))
.ToArray() ?? [];
var lastSeenAt = memberSessions
.Select(session => (DateTimeOffset?)session.LastSeenAt)
.Max() ?? member.LastLoginAt;
var isOnline = member.IsActive
&& memberSessions.Any(session =>
session.IsActive
&& session.LastSeenAt >= (now ?? DateTimeOffset.UtcNow).Subtract(OnlineActivityWindow));
return new(
member.Id,
member.Login,
member.DisplayName,
@@ -407,8 +451,21 @@ public static class AdminTeamEndpoints
member.CreatedAt,
member.UpdatedAt,
member.LastLoginAt,
lastSeenAt,
isOnline,
member.TwitchBoundAt,
member.PasswordResetAt);
}
private static IEnumerable<string> MemberSessionKeys(TeamMember member)
{
yield return $"team:{member.Login}";
if (!string.IsNullOrWhiteSpace(member.BoundTwitchUserId))
{
yield return member.BoundTwitchUserId;
}
}
private static IEnumerable<string> ReadPermissionKeys(string json, IEnumerable<string> fallback)
{