feat(instance): админ-панель с лимитами медиа и действиями над пользователями
- лимиты медиа живут в настройках инстанса (миграция 00016): аватары, оформление сервера, эмодзи, звуки и галерея; все загрузки берут предел оттуда, значения по умолчанию — из AGENT.md 7.7 - действия администратора: временный пароль (показывается один раз, сессии отзываются), выход со всех устройств, мягкое удаление пользователя (сообщения и аудит остаются), переименование и удаление любого сервера - админ-панель разбита на вкладки: обзор и здоровье, лимиты, пользователи, серверы, аудит; смена администраторов подтверждается личностью (step-up) - тесты: Go (действия администратора, лимит эмодзи из настроек) и Vitest (вкладки, сброс пароля, выход, удаление, переименование сервера)
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"math"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -18,9 +19,24 @@ type InstanceSettings struct {
|
||||
MaxMembersPerGuild int
|
||||
MaxMessageLength int
|
||||
AuditRetentionDays int
|
||||
raw map[string]string
|
||||
// Лимиты медиа в байтах (AGENT.md 7.7): 0 — значение по умолчанию.
|
||||
MaxAvatarSize int64
|
||||
MaxGuildImageSize int64
|
||||
MaxEmojiSize int64
|
||||
MaxSoundSize int64
|
||||
MaxCosmeticSize int64
|
||||
raw map[string]string
|
||||
}
|
||||
|
||||
// MediaLimits по умолчанию: совпадают со спецификацией 7.7.
|
||||
const (
|
||||
DefaultAvatarSize int64 = 5 << 20
|
||||
DefaultGuildImageSize int64 = 5 << 20
|
||||
DefaultEmojiSize int64 = 512 << 10
|
||||
DefaultSoundSize int64 = 512 << 10
|
||||
DefaultCosmeticSize int64 = 5 << 20
|
||||
)
|
||||
|
||||
const instanceSettingsQuery = `SELECT key, value FROM instance_settings`
|
||||
|
||||
func (s *Store) InstanceSettings(ctx context.Context) (*InstanceSettings, error) {
|
||||
@@ -53,6 +69,11 @@ func settingsFromMap(values map[string]string) *InstanceSettings {
|
||||
MaxMembersPerGuild: parseSettingInt(values["max_members_per_guild"], 250),
|
||||
MaxMessageLength: parseSettingInt(values["max_message_length"], 4000),
|
||||
AuditRetentionDays: parseSettingInt(values["audit_retention_days"], 90),
|
||||
MaxAvatarSize: parseSettingInt64(values["max_avatar_size"], DefaultAvatarSize),
|
||||
MaxGuildImageSize: parseSettingInt64(values["max_guild_image_size"], DefaultGuildImageSize),
|
||||
MaxEmojiSize: parseSettingInt64(values["max_emoji_size"], DefaultEmojiSize),
|
||||
MaxSoundSize: parseSettingInt64(values["max_sound_size"], DefaultSoundSize),
|
||||
MaxCosmeticSize: parseSettingInt64(values["max_cosmetic_size"], DefaultCosmeticSize),
|
||||
raw: values,
|
||||
}
|
||||
if mainGuild := values["main_guild_id"]; mainGuild != "" {
|
||||
@@ -161,6 +182,18 @@ func optionalID(value sql.NullInt64) *uint64 {
|
||||
return &converted
|
||||
}
|
||||
|
||||
// parseSettingInt64 читает настройку-размер в байтах.
|
||||
func parseSettingInt64(value string, fallback int64) int64 {
|
||||
if value == "" {
|
||||
return fallback
|
||||
}
|
||||
parsed, err := strconv.ParseInt(value, 10, 64)
|
||||
if err != nil || parsed <= 0 {
|
||||
return fallback
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
|
||||
func parseSettingInt(value string, fallback int) int {
|
||||
parsed, ok := parseUint(value)
|
||||
if !ok || parsed > math.MaxInt32 {
|
||||
|
||||
@@ -53,3 +53,46 @@ func (s *Store) RebuildSearchIndex(ctx context.Context) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RevokeUserSessions удаляет все сессии пользователя: администратор инстанса
|
||||
// выкидывает его со всех устройств (AGENT.md 7.19).
|
||||
func (s *Store) RevokeUserSessions(ctx context.Context, userID uint64) (int64, error) {
|
||||
result, err := s.writer.ExecContext(ctx, `DELETE FROM sessions WHERE user_id = ?`, int64(userID))
|
||||
if err != nil {
|
||||
return 0, mapError(err)
|
||||
}
|
||||
affected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return affected, nil
|
||||
}
|
||||
|
||||
// SoftDeleteUser помечает пользователя удалённым и убирает его из серверов:
|
||||
// сообщения остаются, чтобы история и ссылки не ломались (AGENT.md 6.4).
|
||||
func (s *Store) SoftDeleteUser(ctx context.Context, userID uint64) error {
|
||||
tx, err := s.writer.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = tx.Rollback() }()
|
||||
|
||||
if _, err := tx.ExecContext(ctx,
|
||||
`UPDATE users SET deleted_at = ?, updated_at = ? WHERE id = ? AND deleted_at IS NULL`,
|
||||
s.Now(), s.Now(), int64(userID)); err != nil {
|
||||
return mapError(err)
|
||||
}
|
||||
if _, err := tx.ExecContext(ctx, `DELETE FROM guild_members WHERE user_id = ?`, int64(userID)); err != nil {
|
||||
return mapError(err)
|
||||
}
|
||||
if _, err := tx.ExecContext(ctx, `DELETE FROM sessions WHERE user_id = ?`, int64(userID)); err != nil {
|
||||
return mapError(err)
|
||||
}
|
||||
// Личные связи и блокировки удалённого пользователя больше не нужны.
|
||||
if _, err := tx.ExecContext(ctx,
|
||||
`DELETE FROM relationships WHERE user_id = ? OR target_id = ?`,
|
||||
int64(userID), int64(userID)); err != nil {
|
||||
return mapError(err)
|
||||
}
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user