Files
grendervill 1b1a679827 feat(api): ручки Фаз(ы) 1 — профиль, серверы, роли, админ инстанса
REST-слой Фазы 1 на huma (OpenAPI 3.1 генерируется из кода):

- профиль: GET/PATCH /users/@me, смена пароля со step-up, публичный профиль,
  завершение онбординга (новая миграция 00003 с onboarding_completed_at);
- серверы: создание/изменение/удаление, join/leave, список серверов
  пользователя, журнал действий;
- комнаты: список с учётом прав, создание/изменение/удаление;
- участники: список с профилями и ролями, никнейм, тайм-аут, исключение;
- роли: CRUD, выдача/снятие с проверкой иерархии и запретом выдачи прав выше
  собственных;
- админ инстанса: публичная информация, настройки, серверы, пользователи,
  аудит, выдача прав администратора со step-up; обход лимитов фиксируется в
  аудите отдельной записью limits.bypass;
- движок прав: участие в сервере стало обязательным условием (IsMember),
  не участник не получает прав роли @user; калькулятор прав общий для API и
  Gateway, инвалидация кэша после изменений;
- Gateway: браузерный клиент аутентифицируется cookie на рукопожатии, IDENTIFY
  без токена использует её; события GUILD/CHANNEL/MEMBER/ROLE рассылаются из
  ручек, USER_UPDATE — адресно;
- ошибки huma отдаются в едином конверте {"error":{"code","message"}}.

Тесты: 8 сценариев API (профиль, жизненный цикл сервера и права, лимиты и
обход админом, иерархия ролей, тайм-аут, скрытие комнаты оверрайдом,
членство в движке прав, cookie-идентификация Gateway).
2026-09-19 21:50:06 +03:00

94 lines
2.8 KiB
Go

// Package source связывает пакет прав с хранилищем: реализует permissions.Source
// поверх store, чтобы движок прав не зависел от деталей доступа к данным.
package source
import (
"context"
"errors"
"time"
"glchat/internal/permissions"
"glchat/internal/store"
)
// Adapter реализует permissions.Source.
type Adapter struct {
store *store.Store
}
func New(st *store.Store) *Adapter { return &Adapter{store: st} }
var _ permissions.Source = (*Adapter)(nil)
func (a *Adapter) GuildRoles(ctx context.Context, guildID uint64) ([]permissions.RoleData, error) {
roles, err := a.store.ListGuildRoles(ctx, guildID)
if err != nil {
return nil, err
}
result := make([]permissions.RoleData, 0, len(roles))
for _, role := range roles {
result = append(result, permissions.RoleData{
ID: role.ID,
Permissions: permissions.Permission(role.Permissions),
IsDefault: role.IsDefault,
Position: role.Position,
})
}
return result, nil
}
func (a *Adapter) MemberRoleIDs(ctx context.Context, guildID, userID uint64) ([]uint64, error) {
return a.store.MemberRoleIDs(ctx, guildID, userID)
}
// IsMember отвечает, состоит ли пользователь в сервере.
func (a *Adapter) IsMember(ctx context.Context, guildID, userID uint64) (bool, error) {
if _, err := a.store.GetGuildMember(ctx, guildID, userID); err != nil {
if errors.Is(err, store.ErrNotFound) {
return false, nil
}
return false, err
}
return true, nil
}
func (a *Adapter) ChannelOverrides(ctx context.Context, channelID uint64) ([]permissions.OverrideData, error) {
overrides, err := a.store.ListChannelOverrides(ctx, channelID)
if err != nil {
return nil, err
}
result := make([]permissions.OverrideData, 0, len(overrides))
for _, override := range overrides {
result = append(result, permissions.OverrideData{
TargetType: override.TargetType,
TargetID: override.TargetID,
Allow: permissions.Permission(override.Allow),
Deny: permissions.Permission(override.Deny),
})
}
return result, nil
}
func (a *Adapter) GuildOwnerID(ctx context.Context, guildID uint64) (uint64, error) {
guild, err := a.store.GetGuild(ctx, guildID)
if err != nil {
return 0, err
}
return guild.OwnerID, nil
}
func (a *Adapter) MemberTimeout(ctx context.Context, guildID, userID uint64) (bool, error) {
member, err := a.store.GetGuildMember(ctx, guildID, userID)
if err != nil {
if errors.Is(err, store.ErrNotFound) {
// Не участник сервера: тайм-аут не применяется, правами рулит VIEW_GUILD.
return false, nil
}
return false, err
}
if member.TimeoutUntil == nil {
return false, nil
}
return member.TimeoutUntil.After(time.Now().UTC()), nil
}