feat(bootstrap): первичная настройка инстанса и CLI-команды
- internal/bootstrap: создание инстанс-админа, главного сервера, ролей «Администратор» и «Пользователь» (all permissions / дефолтный набор §6.3), общей текстовой комнаты; повторный запуск идемпотентен - админ получает флаг is_instance_admin и системный бейдж instance_admin - CLI: glchat bootstrap-admin, reset-password, make-admin, remove-admin (работают внутри контейнера через DATA_DIR) - auth: UserByEmail, SetPassword (отзывает сессии), RegisterAdmin (в обход флага регистрации — bootstrap выполняет установщик) - тесты: создание админа и главного сервера, идемпотентность, автовступление нового пользователя с ролью @user
This commit is contained in:
@@ -211,6 +211,85 @@ func (s *Service) Register(ctx context.Context, in RegisterInput) (*store.User,
|
||||
return user, token, session, nil
|
||||
}
|
||||
|
||||
// UserByEmail ищет аккаунт по email через blind index (без расшифровки БД).
|
||||
func (s *Service) UserByEmail(ctx context.Context, email string) (*store.User, error) {
|
||||
return s.userByEmail(ctx, email)
|
||||
}
|
||||
|
||||
// SetPassword меняет пароль без проверки текущего: используется установщиком
|
||||
// и CLI-командой reset-password, где пароль задаёт администратор инстанса.
|
||||
func (s *Service) SetPassword(ctx context.Context, userID uint64, password string) error {
|
||||
if err := s.policy.validate(password); err != nil {
|
||||
return ErrWeakPassword
|
||||
}
|
||||
hash, err := s.hasher.Hash(password)
|
||||
if err != nil {
|
||||
return fmt.Errorf("hash password: %w", err)
|
||||
}
|
||||
if err := s.store.UpdateUserPassword(ctx, userID, hash); err != nil {
|
||||
return err
|
||||
}
|
||||
// Смена пароля администратором отзывает все сессии пользователя.
|
||||
if err := s.store.DeleteSessionsForUser(ctx, userID); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.store.RecordSecurityEvent(ctx, &userID, "password_change", "", "", `{"by":"admin"}`)
|
||||
}
|
||||
|
||||
// RegisterAdmin создаёт аккаунт администратора инстанса в обход флага
|
||||
// регистрации: bootstrap выполняется установщиком (AGENT.md 10.3).
|
||||
func (s *Service) RegisterAdmin(ctx context.Context, in RegisterInput) (*store.User, string, *store.Session, error) {
|
||||
username := strings.TrimSpace(in.Username)
|
||||
if err := crypto.ValidateUsername(username); err != nil {
|
||||
return nil, "", nil, ErrInvalidUsername
|
||||
}
|
||||
if err := s.policy.validate(in.Password); err != nil {
|
||||
return nil, "", nil, ErrWeakPassword
|
||||
}
|
||||
email := crypto.NormalizeEmail(in.Email)
|
||||
if err := validateEmail(email); err != nil {
|
||||
return nil, "", nil, err
|
||||
}
|
||||
passwordHash, err := s.hasher.Hash(in.Password)
|
||||
if err != nil {
|
||||
return nil, "", nil, fmt.Errorf("hash password: %w", err)
|
||||
}
|
||||
|
||||
userID := s.store.NextID()
|
||||
encrypted, err := s.encryptEmail(userID, email)
|
||||
if err != nil {
|
||||
return nil, "", nil, err
|
||||
}
|
||||
user, err := s.store.CreateUser(ctx, store.CreateUserParams{
|
||||
ID: userID,
|
||||
Username: username,
|
||||
DisplayName: defaultDisplayName(in.DisplayName, username),
|
||||
EmailEnc: encrypted,
|
||||
EmailIndex: s.masterKey.BlindIndex(email),
|
||||
PasswordHash: passwordHash,
|
||||
Locale: in.Locale,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, "", nil, err
|
||||
}
|
||||
if err := s.store.SetInstanceAdmin(ctx, user.ID, true); err != nil {
|
||||
return nil, "", nil, err
|
||||
}
|
||||
if err := s.store.SetUserBadges(ctx, user.ID, []string{"instance_admin"}); err != nil {
|
||||
s.logger.WarnContext(ctx, "failed to set admin badge", slog.Any("error", err))
|
||||
}
|
||||
token, session, err := s.createSession(ctx, user.ID, in.UserAgent, in.IP)
|
||||
if err != nil {
|
||||
return nil, "", nil, err
|
||||
}
|
||||
_ = s.store.RecordSecurityEvent(ctx, &user.ID, "bootstrap_admin", in.IP, in.UserAgent, "")
|
||||
user, err = s.store.GetUser(ctx, user.ID)
|
||||
if err != nil {
|
||||
return nil, "", nil, err
|
||||
}
|
||||
return user, token, session, nil
|
||||
}
|
||||
|
||||
type LoginInput struct {
|
||||
Email string
|
||||
Password string
|
||||
|
||||
Reference in New Issue
Block a user