upd
This commit is contained in:
+7
-5
@@ -13,6 +13,7 @@
|
||||
"@react-input/mask": "^1.2.5",
|
||||
"@uidotdev/usehooks": "^2.4.1",
|
||||
"date-fns": "^2.30.0",
|
||||
"jose": "^5.9.6",
|
||||
"ky": "^1.0.1",
|
||||
"lodash": "^4.17.21",
|
||||
"react": "^18.2.0",
|
||||
@@ -20,6 +21,7 @@
|
||||
"react-datepicker": "^4.20.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-google-recaptcha": "^3.1.0",
|
||||
"react-hot-toast": "^2.4.1",
|
||||
"react-router-dom": "^6.15.0",
|
||||
"react-transition-group": "^4.4.5",
|
||||
"zustand": "^4.4.1"
|
||||
@@ -34,14 +36,14 @@
|
||||
"@types/react-transition-group": "^4.4.7",
|
||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||
"@typescript-eslint/parser": "^6.0.0",
|
||||
"@vitejs/plugin-react-swc": "^3.3.2",
|
||||
"autoprefixer": "^10.4.15",
|
||||
"@vitejs/plugin-react-swc": "^3.7.1",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"eslint": "^8.45.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.3",
|
||||
"postcss": "^8.4.29",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"typescript": "^5.0.2",
|
||||
"vite": "^4.4.5"
|
||||
"tailwindcss": "^3.4.14",
|
||||
"typescript": "^5.6.3",
|
||||
"vite": "^5.4.10"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,40 +3,37 @@ import useAuthStore from "../stores/useAuthStore";
|
||||
import Button from "./Button";
|
||||
|
||||
function Header() {
|
||||
const [accessToken, setAccessToken] = useAuthStore((state) => [
|
||||
state.accessToken,
|
||||
state.setAccessToken,
|
||||
]);
|
||||
const { user, setUser } = useAuthStore();
|
||||
|
||||
function logout() {
|
||||
setAccessToken(null);
|
||||
setUser(null);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="border-b border-neutral-300">
|
||||
<div className="container mx-auto px-4 xl:max-w-screen-xl">
|
||||
<div className="container px-4 mx-auto xl:max-w-screen-xl">
|
||||
<div className="flex justify-between">
|
||||
<div className="">
|
||||
<a
|
||||
href="/"
|
||||
className="p-2 inline-block transition-colors hover:bg-neutral-200"
|
||||
className="inline-block p-2 transition-colors hover:bg-neutral-200"
|
||||
>
|
||||
<span className="text-2xl">Logo</span>
|
||||
</a>
|
||||
</div>
|
||||
<div className="">
|
||||
{accessToken ? (
|
||||
{user?.accessToken ? (
|
||||
<Button
|
||||
type="button"
|
||||
handleClick={logout}
|
||||
className="bg-transparent text-black hover:bg-neutral-200"
|
||||
className="text-black bg-transparent hover:bg-neutral-200"
|
||||
>
|
||||
Logout
|
||||
</Button>
|
||||
) : (
|
||||
<Link
|
||||
to="/login"
|
||||
className="p-3 inline-block transition-colors hover:bg-neutral-200"
|
||||
className="inline-block p-3 transition-colors hover:bg-neutral-200"
|
||||
>
|
||||
Login
|
||||
</Link>
|
||||
|
||||
@@ -10,18 +10,18 @@ import { useClickAway } from "@uidotdev/usehooks";
|
||||
|
||||
function Menu() {
|
||||
const [isShow, setIsShow] = useState<boolean>(false);
|
||||
const { user, setAccessToken } = useAuthStore();
|
||||
const { user, setUser } = useAuthStore();
|
||||
const ref = useClickAway<HTMLDivElement>(() => {
|
||||
setIsShow(false);
|
||||
});
|
||||
|
||||
function logout() {
|
||||
setAccessToken(null);
|
||||
setUser(null);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<span className="relative cursor-pointer z-20">
|
||||
<span className="relative z-20 cursor-pointer">
|
||||
<button
|
||||
onClick={() => setIsShow(true)}
|
||||
className={`p-3 transition-colors relative z-20 ${
|
||||
@@ -37,7 +37,7 @@ function Menu() {
|
||||
<Transition in={isShow} timeout={150} mountOnEnter unmountOnExit>
|
||||
{(state) => (
|
||||
<div className={`transition-opacity ${state}`}>
|
||||
<div className="absolute top-0 left-0 w-full h-full bg-black bg-opacity-10 z-10"></div>
|
||||
<div className="absolute top-0 left-0 z-10 w-full h-full bg-black bg-opacity-10"></div>
|
||||
<div ref={ref} className="absolute z-20 ml-2 mt-3.5">
|
||||
<div className="relative">
|
||||
<svg
|
||||
@@ -62,7 +62,7 @@ function Menu() {
|
||||
</div>
|
||||
<div className="space-y-1 text-center">
|
||||
<p className="text-sm">{user?.name}</p>
|
||||
<p className="text-[#77828C] text-xs">{user?.username}</p>
|
||||
<p className="text-[#77828C] text-xs">{user?.role}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-b border-[#DAE0E5] py-3 space-y-2">
|
||||
|
||||
@@ -28,6 +28,7 @@ function Schedule({ selectedDay, slots, events }: Props) {
|
||||
const [email, setEmail] = useState<string>("");
|
||||
const [name, setName] = useState<string>("");
|
||||
const [phone, setPhone] = useState<string>("");
|
||||
const [dateForInstantStart, setDateForInstantStart] = useState<Date>();
|
||||
|
||||
function handleChangeSlot(slot: number) {
|
||||
setSlot(slot);
|
||||
@@ -39,6 +40,7 @@ function Schedule({ selectedDay, slots, events }: Props) {
|
||||
|
||||
function handleChangeStartAt(startAt: Date) {
|
||||
setStartAt(setDate(startAt, getDate(selectedDay)));
|
||||
setDateForInstantStart(undefined);
|
||||
}
|
||||
|
||||
function handleChangeDuration(duration: number) {
|
||||
@@ -46,6 +48,10 @@ function Schedule({ selectedDay, slots, events }: Props) {
|
||||
}
|
||||
|
||||
async function handleClickSave() {
|
||||
console.log("slot", slot);
|
||||
console.log("startAt", startAt);
|
||||
console.log("duration", duration);
|
||||
|
||||
if (!slot || !startAt || !duration) return;
|
||||
|
||||
await addSchesuledSession();
|
||||
@@ -70,7 +76,7 @@ function Schedule({ selectedDay, slots, events }: Props) {
|
||||
// setIsLoading(true);
|
||||
|
||||
try {
|
||||
await api
|
||||
const result: any = await api
|
||||
.post(`scheduled_sessions`, {
|
||||
json: {
|
||||
companyId: company!.id,
|
||||
@@ -86,8 +92,12 @@ function Schedule({ selectedDay, slots, events }: Props) {
|
||||
},
|
||||
})
|
||||
.json();
|
||||
|
||||
if (!dateForInstantStart) return;
|
||||
|
||||
window.open(`${result.url}?admin=true`);
|
||||
} catch (error) {
|
||||
alert((error as Error).message);
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
// setIsLoading(false);
|
||||
@@ -106,6 +116,29 @@ function Schedule({ selectedDay, slots, events }: Props) {
|
||||
console.log("events", events);
|
||||
}, [events]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!dateForInstantStart) return;
|
||||
|
||||
setSlot(1);
|
||||
setStartAt(dateForInstantStart);
|
||||
setDuration(30);
|
||||
setDraftMode(true);
|
||||
}, [dateForInstantStart]);
|
||||
|
||||
useEffect(() => {
|
||||
function handleEscKey(e: KeyboardEvent) {
|
||||
if (!draftMode && e.key !== "Escape") return;
|
||||
|
||||
setDraftMode(false);
|
||||
}
|
||||
|
||||
document.addEventListener("keyup", handleEscKey, false);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("keyup", handleEscKey, false);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="relative h-screen overflow-y-auto bg-[#F2F2F2] text-sm">
|
||||
<div className="fixed z-10 flex">
|
||||
@@ -157,10 +190,14 @@ function Schedule({ selectedDay, slots, events }: Props) {
|
||||
</div>
|
||||
|
||||
{draftMode && startAt && (
|
||||
<div className="fixed top-0 right-0 flex flex-col justify-between h-screen overflow-y-auto bg-white shadow w-[320px]">
|
||||
<div className="fixed top-0 right-0 flex flex-col justify-between h-screen overflow-y-auto bg-white shadow w-[320px] z-20">
|
||||
<div className="space-y-4">
|
||||
<div className="p-2 pl-4 flex items-center justify-between border-b border-[#DAE0E5]">
|
||||
<p className="font-semibold">Запланировать демонстрацию</p>
|
||||
<p className="font-semibold">
|
||||
{dateForInstantStart
|
||||
? "Начать демонстрацию"
|
||||
: "Запланировать демонстрацию"}
|
||||
</p>
|
||||
<Button
|
||||
color="tertiary"
|
||||
icon={<CloseIcon />}
|
||||
@@ -199,45 +236,47 @@ function Schedule({ selectedDay, slots, events }: Props) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-4 space-y-2">
|
||||
<p className="font-semibold">Клиент</p>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-1">
|
||||
<Label value="Email" />
|
||||
<Input
|
||||
type="email"
|
||||
className="w-full h-10"
|
||||
value={email}
|
||||
handleChange={(value) => setEmail(value)}
|
||||
/>
|
||||
<span className="text-[#77828C] text-xs">
|
||||
На указанный почтовый адрес придет необходимая для
|
||||
подключения информация
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label value="Телефон" />
|
||||
<Input
|
||||
type="tel"
|
||||
className="w-full h-10"
|
||||
value={phone}
|
||||
handleChange={(value) => setPhone(value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label value="Имя" />
|
||||
<Input
|
||||
className="w-full h-10"
|
||||
value={name}
|
||||
handleChange={(value) => setName(value)}
|
||||
/>
|
||||
{!dateForInstantStart && (
|
||||
<div className="px-4 space-y-2">
|
||||
<p className="font-semibold">Клиент</p>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-1">
|
||||
<Label value="Email" />
|
||||
<Input
|
||||
type="email"
|
||||
className="w-full h-10"
|
||||
value={email}
|
||||
handleChange={(value) => setEmail(value)}
|
||||
/>
|
||||
<span className="text-[#77828C] text-xs">
|
||||
На указанный почтовый адрес придет необходимая для
|
||||
подключения информация
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label value="Телефон" />
|
||||
<Input
|
||||
type="tel"
|
||||
className="w-full h-10"
|
||||
value={phone}
|
||||
handleChange={(value) => setPhone(value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label value="Имя" />
|
||||
<Input
|
||||
className="w-full h-10"
|
||||
value={name}
|
||||
handleChange={(value) => setName(value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-2 p-4">
|
||||
<Button type="submit" handleClick={handleClickSave}>
|
||||
Запланировать
|
||||
{dateForInstantStart ? "Начать" : "Запланировать"}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
@@ -249,6 +288,15 @@ function Schedule({ selectedDay, slots, events }: Props) {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="fixed top-28 right-[336px] z-10">
|
||||
<Button
|
||||
disabled={draftMode}
|
||||
handleClick={() => setDateForInstantStart(new Date())}
|
||||
>
|
||||
Начать демонстрацию с мгновенным запуском
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -191,7 +191,9 @@ function DashboardPage() {
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center px-4 py-2 h-12 border-r border-b border-[#DAE0E5]">
|
||||
<p className="text-sm font-semibold">Расписание</p>
|
||||
<div className="flex items-center gap-4">
|
||||
<p className="text-sm font-semibold">Расписание</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<p className="text-sm font-semibold">
|
||||
{format(selectedDay, "PPPP", { locale: ru })}
|
||||
|
||||
@@ -5,11 +5,11 @@ import { useEffect } from "react";
|
||||
import useAuthStore from "../stores/useAuthStore";
|
||||
|
||||
function HomePage() {
|
||||
const accessToken = useAuthStore().accessToken;
|
||||
const { user } = useAuthStore();
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
if (accessToken) {
|
||||
if (user?.accessToken) {
|
||||
navigate("/dashboard");
|
||||
} else {
|
||||
navigate("/login");
|
||||
@@ -17,7 +17,7 @@ function HomePage() {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="p-8 flex flex-col gap-4 w-fit">
|
||||
<div className="flex flex-col gap-4 p-8 w-fit">
|
||||
<p>HomePage</p>
|
||||
<Link to="/login">
|
||||
<Button type="button">Login</Button>
|
||||
|
||||
@@ -1,62 +1,61 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { useEffect, useState } from "react";
|
||||
import ky from "ky";
|
||||
// import ReCAPTCHA from "react-google-recaptcha";
|
||||
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 { Link, useNavigate } from "react-router-dom";
|
||||
import useAuthStore from "../stores/useAuthStore";
|
||||
import toast from "react-hot-toast";
|
||||
import IError from "../types/IError";
|
||||
import IUser from "../types/IUser";
|
||||
import api from "../utils/api";
|
||||
|
||||
function LoginPage() {
|
||||
const [username, setUsername] = useState<string>();
|
||||
const [password, setPassword] = useState<string>();
|
||||
// const recaptchaRef = useRef(null);
|
||||
const [accessToken, setAccessToken, setUser] = useAuthStore((state) => [
|
||||
state.accessToken,
|
||||
state.setAccessToken,
|
||||
state.setUser,
|
||||
]);
|
||||
const [isLoading, setisLoading] = useState<boolean>(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { setUser } = useAuthStore();
|
||||
const navigate = useNavigate();
|
||||
|
||||
async function login() {
|
||||
setisLoading(true);
|
||||
setLoading(true);
|
||||
|
||||
const result: any = await ky
|
||||
.post(import.meta.env.VITE_API_URL + "/login", {
|
||||
json: {
|
||||
username,
|
||||
password,
|
||||
},
|
||||
})
|
||||
.json();
|
||||
try {
|
||||
const result: IUser | IError = await api
|
||||
.post("login", {
|
||||
credentials: "include",
|
||||
json: { username, password },
|
||||
})
|
||||
.json();
|
||||
|
||||
if (result.error) {
|
||||
alert(result.error);
|
||||
return;
|
||||
if ("error" in result) {
|
||||
setLoading(false);
|
||||
toast.error(`Error: ${result.error}`);
|
||||
return;
|
||||
}
|
||||
|
||||
setUser({
|
||||
id: result.id,
|
||||
username: "username",
|
||||
accessToken: result.accessToken,
|
||||
companyId: result.companyId,
|
||||
name: result.name,
|
||||
role: result.role,
|
||||
buildIds: result.buildIds,
|
||||
});
|
||||
|
||||
navigate("/dashboard");
|
||||
} catch (error) {
|
||||
setLoading(false);
|
||||
toast.error((error as Error).message);
|
||||
}
|
||||
|
||||
setAccessToken(result.accessToken);
|
||||
setUser(result.user);
|
||||
navigate("/dashboard");
|
||||
|
||||
setTimeout(() => {
|
||||
setisLoading(false);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (accessToken) {
|
||||
navigate("/dashboard");
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col justify-center items-center p-8 flex-1 ">
|
||||
<div className=" bg-white rounded-lg shadow-md overflow-hidden">
|
||||
<div className="flex flex-col items-center justify-center flex-1 min-h-screen p-8 ">
|
||||
<div className="overflow-hidden bg-white rounded-lg shadow-md ">
|
||||
<div className="flex flex-col gap-6 p-12">
|
||||
<p className="text-2xl font-semibold">Вход</p>
|
||||
<Form handleSubmit={login}>
|
||||
@@ -88,12 +87,12 @@ function LoginPage() {
|
||||
type="submit"
|
||||
size="medium"
|
||||
className="mt-10"
|
||||
loading={isLoading}
|
||||
loading={loading}
|
||||
>
|
||||
Войти
|
||||
</Button>
|
||||
</Form>
|
||||
<div className="self-center flex gap-6">
|
||||
<div className="flex self-center gap-6">
|
||||
<Link to="/registration" className="text-xs text-[#49A1F5]">
|
||||
Нет аккаунта?
|
||||
</Link>
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
|
||||
import IUser from "../types/IUser";
|
||||
import { persist } from "zustand/middleware";
|
||||
|
||||
interface AuthState {
|
||||
accessToken: string | null;
|
||||
setAccessToken: (accessToken: string | null) => void;
|
||||
user: IUser | null;
|
||||
setUser: (user: IUser | null) => void;
|
||||
}
|
||||
@@ -12,8 +11,6 @@ interface AuthState {
|
||||
const useAuthStore = create<AuthState>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
accessToken: null,
|
||||
setAccessToken: (accessToken) => set({ accessToken }),
|
||||
user: null,
|
||||
setUser: (user) => set({ user }),
|
||||
}),
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
interface IUser {
|
||||
id: string;
|
||||
username: string;
|
||||
accessToken: string;
|
||||
companyId: string;
|
||||
avatar?: string;
|
||||
name: string;
|
||||
username: string;
|
||||
role: string;
|
||||
buildIds?: string[];
|
||||
}
|
||||
|
||||
@@ -1,10 +1,33 @@
|
||||
/* eslint-disable no-empty */
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import { Navigate, Outlet } from "react-router-dom";
|
||||
import api from "./api";
|
||||
import { useEffect, useRef } from "react";
|
||||
import useAuthStore from "../stores/useAuthStore";
|
||||
|
||||
function ProtectedRoute() {
|
||||
const accessToken = useAuthStore((state) => state.accessToken);
|
||||
const timeout = useRef<NodeJS.Timeout>();
|
||||
const { user } = useAuthStore();
|
||||
|
||||
return accessToken ? <Outlet /> : <Navigate to="/login" />;
|
||||
async function checkAuth() {
|
||||
try {
|
||||
await api.get("check").json();
|
||||
} catch (error) {}
|
||||
|
||||
timeout.current = setTimeout(async () => {
|
||||
await checkAuth();
|
||||
}, 10000);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
checkAuth();
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeout.current);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return user?.accessToken ? <Outlet /> : <Navigate to={"/login"} />;
|
||||
}
|
||||
|
||||
export default ProtectedRoute;
|
||||
|
||||
+34
-2
@@ -1,16 +1,48 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import ky from "ky";
|
||||
import useAuthStore from "../stores/useAuthStore";
|
||||
import IError from "../types/IError";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
const api = ky.extend({
|
||||
prefixUrl: `${import.meta.env.VITE_API_URL}`,
|
||||
prefixUrl: import.meta.env.VITE_API_URL,
|
||||
hooks: {
|
||||
beforeRequest: [
|
||||
(request) => {
|
||||
const accessToken = useAuthStore.getState().accessToken;
|
||||
const accessToken = useAuthStore.getState().user?.accessToken;
|
||||
|
||||
request.headers.set("Authorization", `Bearer ${accessToken}`);
|
||||
},
|
||||
],
|
||||
afterResponse: [
|
||||
async (_, __, response) => {
|
||||
if (response.status === 200) return;
|
||||
|
||||
if (response.status === 401) {
|
||||
try {
|
||||
const result: { accessToken: string } | IError = await ky
|
||||
.post(`${import.meta.env.VITE_API_URL}/refresh`, {
|
||||
credentials: "include",
|
||||
})
|
||||
.json();
|
||||
|
||||
if ("error" in result) {
|
||||
window.location.href = "/login";
|
||||
return;
|
||||
}
|
||||
|
||||
useAuthStore.setState((prev) => ({
|
||||
...prev,
|
||||
user: { ...prev.user!, accessToken: result.accessToken },
|
||||
}));
|
||||
} catch (error) {
|
||||
window.location.href = "/login";
|
||||
}
|
||||
} else {
|
||||
toast.error(response.statusText);
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
+730
-357
File diff suppressed because it is too large
Load Diff
+3
-1
@@ -1,3 +1,5 @@
|
||||
PORT=3001
|
||||
MONGO_URI=mongodb://root:p62Z!ZatgY25@194.26.138.94:27017/
|
||||
JWT_SECRET=yDcdWJgvlj2bJAuovYfQHTvtc3U9xQPw
|
||||
JWT_SECRET=yDcdWJgvlj2bJAuovYfQHTvtc3U9xQPw
|
||||
JWT_ACCESS_EXP=10m
|
||||
JWT_REFRESH_EXP=7d
|
||||
Vendored
+18
@@ -0,0 +1,18 @@
|
||||
// interface ProcessEnv {
|
||||
// readonly PORT: number;
|
||||
// readonly MONGO_URI: string;
|
||||
// readonly JWT_SECRET: string;
|
||||
// readonly JWT_ACCESS_EXP: string;
|
||||
// readonly JWT_REFRESH_EXP: string;
|
||||
// }
|
||||
|
||||
declare namespace NodeJS {
|
||||
interface ProcessEnv {
|
||||
readonly PORT: number;
|
||||
readonly MONGO_URI: string;
|
||||
readonly JWT_SECRET: string;
|
||||
readonly JWT_ACCESS_EXP: string;
|
||||
readonly JWT_REFRESH_EXP: string;
|
||||
// add more environment variables and their types here
|
||||
}
|
||||
}
|
||||
@@ -10,10 +10,12 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"bcrypt": "^5.1.1",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"cors": "^2.8.5",
|
||||
"date-fns": "^2.30.0",
|
||||
"dotenv": "^16.3.1",
|
||||
"express": "^4.18.2",
|
||||
"jose": "^5.9.6",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"mongoose": "^7.5.1",
|
||||
"nodemailer": "^6.9.14",
|
||||
@@ -21,6 +23,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bcrypt": "^5.0.0",
|
||||
"@types/cookie-parser": "^1.4.7",
|
||||
"@types/cors": "^2.8.14",
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/jsonwebtoken": "^9.0.2",
|
||||
|
||||
+19
-6
@@ -2,11 +2,14 @@ import "dotenv/config";
|
||||
import express, { json } from "express";
|
||||
import connectDB from "./config/db.js";
|
||||
import cors from "cors";
|
||||
import loginRouter from "./routes/login.js";
|
||||
import registrationRouter from "./routes/registration.js";
|
||||
import cookieParser from "cookie-parser";
|
||||
import authMiddleware from "./middlewares/auth.js";
|
||||
import companiesRouter from "./routes/companies.js";
|
||||
import registerRoute from "./routes/register.js";
|
||||
import refreshRoute from "./routes/refresh.js";
|
||||
import checkRoute from "./routes/check.js";
|
||||
import loginRoute from "./routes/login.js";
|
||||
import usersRouter from "./routes/users.js";
|
||||
import companiesRouter from "./routes/companies.js";
|
||||
import buildsRouter from "./routes/builds.js";
|
||||
import actionsRouter from "./routes/actions.js";
|
||||
import schedulesRouter from "./routes/schedules.js";
|
||||
@@ -20,11 +23,21 @@ await connectDB();
|
||||
const app = express();
|
||||
const port = process.env.PORT || 3000;
|
||||
|
||||
app.use(
|
||||
cors({
|
||||
origin: (origin, cb) => {
|
||||
cb(null, origin);
|
||||
},
|
||||
credentials: true,
|
||||
})
|
||||
);
|
||||
app.use(json());
|
||||
app.use(cors({ origin: "*" }));
|
||||
app.use(cookieParser());
|
||||
|
||||
app.use("/login", loginRouter);
|
||||
app.use("/registration", registrationRouter);
|
||||
app.use("/login", loginRoute);
|
||||
app.use("/check", checkRoute);
|
||||
app.use("/refresh", refreshRoute);
|
||||
app.use("/register", registerRoute);
|
||||
app.use("/actions", actionsRouter);
|
||||
app.use("/builds", buildsRouter);
|
||||
app.use("/scheduled_sessions", scheduledSessionsRoute);
|
||||
|
||||
@@ -1,30 +1,34 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import jwt, { Secret } from "jsonwebtoken";
|
||||
import Token from "../models/Token.js";
|
||||
import User from "../models/User.js";
|
||||
import { jwtVerify } from "jose";
|
||||
import { createSecretKey } from "crypto";
|
||||
|
||||
async function authMiddleware(req: Request, res: Response, next: NextFunction) {
|
||||
if (!req.headers.authorization || !req.headers.authorization.split(" ")[1]) {
|
||||
return res.status(401).json({ error: 10 });
|
||||
return res.status(401).json({ error: 1 });
|
||||
}
|
||||
|
||||
const accessToken = req.headers.authorization.split(" ")[1];
|
||||
|
||||
try {
|
||||
jwt.verify(accessToken, process.env.JWT_SECRET as Secret);
|
||||
await jwtVerify(
|
||||
accessToken,
|
||||
createSecretKey(process.env.JWT_SECRET!, "utf8")
|
||||
);
|
||||
} catch (error) {
|
||||
return res.status(401).json({ erorr: 20 });
|
||||
return res.status(401).json({ error: 2 });
|
||||
}
|
||||
|
||||
const foundAccessToken = await Token.findOne({ accessToken });
|
||||
|
||||
if (!foundAccessToken) {
|
||||
return res.status(401).json({ error: 30 });
|
||||
return res.status(401).json({ error: 3 });
|
||||
}
|
||||
|
||||
const user = await User.findById(foundAccessToken.userId);
|
||||
|
||||
res.locals = { accessToken, user };
|
||||
res.locals = { user, accessToken };
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
@@ -11,6 +11,10 @@ const tokenSchema = new Schema(
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
refreshToken: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
|
||||
@@ -11,6 +11,11 @@ const userSchema = new Schema(
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
companyId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "Company",
|
||||
required: true,
|
||||
},
|
||||
role: {
|
||||
type: String,
|
||||
required: true,
|
||||
@@ -19,14 +24,6 @@ const userSchema = new Schema(
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
avatar: {
|
||||
type: String,
|
||||
},
|
||||
companyId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "Company",
|
||||
required: true,
|
||||
},
|
||||
buildIds: {
|
||||
type: [Schema.Types.ObjectId],
|
||||
ref: "Build",
|
||||
@@ -39,6 +36,12 @@ const userSchema = new Schema(
|
||||
}
|
||||
);
|
||||
|
||||
userSchema.virtual("tokens", {
|
||||
ref: "Token",
|
||||
foreignField: "userId",
|
||||
localField: "_id",
|
||||
});
|
||||
|
||||
const User = model("User", userSchema);
|
||||
|
||||
export default User;
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Router } from "express";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get("/", (req, res) => {
|
||||
res.json({ ok: 1 });
|
||||
});
|
||||
|
||||
const checkRoute = router;
|
||||
|
||||
export default checkRoute;
|
||||
+25
-19
@@ -1,43 +1,49 @@
|
||||
import { Router } from "express";
|
||||
import jwt, { Secret } from "jsonwebtoken";
|
||||
import bcrypt from "bcrypt";
|
||||
import User from "../models/User.js";
|
||||
import Token from "../models/Token.js";
|
||||
import { decodeJwt, SignJWT } from "jose";
|
||||
import { createSecretKey } from "crypto";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.post("/", async (req, res) => {
|
||||
let { username, password } = req.body;
|
||||
const { username, password } = req.body;
|
||||
|
||||
if (!username || !password) {
|
||||
return res.json({
|
||||
error: "You must pass the 'username' and 'password' parameters",
|
||||
});
|
||||
return res.json({ error: 1 });
|
||||
}
|
||||
|
||||
username = username.toLowerCase();
|
||||
|
||||
const user = await User.findOne({ username });
|
||||
const user = await User.findOne({ username }).lean();
|
||||
|
||||
if (!user) {
|
||||
return res.json({ error: "A user with this name was not found" });
|
||||
return res.json({ error: 2 });
|
||||
}
|
||||
|
||||
if (!bcrypt.compareSync(password, user.password)) {
|
||||
return res.json({ error: "Invalid username or password" });
|
||||
if (!bcrypt.compareSync(password, user.password!)) {
|
||||
return res.json({ error: 3 });
|
||||
}
|
||||
|
||||
const accessToken = jwt.sign({ username }, process.env.JWT_SECRET as Secret, {
|
||||
expiresIn: "365d",
|
||||
});
|
||||
const accessToken = await new SignJWT({ username })
|
||||
.setProtectedHeader({ alg: "HS256" })
|
||||
.setExpirationTime(process.env.JWT_ACCESS_EXP)
|
||||
.sign(createSecretKey(process.env.JWT_SECRET, "utf8"));
|
||||
|
||||
const userId = user.id;
|
||||
const refreshToken = await new SignJWT({ username })
|
||||
.setProtectedHeader({ alg: "HS256" })
|
||||
.setExpirationTime(process.env.JWT_REFRESH_EXP)
|
||||
.sign(createSecretKey(process.env.JWT_SECRET, "utf8"));
|
||||
|
||||
await Token.create({ userId, accessToken });
|
||||
await Token.create({ userId: user._id, accessToken, refreshToken });
|
||||
|
||||
res.json({ accessToken, user });
|
||||
res
|
||||
.cookie("refreshToken", refreshToken, {
|
||||
httpOnly: true,
|
||||
expires: new Date(decodeJwt(refreshToken).exp! * 1000),
|
||||
})
|
||||
.json({ accessToken, refreshToken, ...user });
|
||||
});
|
||||
|
||||
const loginRouter = router;
|
||||
const loginRoute = router;
|
||||
|
||||
export default loginRouter;
|
||||
export default loginRoute;
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import { createSecretKey } from "crypto";
|
||||
import { Router } from "express";
|
||||
import { jwtVerify, SignJWT } from "jose";
|
||||
import Token from "../models/Token.js";
|
||||
import User from "../models/User.js";
|
||||
import IToken from "../types/IToken.js";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.post("/", async (req, res) => {
|
||||
let _username;
|
||||
|
||||
if (req.cookies.refreshToken) {
|
||||
try {
|
||||
const {
|
||||
payload: { username },
|
||||
} = await jwtVerify(
|
||||
req.cookies.refreshToken,
|
||||
createSecretKey(process.env.JWT_SECRET!, "utf8")
|
||||
);
|
||||
|
||||
_username = username;
|
||||
} catch (error) {
|
||||
return res.json({
|
||||
error: `refreshToken jwtVerify - ${(error as Error).message}`,
|
||||
});
|
||||
}
|
||||
|
||||
const userWithTokens = await User.findOne({ username: _username }).populate(
|
||||
{
|
||||
path: "tokens",
|
||||
}
|
||||
);
|
||||
|
||||
if (!userWithTokens) {
|
||||
return res.json({ error: "userWithTokens is null" });
|
||||
}
|
||||
|
||||
if (!("tokens" in userWithTokens)) {
|
||||
return res.json({ error: "tokens not found in userWithTokens" });
|
||||
}
|
||||
|
||||
const foundRefreshToken = userWithTokens.tokens as IToken[];
|
||||
|
||||
if (
|
||||
!foundRefreshToken.find(
|
||||
(token) => token.refreshToken === req.cookies.refreshToken
|
||||
)?.refreshToken
|
||||
) {
|
||||
console.log("refreshToken not found in DB");
|
||||
return res.json({ error: "refreshToken not found in DB" });
|
||||
}
|
||||
|
||||
const accessToken = await new SignJWT({ username: _username })
|
||||
.setProtectedHeader({ alg: "HS256" })
|
||||
.setExpirationTime(process.env.JWT_ACCESS_EXP)
|
||||
.sign(createSecretKey(process.env.JWT_SECRET!, "utf8"));
|
||||
|
||||
await Token.findOneAndUpdate(
|
||||
{ refreshToken: req.cookies.refreshToken },
|
||||
{ accessToken }
|
||||
);
|
||||
|
||||
return res.json({ accessToken });
|
||||
}
|
||||
|
||||
// if (req.body.refreshToken) {
|
||||
// try {
|
||||
// jwtVerify(
|
||||
// req.body.refreshToken,
|
||||
// createSecretKey(process.env.JWT_SECRET!, "utf8")
|
||||
// );
|
||||
// } catch (error) {
|
||||
// return res.json({ error: `refreshToken - ${(error as Error).message}` });
|
||||
// }
|
||||
// }
|
||||
|
||||
return res.json({ error: "refreshToken not found" });
|
||||
});
|
||||
|
||||
const refreshRoute = router;
|
||||
|
||||
export default refreshRoute;
|
||||
@@ -0,0 +1,22 @@
|
||||
import bcrypt from "bcrypt";
|
||||
import { Router } from "express";
|
||||
import User from "../models/User.js";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.post("/", async (req, res) => {
|
||||
const { username, password } = req.body;
|
||||
|
||||
try {
|
||||
const passwordHash = bcrypt.hashSync(password, 12);
|
||||
const result = await User.create({ username, password: passwordHash });
|
||||
|
||||
return res.json(result);
|
||||
} catch (error) {
|
||||
return res.json({ error: (error as Error).message });
|
||||
}
|
||||
});
|
||||
|
||||
const registerRoute = router;
|
||||
|
||||
export default registerRoute;
|
||||
@@ -1,40 +0,0 @@
|
||||
import { Router } from "express";
|
||||
import jwt, { Secret } from "jsonwebtoken";
|
||||
import bcrypt from "bcrypt";
|
||||
import User from "../models/User.js";
|
||||
import Token from "../models/Token.js";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.post("/", async (req, res) => {
|
||||
const { username, password } = req.body;
|
||||
|
||||
if (!username || !password) {
|
||||
return res.json({ error: 1 });
|
||||
}
|
||||
|
||||
if (await User.exists({ username })) {
|
||||
return res.json({ error: 2 });
|
||||
}
|
||||
|
||||
const passwordHash = bcrypt.hashSync(password, 12);
|
||||
|
||||
if (!passwordHash) {
|
||||
return res.json({ error: 3 });
|
||||
}
|
||||
|
||||
const accessToken = jwt.sign({ username }, process.env.JWT_SECRET as Secret, {
|
||||
expiresIn: "365d",
|
||||
});
|
||||
|
||||
const user = await User.create({ username, password: passwordHash });
|
||||
const userId = user.id;
|
||||
|
||||
await Token.create({ userId, accessToken });
|
||||
|
||||
res.json({ accessToken, user });
|
||||
});
|
||||
|
||||
const registerRouter = router;
|
||||
|
||||
export default registerRouter;
|
||||
@@ -77,6 +77,8 @@ router.get("/:buildId", async (req, res) => {
|
||||
router.post("/", async (req, res) => {
|
||||
const { companyId, buildId, slot, startAt, client, duration } = req.body;
|
||||
|
||||
console.log("client", client);
|
||||
|
||||
if (!companyId || !buildId || !startAt || !slot) {
|
||||
return res.json({
|
||||
status: "error",
|
||||
@@ -148,109 +150,47 @@ router.post("/", async (req, res) => {
|
||||
startAt: startAtISO,
|
||||
duration,
|
||||
endAt: endAtISO,
|
||||
client,
|
||||
});
|
||||
|
||||
const url = `https://stream.graff.tech/scheduled/${scheduledSession.id}`;
|
||||
|
||||
// <-- Send an mail
|
||||
|
||||
if (client?.email) {
|
||||
console.log("client?.email", client?.email);
|
||||
// create reusable transporter object using the default SMTP transport
|
||||
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: client.email, // list of receivers
|
||||
subject: "Приглашение на демонстрацию - stream.graff.tech", // Subject line
|
||||
html: `<div>
|
||||
Ссылка для подключения к демонстрации: <a href="${url}" target="_blank">${url}</a>
|
||||
</div>`,
|
||||
});
|
||||
} catch (error) {
|
||||
console.log("error", (error as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
return res.json({
|
||||
status: "success",
|
||||
scheduledSessionId: scheduledSession.id,
|
||||
url: `https://stream.graff.tech/scheduled/${scheduledSession.id}`,
|
||||
});
|
||||
}
|
||||
|
||||
const schedule = await Schedule.findOne({
|
||||
buildId,
|
||||
startDate: { $lte: startAtISO },
|
||||
// endDate: { $gte: startAtISO },
|
||||
});
|
||||
|
||||
if (!schedule) {
|
||||
return res.json({
|
||||
status: "error",
|
||||
message: "No matching schedule found", // Подходящего расписания не найдено
|
||||
});
|
||||
}
|
||||
|
||||
const scheduledSessions = await ScheduledSession.find({
|
||||
buildId,
|
||||
startAt: {
|
||||
$gte: startOfDay(startAtISO),
|
||||
$lte: endOfDay(startAtISO),
|
||||
},
|
||||
});
|
||||
|
||||
const endAtISO = addMinutes(startAtISO, schedule.sessionDuration);
|
||||
|
||||
if (scheduledSessions.length) {
|
||||
const overlappingSessions = [];
|
||||
|
||||
for (const session of scheduledSessions) {
|
||||
if (
|
||||
areIntervalsOverlapping(
|
||||
{
|
||||
start: session.startAt,
|
||||
end: addMinutes(session.endAt, schedule.sessionBreak),
|
||||
},
|
||||
{ start: startAtISO, end: endAtISO }
|
||||
)
|
||||
) {
|
||||
overlappingSessions.push(session);
|
||||
}
|
||||
}
|
||||
|
||||
if (overlappingSessions.length >= build.sessionLimit) {
|
||||
return res.json({
|
||||
status: "error",
|
||||
message:
|
||||
"It is not possible to create a session because it overlaps with the time of another session", //Невозможно создать сеанс, поскольку он перекрывается со временем другого сеанса.
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const scheduledSession = await ScheduledSession.create({
|
||||
buildId,
|
||||
client,
|
||||
startAt: startAtISO,
|
||||
endAt: endAtISO,
|
||||
});
|
||||
|
||||
const url = `https://stream.graff.tech/scheduled/${scheduledSession.id}`;
|
||||
|
||||
// <-- Send an mail
|
||||
|
||||
if (client?.email) {
|
||||
// create reusable transporter object using the default SMTP transport
|
||||
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: client.email, // list of receivers
|
||||
subject: "Приглашение на демонстрацию - stream.graff.tech", // Subject line
|
||||
html: `<div>
|
||||
Ссылка для подключения к демонстрации: <a href="${url}" target="_blank">${url}</a>
|
||||
</div>`,
|
||||
});
|
||||
} catch (error) {
|
||||
console.log("error", (error as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
// Send an mail -->
|
||||
|
||||
res.json({
|
||||
status: "success",
|
||||
scheduledSessionId: scheduledSession.id,
|
||||
url,
|
||||
});
|
||||
});
|
||||
|
||||
router.put("/:id", async (req, res) => {
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
interface IToken {
|
||||
refreshToken: string;
|
||||
}
|
||||
|
||||
export default IToken;
|
||||
@@ -0,0 +1,8 @@
|
||||
interface IUser {
|
||||
id: string;
|
||||
accessToken: string;
|
||||
username: string;
|
||||
password?: string;
|
||||
}
|
||||
|
||||
export default IUser;
|
||||
Vendored
-12
@@ -1,12 +0,0 @@
|
||||
declare global {
|
||||
namespace NodeJS {
|
||||
interface ProcessEnv {
|
||||
PORT?: string;
|
||||
NODE_ENV: "development" | "production";
|
||||
MONGO_URI: string;
|
||||
JWT_SECRET: string;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
@@ -55,7 +55,7 @@
|
||||
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
|
||||
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
|
||||
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
|
||||
"outDir": "./dist" /* Specify an output folder for all emitted files. */,
|
||||
"outDir": "dist" /* Specify an output folder for all emitted files. */,
|
||||
// "removeComments": true, /* Disable emitting comments. */
|
||||
// "noEmit": true, /* Disable emitting files from a compilation. */
|
||||
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
|
||||
@@ -105,5 +105,8 @@
|
||||
/* Completeness */
|
||||
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
||||
"skipLibCheck": true /* Skip type checking all .d.ts files. */
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*.ts", "env.d.ts"],
|
||||
"exclude": ["node_modules"],
|
||||
"ts-node": { "transpileOnly": true }
|
||||
}
|
||||
|
||||
@@ -98,6 +98,13 @@
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/cookie-parser@^1.4.7":
|
||||
version "1.4.7"
|
||||
resolved "https://registry.yarnpkg.com/@types/cookie-parser/-/cookie-parser-1.4.7.tgz#c874471f888c72423d78d2b3c32d1e8579cf3c8f"
|
||||
integrity sha512-Fvuyi354Z+uayxzIGCwYTayFKocfV7TuDYZClCdIP9ckhvAu/ixDtCB6qx2TT0FKjPLf1f3P/J1rgf6lPs64mw==
|
||||
dependencies:
|
||||
"@types/express" "*"
|
||||
|
||||
"@types/cors@^2.8.14":
|
||||
version "2.8.17"
|
||||
resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.17.tgz#5d718a5e494a8166f569d986794e49c48b216b2b"
|
||||
@@ -115,6 +122,26 @@
|
||||
"@types/range-parser" "*"
|
||||
"@types/send" "*"
|
||||
|
||||
"@types/express-serve-static-core@^5.0.0":
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-5.0.1.tgz#3c9997ae9d00bc236e45c6374e84f2596458d9db"
|
||||
integrity sha512-CRICJIl0N5cXDONAdlTv5ShATZ4HEwk6kDDIW2/w9qOWKg+NU/5F8wYRWCrONad0/UKkloNSmmyN/wX4rtpbVA==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
"@types/qs" "*"
|
||||
"@types/range-parser" "*"
|
||||
"@types/send" "*"
|
||||
|
||||
"@types/express@*":
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/express/-/express-5.0.0.tgz#13a7d1f75295e90d19ed6e74cab3678488eaa96c"
|
||||
integrity sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ==
|
||||
dependencies:
|
||||
"@types/body-parser" "*"
|
||||
"@types/express-serve-static-core" "^5.0.0"
|
||||
"@types/qs" "*"
|
||||
"@types/serve-static" "*"
|
||||
|
||||
"@types/express@^4.17.17":
|
||||
version "4.17.21"
|
||||
resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.21.tgz#c26d4a151e60efe0084b23dc3369ebc631ed192d"
|
||||
@@ -394,6 +421,14 @@ content-type@~1.0.4:
|
||||
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918"
|
||||
integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==
|
||||
|
||||
cookie-parser@^1.4.7:
|
||||
version "1.4.7"
|
||||
resolved "https://registry.yarnpkg.com/cookie-parser/-/cookie-parser-1.4.7.tgz#e2125635dfd766888ffe90d60c286404fa0e7b26"
|
||||
integrity sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==
|
||||
dependencies:
|
||||
cookie "0.7.2"
|
||||
cookie-signature "1.0.6"
|
||||
|
||||
cookie-signature@1.0.6:
|
||||
version "1.0.6"
|
||||
resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
|
||||
@@ -404,6 +439,11 @@ cookie@0.5.0:
|
||||
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b"
|
||||
integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==
|
||||
|
||||
cookie@0.7.2:
|
||||
version "0.7.2"
|
||||
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.2.tgz#556369c472a2ba910f2979891b526b3436237ed7"
|
||||
integrity sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==
|
||||
|
||||
cors@^2.8.5:
|
||||
version "2.8.5"
|
||||
resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29"
|
||||
@@ -766,6 +806,11 @@ is-number@^7.0.0:
|
||||
resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
|
||||
integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
|
||||
|
||||
jose@^5.9.6:
|
||||
version "5.9.6"
|
||||
resolved "https://registry.yarnpkg.com/jose/-/jose-5.9.6.tgz#77f1f901d88ebdc405e57cce08d2a91f47521883"
|
||||
integrity sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ==
|
||||
|
||||
jsonwebtoken@^9.0.2:
|
||||
version "9.0.2"
|
||||
resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz#65ff91f4abef1784697d40952bb1998c504caaf3"
|
||||
|
||||
Reference in New Issue
Block a user