Fix team profile auth recovery
CI - Build & Verify / Build, Typecheck & Hygiene (push) Successful in 54s
CI - Build & Verify / Deploy to award.noveria.net (push) Successful in 1m5s

This commit is contained in:
AzuTear
2026-06-26 20:06:10 +02:00
parent 47fe59c42a
commit aeade132f9
15 changed files with 237 additions and 37 deletions
+5
View File
@@ -36,3 +36,8 @@ public sealed record AuthSessionDto(
string? TeamLogin = null,
string? BoundTwitchUserId = null,
string? BoundTwitchDisplayName = null);
public sealed record TwitchBindingDisconnectResponse(
bool Disconnected,
bool LoggedOut,
AuthSessionDto? Session);
+2 -5
View File
@@ -85,11 +85,8 @@ public static class TeamAccountBootstrapper
changed = true;
}
if (member.CreatedByTwitchId == SeedActor
&& (string.IsNullOrWhiteSpace(member.UpdatedByTwitchId)
|| string.Equals(member.UpdatedByTwitchId, SeedActor, StringComparison.Ordinal))
&& (!string.Equals(member.PasswordHash, seed.Credentials.Value.Hash, StringComparison.Ordinal)
|| !string.Equals(member.PasswordSalt, seed.Credentials.Value.Salt, StringComparison.Ordinal)))
if (!string.Equals(member.PasswordHash, seed.Credentials.Value.Hash, StringComparison.Ordinal)
|| !string.Equals(member.PasswordSalt, seed.Credentials.Value.Salt, StringComparison.Ordinal))
{
member.PasswordHash = seed.Credentials.Value.Hash;
member.PasswordSalt = seed.Credentials.Value.Salt;
@@ -17,6 +17,14 @@ public static partial class AuthEndpoints
return Results.Unauthorized();
}
var teamMember = await FindTeamMemberForSessionAsync(db, session, context.RequestAborted);
if (teamMember is not null)
{
return Results.Json(
new { message = "Team-Accounts werden nicht ueber die automatische Teilnahme-Datenloeschung entfernt." },
statusCode: StatusCodes.Status403Forbidden);
}
var twitchUserId = session.TwitchUserId;
await using var transaction = await db.Database.BeginTransactionAsync(context.RequestAborted);
+5
View File
@@ -33,6 +33,11 @@ public static partial class AuthEndpoints
.WithName("StartTwitchAuthorization")
.WithOpenApi();
group.MapDelete("/twitch/binding", DisconnectTwitchBinding)
.RequireRateLimiting(ApplicationDefaults.AuthRateLimitPolicy)
.WithName("DisconnectTwitchBinding")
.WithOpenApi();
group.MapGet("/twitch/callback", CompleteTwitchAuthorization)
.RequireRateLimiting(ApplicationDefaults.AuthRateLimitPolicy)
.WithName("CompleteTwitchAuthorization")
@@ -212,6 +212,62 @@ public static partial class AuthEndpoints
return RedirectToTwitchCallback(oauthState, "connected");
}
private static async Task<IResult> DisconnectTwitchBinding(
HttpContext context,
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 boundTwitchUserId = NormalizeTwitchUserId(member.BoundTwitchUserId);
if (string.IsNullOrWhiteSpace(boundTwitchUserId))
{
return Results.Ok(new TwitchBindingDisconnectResponse(
false,
false,
await ToAuthSessionDtoAsync(db, session, cancellationToken: context.RequestAborted)));
}
var currentTeamLogin = ReadTeamLoginFromSession(session.TwitchUserId);
var currentSessionUsesBoundTwitch = string.IsNullOrWhiteSpace(currentTeamLogin)
&& string.Equals(NormalizeTwitchUserId(session.TwitchUserId), boundTwitchUserId, StringComparison.OrdinalIgnoreCase);
foreach (var linkedSession in db.UserSessions.Where(item => item.TwitchUserId == boundTwitchUserId))
{
linkedSession.IsActive = false;
}
member.BoundTwitchUserId = null;
member.BoundTwitchDisplayName = null;
member.TwitchBoundAt = null;
member.UpdatedAt = DateTimeOffset.UtcNow;
member.UpdatedByTwitchId = session.TwitchUserId;
if (currentSessionUsesBoundTwitch)
{
session.IsActive = false;
}
await db.SaveChangesAsync(context.RequestAborted);
return Results.Ok(new TwitchBindingDisconnectResponse(
true,
currentSessionUsesBoundTwitch,
currentSessionUsesBoundTwitch
? null
: await ToAuthSessionDtoAsync(db, session, cancellationToken: context.RequestAborted)));
}
private static async Task<IResult> CompleteTwitchTeamLoginAsync(
HttpContext context,
TwitchOAuthState oauthState,
+4
View File
@@ -112,6 +112,10 @@ VTSA_TEAM_OWNER_PASSWORD=<set-secure-owner-password>
VTSA_TEAM_CREATOR_PASSWORD=<set-secure-creator-password>
```
These fixed owner/creator credentials are authoritative on API startup. If a
personal team account gets locked out after an admin reset, redeploying with the
configured environment password restores the login.
Frontend app-wide demo gate:
```text