R6(a): de-stage code, docs, READMEs; split stage6_test
Mechanical, behaviour-preserving removal of Stage N / TODO-N / phase (RN) references from comments, doc-comments, service READMEs, the current-state docs (ARCHITECTURE, FUNCTIONAL+_ru, TESTING, UI_DESIGN), config-file comments, and the .fbs/.proto schema comments. PLAN.md / PRERELEASE.md / CLAUDE.md keep the stage history. - Rename the only stage-named identifiers: registerStage8 -> registerSocialOps, registerStage11 -> registerLinkOps (gateway transcode). - Split stage6_test.go: TestEmailLoginFlow -> email_test.go, TestGuestAutoMatchLeavesNoStats (+ provisionGuest) -> account_test.go. - Regenerated proto bindings (push.pb.go, telegram_grpc.pb.go) from the de-staged .proto comments; FB Go/TS bindings unchanged (flatc strips schema comments). go build/vet/gofmt clean across modules; integration typecheck and pnpm check green.
This commit is contained in:
@@ -24,8 +24,8 @@ import (
|
||||
|
||||
// Identity kinds recognised by the backend. Email is modelled as an identity
|
||||
// alongside platform identities; its confirmed flag is driven by the email
|
||||
// confirm-code flow in a later stage. Robot is a synthetic kind: each pooled
|
||||
// robot opponent is a durable account bound to one robot identity (Stage 5).
|
||||
// confirm-code flow. Robot is a synthetic kind: each pooled
|
||||
// robot opponent is a durable account bound to one robot identity.
|
||||
const (
|
||||
KindTelegram = "telegram"
|
||||
KindEmail = "email"
|
||||
@@ -66,19 +66,19 @@ type Account struct {
|
||||
IsGuest bool
|
||||
// NotificationsInAppOnly confines notifications to the in-app live stream when
|
||||
// true (the default): the platform side-service skips out-of-app push for the
|
||||
// account (Stage 9).
|
||||
// account.
|
||||
NotificationsInAppOnly bool
|
||||
// PaidAccount marks a lifetime one-time-payment account. It is a service field
|
||||
// (no purchase flow yet); an account linking & merge ORs it so a paid status is
|
||||
// never lost when accounts are consolidated (Stage 11).
|
||||
// never lost when accounts are consolidated.
|
||||
PaidAccount bool
|
||||
// MergedInto is the primary account a retired (merged) secondary points at, or
|
||||
// uuid.Nil for a live account. A tombstone keeps the row so the no-cascade
|
||||
// foreign keys of a shared finished game stay valid (Stage 11).
|
||||
// foreign keys of a shared finished game stay valid.
|
||||
MergedInto uuid.UUID
|
||||
// FlaggedHighRateAt is the soft, reversible "suspected high-rate" marker: the
|
||||
// zero time for an unflagged account, otherwise when the gateway-reported
|
||||
// rate-limiter rejections first crossed the sustained threshold (R3). An
|
||||
// rate-limiter rejections first crossed the sustained threshold. An
|
||||
// operator clears it in the admin console; it never gates any request.
|
||||
FlaggedHighRateAt time.Time
|
||||
CreatedAt time.Time
|
||||
@@ -430,7 +430,7 @@ func (s *Store) SpendHint(ctx context.Context, id uuid.UUID) (bool, error) {
|
||||
// FlagHighRate stamps the soft "suspected high-rate" marker with at, only when
|
||||
// the account is not already flagged — the first sustained episode wins, and a
|
||||
// re-flag after an operator clear starts a fresh timestamp. An infra marker, not
|
||||
// a profile edit, so updated_at is untouched; it never gates any request (R3).
|
||||
// a profile edit, so updated_at is untouched; it never gates any request.
|
||||
// It reports whether the flag was newly set.
|
||||
func (s *Store) FlagHighRate(ctx context.Context, id uuid.UUID, at time.Time) (bool, error) {
|
||||
stmt := table.Accounts.
|
||||
|
||||
@@ -33,7 +33,7 @@ var (
|
||||
// ErrInvalidEmail is returned for an unparseable email address.
|
||||
ErrInvalidEmail = errors.New("account: invalid email address")
|
||||
// ErrEmailTaken is returned when the email is already confirmed by another
|
||||
// account; binding it would be a merge, which Stage 11 owns.
|
||||
// account; binding it would be a merge, which the link/merge flow owns.
|
||||
ErrEmailTaken = errors.New("account: email already confirmed by another account")
|
||||
// ErrAlreadyConfirmed is returned when the email is already confirmed by the
|
||||
// requesting account.
|
||||
@@ -52,8 +52,8 @@ var (
|
||||
// Mailer and verifies it, binding a confirmed email identity to the requesting
|
||||
// account. Only the SHA-256 hash of a code is stored (never the plaintext),
|
||||
// matching the session model. Binding an email already confirmed by a different
|
||||
// account is refused (ErrEmailTaken) — merging two accounts is Stage 11 — and
|
||||
// using an email as a login is Stage 6, which reuses this mechanism.
|
||||
// account is refused (ErrEmailTaken) — merging two accounts is the link/merge flow —
|
||||
// and using an email as a login reuses this mechanism.
|
||||
type EmailService struct {
|
||||
store *Store
|
||||
mailer Mailer
|
||||
@@ -128,7 +128,7 @@ func (s *EmailService) ConfirmCode(ctx context.Context, accountID uuid.UUID, ema
|
||||
|
||||
// RequestLoginCode issues a login confirm-code to the account that owns email,
|
||||
// provisioning a fresh (unconfirmed) durable account when the email is new. It is
|
||||
// the unauthenticated email-login entry point (Stage 6) and, unlike RequestCode,
|
||||
// the unauthenticated email-login entry point and, unlike RequestCode,
|
||||
// does not refuse an already-confirmed email — that is the ordinary returning-user
|
||||
// login. The code is mailed to the address, so only its real owner can complete
|
||||
// the login. It returns the target account id for the subsequent LoginWithCode.
|
||||
|
||||
@@ -13,14 +13,14 @@ import (
|
||||
)
|
||||
|
||||
// ErrIdentityTaken is returned when a platform identity being linked already
|
||||
// belongs to another account; the caller turns it into a merge (Stage 11).
|
||||
// belongs to another account; the caller turns it into a merge.
|
||||
var ErrIdentityTaken = errors.New("account: identity already linked to another account")
|
||||
|
||||
// RequestLinkCode issues and mails a confirm-code for email to accountID,
|
||||
// replacing any prior pending code. Unlike RequestCode it never refuses up front
|
||||
// (taken or already-confirmed): possession of the address is the authorization for
|
||||
// a later link or merge, and the merge is only revealed once the code is verified,
|
||||
// so a probe cannot learn whether an address is registered (Stage 11).
|
||||
// so a probe cannot learn whether an address is registered.
|
||||
func (s *EmailService) RequestLinkCode(ctx context.Context, accountID uuid.UUID, email string) error {
|
||||
addr, err := normalizeEmail(email)
|
||||
if err != nil {
|
||||
@@ -94,7 +94,7 @@ func (s *EmailService) verifyPendingCode(ctx context.Context, accountID uuid.UUI
|
||||
|
||||
// AccountIDByIdentity returns the account owning (kind, externalID) and true, or
|
||||
// (uuid.Nil, false) when the identity is free. It backs the platform-identity link
|
||||
// flow (Stage 11).
|
||||
// flow.
|
||||
func (s *Store) AccountIDByIdentity(ctx context.Context, kind, externalID string) (uuid.UUID, bool, error) {
|
||||
acc, err := s.findByIdentity(ctx, kind, externalID)
|
||||
if errors.Is(err, ErrNotFound) {
|
||||
@@ -109,7 +109,7 @@ func (s *Store) AccountIDByIdentity(ctx context.Context, kind, externalID string
|
||||
// AttachIdentity links a new (kind, externalID) identity to an existing account.
|
||||
// A unique-constraint violation means the identity was taken meanwhile, surfaced
|
||||
// as ErrIdentityTaken. It is used to attach a platform identity (e.g. Telegram)
|
||||
// to the current account during linking (Stage 11).
|
||||
// to the current account during linking.
|
||||
func (s *Store) AttachIdentity(ctx context.Context, accountID uuid.UUID, kind, externalID string, confirmed bool) error {
|
||||
id, err := uuid.NewV7()
|
||||
if err != nil {
|
||||
@@ -129,7 +129,7 @@ func (s *Store) AttachIdentity(ctx context.Context, accountID uuid.UUID, kind, e
|
||||
}
|
||||
|
||||
// ClearGuest removes the is_guest flag from accountID, promoting an ephemeral guest
|
||||
// to a durable account once it gains its first identity (Stage 11). It is a no-op
|
||||
// to a durable account once it gains its first identity. It is a no-op
|
||||
// for an already-durable account.
|
||||
func (s *Store) ClearGuest(ctx context.Context, accountID uuid.UUID) error {
|
||||
upd := table.Accounts.UPDATE(table.Accounts.IsGuest, table.Accounts.UpdatedAt).
|
||||
|
||||
@@ -25,16 +25,16 @@ const maxDisplayName = 32
|
||||
|
||||
// maxDisplayNameSpecials caps the total special characters (the "." / "_" separators —
|
||||
// every name rune that is neither a letter nor a space) an editable display name may
|
||||
// carry, so a still-well-formed name cannot be made of mostly punctuation (Stage 17).
|
||||
// carry, so a still-well-formed name cannot be made of mostly punctuation.
|
||||
const maxDisplayNameSpecials = 5
|
||||
|
||||
// maxAwayWindow bounds the daily away window's duration (midnight-wrap aware).
|
||||
const maxAwayWindow = 12 * time.Hour
|
||||
|
||||
// displayNameRe enforces the editable display-name format (Stage 8): Unicode letters
|
||||
// displayNameRe enforces the editable display-name format: Unicode letters
|
||||
// joined by single space / "." / "_" separators, where a "." or "_" may be followed
|
||||
// by a single space. No leading separator and no two adjacent separators (except
|
||||
// "<dot|underscore> <space>"); a single trailing "." is allowed (Stage 17), so
|
||||
// "<dot|underscore> <space>"); a single trailing "." is allowed, so
|
||||
// "Name_P. Last" and "Anna B." are valid, "Name P._Last" is not.
|
||||
var displayNameRe = regexp.MustCompile(`^\p{L}+(?:(?:[._] ?| )\p{L}+)*\.?$`)
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
|
||||
// TestUpdateProfileValidation checks that bad fields are rejected before any
|
||||
// database access, so a nil-backed Store is enough to exercise the guards. It also
|
||||
// confirms UpdateProfile wires the Stage 8 validators (name format, away window,
|
||||
// confirms UpdateProfile wires the validators (name format, away window,
|
||||
// offset/IANA timezone), not just their unit tests in validate_test.go.
|
||||
func TestUpdateProfileValidation(t *testing.T) {
|
||||
s := &Store{}
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
)
|
||||
|
||||
// offsetZoneRe matches a fixed UTC offset like "+03:00" or "-05:30" — the form the
|
||||
// Stage 8 profile editor stores (an offset dropdown rather than an IANA name).
|
||||
// profile editor stores (an offset dropdown rather than an IANA name).
|
||||
var offsetZoneRe = regexp.MustCompile(`^([+-])(\d{2}):(\d{2})$`)
|
||||
|
||||
// parseOffsetZone parses a "±HH:MM" offset into a fixed-offset location, reporting
|
||||
|
||||
@@ -20,7 +20,7 @@ type UserListItem struct {
|
||||
IsGuest bool
|
||||
IsRobot bool
|
||||
// FlaggedHighRateAt is the soft high-rate marker (zero when unflagged), shown
|
||||
// as a badge in the console list (R3).
|
||||
// as a badge in the console list.
|
||||
FlaggedHighRateAt time.Time
|
||||
CreatedAt time.Time
|
||||
}
|
||||
@@ -105,7 +105,7 @@ type FlaggedAccount struct {
|
||||
const flaggedListCap = 200
|
||||
|
||||
// ListFlaggedHighRate returns the accounts carrying the high-rate flag, most
|
||||
// recently flagged first (R3).
|
||||
// recently flagged first.
|
||||
func (s *Store) ListFlaggedHighRate(ctx context.Context) ([]FlaggedAccount, error) {
|
||||
rows, err := s.db.QueryContext(ctx,
|
||||
`SELECT account_id, display_name, flagged_high_rate_at
|
||||
|
||||
Reference in New Issue
Block a user