Auth
This commit is contained in:
@@ -1,187 +1,122 @@
|
|||||||
import {useEffect, useMemo, useState} from "react";
|
import {apiUrl, getTokensFromCookies} from "../utils/common.ts";
|
||||||
import {DEBUG_MODE} from "../utils/common";
|
import {ApiResponse} from "../types/api.ts";
|
||||||
import {ApiResponse, PageResponse} from "../types/api.ts";
|
|
||||||
import {PageControl} from "../types/common.ts";
|
|
||||||
import {Institution} from "../types/survey.ts";
|
|
||||||
|
|
||||||
export function hostUrl(url: string){
|
|
||||||
return DEBUG_MODE? "http://localhost:8000"+url: url;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function apiUrl(url: string) {
|
|
||||||
return hostUrl("/api/v1"+url);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export async function getFetch(path:string) {
|
export async function getFetch(path: string): Promise<ApiResponse> {
|
||||||
|
const finalTokens = getTokensFromCookies();
|
||||||
|
|
||||||
try{
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch(apiUrl(path), {
|
||||||
apiUrl(path), {
|
method: 'GET',
|
||||||
method: "GET",
|
headers: {
|
||||||
credentials: "include"
|
...(finalTokens?.access && { Authorization: `Bearer ${finalTokens.access}` }),
|
||||||
}
|
},
|
||||||
);
|
credentials: "include"
|
||||||
|
});
|
||||||
|
|
||||||
if (!response.ok){
|
if (!response.ok) {
|
||||||
const body = await response.json();
|
const body = await response.json();
|
||||||
|
|
||||||
console.error('Error fetching:', 'Network response was not ok', body);
|
console.error('Error fetching:', 'Network response was not ok', body);
|
||||||
|
return { body, success: false };
|
||||||
return {
|
|
||||||
body: body,
|
|
||||||
success: false
|
|
||||||
} as ApiResponse;
|
|
||||||
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
body: await response.json(),
|
|
||||||
success: true,
|
|
||||||
} as ApiResponse
|
|
||||||
}
|
|
||||||
|
|
||||||
catch {
|
|
||||||
return {
|
|
||||||
success: false
|
|
||||||
} as ApiResponse;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const createQueryString = (params: Record<string, string | number | boolean | undefined>): string => {
|
|
||||||
const filteredParams = Object.entries(params)
|
|
||||||
.filter(([_, value]) => value !== undefined && value !== null)
|
|
||||||
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
|
|
||||||
return filteredParams.join('&');
|
|
||||||
};
|
|
||||||
|
|
||||||
export function useDataPage<T>(application:string, endpoint: string, params:{[key:string]:string|number}={}, needLoad:boolean=true):PageControl<T> {
|
|
||||||
const [data, setData] = useState<T[]|null>(null);
|
|
||||||
// const [previousUrl, setPreviousUrl] = useState<string|undefined>(undefined)
|
|
||||||
// const [nextUrl, setNextUrl] = useState<string|undefined>(undefined)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(
|
|
||||||
()=>{
|
|
||||||
if (data === null && needLoad){
|
|
||||||
getFetch(`/${application}/${endpoint}/?${createQueryString(params)}`).then((r)=>{
|
|
||||||
if (r.success){
|
|
||||||
const pageData = r.body as PageResponse<T>;
|
|
||||||
|
|
||||||
setData(pageData.results);
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
setData([]);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[data, application, endpoint, needLoad]
|
|
||||||
)
|
|
||||||
|
|
||||||
// function changePage(pageUrl: string|undefined) {
|
|
||||||
// if (pageUrl){ //TODO CHANGING PAGES
|
|
||||||
// getFetch(`/${application}/${endpoint}/`).then((r)=>{
|
|
||||||
// if (r.success){
|
|
||||||
// const pageData = r.body as PageResponse<T>;
|
|
||||||
//
|
|
||||||
// setData(pageData.results);
|
|
||||||
//
|
|
||||||
//
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
function nextPage() {
|
|
||||||
// changePage(nextUrl)
|
|
||||||
}
|
|
||||||
|
|
||||||
function previousPage() {
|
|
||||||
// changePage(previousUrl)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
return {
|
|
||||||
items: data,
|
|
||||||
hasPrevious: false,//previousUrl !== undefined,
|
|
||||||
hasNext: false,//nextUrl !== undefined,
|
|
||||||
nextPage,
|
|
||||||
previousPage
|
|
||||||
} as PageControl<T>
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useData<T>(application:string, endpoint: string, id: string|number, needLoad:boolean=true): T|null|undefined {
|
|
||||||
|
|
||||||
const [data, setData] = useState<T | null | undefined>(null);
|
|
||||||
|
|
||||||
useEffect(
|
|
||||||
()=>{
|
|
||||||
if (data === null && needLoad){
|
|
||||||
getFetch(`/${application}/${endpoint}/${id}`).then((r)=>{
|
|
||||||
if (r.success){
|
|
||||||
const pageData = r.body as T;
|
|
||||||
setData(pageData);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
setData(undefined);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[data, application, endpoint, id, needLoad]
|
|
||||||
)
|
|
||||||
|
|
||||||
return data;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useCachedData<T>(application: string, endpoint: string) {
|
|
||||||
const [cache, setCache] = useState<Map<string | number, T | undefined>>(new Map());
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Очистка кеша или другие побочные эффекты при необходимости
|
|
||||||
return () => {
|
|
||||||
setCache(new Map());
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (id: string | number, needLoad: boolean = true): T | undefined | null => {
|
|
||||||
const cachedData = cache.has(id)?cache.get(id):null;
|
|
||||||
|
|
||||||
if (cachedData !== null) {
|
|
||||||
return cachedData;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (needLoad) {
|
return { body: await response.json(), success: true };
|
||||||
getFetch(`/${application}/${endpoint}/${id}`).then((r) => {
|
} catch (error) {
|
||||||
if (r.success) {
|
console.error('Error fetching:', error);
|
||||||
const data = r.body as T;
|
return { success: false };
|
||||||
setCache((prevCache) => {
|
}
|
||||||
const newCache = new Map(prevCache);
|
|
||||||
newCache.set(id, data);
|
|
||||||
return newCache;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setCache((prevCache) => {
|
|
||||||
const newCache = new Map(prevCache);
|
|
||||||
newCache.set(id, undefined);
|
|
||||||
return newCache;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function postFetch(
|
||||||
|
path: string,
|
||||||
|
body: object | FormData
|
||||||
|
): Promise<ApiResponse> {
|
||||||
|
const finalTokens = getTokensFromCookies();
|
||||||
|
const isFormData = body instanceof FormData;
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
const headers = new Headers();
|
||||||
|
|
||||||
|
if (finalTokens?.access){
|
||||||
|
headers.append("Authorization", `Bearer ${finalTokens.access}`)
|
||||||
|
}
|
||||||
|
if (!isFormData){
|
||||||
|
headers.append('Content-Type','application/json')
|
||||||
|
}
|
||||||
|
const response = await fetch(apiUrl(path), {
|
||||||
|
method: 'POST',
|
||||||
|
headers: headers,
|
||||||
|
body: isFormData ? body as FormData : JSON.stringify(body),
|
||||||
|
credentials: 'include'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const responseBody = await response.json();
|
||||||
|
console.error('Error posting:', 'Network response was not ok', responseBody);
|
||||||
|
return { body: responseBody, success: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { body: await response.json(), success: true };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error posting:', error);
|
||||||
|
return { success: false };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function patchFetch(
|
||||||
|
path: string,
|
||||||
|
body: object | FormData
|
||||||
|
): Promise<ApiResponse> {
|
||||||
|
const finalTokens = getTokensFromCookies();
|
||||||
|
const isFormData = body instanceof FormData;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(apiUrl(path), {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: {
|
||||||
|
...(finalTokens?.access && { Authorization: `Bearer ${finalTokens.access}` }),
|
||||||
|
...(!isFormData && { 'Content-Type': 'application/json' }),
|
||||||
|
},
|
||||||
|
body: isFormData ? body as FormData : JSON.stringify(body),
|
||||||
|
credentials: 'include',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const responseBody = await response.json();
|
||||||
|
console.error('Error patching:', 'Network response was not ok', responseBody);
|
||||||
|
return { body: responseBody, success: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { body: await response.json(), success: true };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error patching:', error);
|
||||||
|
return { success: false };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export async function deleteFetch(path: string): Promise<ApiResponse> {
|
||||||
|
const finalTokens = getTokensFromCookies();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(apiUrl(path), {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
...(finalTokens?.access && { Authorization: `Bearer ${finalTokens.access}` }),
|
||||||
|
},
|
||||||
|
credentials: 'include',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const responseBody = await response.json();
|
||||||
|
console.error('Error deleting:', 'Network response was not ok', responseBody);
|
||||||
|
return { body: responseBody, success: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { body: await response.json(), success: true };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting:', error);
|
||||||
|
return { success: false };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
139
src/API/hooks.ts
Normal file
139
src/API/hooks.ts
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
import {PageControl} from "../types/common.ts";
|
||||||
|
import {useEffect, useState} from "react";
|
||||||
|
import {ApiResponse, PageResponse} from "../types/api.ts";
|
||||||
|
import {getFetch} from "./common.ts";
|
||||||
|
import {createQueryString} from "../utils/common.ts";
|
||||||
|
|
||||||
|
export function useDataPage<T>(application:string, endpoint: string, params:{[key:string]:string|number}={}, needLoad:boolean=true):PageControl<T> {
|
||||||
|
const [data, setData] = useState<T[]|null>(null);
|
||||||
|
// const [previousUrl, setPreviousUrl] = useState<string|undefined>(undefined)
|
||||||
|
// const [nextUrl, setNextUrl] = useState<string|undefined>(undefined)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
()=>{
|
||||||
|
if (data === null && needLoad){
|
||||||
|
getFetch(`/${application}/${endpoint}/?${createQueryString(params)}`).then((r)=>{
|
||||||
|
if (r.success){
|
||||||
|
const pageData = (r as ApiResponse).body as PageResponse<T>;
|
||||||
|
|
||||||
|
setData(pageData.results);
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
setData([]);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[data, application, endpoint, needLoad]
|
||||||
|
)
|
||||||
|
|
||||||
|
// function changePage(pageUrl: string|undefined) {
|
||||||
|
// if (pageUrl){ //TODO CHANGING PAGES
|
||||||
|
// getFetch(`/${application}/${endpoint}/`).then((r)=>{
|
||||||
|
// if (r.success){
|
||||||
|
// const pageData = r.body as PageResponse<T>;
|
||||||
|
//
|
||||||
|
// setData(pageData.results);
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
function nextPage() {
|
||||||
|
// changePage(nextUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
function previousPage() {
|
||||||
|
// changePage(previousUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: data,
|
||||||
|
hasPrevious: false,//previousUrl !== undefined,
|
||||||
|
hasNext: false,//nextUrl !== undefined,
|
||||||
|
nextPage,
|
||||||
|
previousPage
|
||||||
|
} as PageControl<T>
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useData<T>(application:string, endpoint: string, id: string|number, needLoad:boolean=true): T|null|undefined {
|
||||||
|
|
||||||
|
const [data, setData] = useState<T | null | undefined>(null);
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
()=>{
|
||||||
|
if (data === null && needLoad){
|
||||||
|
getFetch(`/${application}/${endpoint}/${id}`).then((r)=>{
|
||||||
|
if (r.success){
|
||||||
|
const pageData = r.body as T;
|
||||||
|
setData(pageData);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
setData(undefined);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[data, application, endpoint, id, needLoad]
|
||||||
|
)
|
||||||
|
|
||||||
|
return data;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CachedData<T>{
|
||||||
|
get(id: string | number, needLoad: boolean): T|undefined|null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCachedData<T>(application: string, endpoint: string): CachedData<T> {
|
||||||
|
const [cache, setCache] = useState<Map<string | number, T | undefined>>(new Map());
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Очистка кеша или другие побочные эффекты при необходимости
|
||||||
|
return () => {
|
||||||
|
setCache(new Map());
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
get: (id: string | number, needLoad: boolean = true): T|undefined|null => {
|
||||||
|
const cachedData = cache.has(id)?cache.get(id):null;
|
||||||
|
|
||||||
|
if (cachedData !== null) {
|
||||||
|
return cachedData;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needLoad) {
|
||||||
|
getFetch(`/${application}/${endpoint}/${id}`).then((r) => {
|
||||||
|
if (r.success) {
|
||||||
|
const data = r.body as T;
|
||||||
|
setCache((prevCache) => {
|
||||||
|
const newCache = new Map(prevCache);
|
||||||
|
newCache.set(id, data);
|
||||||
|
return newCache;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setCache((prevCache) => {
|
||||||
|
const newCache = new Map(prevCache);
|
||||||
|
newCache.set(id, undefined);
|
||||||
|
return newCache;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
46
src/API/users.ts
Normal file
46
src/API/users.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
|
||||||
|
import { TokenObtainPair, TokenRefresh, TokenVerify, JwtTokenResponse } from '../types/users';
|
||||||
|
import {apiUrl} from "../utils/common.ts";
|
||||||
|
|
||||||
|
|
||||||
|
export async function login(data: TokenObtainPair): Promise<JwtTokenResponse | null> {
|
||||||
|
const response = await fetch(apiUrl('/users/auth/jwt/create/'), {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function refreshToken(data: TokenRefresh): Promise<JwtTokenResponse | null> {
|
||||||
|
const response = await fetch(apiUrl('/users/auth/jwt/refresh/'), {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function verifyToken(data: TokenVerify): Promise<boolean> {
|
||||||
|
const response = await fetch(apiUrl('/users/auth/jwt/verify/'), {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.ok;
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@ export function LoadingData({data, children}: {data: object|null|undefined, chil
|
|||||||
return <div className={"loading"}>Идёт загрузка...</div>
|
return <div className={"loading"}>Идёт загрузка...</div>
|
||||||
}
|
}
|
||||||
if (data === undefined){
|
if (data === undefined){
|
||||||
return <Link className={"not-found"} to={"/"}>404 НИЧЕГО НЕ НАЙДЕНО</Link>
|
return <Link className={"not-found"} to={"/"}>404 НЕ НАЙДЕНО</Link>
|
||||||
}
|
}
|
||||||
return children;
|
return children;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import {UniqueItem} from "../types/common.ts";
|
import {UniqueItem} from "../types/common.ts";
|
||||||
import {JSX} from "react";
|
|
||||||
|
|
||||||
export function LoadingList({data, listElement}: {data: UniqueItem[]|null, listElement(e:UniqueItem, i:number):unknown}) {
|
export function LoadingList({data, listElement}: {data: UniqueItem[]|null, listElement(e:UniqueItem, i:number):unknown}) {
|
||||||
if (data === null){
|
if (data === null){
|
||||||
|
|||||||
@@ -1,3 +1,66 @@
|
|||||||
|
import {useEffect, useState} from 'react';
|
||||||
|
import {useUser} from "../utils/users/UseUser.ts";
|
||||||
|
import {useNavigate} from "react-router-dom";
|
||||||
|
|
||||||
export function LoginPage() {
|
export function LoginPage() {
|
||||||
return null;
|
const userContext = useUser();
|
||||||
}
|
const user = userContext?.user;
|
||||||
|
const login = userContext?.login;
|
||||||
|
// Локальные стейты для формы
|
||||||
|
const [loginInput, setLoginInput] = useState('');
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (user != undefined){
|
||||||
|
navigate("/dashboard/")
|
||||||
|
}
|
||||||
|
}, [user, navigate]);
|
||||||
|
|
||||||
|
// Обработчик отправки формы
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (login && loginInput && password) {
|
||||||
|
try {
|
||||||
|
await login({ login: loginInput, password });
|
||||||
|
setError(null);
|
||||||
|
} catch {
|
||||||
|
setError('Ошибка входа');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="login-page">
|
||||||
|
<form className="login-form" onSubmit={handleSubmit}>
|
||||||
|
<h1 className="login-title">Войти в систему</h1>
|
||||||
|
<div className="form-group">
|
||||||
|
<label htmlFor="login" className="form-label">Логин</label>
|
||||||
|
<input
|
||||||
|
id="login"
|
||||||
|
className="form-input"
|
||||||
|
type="text"
|
||||||
|
value={loginInput}
|
||||||
|
onChange={(e) => setLoginInput(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="form-group">
|
||||||
|
<label htmlFor="password" className="form-label">Пароль</label>
|
||||||
|
<input
|
||||||
|
id="password"
|
||||||
|
className="form-input"
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{error && <div className="error-message">{error}</div>}
|
||||||
|
<button type="submit" className="submit-button">Войти</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import '../css/MainPage.css';
|
import '../css/MainPage.css';
|
||||||
import {useDataPage} from "../API/common.ts";
|
|
||||||
import {Survey} from "../types/survey.ts";
|
import {Survey} from "../types/survey.ts";
|
||||||
import {LoadingList} from "../components/LoadingList.tsx";
|
import {LoadingList} from "../components/LoadingList.tsx";
|
||||||
|
import {useDataPage} from "../API/hooks.ts";
|
||||||
|
|
||||||
export function MainPage() {
|
export function MainPage() {
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import {Link, useParams} from 'react-router-dom';
|
import {Link, useParams} from 'react-router-dom';
|
||||||
import {useCachedData, useData, useDataPage} from '../API/common';
|
|
||||||
import { Institution, Profession, Specialty } from '../types/survey';
|
import { Institution, Profession, Specialty } from '../types/survey';
|
||||||
import ReactMarkdown from 'react-markdown';
|
import ReactMarkdown from 'react-markdown';
|
||||||
import { LoadingData } from '../components/LoadingData';
|
import { LoadingData } from '../components/LoadingData';
|
||||||
import { LoadingList } from '../components/LoadingList';
|
import { LoadingList } from '../components/LoadingList';
|
||||||
|
import {useCachedData, useData, useDataPage} from "../API/hooks.ts";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -29,7 +29,7 @@ export function ProfessionPage() {
|
|||||||
<SpecialtyBlock
|
<SpecialtyBlock
|
||||||
key={specialty.id}
|
key={specialty.id}
|
||||||
specialty={specialty as Specialty}
|
specialty={specialty as Specialty}
|
||||||
institution={institutions((specialty as Specialty).institution)}
|
institution={institutions.get((specialty as Specialty).institution)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { Navigate, useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
import {
|
import {
|
||||||
Survey,
|
Survey,
|
||||||
Question,
|
Question,
|
||||||
QuestionPair,
|
QuestionPair,
|
||||||
AgreementQuestion,
|
AgreementQuestion,
|
||||||
QuestionType,
|
QuestionType,
|
||||||
ScoreVariable,
|
|
||||||
} from "../types/survey";
|
} from "../types/survey";
|
||||||
import { useData, useDataPage } from "../API/common";
|
|
||||||
import { LoadingData } from "../components/LoadingData";
|
import { LoadingData } from "../components/LoadingData";
|
||||||
import { LoadingList } from "../components/LoadingList";
|
import { LoadingList } from "../components/LoadingList";
|
||||||
|
import {useData, useDataPage} from "../API/hooks.ts";
|
||||||
|
|
||||||
export function SurveyPage() {
|
export function SurveyPage() {
|
||||||
const { surveyId } = useParams<{ surveyId: string }>();
|
const { surveyId } = useParams<{ surveyId: string }>();
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { Link, useParams } from 'react-router-dom';
|
import { Link, useParams } from 'react-router-dom';
|
||||||
import { useData, useDataPage } from '../API/common';
|
|
||||||
import { Profession, ScoreVariable, Survey } from '../types/survey';
|
import { Profession, ScoreVariable, Survey } from '../types/survey';
|
||||||
import ReactMarkdown from 'react-markdown';
|
import ReactMarkdown from 'react-markdown';
|
||||||
import { LoadingData } from '../components/LoadingData';
|
import { LoadingData } from '../components/LoadingData';
|
||||||
import { LoadingList } from '../components/LoadingList';
|
import { LoadingList } from '../components/LoadingList';
|
||||||
|
import {useData, useDataPage} from "../API/hooks.ts";
|
||||||
|
|
||||||
export function SurveyResultPage() {
|
export function SurveyResultPage() {
|
||||||
const { scoreVariableId } = useParams<{ scoreVariableId: string }>();
|
const { scoreVariableId } = useParams<{ scoreVariableId: string }>();
|
||||||
|
|||||||
@@ -11,6 +11,3 @@ export interface PageControl<T>{
|
|||||||
previousPage(): void;
|
previousPage(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CachedData<T>{
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
import {UniqueItem} from "./common.ts";
|
import {UniqueItem} from "./common.ts";
|
||||||
|
|
||||||
|
export type UserContextType = {
|
||||||
|
user: Account | undefined;
|
||||||
|
login: (credentials: TokenObtainPair) => Promise<void>;
|
||||||
|
logout: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
export interface ContactData{
|
export interface ContactData{
|
||||||
first_name: string;
|
first_name: string;
|
||||||
@@ -19,4 +24,23 @@ export interface Account extends UniqueItem, ContactData{
|
|||||||
is_staff: boolean;
|
is_staff: boolean;
|
||||||
is_active: boolean;
|
is_active: boolean;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export interface TokenObtainPair {
|
||||||
|
login: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TokenRefresh {
|
||||||
|
refresh: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TokenVerify {
|
||||||
|
token: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface JwtTokenResponse {
|
||||||
|
access: string;
|
||||||
|
refresh: string;
|
||||||
}
|
}
|
||||||
5
src/utils/checkdebug.ts
Normal file
5
src/utils/checkdebug.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
/*
|
||||||
|
`true` if debug mode enabled and `false` otherwise
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const DEBUG_MODE: boolean = process.env.NODE_ENV === "development";
|
||||||
@@ -1,4 +1,36 @@
|
|||||||
/*
|
|
||||||
`true` if debug mode enabled and `false` otherwise
|
|
||||||
*/
|
import {JwtTokenResponse} from "../types/users.ts";
|
||||||
export const DEBUG_MODE: boolean = process.env.NODE_ENV === "development";
|
import {DEBUG_MODE} from "./checkdebug.ts";
|
||||||
|
|
||||||
|
export function getTokensFromCookies(): JwtTokenResponse | undefined {
|
||||||
|
const cookies = document.cookie.split('; ').reduce((acc, cookie) => {
|
||||||
|
const [key, value] = cookie.split('=');
|
||||||
|
acc[key] = decodeURIComponent(value);
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, string>);
|
||||||
|
|
||||||
|
const access = cookies['access_token'];
|
||||||
|
const refresh = cookies['refresh_token'];
|
||||||
|
|
||||||
|
if (access && refresh) {
|
||||||
|
return { access, refresh };
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createQueryString = (params: Record<string, string | number | boolean | undefined>): string => {
|
||||||
|
const filteredParams = Object.entries(params)
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
.filter(([_, value]) => value !== undefined && value !== null)
|
||||||
|
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
|
||||||
|
return filteredParams.join('&');
|
||||||
|
};
|
||||||
|
|
||||||
|
export function hostUrl(url: string){
|
||||||
|
return DEBUG_MODE? "http://localhost:8000"+url: url;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function apiUrl(url: string) {
|
||||||
|
return hostUrl("/api/v1"+url);
|
||||||
|
}
|
||||||
16
src/utils/cookies.ts
Normal file
16
src/utils/cookies.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
export function setCookie(name: string, value: string, days: number) {
|
||||||
|
const expires = new Date(Date.now() + days * 864e5).toUTCString();
|
||||||
|
document.cookie = `${name}=${encodeURIComponent(value)}; expires=${expires}; path=/`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCookie(name: string) {
|
||||||
|
return document.cookie.split('; ')
|
||||||
|
.reduce((prev: string | null, current: string) => {
|
||||||
|
const [key, value] = current.split('=');
|
||||||
|
return key === name ? decodeURIComponent(value) : prev;
|
||||||
|
}, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteCookie(name: string) {
|
||||||
|
setCookie(name, '', -1);
|
||||||
|
}
|
||||||
@@ -1,13 +1,15 @@
|
|||||||
|
|
||||||
import {UserContext} from "./UserContext.ts";
|
import {UserContext} from "./UserContext.ts";
|
||||||
import {useContext} from "react";
|
import {useContext} from "react";
|
||||||
|
import {UserContextType} from "../../types/users.ts";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export function useUser() {
|
export function useUser(): undefined| UserContextType {
|
||||||
const context = useContext(UserContext);
|
const context = useContext(UserContext);
|
||||||
if (!context) {
|
if (!context) {
|
||||||
throw new Error('useUser must be used within a UserProvider');
|
console.error('useUser must be used within a UserProvider');
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,5 @@
|
|||||||
import {Account} from "../../types/users.ts";
|
import {UserContextType} from "../../types/users.ts";
|
||||||
import {createContext} from "react";
|
import {createContext} from "react";
|
||||||
|
|
||||||
export type UserContextType = {
|
|
||||||
user: Account | undefined;
|
|
||||||
login: (userData: Account) => void;
|
|
||||||
logout: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const UserContext = createContext<UserContextType | undefined>(undefined);
|
export const UserContext = createContext<UserContextType | undefined>(undefined);
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import React, {ReactNode, useEffect, useState} from 'react';
|
import React, {ReactNode, useEffect, useState} from 'react';
|
||||||
import {Account} from '../../types/users.ts';
|
import {Account, JwtTokenResponse, TokenObtainPair} from '../../types/users.ts';
|
||||||
import {UserContext} from "./UserContext.ts";
|
import {UserContext} from "./UserContext.ts";
|
||||||
|
import {refreshToken, verifyToken, login as apiLogin} from "../../API/users.ts";
|
||||||
|
import {getFetch} from "../../API/common.ts";
|
||||||
|
import {deleteCookie, getCookie, setCookie} from "../cookies.ts";
|
||||||
|
|
||||||
|
|
||||||
export function UserProvider({ children }: { children: ReactNode }) {
|
export function UserProvider({ children }: { children: ReactNode }) {
|
||||||
@@ -8,27 +11,87 @@ export function UserProvider({ children }: { children: ReactNode }) {
|
|||||||
const storedUser = localStorage.getItem('user');
|
const storedUser = localStorage.getItem('user');
|
||||||
return storedUser ? JSON.parse(storedUser) : undefined;
|
return storedUser ? JSON.parse(storedUser) : undefined;
|
||||||
});
|
});
|
||||||
|
const [tokens, setTokens] = useState<JwtTokenResponse | undefined>(() => {
|
||||||
|
const accessToken = getCookie('access_token');
|
||||||
|
const refreshToken = getCookie('refresh_token');
|
||||||
|
return accessToken && refreshToken ? { access: accessToken, refresh: refreshToken } : undefined;
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (tokens?.access) {
|
||||||
|
// Устанавливаем куки для токенов
|
||||||
|
setCookie('access_token', tokens.access, 7);
|
||||||
|
setCookie('refresh_token', tokens.refresh, 7);
|
||||||
|
fetchCurrentUser().then(userData => setUser(userData));
|
||||||
|
} else {
|
||||||
|
// Удаляем куки, если токены отсутствуют
|
||||||
|
deleteCookie('access_token');
|
||||||
|
deleteCookie('refresh_token');
|
||||||
|
}
|
||||||
|
}, [tokens]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log(user)
|
||||||
if (user) {
|
if (user) {
|
||||||
localStorage.setItem('user', JSON.stringify(user));
|
localStorage.setItem('user', JSON.stringify(user));
|
||||||
document.cookie = `user=${encodeURIComponent(JSON.stringify(user))}; path=/;`;
|
|
||||||
} else {
|
} else {
|
||||||
localStorage.removeItem('user');
|
localStorage.removeItem('user');
|
||||||
document.cookie = 'user=; path=/; expires=Thu, 01 Jan 1970 00:00:00 UTC;';
|
|
||||||
}
|
}
|
||||||
}, [user]);
|
}, [user]);
|
||||||
|
|
||||||
const login = (userData: Account) => {
|
|
||||||
setUser(userData);
|
const fetchCurrentUser = async (): Promise<Account | undefined> => {
|
||||||
|
let response = await getFetch(`/users/auth/users/me/`);
|
||||||
|
if (!response.success){
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
response = await getFetch(`/users/auth/users/${(response.body as Account).id}/`);
|
||||||
|
|
||||||
|
if (response.success){
|
||||||
|
return response.body as Account
|
||||||
|
}
|
||||||
|
// if (response?.success && response.body) {
|
||||||
|
// return response.body as Account; // Возвращаем данные о пользователе
|
||||||
|
// }
|
||||||
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
const logout = () => {
|
const handleLogin = async (credentials: TokenObtainPair) => {
|
||||||
setUser(undefined);
|
const tokens = await apiLogin(credentials);
|
||||||
|
if (tokens) {
|
||||||
|
setTokens(tokens);
|
||||||
|
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
setUser(undefined);
|
||||||
|
setTokens(undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateToken = async () => {
|
||||||
|
if (tokens?.access) {
|
||||||
|
const isValid = await verifyToken({ token: tokens.access });
|
||||||
|
if (!isValid && tokens.refresh) {
|
||||||
|
const newTokens = await refreshToken({ refresh: tokens.refresh });
|
||||||
|
if (newTokens) {
|
||||||
|
setTokens(newTokens);
|
||||||
|
} else {
|
||||||
|
handleLogout();
|
||||||
|
}
|
||||||
|
} else if (!isValid) {
|
||||||
|
handleLogout();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
validateToken().then();
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<UserContext.Provider value={{ user, login, logout }}>
|
<UserContext.Provider value={{ user, login: handleLogin, logout: handleLogout }}>
|
||||||
{children}
|
{children}
|
||||||
</UserContext.Provider>
|
</UserContext.Provider>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user