update handle
This commit is contained in:
		
							parent
							
								
									b869b83a3f
								
							
						
					
					
						commit
						db080ad3f2
					
				| 
						 | 
				
			
			@ -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) {
 | 
			
		|||
                />
 | 
			
		||||
 | 
			
		||||
                <Box className="absolute bg-black/60 inset-0 flex items-center justify-center text-white font-bold flex-col gap-3 p-4 rounded-lg transition duration-300 hover:bg-opacity-70">
 | 
			
		||||
                    <Text className="text-lg uppercase tracking-wide">{data.type}</Text>
 | 
			
		||||
                    <Text className="text-lg tracking-wide text-center font-bold">{isIBid(data) ? data.name : 'Tracking page'}</Text>
 | 
			
		||||
                    {isIBid(data) && <Text className="text-xs tracking-wide">{`Max price: $${data.max_price}`}</Text>}
 | 
			
		||||
                    {isIBid(data) && <Text className="text-xs tracking-wide">{`Current price: $${data.current_price}`}</Text>}
 | 
			
		||||
                    <Text className="text-sm italic opacity-80">{moment(lastUpdate).format('HH:mm:ss DD/MM/YYYY')}</Text>
 | 
			
		||||
                    <Button onClick={open} className="bg-white text-black px-4 py-2 rounded-md shadow-md hover:bg-gray-200 transition">
 | 
			
		||||
                        Show
 | 
			
		||||
                    </Button>
 | 
			
		||||
                    <Box className="flex items-center gap-4">
 | 
			
		||||
                        <Button size="xs" color="green" onClick={open} className="bg-white text-black px-4 py-2 rounded-md shadow-md hover:bg-gray-200 transition">
 | 
			
		||||
                            Show
 | 
			
		||||
                        </Button>
 | 
			
		||||
                        <Button
 | 
			
		||||
                            target="_blank"
 | 
			
		||||
                            component="a"
 | 
			
		||||
                            size="xs"
 | 
			
		||||
                            href={data.url || '/'}
 | 
			
		||||
                            className="bg-white text-black px-4 py-2 rounded-md shadow-md hover:bg-gray-200 transition"
 | 
			
		||||
                        >
 | 
			
		||||
                            Link
 | 
			
		||||
                        </Button>
 | 
			
		||||
                    </Box>
 | 
			
		||||
                </Box>
 | 
			
		||||
            </Box>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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 (
 | 
			
		||||
        <AppShell header={{ height: 60 }} navbar={{ width: 300, breakpoint: 'sm' }} padding="md">
 | 
			
		||||
        <AppShell header={{ height: 60 }} navbar={{ width: 300, breakpoint: 'sm', collapsed: { mobile: true, desktop: true } }} padding="md">
 | 
			
		||||
            <AppShell.Header>
 | 
			
		||||
                <Box className="flex items-center justify-between h-full px-4">
 | 
			
		||||
                    <Logo />
 | 
			
		||||
 | 
			
		||||
                    <Box className="flex items-center gap-4">
 | 
			
		||||
                        {Links.MENUS.map((menu, index) => (
 | 
			
		||||
                            <Button
 | 
			
		||||
                                size="xs"
 | 
			
		||||
                                component={Link}
 | 
			
		||||
                                className="rounded-sm"
 | 
			
		||||
                                key={menu.path + index}
 | 
			
		||||
                                to={menu.path}
 | 
			
		||||
                                // label={menu.title}
 | 
			
		||||
                                variant={location.pathname === menu.path ? 'filled' : 'outline'}
 | 
			
		||||
                                leftSection={<menu.icon size={16} stroke={1.5} />}
 | 
			
		||||
                            >
 | 
			
		||||
                                {menu.title}
 | 
			
		||||
                            </Button>
 | 
			
		||||
                        ))}
 | 
			
		||||
                    </Box>
 | 
			
		||||
 | 
			
		||||
                    <UserMenu />
 | 
			
		||||
                </Box>
 | 
			
		||||
            </AppShell.Header>
 | 
			
		||||
            <AppShell.Navbar px={'md'} pb={'md'}>
 | 
			
		||||
            {/* <AppShell.Navbar px={'md'} pb={'md'}>
 | 
			
		||||
                <AppShell.Section grow my="md" component={ScrollArea}>
 | 
			
		||||
                    <div className="w-full h-full flex flex-col gap-3">
 | 
			
		||||
                        {Links.MENUS.map((menu, index) => (
 | 
			
		||||
| 
						 | 
				
			
			@ -48,7 +65,7 @@ export default function PrivateLayout() {
 | 
			
		|||
                    </div>
 | 
			
		||||
                </AppShell.Section>
 | 
			
		||||
                <AppShell.Section className="text-xs text-center">{new Date().getFullYear()}</AppShell.Section>
 | 
			
		||||
            </AppShell.Navbar>
 | 
			
		||||
            </AppShell.Navbar> */}
 | 
			
		||||
            <AppShell.Main>
 | 
			
		||||
                <Outlet />
 | 
			
		||||
            </AppShell.Main>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -54,6 +54,7 @@ export interface TableProps<R extends Record<string, string | number>> extends M
 | 
			
		|||
    onFetched?: (response: AxiosResponse<R[]>) => void;
 | 
			
		||||
    onChooses?: (chooses: R[]) => void;
 | 
			
		||||
    onSearch?: (value: ITableFilter<R>[]) => void;
 | 
			
		||||
    onClickRow?: (row: R) => void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const TableIcon = ({
 | 
			
		||||
| 
						 | 
				
			
			@ -115,6 +116,7 @@ const Table = <R extends Record<string, any>>({
 | 
			
		|||
    onFetchError,
 | 
			
		||||
    onChooses,
 | 
			
		||||
    onSearch,
 | 
			
		||||
    onClickRow,
 | 
			
		||||
    ...props
 | 
			
		||||
}: TableProps<R>) => {
 | 
			
		||||
    const paramsUrl = new URLSearchParams(window.location.search);
 | 
			
		||||
| 
						 | 
				
			
			@ -507,6 +509,7 @@ const Table = <R extends Record<string, any>>({
 | 
			
		|||
 | 
			
		||||
            handleSetChoosesData(newDataChooses);
 | 
			
		||||
        },
 | 
			
		||||
        // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
			
		||||
        [rowsData, chooses, handleSetChoosesData],
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -576,10 +579,6 @@ const Table = <R extends Record<string, any>>({
 | 
			
		|||
        // 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 = <R extends Record<string, any>>({
 | 
			
		|||
                    <MTable.Tbody {...tbody} className="relative">
 | 
			
		||||
                        {rowsData.length > 0 &&
 | 
			
		||||
                            rowsData.map((row, index) => (
 | 
			
		||||
                                <MTable.Tr {...trbody} key={row[rowKey]}>
 | 
			
		||||
                                <MTable.Tr {...trbody} onClick={onClickRow ? () => onClickRow(row) : undefined} key={row[rowKey]}>
 | 
			
		||||
                                    {showChooses && (
 | 
			
		||||
                                        <MTable.Td key={'__choose_' + index} {...td} {...chooseOptions?.defaultBodyProps}>
 | 
			
		||||
                                            {chooseOptions?.renderBody ? (
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,180 @@
 | 
			
		|||
import { ActionIcon, Box, Menu } from '@mantine/core';
 | 
			
		||||
import { useDisclosure } from '@mantine/hooks';
 | 
			
		||||
import { IconEdit, IconMenu, IconPassword, IconTrash } from '@tabler/icons-react';
 | 
			
		||||
import { useCallback, useMemo, useRef, useState } from 'react';
 | 
			
		||||
import { deleteAdmin, getAdmins } from '../apis/admin';
 | 
			
		||||
import { AdminModal, GrantNewPasswordModal } from '../components/admin';
 | 
			
		||||
import Table from '../lib/table/table';
 | 
			
		||||
import { IColumn, TRefTableFn } from '../lib/table/type';
 | 
			
		||||
import { useConfirmStore } from '../lib/zustand/use-confirm';
 | 
			
		||||
import { IAdmin } from '../system/type';
 | 
			
		||||
import { formatTime } from '../utils';
 | 
			
		||||
 | 
			
		||||
export default function Admins() {
 | 
			
		||||
    const { setConfirm } = useConfirmStore();
 | 
			
		||||
 | 
			
		||||
    const refTableFn: TRefTableFn<IAdmin> = useRef({});
 | 
			
		||||
 | 
			
		||||
    const [adminModelOpened, adminModel] = useDisclosure(false);
 | 
			
		||||
    const [grantOpened, grantModel] = useDisclosure(false);
 | 
			
		||||
    const [clickData, setClickData] = useState<IAdmin | null>(null);
 | 
			
		||||
 | 
			
		||||
    const columns: IColumn<IAdmin>[] = [
 | 
			
		||||
        {
 | 
			
		||||
            key: 'id',
 | 
			
		||||
            title: 'ID',
 | 
			
		||||
            typeFilter: 'number',
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            key: 'username',
 | 
			
		||||
            title: 'Username',
 | 
			
		||||
            typeFilter: 'text',
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            key: 'email',
 | 
			
		||||
            title: 'Email',
 | 
			
		||||
            typeFilter: 'text',
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            key: 'created_at',
 | 
			
		||||
            title: 'Created at',
 | 
			
		||||
            typeFilter: 'none',
 | 
			
		||||
            renderRow(row) {
 | 
			
		||||
                return <span>{formatTime(row.created_at)}</span>;
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            key: 'updated_at',
 | 
			
		||||
            title: 'Update at',
 | 
			
		||||
            typeFilter: 'none',
 | 
			
		||||
            renderRow(row) {
 | 
			
		||||
                return <span>{formatTime(row.updated_at)}</span>;
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    const handleDelete = (admin: IAdmin) => {
 | 
			
		||||
        setConfirm({
 | 
			
		||||
            title: 'Delete ?',
 | 
			
		||||
            message: 'This admin will be delete',
 | 
			
		||||
            handleOk: async () => {
 | 
			
		||||
                await deleteAdmin(admin);
 | 
			
		||||
 | 
			
		||||
                if (refTableFn.current?.fetchData) {
 | 
			
		||||
                    refTableFn.current.fetchData();
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
        });
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const getAdminsFn = useCallback(getAdmins, []);
 | 
			
		||||
 | 
			
		||||
    const table = useMemo(() => {
 | 
			
		||||
        return (
 | 
			
		||||
            <Table
 | 
			
		||||
                actionsOptions={{
 | 
			
		||||
                    actions: [
 | 
			
		||||
                        {
 | 
			
		||||
                            key: 'add',
 | 
			
		||||
                            title: 'Add',
 | 
			
		||||
                            callback: () => {
 | 
			
		||||
                                adminModel.open();
 | 
			
		||||
                            },
 | 
			
		||||
                        },
 | 
			
		||||
                    ],
 | 
			
		||||
                }}
 | 
			
		||||
                refTableFn={refTableFn}
 | 
			
		||||
                striped
 | 
			
		||||
                showLoading={true}
 | 
			
		||||
                highlightOnHover
 | 
			
		||||
                styleDefaultHead={{
 | 
			
		||||
                    justifyContent: 'flex-start',
 | 
			
		||||
                    width: 'fit-content',
 | 
			
		||||
                }}
 | 
			
		||||
                options={{
 | 
			
		||||
                    query: getAdminsFn,
 | 
			
		||||
                    pathToData: 'data.data',
 | 
			
		||||
                    keyOptions: {
 | 
			
		||||
                        last_page: 'lastPage',
 | 
			
		||||
                        per_page: 'perPage',
 | 
			
		||||
                        from: 'from',
 | 
			
		||||
                        to: 'to',
 | 
			
		||||
                        total: 'total',
 | 
			
		||||
                    },
 | 
			
		||||
                }}
 | 
			
		||||
                rows={[]}
 | 
			
		||||
                withColumnBorders
 | 
			
		||||
                showChooses={false}
 | 
			
		||||
                withTableBorder
 | 
			
		||||
                columns={columns}
 | 
			
		||||
                actions={{
 | 
			
		||||
                    title: <Box className="w-full text-center">Action</Box>,
 | 
			
		||||
                    body: (row) => {
 | 
			
		||||
                        return (
 | 
			
		||||
                            <Menu shadow="md" width={200}>
 | 
			
		||||
                                <Menu.Target>
 | 
			
		||||
                                    <Box className="flex w-full items-center justify-center">
 | 
			
		||||
                                        <ActionIcon size="sm" variant="light">
 | 
			
		||||
                                            <IconMenu size={14} />
 | 
			
		||||
                                        </ActionIcon>
 | 
			
		||||
                                    </Box>
 | 
			
		||||
                                </Menu.Target>
 | 
			
		||||
 | 
			
		||||
                                <Menu.Dropdown>
 | 
			
		||||
                                    <Menu.Item
 | 
			
		||||
                                        onClick={() => {
 | 
			
		||||
                                            setClickData(row);
 | 
			
		||||
                                            adminModel.open();
 | 
			
		||||
                                        }}
 | 
			
		||||
                                        leftSection={<IconEdit size={14} />}
 | 
			
		||||
                                    >
 | 
			
		||||
                                        Edit
 | 
			
		||||
                                    </Menu.Item>
 | 
			
		||||
                                    <Menu.Item
 | 
			
		||||
                                        onClick={() => {
 | 
			
		||||
                                            setClickData(row);
 | 
			
		||||
                                            grantModel.open();
 | 
			
		||||
                                        }}
 | 
			
		||||
                                        leftSection={<IconPassword size={14} />}
 | 
			
		||||
                                    >
 | 
			
		||||
                                        Grant new password
 | 
			
		||||
                                    </Menu.Item>
 | 
			
		||||
                                    <Menu.Item disabled={row.is_system_account} onClick={() => handleDelete(row)} leftSection={<IconTrash color="red" size={14} />}>
 | 
			
		||||
                                        Delete
 | 
			
		||||
                                    </Menu.Item>
 | 
			
		||||
                                </Menu.Dropdown>
 | 
			
		||||
                            </Menu>
 | 
			
		||||
                        );
 | 
			
		||||
                    },
 | 
			
		||||
                }}
 | 
			
		||||
                rowKey="id"
 | 
			
		||||
            />
 | 
			
		||||
        );
 | 
			
		||||
        // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
			
		||||
    }, []);
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <Box>
 | 
			
		||||
            {table}
 | 
			
		||||
 | 
			
		||||
            <AdminModal
 | 
			
		||||
                onUpdated={refTableFn.current.fetchData}
 | 
			
		||||
                data={clickData}
 | 
			
		||||
                opened={adminModelOpened}
 | 
			
		||||
                onClose={() => {
 | 
			
		||||
                    setClickData(null);
 | 
			
		||||
                    adminModel.close();
 | 
			
		||||
                }}
 | 
			
		||||
            />
 | 
			
		||||
 | 
			
		||||
            <GrantNewPasswordModal
 | 
			
		||||
                opened={grantOpened}
 | 
			
		||||
                data={clickData}
 | 
			
		||||
                onClose={() => {
 | 
			
		||||
                    setClickData(null);
 | 
			
		||||
                    grantModel.close();
 | 
			
		||||
                }}
 | 
			
		||||
            />
 | 
			
		||||
        </Box>
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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<IBid> = useRef({});
 | 
			
		||||
| 
						 | 
				
			
			@ -108,15 +108,6 @@ export default function Bids() {
 | 
			
		|||
                );
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        // {
 | 
			
		||||
        //     key: 'updated_at',
 | 
			
		||||
        //     title: 'Update at',
 | 
			
		||||
        //     typeFilter: 'none',
 | 
			
		||||
        //     renderRow(row) {
 | 
			
		||||
        //         return <span className="text-sm">{formatTime(row.updated_at)}</span>;
 | 
			
		||||
        //     },
 | 
			
		||||
        // },
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    const handleDelete = (bid: IBid) => {
 | 
			
		||||
| 
						 | 
				
			
			@ -156,6 +147,14 @@ export default function Bids() {
 | 
			
		|||
    const table = useMemo(() => {
 | 
			
		||||
        return (
 | 
			
		||||
            <Table
 | 
			
		||||
                onClickRow={(row) => {
 | 
			
		||||
                    window.open(row.url, '_blank');
 | 
			
		||||
                }}
 | 
			
		||||
                tableChildProps={{
 | 
			
		||||
                    trbody: {
 | 
			
		||||
                        className: 'cursor-pointer',
 | 
			
		||||
                    },
 | 
			
		||||
                }}
 | 
			
		||||
                actionsOptions={{
 | 
			
		||||
                    actions: [
 | 
			
		||||
                        {
 | 
			
		||||
| 
						 | 
				
			
			@ -217,14 +216,14 @@ export default function Bids() {
 | 
			
		|||
                        return (
 | 
			
		||||
                            <Menu shadow="md" width={200}>
 | 
			
		||||
                                <Menu.Target>
 | 
			
		||||
                                    <Box className="flex w-full items-center justify-center">
 | 
			
		||||
                                    <Box onClick={(e) => e.stopPropagation()} className="flex w-full items-center justify-center">
 | 
			
		||||
                                        <ActionIcon size="sm" variant="light">
 | 
			
		||||
                                            <IconMenu size={14} />
 | 
			
		||||
                                        </ActionIcon>
 | 
			
		||||
                                    </Box>
 | 
			
		||||
                                </Menu.Target>
 | 
			
		||||
 | 
			
		||||
                                <Menu.Dropdown>
 | 
			
		||||
                                <Menu.Dropdown onClick={(e) => e.stopPropagation()}>
 | 
			
		||||
                                    <Menu.Item
 | 
			
		||||
                                        onClick={() => {
 | 
			
		||||
                                            setClickData(row);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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": {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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 });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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);
 | 
			
		||||
| 
						 | 
				
			
			@ -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();
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,16 @@
 | 
			
		|||
import { MigrationInterface, QueryRunner } from 'typeorm';
 | 
			
		||||
 | 
			
		||||
export class CreateAdminTable1742778498009 implements MigrationInterface {
 | 
			
		||||
  public async up(queryRunner: QueryRunner): Promise<void> {
 | 
			
		||||
    await queryRunner.query(`
 | 
			
		||||
            INSERT INTO admins (email, username, password, is_system_account) VALUES 
 | 
			
		||||
            ('admin@gmail.com', 'admin', '$2b$10$eF7K4Msw32e5ZC2cU78KgOqxMJygQcPDt5xXZP29inBBIV9KEsoyO', 1);
 | 
			
		||||
          `);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async down(queryRunner: QueryRunner): Promise<void> {
 | 
			
		||||
    await queryRunner.query(`
 | 
			
		||||
        DELETE FROM ktq_admins WHERE email = 'admin@gmail.com';
 | 
			
		||||
      `);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,17 @@
 | 
			
		|||
import { MigrationInterface, QueryRunner } from 'typeorm';
 | 
			
		||||
 | 
			
		||||
export class CreatePermissionTable1742778709701 implements MigrationInterface {
 | 
			
		||||
  public async up(queryRunner: QueryRunner): Promise<void> {
 | 
			
		||||
    await queryRunner.query(`
 | 
			
		||||
            INSERT INTO permissions (name, description) VALUES 
 | 
			
		||||
            ('GET', 'Read data from the API'),
 | 
			
		||||
            ('POST', 'Create new records'),
 | 
			
		||||
            ('PUT', 'Update existing records'),
 | 
			
		||||
            ('DELETE', 'Remove records');
 | 
			
		||||
          `);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async down(queryRunner: QueryRunner): Promise<void> {
 | 
			
		||||
    await queryRunner.query(`DROP TABLE permissions;`);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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 {}
 | 
			
		||||
| 
						 | 
				
			
			@ -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);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,6 @@
 | 
			
		|||
import { IsNumber } from 'class-validator';
 | 
			
		||||
 | 
			
		||||
export class DeletesDto {
 | 
			
		||||
  @IsNumber({}, { each: true })
 | 
			
		||||
  ids: number[];
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,7 @@
 | 
			
		|||
import { IsString, MinLength } from 'class-validator';
 | 
			
		||||
 | 
			
		||||
export class GrantNewPasswordDto {
 | 
			
		||||
  @IsString()
 | 
			
		||||
  @MinLength(6)
 | 
			
		||||
  password: string;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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[];
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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[];
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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[];
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,223 @@
 | 
			
		|||
import AppResponse from '@/response/app-response';
 | 
			
		||||
import {
 | 
			
		||||
  BadRequestException,
 | 
			
		||||
  HttpStatus,
 | 
			
		||||
  Injectable,
 | 
			
		||||
  NotFoundException,
 | 
			
		||||
} from '@nestjs/common';
 | 
			
		||||
import { InjectRepository } from '@nestjs/typeorm';
 | 
			
		||||
import * as bcrypt from 'bcrypt';
 | 
			
		||||
import { Request } from 'express';
 | 
			
		||||
import {
 | 
			
		||||
  FilterOperator,
 | 
			
		||||
  FilterSuffix,
 | 
			
		||||
  paginate,
 | 
			
		||||
  PaginateQuery,
 | 
			
		||||
} from 'nestjs-paginate';
 | 
			
		||||
import { Column } from 'nestjs-paginate/lib/helper';
 | 
			
		||||
import { In, Not, Repository } from 'typeorm';
 | 
			
		||||
import Admin from '../entities/admin.entity';
 | 
			
		||||
import Permission from '../entities/permission.entity';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class AdminsService {
 | 
			
		||||
  constructor(
 | 
			
		||||
    @InjectRepository(Admin)
 | 
			
		||||
    readonly adminRepo: Repository<Admin>,
 | 
			
		||||
    @InjectRepository(Permission)
 | 
			
		||||
    readonly permissionRepo: Repository<Permission>,
 | 
			
		||||
  ) {}
 | 
			
		||||
 | 
			
		||||
  async index(query: PaginateQuery) {
 | 
			
		||||
    const filterableColumns: {
 | 
			
		||||
      [key in Column<Admin> | (string & {})]?:
 | 
			
		||||
        | (FilterOperator | FilterSuffix)[]
 | 
			
		||||
        | true;
 | 
			
		||||
    } = {
 | 
			
		||||
      id: true,
 | 
			
		||||
      username: [FilterOperator.ILIKE],
 | 
			
		||||
      fullname: [FilterOperator.ILIKE],
 | 
			
		||||
      email: [FilterOperator.ILIKE],
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    query.filter = AppResponse.processFilters(query.filter, filterableColumns);
 | 
			
		||||
 | 
			
		||||
    const data = await paginate(query, this.adminRepo, {
 | 
			
		||||
      sortableColumns: ['id', 'username', 'email', 'created_at', 'updated_at'],
 | 
			
		||||
      searchableColumns: ['id', 'username', 'email'],
 | 
			
		||||
      defaultLimit: 15,
 | 
			
		||||
      filterableColumns,
 | 
			
		||||
      defaultSortBy: [['id', 'ASC']],
 | 
			
		||||
      maxLimit: 100,
 | 
			
		||||
      relations: {
 | 
			
		||||
        permissions: true,
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return AppResponse.toPagination<Admin>(data, true, Admin);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async delete(id: Admin['id']) {
 | 
			
		||||
    const admin = await this.adminRepo.findOne({ where: { id } });
 | 
			
		||||
 | 
			
		||||
    if (!admin)
 | 
			
		||||
      throw new NotFoundException(
 | 
			
		||||
        AppResponse.toResponse(false, {
 | 
			
		||||
          message: 'Admin is not found',
 | 
			
		||||
          status_code: HttpStatus.NOT_FOUND,
 | 
			
		||||
        }),
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
    if (admin.is_system_account) {
 | 
			
		||||
      throw new BadRequestException(
 | 
			
		||||
        AppResponse.toResponse(false, {
 | 
			
		||||
          message: "Can't delete this account",
 | 
			
		||||
          status_code: HttpStatus.BAD_REQUEST,
 | 
			
		||||
        }),
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    await this.adminRepo.delete({ id: admin.id });
 | 
			
		||||
 | 
			
		||||
    return AppResponse.toResponse(true, { message: 'Delete success !' });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async deletes(ids: Admin['id'][], request: Request) {
 | 
			
		||||
    const curAdmin = request['admin'];
 | 
			
		||||
 | 
			
		||||
    let newIds = ids;
 | 
			
		||||
 | 
			
		||||
    if (curAdmin) {
 | 
			
		||||
      newIds = ids.filter((item) => item !== curAdmin?.id);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const result = await this.adminRepo.delete({
 | 
			
		||||
      id: In(ids),
 | 
			
		||||
      is_system_account: false,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    if (!result.affected) {
 | 
			
		||||
      throw new BadRequestException(
 | 
			
		||||
        AppResponse.toResponse(false, {
 | 
			
		||||
          message: 'No items have been deleted yet.',
 | 
			
		||||
          status_code: HttpStatus.BAD_REQUEST,
 | 
			
		||||
        }),
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return AppResponse.toResponse(true, { message: 'Delete success !' });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async create({
 | 
			
		||||
    password,
 | 
			
		||||
    ...data
 | 
			
		||||
  }: Omit<
 | 
			
		||||
    Admin,
 | 
			
		||||
    | 'id'
 | 
			
		||||
    | 'created_at'
 | 
			
		||||
    | 'updated_at'
 | 
			
		||||
    | 'is_system_account'
 | 
			
		||||
    | 'permissions'
 | 
			
		||||
    | 'posts'
 | 
			
		||||
  >) {
 | 
			
		||||
    const hashPassword = await bcrypt.hash(password, 10);
 | 
			
		||||
 | 
			
		||||
    const newAdmin = await this.adminRepo.save({
 | 
			
		||||
      ...data,
 | 
			
		||||
      password: hashPassword,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    if (!newAdmin)
 | 
			
		||||
      throw new BadRequestException(
 | 
			
		||||
        AppResponse.toResponse(false, {
 | 
			
		||||
          message: "Can't create account",
 | 
			
		||||
          status_code: HttpStatus.BAD_REQUEST,
 | 
			
		||||
        }),
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
    return AppResponse.toResponse(true, { message: 'Create success' });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async grantNewPassword(id: Admin['id'], password: string, request: Request) {
 | 
			
		||||
    const hashPassword = await bcrypt.hash(password, 10);
 | 
			
		||||
 | 
			
		||||
    const admin = request['admin'];
 | 
			
		||||
 | 
			
		||||
    if (admin && admin.id == id) {
 | 
			
		||||
      throw new BadRequestException(
 | 
			
		||||
        AppResponse.toResponse(false, {
 | 
			
		||||
          message:
 | 
			
		||||
            'This future not support for this account, Please use change password !',
 | 
			
		||||
          status_code: HttpStatus.BAD_REQUEST,
 | 
			
		||||
        }),
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const result = await this.adminRepo.update(id, {
 | 
			
		||||
      password: hashPassword,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    if (!result)
 | 
			
		||||
      throw new BadRequestException(
 | 
			
		||||
        AppResponse.toResponse(false, {
 | 
			
		||||
          message: "Can't create account",
 | 
			
		||||
          status_code: HttpStatus.BAD_REQUEST,
 | 
			
		||||
        }),
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
    return AppResponse.toResponse(true, { message: 'Grant success' });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async update(id: Admin['id'], { permissions, ...data }: Partial<Admin>) {
 | 
			
		||||
    try {
 | 
			
		||||
      const admin = await this.adminRepo.findOne({ where: { id } });
 | 
			
		||||
 | 
			
		||||
      if (!admin)
 | 
			
		||||
        throw new NotFoundException(
 | 
			
		||||
          AppResponse.toResponse(false, {
 | 
			
		||||
            message: 'Admin is not found',
 | 
			
		||||
            status_code: HttpStatus.NOT_FOUND,
 | 
			
		||||
          }),
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
      if (data.email) {
 | 
			
		||||
        const isDuplicateAdmin = await this.adminRepo.findOne({
 | 
			
		||||
          where: { email: data.email, id: Not(id) },
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        if (isDuplicateAdmin) {
 | 
			
		||||
          throw new BadRequestException(
 | 
			
		||||
            AppResponse.toResponse(false, {
 | 
			
		||||
              message: 'Email is already exits!',
 | 
			
		||||
              status_code: HttpStatus.BAD_REQUEST,
 | 
			
		||||
            }),
 | 
			
		||||
          );
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (permissions) {
 | 
			
		||||
        const permissionIds = permissions.map((p) => p.id);
 | 
			
		||||
        const permissionsData = await this.permissionRepo.findBy({
 | 
			
		||||
          id: In(permissionIds),
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        admin.permissions = permissionsData;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (data.fullname) {
 | 
			
		||||
        admin.fullname = data.fullname;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      await this.adminRepo.save(admin);
 | 
			
		||||
 | 
			
		||||
      return AppResponse.toResponse(true, { message: 'Update success !' });
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      throw new BadRequestException(
 | 
			
		||||
        AppResponse.toResponse(null, {
 | 
			
		||||
          message: error.message,
 | 
			
		||||
          status_code: HttpStatus.INTERNAL_SERVER_ERROR,
 | 
			
		||||
        }),
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,53 @@
 | 
			
		|||
import { Injectable } from '@nestjs/common';
 | 
			
		||||
import { InjectRepository } from '@nestjs/typeorm';
 | 
			
		||||
import { Repository } from 'typeorm';
 | 
			
		||||
import {
 | 
			
		||||
  FilterOperator,
 | 
			
		||||
  FilterSuffix,
 | 
			
		||||
  paginate,
 | 
			
		||||
  PaginateQuery,
 | 
			
		||||
} from 'nestjs-paginate';
 | 
			
		||||
import { Column } from 'nestjs-paginate/lib/helper';
 | 
			
		||||
import AppResponse from '@/response/app-response';
 | 
			
		||||
import Permission from '../entities/permission.entity';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class PermissionService {
 | 
			
		||||
  constructor(
 | 
			
		||||
    @InjectRepository(Permission)
 | 
			
		||||
    readonly permissionRepo: Repository<Permission>,
 | 
			
		||||
  ) {}
 | 
			
		||||
 | 
			
		||||
  async getPermissionsByAdmin(adminId: number): Promise<Permission[]> {
 | 
			
		||||
    return await this.permissionRepo
 | 
			
		||||
      .createQueryBuilder('permission')
 | 
			
		||||
      .innerJoin('permission.admins', 'admin')
 | 
			
		||||
      .where('admin.id = :adminId', { adminId })
 | 
			
		||||
      .getMany();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async index(query: PaginateQuery) {
 | 
			
		||||
    const filterableColumns: {
 | 
			
		||||
      [key in Column<Permission> | (string & {})]?:
 | 
			
		||||
        | (FilterOperator | FilterSuffix)[]
 | 
			
		||||
        | true;
 | 
			
		||||
    } = {
 | 
			
		||||
      id: true,
 | 
			
		||||
      created_at: true,
 | 
			
		||||
      updated_at: true,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    query.filter = AppResponse.processFilters(query.filter, filterableColumns);
 | 
			
		||||
 | 
			
		||||
    const data = await paginate(query, this.permissionRepo, {
 | 
			
		||||
      sortableColumns: ['id'],
 | 
			
		||||
      searchableColumns: ['id'],
 | 
			
		||||
      defaultLimit: 15,
 | 
			
		||||
      filterableColumns,
 | 
			
		||||
      defaultSortBy: [['id', 'DESC']],
 | 
			
		||||
      maxLimit: 100,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return AppResponse.toPagination<Permission>(data, true, Permission);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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 {}
 | 
			
		||||
| 
						 | 
				
			
			@ -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);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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 {}
 | 
			
		||||
| 
						 | 
				
			
			@ -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);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,11 @@
 | 
			
		|||
import { IsNotEmpty, IsString } from 'class-validator';
 | 
			
		||||
 | 
			
		||||
export class ChangePasswordDto {
 | 
			
		||||
  @IsNotEmpty()
 | 
			
		||||
  @IsString()
 | 
			
		||||
  newPassword: string;
 | 
			
		||||
 | 
			
		||||
  @IsNotEmpty()
 | 
			
		||||
  @IsString()
 | 
			
		||||
  password: string;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,11 @@
 | 
			
		|||
import { IsNotEmpty, IsString } from 'class-validator';
 | 
			
		||||
 | 
			
		||||
export class LoginDto {
 | 
			
		||||
  @IsNotEmpty()
 | 
			
		||||
  @IsString()
 | 
			
		||||
  username: string;
 | 
			
		||||
 | 
			
		||||
  @IsNotEmpty()
 | 
			
		||||
  @IsString()
 | 
			
		||||
  password: string;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,98 @@
 | 
			
		|||
// auth.middleware.ts
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  ForbiddenException,
 | 
			
		||||
  HttpStatus,
 | 
			
		||||
  Injectable,
 | 
			
		||||
  NestMiddleware,
 | 
			
		||||
  UnauthorizedException,
 | 
			
		||||
} from '@nestjs/common';
 | 
			
		||||
import { JwtService } from '@nestjs/jwt';
 | 
			
		||||
import { NextFunction, Request, Response } from 'express';
 | 
			
		||||
import { AdminsService } from '@/modules/admins/services/admins.service';
 | 
			
		||||
import { AuthService } from '@/modules/auth/auth.service';
 | 
			
		||||
import { plainToClass } from 'class-transformer';
 | 
			
		||||
import Admin from '@/modules/admins/entities/admin.entity';
 | 
			
		||||
import AppResponse from '@/response/app-response';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class AuthenticationMiddleware implements NestMiddleware {
 | 
			
		||||
  constructor(
 | 
			
		||||
    private readonly jwtService: JwtService,
 | 
			
		||||
    private readonly adminService: AdminsService,
 | 
			
		||||
    private readonly authService: AuthService,
 | 
			
		||||
  ) {}
 | 
			
		||||
 | 
			
		||||
  async validate(result: Record<string, any>, next: NextFunction) {
 | 
			
		||||
    const admin = await this.adminService.adminRepo.findOne({
 | 
			
		||||
      where: { id: result?._id },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    if (!admin) {
 | 
			
		||||
      return next(
 | 
			
		||||
        new UnauthorizedException(
 | 
			
		||||
          AppResponse.toResponse(null, {
 | 
			
		||||
            message: 'User is invalid',
 | 
			
		||||
            status_code: HttpStatus.UNAUTHORIZED,
 | 
			
		||||
            bonus: { redirect: true },
 | 
			
		||||
          }),
 | 
			
		||||
        ),
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return admin;
 | 
			
		||||
  }
 | 
			
		||||
  async use(req: Request, res: Response, next: NextFunction) {
 | 
			
		||||
    const { access_token, refresh_token }: Record<string, any> = req.cookies;
 | 
			
		||||
 | 
			
		||||
    if (!access_token && !refresh_token) {
 | 
			
		||||
      return next(
 | 
			
		||||
        new UnauthorizedException(
 | 
			
		||||
          AppResponse.toResponse(false, {
 | 
			
		||||
            message: 'Please login to app',
 | 
			
		||||
            status_code: HttpStatus.UNAUTHORIZED,
 | 
			
		||||
            bonus: { redirect: true },
 | 
			
		||||
          }),
 | 
			
		||||
        ),
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      const result = await this.jwtService.verify(access_token);
 | 
			
		||||
 | 
			
		||||
      if (result.refresh_key || !result?._id) {
 | 
			
		||||
        return next(
 | 
			
		||||
          new ForbiddenException(
 | 
			
		||||
            AppResponse.toResponse(null, {
 | 
			
		||||
              message: 'Token is invalid',
 | 
			
		||||
              status_code: HttpStatus.FORBIDDEN,
 | 
			
		||||
            }),
 | 
			
		||||
          ),
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const admin = await this.validate(result, next);
 | 
			
		||||
 | 
			
		||||
      req['admin'] = plainToClass(Admin, admin);
 | 
			
		||||
      next();
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      try {
 | 
			
		||||
        const result = await this.jwtService.verify(refresh_token);
 | 
			
		||||
        const admin = (await this.validate(result, next)) as Admin;
 | 
			
		||||
 | 
			
		||||
        await this.authService.login(admin, res);
 | 
			
		||||
        req['admin'] = plainToClass(Admin, admin);
 | 
			
		||||
        next();
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        return next(
 | 
			
		||||
          new UnauthorizedException(
 | 
			
		||||
            AppResponse.toResponse(null, {
 | 
			
		||||
              message: 'Token is expired',
 | 
			
		||||
              status_code: HttpStatus.UNAUTHORIZED,
 | 
			
		||||
            }),
 | 
			
		||||
          ),
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,4 @@
 | 
			
		|||
export class Constant {
 | 
			
		||||
  public static ACCESS_TOKEN = 'access_token';
 | 
			
		||||
  public static REFRESH_TOKEN = 'refresh_token';
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -5,8 +5,9 @@ export class ClientUpdateBidDto {
 | 
			
		|||
  @IsOptional()
 | 
			
		||||
  lot_id: string;
 | 
			
		||||
 | 
			
		||||
  @IsOptional()
 | 
			
		||||
  @IsString()
 | 
			
		||||
  close_time: string;
 | 
			
		||||
  close_time: string | null;
 | 
			
		||||
 | 
			
		||||
  @IsString()
 | 
			
		||||
  @IsOptional()
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,24 @@
 | 
			
		|||
import AppResponse from '@/response/app-response';
 | 
			
		||||
import {
 | 
			
		||||
  Injectable,
 | 
			
		||||
  ExecutionContext,
 | 
			
		||||
  HttpException,
 | 
			
		||||
  HttpStatus,
 | 
			
		||||
} from '@nestjs/common';
 | 
			
		||||
import { ThrottlerGuard, ThrottlerLimitDetail } from '@nestjs/throttler';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class RateLimitGuard extends ThrottlerGuard {
 | 
			
		||||
  protected async throwThrottlingException(
 | 
			
		||||
    context: ExecutionContext,
 | 
			
		||||
    throttlerLimitDetail: ThrottlerLimitDetail,
 | 
			
		||||
  ): Promise<void> {
 | 
			
		||||
    throw new HttpException(
 | 
			
		||||
      AppResponse.toResponse(null, {
 | 
			
		||||
        message: 'To many request',
 | 
			
		||||
        status_code: HttpStatus.TOO_MANY_REQUESTS,
 | 
			
		||||
      }),
 | 
			
		||||
      HttpStatus.TOO_MANY_REQUESTS,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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)[];
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,24 @@
 | 
			
		|||
import { registerDecorator, ValidationOptions } from 'class-validator';
 | 
			
		||||
import { HasExistedValidator } from '../has-existed.validator';
 | 
			
		||||
 | 
			
		||||
export type IsExitedInput = {
 | 
			
		||||
    tableName: string;
 | 
			
		||||
    column: string;
 | 
			
		||||
    queryOption?: Record<string, any>;
 | 
			
		||||
    message?: string;
 | 
			
		||||
    each?: boolean;
 | 
			
		||||
    update?: boolean;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function HasExisted(options: IsExitedInput, validationOptions?: ValidationOptions) {
 | 
			
		||||
    return function (object: any, propertyName: string) {
 | 
			
		||||
        registerDecorator({
 | 
			
		||||
            name: 'has-exited',
 | 
			
		||||
            target: object.constructor,
 | 
			
		||||
            propertyName: propertyName,
 | 
			
		||||
            options: validationOptions,
 | 
			
		||||
            constraints: [options],
 | 
			
		||||
            validator: HasExistedValidator,
 | 
			
		||||
        });
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,21 @@
 | 
			
		|||
import { registerDecorator, ValidationOptions } from 'class-validator';
 | 
			
		||||
import { IsUniqueValidator } from '../is-unique.validator';
 | 
			
		||||
 | 
			
		||||
export type IsUniqueInput = {
 | 
			
		||||
    tableName: string;
 | 
			
		||||
    column: string;
 | 
			
		||||
    queryOption?: Record<string, any>;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function IsUnique(options: IsUniqueInput, validationOptions?: ValidationOptions) {
 | 
			
		||||
    return function (object: any, propertyName: string) {
 | 
			
		||||
        registerDecorator({
 | 
			
		||||
            name: 'is-unique',
 | 
			
		||||
            target: object.constructor,
 | 
			
		||||
            propertyName: propertyName,
 | 
			
		||||
            options: validationOptions,
 | 
			
		||||
            constraints: [options],
 | 
			
		||||
            validator: IsUniqueValidator,
 | 
			
		||||
        });
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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`;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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`;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -16,6 +16,9 @@
 | 
			
		|||
    "noImplicitAny": false,
 | 
			
		||||
    "strictBindCallApply": false,
 | 
			
		||||
    "forceConsistentCasingInFileNames": false,
 | 
			
		||||
    "noFallthroughCasesInSwitch": false
 | 
			
		||||
    "noFallthroughCasesInSwitch": false,
 | 
			
		||||
    "paths": {
 | 
			
		||||
      "@/*": ["src/*"]
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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();
 | 
			
		||||
})();
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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);
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue