From db080ad3f221ac1e932bb5052947af0461970d68 Mon Sep 17 00:00:00 2001 From: nkhangg Date: Mon, 24 Mar 2025 15:14:14 +0700 Subject: [PATCH] update handle --- .../src/components/dashboard/working-page.tsx | 33 +- auto-bid-admin/src/layouts/private-layout.tsx | 29 +- auto-bid-admin/src/lib/table/table.tsx | 9 +- auto-bid-admin/src/pages/admins.tsx | 180 ++++++ auto-bid-admin/src/pages/bids.tsx | 23 +- auto-bid-admin/src/system/links.ts | 10 +- auto-bid-server/package-lock.json | 531 +++++++++++++++++- auto-bid-server/package.json | 13 +- auto-bid-server/src/app.module.ts | 31 +- auto-bid-server/src/config/typeorm.ts | 24 + auto-bid-server/src/main.ts | 7 +- .../1742778498009-create-admin-table.ts | 16 + .../1742778709701-create-permission-table.ts | 17 + .../src/modules/admins/admins.module.ts | 16 + .../admins/controllers/admins.controller.ts | 69 +++ .../controllers/permissions.controller.ts | 13 + .../src/modules/admins/dtos/create.dto.ts | 20 + .../src/modules/admins/dtos/deletes.dto.ts | 6 + .../admins/dtos/grant-new-password.dto.ts | 7 + .../src/modules/admins/dtos/permission.dto.ts | 11 + .../src/modules/admins/dtos/update.dto.ts | 17 + .../modules/admins/entities/admin.entity.ts | 29 + .../admins/entities/permission.entity.ts | 24 + .../src/modules/admins/entities/timestamp.ts | 8 + .../middlewares/authorization.middleware.ts | 66 +++ .../modules/admins/services/admins.service.ts | 223 ++++++++ .../admins/services/permission.service.ts | 53 ++ .../app-validators/app-validators.module.ts | 10 + .../src/modules/auth/auth.controller.ts | 47 ++ .../src/modules/auth/auth.module.ts | 20 + .../src/modules/auth/auth.service.ts | 122 ++++ .../modules/auth/dto/change-password.dto.ts | 11 + .../src/modules/auth/dto/login.dto.ts | 11 + .../middlewares/authentication.middleware.ts | 98 ++++ .../src/modules/auth/ultils/constant.ts | 4 + .../bids/dto/bid/client-update-bid.dto.ts | 3 +- .../src/modules/bids/services/bids.service.ts | 29 +- .../src/system/guards/rate-limit.guard.ts | 24 + .../src/system/guards/system-account.guard.ts | 26 + .../src/system/routes/exclude-route.ts | 29 + .../validators/decorators/has-existed.ts | 24 + .../system/validators/decorators/is-unique.ts | 21 + .../validators/has-existed.validator.ts | 40 ++ .../system/validators/is-unique.validator.ts | 28 + auto-bid-server/tsconfig.json | 5 +- auto-bid-tool/index.js | 203 +++---- auto-bid-tool/models/bid.js | 12 +- .../models/grays.com/grays-product-bid.js | 41 +- auto-bid-tool/models/product-bid.js | 28 +- auto-bid-tool/system/utils.js | 3 + 50 files changed, 2121 insertions(+), 203 deletions(-) create mode 100644 auto-bid-admin/src/pages/admins.tsx create mode 100644 auto-bid-server/src/config/typeorm.ts create mode 100644 auto-bid-server/src/migrations/1742778498009-create-admin-table.ts create mode 100644 auto-bid-server/src/migrations/1742778709701-create-permission-table.ts create mode 100644 auto-bid-server/src/modules/admins/admins.module.ts create mode 100644 auto-bid-server/src/modules/admins/controllers/admins.controller.ts create mode 100644 auto-bid-server/src/modules/admins/controllers/permissions.controller.ts create mode 100644 auto-bid-server/src/modules/admins/dtos/create.dto.ts create mode 100644 auto-bid-server/src/modules/admins/dtos/deletes.dto.ts create mode 100644 auto-bid-server/src/modules/admins/dtos/grant-new-password.dto.ts create mode 100644 auto-bid-server/src/modules/admins/dtos/permission.dto.ts create mode 100644 auto-bid-server/src/modules/admins/dtos/update.dto.ts create mode 100644 auto-bid-server/src/modules/admins/entities/admin.entity.ts create mode 100644 auto-bid-server/src/modules/admins/entities/permission.entity.ts create mode 100644 auto-bid-server/src/modules/admins/entities/timestamp.ts create mode 100644 auto-bid-server/src/modules/admins/middlewares/authorization.middleware.ts create mode 100644 auto-bid-server/src/modules/admins/services/admins.service.ts create mode 100644 auto-bid-server/src/modules/admins/services/permission.service.ts create mode 100644 auto-bid-server/src/modules/app-validators/app-validators.module.ts create mode 100644 auto-bid-server/src/modules/auth/auth.controller.ts create mode 100644 auto-bid-server/src/modules/auth/auth.module.ts create mode 100644 auto-bid-server/src/modules/auth/auth.service.ts create mode 100644 auto-bid-server/src/modules/auth/dto/change-password.dto.ts create mode 100644 auto-bid-server/src/modules/auth/dto/login.dto.ts create mode 100644 auto-bid-server/src/modules/auth/middlewares/authentication.middleware.ts create mode 100644 auto-bid-server/src/modules/auth/ultils/constant.ts create mode 100644 auto-bid-server/src/system/guards/rate-limit.guard.ts create mode 100644 auto-bid-server/src/system/guards/system-account.guard.ts create mode 100644 auto-bid-server/src/system/routes/exclude-route.ts create mode 100644 auto-bid-server/src/system/validators/decorators/has-existed.ts create mode 100644 auto-bid-server/src/system/validators/decorators/is-unique.ts create mode 100644 auto-bid-server/src/system/validators/has-existed.validator.ts create mode 100644 auto-bid-server/src/system/validators/is-unique.validator.ts diff --git a/auto-bid-admin/src/components/dashboard/working-page.tsx b/auto-bid-admin/src/components/dashboard/working-page.tsx index df1bb41..fbd2194 100644 --- a/auto-bid-admin/src/components/dashboard/working-page.tsx +++ b/auto-bid-admin/src/components/dashboard/working-page.tsx @@ -1,10 +1,10 @@ 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 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 { data: (IBid | IWebBid) & { type: string }; @@ -20,6 +20,10 @@ export default function WorkingPage({ data, socket }: IWorkingPageProps) { const [lastUpdate, setLastUpdate] = useState(new Date()); + function isIBid(obj: IBid | IWebBid): obj is IBid { + return 'name' in obj; + } + useEffect(() => { const updateImage = ({ type, id, filename }: { type: string; filename: string; id: IBid['id'] }) => { if (type == data.type && id == data.id) { @@ -49,11 +53,24 @@ export default function WorkingPage({ data, socket }: IWorkingPageProps) { /> - {data.type} + {isIBid(data) ? data.name : 'Tracking page'} + {isIBid(data) && {`Max price: $${data.max_price}`}} + {isIBid(data) && {`Current price: $${data.current_price}`}} {moment(lastUpdate).format('HH:mm:ss DD/MM/YYYY')} - + + + + diff --git a/auto-bid-admin/src/layouts/private-layout.tsx b/auto-bid-admin/src/layouts/private-layout.tsx index 5be1943..4ba4cdc 100644 --- a/auto-bid-admin/src/layouts/private-layout.tsx +++ b/auto-bid-admin/src/layouts/private-layout.tsx @@ -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 { Outlet, useLocation, useNavigate } from 'react-router'; +import { Link } from 'react-router-dom'; import { me } from '../apis/auth'; import { Logo, UserMenu } from '../components'; import Links from '../system/links'; -import { Link } from 'react-router-dom'; export default function PrivateLayout() { const navigate = useNavigate(); @@ -19,19 +19,36 @@ export default function PrivateLayout() { }, [navigate]); useEffect(() => { - // checkAuth(); + checkAuth(); }, [checkAuth]); return ( - + + + {Links.MENUS.map((menu, index) => ( + + ))} + + - + {/*
{Links.MENUS.map((menu, index) => ( @@ -48,7 +65,7 @@ export default function PrivateLayout() {
{new Date().getFullYear()} -
+
*/} diff --git a/auto-bid-admin/src/lib/table/table.tsx b/auto-bid-admin/src/lib/table/table.tsx index 6cd1d77..06a547c 100644 --- a/auto-bid-admin/src/lib/table/table.tsx +++ b/auto-bid-admin/src/lib/table/table.tsx @@ -54,6 +54,7 @@ export interface TableProps> extends M onFetched?: (response: AxiosResponse) => void; onChooses?: (chooses: R[]) => void; onSearch?: (value: ITableFilter[]) => void; + onClickRow?: (row: R) => void; } const TableIcon = ({ @@ -115,6 +116,7 @@ const Table = >({ onFetchError, onChooses, onSearch, + onClickRow, ...props }: TableProps) => { const paramsUrl = new URLSearchParams(window.location.search); @@ -507,6 +509,7 @@ const Table = >({ handleSetChoosesData(newDataChooses); }, + // eslint-disable-next-line react-hooks/exhaustive-deps [rowsData, chooses, handleSetChoosesData], ); @@ -576,10 +579,6 @@ const Table = >({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [options]); - // useEffect(() => { - // console.log('filter', filter); - // }, [filter]); - useEffect(() => { if (chooses.length < rowsData.length) { setCheckedAll(false); @@ -691,7 +690,7 @@ const Table = >({ {rowsData.length > 0 && rowsData.map((row, index) => ( - + onClickRow(row) : undefined} key={row[rowKey]}> {showChooses && ( {chooseOptions?.renderBody ? ( diff --git a/auto-bid-admin/src/pages/admins.tsx b/auto-bid-admin/src/pages/admins.tsx new file mode 100644 index 0000000..e1cc986 --- /dev/null +++ b/auto-bid-admin/src/pages/admins.tsx @@ -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 = useRef({}); + + const [adminModelOpened, adminModel] = useDisclosure(false); + const [grantOpened, grantModel] = useDisclosure(false); + const [clickData, setClickData] = useState(null); + + const columns: IColumn[] = [ + { + 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 {formatTime(row.created_at)}; + }, + }, + { + key: 'updated_at', + title: 'Update at', + typeFilter: 'none', + renderRow(row) { + return {formatTime(row.updated_at)}; + }, + }, + ]; + + 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 ( + { + 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: Action, + body: (row) => { + return ( + + + + + + + + + + + { + setClickData(row); + adminModel.open(); + }} + leftSection={} + > + Edit + + { + setClickData(row); + grantModel.open(); + }} + leftSection={} + > + Grant new password + + handleDelete(row)} leftSection={}> + Delete + + + + ); + }, + }} + rowKey="id" + /> + ); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( + + {table} + + { + setClickData(null); + adminModel.close(); + }} + /> + + { + setClickData(null); + grantModel.close(); + }} + /> + + ); +} diff --git a/auto-bid-admin/src/pages/bids.tsx b/auto-bid-admin/src/pages/bids.tsx index 14d8af0..3af0acb 100644 --- a/auto-bid-admin/src/pages/bids.tsx +++ b/auto-bid-admin/src/pages/bids.tsx @@ -8,9 +8,9 @@ import { BidModal, ShowHistoriesBidGraysApiModal, ShowHistoriesModal } from '../ import Table from '../lib/table/table'; import { IColumn, TRefTableFn } from '../lib/table/type'; import { useConfirmStore } from '../lib/zustand/use-confirm'; +import { mappingStatusColors } from '../system/constants'; import { IBid } from '../system/type'; import { formatTime } from '../utils'; -import { mappingStatusColors } from '../system/constants'; export default function Bids() { const refTableFn: TRefTableFn = useRef({}); @@ -108,15 +108,6 @@ export default function Bids() { ); }, }, - - // { - // key: 'updated_at', - // title: 'Update at', - // typeFilter: 'none', - // renderRow(row) { - // return {formatTime(row.updated_at)}; - // }, - // }, ]; const handleDelete = (bid: IBid) => { @@ -156,6 +147,14 @@ export default function Bids() { const table = useMemo(() => { return (
{ + window.open(row.url, '_blank'); + }} + tableChildProps={{ + trbody: { + className: 'cursor-pointer', + }, + }} actionsOptions={{ actions: [ { @@ -217,14 +216,14 @@ export default function Bids() { return ( - + e.stopPropagation()} className="flex w-full items-center justify-center"> - + e.stopPropagation()}> { setClickData(row); diff --git a/auto-bid-admin/src/system/links.ts b/auto-bid-admin/src/system/links.ts index 0696776..f22bff5 100644 --- a/auto-bid-admin/src/system/links.ts +++ b/auto-bid-admin/src/system/links.ts @@ -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 WebBids from '../pages/web-bids'; import SendMessageHistories from '../pages/send-message-histories'; +import Admins from '../pages/admins'; export default class Links { public static DASHBOARD = '/dashboard'; public static BIDS = '/bids'; public static WEBS = '/webs'; public static OUT_BIDS_LOG = '/out-bids-log'; public static SEND_MESSAGE_HISTORIES = '/send-message-histories'; + public static ADMINS = '/admins'; public static HOME = '/'; public static LOGIN = '/login'; @@ -19,6 +21,12 @@ export default class Links { icon: IconHome2, element: Dashboard, }, + { + path: this.ADMINS, + title: 'Admins', + icon: IconUserCheck, + element: Admins, + }, { path: this.WEBS, title: 'Webs', diff --git a/auto-bid-server/package-lock.json b/auto-bid-server/package-lock.json index c252b19..a497cd6 100644 --- a/auto-bid-server/package-lock.json +++ b/auto-bid-server/package-lock.json @@ -13,14 +13,18 @@ "@nestjs/config": "^4.0.1", "@nestjs/core": "^10.0.0", "@nestjs/event-emitter": "^3.0.1", + "@nestjs/jwt": "^11.0.0", "@nestjs/mapped-types": "*", "@nestjs/platform-express": "^10.4.15", "@nestjs/platform-socket.io": "^11.0.11", + "@nestjs/throttler": "^6.4.0", "@nestjs/typeorm": "^11.0.0", "@nestjs/websockets": "^11.0.11", "axios": "^1.8.3", + "bcrypt": "^5.1.1", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", + "cookie-parser": "^1.4.7", "dayjs": "^1.11.13", "multer": "^1.4.5-lts.1", "mysql2": "^3.13.0", @@ -34,6 +38,8 @@ "@nestjs/cli": "^10.0.0", "@nestjs/schematics": "^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/jest": "^29.5.2", "@types/multer": "^1.4.12", @@ -2076,6 +2082,50 @@ "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": { "version": "0.15.1", "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.15.1.tgz", @@ -2309,6 +2359,19 @@ "@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": { "version": "2.1.0", "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": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/@nestjs/typeorm/-/typeorm-11.0.0.tgz", @@ -2718,6 +2792,16 @@ "@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": { "version": "1.19.5", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", @@ -2739,6 +2823,16 @@ "@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": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", @@ -2872,6 +2966,15 @@ "dev": true, "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": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", @@ -3383,6 +3486,12 @@ "dev": true, "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": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", @@ -3439,6 +3548,18 @@ "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": { "version": "8.12.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", @@ -3600,6 +3721,40 @@ "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", "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": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", @@ -3858,6 +4013,20 @@ "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": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -4040,6 +4209,12 @@ "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": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -4213,6 +4388,15 @@ "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": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", @@ -4415,6 +4599,15 @@ "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": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -4468,7 +4661,6 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, "license": "MIT" }, "node_modules/concat-stream": { @@ -4492,6 +4684,12 @@ "integrity": "sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==", "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": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -4529,6 +4727,28 @@ "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": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", @@ -4726,6 +4946,12 @@ "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": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", @@ -4887,6 +5113,15 @@ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "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": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -6042,6 +6277,36 @@ "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": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.6.tgz", @@ -6053,7 +6318,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, "license": "ISC" }, "node_modules/fsevents": { @@ -6080,6 +6344,33 @@ "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": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", @@ -6345,6 +6636,12 @@ "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": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -6390,6 +6687,19 @@ "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": { "version": "2.1.0", "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", "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.", - "dev": true, "license": "ISC", "dependencies": { "once": "^1.3.0", @@ -7664,6 +7973,49 @@ "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": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -7785,6 +8137,42 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "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": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -7799,6 +8187,12 @@ "dev": true, "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": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -8063,6 +8457,37 @@ "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": { "version": "0.5.6", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", @@ -8205,6 +8630,12 @@ "dev": true, "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": { "version": "1.11.0", "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz", @@ -8249,6 +8680,21 @@ "dev": true, "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": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -8272,6 +8718,19 @@ "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": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -8328,7 +8787,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -8505,7 +8963,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -9155,7 +9612,6 @@ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, "license": "ISC", "dependencies": { "glob": "^7.1.3" @@ -9171,7 +9627,6 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -9183,7 +9638,6 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", @@ -9204,7 +9658,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -9472,6 +9925,12 @@ "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": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", @@ -10145,6 +10604,50 @@ "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": { "version": "5.39.0", "resolved": "https://registry.npmjs.org/terser/-/terser-5.39.0.tgz", @@ -11107,6 +11610,15 @@ "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": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -11154,7 +11666,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, "license": "ISC" }, "node_modules/write-file-atomic": { diff --git a/auto-bid-server/package.json b/auto-bid-server/package.json index b32c0cb..be1927a 100644 --- a/auto-bid-server/package.json +++ b/auto-bid-server/package.json @@ -17,21 +17,30 @@ "test:watch": "jest --watch", "test:cov": "jest --coverage", "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": { "@nestjs/common": "^10.0.0", "@nestjs/config": "^4.0.1", "@nestjs/core": "^10.0.0", "@nestjs/event-emitter": "^3.0.1", + "@nestjs/jwt": "^11.0.0", "@nestjs/mapped-types": "*", "@nestjs/platform-express": "^10.4.15", "@nestjs/platform-socket.io": "^11.0.11", + "@nestjs/throttler": "^6.4.0", "@nestjs/typeorm": "^11.0.0", "@nestjs/websockets": "^11.0.11", "axios": "^1.8.3", + "bcrypt": "^5.1.1", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", + "cookie-parser": "^1.4.7", "dayjs": "^1.11.13", "multer": "^1.4.5-lts.1", "mysql2": "^3.13.0", @@ -45,6 +54,8 @@ "@nestjs/cli": "^10.0.0", "@nestjs/schematics": "^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/jest": "^29.5.2", "@types/multer": "^1.4.12", diff --git a/auto-bid-server/src/app.module.ts b/auto-bid-server/src/app.module.ts index c933c83..da1f4d6 100644 --- a/auto-bid-server/src/app.module.ts +++ b/auto-bid-server/src/app.module.ts @@ -1,11 +1,36 @@ -import { Module } from '@nestjs/common'; +import { MiddlewareConsumer, Module, RequestMethod } from '@nestjs/common'; import { BidsModule } from './modules/bids/bids.module'; import { DatabasesModule } from './modules/databases/databases.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({ - imports: [BidsModule, DatabasesModule, AppConfigsModule], + imports: [ + BidsModule, + DatabasesModule, + AppConfigsModule, + AppValidatorsModule, + AuthModule, + AdminsModule, + ], controllers: [], 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 }); + } +} diff --git a/auto-bid-server/src/config/typeorm.ts b/auto-bid-server/src/config/typeorm.ts new file mode 100644 index 0000000..ec4e0d2 --- /dev/null +++ b/auto-bid-server/src/config/typeorm.ts @@ -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); diff --git a/auto-bid-server/src/main.ts b/auto-bid-server/src/main.ts index e3f0908..5ed2707 100644 --- a/auto-bid-server/src/main.ts +++ b/auto-bid-server/src/main.ts @@ -3,6 +3,8 @@ import { AppModule } from './app.module'; import { ValidationPipe } from '@nestjs/common'; import { BadRequestExceptionFilter } from './system/filters/bad-request-exception-filter'; import { IoAdapter } from '@nestjs/platform-socket.io'; +import * as cookieParser from 'cookie-parser'; +import { useContainer } from 'class-validator'; async function bootstrap() { const app = await NestFactory.create(AppModule); @@ -16,9 +18,10 @@ async function bootstrap() { credentials: true, }); + app.use(cookieParser()); + app.setGlobalPrefix(prefix_version, {}); - // Sử dụng WebSocket adapter app.useWebSocketAdapter(new IoAdapter(app)); app.useGlobalFilters(new BadRequestExceptionFilter()); @@ -31,6 +34,8 @@ async function bootstrap() { }), ); + useContainer(app.select(AppModule), { fallbackOnErrors: true }); + await app.listen(process.env.PORT); } bootstrap(); diff --git a/auto-bid-server/src/migrations/1742778498009-create-admin-table.ts b/auto-bid-server/src/migrations/1742778498009-create-admin-table.ts new file mode 100644 index 0000000..3d82f86 --- /dev/null +++ b/auto-bid-server/src/migrations/1742778498009-create-admin-table.ts @@ -0,0 +1,16 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class CreateAdminTable1742778498009 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + 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 { + await queryRunner.query(` + DELETE FROM ktq_admins WHERE email = 'admin@gmail.com'; + `); + } +} diff --git a/auto-bid-server/src/migrations/1742778709701-create-permission-table.ts b/auto-bid-server/src/migrations/1742778709701-create-permission-table.ts new file mode 100644 index 0000000..0dbd689 --- /dev/null +++ b/auto-bid-server/src/migrations/1742778709701-create-permission-table.ts @@ -0,0 +1,17 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class CreatePermissionTable1742778709701 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + 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 { + await queryRunner.query(`DROP TABLE permissions;`); + } +} diff --git a/auto-bid-server/src/modules/admins/admins.module.ts b/auto-bid-server/src/modules/admins/admins.module.ts new file mode 100644 index 0000000..4751a7d --- /dev/null +++ b/auto-bid-server/src/modules/admins/admins.module.ts @@ -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 {} diff --git a/auto-bid-server/src/modules/admins/controllers/admins.controller.ts b/auto-bid-server/src/modules/admins/controllers/admins.controller.ts new file mode 100644 index 0000000..58647dc --- /dev/null +++ b/auto-bid-server/src/modules/admins/controllers/admins.controller.ts @@ -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); + } +} diff --git a/auto-bid-server/src/modules/admins/controllers/permissions.controller.ts b/auto-bid-server/src/modules/admins/controllers/permissions.controller.ts new file mode 100644 index 0000000..4f18ed4 --- /dev/null +++ b/auto-bid-server/src/modules/admins/controllers/permissions.controller.ts @@ -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); + } +} diff --git a/auto-bid-server/src/modules/admins/dtos/create.dto.ts b/auto-bid-server/src/modules/admins/dtos/create.dto.ts new file mode 100644 index 0000000..c201591 --- /dev/null +++ b/auto-bid-server/src/modules/admins/dtos/create.dto.ts @@ -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; +} diff --git a/auto-bid-server/src/modules/admins/dtos/deletes.dto.ts b/auto-bid-server/src/modules/admins/dtos/deletes.dto.ts new file mode 100644 index 0000000..133fbb5 --- /dev/null +++ b/auto-bid-server/src/modules/admins/dtos/deletes.dto.ts @@ -0,0 +1,6 @@ +import { IsNumber } from 'class-validator'; + +export class DeletesDto { + @IsNumber({}, { each: true }) + ids: number[]; +} diff --git a/auto-bid-server/src/modules/admins/dtos/grant-new-password.dto.ts b/auto-bid-server/src/modules/admins/dtos/grant-new-password.dto.ts new file mode 100644 index 0000000..051e60a --- /dev/null +++ b/auto-bid-server/src/modules/admins/dtos/grant-new-password.dto.ts @@ -0,0 +1,7 @@ +import { IsString, MinLength } from 'class-validator'; + +export class GrantNewPasswordDto { + @IsString() + @MinLength(6) + password: string; +} diff --git a/auto-bid-server/src/modules/admins/dtos/permission.dto.ts b/auto-bid-server/src/modules/admins/dtos/permission.dto.ts new file mode 100644 index 0000000..aa9eac7 --- /dev/null +++ b/auto-bid-server/src/modules/admins/dtos/permission.dto.ts @@ -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; +} diff --git a/auto-bid-server/src/modules/admins/dtos/update.dto.ts b/auto-bid-server/src/modules/admins/dtos/update.dto.ts new file mode 100644 index 0000000..a26fc1e --- /dev/null +++ b/auto-bid-server/src/modules/admins/dtos/update.dto.ts @@ -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[]; +} diff --git a/auto-bid-server/src/modules/admins/entities/admin.entity.ts b/auto-bid-server/src/modules/admins/entities/admin.entity.ts new file mode 100644 index 0000000..745bd7f --- /dev/null +++ b/auto-bid-server/src/modules/admins/entities/admin.entity.ts @@ -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[]; +} diff --git a/auto-bid-server/src/modules/admins/entities/permission.entity.ts b/auto-bid-server/src/modules/admins/entities/permission.entity.ts new file mode 100644 index 0000000..497cc20 --- /dev/null +++ b/auto-bid-server/src/modules/admins/entities/permission.entity.ts @@ -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[]; +} diff --git a/auto-bid-server/src/modules/admins/entities/timestamp.ts b/auto-bid-server/src/modules/admins/entities/timestamp.ts new file mode 100644 index 0000000..0cf413c --- /dev/null +++ b/auto-bid-server/src/modules/admins/entities/timestamp.ts @@ -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; +} diff --git a/auto-bid-server/src/modules/admins/middlewares/authorization.middleware.ts b/auto-bid-server/src/modules/admins/middlewares/authorization.middleware.ts new file mode 100644 index 0000000..03ea431 --- /dev/null +++ b/auto-bid-server/src/modules/admins/middlewares/authorization.middleware.ts @@ -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(); + } +} diff --git a/auto-bid-server/src/modules/admins/services/admins.service.ts b/auto-bid-server/src/modules/admins/services/admins.service.ts new file mode 100644 index 0000000..59dda01 --- /dev/null +++ b/auto-bid-server/src/modules/admins/services/admins.service.ts @@ -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, + @InjectRepository(Permission) + readonly permissionRepo: Repository, + ) {} + + async index(query: PaginateQuery) { + const filterableColumns: { + [key in Column | (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(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) { + 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, + }), + ); + } + } +} diff --git a/auto-bid-server/src/modules/admins/services/permission.service.ts b/auto-bid-server/src/modules/admins/services/permission.service.ts new file mode 100644 index 0000000..6944b18 --- /dev/null +++ b/auto-bid-server/src/modules/admins/services/permission.service.ts @@ -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, + ) {} + + async getPermissionsByAdmin(adminId: number): Promise { + 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 | (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(data, true, Permission); + } +} diff --git a/auto-bid-server/src/modules/app-validators/app-validators.module.ts b/auto-bid-server/src/modules/app-validators/app-validators.module.ts new file mode 100644 index 0000000..c24d4c2 --- /dev/null +++ b/auto-bid-server/src/modules/app-validators/app-validators.module.ts @@ -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 {} diff --git a/auto-bid-server/src/modules/auth/auth.controller.ts b/auto-bid-server/src/modules/auth/auth.controller.ts new file mode 100644 index 0000000..0e78185 --- /dev/null +++ b/auto-bid-server/src/modules/auth/auth.controller.ts @@ -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); + } +} diff --git a/auto-bid-server/src/modules/auth/auth.module.ts b/auto-bid-server/src/modules/auth/auth.module.ts new file mode 100644 index 0000000..7f8471f --- /dev/null +++ b/auto-bid-server/src/modules/auth/auth.module.ts @@ -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 {} diff --git a/auto-bid-server/src/modules/auth/auth.service.ts b/auto-bid-server/src/modules/auth/auth.service.ts new file mode 100644 index 0000000..5605117 --- /dev/null +++ b/auto-bid-server/src/modules/auth/auth.service.ts @@ -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); + } +} diff --git a/auto-bid-server/src/modules/auth/dto/change-password.dto.ts b/auto-bid-server/src/modules/auth/dto/change-password.dto.ts new file mode 100644 index 0000000..162089a --- /dev/null +++ b/auto-bid-server/src/modules/auth/dto/change-password.dto.ts @@ -0,0 +1,11 @@ +import { IsNotEmpty, IsString } from 'class-validator'; + +export class ChangePasswordDto { + @IsNotEmpty() + @IsString() + newPassword: string; + + @IsNotEmpty() + @IsString() + password: string; +} diff --git a/auto-bid-server/src/modules/auth/dto/login.dto.ts b/auto-bid-server/src/modules/auth/dto/login.dto.ts new file mode 100644 index 0000000..dba5d8c --- /dev/null +++ b/auto-bid-server/src/modules/auth/dto/login.dto.ts @@ -0,0 +1,11 @@ +import { IsNotEmpty, IsString } from 'class-validator'; + +export class LoginDto { + @IsNotEmpty() + @IsString() + username: string; + + @IsNotEmpty() + @IsString() + password: string; +} diff --git a/auto-bid-server/src/modules/auth/middlewares/authentication.middleware.ts b/auto-bid-server/src/modules/auth/middlewares/authentication.middleware.ts new file mode 100644 index 0000000..9834f9d --- /dev/null +++ b/auto-bid-server/src/modules/auth/middlewares/authentication.middleware.ts @@ -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, 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 = 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, + }), + ), + ); + } + } + } +} diff --git a/auto-bid-server/src/modules/auth/ultils/constant.ts b/auto-bid-server/src/modules/auth/ultils/constant.ts new file mode 100644 index 0000000..fd363b6 --- /dev/null +++ b/auto-bid-server/src/modules/auth/ultils/constant.ts @@ -0,0 +1,4 @@ +export class Constant { + public static ACCESS_TOKEN = 'access_token'; + public static REFRESH_TOKEN = 'refresh_token'; +} diff --git a/auto-bid-server/src/modules/bids/dto/bid/client-update-bid.dto.ts b/auto-bid-server/src/modules/bids/dto/bid/client-update-bid.dto.ts index e1071ae..d79e3d5 100644 --- a/auto-bid-server/src/modules/bids/dto/bid/client-update-bid.dto.ts +++ b/auto-bid-server/src/modules/bids/dto/bid/client-update-bid.dto.ts @@ -5,8 +5,9 @@ export class ClientUpdateBidDto { @IsOptional() lot_id: string; + @IsOptional() @IsString() - close_time: string; + close_time: string | null; @IsString() @IsOptional() diff --git a/auto-bid-server/src/modules/bids/services/bids.service.ts b/auto-bid-server/src/modules/bids/services/bids.service.ts index 59b8ebb..e157581 100644 --- a/auto-bid-server/src/modules/bids/services/bids.service.ts +++ b/auto-bid-server/src/modules/bids/services/bids.service.ts @@ -51,6 +51,7 @@ export class BidsService { model: true, lot_id: true, close_time: true, + name: true, }; query.filter = AppResponse.processFilters(query.filter, filterableColumns); @@ -64,8 +65,9 @@ export class BidsService { 'lot_id', 'max_price', 'status', + 'name', ], - searchableColumns: ['id', 'status', 'model', 'lot_id'], + searchableColumns: ['id', 'status', 'model', 'lot_id', 'name'], defaultLimit: 15, filterableColumns, defaultSortBy: [['id', 'DESC']], @@ -185,7 +187,10 @@ export class BidsService { 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 } }); if (!bid) @@ -198,14 +203,28 @@ export class BidsService { if (!bid.close_time && !bid.start_bid_time) { // 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'; } - 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({ ...bid, diff --git a/auto-bid-server/src/system/guards/rate-limit.guard.ts b/auto-bid-server/src/system/guards/rate-limit.guard.ts new file mode 100644 index 0000000..0b7d8ef --- /dev/null +++ b/auto-bid-server/src/system/guards/rate-limit.guard.ts @@ -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 { + throw new HttpException( + AppResponse.toResponse(null, { + message: 'To many request', + status_code: HttpStatus.TOO_MANY_REQUESTS, + }), + HttpStatus.TOO_MANY_REQUESTS, + ); + } +} diff --git a/auto-bid-server/src/system/guards/system-account.guard.ts b/auto-bid-server/src/system/guards/system-account.guard.ts new file mode 100644 index 0000000..a1ce73b --- /dev/null +++ b/auto-bid-server/src/system/guards/system-account.guard.ts @@ -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; + } +} diff --git a/auto-bid-server/src/system/routes/exclude-route.ts b/auto-bid-server/src/system/routes/exclude-route.ts new file mode 100644 index 0000000..0327c32 --- /dev/null +++ b/auto-bid-server/src/system/routes/exclude-route.ts @@ -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)[]; diff --git a/auto-bid-server/src/system/validators/decorators/has-existed.ts b/auto-bid-server/src/system/validators/decorators/has-existed.ts new file mode 100644 index 0000000..6859f33 --- /dev/null +++ b/auto-bid-server/src/system/validators/decorators/has-existed.ts @@ -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; + 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, + }); + }; +} diff --git a/auto-bid-server/src/system/validators/decorators/is-unique.ts b/auto-bid-server/src/system/validators/decorators/is-unique.ts new file mode 100644 index 0000000..196f1d6 --- /dev/null +++ b/auto-bid-server/src/system/validators/decorators/is-unique.ts @@ -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; +}; + +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, + }); + }; +} diff --git a/auto-bid-server/src/system/validators/has-existed.validator.ts b/auto-bid-server/src/system/validators/has-existed.validator.ts new file mode 100644 index 0000000..b837438 --- /dev/null +++ b/auto-bid-server/src/system/validators/has-existed.validator.ts @@ -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`; + } +} diff --git a/auto-bid-server/src/system/validators/is-unique.validator.ts b/auto-bid-server/src/system/validators/is-unique.validator.ts new file mode 100644 index 0000000..0f26e13 --- /dev/null +++ b/auto-bid-server/src/system/validators/is-unique.validator.ts @@ -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`; + } +} diff --git a/auto-bid-server/tsconfig.json b/auto-bid-server/tsconfig.json index 95f5641..38739b7 100644 --- a/auto-bid-server/tsconfig.json +++ b/auto-bid-server/tsconfig.json @@ -16,6 +16,9 @@ "noImplicitAny": false, "strictBindCallApply": false, "forceConsistentCasingInFileNames": false, - "noFallthroughCasesInSwitch": false + "noFallthroughCasesInSwitch": false, + "paths": { + "@/*": ["src/*"] + } } } diff --git a/auto-bid-tool/index.js b/auto-bid-tool/index.js index b9a91a6..435e28f 100644 --- a/auto-bid-tool/index.js +++ b/auto-bid-tool/index.js @@ -4,7 +4,7 @@ import { io } from 'socket.io-client'; import { createApiBid, createBidProduct, deleteProfile, shouldUpdateProductTab } from './service/app-service.js'; import browser from './system/browser.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 = []; @@ -58,10 +58,10 @@ const handleUpdateProductTabs = (data) => { // const productTabs = _.flatMap(MANAGER_BIDS, 'children'); // for (const productTab of productTabs) { +// // Tìm parent context nếu chưa có // if (!productTab.parent_browser_context) { // 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) { // console.log(`🔄 Waiting for parent process to start... (Product ID: ${productTab.id})`); @@ -69,57 +69,37 @@ const handleUpdateProductTabs = (data) => { // } // } -// if (!productTab.first_bid) { -// 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; -// } - +// // Kết nối Puppeteer nếu chưa có page_context // if (!productTab.page_context) { // console.log(`🔌 Connecting to page for Product ID: ${productTab.id}`); // 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}`); // await productTab.action(); // } @@ -127,22 +107,29 @@ const handleUpdateProductTabs = (data) => { // }; const tracking = async () => { - if (_INTERVAL_TRACKING_ID) { - clearInterval(_INTERVAL_TRACKING_ID); - _INTERVAL_TRACKING_ID = null; - } + console.log('🚀 Tracking process started...'); - _INTERVAL_TRACKING_ID = setInterval(async () => { + while (true) { + console.log('🔍 Scanning active bids...'); 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) { + console.log(`📌 Processing Product ID: ${productTab.id}`); + // Tìm parent context nếu chưa có if (!productTab.parent_browser_context) { const parent = _.find(MANAGER_BIDS, { id: productTab.web_bid.id }); productTab.parent_browser_context = 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; } } @@ -155,6 +142,7 @@ const tracking = async () => { // Nếu URL thay đổi, điều hướng đến URL mới if (productTab.page_context.url() !== productTab.url) { + console.log(`🔄 Redirecting to new URL for Product ID: ${productTab.id}`); 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á) 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; } @@ -181,81 +169,70 @@ const tracking = async () => { console.log(`🚀 Executing action for Product ID: ${productTab.id}`); 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 () => { - if (_CLEAR_LAZY_TAB_ID) { - clearInterval(_CLEAR_LAZY_TAB_ID); - _CLEAR_LAZY_TAB_ID = null; + if (!global.IS_CLEANING) return; + + if (!browser) { + console.warn('⚠️ Browser is not available or disconnected.'); + return; } try { - _CLEAR_LAZY_TAB_ID = setInterval(async () => { - if (!global.IS_CLEANING) return; + const pages = await browser.pages(); - if (!browser) { - console.warn('⚠️ Browser is not available or disconnected.'); - clearInterval(_CLEAR_LAZY_TAB_ID); - _CLEAR_LAZY_TAB_ID = null; - return; - } + // Lấy danh sách URL từ flattenedArray + const activeUrls = _.flatMap(MANAGER_BIDS, (item) => [item.url, ...item.children.map((child) => child.url)]).filter(Boolean); // Lọc bỏ null hoặc undefined - try { - const pages = await browser.pages(); + console.log( + '🔍 Page URLs:', + pages.map((page) => page.url()), + ); - // Lấy danh sách URL từ flattenedArray - const activeUrls = _.flatMap(MANAGER_BIDS, (item) => [item.url, ...item.children.map((child) => child.url)]).filter(Boolean); // Lọc bỏ null hoặc undefined + for (const page of pages) { + const pageUrl = page.url(); - console.log( - '🔍 Page URLs:', - pages.map((page) => page.url()), - ); + // 🔥 Bỏ qua tab 'about:blank' hoặc tab không có URL + if (!pageUrl || pageUrl === 'about:blank') continue; - for (const page of pages) { - const pageUrl = page.url(); - - // 🔥 Bỏ qua tab 'about:blank' hoặc tab không có URL - if (!pageUrl || pageUrl === 'about:blank') continue; - - if (!activeUrls.includes(pageUrl)) { - 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); - } - } + if (!activeUrls.includes(pageUrl)) { + 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) { - console.log('CLEAR LAZY TAB ERROR: ', error.message); + } + } catch (err) { + console.error('❌ Error in clearLazyTab:', err.message); } }; -const workTracking = () => { +const workTracking = async () => { try { - if (_WORK_TRACKING_ID) { - clearInterval(_WORK_TRACKING_ID); - _WORK_TRACKING_ID = null; - } + const activeData = _.flatMap(MANAGER_BIDS, (item) => [item, ...item.children]); - _WORK_TRACKING_ID = setInterval(() => { - const activeData = _.flatMap(MANAGER_BIDS, (item) => [item, ...item.children]); - - for (const item of activeData) { - 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) { - console.log('Loi oi day'); + console.log('Lỗi rồi:', error); } }; @@ -277,7 +254,7 @@ const workTracking = () => { 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) => { @@ -303,7 +280,7 @@ const workTracking = () => { // AUTO TRACKING tracking(); - clearLazyTab(); + // clearLazyTab(); - workTracking(); + // workTracking(); })(); diff --git a/auto-bid-tool/models/bid.js b/auto-bid-tool/models/bid.js index d91d1b9..1f2111e 100644 --- a/auto-bid-tool/models/bid.js +++ b/auto-bid-tool/models/bid.js @@ -1,5 +1,6 @@ import CONSTANTS from '../system/constants.js'; import { takeSnapshot } from '../system/utils.js'; +import _ from 'lodash'; export class Bid { type; @@ -14,9 +15,14 @@ export class Bid { this.puppeteer_connect = puppeteer_connect; } - async handleTakeWorkSnapshot() { + handleTakeWorkSnapshot = _.debounce(async () => { 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); } diff --git a/auto-bid-tool/models/grays.com/grays-product-bid.js b/auto-bid-tool/models/grays.com/grays-product-bid.js index 5ece937..04153ad 100644 --- a/auto-bid-tool/models/grays.com/grays-product-bid.js +++ b/auto-bid-tool/models/grays.com/grays-product-bid.js @@ -17,7 +17,6 @@ export class GraysProductBid extends ProductBid { if (!isNumber(price_value)) { console.log("Can't get PRICE_VALUE ❌"); await takeSnapshot(page, this, 'price-value-null'); - // await safeClosePage(this); return { result: false, bid_price: 0 }; } @@ -27,7 +26,6 @@ export class GraysProductBid extends ProductBid { if (bid_price > this.max_price) { console.log('PRICE BID is more than MAX_VALUE => STOP BID THIS PRODUCT ❌'); await takeSnapshot(page, this, 'price-bid-more-than'); - // await safeClosePage(this); await outBid(this.id); @@ -83,7 +81,7 @@ export class GraysProductBid extends ProductBid { } }; - async isCloseProduct(page) { + async isCloseProduct() { const close_time = await this.getCloseTime(); 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 () => { 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}`); - const data = removeFalsyValues({ - lot_id, - reserve_price: price_value, - close_time: String(close_time), - name, - current_price: current_price ? extractNumber(current_price) : null, - }); + const data = removeFalsyValues( + { + lot_id, + reserve_price: price_value, + close_time: close_time ? String(close_time) : null, + name, + current_price: current_price ? extractNumber(current_price) : null, + }, + ['close_time'], + ); this.handleUpdateBid(data); @@ -223,7 +206,7 @@ export class GraysProductBid extends ProductBid { await delay(1000); - const { close_time, ...isCloseProduct } = await this.isCloseProduct(page); + const { close_time, ...isCloseProduct } = await this.isCloseProduct(); if (isCloseProduct.result) { console.log('❌ The product is closed, cannot place a bid.'); return; diff --git a/auto-bid-tool/models/product-bid.js b/auto-bid-tool/models/product-bid.js index 06cfef3..62c5dee 100644 --- a/auto-bid-tool/models/product-bid.js +++ b/auto-bid-tool/models/product-bid.js @@ -135,14 +135,34 @@ export class ProductBid extends Bid { 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() { const page = this.page_context; + + if (page.isClosed()) { + console.error('❌ Page has been closed, cannot navigate.'); + return; + } + console.log('🔄 Starting the bidding process...'); - await page.goto(this.url, { waitUntil: 'networkidle2' }); - console.log(`✅ Navigated to: ${this.url}`); + try { + await page.goto(this.url, { waitUntil: 'networkidle2' }); + console.log(`✅ Navigated to: ${this.url}`); - await page.bringToFront(); - console.log('👀 Brought the tab to the foreground.'); + await page.bringToFront(); + console.log('👀 Brought the tab to the foreground.'); + } catch (error) { + console.error('❌ Error during navigation:', error); + } } } diff --git a/auto-bid-tool/system/utils.js b/auto-bid-tool/system/utils.js index 6d52f84..01ca787 100644 --- a/auto-bid-tool/system/utils.js +++ b/auto-bid-tool/system/utils.js @@ -9,6 +9,7 @@ export const takeSnapshot = async (page, item, imageName, type = CONSTANTS.TYPE_ if (!page || page.isClosed()) return; 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 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) { console.log('Error when snapshot: ' + error.message); + } finally { + global.IS_CLEANING = true; } };