feat(invites): страница приглашения со splash и переопределением

- миграция 00015: переопределение splash у приглашения (AGENT.md 7.9)
- GET /invites/{code} доступен без сессии: гость по ссылке видит карточку
  сервера, описание, число участников и пригласившего
- POST/DELETE /invites/{code}/background: своя картинка страницы под правом
  создателя или MANAGE_GUILD, файл удаляется при замене и снятии
- клиент: маршрут /invite/:code с фоном (переопределение → splash → баннер),
  карточкой сервера, кнопками «Присоединиться»/«Открыть сервер» и входом с
  возвратом на страницу (?next=)
- тесты: Go (карточка, загрузка, права, снятие) и Vitest (splash, гость,
  присоединение, недействительная ссылка)
This commit is contained in:
2026-09-21 19:25:06 +03:00
parent ed98c78c9e
commit 566e1cf981
14 changed files with 701 additions and 24 deletions
+7 -2
View File
@@ -1,7 +1,7 @@
import { useState, type FormEvent } from 'react';
import { useMutation } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import { Link, useLocation, useNavigate } from 'react-router';
import { Link, useLocation, useNavigate, useSearchParams } from 'react-router';
import { login } from '@/api/auth';
import { ErrorNotice } from '@/components/ui/ErrorNotice';
@@ -30,8 +30,13 @@ export default function LoginPage() {
const [password, setPassword] = useState('');
const [totpCode, setTotpCode] = useState('');
const [needsTotp, setNeedsTotp] = useState(false);
const [searchParams] = useSearchParams();
const from = (location.state as LocationState | null)?.from ?? '/app';
// Возврат после входа: `?next=` (страница приглашения) важнее состояния
// перехода из AuthGuard, но только для внутренних путей.
const next = searchParams.get('next');
const stateFrom = (location.state as LocationState | null)?.from ?? null;
const from = next !== null && next.startsWith('/') ? next : (stateFrom ?? '/app');
const submit = useMutation({
mutationFn: () => {