merged from gramsrv upstream

This commit is contained in:
onysd 2026-09-01 12:06:31 +03:00
parent 79c64ee916
commit 21a0856587
651 changed files with 54774 additions and 4590 deletions

View file

@ -19,6 +19,22 @@ import (
const accountDeletionDelay = 7 * 24 * time.Hour
func (s *Service) RevenueWithdrawalPasswordState(ctx context.Context, userID int64) (domain.RevenueWithdrawalPasswordState, error) {
if s == nil || s.lifecycle == nil || userID == 0 {
return domain.RevenueWithdrawalPasswordState{}, fmt.Errorf("revenue withdrawal password state is unavailable")
}
snapshot, found, err := s.lifecycle.AccountDeletionSnapshot(ctx, userID)
if err != nil {
return domain.RevenueWithdrawalPasswordState{}, err
}
if !found {
return domain.RevenueWithdrawalPasswordState{}, domain.ErrUserNotFound
}
return domain.RevenueWithdrawalPasswordState{
HasPassword: snapshot.HasPassword, PasswordChangedAt: snapshot.PasswordUpdatedAt,
}, nil
}
// DeleteAccount implements the official 2FA deletion decision. A supplied and
// valid SRP proof always deletes immediately. Without a proof, an account whose
// password is older than seven days and which was active during the last seven
@ -345,17 +361,3 @@ func (s *Service) SweepDueAccountDeletions(ctx context.Context, now time.Time, l
}
return out, nil
}
func (s *Service) ClaimAccountDeletionNotifications(ctx context.Context, now time.Time, limit int, lease time.Duration) ([]domain.AccountDeletionNotification, error) {
if s == nil || s.lifecycle == nil {
return nil, nil
}
return s.lifecycle.ClaimAccountDeletionNotifications(ctx, now, limit, lease)
}
func (s *Service) CompleteAccountDeletionNotification(ctx context.Context, id int64, now time.Time) error {
if s == nil || s.lifecycle == nil {
return nil
}
return s.lifecycle.CompleteAccountDeletionNotification(ctx, id, now)
}

View file

