Connect Steam & import owned games - #105
Open
mforce wants to merge 2 commits into
Open
Conversation
added 2 commits
August 15, 2026 00:04
Implements the spec-first feature from issue #101 (spec v3 after Codex + Claude review). Steam-only now, architected for more stores later. Backend: - New entities + EF migration AddStoreImport: GameStoreConnection, GameStoreOwnedTitle (import provenance ledger, idempotency key (OwnerId, Store, ExternalGameId)), SteamAuthRequest. Games gains (Id, OwnerId) alternate key; ledger uses composite FK (GameId, OwnerId) with Restrict so deleting a game nulls the link instead of violating FK. - SteamStoreImportService: transactional, owner-scoped, idempotent import; disconnect keeps games + ledger. - SteamOpenIdVerifier: strict check_authentication round-trip (endpoint, return_to byte-match, SteamID64, signed coverage) + state+cookie 2nd factor. - SteamStoreEndpoints: public callback + /api/accounts/steam/* group. - GamesEndpoints DELETE nulls ledger GameId before delete. - ISteamClient / ISteamOpenIdVerifier testable seams. Frontend: - services/steam.ts hooks, pages/ImportSteam.tsx (connect -> select -> import), route /import/steam, entry in GamesList. Tests: 11 new xUnit (endpoints, callback, idempotency, owner scoping, disconnect) + 6 Vitest (ImportSteam). Full suite green.
Codex + Claude reviews of PR #105 flagged critical and should-fix issues; the following are now addressed and verified (407 backend tests, 116 client tests, clean client production build): Backend correctness/security: - SteamId64 parser: require /openid/id/<steamid64> (3 segments) so real Steam callbacks no longer fail and crafted claimed-ids can't throw. - OpenID checkid_setup now uses identifier_select instead of a bare prefix. - Callback consumes state + upserts the connection atomically, with a local pre-check before the outbound Steam round-trip; public callback is rate-limited. - Startup SteamAuthRequest sweep is best-effort so existing Postgres (EnsureCreated) DBs don't crash the app at boot. - State tokens now CSPRNG-backed (RandomNumberGenerator) not Guid. - Import cap enforced with HTTP 400; import request null/{} body handled; concurrent-import unique races no longer 500; LastSyncedAt written on successful fetch. - Owned-games preview is a discriminated result ({ status, titles, truncated }) so the client can distinguish ok / empty / provider-unavailable instead of a blank 200 []. - Secure cookie derived from request (plain-HTTP self-hosted compatible). DLC schema hook (provider-agnostic, import stays flat): - Game.ParentGameId nullable self-FK (Restrict) + GameStoreOwnedTitle ParentExternalGameId; migration AddGameDlcParent; docs/data-model.md updated. Tests: - New SteamOpenIdVerifierTests, SteamId64Tests, delete-an-imported-game integration test, unavailable-status coverage; frontend ImportSteam tests updated for the new preview shape + search filter + empty/unavailable states. Dead code: removed unused CompleteConnectAsync and SteamId64.IsValidSteamId.
Owner
Author
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #101
Spec-first:
docs/platform-import.md(v3) reviewed independently by Claude Code and Codex; findings folded in. Steam-only first, architected for more stores later.What
Lets a user link their Steam account (OpenID) and import games they already own into their collection. DLC/free-to-play/products are user-selected (we list all owned apps, they pick). Disconnecting Steam keeps imported games and the provenance ledger.
Backend
AddStoreImport:GameStoreConnection,GameStoreOwnedTitle(idempotency ledger keyed(OwnerId, Store, ExternalGameId)),SteamAuthRequest.Gamesgets(Id, OwnerId)alternate key; ledger uses composite FK(GameId, OwnerId)withRestrict(deleting a game nulls the link rather than violating FK).SteamStoreImportService— transactional, owner-scoped, idempotent import; disconnect keeps games.SteamOpenIdVerifier— strictcheck_authentication(endpoint, return_to byte-match, SteamID64, signed coverage) + state+cookie second factor.SteamStoreEndpoints— public callback + authenticated/api/accounts/steam/*.GamesEndpointsDELETE updated for the new composite FK.ISteamClient/ISteamOpenIdVerifier.Frontend
services/steam.tshooks,pages/ImportSteam.tsx(connect → select → import), route/import/steam, entry inGamesList.Tests
npm run buildpasses.Notes
Platform=Pc,DigitalStore=Steam,Status=Owned.EnsureCreatedtoday — new tables require a fresh DB/reset (documented in issue).