Files
pipu-music/services/api/internal/auth/store.go
T
mrleo1nidandClaude Opus 5.5 978f03b8f8
ci / api / api (push) Successful in 40s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 55s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 1m10s
ci / db / db (push) Successful in 2m35s
ci / ts / ts (packages + web) (push) Successful in 1m23s
feat(admin): reset a user's recommendations from the users panel
- POST /admin/users/{id}/reset-recommendations clears the taste profile
  (onboarding, CF vector, bandit), play stats and wave impressions; likes,
  dislikes and history stay, wave sessions stay so a playing wave keeps going
- moderators may reset plain users, admins anyone including themselves;
  the reset is written to the audit log
- users panel gets the menu action with a confirmation; docs updated

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 08:52:31 +03:00

112 lines
5.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package auth
import (
"context"
"errors"
"time"
"github.com/google/uuid"
)
// Ошибки хранилища.
var (
ErrNotFound = errors.New("auth: not found")
ErrUsernameTaken = errors.New("auth: username taken")
)
// ProfileUpdate — изменяемые пользователем поля профиля. nil — не менять.
type ProfileUpdate struct {
DisplayName **string // *nil — сбросить имя
Locale *string
UIPrefs *UIPrefs
}
// UserFilter — параметры списка пользователей.
type UserFilter struct {
Query string // подстрока логина или имени
Role *Role
After string // логин последнего элемента предыдущей страницы
Limit int
}
// UserUpdate — изменения из админки. nil — не менять.
type UserUpdate struct {
DisplayName **string
Role *Role
IsBlocked *bool
RevokeTokens bool
}
// Store — доступ к users, refresh_tokens, audit_log и app_settings.
type Store interface {
// WithTx выполняет fn в транзакции; Store внутри fn работает в ней же.
WithTx(ctx context.Context, fn func(Store) error) error
CountAdmins(ctx context.Context) (int, error)
CreateUser(ctx context.Context, u User, createdBy *uuid.UUID) error
GetUserByID(ctx context.Context, id uuid.UUID) (User, error)
GetUserByUsername(ctx context.Context, username string) (User, error)
RecordLoginFailure(ctx context.Context, id uuid.UUID, failedCount int, lockedUntil *time.Time) error
// RecordLoginSuccess сбрасывает счётчик неудач; newHash != nil — пересчитанный хэш пароля.
RecordLoginSuccess(ctx context.Context, id uuid.UUID, at time.Time, newHash *string) error
// SetPassword меняет хэш, снимает must_change_password и увеличивает token_version.
SetPassword(ctx context.Context, id uuid.UUID, hash string) error
// SetCredentials меняет логин и хэш, снимает must_change_credentials и увеличивает token_version.
SetCredentials(ctx context.Context, id uuid.UUID, username, hash string) error
UpdateProfile(ctx context.Context, id uuid.UUID, upd ProfileUpdate) (User, error)
InsertRefreshToken(ctx context.Context, t RefreshToken) error
// GetRefreshTokenForUpdate находит токен по хэшу и блокирует строку до конца транзакции.
GetRefreshTokenForUpdate(ctx context.Context, hash []byte) (RefreshToken, error)
MarkRefreshTokenUsed(ctx context.Context, id uuid.UUID, at time.Time) error
RevokeFamily(ctx context.Context, familyID uuid.UUID, at time.Time) error
// RevokeUserSessions отзывает все семьи пользователя.
RevokeUserSessions(ctx context.Context, userID uuid.UUID, at time.Time) error
// ListSessions — активные семьи пользователя: есть неиспользованный, неотозванный, не истёкший токен.
ListSessions(ctx context.Context, userID uuid.UUID, now time.Time) ([]Session, error)
// ListUsers — страница пользователей по возрастанию логина, после логина f.After.
ListUsers(ctx context.Context, f UserFilter) ([]User, error)
// LockActiveAdmins блокирует строки активных админов до конца транзакции и возвращает их число:
// два параллельных понижения не оставят систему без админа.
LockActiveAdmins(ctx context.Context) (int, error)
// UpdateUser применяет изменения админки; RevokeTokens увеличивает token_version.
UpdateUser(ctx context.Context, id uuid.UUID, upd UserUpdate) (User, error)
// ResetPassword ставит временный пароль: must_change_password, сброс блокировки входа, token_version + 1.
ResetPassword(ctx context.Context, id uuid.UUID, hash string) error
DeleteUser(ctx context.Context, id uuid.UUID) error
// ResetRecommendations стирает то, из чего рекомендатор строит вкус: профиль (онбординг, CF, бандит),
// агрегаты прослушиваний и показы волны. Лайки, дизлайки и история остаются.
ResetRecommendations(ctx context.Context, id uuid.UUID) error
InsertAudit(ctx context.Context, id uuid.UUID, e AuditEntry) error
// ListAudit — записи журнала новее-первыми, с id меньше before (uuid.Nil — с начала).
ListAudit(ctx context.Context, before uuid.UUID, limit int) ([]AuditRecord, error)
GetSettingBool(ctx context.Context, key string) (bool, error)
SetSetting(ctx context.Context, key string, value any) error
}
// Limiter ограничивает частоту попыток входа.
type Limiter interface {
// Allow учитывает попытку по ключу. retryAfter — секунды до сброса окна, если попытка отклонена.
Allow(ctx context.Context, key string, limit int, window time.Duration) (ok bool, retryAfter int, err error)
}
// Clock — источник времени (подменяется в тестах).
type Clock interface{ Now() time.Time }
// IDGen — генератор UUIDv7.
type IDGen interface{ New() uuid.UUID }
// SystemClock — реальное время в UTC.
type SystemClock struct{}
// Now возвращает текущее время.
func (SystemClock) Now() time.Time { return time.Now().UTC() }
// UUIDv7 — генератор сортируемых по времени UUID.
type UUIDv7 struct{}
// New возвращает новый UUIDv7.
func (UUIDv7) New() uuid.UUID { return uuid.Must(uuid.NewV7()) }