Files
pipu-music/services/api/internal/auth/store.go
T
mrleo1nidandClaude Opus 5.5 f075863f15
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 47s
ci / ts / ts (packages + web) (push) Successful in 38s
ci / api / api (push) Successful in 18s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 35s
feat(api): user management and audit log for admins (M1)
- /admin/users: list with search, role filter and cursor pages; create
  with a one-time temporary password; rename, change role, block;
  reset password; delete
- every change is checked against the role policy, a forbidden field
  rejects the whole patch; the last active admin is locked in the
  transaction and cannot be demoted, blocked or deleted
- role change and blocking revoke access tokens at once; blocking and
  password reset also end all sessions
- /admin/audit: newest-first journal with actor names
- path and query parameter errors are problem+json now

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 06:18:02 +03:00

109 lines
5.5 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
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()) }