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}

} -
-
-
- - {message}} - /> -
- -
- -
- -
-
- ); -} - -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}

} +
+
+
+ + + {message}} + /> +
+ +
+ +
+ {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}

} +
+ +
+
+ +
+ +
+ + {errors.email && ( +

{errors.email?.message}

+ )} +
+ +
+ + ( +

+ {message} +

+ )} + /> +
+ +
+ + + ( +

+ {message} +

+ )} + /> +
+ + {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 ( +
+
+
+ + ( +

+ {message} +

+ )} + /> +
+ +
+ + + ( +

+ {message} +

+ )} + /> +
+ + +
+
+ ); +} +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}

} +
+
+ + {errors.email && ( +

{errors.email?.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 }; +};