Add team roles and content management updates
This commit is contained in:
@@ -0,0 +1,221 @@
|
||||
using Backend.Common;
|
||||
using Backend.Contracts;
|
||||
using Backend.Data;
|
||||
using Backend.Domain;
|
||||
using Backend.Security;
|
||||
using Backend.Services;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Backend.Endpoints;
|
||||
|
||||
public static partial class AuthEndpoints
|
||||
{
|
||||
private const string TeamSessionPrefix = "team:";
|
||||
private const int MinTeamPasswordLength = 10;
|
||||
|
||||
private static async Task<IResult> TeamLogin(
|
||||
HttpContext context,
|
||||
TeamLoginRequest request,
|
||||
AwardsDbContext db,
|
||||
IUserSessionService userSessionService)
|
||||
{
|
||||
var login = NormalizeTeamLogin(request.Login);
|
||||
var password = request.Password ?? string.Empty;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(login) || string.IsNullOrWhiteSpace(password))
|
||||
{
|
||||
return Results.Unauthorized();
|
||||
}
|
||||
|
||||
var member = await db.TeamMembers.FirstOrDefaultAsync(item => item.Login == login, context.RequestAborted);
|
||||
if (member is null
|
||||
|| !member.IsActive
|
||||
|| !DemoCredentialHasher.VerifyPassword(password, member.PasswordHash, member.PasswordSalt))
|
||||
{
|
||||
return Results.Unauthorized();
|
||||
}
|
||||
|
||||
member.LastLoginAt = DateTimeOffset.UtcNow;
|
||||
var session = await userSessionService.CreateSessionAsync(
|
||||
BuildTeamSessionId(member.Login),
|
||||
member.DisplayName,
|
||||
member.Role,
|
||||
RequestMetadataReader.Read(context),
|
||||
context.RequestAborted);
|
||||
|
||||
await db.SaveChangesAsync(context.RequestAborted);
|
||||
return Results.Ok(await ToAuthSessionDtoAsync(db, session, member.MustChangePassword, context.RequestAborted));
|
||||
}
|
||||
|
||||
private static async Task<IResult> TeamTwitchLogin(
|
||||
HttpContext context,
|
||||
TeamTwitchLoginRequest request,
|
||||
AwardsDbContext db,
|
||||
IUserSessionService userSessionService)
|
||||
{
|
||||
var twitchUserId = NormalizeTwitchUserId(request.TwitchUserId);
|
||||
if (string.IsNullOrWhiteSpace(twitchUserId))
|
||||
{
|
||||
return Results.Unauthorized();
|
||||
}
|
||||
|
||||
var member = await db.TeamMembers.FirstOrDefaultAsync(
|
||||
item => item.BoundTwitchUserId == twitchUserId,
|
||||
context.RequestAborted);
|
||||
if (member is null || !member.IsActive)
|
||||
{
|
||||
return Results.Unauthorized();
|
||||
}
|
||||
|
||||
member.LastLoginAt = DateTimeOffset.UtcNow;
|
||||
var session = await userSessionService.CreateSessionAsync(
|
||||
twitchUserId,
|
||||
member.DisplayName,
|
||||
member.Role,
|
||||
RequestMetadataReader.Read(context),
|
||||
context.RequestAborted);
|
||||
|
||||
await db.SaveChangesAsync(context.RequestAborted);
|
||||
return Results.Ok(await ToAuthSessionDtoAsync(db, session, member.MustChangePassword, context.RequestAborted));
|
||||
}
|
||||
|
||||
private static async Task<IResult> ChangePassword(
|
||||
HttpContext context,
|
||||
ChangePasswordRequest request,
|
||||
AwardsDbContext db,
|
||||
IUserSessionService userSessionService)
|
||||
{
|
||||
var session = await userSessionService.ResolveSessionAsync(context, context.RequestAborted);
|
||||
if (session is null)
|
||||
{
|
||||
return Results.Unauthorized();
|
||||
}
|
||||
|
||||
var member = await FindTeamMemberForSessionAsync(db, session, context.RequestAborted);
|
||||
if (member is null || !member.IsActive)
|
||||
{
|
||||
return Results.BadRequest(new { message = "Dieser Account nutzt keinen aktiven Team-Login." });
|
||||
}
|
||||
|
||||
var currentPassword = request.CurrentPassword ?? string.Empty;
|
||||
var newPassword = request.NewPassword?.Trim() ?? string.Empty;
|
||||
if (!DemoCredentialHasher.VerifyPassword(currentPassword, member.PasswordHash, member.PasswordSalt))
|
||||
{
|
||||
return Results.BadRequest(new { message = "Das aktuelle Passwort stimmt nicht." });
|
||||
}
|
||||
|
||||
if (newPassword.Length < MinTeamPasswordLength)
|
||||
{
|
||||
return Results.BadRequest(new { message = $"Das neue Passwort muss mindestens {MinTeamPasswordLength} Zeichen lang sein." });
|
||||
}
|
||||
|
||||
if (DemoCredentialHasher.FixedTimePlainTextEquals(currentPassword, newPassword))
|
||||
{
|
||||
return Results.BadRequest(new { message = "Das neue Passwort muss sich vom aktuellen Passwort unterscheiden." });
|
||||
}
|
||||
|
||||
var credentials = DemoCredentialHasher.HashPassword(newPassword);
|
||||
member.PasswordHash = credentials.Hash;
|
||||
member.PasswordSalt = credentials.Salt;
|
||||
member.MustChangePassword = false;
|
||||
member.UpdatedAt = DateTimeOffset.UtcNow;
|
||||
member.UpdatedByTwitchId = session.TwitchUserId;
|
||||
session.DisplayName = member.DisplayName;
|
||||
session.Role = AdminRoles.Normalize(member.Role);
|
||||
|
||||
await db.SaveChangesAsync(context.RequestAborted);
|
||||
return Results.Ok(await ToAuthSessionDtoAsync(db, session, false, context.RequestAborted));
|
||||
}
|
||||
|
||||
private static async Task<IResult> BindTeamTwitch(
|
||||
HttpContext context,
|
||||
BindTeamTwitchRequest request,
|
||||
AwardsDbContext db,
|
||||
IUserSessionService userSessionService)
|
||||
{
|
||||
var session = await userSessionService.ResolveSessionAsync(context, context.RequestAborted);
|
||||
if (session is null)
|
||||
{
|
||||
return Results.Unauthorized();
|
||||
}
|
||||
|
||||
var member = await FindTeamMemberForSessionAsync(db, session, context.RequestAborted);
|
||||
if (member is null || !member.IsActive)
|
||||
{
|
||||
return Results.BadRequest(new { message = "Dieser Account ist kein aktiver Team-Login." });
|
||||
}
|
||||
|
||||
if (member.MustChangePassword)
|
||||
{
|
||||
return Results.BadRequest(new { message = "Bitte ändere zuerst dein temporäres Passwort." });
|
||||
}
|
||||
|
||||
var twitchUserId = NormalizeTwitchUserId(request.TwitchUserId);
|
||||
var twitchDisplayName = NormalizeTwitchDisplayName(request.DisplayName);
|
||||
if (string.IsNullOrWhiteSpace(twitchUserId))
|
||||
{
|
||||
return Results.BadRequest(new { message = "Bitte gib deine private Twitch-ID an." });
|
||||
}
|
||||
|
||||
var alreadyBound = await db.TeamMembers.AnyAsync(
|
||||
item => item.Id != member.Id && item.BoundTwitchUserId == twitchUserId,
|
||||
context.RequestAborted);
|
||||
if (alreadyBound)
|
||||
{
|
||||
return Results.Conflict(new { message = "Diese Twitch-ID ist bereits mit einem Team-Account verbunden." });
|
||||
}
|
||||
|
||||
var previousTwitchUserId = member.BoundTwitchUserId;
|
||||
if (!string.IsNullOrWhiteSpace(previousTwitchUserId)
|
||||
&& !string.Equals(previousTwitchUserId, twitchUserId, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
foreach (var oldSession in db.UserSessions.Where(item => item.TwitchUserId == previousTwitchUserId))
|
||||
{
|
||||
oldSession.IsActive = false;
|
||||
}
|
||||
}
|
||||
|
||||
member.BoundTwitchUserId = twitchUserId;
|
||||
member.BoundTwitchDisplayName = string.IsNullOrWhiteSpace(twitchDisplayName) ? twitchUserId : twitchDisplayName;
|
||||
member.TwitchBoundAt = DateTimeOffset.UtcNow;
|
||||
member.UpdatedAt = DateTimeOffset.UtcNow;
|
||||
member.UpdatedByTwitchId = session.TwitchUserId;
|
||||
|
||||
await db.SaveChangesAsync(context.RequestAborted);
|
||||
return Results.Ok(await ToAuthSessionDtoAsync(db, session, false, context.RequestAborted));
|
||||
}
|
||||
|
||||
private static string BuildTeamSessionId(string login) =>
|
||||
$"{TeamSessionPrefix}{login}";
|
||||
|
||||
internal static string ReadTeamLoginFromSession(string twitchUserId) =>
|
||||
twitchUserId.StartsWith(TeamSessionPrefix, StringComparison.OrdinalIgnoreCase)
|
||||
? NormalizeTeamLogin(twitchUserId[TeamSessionPrefix.Length..])
|
||||
: string.Empty;
|
||||
|
||||
internal static async Task<TeamMember?> FindTeamMemberForSessionAsync(
|
||||
AwardsDbContext db,
|
||||
UserSession session,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var login = ReadTeamLoginFromSession(session.TwitchUserId);
|
||||
if (!string.IsNullOrWhiteSpace(login))
|
||||
{
|
||||
return await db.TeamMembers.FirstOrDefaultAsync(item => item.Login == login, cancellationToken);
|
||||
}
|
||||
|
||||
var twitchUserId = NormalizeTwitchUserId(session.TwitchUserId);
|
||||
return string.IsNullOrWhiteSpace(twitchUserId)
|
||||
? null
|
||||
: await db.TeamMembers.FirstOrDefaultAsync(item => item.BoundTwitchUserId == twitchUserId, cancellationToken);
|
||||
}
|
||||
|
||||
private static string NormalizeTeamLogin(string value) =>
|
||||
value.Trim().TrimStart('@').ToLowerInvariant();
|
||||
|
||||
private static string NormalizeTwitchUserId(string? value) =>
|
||||
(value ?? string.Empty).Trim().TrimStart('@').ToLowerInvariant();
|
||||
|
||||
private static string NormalizeTwitchDisplayName(string? value) =>
|
||||
(value ?? string.Empty).Trim();
|
||||
}
|
||||
Reference in New Issue
Block a user