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
- 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>
80 lines
3.8 KiB
Go
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()) }
|