parent
1352b2a080
commit
5313a8f5dd
@ -1,8 +1,21 @@
|
||||
import "./App.css";
|
||||
import { MapComponent } from "./Map/MapComponent";
|
||||
import { BrowserRouter, Route, Routes } from "react-router-dom";
|
||||
import { LoginPage } from "./pages/Login";
|
||||
import { VerifyRegistrationPage } from "./pages/VerifyRegistration";
|
||||
import { RegisterPage } from "./pages/Register";
|
||||
import { MapPage } from "./pages/Map";
|
||||
|
||||
function App() {
|
||||
return <MapComponent />;
|
||||
return (
|
||||
<BrowserRouter basename={import.meta.env.BASE_URL}>
|
||||
<Routes>
|
||||
<Route path="/" element={<MapPage />} />
|
||||
<Route path="/signin" element={<LoginPage />} />
|
||||
<Route path="/verify-user" element={<VerifyRegistrationPage />} />
|
||||
<Route path="/register" element={<RegisterPage />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
||||
@ -0,0 +1,22 @@
|
||||
import { Button } from "antd";
|
||||
import { LogoutOutlined } from "@ant-design/icons";
|
||||
import { setToken } from "./stores/auth";
|
||||
import { api } from "./api";
|
||||
|
||||
export function SignOut() {
|
||||
return (
|
||||
<div className="absolute top-[20px] right-[20px]">
|
||||
<Button
|
||||
icon={<LogoutOutlined />}
|
||||
type="primary"
|
||||
size="large"
|
||||
title="Выйти"
|
||||
onClick={async () => {
|
||||
await api.post("accounts/logout/");
|
||||
|
||||
setToken("");
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
import { useStore } from "@nanostores/react";
|
||||
import { Spin } from "antd";
|
||||
|
||||
import { Navigate } from "react-router-dom";
|
||||
|
||||
import { isAuthorized$, userInfoLoading$ } from "./stores/auth";
|
||||
|
||||
export function WithAuth(props) {
|
||||
const isAuthorized = useStore(isAuthorized$);
|
||||
const userInfoLoading = useStore(userInfoLoading$);
|
||||
|
||||
if (userInfoLoading) {
|
||||
return <Spin className="user-info-loader" />;
|
||||
}
|
||||
|
||||
if (isAuthorized) {
|
||||
return <>{props.children}</>;
|
||||
}
|
||||
|
||||
return <Navigate to="/signin" replace={true} />;
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
import axios from "axios";
|
||||
|
||||
export const api = axios.create({
|
||||
baseURL:
|
||||
import.meta.env.MODE === "development"
|
||||
? "http://localhost:5173/"
|
||||
: "https://property.spatiality.website/",
|
||||
withCredentials: true,
|
||||
xsrfHeaderName: "X-CSRFToken",
|
||||
xsrfCookieName: "csrftoken",
|
||||
});
|
||||
@ -0,0 +1,80 @@
|
||||
import { useStore } from "@nanostores/react";
|
||||
import { Alert, Button, Form, Input, Space, Typography } from "antd";
|
||||
import { LockOutlined, UserOutlined } from "@ant-design/icons";
|
||||
import React from "react";
|
||||
import { Link, Navigate } from "react-router-dom";
|
||||
import { isAuthorized$ } from "../stores/auth";
|
||||
import { signin, signinError$, signinLoading$ } from "../stores/signin";
|
||||
|
||||
function LoginForm() {
|
||||
const signinError = useStore(signinError$);
|
||||
const signinLoading = useStore(signinLoading$);
|
||||
|
||||
const onFinish = (values) => {
|
||||
signin(values);
|
||||
};
|
||||
|
||||
return (
|
||||
<Space direction="vertical" style={{ width: "320px" }}>
|
||||
{signinError.length > 0 ? (
|
||||
<Alert type="error" showIcon closable description={signinError} />
|
||||
) : null}
|
||||
<Typography.Title level={4}>Вход</Typography.Title>
|
||||
|
||||
<Form
|
||||
disabled={signinLoading}
|
||||
name="basic"
|
||||
layout="vertical"
|
||||
onFinish={onFinish}
|
||||
autoComplete="off"
|
||||
>
|
||||
<Form.Item
|
||||
label="Логин"
|
||||
name="login"
|
||||
rules={[{ required: true, message: "Обязательное поле" }]}
|
||||
>
|
||||
<Input
|
||||
prefix={<UserOutlined className="site-form-item-icon" />}
|
||||
placeholder="логин"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="Пароль"
|
||||
name="password"
|
||||
rules={[{ required: true, message: "Обязательное поле" }]}
|
||||
>
|
||||
<Input
|
||||
prefix={<LockOutlined className="site-form-item-icon" />}
|
||||
type="password"
|
||||
placeholder="пароль"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<Button block type="primary" htmlType="submit">
|
||||
Войти
|
||||
</Button>
|
||||
</Form.Item>
|
||||
|
||||
<div style={{ textAlign: "center" }}>
|
||||
<Link to="/register">Регистрация</Link>
|
||||
</div>
|
||||
</Form>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
|
||||
export function LoginPage() {
|
||||
const isAuthorized = useStore(isAuthorized$);
|
||||
|
||||
if (isAuthorized) {
|
||||
return <Navigate to="/" replace={true} />;
|
||||
} else {
|
||||
return (
|
||||
<main className="w-screen h-screen flex items-center justify-center">
|
||||
<LoginForm />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
import { WithAuth } from "../WithAuth";
|
||||
import { MapComponent } from "../Map/MapComponent";
|
||||
|
||||
export function MapPage() {
|
||||
return (
|
||||
<WithAuth>
|
||||
<MapComponent />
|
||||
</WithAuth>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,30 @@
|
||||
import { useState } from "react";
|
||||
import { Navigate, useSearchParams } from "react-router-dom";
|
||||
import { api } from "../api";
|
||||
import { useEffectOnce } from "../useEffectOnce";
|
||||
|
||||
export function VerifyRegistrationPage() {
|
||||
const [params] = useSearchParams();
|
||||
const [isVerified, setIsVerified] = useState(false);
|
||||
|
||||
useEffectOnce(() => {
|
||||
async function verify() {
|
||||
try {
|
||||
await api.post("accounts/verify-registration/", {
|
||||
user_id: params.get("user_id"),
|
||||
timestamp: params.get("timestamp"),
|
||||
signature: params.get("signature"),
|
||||
});
|
||||
} finally {
|
||||
setIsVerified(true);
|
||||
}
|
||||
}
|
||||
|
||||
verify();
|
||||
}, []);
|
||||
|
||||
if (!isVerified) {
|
||||
return <div>Verifying...</div>;
|
||||
}
|
||||
return <Navigate to="/" />;
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
import { action, atom, computed } from "nanostores";
|
||||
import { api } from "../api";
|
||||
|
||||
export const token$ = atom("");
|
||||
export const userInfoLoading$ = atom(true);
|
||||
|
||||
export const isAuthorized$ = computed([token$], (token) => token !== "");
|
||||
|
||||
export const setToken = action(token$, "setToken", (store, newValue) => {
|
||||
store.set(newValue);
|
||||
});
|
||||
|
||||
async function checkAuth() {
|
||||
try {
|
||||
await api.get("/accounts/profile/");
|
||||
|
||||
setToken("123456");
|
||||
} catch (e) {
|
||||
console.log("Not authorized");
|
||||
} finally {
|
||||
userInfoLoading$.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
checkAuth();
|
||||
@ -0,0 +1,37 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
export const useEffectOnce = (effect) => {
|
||||
const effectFn = useRef(effect);
|
||||
const destroyFn = useRef();
|
||||
const effectCalled = useRef(false);
|
||||
const rendered = useRef(false);
|
||||
const [, setVal] = useState(0);
|
||||
|
||||
if (effectCalled.current) {
|
||||
rendered.current = true;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// only execute the effect first time around
|
||||
if (!effectCalled.current) {
|
||||
destroyFn.current = effectFn.current();
|
||||
effectCalled.current = true;
|
||||
}
|
||||
|
||||
// this forces one render after the effect is run
|
||||
setVal((val) => val + 1);
|
||||
|
||||
return () => {
|
||||
// if the comp didn't render since the useEffect was called,
|
||||
// we know it's the dummy React cycle
|
||||
if (!rendered.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
// otherwise this is not a dummy destroy, so call the destroy func
|
||||
if (destroyFn.current) {
|
||||
destroyFn.current();
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
};
|
||||
Loading…
Reference in new issue