Fix team profile auth recovery
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user