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).
This commit is contained in:
2026-09-19 21:50:06 +03:00
parent 86dff94a02
commit 1b1a679827
63 changed files with 8477 additions and 187 deletions
+58
View File
@@ -0,0 +1,58 @@
import { useTranslation } from 'react-i18next';
import { NavLink } from 'react-router';
import { Avatar } from '@/components/ui/Avatar';
interface GuildRailProps {
guilds: { id: string; name: string }[];
selectedGuildId: string | null;
canCreate: boolean;
onCreate: () => void;
}
/** Левая рейка: иконки серверов-инициалов и кнопка создания сервера. */
export function GuildRail({ guilds, selectedGuildId, canCreate, onCreate }: GuildRailProps) {
const { t } = useTranslation();
return (
<nav
aria-label={t('guilds.railLabel')}
className="flex w-[72px] shrink-0 flex-col items-center gap-2 overflow-y-auto border-r border-border/40 bg-surface-2/60 py-3"
>
<ul className="flex flex-col items-center gap-2" data-testid="guild-rail">
{guilds.map((guild) => {
const active = guild.id === selectedGuildId;
return (
<li key={guild.id}>
<NavLink
to={`/app/${guild.id}`}
title={guild.name}
aria-label={t('guilds.switchTo', { name: guild.name })}
aria-current={active ? 'page' : undefined}
className={`block rounded-[var(--radius-md)] transition-[border-radius,background-color] duration-[var(--duration-fast)] ${
active
? 'rounded-[var(--radius-lg)] ring-2 ring-accent'
: 'hover:rounded-[var(--radius-lg)]'
}`}
>
<Avatar name={guild.name} seed={guild.id} size="lg" shape="square" />
</NavLink>
</li>
);
})}
</ul>
{canCreate ? (
<button
type="button"
onClick={onCreate}
aria-label={t('guilds.add')}
title={t('guilds.add')}
className="flex h-14 w-14 items-center justify-center rounded-[var(--radius-md)] border border-dashed border-border bg-surface-1 text-2xl text-success hover:rounded-[var(--radius-lg)] hover:border-success"
>
+
</button>
) : null}
</nav>
);
}