package api import ( "context" "crypto/sha256" "database/sql" "encoding/hex" "time" "github.com/cosmic-clash/cosmic-clash/server/domain" "github.com/cosmic-clash/cosmic-clash/server/steam" "github.com/cosmic-clash/cosmic-clash/server/store" ) // SteamTicketVerifier is the boundary to Valve. Keeping it an interface means // the production login path can be exercised end to end with the external call // stubbed, instead of only through a fake login provider that skips the whole // flow. type SteamTicketVerifier interface { Verify(ctx context.Context, ticket string) (steam.Identity, error) } // SteamLogin is the production SteamLoginProvider: verify the ticket with // Valve, then resolve the verified Steam ID to a durable player ID. type SteamLogin struct { DB *sql.DB Verifier SteamTicketVerifier } // PlayerIDForSteamID derives the durable player ID for a Steam ID on first // sign-in. It is a hash rather than the Steam ID itself so player IDs, which // appear in rosters and logs, do not restate the platform identifier. func PlayerIDForSteamID(steamID string) string { digest := sha256.Sum256([]byte("cosmic-clash/player/" + steamID)) return "player-" + hex.EncodeToString(digest[:12]) } func (s SteamLogin) Authenticate(ctx context.Context, ticket string, _ time.Time) (domain.VerifiedIdentity, error) { if s.DB == nil || s.Verifier == nil { return domain.VerifiedIdentity{}, domain.ErrTicketRejected } identity, err := s.Verifier.Verify(ctx, ticket) if err != nil { return domain.VerifiedIdentity{}, err } // A returning player keeps the player ID they already had, so ratings, // penalties and bans follow the account rather than the session. playerID, err := store.ResolveSteamIdentity(ctx, s.DB, identity.SteamID, PlayerIDForSteamID(identity.SteamID)) if err != nil { return domain.VerifiedIdentity{}, err } return domain.VerifiedIdentity{PlayerID: playerID, SteamID: identity.SteamID}, nil }