feat(invites): приглашения, каталог публичных серверов и публичность сервера

- POST /guilds/{id}/invites (CREATE_INVITES): код из 10 символов, лимит
  использований и срок жизни; GET /guilds/{id}/invites (MANAGE_GUILD);
  DELETE /invites/{code} — создатель или MANAGE_GUILD;
- GET /invites/{code} — предпросмотр с карточкой сервера, 410 для истёкших и
  исчерпанных; POST /invites/{code} — принятие с проверкой лимита участников,
  повторное принятие не расходует использование;
- PATCH /guilds/{id} принимает `public`: сервер попадает в каталог и открыт для
  входа без приглашения (главный сервер публиковать нельзя);
- GET /guilds/public — каталог (имя, описание, иконка, число участников);
- аудит invite.create/invite.revoke/invite.accept, события INVITE_CREATE и
  INVITE_DELETE владельцу сессии;
- тесты: полный жизненный цикл приглашения, исчерпание, отзыв, права,
  публичный каталог и вход в публичный сервер.
This commit is contained in:
2026-09-19 23:33:34 +03:00
parent e4ffbb05a5
commit 61a84bfeae
16 changed files with 1233 additions and 32 deletions
+5 -5
View File
@@ -19,7 +19,7 @@ export function GuildRail({ guilds, selectedGuildId, canCreate, onCreate }: Guil
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">
<ul className="gl-stagger flex flex-col items-center gap-2" data-testid="guild-rail">
{guilds.map((guild) => {
const active = guild.id === selectedGuildId;
return (
@@ -29,10 +29,10 @@ export function GuildRail({ guilds, selectedGuildId, canCreate, onCreate }: Guil
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)] ${
className={`relative block rounded-[var(--radius-md)] transition-[border-radius,background-color,transform] duration-[var(--duration-fast)] ${
active
? 'rounded-[var(--radius-lg)] ring-2 ring-accent'
: 'hover:rounded-[var(--radius-lg)]'
? 'gl-ring rounded-[var(--radius-lg)]'
: 'hover:-translate-y-0.5 hover:rounded-[var(--radius-lg)] active:translate-y-0'
}`}
>
<Avatar name={guild.name} seed={guild.id} size="lg" shape="square" />
@@ -48,7 +48,7 @@ export function GuildRail({ guilds, selectedGuildId, canCreate, onCreate }: Guil
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"
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 transition-[border-radius,border-color,transform] duration-[var(--duration-fast)] hover:-translate-y-0.5 hover:rounded-[var(--radius-lg)] hover:border-success active:translate-y-0"
>
+
</button>