diff --git a/frontend/src/api/LoginAPI/Auth.ts b/frontend/src/api/LoginAPI/Auth.ts index 97935c9..9db04f4 100644 --- a/frontend/src/api/LoginAPI/Auth.ts +++ b/frontend/src/api/LoginAPI/Auth.ts @@ -7,7 +7,9 @@ export async function login(email: string, password: string) { body, credentials: "include", }); - if (!res.ok) throw new Error("Неверный email или пароль"); + if (!res.ok){ + throw new Error("Неверный email или пароль"); + }; const data = await res.json(); setAccessToken(data.access_token); return data; diff --git a/frontend/src/api/RefreshAPI/Refresh.ts b/frontend/src/api/RefreshAPI/Refresh.ts new file mode 100644 index 0000000..a5f43e7 --- /dev/null +++ b/frontend/src/api/RefreshAPI/Refresh.ts @@ -0,0 +1,13 @@ +import { API_URL, setAccessToken } from "../token"; + +export async function refreshToken(): Promise { + const response = await fetch(`${API_URL}/protected/refresh`, { + method: "POST", + credentials: "include", + }); + if (!response.ok) { + throw new Error("Session update wasn't successful"); + }; + const data = await response.json(); + setAccessToken(data.access_token); +} \ No newline at end of file diff --git a/frontend/src/api/ResetAPI/Reset.ts b/frontend/src/api/ResetAPI/Reset.ts index 4912fae..887fca2 100644 --- a/frontend/src/api/ResetAPI/Reset.ts +++ b/frontend/src/api/ResetAPI/Reset.ts @@ -5,7 +5,9 @@ export async function requestPasswordReset(email: string): Promise { credentials: "include", }); if (!res.ok) { - const body = await res.json().catch(() => null); + const body = await res.json().catch(function(){ + return null + }); throw new Error(typeof body?.detail === "string" ? body.detail : "Не удалось отправить запрос"); } } \ No newline at end of file diff --git a/frontend/src/api/UserAPI/User.ts b/frontend/src/api/UserAPI/User.ts new file mode 100644 index 0000000..f622ccb --- /dev/null +++ b/frontend/src/api/UserAPI/User.ts @@ -0,0 +1,13 @@ +import type { UserOut } from "../../types/UserTypes"; +import { authFetch } from "../token"; + + +export async function fetchUser(): Promise { + const response = await authFetch("/protected/me"); + if (!response.ok) { + throw new Error("Not authenticated"); + } + const data = await response.json() + console.log(data) + return data; +} \ No newline at end of file diff --git a/frontend/src/api/token.ts b/frontend/src/api/token.ts index 14bcfce..0d691bd 100644 --- a/frontend/src/api/token.ts +++ b/frontend/src/api/token.ts @@ -1,17 +1,49 @@ +import { refreshToken } from "./RefreshAPI/Refresh"; + export const API_URL = import.meta.env.VITE_BACKEND_URL; let accessToken: string | null = null; +let refreshTimer: ReturnType | null =null; -export function setAccessToken(token: string | null) { +export function setAccessToken(token: string | null):void{ accessToken = token; } -export function getAccessToken() { +export function getAccessToken():string|null { return accessToken; } export async function authFetch(path: string, init: RequestInit = {}) { const headers = new Headers(init.headers); - if (accessToken) headers.set("Authorization", `Bearer ${accessToken}`); + if (accessToken) { + headers.set("Authorization", `Bearer ${accessToken}`); + }; return fetch(`${API_URL}${path}`, { ...init, headers, credentials: "include" }); } + +export function startRefreshTimer(intervalms:number) { + stopRefreshTimer(); + refreshTimer = setInterval(function () { + refreshToken().catch(function() { + + }); + }, intervalms); +} + +export function stopRefreshTimer() { + if (refreshTimer) clearInterval(refreshTimer); + refreshTimer = null; +} + +export async function logout() { + try { + await authFetch(`/protected/logout`); + } catch (err) { + throw new Error("Something went wrong", { cause: err }); + } + + setAccessToken(null); + stopRefreshTimer(); + localStorage.removeItem("rememberMe"); + sessionStorage.removeItem("rememberMe"); +} diff --git a/frontend/src/features/auth/components/AuthComponent.tsx b/frontend/src/features/auth/components/AuthComponent.tsx index f36d79b..5248bae 100644 --- a/frontend/src/features/auth/components/AuthComponent.tsx +++ b/frontend/src/features/auth/components/AuthComponent.tsx @@ -12,29 +12,30 @@ export function Auth():ReactElement { const [errors, setErrors] = useState<{ email?: string; password?: string }>({}); const navigate = useNavigate(); - const validateEmail = (value: string) => { + function validateEmail(value: string) { if (!value) return "Введите email"; if (!/^\S+@\S+\.\S+$/.test(value)) return "Некорректный email"; return undefined; - }; - const validatePassword = (value: string) => - value ? undefined : "Введите пароль"; + } + function validatePassword(value: string) { + return value ? undefined : "Введите пароль"; + } - const handleSubmit = async (e: SubmitEvent) => { - e.preventDefault(); - const next = { email: validateEmail(email), password: validatePassword(password) }; - setErrors(next); - if (next.email || next.password) return; + async function handleSubmit(e: SubmitEvent) { + e.preventDefault(); + const next = { email: validateEmail(email), password: validatePassword(password) }; + setErrors(next); + if (next.email || next.password) return; - try { - await login(email, password); - persistRememberMe(rememberMe); - navigate("/dashboard"); - } catch (err) { - console.error(err); - setErrors({ password: "Неверный email или пароль" }); + try { + await login(email, password); + persistRememberMe(rememberMe); + navigate("/dashboard"); + } catch (err) { + console.error(err); + setErrors({ password: "Неверный email или пароль" }); + } } - }; return ( <> diff --git a/frontend/src/features/auth/components/ResetComponent.tsx b/frontend/src/features/auth/components/ResetComponent.tsx index abf25cd..7e7a438 100644 --- a/frontend/src/features/auth/components/ResetComponent.tsx +++ b/frontend/src/features/auth/components/ResetComponent.tsx @@ -8,12 +8,12 @@ export function Reset():ReactElement { const [email, setEmail] = useState(""); const [errors, setErrors] = useState<{ email?: string;}>({}); - const validateEmail = (value: string) => { + function validateEmail(value: string) { if (!value) return "Введите email"; if (!/^\S+@\S+\.\S+$/.test(value)) return "Некорректный email"; return undefined; }; - const handleSubmit = async (e: SubmitEvent) => { + async function handleSubmit(e: SubmitEvent) { e.preventDefault(); const next = { email: validateEmail(email), diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index ade9d64..f45cf7e 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -3,7 +3,17 @@ import { createRoot } from 'react-dom/client' import { BrowserRouter } from 'react-router-dom' import './index.css' import App from './App.tsx' +import { refreshToken } from './api/RefreshAPI/Refresh.ts' +import { getRememberMe } from './utils/rememberMe.ts' +import { startRefreshTimer } from './api/token.ts' + + +if (getRememberMe()) { + await refreshToken().catch(() => {}); + console.log("Timer started"); + startRefreshTimer(10*60*1000); +} createRoot(document.getElementById('root')!).render( diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index ae80c62..5d10a4c 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -1,9 +1,50 @@ +import { useEffect, useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { fetchUser } from "../api/UserAPI/User"; +import { logout } from "../api/token"; export function DashboardPage() { + const navigate = useNavigate(); + const [loading, setLoading] = useState(true); + const [permissions, setPermissions] = useState>(new Set()); + + useEffect(function () { + async function load() { + try { + const user = await fetchUser(); + + const permissions = new Set(); + + for (const p of user.direct_permissions) { + permissions.add(p.permission); + } + for (const g of user.group) { + for (const p of g.permissions) { + permissions.add(p.permission); + } + } + setPermissions(permissions); + setLoading(false); + } catch { + navigate("/"); + } + } + load(); + }, [navigate]); + if (loading) { + return null; + } + async function handleLogout() { + await logout(); + navigate("/"); + } + return ( <> -

test

+ {permissions.has("admin") && (

test

+ )} + ); } \ No newline at end of file diff --git a/frontend/src/pages/LoginPage.tsx b/frontend/src/pages/LoginPage.tsx index 1171ed6..88f9d6e 100644 --- a/frontend/src/pages/LoginPage.tsx +++ b/frontend/src/pages/LoginPage.tsx @@ -1,7 +1,36 @@ +import { useNavigate } from "react-router-dom"; import { Auth } from "../features/auth/components/AuthComponent"; import { AuthLayout } from "../layouts/AuthLayout"; +import { useEffect, useState } from "react"; +import { fetchUser } from "../api/UserAPI/User"; +import { getAccessToken } from "../api/token"; export function LoginPage(){ + const navigate = useNavigate(); + const [checking, setChecking] = useState(function () { + return !!getAccessToken(); + }); + + + useEffect(function () { + if (!getAccessToken()) return; + + async function check() { + try { + await fetchUser(); + navigate("/dashboard"); + } catch { + setChecking(false); + } + } + check(); + }, [navigate]); + + + + if (checking) { + return null; + } return( diff --git a/frontend/src/utils/rememberMe.ts b/frontend/src/utils/rememberMe.ts index 839b2d5..403d4af 100644 --- a/frontend/src/utils/rememberMe.ts +++ b/frontend/src/utils/rememberMe.ts @@ -3,7 +3,19 @@ export function persistRememberMe(value:boolean):void { if (value) { localStorage.setItem("rememberMe", "True"); + sessionStorage.removeItem("rememberMe"); } else{ + sessionStorage.setItem("rememberMe", "True"); localStorage.removeItem("rememberMe"); } -}; \ No newline at end of file +}; + +export function getRememberMe():boolean { + if (localStorage.getItem("rememberMe") || sessionStorage.getItem("rememberMe")) { + return true; + } + else { + return false; + } +} +