Files
api-copp/web-react/src/pages/LoginPage.tsx
jze9 94299d4af6 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>
2026-07-16 11:56:08 +05:00

83 lines
2.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { useAuth } from "../auth/AuthContext";
import { Button, TextField } from "../components/ui";
import { AgreementDialog } from "../components/AgreementDialog";
import { IconPerson, IconLock } from "../components/icons";
export function LoginPage() {
const { login } = useAuth();
const navigate = useNavigate();
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const [busy, setBusy] = useState(false);
const [askReg, setAskReg] = useState(false);
const doLogin = async () => {
if (!username.trim() || !password) {
setError("Заполните логин и пароль");
return;
}
setBusy(true);
setError("");
const err = await login(username.trim(), password);
setBusy(false);
if (err) {
setError(err);
} else {
navigate("/");
}
};
return (
<div className="auth-wrap">
<div className="auth-logo">ЦОПП</div>
<div className="auth-sub">
Это проект ЦОПП по проведению исследовательских тестирований
</div>
<form
className="auth-card"
onSubmit={(e) => {
e.preventDefault();
doLogin();
}}
>
<TextField
label="Логин"
placeholder="Введите логин..."
icon={<IconPerson />}
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
<TextField
label="Пароль"
placeholder="Введите пароль..."
icon={<IconLock />}
password
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
{error && <div className="error-text">{error}</div>}
<Button type="submit" disabled={busy} block>
{busy ? "Вход..." : "Войти"}
</Button>
<button type="button" className="link-btn" onClick={() => setAskReg(true)}>
Нет аккаунта? Зарегистрироваться
</button>
<button type="button" className="link-btn" onClick={() => navigate("/anonymous")}>
Пройти тест без регистрации
</button>
</form>
{askReg && (
<AgreementDialog
onClose={() => setAskReg(false)}
onAccept={() => navigate("/register")}
/>
)}
</div>
);
}