diff --git a/client/src/components/Calendar.tsx b/client/src/components/Calendar.tsx index 2d8389d..3b912f4 100644 --- a/client/src/components/Calendar.tsx +++ b/client/src/components/Calendar.tsx @@ -51,21 +51,21 @@ function Calendar() { {isShowCalendar && (
- + />

{_.capitalize(format(currentMonth, "LLLL, yyyy", { locale: ru }))}

- + />
{["Пн", "Вт", "Ср", "Чт", "Пт", "Сб", "Вс"].map((value, index) => ( diff --git a/client/src/components/Managers.tsx b/client/src/components/Managers.tsx index 52a64bb..44fbb3f 100644 --- a/client/src/components/Managers.tsx +++ b/client/src/components/Managers.tsx @@ -30,8 +30,7 @@ function Managers() {
- {user?.name.split(" ")[0][0]} - {user?.name.split(" ")[1][0]} + {manager?.name.split(" ")[0][0]}

{manager.name}

diff --git a/client/src/components/Menu.tsx b/client/src/components/Menu.tsx index 78bf571..80c5ef5 100644 --- a/client/src/components/Menu.tsx +++ b/client/src/components/Menu.tsx @@ -73,7 +73,6 @@ function Menu() {

{user?.name.split(" ")[0][0]} - {user?.name.split(" ")[1][0]}

diff --git a/client/src/components/Schedule.tsx b/client/src/components/Schedule.tsx index a048a28..8a8e15f 100644 --- a/client/src/components/Schedule.tsx +++ b/client/src/components/Schedule.tsx @@ -2,7 +2,15 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { useEffect, useState } from "react"; import Timeline from "./Timeline"; -import { format, getDate, setDate, setHours, startOfDay } from "date-fns"; +import { + format, + getDate, + getMonth, + setDate, + setHours, + setMonth, + startOfDay, +} from "date-fns"; // import useEventStore from "../stores/useEventStore"; import Button from "./Button"; import CloseIcon from "./icons/CloseIcon"; @@ -40,7 +48,12 @@ function Schedule({ selectedDay, slots, events }: Props) { } function handleChangeStartAt(startAt: Date) { - setStartAt(setDate(startAt, getDate(selectedDay))); + let newStartAt = setMonth(startAt, getMonth(selectedDay)); + newStartAt = setDate(newStartAt, getDate(selectedDay)); + + // console.log("newStartAt", newStartAt); + + setStartAt(newStartAt); setDateForInstantStart(undefined); } diff --git a/client/src/components/Select3.tsx b/client/src/components/Select3.tsx index 2ab6e07..3911dad 100644 --- a/client/src/components/Select3.tsx +++ b/client/src/components/Select3.tsx @@ -5,12 +5,13 @@ import { useClickAway } from "@uidotdev/usehooks"; import CheckIcon from "./icons/CheckIcon"; interface Props { + required?: boolean; defaultOption?: string; options: string[]; onSelect: (option: string) => void; } -function Select3({ defaultOption, options, onSelect }: Props) { +function Select3({ required, defaultOption, options, onSelect }: Props) { const [showOptions, setShowOptions] = useState(false); const [selectedOption, setSelectedOption] = useState(); @@ -37,9 +38,10 @@ function Select3({ defaultOption, options, onSelect }: Props) { onClick={() => setShowOptions((prev) => !prev)} >
diff --git a/client/src/components/modals/AddManagerModal.tsx b/client/src/components/modals/AddManagerModal.tsx new file mode 100644 index 0000000..36aa933 --- /dev/null +++ b/client/src/components/modals/AddManagerModal.tsx @@ -0,0 +1,118 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { FormEvent, useState } from "react"; +import useModalStore from "../../stores/useModalStore"; +import Button from "../Button"; +import CloseIcon from "../icons/CloseIcon"; +import ModalTabButton from "../ModalTabButton"; +import Label from "../Label"; +import Input from "../Input"; +import Select3 from "../Select3"; +import IError from "../../types/IError"; +import api from "../../utils/api"; +import toast from "react-hot-toast"; +import useAuthStore from "../../stores/useAuthStore"; + +function AddManagerModal() { + const { setModal } = useModalStore(); + const { user } = useAuthStore(); + const [username, setUsername] = useState(""); + const [name, setName] = useState(""); + const [role, setRole] = useState(""); + const [loading, setLoading] = useState(false); + + async function handleSubmitAddManager(e: FormEvent) { + e.preventDefault(); + + setLoading(true); + await addManager(); + setLoading(false); + } + + async function addManager() { + try { + const result: any | IError = await api + .post("addManager", { + json: { + companyId: user?.companyId, + username, + name, + role: + (role === "Менеджер" && "manager") || + (role === "Администратор" && "admin"), + }, + }) + .json(); + + if ("error" in result) { + toast.error(result.error); + return; + } + + setModal(null); + toast.success("Пользователь успешно добавлен"); + + setTimeout(() => { + window.location.reload(); + }, 2000); + } catch (error) { + toast.error((error as Error).message); + } + } + + return ( +
+
+ Добавить сотрудника +
+
+
+

+ Информация для входа будет отправлена на почтовый адрес +

+
+
+
+
+
+
+
+
+ +
+
+
+ ); +} + +export default AddManagerModal; diff --git a/client/src/components/modals/CompanyModal.tsx b/client/src/components/modals/CompanyModal.tsx index e0608fa..c56303f 100644 --- a/client/src/components/modals/CompanyModal.tsx +++ b/client/src/components/modals/CompanyModal.tsx @@ -7,6 +7,7 @@ import useAuthStore from "../../stores/useAuthStore"; import MoreIcon from "../icons/MoreIcon"; import useStore from "../../stores/useStore"; import ModalTabButton from "../ModalTabButton"; +import AddManagerModal from "./AddManagerModal"; function CompanyModal() { const [selectedTab, setSelectedTab] = useState<"company" | "employees">( @@ -99,8 +100,7 @@ function CompanyModal() {

- {user?.name.split(" ")[0][0]} - {user?.name.split(" ")[1][0]} + {manager?.name.split(" ")[0][0]}

@@ -129,11 +129,13 @@ function CompanyModal() {
))}
-
- -
+ {user?.role === "admin" && ( +
+ +
+ )} )}
diff --git a/client/src/components/modals/SettingsModal.tsx b/client/src/components/modals/SettingsModal.tsx index 018cc45..c3792e8 100644 --- a/client/src/components/modals/SettingsModal.tsx +++ b/client/src/components/modals/SettingsModal.tsx @@ -64,7 +64,6 @@ function SettingsModal() {

{user?.name.split(" ")[0][0]} - {user?.name.split(" ")[1][0]}

diff --git a/client/src/main.tsx b/client/src/main.tsx index 910fe61..4fffc33 100644 --- a/client/src/main.tsx +++ b/client/src/main.tsx @@ -10,6 +10,8 @@ import RegistrationCompanyPage from "./pages/RegistrationCompanyPage.tsx"; import RegistrationManagerPage from "./pages/RegistrationManagerPage.tsx"; import AdminPage from "./pages/AdminPage.tsx"; import AdminCompanyPage from "./pages/AdminCompanyPage.tsx"; +import ResetPasswordPage from "./pages/ResetPasswordPage.tsx"; +import ResetPasswordConfirmPage from "./pages/ResetPasswordConfirmPage.tsx"; const router = createBrowserRouter([ { @@ -23,6 +25,14 @@ const router = createBrowserRouter([ path: "login", element: , }, + { + path: "reset", + element: , + }, + { + path: "resetConfirm", + element: , + }, { path: "registration", children: [ diff --git a/client/src/pages/DashboardPage.tsx b/client/src/pages/DashboardPage.tsx index 81bf1a0..4cb4ff6 100644 --- a/client/src/pages/DashboardPage.tsx +++ b/client/src/pages/DashboardPage.tsx @@ -18,6 +18,7 @@ import IError from "../types/IError"; import Schedule from "../components/Schedule"; import IScheduledSession from "../types/IScheduledSession"; import toast, { Toaster } from "react-hot-toast"; +import Calendar from "../components/Calendar"; function DashboardPage() { const { user } = useAuthStore(); @@ -228,7 +229,7 @@ function DashboardPage() {
- {/* */} + {/* */}
diff --git a/client/src/pages/LoginPage.tsx b/client/src/pages/LoginPage.tsx index 4747226..389e455 100644 --- a/client/src/pages/LoginPage.tsx +++ b/client/src/pages/LoginPage.tsx @@ -70,11 +70,6 @@ function LoginPage() { handleChange={(value) => setPassword(value)} />
- {/* */} + +
+ +
+
+ + + + ); +} + +export default ResetPasswordConfirmPage; diff --git a/client/src/pages/ResetPasswordPage.tsx b/client/src/pages/ResetPasswordPage.tsx new file mode 100644 index 0000000..cea9719 --- /dev/null +++ b/client/src/pages/ResetPasswordPage.tsx @@ -0,0 +1,118 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { useState } from "react"; +import Form from "../components/Form"; +import Label from "../components/Label"; +import Input from "../components/Input"; +import Button from "../components/Button"; +import { useNavigate } from "react-router-dom"; +import toast, { Toaster } from "react-hot-toast"; +import IError from "../types/IError"; +import api from "../utils/api"; + +function ResetPasswordPage() { + const [step, setStep] = useState(1); + const [username, setUsername] = useState(); + const [loading, setLoading] = useState(false); + const navigate = useNavigate(); + + async function resetPassword() { + try { + const result: any | IError = await api + .post("reset", { + credentials: "include", + json: { username }, + }) + .json(); + + if ("error" in result) { + toast.error(result.error); + return; + } + + setStep(2); + } catch (error) { + toast.error((error as Error).message); + } + } + + async function handleSubmitResetPassword() { + setLoading(true); + await resetPassword(); + setLoading(false); + } + + return ( +
+
+
+

Сброс пароля

+ {step === 1 && ( +
+
+

+ На указанный почтовый адрес будет отправлена информация для + восстановления пароля +

+
+
+
+
+ + +
+
+ )} + {step === 2 && ( +
+
+
+

+ Мы отправили вам письмо на адрес {username} с + инструкциями для восстановления пароля. +

+

+ Если вы не получите его в ближайшее время, проверьте папку + «Спам». +

+
+

+ Если же письма нигде не будет, пожалуйста, свяжитесь с нами по + адресу support@graff.tech +

+
+ +
+ )} +
+
+ + +
+ ); +} + +export default ResetPasswordPage; diff --git a/client/src/utils/api.ts b/client/src/utils/api.ts index 216c501..ab8add0 100644 --- a/client/src/utils/api.ts +++ b/client/src/utils/api.ts @@ -34,6 +34,8 @@ const api = ky.extend({ ...prev, user: { ...prev.user!, accessToken: result.accessToken }, })); + + window.location.reload(); } catch (error) { window.location.href = "/login"; } diff --git a/server/.env b/server/.env index b57883b..4dee30f 100644 --- a/server/.env +++ b/server/.env @@ -1,5 +1,5 @@ PORT=3001 MONGO_URI=mongodb://root:p62Z!ZatgY25@194.26.138.94:27017/ JWT_SECRET=yDcdWJgvlj2bJAuovYfQHTvtc3U9xQPw -JWT_ACCESS_EXP=10m +JWT_ACCESS_EXP=1h JWT_REFRESH_EXP=7d \ No newline at end of file diff --git a/server/package.json b/server/package.json index 556d7e1..78d9224 100644 --- a/server/package.json +++ b/server/package.json @@ -15,6 +15,7 @@ "date-fns": "^2.30.0", "dotenv": "^16.3.1", "express": "^4.18.2", + "generate-password": "^1.7.1", "jose": "^5.9.6", "jsonwebtoken": "^9.0.2", "mongoose": "^7.5.1", diff --git a/server/src/index.ts b/server/src/index.ts index ab5b960..7a67275 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -18,6 +18,9 @@ import adminCompaniesRoute from "./routes/admin/adminCompaniesRoute.js"; import adminBuildsRoute from "./routes/admin/adminBuildsRoute.js"; import adminUsersRoute from "./routes/admin/adminUsersRoute.js"; import changePasswordRoute from "./routes/changePassword.js"; +import resetRoute from "./routes/reset.js"; +import resetConfirmRoute from "./routes/resetConfirm.js"; +import addManagerRoute from "./routes/addManager.js"; await connectDB(); @@ -39,6 +42,8 @@ app.use("/login", loginRoute); app.use("/check", checkRoute); app.use("/refresh", refreshRoute); app.use("/register", registerRoute); +app.use("/reset", resetRoute); +app.use("/resetConfirm", resetConfirmRoute); app.use("/actions", actionsRouter); app.use("/builds", buildsRouter); app.use("/scheduled_sessions", scheduledSessionsRoute); @@ -49,6 +54,7 @@ app.use("/admin/users", adminUsersRoute); app.use("/companies", authMiddleware, companiesRouter); app.use("/users", authMiddleware, usersRouter); app.use("/changePassword", authMiddleware, changePasswordRoute); +app.use("/addManager", authMiddleware, addManagerRoute); app.listen(port, () => { console.log(`Server listening on port ${port}`); diff --git a/server/src/models/User.ts b/server/src/models/User.ts index f572fa3..8b2dcd3 100644 --- a/server/src/models/User.ts +++ b/server/src/models/User.ts @@ -11,6 +11,9 @@ const userSchema = new Schema( type: String, required: true, }, + resetCode: { + type: String, + }, companyId: { type: Schema.Types.ObjectId, ref: "Company", diff --git a/server/src/routes/addManager.ts b/server/src/routes/addManager.ts new file mode 100644 index 0000000..8531853 --- /dev/null +++ b/server/src/routes/addManager.ts @@ -0,0 +1,68 @@ +import { Router } from "express"; +import { generate } from "generate-password"; +import User from "../models/User.js"; +import { hashSync } from "bcrypt"; +import { createTransport } from "nodemailer"; + +const router = Router(); + +router.post("/", async (req, res) => { + const { companyId, username, name, role } = req.body; + + try { + const password = generate({ + length: 8, + numbers: true, + }); + + console.log("password", password); + + const passwordHash = hashSync(password, 12); + + const userExist = await User.exists({ username }); + + if (userExist) { + return res.json({ error: "User exist" }); + } + + await User.create({ + companyId, + username, + name, + role, + password: passwordHash, + }); + + let transporter = createTransport({ + host: "mail.netangels.ru", + port: 587, + secure: false, // true for 465, false for other ports + auth: { + user: "stream@graff.tech", // generated ethereal user + pass: "zLUbt8Io7dh2F9KT", // generated ethereal password + }, + }); + + // send mail with defined transport object + try { + await transporter.sendMail({ + from: "stream@graff.tech", // sender address + to: username, // list of receivers + subject: "Данные аккаунта - stream.graff.tech", // Subject line + html: `
+ Пароль для входа в аккаунт: ${password} +
`, + }); + } catch (error) { + console.log("error", (error as Error).message); + } + + return res.json({ ok: 1 }); + } catch (error) { + return res.json({ error: (error as Error).message }); + } +}); + +const addManagerRoute = router; + +export default addManagerRoute; diff --git a/server/src/routes/reset.ts b/server/src/routes/reset.ts new file mode 100644 index 0000000..194d2c1 --- /dev/null +++ b/server/src/routes/reset.ts @@ -0,0 +1,57 @@ +import bcrypt from "bcrypt"; +import { Router } from "express"; +import User from "../models/User.js"; +import { randomBytes } from "crypto"; +import { createTransport } from "nodemailer"; + +const router = Router(); + +router.post("/", async (req, res) => { + const { username } = req.body; + + try { + const user = await User.findOne({ username }); + + if (!user) { + return res.json({ error: "Username not found" }); + } + + const resetCode = randomBytes(32).toString("hex"); + + await User.findByIdAndUpdate(user._id, { resetCode }); + + const url = `https://crm.stream.graff.tech/resetConfirm?code=${resetCode}`; + + let transporter = createTransport({ + host: "mail.netangels.ru", + port: 587, + secure: false, // true for 465, false for other ports + auth: { + user: "stream@graff.tech", // generated ethereal user + pass: "zLUbt8Io7dh2F9KT", // generated ethereal password + }, + }); + + // send mail with defined transport object + try { + await transporter.sendMail({ + from: "stream@graff.tech", // sender address + to: username, // list of receivers + subject: "Сброс пароля - stream.graff.tech", // Subject line + html: `
+ Ссылка для сброса пароля: ${url} +
`, + }); + } catch (error) { + console.log("error", (error as Error).message); + } + + return res.json({ ok: 1 }); + } catch (error) { + return res.json({ error: (error as Error).message }); + } +}); + +const resetRoute = router; + +export default resetRoute; diff --git a/server/src/routes/resetConfirm.ts b/server/src/routes/resetConfirm.ts new file mode 100644 index 0000000..9c42d03 --- /dev/null +++ b/server/src/routes/resetConfirm.ts @@ -0,0 +1,34 @@ +import bcrypt from "bcrypt"; +import { Router } from "express"; +import User from "../models/User.js"; +import Token from "../models/Token.js"; + +const router = Router(); + +router.post("/", async (req, res) => { + const { resetCode, password } = req.body; + + try { + const passwordHash = bcrypt.hashSync(password, 12); + + const user = await User.findOneAndUpdate( + { resetCode }, + { password: passwordHash } + ); + + if (!user) { + return res.json({ error: "Reset code not valid" }); + } + + await User.findByIdAndUpdate(user._id, { $unset: { resetCode } }); + await Token.deleteMany({ userId: user._id }); + + return res.json({ ok: 1 }); + } catch (error) { + return res.json({ error: (error as Error).message }); + } +}); + +const resetConfirmRoute = router; + +export default resetConfirmRoute; diff --git a/server/yarn.lock b/server/yarn.lock index f5b7d7e..6c2f61f 100644 --- a/server/yarn.lock +++ b/server/yarn.lock @@ -653,6 +653,11 @@ gauge@^3.0.0: strip-ansi "^6.0.1" wide-align "^1.1.2" +generate-password@^1.7.1: + version "1.7.1" + resolved "https://registry.yarnpkg.com/generate-password/-/generate-password-1.7.1.tgz#b354255893da7755b033999821d3f1f1a97c1cb4" + integrity sha512-9bVYY+16m7W7GczRBDqXE+VVuCX+bWNrfYKC/2p2JkZukFb2sKxT6E3zZ3mJGz7GMe5iRK0A/WawSL3jQfJuNQ== + get-intrinsic@^1.0.2, get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.2.tgz#281b7622971123e1ef4b3c90fd7539306da93f3b"