feat(web): анонимный вход для школьников — только школа и класс

Школьники не должны придумывать пароль, чтобы пройти тест. Бэкенд уже
поддерживал это: POST /polls/{poll_id}/responses принимает user_id как
Optional, а Response.group_id/organization_id — самостоятельные nullable
снимки школы/класса, не завязанные на аккаунт. Не хватало только
фронтенда — все роуты были жёстко завёрнуты в ProtectedRoute с
обязательным JWT.

- AuthContext: параллельная "анонимная идентичность" (org+group в
  localStorage, без токена и без запроса к API)
- ProtectedRoute пускает по токену ИЛИ анонимной сессии; новый
  AccountRoute — строго по токену, для /my-tests, /profile,
  /response/:id (у анонимной сессии нет истории — её негде хранить)
- Новая страница /anonymous — выбор школы+класса тем же
  SearchableDropdown, что и в регистрации
- PollPage при отправке ответа берёт org/group из анонимной сессии,
  если пользователь не залогинен
- HomePage скрывает "Мои тесты" и профиль для анонимов, добавлена
  кнопка выхода из анонимного режима

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
jze9
2026-07-16 11:56:08 +05:00
parent dd0c29ccad
commit 94299d4af6
7 changed files with 171 additions and 14 deletions

View File

@@ -3,8 +3,26 @@ import type { ReactNode } from "react";
import { useAuth } from "../auth/AuthContext";
import { Spinner } from "./ui";
// Пускает дальше только при наличии токена. Пока идёт первичная проверка — спиннер.
// Пускает дальше при наличии токена ИЛИ анонимной идентичности (школа+класс,
// без аккаунта). Используется для страниц, которые школьник может пройти
// анонимно: главная, список тестов, прохождение теста.
export function ProtectedRoute({ children }: { children: ReactNode }) {
const { token, anon, ready } = useAuth();
if (!ready) {
return (
<div className="spinner-center">
<Spinner />
</div>
);
}
if (!token && !anon) return <Navigate to="/login" replace />;
return <>{children}</>;
}
// Строже: пускает дальше только при реальном токене (не анонимной сессии).
// Для страниц, которым нужен настоящий аккаунт: история тестов, профиль,
// просмотр конкретного результата.
export function AccountRoute({ children }: { children: ReactNode }) {
const { token, ready } = useAuth();
if (!ready) {
return (