diff --git a/package-lock.json b/package-lock.json
index 88aea26..e00448d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,6 +12,7 @@
"react": "^19.2.4",
"react-dom": "^19.2.4",
"react-hook-form": "^7.72.1",
+ "react-router-dom": "^7.14.0",
"sass": "^1.99.0",
"zod": "^4.3.6"
},
@@ -1780,6 +1781,19 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/cookie": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
+ "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -2926,6 +2940,44 @@
"react": "^16.8.0 || ^17 || ^18 || ^19"
}
},
+ "node_modules/react-router": {
+ "version": "7.14.0",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.14.0.tgz",
+ "integrity": "sha512-m/xR9N4LQLmAS0ZhkY2nkPA1N7gQ5TUVa5n8TgANuDTARbn1gt+zLPXEm7W0XDTbrQ2AJSJKhoa6yx1D8BcpxQ==",
+ "license": "MIT",
+ "dependencies": {
+ "cookie": "^1.0.1",
+ "set-cookie-parser": "^2.6.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-router-dom": {
+ "version": "7.14.0",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.14.0.tgz",
+ "integrity": "sha512-2G3ajSVSZMEtmTjIklRWlNvo8wICEpLihfD/0YMDxbWK2UyP5EGfnoIn9AIQGnF3G/FX0MRbHXdFcD+rL1ZreQ==",
+ "license": "MIT",
+ "dependencies": {
+ "react-router": "7.14.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18"
+ }
+ },
"node_modules/readdirp": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
@@ -3400,6 +3452,12 @@
"semver": "bin/semver.js"
}
},
+ "node_modules/set-cookie-parser": {
+ "version": "2.7.2",
+ "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
+ "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
+ "license": "MIT"
+ },
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
diff --git a/package.json b/package.json
index 4d65347..3402283 100644
--- a/package.json
+++ b/package.json
@@ -14,6 +14,7 @@
"react": "^19.2.4",
"react-dom": "^19.2.4",
"react-hook-form": "^7.72.1",
+ "react-router-dom": "^7.14.0",
"sass": "^1.99.0",
"zod": "^4.3.6"
},
diff --git a/server/package-lock.json b/server/package-lock.json
index fb578d9..f666c51 100644
--- a/server/package-lock.json
+++ b/server/package-lock.json
@@ -9,8 +9,10 @@
"version": "1.0.0",
"license": "ISC",
"dependencies": {
+ "bcrypt": "^6.0.0",
"bcryptjs": "^3.0.3",
- "jsonwebtoken": "^9.0.3"
+ "jsonwebtoken": "^9.0.3",
+ "nodemailer": "^8.0.5"
},
"devDependencies": {
"@types/jsonwebtoken": "^9.0.10"
@@ -44,6 +46,20 @@
"undici-types": "~7.18.0"
}
},
+ "node_modules/bcrypt": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz",
+ "integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "dependencies": {
+ "node-addon-api": "^8.3.0",
+ "node-gyp-build": "^4.8.4"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
"node_modules/bcryptjs": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz",
@@ -159,6 +175,35 @@
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
+ "node_modules/node-addon-api": {
+ "version": "8.7.0",
+ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.7.0.tgz",
+ "integrity": "sha512-9MdFxmkKaOYVTV+XVRG8ArDwwQ77XIgIPyKASB1k3JPq3M8fGQQQE3YpMOrKm6g//Ktx8ivZr8xo1Qmtqub+GA==",
+ "license": "MIT",
+ "engines": {
+ "node": "^18 || ^20 || >= 21"
+ }
+ },
+ "node_modules/node-gyp-build": {
+ "version": "4.8.4",
+ "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
+ "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==",
+ "license": "MIT",
+ "bin": {
+ "node-gyp-build": "bin.js",
+ "node-gyp-build-optional": "optional.js",
+ "node-gyp-build-test": "build-test.js"
+ }
+ },
+ "node_modules/nodemailer": {
+ "version": "8.0.5",
+ "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.5.tgz",
+ "integrity": "sha512-0PF8Yb1yZuQfQbq+5/pZJrtF6WQcjTd5/S4JOHs9PGFxuTqoB/icwuB44pOdURHJbRKX1PPoJZtY7R4VUoCC8w==",
+ "license": "MIT-0",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
diff --git a/server/package.json b/server/package.json
index 8cae84e..9eab7a9 100644
--- a/server/package.json
+++ b/server/package.json
@@ -11,8 +11,10 @@
"license": "ISC",
"type": "commonjs",
"dependencies": {
+ "bcrypt": "^6.0.0",
"bcryptjs": "^3.0.3",
- "jsonwebtoken": "^9.0.3"
+ "jsonwebtoken": "^9.0.3",
+ "nodemailer": "^8.0.5"
},
"devDependencies": {
"@types/jsonwebtoken": "^9.0.10"
diff --git a/server/server.js b/server/server.js
index 33f4713..d908e27 100644
--- a/server/server.js
+++ b/server/server.js
@@ -1,74 +1,171 @@
-const http = require('http');
+const http = require("http");
-const jwt = require('jsonwebtoken');
+const jwt = require("jsonwebtoken");
+const bcrypt = require("bcrypt");
-const JWT_SECRET = 'my_super_secret_key';
+const crypto = require("crypto");
+
+const JWT_SECRET = "my_secret_key";
const PORT = 3000;
+const { parseBody } = require("./utils/parsedBody.ts");
+const { sendJSON } = require("./utils/sendJSON.ts");
+
const users = [
{
id: 1,
- name: 'Alex',
- password: '123456',
+ name: "Alex",
+ email: "test@mail.ru",
+ password: bcrypt.hashSync("123456", 10),
},
];
-console.log(users);
-
-function parseBody(req) {
- return new Promise((resolve, reject) => {
- let body = '';
-
- req.on('data', (chunk) => (body += chunk));
- req.on('end', () => {
- try {
- resolve(body ? JSON.parse(body) : {});
- } catch (err) {
- reject(err);
- }
- });
- req.on('error', reject);
- });
-}
-
-function sendJSON(res, statusCode, data) {
- res.writeHead(statusCode, { 'Content-type': 'application/json' });
- res.end(JSON.stringify(data));
-}
+const resetStore = new Map();
const server = http.createServer(async (req, res) => {
- res.setHeader('Access-Control-Allow-Origin', '*');
- res.setHeader('Access-Control-Allow-Methods', 'POST,GET, OPTIONS');
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
+ res.setHeader("Access-Control-Allow-Origin", "*");
+ res.setHeader("Access-Control-Allow-Methods", "POST,GET, OPTIONS");
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
- if (req.method === 'OPTIONS') {
+ if (req.method === "OPTIONS") {
res.writeHead(204);
res.end();
return;
}
- if (req.method === 'POST' && req.url === '/login') {
- const { name, password } = await parseBody(req);
+ if (req.method === "POST" && req.url === "/reset") {
+ const { password, token } = await parseBody(req);
+
+ const record = resetStore.get(token);
+
+ if (!record) {
+ return sendJSON(res, 404, {
+ message: "Неверная ссылка для восстановления",
+ });
+ }
+
+ if (record.expires < Date.now()) {
+ return sendJSON(res, 400, { message: "Срок действия ссылки истек" });
+ }
+
+ const user = users.find((u) => u.email === record.email);
+
+ if (!user) {
+ return sendJSON(res, 404, { message: "Польователь не найден" });
+ }
+
+ const hashPassword = await bcrypt.hash(password, 10);
+
+ user.password = hashPassword;
+ resetStore.delete(token);
+ sendJSON(res, 200, { message: "Пароль успешно обновлен" });
+ return;
+ }
+
+ if (req.method === "POST" && req.url === "/restore") {
+ const { email } = await parseBody(req);
+ const user = users.find((u) => u.email === email);
+
+ if (user) {
+ const token = crypto.randomBytes(32).toString("hex");
+ const expires = new Date(Date.now() + 15000);
+ resetStore.set(token, {
+ email,
+ expires,
+ });
+ const resetLink = "http://localhost:5173/reset?token=" + token;
+ console.log("Ссылка для сброса ", resetLink);
+ sendJSON(res, 200, { message: "Письмо отправлено на почту" });
+ } else {
+ sendJSON(res, 401, {
+ message: "Пользователя с таким email не существует",
+ });
+ }
+ return;
+ }
+
+ if (req.method === "POST" && req.url === "/login") {
+ const { name, password } = await parseBody(req);
const user = users.find((u) => u.name === name);
- if (user && password === user.password) {
+ if (user && (await bcrypt.compare(password, user.password))) {
const token = jwt.sign({ id: user.id, name: user.name }, JWT_SECRET, {
- expiresIn: '1h',
+ expiresIn: "1h",
});
sendJSON(res, 200, { token });
} else {
- sendJSON(res, 401, { message: 'Неверный email or пароль' });
+ sendJSON(res, 401, { message: "Неверное имя пользователя или пароль" });
}
return;
}
- if (req.method === 'GET' && req.url === '/profile') {
+ if (req.method === "POST" && req.url === "/registration") {
+ const { name, email, password } = await parseBody(req);
+
+ if (!name || !email || !password) {
+ return sendJSON(res, 400, { message: "Необходимо заполнить все поля" });
+ }
+
+ const findName = users.find((u) => u.name === name);
+ const findEmail = users.find((u) => u.email === email);
+
+ if (findName && findEmail) {
+ return sendJSON(res, 409, {
+ message: "Пользователь с таким именем и email уже существует",
+ fields: ["name", "email"],
+ });
+ }
+
+ if (findName) {
+ return sendJSON(res, 409, {
+ message: "Пользователь с таким именем уже существует",
+ fields: ["name"],
+ });
+ }
+
+ if (findEmail) {
+ return sendJSON(res, 409, {
+ message: "Пользователь с таким email уже существует",
+ fields: ["email"],
+ });
+ }
+
+ const hashedPassword = await bcrypt.hash(password, 10);
+
+ const newUser = {
+ id: Date.now(),
+ name,
+ email,
+ password: hashedPassword,
+ };
+
+ const token = jwt.sign(
+ {
+ id: newUser.id,
+ name: newUser.name,
+ email: newUser.email,
+ },
+ JWT_SECRET,
+ { expiresIn: "1h" },
+ );
+
+ users.push(newUser);
+
+ sendJSON(res, 201, {
+ message: "Регистрация прошла успешно",
+ token,
+ user: { id: newUser.id, name: newUser.name, email: newUser.email },
+ });
+ return;
+ }
+
+ if (req.method === "GET" && req.url === "/profile") {
const authHeader = req.headers.authorization;
- const token = authHeader && authHeader.split(' ')[1];
+ const token = authHeader && authHeader.split(" ")[1];
if (!token) {
- return sendJSON(res, 401, { message: 'Токен не предоставлен' });
+ return sendJSON(res, 401, { message: "Токен не предоставлен" });
}
try {
@@ -76,10 +173,10 @@ const server = http.createServer(async (req, res) => {
sendJSON(res, 200, { id: decoded.id, name: decoded.name });
return;
} catch (err) {
- sendJSON(res, 403, { message: 'Недействительный токен' });
+ sendJSON(res, 403, { message: "Недействительный токен" });
}
}
- sendJSON(res, 404, { message: 'Маршрут не найден' });
+ sendJSON(res, 404, { message: "Маршрут не найден" });
});
server.listen(PORT, () => {
diff --git a/server/utils/parsedBody.ts b/server/utils/parsedBody.ts
new file mode 100644
index 0000000..c226e37
--- /dev/null
+++ b/server/utils/parsedBody.ts
@@ -0,0 +1,17 @@
+function parseBody(req) {
+ return new Promise((resolve, reject) => {
+ let body = "";
+
+ req.on("data", (chunk) => (body += chunk));
+ req.on("end", () => {
+ try {
+ resolve(body ? JSON.parse(body) : {});
+ } catch (err) {
+ reject(err);
+ }
+ });
+ req.on("error", reject);
+ });
+}
+
+module.exports = { parseBody };
diff --git a/server/utils/sendJSON.ts b/server/utils/sendJSON.ts
new file mode 100644
index 0000000..3ac6e18
--- /dev/null
+++ b/server/utils/sendJSON.ts
@@ -0,0 +1,6 @@
+function sendJSON(res, statusCode, data) {
+ res.writeHead(statusCode, { "Content-type": "application/json" });
+ return res.end(JSON.stringify(data));
+}
+
+module.exports = { sendJSON };
diff --git a/src/App.tsx b/src/App.tsx
index e5fa125..f8264d1 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,9 +1,18 @@
-import Authorization from './components/Authorization';
+import { Route, Routes } from "react-router-dom";
+import { Authorization } from "./components/authorization";
+import { Registration } from "./components/registration";
+import { RestorePassword } from "./components/restorePassword";
+import { ResetPassword } from "./components/resetPassword";
function App() {
return (
-
-
+
+
+ } />
+ } />
+ } />
+ } />
+
);
}
diff --git a/src/api/url.js b/src/api/url.js
new file mode 100644
index 0000000..fcd33c6
--- /dev/null
+++ b/src/api/url.js
@@ -0,0 +1 @@
+export const url = `http://localhost:3000`;
diff --git a/src/assets/HidePassword.tsx b/src/assets/HidePassword.tsx
new file mode 100644
index 0000000..0f34f4c
--- /dev/null
+++ b/src/assets/HidePassword.tsx
@@ -0,0 +1,49 @@
+function HidePassword() {
+ return (
+
+ );
+}
+
+export default HidePassword;
diff --git a/src/components/Authorization.tsx b/src/components/Authorization.tsx
deleted file mode 100644
index 0dc8069..0000000
--- a/src/components/Authorization.tsx
+++ /dev/null
@@ -1,138 +0,0 @@
-import { useForm } from 'react-hook-form';
-import { ErrorMessage } from '@hookform/error-message';
-import { useState } from 'react';
-import styles from './authorization.module.scss';
-import photo from '../assets/hidepassword.svg';
-interface UserLogin {
- name: string;
- password: string;
-}
-
-const URL = `http://localhost:3000`;
-
-function Authorization() {
- const [error, setError] = useState('');
-
- const {
- register,
- handleSubmit,
- reset,
- formState: { errors },
- } = useForm({
- mode: 'onChange',
- defaultValues: {
- name: '',
- password: '',
- },
- });
-
- const onSubmit = async (data: UserLogin) => {
- try {
- const response = await fetch(`${URL}/login`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ name: data.name, password: data.password }),
- });
-
- const responseData = await response.json();
-
- if (!response.ok) {
- throw new Error(responseData.message || 'Ошибка входа');
- }
- localStorage.setItem('token', responseData.token);
- reset();
- setError('');
- } catch (error) {
- console.error('Ошибка при логине:', error);
- setError(error.message);
- }
- };
-
- return (
-
-
-
ВХОД
- {error &&
{error}
}
-
-
-
- );
-}
-
-export default Authorization;
diff --git a/src/components/authorization.module.scss b/src/components/authorization.module.scss
deleted file mode 100644
index a09cba1..0000000
--- a/src/components/authorization.module.scss
+++ /dev/null
@@ -1,43 +0,0 @@
-.wrapperHeader{
- position: relative;
- display: inline-block;
- text-align: center;
- margin-bottom: 20px;
- width: 250px;
-}
-.title{
- color:#000;
- width: 100%;
- font-size: 30px;
- text-align: center;
- font-weight: 300;
- margin-bottom: 20px;
-}
-
-.error{
- position: absolute;
- left: 0;
- width: 250px;
- font-size: 13px;
- text-align: left;
- display: inline-flex;
- color: red;
-}
-
-.password{
- position: relative;
-}
-
-.passwordIcon{
- position: absolute;
- top: 4px;
- right: 5px;
- stroke:#727272;
- fill: none;
- transition: all .2s ease-in-out;
-
- &:hover{
- cursor: pointer;
- stroke:red;
- }
-}
\ No newline at end of file
diff --git a/src/components/authorization/Authorization.tsx b/src/components/authorization/Authorization.tsx
new file mode 100644
index 0000000..edfcb52
--- /dev/null
+++ b/src/components/authorization/Authorization.tsx
@@ -0,0 +1,126 @@
+import { useState } from "react";
+import { url } from "../../api/url";
+import { useForm } from "react-hook-form";
+import { ErrorMessage } from "@hookform/error-message";
+import { Link } from "react-router-dom";
+
+import HidePassword from "../../assets/HidePassword";
+import { useTogglePassword } from "../../utils/togglePassword";
+
+import styles from "./authorization.module.scss";
+
+interface UserLogin {
+ name: string;
+ password: string;
+}
+
+function Authorization() {
+ const [error, setError] = useState("");
+ const { showPassword, togglePassword } = useTogglePassword();
+
+ const {
+ register,
+ handleSubmit,
+ reset,
+ formState: { errors },
+ } = useForm({
+ mode: "onChange",
+ defaultValues: {
+ name: "",
+ password: "",
+ },
+ });
+
+ const onSubmit = async (data: UserLogin) => {
+ try {
+ const response = await fetch(`${url}/login`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ name: data.name, password: data.password }),
+ });
+
+ const responseData = await response.json();
+
+ if (!response.ok) {
+ throw new Error(responseData.message || "Ошибка входа");
+ }
+ alert("Вы успешно авторизовались ");
+ console.log(data);
+ localStorage.setItem("token", responseData.token);
+ reset();
+ setError("");
+ } catch (error) {
+ setError(error.message);
+ }
+ };
+
+ return (
+
+
+
+
+
+
ВХОД
+ {error &&
{error}
}
+
+
+
+ );
+}
+
+export default Authorization;
diff --git a/src/components/authorization/authorization.module.scss b/src/components/authorization/authorization.module.scss
new file mode 100644
index 0000000..41c448a
--- /dev/null
+++ b/src/components/authorization/authorization.module.scss
@@ -0,0 +1,154 @@
+.form {
+ width: 250px;
+ gap: 20px;
+ display: flex;
+ flex-direction: column;
+}
+
+.block {
+ position: relative;
+}
+
+input {
+ outline: none;
+ outline: none;
+ font-size: 16;
+ color: black;
+ padding: 10px;
+ border-radius: 5px;
+}
+
+.wrapperHeader {
+ position: relative;
+ display: inline-block;
+ text-align: center;
+ margin-bottom: 20px;
+ width: 250px;
+}
+
+.title {
+ text-transform: uppercase;
+ color: #000;
+ width: 100%;
+ font-size: 30px;
+ text-align: center;
+ font-weight: 300;
+ margin-bottom: 20px;
+}
+
+
+.errorPassword {
+ position: relative;
+ color: red;
+ font-size: 13px;
+ display: inline-flex;
+}
+
+.error {
+ width: 260px;
+ position: absolute;
+ left: 0;
+ bottom: -18px;
+ color: red;
+ font-size: 13px;
+ display: inline-flex;
+}
+
+.span {
+ display: inline-block;
+ margin-bottom: 5px;
+}
+
+.password,
+.name {
+ width: 250px;
+}
+
+.label {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+}
+
+.passwordIcon {
+ border: none;
+ background: none;
+ position: absolute;
+ bottom: 6px;
+ right: 0px;
+ stroke: #727272;
+ fill: none;
+ transition: all .2s ease-in-out;
+
+ &:hover {
+ cursor: pointer;
+ stroke: red;
+ }
+}
+
+.passwordIconActive {
+ border: none;
+ background: none;
+ position: absolute;
+ bottom: 6px;
+ right: 0px;
+ stroke: red;
+ fill: none;
+ transition: all .2s ease-in-out;
+
+ &:hover {
+ cursor: pointer;
+ stroke: 727272;
+ }
+}
+
+.restore {
+ cursor: pointer;
+ margin-top: -15px;
+ border: none;
+ background: none;
+ transition: all .2s ease-in-out;
+
+
+ &:hover {
+ color: red;
+ }
+}
+
+.btn {
+ width: 100%;
+ cursor: pointer;
+ border: none;
+ color: #fff;
+ background: #000;
+ padding: 15px;
+ border-radius: 10px;
+ transition: all .18s ease-in-out;
+
+
+ &:hover {
+ background: #cbcbcb;
+ color: #000;
+ z-index: 100;
+ }
+}
+
+.btnRegister {
+ position: absolute;
+ right: 0;
+ top: 0;
+ width: 150px;
+ cursor: pointer;
+ border: none;
+ color: #fff;
+ background: #000;
+ padding: 15px;
+ border-radius: 10px;
+ transition: all .18s ease-in-out;
+
+ &:hover {
+ background: #cbcbcb;
+ color: #000;
+ z-index: 100;
+ }
+}
\ No newline at end of file
diff --git a/src/components/authorization/index.tsx b/src/components/authorization/index.tsx
new file mode 100644
index 0000000..4334e2e
--- /dev/null
+++ b/src/components/authorization/index.tsx
@@ -0,0 +1 @@
+export { default as Authorization } from './Authorization';
diff --git a/src/components/registration/Registration.tsx b/src/components/registration/Registration.tsx
new file mode 100644
index 0000000..0d72825
--- /dev/null
+++ b/src/components/registration/Registration.tsx
@@ -0,0 +1,232 @@
+import { useState } from "react";
+import { Link } from "react-router-dom";
+import { useForm } from "react-hook-form";
+import { ErrorMessage } from "@hookform/error-message";
+
+import { url } from "../../api/url";
+import HidePassword from "../../assets/HidePassword";
+
+import { useTogglePassword } from "../../utils/togglePassword";
+
+import styles from "./registration.module.scss";
+
+interface UserLogin {
+ name: string;
+ password: string;
+ email: string;
+ passwordConfirm: string;
+}
+
+function Registration() {
+ const { showPassword, setShowPassword, togglePassword } = useTogglePassword();
+
+ const [generalError, setGeneralError] = useState("");
+ const {
+ register,
+ handleSubmit,
+ reset,
+ setError,
+ watch,
+ formState: { errors },
+ clearErrors,
+ } = useForm({
+ mode: "onChange",
+ defaultValues: {
+ name: "",
+ password: "",
+ email: "",
+ passwordConfirm: "",
+ },
+ });
+
+ const onSubmit = async (data: UserLogin) => {
+ try {
+ const { passwordConfirm, ...dataForServer } = data;
+
+ const response = await fetch(`${url}/registration`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(dataForServer),
+ });
+
+ const responseData = await response.json();
+
+ if (responseData.fields && Array.isArray(responseData.fields)) {
+ console.log(responseData);
+ setGeneralError(responseData.message);
+
+ responseData.fields.forEach((field) => {
+ setError(field, { type: "manual", message: "" });
+ });
+ }
+
+ if (!response.ok) {
+ throw new Error(responseData.message || "Ошибка входа");
+ }
+ setGeneralError("");
+ reset();
+ setShowPassword(false);
+ alert("Вы успешно зарегестрировались ");
+ } catch (error) {
+ throw new Error(error);
+ }
+ };
+
+ return (
+
+
+
+
+
+
Регистрация
+ {generalError &&
{generalError}
}
+
+
+
+
+ );
+}
+
+export default Registration;
diff --git a/src/components/registration/index.tsx b/src/components/registration/index.tsx
new file mode 100644
index 0000000..23c7d57
--- /dev/null
+++ b/src/components/registration/index.tsx
@@ -0,0 +1 @@
+export { default as Registration } from './Registration';
diff --git a/src/components/registration/registration.module.scss b/src/components/registration/registration.module.scss
new file mode 100644
index 0000000..c7ea50d
--- /dev/null
+++ b/src/components/registration/registration.module.scss
@@ -0,0 +1,170 @@
+.form {
+ width: 250px;
+ display: flex;
+ flex-direction: column;
+ gap: 15px;
+}
+
+.errorsInput {
+ border: 1px solid red;
+}
+
+.block {
+ position: relative;
+}
+
+input {
+ outline: none;
+ outline: none;
+ font-size: 16;
+ color: black;
+ padding: 10px;
+ border-radius: 5px;
+}
+
+.wrapperHeader {
+ position: relative;
+ display: inline-block;
+ text-align: center;
+ margin-bottom: 20px;
+ width: 250px;
+}
+
+.title {
+ text-transform: uppercase;
+ color: #000;
+ width: 100%;
+ font-size: 30px;
+ text-align: center;
+ font-weight: 300;
+ margin-bottom: 20px;
+}
+
+
+.passwordConfirm {
+ margin-bottom: 15px;
+}
+
+.errorPasswordConfirm {
+ position: absolute;
+ left: 0;
+ bottom: -16px;
+ color: red;
+ font-size: 13px;
+ display: inline-flex;
+}
+
+.errorPassword {
+ position: relative;
+ color: red;
+ font-size: 13px;
+ display: inline-flex;
+}
+
+.error {
+ position: absolute;
+ left: 0;
+ bottom: -18px;
+ color: red;
+ font-size: 13px;
+ display: inline-flex;
+}
+
+.span {
+ display: inline-block;
+ margin-bottom: 5px;
+}
+
+.password,
+.name {
+ width: 250px;
+}
+
+.label {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+}
+
+.passwordIcon {
+ border: none;
+ background: none;
+ position: absolute;
+ bottom: 6px;
+ right: 0px;
+ stroke: #727272;
+ fill: none;
+ transition: all .2s ease-in-out;
+
+ &:hover {
+ cursor: pointer;
+ stroke: red;
+ }
+}
+
+.passwordIconActive {
+ border: none;
+ background: none;
+ position: absolute;
+ bottom: 6px;
+ right: 0px;
+ stroke: red;
+ fill: none;
+ transition: all .2s ease-in-out;
+
+ &:hover {
+ cursor: pointer;
+ stroke: 727272;
+ }
+}
+
+.btn {
+ width: 100%;
+ cursor: pointer;
+ border: none;
+ color: #fff;
+ background: #000;
+ padding: 15px;
+ border-radius: 10px;
+ transition: all .2s ease-in;
+
+
+ &:hover {
+ background: #cbcbcb;
+ color: #000;
+ z-index: 100;
+ }
+}
+
+.btnRegister {
+ position: absolute;
+ right: 0;
+ top: 0;
+ width: 150px;
+ cursor: pointer;
+ border: none;
+ color: #fff;
+ background: #000;
+ padding: 15px;
+ border-radius: 10px;
+ transition: all .18s ease-in-out;
+
+ &:hover {
+ background: #cbcbcb;
+ color: #000;
+ z-index: 100;
+ }
+}
+
+.restore {
+ cursor: pointer;
+ margin-top: -15px;
+ border: none;
+ background: none;
+ transition: all .2s ease-in-out;
+
+
+ &:hover {
+ color: red;
+ }
+}
\ No newline at end of file
diff --git a/src/components/resetPassword/ResetPassword.tsx b/src/components/resetPassword/ResetPassword.tsx
new file mode 100644
index 0000000..e3eb846
--- /dev/null
+++ b/src/components/resetPassword/ResetPassword.tsx
@@ -0,0 +1,150 @@
+import { ErrorMessage } from "@hookform/error-message";
+import { useForm } from "react-hook-form";
+import HidePassword from "../../assets/HidePassword";
+import { url } from "../../api/url";
+
+import { useTogglePassword } from "../../utils/togglePassword";
+
+import styles from "./resetPassword.module.scss";
+import { useNavigate } from "react-router-dom";
+
+function ResetPassword() {
+ const navigate = useNavigate();
+
+ const {
+ register,
+ handleSubmit,
+ watch,
+ reset,
+ formState: { errors },
+ } = useForm({
+ mode: "onChange",
+ defaultValues: {
+ password: "",
+ passwordConfirm: "",
+ },
+ });
+
+ const onSubmit = async (data) => {
+ try {
+ const urlParams = new URLSearchParams(window.location.search);
+
+ const token = urlParams.get("token");
+ data["token"] = token;
+
+ const { passwordConfirm, ...dataForServer } = data;
+
+ const response = await fetch(`${url}/reset`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(dataForServer),
+ });
+
+ if (response) {
+ const responseData = await response.json();
+ console.log(responseData);
+ alert("Пароль изменен");
+ reset();
+ setTimeout(() => {
+ navigate("/");
+ }, 2000);
+ }
+ } catch (error) {
+ console.log(error);
+ }
+ };
+
+ const { showPassword, setShowPassword, togglePassword } = useTogglePassword();
+
+ return (
+
+ );
+}
+export default ResetPassword;
diff --git a/src/components/resetPassword/index.ts b/src/components/resetPassword/index.ts
new file mode 100644
index 0000000..301cb09
--- /dev/null
+++ b/src/components/resetPassword/index.ts
@@ -0,0 +1 @@
+export { default as ResetPassword } from './ResetPassword';
diff --git a/src/components/resetPassword/resetPassword.module.scss b/src/components/resetPassword/resetPassword.module.scss
new file mode 100644
index 0000000..e69de29
diff --git a/src/components/restorePassword/RestorePassword.tsx b/src/components/restorePassword/RestorePassword.tsx
new file mode 100644
index 0000000..8b6d01b
--- /dev/null
+++ b/src/components/restorePassword/RestorePassword.tsx
@@ -0,0 +1,81 @@
+import { useForm } from "react-hook-form";
+import { url } from "../../api/url";
+
+import { Link } from "react-router-dom";
+
+import styles from "./restore.module.scss";
+import { useState } from "react";
+
+function RestorePassword() {
+ const [message, setMessage] = useState("");
+
+ const {
+ register,
+ handleSubmit,
+ reset,
+ formState: { errors },
+ clearErrors,
+ } = useForm({
+ mode: "onChange",
+ defaultValues: {
+ email: "",
+ },
+ });
+
+ const onSubmit = async (data: { email: string }) => {
+ setMessage("");
+
+ try {
+ const response = await fetch(`${url}/restore`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(data),
+ });
+ const responseData = await response.json();
+
+ if (responseData) {
+ reset();
+ setMessage("Успешно! Письмо отправлено на почту!");
+
+ }
+ } catch (error) {
+ console.error(error);
+ }
+ };
+
+ return (
+
+ {message &&
{message}
}
+
+ {message && (
+
+ Хотите вернуться на главную страницу?
+
+ )}
+
+ );
+}
+
+export default RestorePassword;
diff --git a/src/components/restorePassword/index.tsx b/src/components/restorePassword/index.tsx
new file mode 100644
index 0000000..2697053
--- /dev/null
+++ b/src/components/restorePassword/index.tsx
@@ -0,0 +1 @@
+export { default as RestorePassword } from './RestorePassword';
diff --git a/src/components/restorePassword/restore.module.scss b/src/components/restorePassword/restore.module.scss
new file mode 100644
index 0000000..5f5dd24
--- /dev/null
+++ b/src/components/restorePassword/restore.module.scss
@@ -0,0 +1,3 @@
+.message{
+ display: block;
+}
\ No newline at end of file
diff --git a/src/index.css b/src/index.css
index 6884284..cf346ab 100644
--- a/src/index.css
+++ b/src/index.css
@@ -5,6 +5,11 @@ h1,h2,h3,h4,h5,h6,p, a{
padding: 0;
margin: 0;
}
+
+a{
+ text-decoration: none;
+}
+
body{
font-family: "Lora", serif;
font-size: 16px;
@@ -12,36 +17,18 @@ body{
height: 100%;
opacity: 0.8;
}
-.form{
- display: flex;
- flex-direction: column;
- gap: 20px ;
+
+.container{
+ position: relative;
+ margin: 0 auto;
+ max-width: 700px;
+ padding: 0 15px;
}
.wrapper{
+ align-items: center;
display: flex;
flex-direction: column;
align-items: center;
- justify-content: center;
-}
-
-.title{
- color:#fff;
- width: 100%;
- font-size: 30px;
- text-align: center;
- font-weight: 300;
-}
-
-.firstName, .password{
- position: relative;
}
-.error{
- position: absolute;
- left: 0;
- bottom: -30px;
- font-size: 13px;
- display: inline-flex;
- color: red;
-}
\ No newline at end of file
diff --git a/src/main.tsx b/src/main.tsx
index 6a0a2d0..24c6ae4 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -1,5 +1,10 @@
import { createRoot } from 'react-dom/client';
import './index.css';
import App from './App.tsx';
+import { BrowserRouter } from 'react-router-dom';
-createRoot(document.getElementById('root')!).render(
);
+createRoot(document.getElementById('root')!).render(
+
+
+ ,
+);
diff --git a/src/utils/togglePassword.ts b/src/utils/togglePassword.ts
new file mode 100644
index 0000000..b5b1718
--- /dev/null
+++ b/src/utils/togglePassword.ts
@@ -0,0 +1,10 @@
+import { useCallback, useState } from 'react';
+
+export const useTogglePassword = () => {
+ const [showPassword, setShowPassword] = useState
(false);
+
+ const togglePassword = useCallback(() => {
+ setShowPassword((prev) => !prev);
+ }, []);
+ return { showPassword, togglePassword, setShowPassword };
+};