parent
1352b2a080
commit
5313a8f5dd
@ -1,8 +1,21 @@
|
|||||||
import "./App.css";
|
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() {
|
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;
|
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