rememberme, dashboard, logout features

This commit is contained in:
2026-10-02 17:17:54 +03:00
parent af151f254b
commit e99aafa36a
11 changed files with 181 additions and 26 deletions
+3 -1
View File
@@ -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;
+13
View File
@@ -0,0 +1,13 @@
import { API_URL, setAccessToken } from "../token";
export async function refreshToken(): Promise<void> {
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);
}
+3 -1
View File
@@ -5,7 +5,9 @@ export async function requestPasswordReset(email: string): Promise<void> {
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 : "Не удалось отправить запрос");
}
}
+13
View File
@@ -0,0 +1,13 @@
import type { UserOut } from "../../types/UserTypes";
import { authFetch } from "../token";
export async function fetchUser(): Promise<UserOut> {
const response = await authFetch("/protected/me");
if (!response.ok) {
throw new Error("Not authenticated");
}
const data = await response.json()
console.log(data)
return data;
}
+35 -3
View File
@@ -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<typeof setInterval> | 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");
}
@@ -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<HTMLFormElement>) => {
e.preventDefault();
const next = { email: validateEmail(email), password: validatePassword(password) };
setErrors(next);
if (next.email || next.password) return;
async function handleSubmit(e: SubmitEvent<HTMLFormElement>) {
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 (
<>
@@ -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<HTMLFormElement>) => {
async function handleSubmit(e: SubmitEvent<HTMLFormElement>) {
e.preventDefault();
const next = {
email: validateEmail(email),
+10
View File
@@ -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(
<StrictMode>
<BrowserRouter>
+42 -1
View File
@@ -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<Set<string>>(new Set());
useEffect(function () {
async function load() {
try {
const user = await fetchUser();
const permissions = new Set<string>();
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 (
<>
<div><p>test</p></div>
{permissions.has("admin") && (<div><p>test</p></div>
)}
<button onClick={handleLogout}>logout</button>
</>
);
}
+29
View File
@@ -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(
<AuthLayout>
<Auth />
+13 -1
View File
@@ -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");
}
};
};
export function getRememberMe():boolean {
if (localStorage.getItem("rememberMe") || sessionStorage.getItem("rememberMe")) {
return true;
}
else {
return false;
}
}