feat(api): sign-in, refresh rotation and bootstrap admin (M1)
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>
This commit is contained in:
mrleo1nidandClaude Opus 5.5 committed 2026-10-20 06:12:25 +03:00
1 parent b6def39e29
commit ed4a492dba
37 files changed
+5826 -49

No files matched your search

+94
View File
@@ -0,0 +1,94 @@
package auth
import "github.com/google/uuid"
// Матрица прав из docs/auth-and-roles.md. Проверяется в сервисном слое, а не только в middleware.
// Permission — право, не зависящее от целевого пользователя.
type Permission int
// Глобальные права.
const (
PermAdminPanel Permission = iota // открыть админку
PermListUsers // видеть список пользователей
PermManageLibrary // хранилища, сканирование, переиндексация
PermViewLibraryStatus // статус библиотеки (просмотр)
PermSuggestTracks // предлагать треки, видеть свои предложения
PermReviewUploads // очередь, одобрение, правка метаданных, лимиты
PermSystemStatus // статус рекомендательной системы, аудит
)
var permissions = map[Permission][]Role{
PermAdminPanel: {RoleModerator, RoleAdmin},
PermListUsers: {RoleModerator, RoleAdmin},
PermManageLibrary: {RoleAdmin},
PermViewLibraryStatus: {RoleModerator, RoleAdmin},
PermSuggestTracks: {RoleModerator, RoleAdmin},
PermReviewUploads: {RoleAdmin},
PermSystemStatus: {RoleAdmin},
}
// Can сообщает, есть ли у роли глобальное право.
func Can(r Role, p Permission) bool {
for _, allowed := range permissions[p] {
if allowed == r {
return true
}
}
return false
}
// UserAction — действие над учётной записью другого пользователя.
type UserAction int
// Действия над пользователями.
const (
UserCreate UserAction = iota // создать пользователя с ролью target
UserEdit // изменить отображаемое имя
UserResetPassword // сбросить пароль
UserBlock // заблокировать / разблокировать
UserChangeRole // сменить роль (target — текущая роль)
UserDelete // удалить
)
// Actor — кто действует (или над кем). ID нужен, чтобы запретить действия над собой.
type Actor struct {
ID uuid.UUID
Role Role
}
// CanManageUser — может ли actor выполнить action над target.
// Модератор управляет только обычными пользователями; роли и удаление — только админ.
// Блокировать, удалять себя и менять себе роль нельзя никому: так не потерять доступ случайно.
// Правило «последний админ» зависит от данных и проверяется в сервисе.
func CanManageUser(actor, target Actor, action UserAction) bool {
self := actor.ID != uuid.Nil && actor.ID == target.ID
if self && (action == UserBlock || action == UserDelete || action == UserChangeRole) {
return false
}
switch actor.Role {
case RoleAdmin:
return true
case RoleModerator:
switch action {
case UserCreate, UserEdit, UserResetPassword, UserBlock:
return target.Role == RoleUser
default:
return false
}
default:
return false
}
}
// CanAssignRole — может ли actor выдать роль newRole (при создании или смене роли).
func CanAssignRole(actor Role, newRole Role) bool {
switch actor {
case RoleAdmin:
return newRole.Valid()
case RoleModerator:
return newRole == RoleUser
default:
return false
}
}