revert the front

This commit is contained in:
2026-09-30 16:18:36 +03:00
parent bcfd4acfad
commit 1fbec06c23
16 changed files with 13 additions and 243 deletions
+4
View File
@@ -1,3 +1,7 @@
#root {
height: 100%;
box-shadow: 1px 1px 20px 1px rgba(68, 68, 68, 0.5);
border-radius: 40px;
border: 1px solid #444;
overflow: hidden;
}
+2 -6
View File
@@ -2,16 +2,12 @@ import './App.css'
import { Route, Routes } from "react-router-dom";
import { LoginPage } from "./pages/LoginPage";
import { ForgotPasswordPage } from "./pages/ForgotPasswordPage";
import { DashboardPage } from "./pages/DashboardPage";
import { RequirePermission } from "./components/RequirePermission";
import { RedirectIfAuthenticated } from "./components/RedirectIfAuthenticated";
function App() {
return (
<Routes>
<Route path="/" element={<RedirectIfAuthenticated><LoginPage /></RedirectIfAuthenticated>} />
<Route path="/forgot-password" element={<RedirectIfAuthenticated><ForgotPasswordPage /></RedirectIfAuthenticated>} />
<Route path="/dashboard" element={<RequirePermission><DashboardPage /></RequirePermission>} />
<Route path="/" element={<LoginPage />} />
<Route path="/forgot-password" element={<ForgotPasswordPage />} />
</Routes>
);
}
+3 -7
View File
@@ -1,18 +1,14 @@
import { API_URL, authFetch, setAccessToken } from "../token";
import { API_URL, setAccessToken } from "../token";
export async function login(email: string, password: string) {
const body = new URLSearchParams({ username: email, password });
const res = await fetch(`${API_URL}/protected/token`, {
method: "POST",
body,
credentials: "include",
body,
credentials: "include",
});
if (!res.ok) throw new Error("Неверный email или пароль");
const data = await res.json();
setAccessToken(data.access_token);
return data;
}
export async function logout() {
return authFetch("/protected/logout");
}
-9
View File
@@ -1,9 +0,0 @@
import type { UserOut } from "../../types/UserTypes";
import { authFetch } from "../token";
export async function fetchCurrentUser(): Promise<UserOut> {
const res = await authFetch("/protected/me");
if (!res.ok) throw new Error("Не удалось получить данные пользователя");
return res.json();
}
+1 -51
View File
@@ -10,58 +10,8 @@ export function getAccessToken() {
return accessToken;
}
async function requestRefresh(): Promise<{ access_token: string } | null> {
const res = await fetch(`${API_URL}/protected/refresh`, {
method: "POST",
credentials: "include",
});
if (!res.ok) return null;
return res.json();
}
let refreshInFlight: Promise<boolean> | null = null;
export function refreshAccessToken(): Promise<boolean> {
if (refreshInFlight) return refreshInFlight;
refreshInFlight = (async () => {
try {
// refresh_token в cookie одноразовый (ротация): если другая вкладка
// или повторный вызов уже успели обменять его на новый ровно перед
// нашим запросом, наш "старый" токен уже отозван и сервер ответит
// 404/401, хотя сессия на самом деле жива. Одна короткая повторная
// попытка подхватит уже обновлённую браузером cookie и не даст
// разлогинить валидного пользователя из-за проигранной гонки.
let data = await requestRefresh();
if (!data) {
await new Promise((resolve) => setTimeout(resolve, 300));
data = await requestRefresh();
}
if (!data) {
setAccessToken(null);
return false;
}
setAccessToken(data.access_token);
return true;
} finally {
refreshInFlight = null;
}
})();
return refreshInFlight;
}
export async function authFetch(path: string, init: RequestInit = {}) {
const headers = new Headers(init.headers);
if (accessToken) headers.set("Authorization", `Bearer ${accessToken}`);
const res = await fetch(`${API_URL}${path}`, { ...init, headers, credentials: "include" });
if (res.status !== 401) return res;
const refreshed = await refreshAccessToken();
if (!refreshed) return res;
const retryHeaders = new Headers(init.headers);
if (accessToken) retryHeaders.set("Authorization", `Bearer ${accessToken}`);
return fetch(`${API_URL}${path}`, { ...init, headers: retryHeaders, credentials: "include" });
return fetch(`${API_URL}${path}`, { ...init, headers, credentials: "include" });
}
@@ -1,12 +0,0 @@
import type { ReactNode } from "react";
import { Navigate } from "react-router-dom";
import { useAuth } from "../hooks/useAuth";
export function RedirectIfAuthenticated({ children }: { children: ReactNode }) {
const { user, loading } = useAuth();
if (loading) return null;
if (user) return <Navigate to="/dashboard" replace />;
return children;
}
@@ -1,13 +0,0 @@
import type { ReactNode } from "react";
import { Navigate } from "react-router-dom";
import { useAuth } from "../hooks/useAuth";
export function RequirePermission({ permission, children }: { permission?: string; children: ReactNode }) {
const { user, permissions, loading } = useAuth();
if (loading) return null;
if (!user) return <Navigate to="/" replace />;
if (permission && !permissions.has(permission)) return <Navigate to="/dashboard" replace />;
return children;
}
-12
View File
@@ -1,12 +0,0 @@
import { createContext } from "react";
import type { UserOut } from "../types/UserTypes";
export type AuthState = {
user: UserOut | null;
permissions: Set<string>;
loading: boolean;
refreshUser: () => Promise<Set<string>>;
logout: () => Promise<void>;
};
export const AuthContext = createContext<AuthState | null>(null);
-67
View File
@@ -1,67 +0,0 @@
import { useCallback, useEffect, useRef, useState, type ReactNode } from "react";
import { fetchCurrentUser } from "../api/UserAPI/User";
import { logout as apiLogout } from "../api/LoginAPI/Auth";
import { refreshAccessToken, setAccessToken } from "../api/token";
import { getEffectivePermissions } from "../utils/permissions/Permissions";
import { getRememberMe, setRememberMe } from "../utils/rememberMe";
import { AuthContext, type AuthState } from "./AuthContext";
export function AuthProvider({ children }: { children: ReactNode }) {
const [state, setState] = useState<Omit<AuthState, "refreshUser" | "logout">>({
user: null,
permissions: new Set(),
loading: true,
});
const refreshUser = useCallback(async () => {
setState((s) => ({ ...s, loading: true }));
try {
const user = await fetchCurrentUser();
const permissions = getEffectivePermissions(user);
setState({ user, permissions, loading: false });
return permissions;
} catch {
setState({ user: null, permissions: new Set(), loading: false });
return new Set<string>();
}
}, []);
const logout = useCallback(async () => {
try {
await apiLogout();
} catch {
// ошибку сети/сервера игнорируем — состояние всё равно чистим ниже
} finally {
setAccessToken(null);
setRememberMe(false);
setState({ user: null, permissions: new Set(), loading: false });
}
}, []);
const started = useRef(false);
useEffect(() => {
if (started.current) return;
started.current = true;
if (!getRememberMe()) {
setState({ user: null, permissions: new Set(), loading: false });
return;
}
(async () => {
const ok = await refreshAccessToken();
if (ok) {
await refreshUser();
} else {
setState({ user: null, permissions: new Set(), loading: false });
}
})();
}, [refreshUser]);
return (
<AuthContext.Provider value={{ ...state, refreshUser, logout }}>
{children}
</AuthContext.Provider>
);
}
@@ -1,13 +1,9 @@
import { useState, type SubmitEvent, type ReactElement } from "react";
import { Link, useNavigate } from "react-router-dom";
import { Link } from "react-router-dom";
import styles from "./AuthComponent.module.css";
import { login } from "../../../api/LoginAPI/Auth";
import { useAuth } from "../../../hooks/useAuth";
import { setRememberMe as persistRememberMe } from "../../../utils/rememberMe";
export function Auth():ReactElement {
const { refreshUser } = useAuth();
const navigate = useNavigate();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [rememberMe, setRememberMe] = useState(false);
@@ -30,9 +26,7 @@ export function Auth():ReactElement {
try {
await login(email, password);
persistRememberMe(rememberMe);
await refreshUser();
navigate("/dashboard");
window.location.href = "http://localhost:8000/docs";
} catch (err) {
console.error(err);
setErrors({ password: "Неверный email или пароль" });
-8
View File
@@ -1,8 +0,0 @@
import { useContext } from "react";
import { AuthContext } from "../context/AuthContext";
export function useAuth() {
const ctx = useContext(AuthContext);
if (!ctx) throw new Error("useAuth must be used inside AuthProvider");
return ctx;
}
-3
View File
@@ -5,9 +5,6 @@
width: 100%;
overflow: hidden;
height: 100%;
box-shadow: 1px 1px 20px 1px rgba(68, 68, 68, 0.5);
border-radius: 40px;
border: 1px solid #444;
}
.left_side {
width: 60%;
+1 -4
View File
@@ -3,14 +3,11 @@ import { createRoot } from 'react-dom/client'
import { BrowserRouter } from 'react-router-dom'
import './index.css'
import App from './App.tsx'
import { AuthProvider } from './context/AuthProvider.tsx'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<BrowserRouter>
<AuthProvider>
<App />
</AuthProvider>
<App />
</BrowserRouter>
</StrictMode>,
)
-22
View File
@@ -1,22 +0,0 @@
import { useAuth } from "../hooks/useAuth";
export function DashboardPage() {
const { permissions, logout } = useAuth();
const isAdmin = permissions.has("admin");
return (
<div>
<div>
<p>User</p>
</div>
<button onClick={() => { logout(); }}>Выйти</button>
{isAdmin && (
<div>
<p>Admin</p>
</div>
)}
</div>
);
}
@@ -1,8 +0,0 @@
import type { UserOut } from "../../types/UserTypes";
export function getEffectivePermissions(user: UserOut): Set<string> {
const direct = user.direct_permissions.map(p => p.permission);
const fromGroups = user.group.flatMap(g => g.permissions.map(p => p.permission));
return new Set([...direct, ...fromGroups]);
}
-13
View File
@@ -1,13 +0,0 @@
const REMEMBER_ME_KEY = "rememberMe";
export function setRememberMe(value: boolean) {
if (value) {
localStorage.setItem(REMEMBER_ME_KEY, "1");
} else {
localStorage.removeItem(REMEMBER_ME_KEY);
}
}
export function getRememberMe(): boolean {
return localStorage.getItem(REMEMBER_ME_KEY) === "1";
}