Контекст: В backend/internal/services/auth.go:85-86, метод Register сначала проверяет существование email, а потом создаёт пользователя. Между этими двумя операциями возможна гонка.
func (s *AuthService) Register(ctx context.Context, input RegisterInput) (*AuthResult, string, error) {
// Шаг 1: Check (Time-of-Check)
existing, _ := s.userRepo.GetByEmail(ctx, input.Email) // ❌ ошибка подавлена
if existing != nil {
return nil, "", ErrEmailExists
}
// ⚡ В этот момент другой запрос может создать пользователя с тем же email
// Шаг 2: Act (Time-of-Use)
if err := s.userRepo.Create(ctx, user); err != nil {
return nil, "", fmt.Errorf("create user: %w", err)
}
}
Две проблемы:
_ игнорирует ошибку GetByEmail. Если БД недоступна — пользователь получит "email already exists" вместо ошибки сервера.Использовать уникальный constraint на уровне БД + проверять ошибку Create:
-- миграция (уже есть)
ALTER TABLE users ADD CONSTRAINT users_email_unique UNIQUE (email);
func (s *AuthService) Register(ctx context.Context, input RegisterInput) (*AuthResult, string, error) {
// ... хеширование пароля, создание User ...
if err := s.userRepo.Create(ctx, user); err != nil {
// Проверяем, что ошибка — нарушение уникального constraint
if isDuplicateEmailError(err) {
return nil, "", ErrEmailExists
}
return nil, "", fmt.Errorf("create user: %w", err)
}
return s.generateTokens(ctx, user)
}
// repository/users.go — проверка ошибки pgx
func isDuplicateEmailError(err error) bool {
var pgErr *pgconn.PgError
return errors.As(err, &pgErr) && pgErr.Code == "23505" // unique_violation
}
| Подход | Плюсы | Минусы |
|---|---|---|
| Unique constraint + проверка ошибки | Атомарно, стандартная практика | Зависит от кода ошибки pgx |
| Транзакция + FOR UPDATE | Можно сделать доп. проверки | Блокировка строки, медленнее |
| Optimistic locking | Нет блокировок | Больше кода |
Code review PhotoPlaces 2026-06. services/auth.go:85-86.
#concurrency #database #security #golang #race-condition #best-practice