auth: link Steam to UDID-keyed viewer on first authenticated request

After /tool/signup, the client has a viewer_id but no Steam social row.
The first authenticated request (typically /check/game_start) carries
the Steam ticket; if the SteamId lookup misses but the UDID resolves
to a viewer, attach the Steam social now. Subsequent requests hit the
fast SteamId path. Closes the CheckController.GameStart TODO that was
blocking fresh-client boot.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
gamer147
2026-05-27 14:31:06 -04:00
parent 68367db214
commit 26bb0ac268
2 changed files with 27 additions and 11 deletions

View File

@@ -30,10 +30,6 @@ public class CheckController : SVSimController
});
}
// TODO: spec lists this as anonymous (identity from SHORT_UDID), but the base controller's
// [Authorize] still applies. For now requires a Steam-linked viewer; new-user bootstrap (where
// the server creates a viewer + returns rewrite_viewer_id) is deferred until the boot flow is
// exercised end-to-end with a real client.
[HttpPost("game_start")]
public async Task<GameStartResponse> GameStart(GameStartRequest request)
{

View File

@@ -97,13 +97,33 @@ public class SteamSessionAuthenticationHandler : AuthenticationHandler<SteamAuth
if (viewer is null)
{
// Most common dev-loop cause: DB was re-bootstrapped and this Steam account hasn't
// been re-linked yet. Log loudly with the steam_id so it's obvious what to add back.
Logger.LogWarning(
"Auth: no viewer linked to steamId={SteamId} on {Path}. " +
"Likely you re-bootstrapped the DB without re-linking this Steam account.",
requestJson.SteamId, path);
return AuthenticateResult.Fail("User not found.");
// Find-or-link: first authenticated request after /tool/signup. The client signed up
// anonymously and has no Steam social row yet; if the UDID resolves to a viewer, attach
// Steam to it now so subsequent requests hit the fast SteamId path. The SteamId unique
// index on SocialAccountConnection is the dedup backstop for concurrent first-touch.
Guid? udid = Context.GetUdid();
if (udid is Guid u && u != Guid.Empty)
{
viewer = await _viewerRepository.GetViewerByUdid(u);
if (viewer is not null)
{
await _viewerRepository.LinkSteamToViewer(viewer.Id, requestJson.SteamId);
// Re-read with socials so transition_account_data downstream sees the new link.
viewer = await _viewerRepository.GetViewerWithSocials(viewer.Id) ?? viewer;
Logger.LogInformation(
"Auth: linked steamId={SteamId} to UDID-keyed viewer_id={ViewerId} on {Path} (first-Steam-touch).",
requestJson.SteamId, viewer.Id, path);
}
}
if (viewer is null)
{
Logger.LogWarning(
"Auth: no viewer linked to steamId={SteamId} on {Path}, and no UDID-keyed viewer to link to. " +
"Client must call /tool/signup before authenticated endpoints.",
requestJson.SteamId, path);
return AuthenticateResult.Fail("User not found.");
}
}
// Add viewer to context