Skip to content

Commit 179ecd7

Browse files
committed
implemented JWT-based authentification: token generation, storage, and route guards.
1 parent 3285602 commit 179ecd7

File tree

18 files changed

+418
-254
lines changed

18 files changed

+418
-254
lines changed

frontend/index.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<!doctype html>
1+
<!DOCTYPE html>
22
<html lang="en">
33
<head>
44
<meta charset="UTF-8" />
@@ -7,7 +7,7 @@
77
<title>Vite + React</title>
88
</head>
99
<body>
10-
<div id="root"></div>
10+
<div id="root" class="vh-100"></div>
1111
<script type="module" src="/src/main.jsx"></script>
1212
</body>
1313
</html>

frontend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"name": "frontend",
33
"private": true,
44
"version": "0.0.0",
5+
"proxy": "http://localhost:5001",
56
"type": "module",
67
"scripts": {
78
"dev": "vite",

frontend/src/App.css

Lines changed: 0 additions & 42 deletions
This file was deleted.

frontend/src/App.jsx

Lines changed: 47 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,52 @@
1-
import { useState } from 'react';
2-
import reactLogo from './assets/react.svg';
3-
import viteLogo from '/vite.svg';
4-
import './App.css';
1+
/* eslint-disable react/prop-types */
2+
import 'bootstrap/dist/css/bootstrap.min.css';
3+
import { useSelector } from 'react-redux';
4+
import {
5+
BrowserRouter as Router,
6+
Routes,
7+
Route,
8+
Navigate,
9+
useLocation,
10+
} from 'react-router-dom';
511

6-
function App() {
7-
const [count, setCount] = useState(0);
12+
import Header from './Components/Header.jsx';
13+
import Login from './Components/Login.jsx';
14+
import MainPage from './Components/MainPage.jsx';
15+
import PageNotFound from './Components/PageNotFound.jsx';
816

9-
return (
10-
<>
11-
<div>
12-
<a href="https://vite.dev" target="_blank">
13-
<img src={viteLogo} className="logo" alt="Vite logo" />
14-
</a>
15-
<a href="https://react.dev" target="_blank">
16-
<img src={reactLogo} className="logo react" alt="React logo" />
17-
</a>
18-
</div>
19-
<h1>Vite + React</h1>
20-
<div className="card">
21-
<button onClick={() => setCount((count) => count + 1)}>
22-
count is {count}
23-
</button>
24-
<p>
25-
Edit <code>src/App.jsx</code> and save to test HMR
26-
</p>
27-
</div>
28-
<p className="read-the-docs">
29-
Click on the Vite and React logos to learn more
30-
</p>
31-
</>
17+
import './index.css';
18+
19+
const PrivateRoute = ({ children }) => {
20+
const loggedIn = useSelector((state) => state.auth.loggedIn);
21+
const location = useLocation();
22+
23+
return loggedIn ? (
24+
children
25+
) : (
26+
<Navigate to="/login" state={{ from: location }} />
3227
);
33-
}
28+
};
29+
30+
const App = () => (
31+
<Router>
32+
<div className="vh-100 d-flex flex-column">
33+
<Header />
34+
<div className="flex-grow-1">
35+
<Routes>
36+
<Route
37+
path="/"
38+
element={
39+
<PrivateRoute>
40+
<MainPage />
41+
</PrivateRoute>
42+
}
43+
/>
44+
<Route path="/login" element={<Login />} />
45+
<Route path="*" element={<PageNotFound />} />
46+
</Routes>
47+
</div>
48+
</div>
49+
</Router>
50+
);
3451

3552
export default App;

frontend/src/Components/Header.jsx

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { Button, Navbar, Container } from 'react-bootstrap';
2+
import { Link, useLocation, useNavigate } from 'react-router-dom';
3+
import { useSelector, useDispatch } from 'react-redux';
4+
5+
import { logout } from '../features/auth/authSlice';
6+
7+
const AuthButton = () => {
8+
const loggedIn = useSelector((state) => state.auth.loggedIn);
9+
const location = useLocation();
10+
const dispatch = useDispatch();
11+
const navigate = useNavigate();
12+
13+
const handleClick = () => {
14+
dispatch(logout());
15+
navigate('/login', { state: { from: location } });
16+
};
17+
18+
return loggedIn ? <Button onClick={handleClick}>Log out</Button> : <></>;
19+
};
20+
21+
const Header = () => {
22+
return (
23+
<Navbar className="shadow-sm navbar navbar-expand-lg navbar-light bg-white">
24+
<Container>
25+
<Navbar.Brand as={Link} to="/">
26+
Hexlet Chat
27+
</Navbar.Brand>
28+
<AuthButton />
29+
</Container>
30+
</Navbar>
31+
);
32+
};
33+
34+
export default Header;

frontend/src/Components/Login.jsx

Lines changed: 100 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,93 +1,124 @@
1-
import { useFormik /*, ErrorMessage*/ } from 'formik';
2-
import { Form, Button } from 'react-bootstrap';
1+
import { useState } from 'react';
2+
import { useFormik } from 'formik';
3+
import axios from 'axios';
4+
import { Form, Button, Container, Col, Card, Row } from 'react-bootstrap';
5+
import { useNavigate } from 'react-router-dom';
6+
import { useDispatch } from 'react-redux';
7+
8+
import { login } from '../features/auth/authSlice';
9+
10+
import logInImage from '../assets/avatar-DIE1AEpS.jpg';
311

412
const Login = () => {
13+
const navigate = useNavigate();
14+
const dispatch = useDispatch();
15+
const [authError, setAuthError] = useState(null);
16+
const [submitted, setSubmitted] = useState(false);
17+
518
const formik = useFormik({
619
initialValues: {
7-
nickname: '',
20+
username: '',
821
password: '',
922
},
1023
validate: (values) => {
1124
const errors = {};
1225

13-
if (!values.nickname) {
14-
errors.nickname = 'Обязательное поле';
15-
} else if (values.nickname.length < 3) {
16-
errors.nickname = 'Ник должен содержать минимум 3 символа';
26+
if (!values.username) {
27+
errors.username = 'Обязательное поле';
28+
} else if (values.username.length < 3) {
29+
errors.username = 'Ник должен содержать минимум 3 символа';
1730
}
1831

1932
if (!values.password) {
2033
errors.password = 'Обязательное поле';
21-
} else if (values.password.length < 6) {
22-
errors.password = 'Пароль должен содержать минимум 6 символов';
34+
} else if (values.password.length < 5) {
35+
errors.password = 'Пароль должен содержать минимум 5 символов';
2336
}
2437
return errors;
2538
},
26-
onSubmit: (values) => {
27-
console.log(JSON.stringify(values, null, 2));
39+
onSubmit: async (values) => {
40+
try {
41+
const response = await axios.post('/api/v1/login', values);
42+
dispatch(login({ user: values.username, token: response.data.token }));
43+
navigate('/');
44+
setSubmitted(false);
45+
} catch (error) {
46+
console.error('Ошибка авторизации', error);
47+
setAuthError('Неверное имя пользователя или пароль');
48+
setSubmitted(true);
49+
}
2850
},
2951
});
3052
return (
31-
<Form onSubmit={formik.handleSubmit}>
32-
<Form.Group className="mb-3" controlId="nickname">
33-
<Form.Label>Ваш ник</Form.Label>
34-
<Form.Control
35-
type="text"
36-
placeholder="Введите ваш ник"
37-
{...formik.getFieldProps('nickname')}
38-
isInvalid={formik.touched.nickname && !!formik.errors.nickname}
39-
/>
40-
<Form.Control.Feedback type="invalid">
41-
{formik.errors.nickname}
42-
</Form.Control.Feedback>
43-
</Form.Group>
44-
45-
<Form.Group className="mb-3" controlId="password">
46-
<Form.Label>Пароль</Form.Label>
47-
<Form.Control
48-
type="password"
49-
placeholder="Введите пароль"
50-
{...formik.getFieldProps('password')}
51-
isInvalid={formik.touched.password && !!formik.errors.password}
52-
/>
53-
<Form.Control.Feedback type="invalid">
54-
{formik.errors.password}
55-
</Form.Control.Feedback>
56-
</Form.Group>
53+
<Container className="h-100 d-flex flex-column">
54+
<Row className="justify-content-center align-content-center flex-grow-1">
55+
<Col md={8} xxl={6}>
56+
<Card className="shadow-sm">
57+
<Card.Body className="row p-5">
58+
{/* <Row className="align-items-center"> */}
59+
<Col
60+
md={6}
61+
className="d-flex align-items-center justify-content-center"
62+
>
63+
<img
64+
src={logInImage}
65+
alt="Log in"
66+
className="img-fluid rounded-circle"
67+
/>
68+
</Col>
69+
<Form
70+
onSubmit={formik.handleSubmit}
71+
className="col-12 col-md-6 mt-3 mt-md-0"
72+
>
73+
<h1 className="text-center mb-4">Войти</h1>
74+
<Form.Floating className="mb-3">
75+
<Form.Control
76+
type="text"
77+
placeholder="Ваш ник"
78+
{...formik.getFieldProps('username')}
79+
isInvalid={submitted && !!formik.errors.username}
80+
/>
81+
<Form.Label>Ваш ник</Form.Label>
82+
<Form.Control.Feedback type="invalid">
83+
{formik.errors.username}
84+
</Form.Control.Feedback>
85+
</Form.Floating>
5786

58-
<Button variant="primary" type="submit">
59-
Войти
60-
</Button>
61-
</Form>
62-
// <form onSubmit={formik.handleSubmit}>
63-
// <label htmlFor="Nickname">Ваш ник</label>
64-
// <input
65-
// id="nickname"
66-
// name="nickname"
67-
// type="text"
68-
// onChange={formik.handleChange}
69-
// value={formik.values.nickname}
70-
// />
71-
// <label htmlFor="password">Пароль</label>
72-
// <input
73-
// type="password"
74-
// name="password"
75-
// onChange={formik.handleChange}
76-
// value={formik.values.password}
77-
// />
78-
// {/* <ErrorMessage
79-
// component="div"
80-
// name="nickname"
81-
// className="invalid-feedback"
82-
// />
83-
// <ErrorMessage
84-
// component="div"
85-
// name="password"
86-
// className="invalid-feedback"
87-
// /> */}
87+
<Form.Floating className="mb-4">
88+
<Form.Control
89+
type="password"
90+
placeholder="Пароль"
91+
{...formik.getFieldProps('password')}
92+
isInvalid={submitted && !!formik.errors.password}
93+
/>
94+
<Form.Label>Пароль</Form.Label>
95+
<Form.Control.Feedback type="invalid">
96+
{formik.errors.password}
97+
</Form.Control.Feedback>
98+
</Form.Floating>
8899

89-
// <button type="submit">Submit</button>
90-
// </form>
100+
<Button
101+
variant="outline-primary"
102+
type="submit"
103+
className="w-100 mb-3"
104+
>
105+
Войти
106+
</Button>
107+
{authError && (
108+
<div className="text-danger mt-3 text-center">
109+
{authError}
110+
</div>
111+
)}
112+
</Form>
113+
{/* </Row> */}
114+
</Card.Body>
115+
<Card.Footer className="card_footer text-center p-4">
116+
Нет аккаунта? <a href="#">Регистрация</a>
117+
</Card.Footer>
118+
</Card>
119+
</Col>
120+
</Row>
121+
</Container>
91122
);
92123
};
93124

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
function MainPage() {
2+
return <div>Главная страница</div>;
3+
}
4+
5+
export default MainPage;

0 commit comments

Comments
 (0)