Files
pipu-music/services/api/internal/auth/store.go
T
mrleo1nidandClaude Opus 5.5 ed4a492dba
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 1m15s
ci / api / api (push) Successful in 1m31s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 47s
ci / ts / ts (packages + web) (push) Successful in 39s
ci / docker / docker (образы собираются — их сервер собирает сам при обновлении) (push) Successful in 26s
feat(api): sign-in, refresh rotation and bootstrap admin (M1)
- users, refresh_tokens and audit_log schema (migration 00002)
- argon2id passwords with rehash on login, password policy, lockout
  with exponential back-off and a per-IP rate limit in Redis
- Ed25519 JWT access tokens with kid-based key rotation; rotating
  refresh tokens with reuse detection (cookie for web, body for Android)
- bootstrap admin on first start; must_change_credentials and
  must_change_password gate every other endpoint
- /auth/login, /auth/refresh, /auth/logout(-all), /me, /me/password,
  /me/bootstrap-credentials, /me/sessions; server-info reports
  bootstrap_required
- role policy from docs/auth-and-roles.md with table tests
- problem+json errors with field errors; ru/en translations of the codes
- install.sh and update.sh generate JWT_PRIVATE_KEY

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

80 lines
3.8 KiB
Go

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
}
// 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)
InsertAudit(ctx context.Context, id uuid.UUID, e AuditEntry) 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()) }