update handle

This commit is contained in:
nkhangg 2025-03-24 15:14:14 +07:00
parent b869b83a3f
commit db080ad3f2
50 changed files with 2121 additions and 203 deletions

View File

@ -1,10 +1,10 @@
import { Box, Button, Image, Text } from '@mantine/core'; import { Box, Button, Image, Text } from '@mantine/core';
import { IBid, IWebBid } from '../../system/type';
import { Socket } from 'socket.io-client';
import { useEffect, useState } from 'react';
import ShowImageModal from './show-image-modal';
import { useDisclosure } from '@mantine/hooks'; import { useDisclosure } from '@mantine/hooks';
import moment from 'moment'; import moment from 'moment';
import { useEffect, useState } from 'react';
import { Socket } from 'socket.io-client';
import { IBid, IWebBid } from '../../system/type';
import ShowImageModal from './show-image-modal';
export interface IWorkingPageProps { export interface IWorkingPageProps {
data: (IBid | IWebBid) & { type: string }; data: (IBid | IWebBid) & { type: string };
@ -20,6 +20,10 @@ export default function WorkingPage({ data, socket }: IWorkingPageProps) {
const [lastUpdate, setLastUpdate] = useState(new Date()); const [lastUpdate, setLastUpdate] = useState(new Date());
function isIBid(obj: IBid | IWebBid): obj is IBid {
return 'name' in obj;
}
useEffect(() => { useEffect(() => {
const updateImage = ({ type, id, filename }: { type: string; filename: string; id: IBid['id'] }) => { const updateImage = ({ type, id, filename }: { type: string; filename: string; id: IBid['id'] }) => {
if (type == data.type && id == data.id) { if (type == data.type && id == data.id) {
@ -49,11 +53,24 @@ export default function WorkingPage({ data, socket }: IWorkingPageProps) {
/> />
<Box className="absolute bg-black/60 inset-0 flex items-center justify-center text-white font-bold flex-col gap-3 p-4 rounded-lg transition duration-300 hover:bg-opacity-70"> <Box className="absolute bg-black/60 inset-0 flex items-center justify-center text-white font-bold flex-col gap-3 p-4 rounded-lg transition duration-300 hover:bg-opacity-70">
<Text className="text-lg uppercase tracking-wide">{data.type}</Text> <Text className="text-lg tracking-wide text-center font-bold">{isIBid(data) ? data.name : 'Tracking page'}</Text>
{isIBid(data) && <Text className="text-xs tracking-wide">{`Max price: $${data.max_price}`}</Text>}
{isIBid(data) && <Text className="text-xs tracking-wide">{`Current price: $${data.current_price}`}</Text>}
<Text className="text-sm italic opacity-80">{moment(lastUpdate).format('HH:mm:ss DD/MM/YYYY')}</Text> <Text className="text-sm italic opacity-80">{moment(lastUpdate).format('HH:mm:ss DD/MM/YYYY')}</Text>
<Button onClick={open} className="bg-white text-black px-4 py-2 rounded-md shadow-md hover:bg-gray-200 transition"> <Box className="flex items-center gap-4">
Show <Button size="xs" color="green" onClick={open} className="bg-white text-black px-4 py-2 rounded-md shadow-md hover:bg-gray-200 transition">
</Button> Show
</Button>
<Button
target="_blank"
component="a"
size="xs"
href={data.url || '/'}
className="bg-white text-black px-4 py-2 rounded-md shadow-md hover:bg-gray-200 transition"
>
Link
</Button>
</Box>
</Box> </Box>
</Box> </Box>

View File

@ -1,10 +1,10 @@
import { AppShell, Box, NavLink, ScrollArea } from '@mantine/core'; import { AppShell, Box, Button } from '@mantine/core';
import { useCallback, useEffect } from 'react'; import { useCallback, useEffect } from 'react';
import { Outlet, useLocation, useNavigate } from 'react-router'; import { Outlet, useLocation, useNavigate } from 'react-router';
import { Link } from 'react-router-dom';
import { me } from '../apis/auth'; import { me } from '../apis/auth';
import { Logo, UserMenu } from '../components'; import { Logo, UserMenu } from '../components';
import Links from '../system/links'; import Links from '../system/links';
import { Link } from 'react-router-dom';
export default function PrivateLayout() { export default function PrivateLayout() {
const navigate = useNavigate(); const navigate = useNavigate();
@ -19,19 +19,36 @@ export default function PrivateLayout() {
}, [navigate]); }, [navigate]);
useEffect(() => { useEffect(() => {
// checkAuth(); checkAuth();
}, [checkAuth]); }, [checkAuth]);
return ( return (
<AppShell header={{ height: 60 }} navbar={{ width: 300, breakpoint: 'sm' }} padding="md"> <AppShell header={{ height: 60 }} navbar={{ width: 300, breakpoint: 'sm', collapsed: { mobile: true, desktop: true } }} padding="md">
<AppShell.Header> <AppShell.Header>
<Box className="flex items-center justify-between h-full px-4"> <Box className="flex items-center justify-between h-full px-4">
<Logo /> <Logo />
<Box className="flex items-center gap-4">
{Links.MENUS.map((menu, index) => (
<Button
size="xs"
component={Link}
className="rounded-sm"
key={menu.path + index}
to={menu.path}
// label={menu.title}
variant={location.pathname === menu.path ? 'filled' : 'outline'}
leftSection={<menu.icon size={16} stroke={1.5} />}
>
{menu.title}
</Button>
))}
</Box>
<UserMenu /> <UserMenu />
</Box> </Box>
</AppShell.Header> </AppShell.Header>
<AppShell.Navbar px={'md'} pb={'md'}> {/* <AppShell.Navbar px={'md'} pb={'md'}>
<AppShell.Section grow my="md" component={ScrollArea}> <AppShell.Section grow my="md" component={ScrollArea}>
<div className="w-full h-full flex flex-col gap-3"> <div className="w-full h-full flex flex-col gap-3">
{Links.MENUS.map((menu, index) => ( {Links.MENUS.map((menu, index) => (
@ -48,7 +65,7 @@ export default function PrivateLayout() {
</div> </div>
</AppShell.Section> </AppShell.Section>
<AppShell.Section className="text-xs text-center">{new Date().getFullYear()}</AppShell.Section> <AppShell.Section className="text-xs text-center">{new Date().getFullYear()}</AppShell.Section>
</AppShell.Navbar> </AppShell.Navbar> */}
<AppShell.Main> <AppShell.Main>
<Outlet /> <Outlet />
</AppShell.Main> </AppShell.Main>

View File

@ -54,6 +54,7 @@ export interface TableProps<R extends Record<string, string | number>> extends M
onFetched?: (response: AxiosResponse<R[]>) => void; onFetched?: (response: AxiosResponse<R[]>) => void;
onChooses?: (chooses: R[]) => void; onChooses?: (chooses: R[]) => void;
onSearch?: (value: ITableFilter<R>[]) => void; onSearch?: (value: ITableFilter<R>[]) => void;
onClickRow?: (row: R) => void;
} }
const TableIcon = ({ const TableIcon = ({
@ -115,6 +116,7 @@ const Table = <R extends Record<string, any>>({
onFetchError, onFetchError,
onChooses, onChooses,
onSearch, onSearch,
onClickRow,
...props ...props
}: TableProps<R>) => { }: TableProps<R>) => {
const paramsUrl = new URLSearchParams(window.location.search); const paramsUrl = new URLSearchParams(window.location.search);
@ -507,6 +509,7 @@ const Table = <R extends Record<string, any>>({
handleSetChoosesData(newDataChooses); handleSetChoosesData(newDataChooses);
}, },
// eslint-disable-next-line react-hooks/exhaustive-deps
[rowsData, chooses, handleSetChoosesData], [rowsData, chooses, handleSetChoosesData],
); );
@ -576,10 +579,6 @@ const Table = <R extends Record<string, any>>({
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [options]); }, [options]);
// useEffect(() => {
// console.log('filter', filter);
// }, [filter]);
useEffect(() => { useEffect(() => {
if (chooses.length < rowsData.length) { if (chooses.length < rowsData.length) {
setCheckedAll(false); setCheckedAll(false);
@ -691,7 +690,7 @@ const Table = <R extends Record<string, any>>({
<MTable.Tbody {...tbody} className="relative"> <MTable.Tbody {...tbody} className="relative">
{rowsData.length > 0 && {rowsData.length > 0 &&
rowsData.map((row, index) => ( rowsData.map((row, index) => (
<MTable.Tr {...trbody} key={row[rowKey]}> <MTable.Tr {...trbody} onClick={onClickRow ? () => onClickRow(row) : undefined} key={row[rowKey]}>
{showChooses && ( {showChooses && (
<MTable.Td key={'__choose_' + index} {...td} {...chooseOptions?.defaultBodyProps}> <MTable.Td key={'__choose_' + index} {...td} {...chooseOptions?.defaultBodyProps}>
{chooseOptions?.renderBody ? ( {chooseOptions?.renderBody ? (

View File

@ -0,0 +1,180 @@
import { ActionIcon, Box, Menu } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
import { IconEdit, IconMenu, IconPassword, IconTrash } from '@tabler/icons-react';
import { useCallback, useMemo, useRef, useState } from 'react';
import { deleteAdmin, getAdmins } from '../apis/admin';
import { AdminModal, GrantNewPasswordModal } from '../components/admin';
import Table from '../lib/table/table';
import { IColumn, TRefTableFn } from '../lib/table/type';
import { useConfirmStore } from '../lib/zustand/use-confirm';
import { IAdmin } from '../system/type';
import { formatTime } from '../utils';
export default function Admins() {
const { setConfirm } = useConfirmStore();
const refTableFn: TRefTableFn<IAdmin> = useRef({});
const [adminModelOpened, adminModel] = useDisclosure(false);
const [grantOpened, grantModel] = useDisclosure(false);
const [clickData, setClickData] = useState<IAdmin | null>(null);
const columns: IColumn<IAdmin>[] = [
{
key: 'id',
title: 'ID',
typeFilter: 'number',
},
{
key: 'username',
title: 'Username',
typeFilter: 'text',
},
{
key: 'email',
title: 'Email',
typeFilter: 'text',
},
{
key: 'created_at',
title: 'Created at',
typeFilter: 'none',
renderRow(row) {
return <span>{formatTime(row.created_at)}</span>;
},
},
{
key: 'updated_at',
title: 'Update at',
typeFilter: 'none',
renderRow(row) {
return <span>{formatTime(row.updated_at)}</span>;
},
},
];
const handleDelete = (admin: IAdmin) => {
setConfirm({
title: 'Delete ?',
message: 'This admin will be delete',
handleOk: async () => {
await deleteAdmin(admin);
if (refTableFn.current?.fetchData) {
refTableFn.current.fetchData();
}
},
});
};
const getAdminsFn = useCallback(getAdmins, []);
const table = useMemo(() => {
return (
<Table
actionsOptions={{
actions: [
{
key: 'add',
title: 'Add',
callback: () => {
adminModel.open();
},
},
],
}}
refTableFn={refTableFn}
striped
showLoading={true}
highlightOnHover
styleDefaultHead={{
justifyContent: 'flex-start',
width: 'fit-content',
}}
options={{
query: getAdminsFn,
pathToData: 'data.data',
keyOptions: {
last_page: 'lastPage',
per_page: 'perPage',
from: 'from',
to: 'to',
total: 'total',
},
}}
rows={[]}
withColumnBorders
showChooses={false}
withTableBorder
columns={columns}
actions={{
title: <Box className="w-full text-center">Action</Box>,
body: (row) => {
return (
<Menu shadow="md" width={200}>
<Menu.Target>
<Box className="flex w-full items-center justify-center">
<ActionIcon size="sm" variant="light">
<IconMenu size={14} />
</ActionIcon>
</Box>
</Menu.Target>
<Menu.Dropdown>
<Menu.Item
onClick={() => {
setClickData(row);
adminModel.open();
}}
leftSection={<IconEdit size={14} />}
>
Edit
</Menu.Item>
<Menu.Item
onClick={() => {
setClickData(row);
grantModel.open();
}}
leftSection={<IconPassword size={14} />}
>
Grant new password
</Menu.Item>
<Menu.Item disabled={row.is_system_account} onClick={() => handleDelete(row)} leftSection={<IconTrash color="red" size={14} />}>
Delete
</Menu.Item>
</Menu.Dropdown>
</Menu>
);
},
}}
rowKey="id"
/>
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<Box>
{table}
<AdminModal
onUpdated={refTableFn.current.fetchData}
data={clickData}
opened={adminModelOpened}
onClose={() => {
setClickData(null);
adminModel.close();
}}
/>
<GrantNewPasswordModal
opened={grantOpened}
data={clickData}
onClose={() => {
setClickData(null);
grantModel.close();
}}
/>
</Box>
);
}

View File

@ -8,9 +8,9 @@ import { BidModal, ShowHistoriesBidGraysApiModal, ShowHistoriesModal } from '../
import Table from '../lib/table/table'; import Table from '../lib/table/table';
import { IColumn, TRefTableFn } from '../lib/table/type'; import { IColumn, TRefTableFn } from '../lib/table/type';
import { useConfirmStore } from '../lib/zustand/use-confirm'; import { useConfirmStore } from '../lib/zustand/use-confirm';
import { mappingStatusColors } from '../system/constants';
import { IBid } from '../system/type'; import { IBid } from '../system/type';
import { formatTime } from '../utils'; import { formatTime } from '../utils';
import { mappingStatusColors } from '../system/constants';
export default function Bids() { export default function Bids() {
const refTableFn: TRefTableFn<IBid> = useRef({}); const refTableFn: TRefTableFn<IBid> = useRef({});
@ -108,15 +108,6 @@ export default function Bids() {
); );
}, },
}, },
// {
// key: 'updated_at',
// title: 'Update at',
// typeFilter: 'none',
// renderRow(row) {
// return <span className="text-sm">{formatTime(row.updated_at)}</span>;
// },
// },
]; ];
const handleDelete = (bid: IBid) => { const handleDelete = (bid: IBid) => {
@ -156,6 +147,14 @@ export default function Bids() {
const table = useMemo(() => { const table = useMemo(() => {
return ( return (
<Table <Table
onClickRow={(row) => {
window.open(row.url, '_blank');
}}
tableChildProps={{
trbody: {
className: 'cursor-pointer',
},
}}
actionsOptions={{ actionsOptions={{
actions: [ actions: [
{ {
@ -217,14 +216,14 @@ export default function Bids() {
return ( return (
<Menu shadow="md" width={200}> <Menu shadow="md" width={200}>
<Menu.Target> <Menu.Target>
<Box className="flex w-full items-center justify-center"> <Box onClick={(e) => e.stopPropagation()} className="flex w-full items-center justify-center">
<ActionIcon size="sm" variant="light"> <ActionIcon size="sm" variant="light">
<IconMenu size={14} /> <IconMenu size={14} />
</ActionIcon> </ActionIcon>
</Box> </Box>
</Menu.Target> </Menu.Target>
<Menu.Dropdown> <Menu.Dropdown onClick={(e) => e.stopPropagation()}>
<Menu.Item <Menu.Item
onClick={() => { onClick={() => {
setClickData(row); setClickData(row);

View File

@ -1,13 +1,15 @@
import { IconHammer, IconHome2, IconMessage, IconOutlet, IconPageBreak } from '@tabler/icons-react'; import { IconHammer, IconHome2, IconMessage, IconOutlet, IconPageBreak, IconUserCheck } from '@tabler/icons-react';
import { Bids, Dashboard, OutBidsLog } from '../pages'; import { Bids, Dashboard, OutBidsLog } from '../pages';
import WebBids from '../pages/web-bids'; import WebBids from '../pages/web-bids';
import SendMessageHistories from '../pages/send-message-histories'; import SendMessageHistories from '../pages/send-message-histories';
import Admins from '../pages/admins';
export default class Links { export default class Links {
public static DASHBOARD = '/dashboard'; public static DASHBOARD = '/dashboard';
public static BIDS = '/bids'; public static BIDS = '/bids';
public static WEBS = '/webs'; public static WEBS = '/webs';
public static OUT_BIDS_LOG = '/out-bids-log'; public static OUT_BIDS_LOG = '/out-bids-log';
public static SEND_MESSAGE_HISTORIES = '/send-message-histories'; public static SEND_MESSAGE_HISTORIES = '/send-message-histories';
public static ADMINS = '/admins';
public static HOME = '/'; public static HOME = '/';
public static LOGIN = '/login'; public static LOGIN = '/login';
@ -19,6 +21,12 @@ export default class Links {
icon: IconHome2, icon: IconHome2,
element: Dashboard, element: Dashboard,
}, },
{
path: this.ADMINS,
title: 'Admins',
icon: IconUserCheck,
element: Admins,
},
{ {
path: this.WEBS, path: this.WEBS,
title: 'Webs', title: 'Webs',

View File

@ -13,14 +13,18 @@
"@nestjs/config": "^4.0.1", "@nestjs/config": "^4.0.1",
"@nestjs/core": "^10.0.0", "@nestjs/core": "^10.0.0",
"@nestjs/event-emitter": "^3.0.1", "@nestjs/event-emitter": "^3.0.1",
"@nestjs/jwt": "^11.0.0",
"@nestjs/mapped-types": "*", "@nestjs/mapped-types": "*",
"@nestjs/platform-express": "^10.4.15", "@nestjs/platform-express": "^10.4.15",
"@nestjs/platform-socket.io": "^11.0.11", "@nestjs/platform-socket.io": "^11.0.11",
"@nestjs/throttler": "^6.4.0",
"@nestjs/typeorm": "^11.0.0", "@nestjs/typeorm": "^11.0.0",
"@nestjs/websockets": "^11.0.11", "@nestjs/websockets": "^11.0.11",
"axios": "^1.8.3", "axios": "^1.8.3",
"bcrypt": "^5.1.1",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"class-validator": "^0.14.1", "class-validator": "^0.14.1",
"cookie-parser": "^1.4.7",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"multer": "^1.4.5-lts.1", "multer": "^1.4.5-lts.1",
"mysql2": "^3.13.0", "mysql2": "^3.13.0",
@ -34,6 +38,8 @@
"@nestjs/cli": "^10.0.0", "@nestjs/cli": "^10.0.0",
"@nestjs/schematics": "^10.0.0", "@nestjs/schematics": "^10.0.0",
"@nestjs/testing": "^10.0.0", "@nestjs/testing": "^10.0.0",
"@types/bcrypt": "^5.0.2",
"@types/cookie-parser": "^1.4.8",
"@types/express": "^4.17.17", "@types/express": "^4.17.17",
"@types/jest": "^29.5.2", "@types/jest": "^29.5.2",
"@types/multer": "^1.4.12", "@types/multer": "^1.4.12",
@ -2076,6 +2082,50 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/@mapbox/node-pre-gyp": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz",
"integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==",
"license": "BSD-3-Clause",
"dependencies": {
"detect-libc": "^2.0.0",
"https-proxy-agent": "^5.0.0",
"make-dir": "^3.1.0",
"node-fetch": "^2.6.7",
"nopt": "^5.0.0",
"npmlog": "^5.0.1",
"rimraf": "^3.0.2",
"semver": "^7.3.5",
"tar": "^6.1.11"
},
"bin": {
"node-pre-gyp": "bin/node-pre-gyp"
}
},
"node_modules/@mapbox/node-pre-gyp/node_modules/make-dir": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
"integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
"license": "MIT",
"dependencies": {
"semver": "^6.0.0"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@mapbox/node-pre-gyp/node_modules/make-dir/node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
}
},
"node_modules/@microsoft/tsdoc": { "node_modules/@microsoft/tsdoc": {
"version": "0.15.1", "version": "0.15.1",
"resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.15.1.tgz", "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.15.1.tgz",
@ -2309,6 +2359,19 @@
"@nestjs/core": "^10.0.0 || ^11.0.0" "@nestjs/core": "^10.0.0 || ^11.0.0"
} }
}, },
"node_modules/@nestjs/jwt": {
"version": "11.0.0",
"resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-11.0.0.tgz",
"integrity": "sha512-v7YRsW3Xi8HNTsO+jeHSEEqelX37TVWgwt+BcxtkG/OfXJEOs6GZdbdza200d6KqId1pJQZ6UPj1F0M6E+mxaA==",
"license": "MIT",
"dependencies": {
"@types/jsonwebtoken": "9.0.7",
"jsonwebtoken": "9.0.2"
},
"peerDependencies": {
"@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0"
}
},
"node_modules/@nestjs/mapped-types": { "node_modules/@nestjs/mapped-types": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.1.0.tgz", "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.1.0.tgz",
@ -2483,6 +2546,17 @@
} }
} }
}, },
"node_modules/@nestjs/throttler": {
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/@nestjs/throttler/-/throttler-6.4.0.tgz",
"integrity": "sha512-osL67i0PUuwU5nqSuJjtUJZMkxAnYB4VldgYUMGzvYRJDCqGRFMWbsbzm/CkUtPLRL30I8T74Xgt/OQxnYokiA==",
"license": "MIT",
"peerDependencies": {
"@nestjs/common": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0",
"@nestjs/core": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0",
"reflect-metadata": "^0.1.13 || ^0.2.0"
}
},
"node_modules/@nestjs/typeorm": { "node_modules/@nestjs/typeorm": {
"version": "11.0.0", "version": "11.0.0",
"resolved": "https://registry.npmjs.org/@nestjs/typeorm/-/typeorm-11.0.0.tgz", "resolved": "https://registry.npmjs.org/@nestjs/typeorm/-/typeorm-11.0.0.tgz",
@ -2718,6 +2792,16 @@
"@babel/types": "^7.20.7" "@babel/types": "^7.20.7"
} }
}, },
"node_modules/@types/bcrypt": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.2.tgz",
"integrity": "sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/body-parser": { "node_modules/@types/body-parser": {
"version": "1.19.5", "version": "1.19.5",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
@ -2739,6 +2823,16 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"node_modules/@types/cookie-parser": {
"version": "1.4.8",
"resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.8.tgz",
"integrity": "sha512-l37JqFrOJ9yQfRQkljb41l0xVphc7kg5JTjjr+pLRZ0IyZ49V4BQ8vbF4Ut2C2e+WH4al3xD3ZwYwIUfnbT4NQ==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"@types/express": "*"
}
},
"node_modules/@types/cookiejar": { "node_modules/@types/cookiejar": {
"version": "2.1.5", "version": "2.1.5",
"resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz",
@ -2872,6 +2966,15 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/jsonwebtoken": {
"version": "9.0.7",
"resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.7.tgz",
"integrity": "sha512-ugo316mmTYBl2g81zDFnZ7cfxlut3o+/EQdaP7J8QN2kY6lJ22hmQYCK5EHcJHbrW+dkCGSCPgbG8JtYj6qSrg==",
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/methods": { "node_modules/@types/methods": {
"version": "1.1.4", "version": "1.1.4",
"resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz",
@ -3383,6 +3486,12 @@
"dev": true, "dev": true,
"license": "Apache-2.0" "license": "Apache-2.0"
}, },
"node_modules/abbrev": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
"license": "ISC"
},
"node_modules/abstract-logging": { "node_modules/abstract-logging": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz",
@ -3439,6 +3548,18 @@
"node": ">=0.4.0" "node": ">=0.4.0"
} }
}, },
"node_modules/agent-base": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
"license": "MIT",
"dependencies": {
"debug": "4"
},
"engines": {
"node": ">= 6.0.0"
}
},
"node_modules/ajv": { "node_modules/ajv": {
"version": "8.12.0", "version": "8.12.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
@ -3600,6 +3721,40 @@
"integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/aproba": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz",
"integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==",
"license": "ISC"
},
"node_modules/are-we-there-yet": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz",
"integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==",
"deprecated": "This package is no longer supported.",
"license": "ISC",
"dependencies": {
"delegates": "^1.0.0",
"readable-stream": "^3.6.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/are-we-there-yet/node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"license": "MIT",
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/arg": { "node_modules/arg": {
"version": "4.1.3", "version": "4.1.3",
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
@ -3858,6 +4013,20 @@
"node": "^4.5.0 || >= 5.9" "node": "^4.5.0 || >= 5.9"
} }
}, },
"node_modules/bcrypt": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz",
"integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"@mapbox/node-pre-gyp": "^1.0.11",
"node-addon-api": "^5.0.0"
},
"engines": {
"node": ">= 10.0.0"
}
},
"node_modules/binary-extensions": { "node_modules/binary-extensions": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
@ -4040,6 +4209,12 @@
"ieee754": "^1.1.13" "ieee754": "^1.1.13"
} }
}, },
"node_modules/buffer-equal-constant-time": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
"license": "BSD-3-Clause"
},
"node_modules/buffer-from": { "node_modules/buffer-from": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
@ -4213,6 +4388,15 @@
"fsevents": "~2.3.2" "fsevents": "~2.3.2"
} }
}, },
"node_modules/chownr": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
"integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
"license": "ISC",
"engines": {
"node": ">=10"
}
},
"node_modules/chrome-trace-event": { "node_modules/chrome-trace-event": {
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz",
@ -4415,6 +4599,15 @@
"simple-swizzle": "^0.2.2" "simple-swizzle": "^0.2.2"
} }
}, },
"node_modules/color-support": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
"integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==",
"license": "ISC",
"bin": {
"color-support": "bin.js"
}
},
"node_modules/combined-stream": { "node_modules/combined-stream": {
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@ -4468,7 +4661,6 @@
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/concat-stream": { "node_modules/concat-stream": {
@ -4492,6 +4684,12 @@
"integrity": "sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==", "integrity": "sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/console-control-strings": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
"integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==",
"license": "ISC"
},
"node_modules/content-disposition": { "node_modules/content-disposition": {
"version": "0.5.4", "version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
@ -4529,6 +4727,28 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/cookie-parser": {
"version": "1.4.7",
"resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz",
"integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==",
"license": "MIT",
"dependencies": {
"cookie": "0.7.2",
"cookie-signature": "1.0.6"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/cookie-parser/node_modules/cookie": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie-signature": { "node_modules/cookie-signature": {
"version": "1.0.6", "version": "1.0.6",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
@ -4726,6 +4946,12 @@
"node": ">=0.4.0" "node": ">=0.4.0"
} }
}, },
"node_modules/delegates": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
"integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==",
"license": "MIT"
},
"node_modules/denque": { "node_modules/denque": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
@ -4887,6 +5113,15 @@
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/ecdsa-sig-formatter": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
"license": "Apache-2.0",
"dependencies": {
"safe-buffer": "^5.0.1"
}
},
"node_modules/ee-first": { "node_modules/ee-first": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@ -6042,6 +6277,36 @@
"node": ">=12" "node": ">=12"
} }
}, },
"node_modules/fs-minipass": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
"integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
"license": "ISC",
"dependencies": {
"minipass": "^3.0.0"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/fs-minipass/node_modules/minipass": {
"version": "3.3.6",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
"integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
"license": "ISC",
"dependencies": {
"yallist": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/fs-minipass/node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"license": "ISC"
},
"node_modules/fs-monkey": { "node_modules/fs-monkey": {
"version": "1.0.6", "version": "1.0.6",
"resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.6.tgz", "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.6.tgz",
@ -6053,7 +6318,6 @@
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
"dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/fsevents": { "node_modules/fsevents": {
@ -6080,6 +6344,33 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/gauge": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz",
"integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==",
"deprecated": "This package is no longer supported.",
"license": "ISC",
"dependencies": {
"aproba": "^1.0.3 || ^2.0.0",
"color-support": "^1.1.2",
"console-control-strings": "^1.0.0",
"has-unicode": "^2.0.1",
"object-assign": "^4.1.1",
"signal-exit": "^3.0.0",
"string-width": "^4.2.3",
"strip-ansi": "^6.0.1",
"wide-align": "^1.1.2"
},
"engines": {
"node": ">=10"
}
},
"node_modules/gauge/node_modules/signal-exit": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
"license": "ISC"
},
"node_modules/generate-function": { "node_modules/generate-function": {
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz",
@ -6345,6 +6636,12 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/has-unicode": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
"integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==",
"license": "ISC"
},
"node_modules/hasown": { "node_modules/hasown": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
@ -6390,6 +6687,19 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/https-proxy-agent": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
"license": "MIT",
"dependencies": {
"agent-base": "6",
"debug": "4"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/human-signals": { "node_modules/human-signals": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
@ -6494,7 +6804,6 @@
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
"deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
"dev": true,
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"once": "^1.3.0", "once": "^1.3.0",
@ -7664,6 +7973,49 @@
"graceful-fs": "^4.1.6" "graceful-fs": "^4.1.6"
} }
}, },
"node_modules/jsonwebtoken": {
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
"integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==",
"license": "MIT",
"dependencies": {
"jws": "^3.2.2",
"lodash.includes": "^4.3.0",
"lodash.isboolean": "^3.0.3",
"lodash.isinteger": "^4.0.4",
"lodash.isnumber": "^3.0.3",
"lodash.isplainobject": "^4.0.6",
"lodash.isstring": "^4.0.1",
"lodash.once": "^4.0.0",
"ms": "^2.1.1",
"semver": "^7.5.4"
},
"engines": {
"node": ">=12",
"npm": ">=6"
}
},
"node_modules/jwa": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
"integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==",
"license": "MIT",
"dependencies": {
"buffer-equal-constant-time": "1.0.1",
"ecdsa-sig-formatter": "1.0.11",
"safe-buffer": "^5.0.1"
}
},
"node_modules/jws": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
"integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
"license": "MIT",
"dependencies": {
"jwa": "^1.4.1",
"safe-buffer": "^5.0.1"
}
},
"node_modules/keyv": { "node_modules/keyv": {
"version": "4.5.4", "version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@ -7785,6 +8137,42 @@
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/lodash.includes": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
"license": "MIT"
},
"node_modules/lodash.isboolean": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
"license": "MIT"
},
"node_modules/lodash.isinteger": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
"integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
"license": "MIT"
},
"node_modules/lodash.isnumber": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
"integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
"license": "MIT"
},
"node_modules/lodash.isplainobject": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
"license": "MIT"
},
"node_modules/lodash.isstring": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
"license": "MIT"
},
"node_modules/lodash.memoize": { "node_modules/lodash.memoize": {
"version": "4.1.2", "version": "4.1.2",
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
@ -7799,6 +8187,12 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/lodash.once": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
"license": "MIT"
},
"node_modules/log-symbols": { "node_modules/log-symbols": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
@ -8063,6 +8457,37 @@
"node": ">=16 || 14 >=14.17" "node": ">=16 || 14 >=14.17"
} }
}, },
"node_modules/minizlib": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
"integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
"license": "MIT",
"dependencies": {
"minipass": "^3.0.0",
"yallist": "^4.0.0"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/minizlib/node_modules/minipass": {
"version": "3.3.6",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
"integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
"license": "ISC",
"dependencies": {
"yallist": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/minizlib/node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"license": "ISC"
},
"node_modules/mkdirp": { "node_modules/mkdirp": {
"version": "0.5.6", "version": "0.5.6",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
@ -8205,6 +8630,12 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/node-addon-api": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz",
"integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==",
"license": "MIT"
},
"node_modules/node-emoji": { "node_modules/node-emoji": {
"version": "1.11.0", "version": "1.11.0",
"resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz", "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz",
@ -8249,6 +8680,21 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/nopt": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
"integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==",
"license": "ISC",
"dependencies": {
"abbrev": "1"
},
"bin": {
"nopt": "bin/nopt.js"
},
"engines": {
"node": ">=6"
}
},
"node_modules/normalize-path": { "node_modules/normalize-path": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
@ -8272,6 +8718,19 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/npmlog": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz",
"integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==",
"deprecated": "This package is no longer supported.",
"license": "ISC",
"dependencies": {
"are-we-there-yet": "^2.0.0",
"console-control-strings": "^1.1.0",
"gauge": "^3.0.0",
"set-blocking": "^2.0.0"
}
},
"node_modules/object-assign": { "node_modules/object-assign": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@ -8328,7 +8787,6 @@
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"dev": true,
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"wrappy": "1" "wrappy": "1"
@ -8505,7 +8963,6 @@
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
@ -9155,7 +9612,6 @@
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
"deprecated": "Rimraf versions prior to v4 are no longer supported", "deprecated": "Rimraf versions prior to v4 are no longer supported",
"dev": true,
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"glob": "^7.1.3" "glob": "^7.1.3"
@ -9171,7 +9627,6 @@
"version": "1.1.11", "version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"balanced-match": "^1.0.0", "balanced-match": "^1.0.0",
@ -9183,7 +9638,6 @@
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"deprecated": "Glob versions prior to v9 are no longer supported", "deprecated": "Glob versions prior to v9 are no longer supported",
"dev": true,
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"fs.realpath": "^1.0.0", "fs.realpath": "^1.0.0",
@ -9204,7 +9658,6 @@
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"brace-expansion": "^1.1.7" "brace-expansion": "^1.1.7"
@ -9472,6 +9925,12 @@
"node": ">= 0.8.0" "node": ">= 0.8.0"
} }
}, },
"node_modules/set-blocking": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
"license": "ISC"
},
"node_modules/set-cookie-parser": { "node_modules/set-cookie-parser": {
"version": "2.7.1", "version": "2.7.1",
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
@ -10145,6 +10604,50 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/tar": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
"integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==",
"license": "ISC",
"dependencies": {
"chownr": "^2.0.0",
"fs-minipass": "^2.0.0",
"minipass": "^5.0.0",
"minizlib": "^2.1.1",
"mkdirp": "^1.0.3",
"yallist": "^4.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/tar/node_modules/minipass": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
"integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==",
"license": "ISC",
"engines": {
"node": ">=8"
}
},
"node_modules/tar/node_modules/mkdirp": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
"license": "MIT",
"bin": {
"mkdirp": "bin/cmd.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/tar/node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"license": "ISC"
},
"node_modules/terser": { "node_modules/terser": {
"version": "5.39.0", "version": "5.39.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.39.0.tgz", "resolved": "https://registry.npmjs.org/terser/-/terser-5.39.0.tgz",
@ -11107,6 +11610,15 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/wide-align": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz",
"integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==",
"license": "ISC",
"dependencies": {
"string-width": "^1.0.2 || 2 || 3 || 4"
}
},
"node_modules/word-wrap": { "node_modules/word-wrap": {
"version": "1.2.5", "version": "1.2.5",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
@ -11154,7 +11666,6 @@
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/write-file-atomic": { "node_modules/write-file-atomic": {

View File

@ -17,21 +17,30 @@
"test:watch": "jest --watch", "test:watch": "jest --watch",
"test:cov": "jest --coverage", "test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json" "test:e2e": "jest --config ./test/jest-e2e.json",
"typeorm": "ts-node ./node_modules/typeorm/cli",
"migration:run": "npm run typeorm migration:run -- -d ./src/config/typeorm.ts",
"migration:generate": "npm run typeorm -- -d ./src/config/typeorm.ts migration:generate ./src/migrations/$npm_config_name",
"migration:create": "npm run typeorm -- migration:create ./src/migrations/$npm_config_name",
"migration:revert": "npm run typeorm -- -d ./src/config/typeorm.ts migration:revert"
}, },
"dependencies": { "dependencies": {
"@nestjs/common": "^10.0.0", "@nestjs/common": "^10.0.0",
"@nestjs/config": "^4.0.1", "@nestjs/config": "^4.0.1",
"@nestjs/core": "^10.0.0", "@nestjs/core": "^10.0.0",
"@nestjs/event-emitter": "^3.0.1", "@nestjs/event-emitter": "^3.0.1",
"@nestjs/jwt": "^11.0.0",
"@nestjs/mapped-types": "*", "@nestjs/mapped-types": "*",
"@nestjs/platform-express": "^10.4.15", "@nestjs/platform-express": "^10.4.15",
"@nestjs/platform-socket.io": "^11.0.11", "@nestjs/platform-socket.io": "^11.0.11",
"@nestjs/throttler": "^6.4.0",
"@nestjs/typeorm": "^11.0.0", "@nestjs/typeorm": "^11.0.0",
"@nestjs/websockets": "^11.0.11", "@nestjs/websockets": "^11.0.11",
"axios": "^1.8.3", "axios": "^1.8.3",
"bcrypt": "^5.1.1",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"class-validator": "^0.14.1", "class-validator": "^0.14.1",
"cookie-parser": "^1.4.7",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"multer": "^1.4.5-lts.1", "multer": "^1.4.5-lts.1",
"mysql2": "^3.13.0", "mysql2": "^3.13.0",
@ -45,6 +54,8 @@
"@nestjs/cli": "^10.0.0", "@nestjs/cli": "^10.0.0",
"@nestjs/schematics": "^10.0.0", "@nestjs/schematics": "^10.0.0",
"@nestjs/testing": "^10.0.0", "@nestjs/testing": "^10.0.0",
"@types/bcrypt": "^5.0.2",
"@types/cookie-parser": "^1.4.8",
"@types/express": "^4.17.17", "@types/express": "^4.17.17",
"@types/jest": "^29.5.2", "@types/jest": "^29.5.2",
"@types/multer": "^1.4.12", "@types/multer": "^1.4.12",

View File

@ -1,11 +1,36 @@
import { Module } from '@nestjs/common'; import { MiddlewareConsumer, Module, RequestMethod } from '@nestjs/common';
import { BidsModule } from './modules/bids/bids.module'; import { BidsModule } from './modules/bids/bids.module';
import { DatabasesModule } from './modules/databases/databases.module'; import { DatabasesModule } from './modules/databases/databases.module';
import { AppConfigsModule } from './modules/app-configs/app-configs.module'; import { AppConfigsModule } from './modules/app-configs/app-configs.module';
import { AppValidatorsModule } from './modules/app-validators/app-validators.module';
import { AuthModule } from './modules/auth/auth.module';
import { AdminsModule } from './modules/admins/admins.module';
import { AuthenticationMiddleware } from './modules/auth/middlewares/authentication.middleware';
import { excludeAuth, excludeAuthor } from './system/routes/exclude-route';
import { AuthorizationMiddleware } from './modules/admins/middlewares/authorization.middleware';
@Module({ @Module({
imports: [BidsModule, DatabasesModule, AppConfigsModule], imports: [
BidsModule,
DatabasesModule,
AppConfigsModule,
AppValidatorsModule,
AuthModule,
AdminsModule,
],
controllers: [], controllers: [],
providers: [], providers: [],
}) })
export class AppModule {} export class AppModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(AuthenticationMiddleware)
.exclude(...excludeAuth)
.forRoutes({ path: 'admin/*', method: RequestMethod.ALL });
consumer
.apply(AuthorizationMiddleware)
.exclude(...excludeAuthor)
.forRoutes({ path: 'admin/*', method: RequestMethod.ALL });
}
}

View File

@ -0,0 +1,24 @@
import { registerAs } from '@nestjs/config';
import { config as dotenvConfig } from 'dotenv';
import { DataSource, DataSourceOptions } from 'typeorm';
dotenvConfig({ path: '.env' });
const config = {
type: 'mysql',
host: `${process.env.DB_HOST}`,
port: `${process.env.DB_PORT}`,
username: `${process.env.DB_USERNAME}`,
password: `${process.env.DB_PASSWORD}`,
database: `${process.env.DB_NAME}`,
entities: ['dist/**/*.entity{.ts,.js}'],
migrations: ['dist/migrations/*{.ts,.js}'],
autoLoadEntities: true,
synchronize: false,
retryAttempts: 3,
retryDelay: 3000,
logging: true,
};
export default registerAs('typeorm', () => config);
export const connectionSource = new DataSource(config as DataSourceOptions);

View File

@ -3,6 +3,8 @@ import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common'; import { ValidationPipe } from '@nestjs/common';
import { BadRequestExceptionFilter } from './system/filters/bad-request-exception-filter'; import { BadRequestExceptionFilter } from './system/filters/bad-request-exception-filter';
import { IoAdapter } from '@nestjs/platform-socket.io'; import { IoAdapter } from '@nestjs/platform-socket.io';
import * as cookieParser from 'cookie-parser';
import { useContainer } from 'class-validator';
async function bootstrap() { async function bootstrap() {
const app = await NestFactory.create(AppModule); const app = await NestFactory.create(AppModule);
@ -16,9 +18,10 @@ async function bootstrap() {
credentials: true, credentials: true,
}); });
app.use(cookieParser());
app.setGlobalPrefix(prefix_version, {}); app.setGlobalPrefix(prefix_version, {});
// Sử dụng WebSocket adapter
app.useWebSocketAdapter(new IoAdapter(app)); app.useWebSocketAdapter(new IoAdapter(app));
app.useGlobalFilters(new BadRequestExceptionFilter()); app.useGlobalFilters(new BadRequestExceptionFilter());
@ -31,6 +34,8 @@ async function bootstrap() {
}), }),
); );
useContainer(app.select(AppModule), { fallbackOnErrors: true });
await app.listen(process.env.PORT); await app.listen(process.env.PORT);
} }
bootstrap(); bootstrap();

View File

@ -0,0 +1,16 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class CreateAdminTable1742778498009 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
INSERT INTO admins (email, username, password, is_system_account) VALUES
('admin@gmail.com', 'admin', '$2b$10$eF7K4Msw32e5ZC2cU78KgOqxMJygQcPDt5xXZP29inBBIV9KEsoyO', 1);
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
DELETE FROM ktq_admins WHERE email = 'admin@gmail.com';
`);
}
}

View File

@ -0,0 +1,17 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class CreatePermissionTable1742778709701 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
INSERT INTO permissions (name, description) VALUES
('GET', 'Read data from the API'),
('POST', 'Create new records'),
('PUT', 'Update existing records'),
('DELETE', 'Remove records');
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE permissions;`);
}
}

View File

@ -0,0 +1,16 @@
import { Module } from '@nestjs/common';
import { AdminsService } from './services/admins.service';
import Admin from './entities/admin.entity';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AdminsController } from './controllers/admins.controller';
import Permission from './entities/permission.entity';
import { PermissionService } from './services/permission.service';
import { PermissionsController } from './controllers/permissions.controller';
@Module({
imports: [TypeOrmModule.forFeature([Admin, Permission])],
providers: [AdminsService, PermissionService],
exports: [AdminsService, PermissionService],
controllers: [AdminsController, PermissionsController],
})
export class AdminsModule {}

View File

@ -0,0 +1,69 @@
import {
Body,
Controller,
Delete,
Get,
Param,
Post,
Put,
Req,
UseGuards,
} from '@nestjs/common';
import { AdminsService } from '../services/admins.service';
import { Paginate, PaginateQuery } from 'nestjs-paginate';
import Admin from '../entities/admin.entity';
import { UpdateDto } from '../dtos/update.dto';
import { DeletesDto } from '../dtos/deletes.dto';
import { Request } from 'express';
import { CreateDto } from '../dtos/create.dto';
import Permission from '../entities/permission.entity';
import { GrantNewPasswordDto } from '../dtos/grant-new-password.dto';
import { SystemAccountGuard } from '@/system/guards/system-account.guard';
@Controller('admin/admins')
export class AdminsController {
constructor(private readonly adminService: AdminsService) {}
@Get()
async index(@Paginate() query: PaginateQuery) {
return await this.adminService.index(query);
}
@Delete(':id')
async delete(@Param('id') id: Admin['id']) {
return await this.adminService.delete(id);
}
@Put(':id')
async update(@Param('id') id: Admin['id'], @Body() data: UpdateDto) {
const permissions = data.permissions
? data.permissions.map((item) => {
const permission = new Permission();
permission.id = item.id;
permission.name = item.name;
return permission;
})
: undefined;
return await this.adminService.update(id, { ...data, permissions });
}
@Post('')
async create(@Body() data: CreateDto) {
return await this.adminService.create(data);
}
@Post('grant-new-password/:id')
@UseGuards(SystemAccountGuard)
async grantNewPassword(
@Param('id') id: Admin['id'],
@Body() data: GrantNewPasswordDto,
@Req() request: Request,
) {
return await this.adminService.grantNewPassword(id, data.password, request);
}
@Post('deletes')
async deletes(@Body() data: DeletesDto, @Req() request: Request) {
return await this.adminService.deletes(data.ids, request);
}
}

View File

@ -0,0 +1,13 @@
import { Controller, Get } from '@nestjs/common';
import { Paginate, PaginateQuery } from 'nestjs-paginate';
import { PermissionService } from '../services/permission.service';
@Controller('admin/permissions')
export class PermissionsController {
constructor(private readonly permissionService: PermissionService) {}
@Get()
async index(@Paginate() query: PaginateQuery) {
return await this.permissionService.index(query);
}
}

View File

@ -0,0 +1,20 @@
import { IsUnique } from '@/system/validators/decorators/is-unique';
import { IsEmail, IsString, MinLength } from 'class-validator';
export class CreateDto {
@IsString()
@IsEmail()
@IsUnique({ tableName: 'admins', column: 'email' })
email: string;
@IsString()
fullname: string;
@IsString()
@IsUnique({ tableName: 'admins', column: 'username' })
username: string;
@IsString()
@MinLength(6)
password: string;
}

View File

@ -0,0 +1,6 @@
import { IsNumber } from 'class-validator';
export class DeletesDto {
@IsNumber({}, { each: true })
ids: number[];
}

View File

@ -0,0 +1,7 @@
import { IsString, MinLength } from 'class-validator';
export class GrantNewPasswordDto {
@IsString()
@MinLength(6)
password: string;
}

View File

@ -0,0 +1,11 @@
import { HasExisted } from '@/system/validators/decorators/has-existed';
import { IsNumber, IsString } from 'class-validator';
export class PermissionDto {
@IsNumber()
@HasExisted({ tableName: 'permissions', column: 'id' })
id: number;
@IsString()
name: string;
}

View File

@ -0,0 +1,17 @@
import { IsArray, IsEmail, IsOptional, IsString } from 'class-validator';
import { PermissionDto } from './permission.dto';
export class UpdateDto {
@IsString()
@IsEmail()
@IsOptional()
email: string;
@IsString()
@IsOptional()
fullname: string;
@IsOptional()
@IsArray()
permissions: PermissionDto[];
}

View File

@ -0,0 +1,29 @@
import { Exclude } from 'class-transformer';
import { Column, Entity, ManyToMany, PrimaryGeneratedColumn } from 'typeorm';
import Permission from './permission.entity';
import { Timestamp } from './timestamp';
@Entity('admins')
export default class Admin extends Timestamp {
@PrimaryGeneratedColumn('increment')
id: number;
@Column({ type: 'varchar', unique: true })
email: string;
@Column({ type: 'varchar', unique: true })
username: string;
@Column({ type: 'varchar', default: null, nullable: true })
fullname: string;
@Column({ type: 'varchar' })
@Exclude()
password: string;
@Column({ type: 'boolean', default: false })
is_system_account: boolean;
@ManyToMany(() => Permission, (permission) => permission.admins)
permissions: Permission[];
}

View File

@ -0,0 +1,24 @@
import {
Column,
Entity,
JoinTable,
ManyToMany,
PrimaryGeneratedColumn,
} from 'typeorm';
import Admin from './admin.entity';
@Entity('permissions')
export default class Permission {
@PrimaryGeneratedColumn('increment')
id: number;
@Column({ type: 'varchar', unique: true })
name: string;
@Column({ type: 'varchar', nullable: true })
description: string;
@ManyToMany(() => Admin, (admin) => admin.permissions)
@JoinTable()
admins: Admin[];
}

View File

@ -0,0 +1,8 @@
import { CreateDateColumn, UpdateDateColumn } from 'typeorm';
export abstract class Timestamp {
@CreateDateColumn({ type: 'timestamp', name: 'created_at' })
created_at: Date;
@UpdateDateColumn({ type: 'timestamp', name: 'updated_at' })
updated_at: Date;
}

View File

@ -0,0 +1,66 @@
// auth.middleware.ts
import Admin from '@/modules/admins/entities/admin.entity';
import { AdminsService } from '@/modules/admins/services/admins.service';
import { AuthService } from '@/modules/auth/auth.service';
import AppResponse from '@/response/app-response';
import {
ForbiddenException,
HttpStatus,
Injectable,
NestMiddleware,
UnauthorizedException,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { NextFunction, Request, Response } from 'express';
import { PermissionService } from '../services/permission.service';
@Injectable()
export class AuthorizationMiddleware implements NestMiddleware {
constructor(
private readonly jwtService: JwtService,
private readonly adminService: AdminsService,
private readonly authService: AuthService,
private readonly permissionService: PermissionService,
) {}
async use(req: Request, res: Response, next: NextFunction) {
const admin: Admin | undefined = ({} = req['admin']);
if (!admin) {
next(
new UnauthorizedException(
AppResponse.toResponse(null, {
message: 'Un authentication',
status_code: HttpStatus.UNAUTHORIZED,
}),
),
);
return;
}
if (admin.is_system_account) {
next();
return;
}
const permissions = await this.permissionService.getPermissionsByAdmin(
admin.id,
);
if (!permissions.some((item) => item.name === req.method)) {
next(
new ForbiddenException(
AppResponse.toResponse(null, {
message: 'Forbidden',
status_code: HttpStatus.FORBIDDEN,
}),
),
);
return;
}
next();
}
}

View File

@ -0,0 +1,223 @@
import AppResponse from '@/response/app-response';
import {
BadRequestException,
HttpStatus,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import * as bcrypt from 'bcrypt';
import { Request } from 'express';
import {
FilterOperator,
FilterSuffix,
paginate,
PaginateQuery,
} from 'nestjs-paginate';
import { Column } from 'nestjs-paginate/lib/helper';
import { In, Not, Repository } from 'typeorm';
import Admin from '../entities/admin.entity';
import Permission from '../entities/permission.entity';
@Injectable()
export class AdminsService {
constructor(
@InjectRepository(Admin)
readonly adminRepo: Repository<Admin>,
@InjectRepository(Permission)
readonly permissionRepo: Repository<Permission>,
) {}
async index(query: PaginateQuery) {
const filterableColumns: {
[key in Column<Admin> | (string & {})]?:
| (FilterOperator | FilterSuffix)[]
| true;
} = {
id: true,
username: [FilterOperator.ILIKE],
fullname: [FilterOperator.ILIKE],
email: [FilterOperator.ILIKE],
};
query.filter = AppResponse.processFilters(query.filter, filterableColumns);
const data = await paginate(query, this.adminRepo, {
sortableColumns: ['id', 'username', 'email', 'created_at', 'updated_at'],
searchableColumns: ['id', 'username', 'email'],
defaultLimit: 15,
filterableColumns,
defaultSortBy: [['id', 'ASC']],
maxLimit: 100,
relations: {
permissions: true,
},
});
return AppResponse.toPagination<Admin>(data, true, Admin);
}
async delete(id: Admin['id']) {
const admin = await this.adminRepo.findOne({ where: { id } });
if (!admin)
throw new NotFoundException(
AppResponse.toResponse(false, {
message: 'Admin is not found',
status_code: HttpStatus.NOT_FOUND,
}),
);
if (admin.is_system_account) {
throw new BadRequestException(
AppResponse.toResponse(false, {
message: "Can't delete this account",
status_code: HttpStatus.BAD_REQUEST,
}),
);
}
await this.adminRepo.delete({ id: admin.id });
return AppResponse.toResponse(true, { message: 'Delete success !' });
}
async deletes(ids: Admin['id'][], request: Request) {
const curAdmin = request['admin'];
let newIds = ids;
if (curAdmin) {
newIds = ids.filter((item) => item !== curAdmin?.id);
}
const result = await this.adminRepo.delete({
id: In(ids),
is_system_account: false,
});
if (!result.affected) {
throw new BadRequestException(
AppResponse.toResponse(false, {
message: 'No items have been deleted yet.',
status_code: HttpStatus.BAD_REQUEST,
}),
);
}
return AppResponse.toResponse(true, { message: 'Delete success !' });
}
async create({
password,
...data
}: Omit<
Admin,
| 'id'
| 'created_at'
| 'updated_at'
| 'is_system_account'
| 'permissions'
| 'posts'
>) {
const hashPassword = await bcrypt.hash(password, 10);
const newAdmin = await this.adminRepo.save({
...data,
password: hashPassword,
});
if (!newAdmin)
throw new BadRequestException(
AppResponse.toResponse(false, {
message: "Can't create account",
status_code: HttpStatus.BAD_REQUEST,
}),
);
return AppResponse.toResponse(true, { message: 'Create success' });
}
async grantNewPassword(id: Admin['id'], password: string, request: Request) {
const hashPassword = await bcrypt.hash(password, 10);
const admin = request['admin'];
if (admin && admin.id == id) {
throw new BadRequestException(
AppResponse.toResponse(false, {
message:
'This future not support for this account, Please use change password !',
status_code: HttpStatus.BAD_REQUEST,
}),
);
}
const result = await this.adminRepo.update(id, {
password: hashPassword,
});
if (!result)
throw new BadRequestException(
AppResponse.toResponse(false, {
message: "Can't create account",
status_code: HttpStatus.BAD_REQUEST,
}),
);
return AppResponse.toResponse(true, { message: 'Grant success' });
}
async update(id: Admin['id'], { permissions, ...data }: Partial<Admin>) {
try {
const admin = await this.adminRepo.findOne({ where: { id } });
if (!admin)
throw new NotFoundException(
AppResponse.toResponse(false, {
message: 'Admin is not found',
status_code: HttpStatus.NOT_FOUND,
}),
);
if (data.email) {
const isDuplicateAdmin = await this.adminRepo.findOne({
where: { email: data.email, id: Not(id) },
});
if (isDuplicateAdmin) {
throw new BadRequestException(
AppResponse.toResponse(false, {
message: 'Email is already exits!',
status_code: HttpStatus.BAD_REQUEST,
}),
);
}
}
if (permissions) {
const permissionIds = permissions.map((p) => p.id);
const permissionsData = await this.permissionRepo.findBy({
id: In(permissionIds),
});
admin.permissions = permissionsData;
}
if (data.fullname) {
admin.fullname = data.fullname;
}
await this.adminRepo.save(admin);
return AppResponse.toResponse(true, { message: 'Update success !' });
} catch (error) {
throw new BadRequestException(
AppResponse.toResponse(null, {
message: error.message,
status_code: HttpStatus.INTERNAL_SERVER_ERROR,
}),
);
}
}
}

View File

@ -0,0 +1,53 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import {
FilterOperator,
FilterSuffix,
paginate,
PaginateQuery,
} from 'nestjs-paginate';
import { Column } from 'nestjs-paginate/lib/helper';
import AppResponse from '@/response/app-response';
import Permission from '../entities/permission.entity';
@Injectable()
export class PermissionService {
constructor(
@InjectRepository(Permission)
readonly permissionRepo: Repository<Permission>,
) {}
async getPermissionsByAdmin(adminId: number): Promise<Permission[]> {
return await this.permissionRepo
.createQueryBuilder('permission')
.innerJoin('permission.admins', 'admin')
.where('admin.id = :adminId', { adminId })
.getMany();
}
async index(query: PaginateQuery) {
const filterableColumns: {
[key in Column<Permission> | (string & {})]?:
| (FilterOperator | FilterSuffix)[]
| true;
} = {
id: true,
created_at: true,
updated_at: true,
};
query.filter = AppResponse.processFilters(query.filter, filterableColumns);
const data = await paginate(query, this.permissionRepo, {
sortableColumns: ['id'],
searchableColumns: ['id'],
defaultLimit: 15,
filterableColumns,
defaultSortBy: [['id', 'DESC']],
maxLimit: 100,
});
return AppResponse.toPagination<Permission>(data, true, Permission);
}
}

View File

@ -0,0 +1,10 @@
import { HasExistedValidator } from '@/system/validators/has-existed.validator';
import { IsUniqueValidator } from '@/system/validators/is-unique.validator';
import { Module } from '@nestjs/common';
@Module({
imports: [],
providers: [IsUniqueValidator, HasExistedValidator],
exports: [IsUniqueValidator, HasExistedValidator],
})
export class AppValidatorsModule {}

View File

@ -0,0 +1,47 @@
import {
Controller,
Post,
Body,
Req,
UseGuards,
Res,
Get,
} from '@nestjs/common';
import { AuthService } from './auth.service';
import { LoginDto } from './dto/login.dto';
import { Request, Response } from 'express';
import { ChangePasswordDto } from './dto/change-password.dto';
@Controller('admin/auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@Post('login')
async login(
@Body() loginDto: LoginDto,
@Res({ passthrough: true }) response: Response,
) {
return this.authService
.validateAdmin(loginDto.username, loginDto.password)
.then((admin) => this.authService.login(admin, response));
}
@Post('logout')
async logout(@Res({ passthrough: true }) response: Response) {
return await this.authService.logout(response);
}
@Post('change-password')
async changePassword(
@Res({ passthrough: true }) response: Response,
@Req() request: Request,
@Body() data: ChangePasswordDto,
) {
return await this.authService.changePassword(data, request, response);
}
@Get('me')
async me(@Req() request: Request) {
return this.authService.me(request);
}
}

View File

@ -0,0 +1,20 @@
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { AdminsModule } from '../admins/admins.module';
@Module({
imports: [
JwtModule.register({
global: true,
secret: process.env.SECRET_KEY,
signOptions: { expiresIn: '15m' },
}),
AdminsModule,
],
controllers: [AuthController],
providers: [AuthService],
exports: [AuthService],
})
export class AuthModule {}

View File

@ -0,0 +1,122 @@
import {
BadRequestException,
HttpStatus,
Injectable,
Res,
UnauthorizedException,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import * as bcrypt from 'bcrypt';
import { AdminsService } from '../admins/services/admins.service';
import Admin from '../admins/entities/admin.entity';
import { plainToClass } from 'class-transformer';
import { Request, Response } from 'express';
import { Constant } from './ultils/constant';
import { ChangePasswordDto } from './dto/change-password.dto';
import AppResponse from '@/response/app-response';
@Injectable()
export class AuthService {
constructor(
private jwtService: JwtService,
private readonly adminService: AdminsService,
) {}
async validateAdmin(
username: string,
password: string,
message = 'Username or Password is cornet',
) {
const user = await this.adminService.adminRepo.findOne({
where: { username },
});
if (!user || !(await bcrypt.compare(password, user.password))) {
throw new UnauthorizedException(
AppResponse.toResponse(null, {
message,
status_code: HttpStatus.UNAUTHORIZED,
}),
);
}
return user;
}
async login(user: Admin, response: Response) {
const payload = { _id: user.id };
const accessToken = this.jwtService.sign(payload, { expiresIn: '15m' });
const refreshToken = this.jwtService.sign(
{ refresh_key: true, ...payload },
{ expiresIn: '7d' },
);
response.cookie(Constant.ACCESS_TOKEN, accessToken, {
httpOnly: true,
maxAge: 15 * 60 * 1000,
sameSite: 'lax',
});
response.cookie(Constant.REFRESH_TOKEN, refreshToken, {
httpOnly: true,
maxAge: 7 * 24 * 60 * 60 * 1000,
sameSite: 'lax',
});
return AppResponse.toResponse(true, { message: 'Login Success' });
}
async logout(@Res() response: Response) {
response.cookie(Constant.ACCESS_TOKEN, '', {
httpOnly: true,
maxAge: 0,
sameSite: 'lax',
});
response.cookie(Constant.REFRESH_TOKEN, '', {
httpOnly: true,
maxAge: 0,
sameSite: 'lax',
});
return AppResponse.toResponse(true, { message: 'Logout Success' });
}
async changePassword(
data: ChangePasswordDto,
request: Request,
response: Response,
) {
const admin = request['admin'];
if (!admin) throw new UnauthorizedException(AppResponse.toResponse(false));
await this.validateAdmin(
admin.username,
data.password,
'Password is invalid !',
);
const newPassword = await bcrypt.hash(data.newPassword, 10);
const result = await this.adminService.adminRepo.update(admin.id, {
password: newPassword,
});
if (!result.affected)
throw new BadRequestException(AppResponse.toResponse(false));
await this.logout(response);
return AppResponse.toResponse(true, {
message: 'Change password success. Please re-login !',
});
}
async me(request: Request) {
const admin = request['admin'];
if (!admin) throw new UnauthorizedException(AppResponse.toResponse(null));
return AppResponse.toResponse(admin);
}
}

View File

@ -0,0 +1,11 @@
import { IsNotEmpty, IsString } from 'class-validator';
export class ChangePasswordDto {
@IsNotEmpty()
@IsString()
newPassword: string;
@IsNotEmpty()
@IsString()
password: string;
}

View File

@ -0,0 +1,11 @@
import { IsNotEmpty, IsString } from 'class-validator';
export class LoginDto {
@IsNotEmpty()
@IsString()
username: string;
@IsNotEmpty()
@IsString()
password: string;
}

View File

@ -0,0 +1,98 @@
// auth.middleware.ts
import {
ForbiddenException,
HttpStatus,
Injectable,
NestMiddleware,
UnauthorizedException,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { NextFunction, Request, Response } from 'express';
import { AdminsService } from '@/modules/admins/services/admins.service';
import { AuthService } from '@/modules/auth/auth.service';
import { plainToClass } from 'class-transformer';
import Admin from '@/modules/admins/entities/admin.entity';
import AppResponse from '@/response/app-response';
@Injectable()
export class AuthenticationMiddleware implements NestMiddleware {
constructor(
private readonly jwtService: JwtService,
private readonly adminService: AdminsService,
private readonly authService: AuthService,
) {}
async validate(result: Record<string, any>, next: NextFunction) {
const admin = await this.adminService.adminRepo.findOne({
where: { id: result?._id },
});
if (!admin) {
return next(
new UnauthorizedException(
AppResponse.toResponse(null, {
message: 'User is invalid',
status_code: HttpStatus.UNAUTHORIZED,
bonus: { redirect: true },
}),
),
);
}
return admin;
}
async use(req: Request, res: Response, next: NextFunction) {
const { access_token, refresh_token }: Record<string, any> = req.cookies;
if (!access_token && !refresh_token) {
return next(
new UnauthorizedException(
AppResponse.toResponse(false, {
message: 'Please login to app',
status_code: HttpStatus.UNAUTHORIZED,
bonus: { redirect: true },
}),
),
);
}
try {
const result = await this.jwtService.verify(access_token);
if (result.refresh_key || !result?._id) {
return next(
new ForbiddenException(
AppResponse.toResponse(null, {
message: 'Token is invalid',
status_code: HttpStatus.FORBIDDEN,
}),
),
);
}
const admin = await this.validate(result, next);
req['admin'] = plainToClass(Admin, admin);
next();
} catch (error) {
try {
const result = await this.jwtService.verify(refresh_token);
const admin = (await this.validate(result, next)) as Admin;
await this.authService.login(admin, res);
req['admin'] = plainToClass(Admin, admin);
next();
} catch (error) {
return next(
new UnauthorizedException(
AppResponse.toResponse(null, {
message: 'Token is expired',
status_code: HttpStatus.UNAUTHORIZED,
}),
),
);
}
}
}
}

View File

@ -0,0 +1,4 @@
export class Constant {
public static ACCESS_TOKEN = 'access_token';
public static REFRESH_TOKEN = 'refresh_token';
}

View File

@ -5,8 +5,9 @@ export class ClientUpdateBidDto {
@IsOptional() @IsOptional()
lot_id: string; lot_id: string;
@IsOptional()
@IsString() @IsString()
close_time: string; close_time: string | null;
@IsString() @IsString()
@IsOptional() @IsOptional()

View File

@ -51,6 +51,7 @@ export class BidsService {
model: true, model: true,
lot_id: true, lot_id: true,
close_time: true, close_time: true,
name: true,
}; };
query.filter = AppResponse.processFilters(query.filter, filterableColumns); query.filter = AppResponse.processFilters(query.filter, filterableColumns);
@ -64,8 +65,9 @@ export class BidsService {
'lot_id', 'lot_id',
'max_price', 'max_price',
'status', 'status',
'name',
], ],
searchableColumns: ['id', 'status', 'model', 'lot_id'], searchableColumns: ['id', 'status', 'model', 'lot_id', 'name'],
defaultLimit: 15, defaultLimit: 15,
filterableColumns, filterableColumns,
defaultSortBy: [['id', 'DESC']], defaultSortBy: [['id', 'DESC']],
@ -185,7 +187,10 @@ export class BidsService {
return AppResponse.toResponse(true); return AppResponse.toResponse(true);
} }
async clientUpdate(id: Bid['id'], data: ClientUpdateBidDto) { async clientUpdate(
id: Bid['id'],
{ close_time, ...data }: ClientUpdateBidDto,
) {
const bid = await this.bidsRepo.findOne({ where: { id } }); const bid = await this.bidsRepo.findOne({ where: { id } });
if (!bid) if (!bid)
@ -198,14 +203,28 @@ export class BidsService {
if (!bid.close_time && !bid.start_bid_time) { if (!bid.close_time && !bid.start_bid_time) {
// bid.start_bid_time = new Date().toUTCString(); // bid.start_bid_time = new Date().toUTCString();
bid.start_bid_time = subtractMinutes(data.close_time, 5); bid.start_bid_time = subtractMinutes(close_time, 5);
} }
if (data.current_price > bid.max_price + bid.plus_price) { if (
data.current_price >= bid.max_price + bid.plus_price ||
(bid.close_time && isTimeReached(bid.close_time))
) {
console.log({
a: data.current_price >= bid.max_price + bid.plus_price,
b: bid.close_time && !close_time,
c: bid.close_time && isTimeReached(bid.close_time),
});
bid.status = 'out-bid'; bid.status = 'out-bid';
} }
console.log('Update ' + id); if (
close_time &&
new Date(close_time).getTime() > new Date(bid.close_time).getTime()
) {
bid.close_time = close_time;
}
const result = await this.bidsRepo.save({ const result = await this.bidsRepo.save({
...bid, ...bid,

View File

@ -0,0 +1,24 @@
import AppResponse from '@/response/app-response';
import {
Injectable,
ExecutionContext,
HttpException,
HttpStatus,
} from '@nestjs/common';
import { ThrottlerGuard, ThrottlerLimitDetail } from '@nestjs/throttler';
@Injectable()
export class RateLimitGuard extends ThrottlerGuard {
protected async throwThrottlingException(
context: ExecutionContext,
throttlerLimitDetail: ThrottlerLimitDetail,
): Promise<void> {
throw new HttpException(
AppResponse.toResponse(null, {
message: 'To many request',
status_code: HttpStatus.TOO_MANY_REQUESTS,
}),
HttpStatus.TOO_MANY_REQUESTS,
);
}
}

View File

@ -0,0 +1,26 @@
import AppResponse from '@/response/app-response';
import {
CanActivate,
ExecutionContext,
ForbiddenException,
HttpStatus,
Injectable,
} from '@nestjs/common';
@Injectable()
export class SystemAccountGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
if (!request.admin || !request.admin.is_system_account) {
throw new ForbiddenException(
AppResponse.toResponse(null, {
message: 'You are not allowed to access this resource.',
status_code: HttpStatus.FORBIDDEN,
}),
);
}
return true;
}
}

View File

@ -0,0 +1,29 @@
import { RequestMethod } from '@nestjs/common';
import { RouteInfo } from '@nestjs/common/interfaces';
export const excludeAuth = [
{
path: 'admin/auth/login',
method: RequestMethod.POST,
},
{
path: 'admin/auth/forgot-password',
method: RequestMethod.POST,
},
] as (string | RouteInfo)[];
export const excludeAuthor = [
...excludeAuth,
{
path: 'admin/auth/change-password',
method: RequestMethod.POST,
},
{
path: 'admin/auth/logout',
method: RequestMethod.POST,
},
{
path: 'admin/auth/me',
method: RequestMethod.GET,
},
] as (string | RouteInfo)[];

View File

@ -0,0 +1,24 @@
import { registerDecorator, ValidationOptions } from 'class-validator';
import { HasExistedValidator } from '../has-existed.validator';
export type IsExitedInput = {
tableName: string;
column: string;
queryOption?: Record<string, any>;
message?: string;
each?: boolean;
update?: boolean;
};
export function HasExisted(options: IsExitedInput, validationOptions?: ValidationOptions) {
return function (object: any, propertyName: string) {
registerDecorator({
name: 'has-exited',
target: object.constructor,
propertyName: propertyName,
options: validationOptions,
constraints: [options],
validator: HasExistedValidator,
});
};
}

View File

@ -0,0 +1,21 @@
import { registerDecorator, ValidationOptions } from 'class-validator';
import { IsUniqueValidator } from '../is-unique.validator';
export type IsUniqueInput = {
tableName: string;
column: string;
queryOption?: Record<string, any>;
};
export function IsUnique(options: IsUniqueInput, validationOptions?: ValidationOptions) {
return function (object: any, propertyName: string) {
registerDecorator({
name: 'is-unique',
target: object.constructor,
propertyName: propertyName,
options: validationOptions,
constraints: [options],
validator: IsUniqueValidator,
});
};
}

View File

@ -0,0 +1,40 @@
import { Injectable } from '@nestjs/common';
import { ValidationArguments, ValidatorConstraint, ValidatorConstraintInterface } from 'class-validator';
import { EntityManager, In } from 'typeorm';
import { IsExitedInput } from './decorators/has-existed';
@Injectable()
@ValidatorConstraint({ name: 'isUnique', async: true })
export class HasExistedValidator implements ValidatorConstraintInterface {
constructor(private readonly entityManager: EntityManager) {}
async validate(value: any, args: ValidationArguments) {
const { column, tableName, queryOption, each }: IsExitedInput = args.constraints[0];
let result = null;
if (!each) {
result = await this.entityManager
.getRepository(tableName)
.createQueryBuilder(tableName)
.where({ [column]: value, ...(queryOption || {}) })
.getOne();
} else {
const response = await this.entityManager
.getRepository(tableName)
.createQueryBuilder(tableName)
.where({ [column]: In(value), ...(queryOption || {}) })
.getMany();
result = response.length === value.length;
}
return !!result;
}
defaultMessage(args: ValidationArguments) {
const { column, message }: IsExitedInput = args.constraints[0];
return message || `${column} $value is not found`;
}
}

View File

@ -0,0 +1,28 @@
import { Injectable } from '@nestjs/common';
import { ValidationArguments, ValidatorConstraint, ValidatorConstraintInterface } from 'class-validator';
import { EntityManager } from 'typeorm';
import { IsUniqueInput } from './decorators/is-unique';
@Injectable()
@ValidatorConstraint({ name: 'isUnique', async: true })
export class IsUniqueValidator implements ValidatorConstraintInterface {
constructor(private readonly entityManager: EntityManager) {}
async validate(value: string, args: ValidationArguments) {
const { column, tableName, queryOption }: IsUniqueInput = args.constraints[0];
const result = await this.entityManager
.getRepository(tableName)
.createQueryBuilder(tableName)
.where({ [column]: value, ...(queryOption || {}) })
.getOne();
return !result;
}
defaultMessage(args: ValidationArguments) {
const { column }: IsUniqueInput = args.constraints[0];
return `${column} $value already exists`;
}
}

View File

@ -16,6 +16,9 @@
"noImplicitAny": false, "noImplicitAny": false,
"strictBindCallApply": false, "strictBindCallApply": false,
"forceConsistentCasingInFileNames": false, "forceConsistentCasingInFileNames": false,
"noFallthroughCasesInSwitch": false "noFallthroughCasesInSwitch": false,
"paths": {
"@/*": ["src/*"]
}
} }
} }

View File

@ -4,7 +4,7 @@ import { io } from 'socket.io-client';
import { createApiBid, createBidProduct, deleteProfile, shouldUpdateProductTab } from './service/app-service.js'; import { createApiBid, createBidProduct, deleteProfile, shouldUpdateProductTab } from './service/app-service.js';
import browser from './system/browser.js'; import browser from './system/browser.js';
import configs from './system/config.js'; import configs from './system/config.js';
import { isTimeReached, safeClosePage } from './system/utils.js'; import { delay, isTimeReached, safeClosePage } from './system/utils.js';
let MANAGER_BIDS = []; let MANAGER_BIDS = [];
@ -58,10 +58,10 @@ const handleUpdateProductTabs = (data) => {
// const productTabs = _.flatMap(MANAGER_BIDS, 'children'); // const productTabs = _.flatMap(MANAGER_BIDS, 'children');
// for (const productTab of productTabs) { // for (const productTab of productTabs) {
// // Tìm parent context nếu chưa có
// if (!productTab.parent_browser_context) { // if (!productTab.parent_browser_context) {
// const parent = _.find(MANAGER_BIDS, { id: productTab.web_bid.id }); // const parent = _.find(MANAGER_BIDS, { id: productTab.web_bid.id });
// productTab.parent_browser_context = parent?.browser_context;
// productTab.parent_browser_context = parent.browser_context;
// if (!productTab.parent_browser_context) { // if (!productTab.parent_browser_context) {
// console.log(`🔄 Waiting for parent process to start... (Product ID: ${productTab.id})`); // console.log(`🔄 Waiting for parent process to start... (Product ID: ${productTab.id})`);
@ -69,57 +69,37 @@ const handleUpdateProductTabs = (data) => {
// } // }
// } // }
// if (!productTab.first_bid) { // // Kết nối Puppeteer nếu chưa có page_context
// console.log(`🎯 Tracking out-bid event for Product ID: ${productTab.id}`);
// const updatedAt = new Date(productTab.updated_at).getTime();
// const now = Date.now();
// if (!productTab.page_context) {
// await productTab.puppeteer_connect();
// }
// if (productTab.page_context.url() !== productTab.url) {
// await productTab.gotoLink();
// }
// if (now - updatedAt < ONE_MINUTE) {
// console.log(`⏳ Product ID: ${productTab.id} was updated recently. Skipping update.`);
// }
// await productTab.update();
// console.log(`🔄 Updating Product ID: ${productTab.id}...`);
// continue;
// }
// if (productTab.start_bid_time && !isTimeReached(productTab.start_bid_time)) {
// console.log(`⏳ Not yet time to bid. Skipping Product ID: ${productTab.id}`);
// const updatedAt = new Date(productTab.updated_at).getTime();
// const now = Date.now();
// if (!productTab.page_context) {
// await productTab.puppeteer_connect();
// }
// if (productTab.page_context.url() !== productTab.url) {
// await productTab.gotoLink();
// }
// if (now - updatedAt < ONE_MINUTE) {
// console.log(`⏳ Product ID: ${productTab.id} was updated recently. Skipping update.`);
// }
// await productTab.update();
// continue;
// }
// if (!productTab.page_context) { // if (!productTab.page_context) {
// console.log(`🔌 Connecting to page for Product ID: ${productTab.id}`); // console.log(`🔌 Connecting to page for Product ID: ${productTab.id}`);
// await productTab.puppeteer_connect(); // await productTab.puppeteer_connect();
// } // }
// // Nếu URL thay đổi, điều hướng đến URL mới
// if (productTab.page_context.url() !== productTab.url) {
// await productTab.gotoLink();
// }
// // Kiểm tra nếu cần cập nhật trước khi gọi update()
// if (shouldUpdateProductTab(productTab)) {
// console.log(`🔄 Updating Product ID: ${productTab.id}...`);
// await productTab.update();
// } else {
// console.log(`⏳ Product ID: ${productTab.id} was updated recently. Skipping update.`);
// }
// // Nếu chưa có first_bid (trạng thái chưa đặt giá)
// if (!productTab.first_bid) {
// console.log(`🎯 Tracking out-bid event for Product ID: ${productTab.id}`);
// continue;
// }
// // Nếu chưa đến giờ bid
// if (productTab.start_bid_time && !isTimeReached(productTab.start_bid_time)) {
// console.log(`⏳ Not yet time to bid. Skipping Product ID: ${productTab.id}`);
// continue;
// }
// console.log(`🚀 Executing action for Product ID: ${productTab.id}`); // console.log(`🚀 Executing action for Product ID: ${productTab.id}`);
// await productTab.action(); // await productTab.action();
// } // }
@ -127,22 +107,29 @@ const handleUpdateProductTabs = (data) => {
// }; // };
const tracking = async () => { const tracking = async () => {
if (_INTERVAL_TRACKING_ID) { console.log('🚀 Tracking process started...');
clearInterval(_INTERVAL_TRACKING_ID);
_INTERVAL_TRACKING_ID = null;
}
_INTERVAL_TRACKING_ID = setInterval(async () => { while (true) {
console.log('🔍 Scanning active bids...');
const productTabs = _.flatMap(MANAGER_BIDS, 'children'); const productTabs = _.flatMap(MANAGER_BIDS, 'children');
for (const apiBid of MANAGER_BIDS) {
if (apiBid.page_context) continue;
console.log(`🎧 Listening to events for API Bid ID: ${apiBid.id}`);
await apiBid.listen_events();
}
for (const productTab of productTabs) { for (const productTab of productTabs) {
console.log(`📌 Processing Product ID: ${productTab.id}`);
// Tìm parent context nếu chưa có // Tìm parent context nếu chưa có
if (!productTab.parent_browser_context) { if (!productTab.parent_browser_context) {
const parent = _.find(MANAGER_BIDS, { id: productTab.web_bid.id }); const parent = _.find(MANAGER_BIDS, { id: productTab.web_bid.id });
productTab.parent_browser_context = parent?.browser_context; productTab.parent_browser_context = parent?.browser_context;
if (!productTab.parent_browser_context) { if (!productTab.parent_browser_context) {
console.log(`🔄 Waiting for parent process to start... (Product ID: ${productTab.id})`); console.log(` Waiting for parent process to start... (Product ID: ${productTab.id})`);
continue; continue;
} }
} }
@ -155,6 +142,7 @@ const tracking = async () => {
// Nếu URL thay đổi, điều hướng đến URL mới // Nếu URL thay đổi, điều hướng đến URL mới
if (productTab.page_context.url() !== productTab.url) { if (productTab.page_context.url() !== productTab.url) {
console.log(`🔄 Redirecting to new URL for Product ID: ${productTab.id}`);
await productTab.gotoLink(); await productTab.gotoLink();
} }
@ -168,7 +156,7 @@ const tracking = async () => {
// Nếu chưa có first_bid (trạng thái chưa đặt giá) // Nếu chưa có first_bid (trạng thái chưa đặt giá)
if (!productTab.first_bid) { if (!productTab.first_bid) {
console.log(`🎯 Tracking out-bid event for Product ID: ${productTab.id}`); console.log(`🎯 Waiting for first bid for Product ID: ${productTab.id}`);
continue; continue;
} }
@ -181,81 +169,70 @@ const tracking = async () => {
console.log(`🚀 Executing action for Product ID: ${productTab.id}`); console.log(`🚀 Executing action for Product ID: ${productTab.id}`);
await productTab.action(); await productTab.action();
} }
}, configs.AUTO_TRACKING_DELAY);
console.log('🧹 Cleaning up unused tabs...');
await clearLazyTab();
console.log('📊 Tracking work status...');
workTracking();
console.log(`⏳ Waiting ${configs.AUTO_TRACKING_DELAY / 1000} seconds before the next iteration...`);
await delay(configs.AUTO_TRACKING_DELAY);
}
}; };
const clearLazyTab = async () => { const clearLazyTab = async () => {
if (_CLEAR_LAZY_TAB_ID) { if (!global.IS_CLEANING) return;
clearInterval(_CLEAR_LAZY_TAB_ID);
_CLEAR_LAZY_TAB_ID = null; if (!browser) {
console.warn('⚠️ Browser is not available or disconnected.');
return;
} }
try { try {
_CLEAR_LAZY_TAB_ID = setInterval(async () => { const pages = await browser.pages();
if (!global.IS_CLEANING) return;
if (!browser) { // Lấy danh sách URL từ flattenedArray
console.warn('⚠️ Browser is not available or disconnected.'); const activeUrls = _.flatMap(MANAGER_BIDS, (item) => [item.url, ...item.children.map((child) => child.url)]).filter(Boolean); // Lọc bỏ null hoặc undefined
clearInterval(_CLEAR_LAZY_TAB_ID);
_CLEAR_LAZY_TAB_ID = null;
return;
}
try { console.log(
const pages = await browser.pages(); '🔍 Page URLs:',
pages.map((page) => page.url()),
);
// Lấy danh sách URL từ flattenedArray for (const page of pages) {
const activeUrls = _.flatMap(MANAGER_BIDS, (item) => [item.url, ...item.children.map((child) => child.url)]).filter(Boolean); // Lọc bỏ null hoặc undefined const pageUrl = page.url();
console.log( // 🔥 Bỏ qua tab 'about:blank' hoặc tab không có URL
'🔍 Page URLs:', if (!pageUrl || pageUrl === 'about:blank') continue;
pages.map((page) => page.url()),
);
for (const page of pages) { if (!activeUrls.includes(pageUrl)) {
const pageUrl = page.url(); if (!page.isClosed() && browser.isConnected()) {
try {
// 🔥 Bỏ qua tab 'about:blank' hoặc tab không có URL await page.close();
if (!pageUrl || pageUrl === 'about:blank') continue; console.log(`🛑 Closing unused tab: ${pageUrl}`);
} catch (err) {
if (!activeUrls.includes(pageUrl)) { console.warn(`⚠️ Error closing tab ${pageUrl}:`, err.message);
if (!page.isClosed() && browser.isConnected()) {
try {
await page.close();
console.log(`🛑 Closing unused tab: ${pageUrl}`);
} catch (err) {
console.warn(`⚠️ Error closing tab ${pageUrl}:`, err.message);
}
}
} }
} }
} catch (err) {
console.error('❌ Error in clearLazyTab:', err.message);
} }
}, configs.AUTO_TRACKING_CLEANING); }
} catch (error) { } catch (err) {
console.log('CLEAR LAZY TAB ERROR: ', error.message); console.error('❌ Error in clearLazyTab:', err.message);
} }
}; };
const workTracking = () => { const workTracking = async () => {
try { try {
if (_WORK_TRACKING_ID) { const activeData = _.flatMap(MANAGER_BIDS, (item) => [item, ...item.children]);
clearInterval(_WORK_TRACKING_ID);
_WORK_TRACKING_ID = null;
}
_WORK_TRACKING_ID = setInterval(() => { for (const item of activeData) {
const activeData = _.flatMap(MANAGER_BIDS, (item) => [item, ...item.children]); if (item.page_context && !item.page_context.isClosed()) {
item.handleTakeWorkSnapshot();
for (const item of activeData) {
if (item.page_context && !item.page_context.isClosed()) {
item.handleTakeWorkSnapshot();
}
} }
}, 10000); }
} catch (error) { } catch (error) {
console.log('Loi oi day'); console.log('Lỗi rồi:', error);
} }
}; };
@ -277,7 +254,7 @@ const workTracking = () => {
handleUpdateProductTabs(data); handleUpdateProductTabs(data);
await Promise.all(MANAGER_BIDS.map((apiBid) => apiBid.listen_events())); // await Promise.all(MANAGER_BIDS.map((apiBid) => apiBid.listen_events()));
}); });
socket.on('webUpdated', async (data) => { socket.on('webUpdated', async (data) => {
@ -303,7 +280,7 @@ const workTracking = () => {
// AUTO TRACKING // AUTO TRACKING
tracking(); tracking();
clearLazyTab(); // clearLazyTab();
workTracking(); // workTracking();
})(); })();

View File

@ -1,5 +1,6 @@
import CONSTANTS from '../system/constants.js'; import CONSTANTS from '../system/constants.js';
import { takeSnapshot } from '../system/utils.js'; import { takeSnapshot } from '../system/utils.js';
import _ from 'lodash';
export class Bid { export class Bid {
type; type;
@ -14,9 +15,14 @@ export class Bid {
this.puppeteer_connect = puppeteer_connect; this.puppeteer_connect = puppeteer_connect;
} }
async handleTakeWorkSnapshot() { handleTakeWorkSnapshot = _.debounce(async () => {
if (!this.page_context) return; if (!this.page_context) return;
await takeSnapshot(this.page_context, this, 'working', CONSTANTS.TYPE_IMAGE.WORK); try {
} console.log(`✅ Page loaded. Taking snapshot for Product ID: ${this.id}`);
takeSnapshot(this.page_context, this, 'working', CONSTANTS.TYPE_IMAGE.WORK);
} catch (error) {
console.error(`❌ Error taking snapshot for Product ID: ${this.id}:`, error.message);
}
}, 500);
} }

View File

@ -17,7 +17,6 @@ export class GraysProductBid extends ProductBid {
if (!isNumber(price_value)) { if (!isNumber(price_value)) {
console.log("Can't get PRICE_VALUE ❌"); console.log("Can't get PRICE_VALUE ❌");
await takeSnapshot(page, this, 'price-value-null'); await takeSnapshot(page, this, 'price-value-null');
// await safeClosePage(this);
return { result: false, bid_price: 0 }; return { result: false, bid_price: 0 };
} }
@ -27,7 +26,6 @@ export class GraysProductBid extends ProductBid {
if (bid_price > this.max_price) { if (bid_price > this.max_price) {
console.log('PRICE BID is more than MAX_VALUE => STOP BID THIS PRODUCT ❌'); console.log('PRICE BID is more than MAX_VALUE => STOP BID THIS PRODUCT ❌');
await takeSnapshot(page, this, 'price-bid-more-than'); await takeSnapshot(page, this, 'price-bid-more-than');
// await safeClosePage(this);
await outBid(this.id); await outBid(this.id);
@ -83,7 +81,7 @@ export class GraysProductBid extends ProductBid {
} }
}; };
async isCloseProduct(page) { async isCloseProduct() {
const close_time = await this.getCloseTime(); const close_time = await this.getCloseTime();
if (!close_time) { if (!close_time) {
@ -153,24 +151,6 @@ export class GraysProductBid extends ProductBid {
} }
} }
// update = async () => {
// if (!this.page_context) return;
// const page = this.page_context;
// const close_time = await this.getCloseTime();
// const price_value = (await page.$eval('#priceValue', (el) => el.value)) || null;
// const lot_id = await page.$eval('#lotId', (el) => el.value);
// const name = (await page.$eval('#placebid-sticky > div:nth-child(2) > div > h3', (el) => el.innerText)) || null;
// const current_price =
// (await page.$eval('#biddableLot > form > div > div:nth-child(1) > div:nth-child(1) > div:nth-child(2) > div > span > span', (el) => el.innerText)) || null;
// console.log(`📌 Product Info: Lot ID: ${lot_id}, Name: ${name}, Current Price: ${current_price}, Reserve price ${price_value}`);
// this.handleUpdateBid({ lot_id, reserve_price: price_value, close_time, name, current_price: current_price ? extractNumber(current_price) : null });
// return { price_value, lot_id, name, current_price };
// };
update = async () => { update = async () => {
if (!this.page_context) return; if (!this.page_context) return;
@ -198,13 +178,16 @@ export class GraysProductBid extends ProductBid {
console.log(`📌 Product Info: Lot ID: ${lot_id}, Name: ${name}, Current Price: ${current_price}, Reserve price: ${price_value}`); console.log(`📌 Product Info: Lot ID: ${lot_id}, Name: ${name}, Current Price: ${current_price}, Reserve price: ${price_value}`);
const data = removeFalsyValues({ const data = removeFalsyValues(
lot_id, {
reserve_price: price_value, lot_id,
close_time: String(close_time), reserve_price: price_value,
name, close_time: close_time ? String(close_time) : null,
current_price: current_price ? extractNumber(current_price) : null, name,
}); current_price: current_price ? extractNumber(current_price) : null,
},
['close_time'],
);
this.handleUpdateBid(data); this.handleUpdateBid(data);
@ -223,7 +206,7 @@ export class GraysProductBid extends ProductBid {
await delay(1000); await delay(1000);
const { close_time, ...isCloseProduct } = await this.isCloseProduct(page); const { close_time, ...isCloseProduct } = await this.isCloseProduct();
if (isCloseProduct.result) { if (isCloseProduct.result) {
console.log('❌ The product is closed, cannot place a bid.'); console.log('❌ The product is closed, cannot place a bid.');
return; return;

View File

@ -135,14 +135,34 @@ export class ProductBid extends Bid {
return true; return true;
} }
// async gotoLink() {
// const page = this.page_context;
// console.log('🔄 Starting the bidding process...');
// await page.goto(this.url, { waitUntil: 'networkidle2' });
// console.log(`✅ Navigated to: ${this.url}`);
// await page.bringToFront();
// console.log('👀 Brought the tab to the foreground.');
// }
async gotoLink() { async gotoLink() {
const page = this.page_context; const page = this.page_context;
if (page.isClosed()) {
console.error('❌ Page has been closed, cannot navigate.');
return;
}
console.log('🔄 Starting the bidding process...'); console.log('🔄 Starting the bidding process...');
await page.goto(this.url, { waitUntil: 'networkidle2' }); try {
console.log(`✅ Navigated to: ${this.url}`); await page.goto(this.url, { waitUntil: 'networkidle2' });
console.log(`✅ Navigated to: ${this.url}`);
await page.bringToFront(); await page.bringToFront();
console.log('👀 Brought the tab to the foreground.'); console.log('👀 Brought the tab to the foreground.');
} catch (error) {
console.error('❌ Error during navigation:', error);
}
} }
} }

View File

@ -9,6 +9,7 @@ export const takeSnapshot = async (page, item, imageName, type = CONSTANTS.TYPE_
if (!page || page.isClosed()) return; if (!page || page.isClosed()) return;
try { try {
global.IS_CLEANING = false;
const baseDir = path.join(CONSTANTS.ERROR_IMAGES_PATH, item.type, String(item.id)); // Thư mục theo lot_id const baseDir = path.join(CONSTANTS.ERROR_IMAGES_PATH, item.type, String(item.id)); // Thư mục theo lot_id
const typeDir = path.join(baseDir, type); // Thư mục con theo type const typeDir = path.join(baseDir, type); // Thư mục con theo type
@ -48,6 +49,8 @@ export const takeSnapshot = async (page, item, imageName, type = CONSTANTS.TYPE_
} }
} catch (error) { } catch (error) {
console.log('Error when snapshot: ' + error.message); console.log('Error when snapshot: ' + error.message);
} finally {
global.IS_CLEANING = true;
} }
}; };