@ -141,9 +141,3 @@ func (f *fakeAccountLifecycleStore) CancelAccountDeletion(_ context.Context, use
func (*fakeAccountLifecycleStore) DueAccountDeletions(context.Context, time.Time, int) ([]domain.AccountDeletionCandidate, error) {
return nil, nil
}
func (*fakeAccountLifecycleStore) ClaimAccountDeletionNotifications(context.Context, time.Time, int, time.Duration) ([]domain.AccountDeletionNotification, error) {
return nil, nil
}
func (*fakeAccountLifecycleStore) CompleteAccountDeletionNotification(context.Context, int64, time.Time) error {
return nil
}

View file

@ -15,18 +15,6 @@ import (
"telesrv/internal/store"
)
type reliablePhoneChangeDispatcher interface {
UsesReliableDispatch() bool
}
func (s *Service) PhoneChangeUsesReliableDispatch() bool {
if s == nil {
return false
}
reporter, ok := s.phoneChanges.(reliablePhoneChangeDispatcher)
return ok && reporter.UsesReliableDispatch()
}
// SendChangePhoneCode 创建只允许当前 user + perm auth_key 消费的改号验证码。
// CodeStore 会按 purpose+user+auth_key+phone 原子轮换:同一作用域的新请求
// 立即使旧 hash 失效,避免 Android 返回重进页面时留下并行有效验证码。

View file

@ -147,7 +147,7 @@ func TestPhoneChangeScopesCodeAndPersistsDurableEvent(t *testing.T) {
if err != nil {
t.Fatalf("change phone after session reconnect: %v", err)
}
if !result.Changed || result.User.Phone != "15550012002" || result.Event.Type != domain.UpdateEventUserPhone || result.Event.Phone != "15550012002" || result.Event.Pts != 1 {
if !result.Changed || result.User.Phone != "15550012002" {
t.Fatalf("change result = %+v", result)
}
if got := f.changes.lastRequest().ExcludeAuthKeyID; got != rawAuthKeyID {
@ -160,7 +160,7 @@ func TestPhoneChangeScopesCodeAndPersistsDurableEvent(t *testing.T) {
t.Fatalf("new phone resolves to %+v found=%v", got, found)
}
events, err := f.events.ListAfter(f.ctx, f.user.ID, 0, 10)
if err != nil || len(events) != 1 || events[0].Type != domain.UpdateEventUserPhone || events[0].Phone != "15550012002" {
if err != nil || len(events) != 0 {
t.Fatalf("durable events = %+v err=%v", events, err)
}
if _, found, _ := f.codes.Get(f.ctx, hash); found {
@ -194,6 +194,26 @@ func TestPhoneChangeRejectsOccupiedAndCrossAuthCode(t *testing.T) {
}
}
func TestPhoneChangeRejectsOccupiedNationalTrunkVariant(t *testing.T) {
f := newPhoneChangeFixture(t)
occupied, err := f.users.Create(f.ctx, domain.User{AccessHash: 103, Phone: "989981679461", FirstName: "Iran"})
if err != nil {
t.Fatalf("create occupied user: %v", err)
}
if _, _, err := f.service.SendChangePhoneCode(
f.ctx,
f.user.ID,
f.authKeyID,
77,
"+98 0998 167 9461",
); !errors.Is(err, domain.ErrPhoneNumberOccupied) {
t.Fatalf("occupied trunk variant err = %v", err)
}
if got, found, err := f.users.ByPhone(f.ctx, "989981679461"); err != nil || !found || got.ID != occupied.ID {
t.Fatalf("canonical owner user=%+v found=%v err=%v", got, found, err)
}
}
func TestPhoneChangeWrongCodeExhaustsAttempts(t *testing.T) {
f := newPhoneChangeFixture(t)
hash, _, err := f.service.SendChangePhoneCode(f.ctx, f.user.ID, f.authKeyID, 77, "15550012005")
@ -233,12 +253,12 @@ func TestPhoneChangeNewSendInvalidatesPreviousHash(t *testing.T) {
t.Fatalf("new hash change: %v", err)
}
events, err := f.events.ListAfter(f.ctx, f.user.ID, 0, 10)
if err != nil || len(events) != 1 || events[0].Type != domain.UpdateEventUserPhone {
if err != nil || len(events) != 0 {
t.Fatalf("events = %+v err=%v", events, err)
}
}
func TestPhoneChangeConcurrentReplayAppendsOneEvent(t *testing.T) {
func TestPhoneChangeConcurrentReplayChangesOnceWithoutPTSEvent(t *testing.T) {
f := newPhoneChangeFixture(t)
hash, _, err := f.service.SendChangePhoneCode(f.ctx, f.user.ID, f.authKeyID, 77, "15550012007")
if err != nil {
@ -273,7 +293,7 @@ func TestPhoneChangeConcurrentReplayAppendsOneEvent(t *testing.T) {
t.Fatalf("successes=%d expired=%d", successes, expired)
}
events, err := f.events.ListAfter(f.ctx, f.user.ID, 0, 10)
if err != nil || len(events) != 1 || events[0].Pts != 1 {
if err != nil || len(events) != 0 {
t.Fatalf("events = %+v err=%v", events, err)
}
}

View file

@ -1053,6 +1053,12 @@ func (s *Service) SetLoginEmail(ctx context.Context, userID int64, email string)
return s.passwords.Save(ctx, userID, settings)
}
// ValidLoginEmail exposes the same normalization/shape gate to trusted
// administrative dry-runs without duplicating the address policy.
func (s *Service) ValidLoginEmail(email string) bool {
return validLoginEmail(normalizeLoginEmail(email))
}
// LoginEmail 返回已登录用户的登录邮箱原始地址(用于 verifyEmail 回显 emailVerified.email)。
func (s *Service) LoginEmail(ctx context.Context, userID int64) (string, bool, error) {
if s == nil || s.passwords == nil || userID == 0 {