feat(auth): minimal login/logout + ProtectedRoute (B-LOGOUT)
- api/client.js: apiFetch подставляет Bearer-токен, выставляет Content-Type: application/json для не-FormData тел, ловит 401 -> событие - context/AuthContext: валидация токена через /auth/me, loading-гейт, logout(), подписка на 401 - components/ProtectedRoute: guard с ролями и заглушкой на время проверки - pages/Login: форма входа (OAuth2 form-urlencoded), редирект на from - App.js: AuthProvider, /login, ProtectedRoute на /sar /desktop /analysis /admin (admin -> roles=[admin]) /mobile - AdminDashboard/CaseForm/AnalysisResult: переход на apiFetch - AdminDashboard: кнопка "Выйти" в шапке (useAuth().logout) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@ import Step2Health from "./Step2Health";
|
||||
import Step2bPsychotype from "./Step2bPsychotype";
|
||||
import Step3Circumstances from "./Step3Circumstances";
|
||||
import Step4Environment from "./Step4Environment";
|
||||
import { apiFetch } from "../../api/client";
|
||||
import "./CaseForm.css";
|
||||
|
||||
const INITIAL_DATA = {
|
||||
@@ -106,9 +107,8 @@ const CaseForm = ({ isMobile = false }) => {
|
||||
};
|
||||
|
||||
// Создаем карточку
|
||||
const response = await fetch("/api/v1/cases", {
|
||||
const response = await apiFetch("/api/v1/cases", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import React from 'react';
|
||||
import { Navigate, useLocation } from 'react-router-dom';
|
||||
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
|
||||
/**
|
||||
* Пускает дальше только с валидным токеном. Пока идёт первая проверка
|
||||
* (/auth/me) — держим заглушку, иначе при живом токене будет ложный редирект.
|
||||
* roles задаёт минимальные права: бэкенд всё равно проверяет их сам
|
||||
* (require_roles), это лишь чтобы не показывать заведомо пустую страницу.
|
||||
*/
|
||||
function ProtectedRoute({ children, roles }) {
|
||||
const { isAuthenticated, loading, user } = useAuth();
|
||||
const location = useLocation();
|
||||
|
||||
if (loading) {
|
||||
return <div className="login-loading">Проверка сессии…</div>;
|
||||
}
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return <Navigate to="/login" replace state={{ from: location.pathname }} />;
|
||||
}
|
||||
|
||||
if (roles && !roles.includes(user.role)) {
|
||||
return (
|
||||
<div className="login-loading">
|
||||
Недостаточно прав: нужна роль {roles.join(' или ')}, у вас «{user.role}».
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return children;
|
||||
}
|
||||
|
||||
export default ProtectedRoute;
|
||||
Reference in New Issue
Block a user