- user_repo.create(): keep the TxFromContext fast path, but restore
tolerance for dbent.ErrTxStarted in the self-owned-transaction branch.
ent's Client.Tx only inspects the driver type, so a repository built
from a tx-bound client (client-injected transactions, e.g. the
integration fixture testEntTx + tx.Client()) hits ErrTxStarted; reuse
that client instead of failing. Fixes the two red integration tests in
allowed_groups_contract_integration_test.go.
- createUserAndClaimInvitation: roll back via defer (matching the OAuth
registration precedent) so a panic inside the transaction cannot leak
the connection.
- settingRepoStub: guard call counters and state with a mutex; the new
concurrency regression test exercises it from multiple goroutines and
the unsynchronized counters were flagged by -race.
RegisterWithVerification checked CanUse() and then marked the code used in
two separate, non-transactional steps; the second step's failure was
swallowed ("invitation code mark failure does not affect registration").
Concurrent registrations with the same invitation code could all pass the
check and each create an account, turning a one-time invitation code into
an unlimited account factory (TOCTOU race).
Fix:
- AuthService: create user and claim the invitation code inside one DB
transaction (createUserAndClaimInvitation). The claim reuses
redeemRepo.Use's conditional UPDATE (WHERE status='unused'); losers are
rejected with INVITATION_CODE_INVALID and their transaction (including
the user insert) is rolled back. No-code registration path unchanged.
- userRepository.create: explicitly join an outer ent transaction via
TxFromContext instead of relying on Client.Tx returning ErrTxStarted
(ent's Tx never inspects the context, so the old reuse branch was dead
code and user inserts always committed in their own transaction,
leaving orphan users behind when the outer transaction rolled back).
Regression tests:
- unit: concurrent register with one invitation code must succeed exactly
once (8 goroutines -> 1 success, 7 x INVITATION_CODE_INVALID)
- integration: outer-tx rollback removes user and releases the claim;
commit persists both atomically
PR #5423 relaxed the email suffix whitelist: once a whitelist is
configured, non-whitelisted registrable domains are each allowed to
register one account. That behavior activated unconditionally.
Add registration_email_domain_quota_enabled (default false) to gate it:
- Off (default): restore pre-#5423 strict whitelist semantics — with a
non-empty whitelist, non-whitelisted domains are rejected with
EMAIL_SUFFIX_NOT_ALLOWED; the register/verify views restore the
client-side whitelist pre-check and allowed-domain hint.
- On: keep #5423 behavior — one account per non-whitelisted registrable
domain (EMAIL_DOMAIN_REGISTRATION_LIMIT).
- Empty whitelist keeps allowing all domains in both states.
Gating lives in validateRegistrationEmailQuota and (as a race-safety
backstop) createUserWithRegistrationEmailGuard; the repository-level
domain lock + in-tx recheck is unchanged. The admin update field is
*bool (omitted = keep current) so stale full-payload saves cannot
silently flip the switch. Email binding and OAuth auto-signup keep
their strict policy, and pending-OAuth bind-login for existing
accounts is unaffected because the handler resolves existing emails
before the quota check.
Frontend adds the toggle to admin settings (zh/en copy; whitelist hint
restored to strict wording, quota wording moved to the new toggle) and
exposes the flag via public settings + SSR injection payload.
Tests: #5423 quota tests now enable the switch explicitly; new
default-off regression tests cover register/send-code/async/pending
OAuth/OIDC create-account plus both register views; API contract JSON
and the injection drift guard are updated.
UserRepository.Update and APIKeyRepository.Update rewrote the whole row on
every call, regardless of which fields the caller meant to change. Several
columns on those tables are maintained by dedicated atomic paths (balance
deduction, quota and rate-limit counters, limit adjustments, activity
timestamps), so a caller holding a slightly older snapshot could silently
roll them back - a lost update.
Both methods now take an explicit column mask and persist only the columns
the caller declares; everything else keeps its current database value.
- All user and API-key call sites declare exactly what they mutate, which
turns admin edits and profile saves into genuine partial updates.
- Email uniqueness locking/lookup and allowed_groups sync only run when
those fields are part of the update.
- UserUpdateFields deliberately has no balance/total_recharged members, so
Update cannot touch them. New AdjustBalance/SetBalance apply the change in
a single statement and return before/after values; admin balance
adjustment uses them instead of read-modify-write.
- promo_codes.used_count is no longer written by Update; it is only ever
incremented by the redemption path.
- The billing hot path that marks an API key quota-exhausted writes only
status.
- Dropped a no-op row write in RevokeAllUserTokens: users has no
token_version column, so it persisted nothing while still overwriting
concurrently-updated columns.
Adds integration coverage that a stale snapshot cannot revert concurrent
atomic writes, and unit coverage pinning the column set each entry point
declares.
Registration duplicate checks compared the email verbatim (lowercase +
trim only), so a single inbox could spawn unlimited accounts via provider
alias features: plus addressing (user+tag@gmail.com) and the Gmail dot
trick (u.s.e.r@gmail.com) both deliver to the same mailbox but were seen
as distinct, verifiable addresses. This lets abusers bulk-register to farm
signup grants while the domain whitelist and email verification pass.
Add NormalizeEmailForAliasDedup to collapse these variants to a single
"inbox identity" and use it (via existsByEmailOrAlias) on the three local
email-registration paths: register, send-verify-code, and its async
variant. The exact ExistsByEmail check runs first; only on a miss do we
scan the candidate domains (gmail.com/googlemail.com are mutual aliases)
and compare normalized forms. Lookup errors fail closed, matching the
existing check, so the path cannot be bypassed by inducing errors.
Scoped to registration only — email storage, display, login, and delivery
are unchanged. OAuth-bound emails (already provider-verified) are out of
scope.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Drop SetAffiliateService setters and ProvideAuthService /
ProvidePaymentService / ProvideUserHandler wrappers in favor of direct
Wire constructor injection. AffiliateService has no back-edge to
Auth/Payment/User, so the indirection was never required.
- Change RegisterWithVerification's variadic affiliateCode to a fixed
parameter; adjust all call sites.
- Validate aff_code length and charset in BindInviterByCode before any
DB lookup, eliminating timing-side-channel and useless DB roundtrips
on malformed input.
- Make affiliate cache invalidation synchronous; surface Redis errors
via the project logger instead of swallowing them in a detached
goroutine.
- Add an integration test guarding cross-layer tx propagation in
AccrueQuota and a unit test pinning the aff_code format rules.
Add a system-wide "Backend Mode" that disables user self-registration
and self-service while keeping admin panel and API gateway fully
functional. When enabled, only admin can log in; all user-facing
routes return 403.
Backend:
- New setting key `backend_mode_enabled` with atomic cached reads (60s TTL)
- BackendModeUserGuard middleware blocks non-admin authenticated routes
- BackendModeAuthGuard middleware blocks registration/password-reset auth routes
- Login/Login2FA/RefreshToken handlers reject non-admin when enabled
- TokenPairWithUser struct for role-aware token refresh
- 20 unit tests (middleware + service layer)
Frontend:
- Router guards redirect unauthenticated users to /login
- Admin toggle in Settings page
- Login page hides register link and footer in backend mode
- 9 unit tests for router guard logic
- i18n support (en/zh)
27 files changed, 833 insertions(+), 17 deletions(-)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- add default subscriptions to admin settings
- auto-assign subscriptions on register and admin user creation
- add validation/tests and align settings UI with subscription selector patterns