1b1a679827
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).
94 lines
2.8 KiB
Go
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
|
|
}
|