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:
75
web-react/src/pages/AnonymousEntryPage.tsx
Normal file
75
web-react/src/pages/AnonymousEntryPage.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import * as api from "../api/client";
|
||||
import { Button } from "../components/ui";
|
||||
import { SearchableDropdown, type Option } from "../components/SearchableDropdown";
|
||||
import { useAuth } from "../auth/AuthContext";
|
||||
|
||||
// Вход без регистрации: школьник выбирает только школу и класс, аккаунт
|
||||
// и пароль не нужны. Результат теста запишется в базу со снимком
|
||||
// организации/группы, но без привязки к пользователю.
|
||||
export function AnonymousEntryPage() {
|
||||
const navigate = useNavigate();
|
||||
const { loginAnonymous } = useAuth();
|
||||
|
||||
const [orgId, setOrgId] = useState<string | null>(null);
|
||||
const [groupId, setGroupId] = useState<string | null>(null);
|
||||
const [orgs, setOrgs] = useState<api.Organization[]>([]);
|
||||
const [groups, setGroups] = useState<api.Group[]>([]);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
(async () => {
|
||||
const [o, g] = await Promise.all([api.getOrganizations(), api.getGroups()]);
|
||||
if (cancelled) return;
|
||||
setOrgs(o);
|
||||
setGroups(g);
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const onOrgChange = (id: string | null) => {
|
||||
setOrgId(id);
|
||||
const org = orgs.find((o) => o.id === id);
|
||||
if (org?.group_id) setGroupId(org.group_id);
|
||||
};
|
||||
|
||||
const orgOptions: Option[] = orgs.map((o) => ({ key: o.id, text: o.name_organization }));
|
||||
const groupOptions: Option[] = groups.map((g) => ({ key: g.id, text: g.name_group }));
|
||||
|
||||
const proceed = () => {
|
||||
if (!orgId || !groupId) {
|
||||
setError("Выберите школу и класс");
|
||||
return;
|
||||
}
|
||||
loginAnonymous(orgId, groupId);
|
||||
navigate("/");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="auth-wrap">
|
||||
<div className="auth-logo">ЦОПП</div>
|
||||
<div className="auth-sub">Пройти тест без регистрации — выберите школу и класс</div>
|
||||
<form
|
||||
className="auth-card"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
proceed();
|
||||
}}
|
||||
>
|
||||
<SearchableDropdown label="Школа" hint="Выберите школу..." options={orgOptions} value={orgId} onChange={onOrgChange} />
|
||||
<SearchableDropdown label="Класс" hint="Выберите класс..." options={groupOptions} value={groupId} onChange={setGroupId} />
|
||||
{error && <div className="error-text">{error}</div>}
|
||||
<div className="wrap-row">
|
||||
<Button type="button" variant="ghost" onClick={() => navigate("/login")}>
|
||||
Назад
|
||||
</Button>
|
||||
<Button type="submit">Продолжить</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user