Stage 6: gateway edge (Connect/FlatBuffers over h2c, platform/email/guest auth, sessions, rate-limit, admin passthrough, live push bridge)
New public ingress and the first network edge. Framework + a vertical slice of operations end-to-end; remaining ops reuse the same transcode pattern in Stage 7. Contracts (new module scrabble/pkg): - push.proto (backend->gateway gRPC server-stream) + scrabble.fbs (FlatBuffers edge payloads), committed generated Go; buf/flatc Makefiles (dev-time codegen). Backend: - REST handlers on the /api/v1 groups: internal session endpoints (telegram/guest/email login -> mint, resolve, revoke) and the user slice (profile, submit_play, state, lobby enqueue/poll, chat). - internal/notify in-process Publisher hub + internal/pushgrpc gRPC server (BACKEND_GRPC_ADDR) streaming your_turn/opponent_moved/chat/nudge/match_found; emission in game.commit, social, matchmaker. - migration 00005 accounts.is_guest; guests are durable rows excluded from stats; ProvisionGuest; email-as-login (RequestLoginCode/LoginWithCode). Gateway (new module scrabble/gateway): - Connect Gateway service over h2c (Execute + Subscribe), FlatBuffers<->JSON transcode registry, Telegram initData HMAC validator (seam), session cache, token-bucket rate limiter (3 classes), push fan-out hub, backend REST + push gRPC client, admin Basic-Auth reverse proxy. go.work: use ./pkg, ./gateway + replace scrabble/pkg. CI: gateway/**, pkg/** path filters; unit build/vet/test span all three modules. Docs (PLAN, ARCHITECTURE, FUNCTIONAL+ru, TESTING, READMEs) updated; gateway/pkg unit tests + guest/email-login integration tests.
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
// Package account owns durable internal accounts and their platform/email
|
||||
// identities. First contact from a platform auto-provisions an account bound to
|
||||
// that identity; guests are session-only and never reach this package.
|
||||
// that identity. An ephemeral guest is also a durable account row (the sessions
|
||||
// and game_players foreign keys both require one) but carries no identity and is
|
||||
// flagged is_guest, which excludes it from statistics, friends and history.
|
||||
package account
|
||||
|
||||
import (
|
||||
@@ -52,8 +54,11 @@ type Account struct {
|
||||
HintBalance int
|
||||
BlockChat bool
|
||||
BlockFriendRequests bool
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
// IsGuest marks an ephemeral guest account: a durable row with no identity,
|
||||
// excluded from statistics, friends and history.
|
||||
IsGuest bool
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
// Store is the Postgres-backed query surface for accounts and identities.
|
||||
@@ -176,6 +181,30 @@ func (s *Store) create(ctx context.Context, kind, externalID string) (Account, e
|
||||
return created, nil
|
||||
}
|
||||
|
||||
// guestDisplayName is the display name stamped on a freshly provisioned guest.
|
||||
const guestDisplayName = "Guest"
|
||||
|
||||
// ProvisionGuest creates a fresh ephemeral guest account: a durable row carrying
|
||||
// no identity, flagged is_guest, so it can hold a session and a game seat (both
|
||||
// foreign-key the accounts table) while being excluded from statistics, friends
|
||||
// and history. Guests are not reused — each bootstrap mints a new account.
|
||||
func (s *Store) ProvisionGuest(ctx context.Context) (Account, error) {
|
||||
accountID, err := uuid.NewV7()
|
||||
if err != nil {
|
||||
return Account{}, fmt.Errorf("account: new guest id: %w", err)
|
||||
}
|
||||
stmt := table.Accounts.
|
||||
INSERT(table.Accounts.AccountID, table.Accounts.DisplayName, table.Accounts.IsGuest).
|
||||
VALUES(accountID, guestDisplayName, true).
|
||||
RETURNING(table.Accounts.AllColumns)
|
||||
|
||||
var row model.Accounts
|
||||
if err := stmt.QueryContext(ctx, s.db, &row); err != nil {
|
||||
return Account{}, fmt.Errorf("account: provision guest: %w", err)
|
||||
}
|
||||
return modelToAccount(row), nil
|
||||
}
|
||||
|
||||
// SpendHint atomically decrements the account's hint wallet by one, returning
|
||||
// true when a hint was spent and false when the balance was already empty. The
|
||||
// guarded UPDATE keeps it safe under concurrent spends across the player's games.
|
||||
@@ -210,6 +239,7 @@ func modelToAccount(row model.Accounts) Account {
|
||||
HintBalance: int(row.HintBalance),
|
||||
BlockChat: row.BlockChat,
|
||||
BlockFriendRequests: row.BlockFriendRequests,
|
||||
IsGuest: row.IsGuest,
|
||||
CreatedAt: row.CreatedAt,
|
||||
UpdatedAt: row.UpdatedAt,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user