Deploy to production #11
			
				
			
		
		
		
	| 
						 | 
				
			
			@ -19,6 +19,20 @@ export const login = async (credentials: { username: string; password: string })
 | 
			
		|||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const checkStatus = async () => {
 | 
			
		||||
    try {
 | 
			
		||||
        const { data } = await axios({
 | 
			
		||||
            url: 'auth/check-status',
 | 
			
		||||
            withCredentials: true,
 | 
			
		||||
            method: 'POST',
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return data as IResponse<boolean>;
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
        handleError(error);
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const logout = async () => {
 | 
			
		||||
    try {
 | 
			
		||||
        const { data } = await axios({
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,67 @@
 | 
			
		|||
import { generateNestParams, handleError, handleSuccess } from '.';
 | 
			
		||||
import axios from '../lib/axios';
 | 
			
		||||
import { IKey } from '../system/type';
 | 
			
		||||
 | 
			
		||||
export const getKeys = async (params: Record<string, string | number>) => {
 | 
			
		||||
    return await axios({
 | 
			
		||||
        url: 'keys',
 | 
			
		||||
        params: generateNestParams(params),
 | 
			
		||||
        withCredentials: true,
 | 
			
		||||
        method: 'GET',
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const createKey = async () => {
 | 
			
		||||
    try {
 | 
			
		||||
        const { data } = await axios({
 | 
			
		||||
            url: 'keys',
 | 
			
		||||
            withCredentials: true,
 | 
			
		||||
            method: 'POST',
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        handleSuccess(data);
 | 
			
		||||
 | 
			
		||||
        return data;
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
        handleError(error);
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const deleteKey = async (key: IKey) => {
 | 
			
		||||
    try {
 | 
			
		||||
        const { data } = await axios({
 | 
			
		||||
            url: 'keys/' + key.id,
 | 
			
		||||
            withCredentials: true,
 | 
			
		||||
            method: 'DELETE',
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        handleSuccess(data);
 | 
			
		||||
 | 
			
		||||
        return data;
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
        handleError(error);
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const deletesKey = async (keys: IKey[]) => {
 | 
			
		||||
    const ids = keys.reduce((prev, cur) => {
 | 
			
		||||
        prev.push(cur.id);
 | 
			
		||||
        return prev;
 | 
			
		||||
    }, [] as number[]);
 | 
			
		||||
    try {
 | 
			
		||||
        const { data } = await axios({
 | 
			
		||||
            url: 'keys/deletes',
 | 
			
		||||
            withCredentials: true,
 | 
			
		||||
            method: 'POST',
 | 
			
		||||
            data: {
 | 
			
		||||
                ids,
 | 
			
		||||
            },
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        handleSuccess(data);
 | 
			
		||||
 | 
			
		||||
        return data;
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
        handleError(error);
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -1,13 +1,14 @@
 | 
			
		|||
import { Avatar, Button, LoadingOverlay, Menu, Modal, PasswordInput } from '@mantine/core';
 | 
			
		||||
import { useForm, zodResolver } from '@mantine/form';
 | 
			
		||||
import { useDisclosure } from '@mantine/hooks';
 | 
			
		||||
import { IconLogout, IconSettings, IconUser } from '@tabler/icons-react';
 | 
			
		||||
import { IconKey, IconLogout, IconSettings, IconUser } from '@tabler/icons-react';
 | 
			
		||||
import { useState } from 'react';
 | 
			
		||||
import { useNavigate } from 'react-router';
 | 
			
		||||
import { Link } from 'react-router-dom';
 | 
			
		||||
import { z } from 'zod';
 | 
			
		||||
import { changePassword, logout } from '../apis/auth';
 | 
			
		||||
import { useConfirmStore } from '../lib/zustand/use-confirm';
 | 
			
		||||
import Links from '../system/links';
 | 
			
		||||
import { useState } from 'react';
 | 
			
		||||
 | 
			
		||||
const schema = z
 | 
			
		||||
    .object({
 | 
			
		||||
| 
						 | 
				
			
			@ -92,6 +93,9 @@ export default function UserMenu() {
 | 
			
		|||
                    <Menu.Item onClick={open} leftSection={<IconSettings size={14} />}>
 | 
			
		||||
                        Change password
 | 
			
		||||
                    </Menu.Item>
 | 
			
		||||
                    <Menu.Item component={Link} to={Links.GENERATE_KEYS} leftSection={<IconKey size={14} />}>
 | 
			
		||||
                        Keys
 | 
			
		||||
                    </Menu.Item>
 | 
			
		||||
 | 
			
		||||
                    <Menu.Divider />
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -29,7 +29,7 @@ export default function PrivateLayout() {
 | 
			
		|||
                    <Logo />
 | 
			
		||||
 | 
			
		||||
                    <Box className="flex items-center gap-4">
 | 
			
		||||
                        {Links.MENUS.map((menu, index) => (
 | 
			
		||||
                        {Links.MENUS.filter((i) => i.show).map((menu, index) => (
 | 
			
		||||
                            <Button
 | 
			
		||||
                                size="xs"
 | 
			
		||||
                                component={Link}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,11 +1,12 @@
 | 
			
		|||
/* eslint-disable @typescript-eslint/no-explicit-any */
 | 
			
		||||
import { useEffect, useState } from 'react';
 | 
			
		||||
import { Box, Text, Title } from '@mantine/core';
 | 
			
		||||
import { useEffect, useRef, useState } from 'react';
 | 
			
		||||
import io from 'socket.io-client';
 | 
			
		||||
import { WorkingPage } from '../components/dashboard';
 | 
			
		||||
import { IBid, IWebBid } from '../system/type';
 | 
			
		||||
import { checkStatus } from '../apis/auth';
 | 
			
		||||
 | 
			
		||||
const socket = io(import.meta.env.VITE_SOCKET_URL, {
 | 
			
		||||
const socket = io(`${import.meta.env.VITE_SOCKET_URL}/admin-bid-ws`, {
 | 
			
		||||
    autoConnect: true,
 | 
			
		||||
    transports: ['websocket'],
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -13,18 +14,24 @@ const socket = io(import.meta.env.VITE_SOCKET_URL, {
 | 
			
		|||
export default function DashBoard() {
 | 
			
		||||
    const [workingData, setWorkingData] = useState<(IWebBid & { type: string })[] | (IBid & { type: string })[]>([]);
 | 
			
		||||
 | 
			
		||||
    const RETRY_CONNECT = useRef(2);
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        socket.connect();
 | 
			
		||||
 | 
			
		||||
        socket.on('connect', () => {
 | 
			
		||||
            console.log('✅ WebSocket connected:', socket.id);
 | 
			
		||||
 | 
			
		||||
            // 🔥 Gửi yêu cầu lấy dữ liệu ngay khi kết nối
 | 
			
		||||
            socket.emit('getBidsData');
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        socket.on('disconnect', () => {
 | 
			
		||||
            console.log('❌ WebSocket disconnected');
 | 
			
		||||
        socket.on('disconnect', async () => {
 | 
			
		||||
            if (RETRY_CONNECT.current > 0) {
 | 
			
		||||
                await checkStatus();
 | 
			
		||||
 | 
			
		||||
                socket.connect();
 | 
			
		||||
 | 
			
		||||
                RETRY_CONNECT.current--;
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        socket.on('adminBidsUpdated', (data: IWebBid[]) => {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,168 @@
 | 
			
		|||
import { ActionIcon, Box, CopyButton, Menu, TextInput } from '@mantine/core';
 | 
			
		||||
import { IconCopy, IconMenu, IconTrash } from '@tabler/icons-react';
 | 
			
		||||
import { useMemo, useRef } from 'react';
 | 
			
		||||
import { createKey, deleteKey, deletesKey, getKeys } from '../apis/generate-key';
 | 
			
		||||
import Table from '../lib/table/table';
 | 
			
		||||
import { IColumn, TRefTableFn } from '../lib/table/type';
 | 
			
		||||
import { useConfirmStore } from '../lib/zustand/use-confirm';
 | 
			
		||||
import { IKey } from '../system/type';
 | 
			
		||||
import { formatTime } from '../utils';
 | 
			
		||||
 | 
			
		||||
export default function GenerateKeys() {
 | 
			
		||||
    const refTableFn: TRefTableFn<IKey> = useRef({});
 | 
			
		||||
 | 
			
		||||
    const { setConfirm } = useConfirmStore();
 | 
			
		||||
 | 
			
		||||
    const columns: IColumn<IKey>[] = [
 | 
			
		||||
        {
 | 
			
		||||
            key: 'id',
 | 
			
		||||
            title: 'ID',
 | 
			
		||||
            typeFilter: 'number',
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            key: 'client_key',
 | 
			
		||||
            title: 'Client key',
 | 
			
		||||
            typeFilter: 'text',
 | 
			
		||||
            renderRow(row) {
 | 
			
		||||
                return (
 | 
			
		||||
                    <CopyButton value={row.client_key}>
 | 
			
		||||
                        {({ copied, copy }) => (
 | 
			
		||||
                            <TextInput
 | 
			
		||||
                                size="xs"
 | 
			
		||||
                                value={row.client_key}
 | 
			
		||||
                                rightSection={
 | 
			
		||||
                                    <ActionIcon className="z-30 cursor-pointer" size="xs" color={copied ? 'teal' : 'blue'} onClick={copy}>
 | 
			
		||||
                                        <IconCopy size={12} />
 | 
			
		||||
                                    </ActionIcon>
 | 
			
		||||
                                }
 | 
			
		||||
                            />
 | 
			
		||||
                        )}
 | 
			
		||||
                    </CopyButton>
 | 
			
		||||
                );
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            key: 'created_at',
 | 
			
		||||
            title: 'Create at',
 | 
			
		||||
            typeFilter: 'none',
 | 
			
		||||
            renderRow(row) {
 | 
			
		||||
                return <span className="text-sm">{formatTime(row.created_at)}</span>;
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    const handleDelete = (bid: IKey) => {
 | 
			
		||||
        setConfirm({
 | 
			
		||||
            title: 'Delete ?',
 | 
			
		||||
            message: 'This key will be delete',
 | 
			
		||||
            handleOk: async () => {
 | 
			
		||||
                await deleteKey(bid);
 | 
			
		||||
 | 
			
		||||
                if (refTableFn.current?.fetchData) {
 | 
			
		||||
                    refTableFn.current.fetchData();
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
        });
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const table = useMemo(() => {
 | 
			
		||||
        return (
 | 
			
		||||
            <Table
 | 
			
		||||
                actionsOptions={{
 | 
			
		||||
                    actions: [
 | 
			
		||||
                        {
 | 
			
		||||
                            key: 'create',
 | 
			
		||||
                            title: 'New key',
 | 
			
		||||
                            callback: async () => {
 | 
			
		||||
                                setConfirm({
 | 
			
		||||
                                    okButton: {
 | 
			
		||||
                                        color: 'lime',
 | 
			
		||||
                                        value: 'Ok',
 | 
			
		||||
                                    },
 | 
			
		||||
                                    title: 'A new key will generate',
 | 
			
		||||
                                    handleOk: async () => {
 | 
			
		||||
                                        await createKey();
 | 
			
		||||
 | 
			
		||||
                                        if (!refTableFn.current?.fetchData) return;
 | 
			
		||||
 | 
			
		||||
                                        refTableFn.current.fetchData();
 | 
			
		||||
                                    },
 | 
			
		||||
                                });
 | 
			
		||||
                            },
 | 
			
		||||
                        },
 | 
			
		||||
                        {
 | 
			
		||||
                            key: 'delete',
 | 
			
		||||
                            title: 'Delete',
 | 
			
		||||
                            callback: (data) => {
 | 
			
		||||
                                if (!data.length) return;
 | 
			
		||||
                                setConfirm({
 | 
			
		||||
                                    title: 'Delete',
 | 
			
		||||
                                    message: `${data.length} keys will be delete`,
 | 
			
		||||
                                    handleOk: async () => {
 | 
			
		||||
                                        const result = await deletesKey(data);
 | 
			
		||||
 | 
			
		||||
                                        if (!result) return;
 | 
			
		||||
                                        if (refTableFn.current.fetchData) {
 | 
			
		||||
                                            refTableFn.current.fetchData();
 | 
			
		||||
                                        }
 | 
			
		||||
                                    },
 | 
			
		||||
                                });
 | 
			
		||||
                            },
 | 
			
		||||
                            disabled: (data) => data.length <= 0,
 | 
			
		||||
                        },
 | 
			
		||||
                    ],
 | 
			
		||||
                }}
 | 
			
		||||
                refTableFn={refTableFn}
 | 
			
		||||
                striped
 | 
			
		||||
                showLoading={true}
 | 
			
		||||
                highlightOnHover
 | 
			
		||||
                styleDefaultHead={{
 | 
			
		||||
                    justifyContent: 'flex-start',
 | 
			
		||||
                    width: 'fit-content',
 | 
			
		||||
                }}
 | 
			
		||||
                options={{
 | 
			
		||||
                    query: getKeys,
 | 
			
		||||
                    pathToData: 'data.data',
 | 
			
		||||
                    keyOptions: {
 | 
			
		||||
                        last_page: 'lastPage',
 | 
			
		||||
                        per_page: 'perPage',
 | 
			
		||||
                        from: 'from',
 | 
			
		||||
                        to: 'to',
 | 
			
		||||
                        total: 'total',
 | 
			
		||||
                    },
 | 
			
		||||
                }}
 | 
			
		||||
                actions={{
 | 
			
		||||
                    title: <Box className="w-full text-center">Action</Box>,
 | 
			
		||||
                    body: (row) => {
 | 
			
		||||
                        return (
 | 
			
		||||
                            <Menu shadow="md" width={200}>
 | 
			
		||||
                                <Menu.Target>
 | 
			
		||||
                                    <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 onClick={(e) => e.stopPropagation()}>
 | 
			
		||||
                                    <Menu.Item onClick={() => handleDelete(row)} leftSection={<IconTrash color="red" size={14} />}>
 | 
			
		||||
                                        Delete
 | 
			
		||||
                                    </Menu.Item>
 | 
			
		||||
                                </Menu.Dropdown>
 | 
			
		||||
                            </Menu>
 | 
			
		||||
                        );
 | 
			
		||||
                    },
 | 
			
		||||
                }}
 | 
			
		||||
                rows={[]}
 | 
			
		||||
                withColumnBorders
 | 
			
		||||
                showChooses={true}
 | 
			
		||||
                withTableBorder
 | 
			
		||||
                columns={columns}
 | 
			
		||||
                rowKey="id"
 | 
			
		||||
            />
 | 
			
		||||
        );
 | 
			
		||||
        // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
			
		||||
    }, []);
 | 
			
		||||
 | 
			
		||||
    return <Box>{table}</Box>;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,14 +1,16 @@
 | 
			
		|||
import { IconHammer, IconHome2, IconMessage, IconOutlet, IconPageBreak, IconUserCheck } from '@tabler/icons-react';
 | 
			
		||||
import { IconHammer, IconHome2, IconKey, 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';
 | 
			
		||||
import GenerateKeys from '../pages/generate-keys';
 | 
			
		||||
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 GENERATE_KEYS = '/generate-keys';
 | 
			
		||||
    public static ADMINS = '/admins';
 | 
			
		||||
 | 
			
		||||
    public static HOME = '/';
 | 
			
		||||
| 
						 | 
				
			
			@ -20,36 +22,49 @@ export default class Links {
 | 
			
		|||
            title: 'Dashboard',
 | 
			
		||||
            icon: IconHome2,
 | 
			
		||||
            element: Dashboard,
 | 
			
		||||
            show: true,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            path: this.ADMINS,
 | 
			
		||||
            title: 'Admins',
 | 
			
		||||
            icon: IconUserCheck,
 | 
			
		||||
            element: Admins,
 | 
			
		||||
            show: true,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            path: this.WEBS,
 | 
			
		||||
            title: 'Webs',
 | 
			
		||||
            icon: IconPageBreak,
 | 
			
		||||
            element: WebBids,
 | 
			
		||||
            show: true,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            path: this.BIDS,
 | 
			
		||||
            title: 'Bids',
 | 
			
		||||
            icon: IconHammer,
 | 
			
		||||
            element: Bids,
 | 
			
		||||
            show: true,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            path: this.OUT_BIDS_LOG,
 | 
			
		||||
            title: 'Out bids log',
 | 
			
		||||
            icon: IconOutlet,
 | 
			
		||||
            element: OutBidsLog,
 | 
			
		||||
            show: true,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            path: this.SEND_MESSAGE_HISTORIES,
 | 
			
		||||
            title: 'Send message histories',
 | 
			
		||||
            icon: IconMessage,
 | 
			
		||||
            element: SendMessageHistories,
 | 
			
		||||
            show: true,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            path: this.GENERATE_KEYS,
 | 
			
		||||
            title: 'Generate keys',
 | 
			
		||||
            icon: IconKey,
 | 
			
		||||
            element: GenerateKeys,
 | 
			
		||||
            show: false,
 | 
			
		||||
        },
 | 
			
		||||
    ];
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,6 +8,11 @@ export interface IAdmin extends ITimestamp {
 | 
			
		|||
    permissions: [];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface IKey extends ITimestamp {
 | 
			
		||||
    id: number;
 | 
			
		||||
    client_key: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface ITimestamp {
 | 
			
		||||
    created_at: string;
 | 
			
		||||
    updated_at: string;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -24,6 +24,7 @@
 | 
			
		|||
        "bcrypt": "^5.1.1",
 | 
			
		||||
        "class-transformer": "^0.5.1",
 | 
			
		||||
        "class-validator": "^0.14.1",
 | 
			
		||||
        "cookie": "^1.0.2",
 | 
			
		||||
        "cookie-parser": "^1.4.7",
 | 
			
		||||
        "dayjs": "^1.11.13",
 | 
			
		||||
        "multer": "^1.4.5-lts.1",
 | 
			
		||||
| 
						 | 
				
			
			@ -32,13 +33,15 @@
 | 
			
		|||
        "reflect-metadata": "^0.2.0",
 | 
			
		||||
        "rxjs": "^7.8.1",
 | 
			
		||||
        "sharp": "^0.33.5",
 | 
			
		||||
        "typeorm": "^0.3.21"
 | 
			
		||||
        "typeorm": "^0.3.21",
 | 
			
		||||
        "uuid": "^11.1.0"
 | 
			
		||||
      },
 | 
			
		||||
      "devDependencies": {
 | 
			
		||||
        "@nestjs/cli": "^10.0.0",
 | 
			
		||||
        "@nestjs/schematics": "^10.0.0",
 | 
			
		||||
        "@nestjs/testing": "^10.0.0",
 | 
			
		||||
        "@types/bcrypt": "^5.0.2",
 | 
			
		||||
        "@types/cookie": "^0.6.0",
 | 
			
		||||
        "@types/cookie-parser": "^1.4.8",
 | 
			
		||||
        "@types/express": "^4.17.17",
 | 
			
		||||
        "@types/jest": "^29.5.2",
 | 
			
		||||
| 
						 | 
				
			
			@ -2823,6 +2826,13 @@
 | 
			
		|||
        "@types/node": "*"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@types/cookie": {
 | 
			
		||||
      "version": "0.6.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
 | 
			
		||||
      "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "license": "MIT"
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@types/cookie-parser": {
 | 
			
		||||
      "version": "1.4.8",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.8.tgz",
 | 
			
		||||
| 
						 | 
				
			
			@ -4719,12 +4729,12 @@
 | 
			
		|||
      "license": "MIT"
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/cookie": {
 | 
			
		||||
      "version": "0.7.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
 | 
			
		||||
      "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
 | 
			
		||||
      "version": "1.0.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
 | 
			
		||||
      "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">= 0.6"
 | 
			
		||||
        "node": ">=18"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/cookie-parser": {
 | 
			
		||||
| 
						 | 
				
			
			@ -5736,6 +5746,15 @@
 | 
			
		|||
        "url": "https://opencollective.com/express"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/express/node_modules/cookie": {
 | 
			
		||||
      "version": "0.7.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
 | 
			
		||||
      "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">= 0.6"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/express/node_modules/debug": {
 | 
			
		||||
      "version": "2.6.9",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
 | 
			
		||||
| 
						 | 
				
			
			@ -8088,16 +8107,6 @@
 | 
			
		|||
        "set-cookie-parser": "^2.6.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/light-my-request/node_modules/cookie": {
 | 
			
		||||
      "version": "1.0.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
 | 
			
		||||
      "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "peer": true,
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=18"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/lines-and-columns": {
 | 
			
		||||
      "version": "1.2.4",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -40,6 +40,7 @@
 | 
			
		|||
    "bcrypt": "^5.1.1",
 | 
			
		||||
    "class-transformer": "^0.5.1",
 | 
			
		||||
    "class-validator": "^0.14.1",
 | 
			
		||||
    "cookie": "^1.0.2",
 | 
			
		||||
    "cookie-parser": "^1.4.7",
 | 
			
		||||
    "dayjs": "^1.11.13",
 | 
			
		||||
    "multer": "^1.4.5-lts.1",
 | 
			
		||||
| 
						 | 
				
			
			@ -48,13 +49,15 @@
 | 
			
		|||
    "reflect-metadata": "^0.2.0",
 | 
			
		||||
    "rxjs": "^7.8.1",
 | 
			
		||||
    "sharp": "^0.33.5",
 | 
			
		||||
    "typeorm": "^0.3.21"
 | 
			
		||||
    "typeorm": "^0.3.21",
 | 
			
		||||
    "uuid": "^11.1.0"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@nestjs/cli": "^10.0.0",
 | 
			
		||||
    "@nestjs/schematics": "^10.0.0",
 | 
			
		||||
    "@nestjs/testing": "^10.0.0",
 | 
			
		||||
    "@types/bcrypt": "^5.0.2",
 | 
			
		||||
    "@types/cookie": "^0.6.0",
 | 
			
		||||
    "@types/cookie-parser": "^1.4.8",
 | 
			
		||||
    "@types/express": "^4.17.17",
 | 
			
		||||
    "@types/jest": "^29.5.2",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,8 +6,13 @@ import { AppValidatorsModule } from './modules/app-validators/app-validators.mod
 | 
			
		|||
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 {
 | 
			
		||||
  clientExcludeAuth,
 | 
			
		||||
  excludeAuth,
 | 
			
		||||
  excludeAuthor,
 | 
			
		||||
} from './system/routes/exclude-route';
 | 
			
		||||
import { AuthorizationMiddleware } from './modules/admins/middlewares/authorization.middleware';
 | 
			
		||||
import { ClientAuthenticationMiddleware } from './modules/auth/middlewares/client-authentication.middleware';
 | 
			
		||||
 | 
			
		||||
@Module({
 | 
			
		||||
  imports: [
 | 
			
		||||
| 
						 | 
				
			
			@ -32,5 +37,10 @@ export class AppModule {
 | 
			
		|||
      .apply(AuthorizationMiddleware)
 | 
			
		||||
      .exclude(...excludeAuthor)
 | 
			
		||||
      .forRoutes({ path: 'admin/*', method: RequestMethod.ALL });
 | 
			
		||||
 | 
			
		||||
    consumer
 | 
			
		||||
      .apply(ClientAuthenticationMiddleware)
 | 
			
		||||
      .exclude(...clientExcludeAuth)
 | 
			
		||||
      .forRoutes({ path: '/*', method: RequestMethod.ALL });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -12,7 +12,7 @@ async function bootstrap() {
 | 
			
		|||
  const prefix_version = '/api/v1';
 | 
			
		||||
 | 
			
		||||
  app.enableCors({
 | 
			
		||||
    origin: process.env.CORS.split(', '),
 | 
			
		||||
    origin: process.env.CORS.split(', ') || '*',
 | 
			
		||||
    methods: ['GET', 'PUT', 'POST', 'DELETE', 'PATCH'],
 | 
			
		||||
    allowedHeaders: 'Content-Type, Authorization',
 | 
			
		||||
    credentials: true,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,11 +6,18 @@ import { AdminsController } from './controllers/admins.controller';
 | 
			
		|||
import Permission from './entities/permission.entity';
 | 
			
		||||
import { PermissionService } from './services/permission.service';
 | 
			
		||||
import { PermissionsController } from './controllers/permissions.controller';
 | 
			
		||||
import { GenerateKey } from './entities/generate-key.entity';
 | 
			
		||||
import { GenerateKeysController } from './controllers/generate-keys.controller';
 | 
			
		||||
import { GenerateKeysService } from './services/generate-key.service';
 | 
			
		||||
 | 
			
		||||
@Module({
 | 
			
		||||
  imports: [TypeOrmModule.forFeature([Admin, Permission])],
 | 
			
		||||
  providers: [AdminsService, PermissionService],
 | 
			
		||||
  exports: [AdminsService, PermissionService],
 | 
			
		||||
  controllers: [AdminsController, PermissionsController],
 | 
			
		||||
  imports: [TypeOrmModule.forFeature([Admin, Permission, GenerateKey])],
 | 
			
		||||
  providers: [AdminsService, PermissionService, GenerateKeysService],
 | 
			
		||||
  exports: [AdminsService, PermissionService, GenerateKeysService],
 | 
			
		||||
  controllers: [
 | 
			
		||||
    AdminsController,
 | 
			
		||||
    PermissionsController,
 | 
			
		||||
    GenerateKeysController,
 | 
			
		||||
  ],
 | 
			
		||||
})
 | 
			
		||||
export class AdminsModule {}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,47 @@
 | 
			
		|||
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';
 | 
			
		||||
import { GenerateKeysService } from '../services/generate-key.service';
 | 
			
		||||
 | 
			
		||||
@Controller('admin/keys')
 | 
			
		||||
export class GenerateKeysController {
 | 
			
		||||
  constructor(private readonly generateKeyService: GenerateKeysService) {}
 | 
			
		||||
 | 
			
		||||
  @Get()
 | 
			
		||||
  async index(@Paginate() query: PaginateQuery) {
 | 
			
		||||
    return await this.generateKeyService.index(query);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Delete(':id')
 | 
			
		||||
  async delete(@Param('id') id: Admin['id']) {
 | 
			
		||||
    return await this.generateKeyService.delete(id);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Post('')
 | 
			
		||||
  async create(@Req() request: Request) {
 | 
			
		||||
    return await this.generateKeyService.create(request);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Post('deletes')
 | 
			
		||||
  async deletes(@Body() data: DeletesDto) {
 | 
			
		||||
    return await this.generateKeyService.deletes(data.ids);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,7 +1,15 @@
 | 
			
		|||
import { Exclude } from 'class-transformer';
 | 
			
		||||
import { Column, Entity, ManyToMany, PrimaryGeneratedColumn } from 'typeorm';
 | 
			
		||||
import {
 | 
			
		||||
  Column,
 | 
			
		||||
  Entity,
 | 
			
		||||
  ManyToMany,
 | 
			
		||||
  ManyToOne,
 | 
			
		||||
  OneToMany,
 | 
			
		||||
  PrimaryGeneratedColumn,
 | 
			
		||||
} from 'typeorm';
 | 
			
		||||
import Permission from './permission.entity';
 | 
			
		||||
import { Timestamp } from './timestamp';
 | 
			
		||||
import { GenerateKey } from './generate-key.entity';
 | 
			
		||||
 | 
			
		||||
@Entity('admins')
 | 
			
		||||
export default class Admin extends Timestamp {
 | 
			
		||||
| 
						 | 
				
			
			@ -29,4 +37,9 @@ export default class Admin extends Timestamp {
 | 
			
		|||
    onDelete: 'CASCADE',
 | 
			
		||||
  })
 | 
			
		||||
  permissions: Permission[];
 | 
			
		||||
 | 
			
		||||
  @OneToMany(() => GenerateKey, (key) => key.admin, {
 | 
			
		||||
    cascade: true,
 | 
			
		||||
  })
 | 
			
		||||
  generateKeys: GenerateKey[];
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,18 @@
 | 
			
		|||
import { Exclude } from 'class-transformer';
 | 
			
		||||
import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
 | 
			
		||||
import Admin from './admin.entity';
 | 
			
		||||
import { Timestamp } from './timestamp';
 | 
			
		||||
 | 
			
		||||
@Entity('generate_keys')
 | 
			
		||||
export class GenerateKey extends Timestamp {
 | 
			
		||||
  @PrimaryGeneratedColumn('increment')
 | 
			
		||||
  id: number;
 | 
			
		||||
 | 
			
		||||
  @Column({ unique: true })
 | 
			
		||||
  client_key: string;
 | 
			
		||||
 | 
			
		||||
  @ManyToOne(() => Admin, (admin) => admin.generateKeys, {
 | 
			
		||||
    onDelete: 'CASCADE',
 | 
			
		||||
  })
 | 
			
		||||
  admin: Admin;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -119,6 +119,7 @@ export class AdminsService {
 | 
			
		|||
    | 'is_system_account'
 | 
			
		||||
    | 'permissions'
 | 
			
		||||
    | 'posts'
 | 
			
		||||
    | 'generateKeys'
 | 
			
		||||
  >) {
 | 
			
		||||
    const hashPassword = await bcrypt.hash(password, 10);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,106 @@
 | 
			
		|||
import AppResponse from '@/response/app-response';
 | 
			
		||||
import {
 | 
			
		||||
  BadRequestException,
 | 
			
		||||
  HttpStatus,
 | 
			
		||||
  Injectable,
 | 
			
		||||
  NotFoundException,
 | 
			
		||||
} from '@nestjs/common';
 | 
			
		||||
import { InjectRepository } from '@nestjs/typeorm';
 | 
			
		||||
import { Request } from 'express';
 | 
			
		||||
import {
 | 
			
		||||
  FilterOperator,
 | 
			
		||||
  FilterSuffix,
 | 
			
		||||
  paginate,
 | 
			
		||||
  PaginateQuery,
 | 
			
		||||
} from 'nestjs-paginate';
 | 
			
		||||
import { Column } from 'nestjs-paginate/lib/helper';
 | 
			
		||||
import { In, Repository } from 'typeorm';
 | 
			
		||||
import { v1 as uuidV1 } from 'uuid';
 | 
			
		||||
import Admin from '../entities/admin.entity';
 | 
			
		||||
import { GenerateKey } from '../entities/generate-key.entity';
 | 
			
		||||
import Permission from '../entities/permission.entity';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class GenerateKeysService {
 | 
			
		||||
  constructor(
 | 
			
		||||
    @InjectRepository(GenerateKey)
 | 
			
		||||
    readonly generateKeyRepo: Repository<GenerateKey>,
 | 
			
		||||
    @InjectRepository(Permission)
 | 
			
		||||
    readonly permissionRepo: Repository<Permission>,
 | 
			
		||||
  ) {}
 | 
			
		||||
 | 
			
		||||
  async index(query: PaginateQuery) {
 | 
			
		||||
    const filterableColumns: {
 | 
			
		||||
      [key in Column<Admin> | (string & {})]?:
 | 
			
		||||
        | (FilterOperator | FilterSuffix)[]
 | 
			
		||||
        | true;
 | 
			
		||||
    } = {
 | 
			
		||||
      id: true,
 | 
			
		||||
      client_key: [FilterOperator.ILIKE],
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    query.filter = AppResponse.processFilters(query.filter, filterableColumns);
 | 
			
		||||
 | 
			
		||||
    const data = await paginate(query, this.generateKeyRepo, {
 | 
			
		||||
      sortableColumns: ['id', 'client_key'],
 | 
			
		||||
      searchableColumns: ['id', 'client_key'],
 | 
			
		||||
      defaultLimit: 15,
 | 
			
		||||
      filterableColumns,
 | 
			
		||||
      defaultSortBy: [['id', 'DESC']],
 | 
			
		||||
      maxLimit: 100,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return AppResponse.toPagination<GenerateKey>(data, true, GenerateKey);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async delete(id: Admin['id']) {
 | 
			
		||||
    const admin = await this.generateKeyRepo.findOne({ where: { id } });
 | 
			
		||||
 | 
			
		||||
    if (!admin)
 | 
			
		||||
      throw new NotFoundException(
 | 
			
		||||
        AppResponse.toResponse(false, {
 | 
			
		||||
          message: 'Key is not found',
 | 
			
		||||
          status_code: HttpStatus.NOT_FOUND,
 | 
			
		||||
        }),
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
    await this.generateKeyRepo.delete({ id: admin.id });
 | 
			
		||||
 | 
			
		||||
    return AppResponse.toResponse(true, { message: 'Delete success !' });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async deletes(ids: Admin['id'][]) {
 | 
			
		||||
    const result = await this.generateKeyRepo.delete({
 | 
			
		||||
      id: In(ids),
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    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(request: Request) {
 | 
			
		||||
    const admin = request['admin'];
 | 
			
		||||
 | 
			
		||||
    const clientKey = uuidV1();
 | 
			
		||||
 | 
			
		||||
    const result = await this.generateKeyRepo.save({
 | 
			
		||||
      admin: { id: admin.id },
 | 
			
		||||
      client_key: `client-${clientKey}-${admin.id}`,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    if (!result)
 | 
			
		||||
      throw new BadRequestException(
 | 
			
		||||
        AppResponse.toResponse(null, { message: `Can't create key` }),
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
    return AppResponse.toResponse(result);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,6 +1,6 @@
 | 
			
		|||
import { Module } from '@nestjs/common';
 | 
			
		||||
import { JwtModule } from '@nestjs/jwt';
 | 
			
		||||
import { AuthController } from './auth.controller';
 | 
			
		||||
import { AuthController } from './controllers/auth.controller';
 | 
			
		||||
import { AuthService } from './auth.service';
 | 
			
		||||
import { AdminsModule } from '../admins/admins.module';
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -14,12 +14,14 @@ import { Request, Response } from 'express';
 | 
			
		|||
import { Constant } from './ultils/constant';
 | 
			
		||||
import { ChangePasswordDto } from './dto/change-password.dto';
 | 
			
		||||
import AppResponse from '@/response/app-response';
 | 
			
		||||
import { GenerateKeysService } from '../admins/services/generate-key.service';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class AuthService {
 | 
			
		||||
  constructor(
 | 
			
		||||
    private jwtService: JwtService,
 | 
			
		||||
    private readonly adminService: AdminsService,
 | 
			
		||||
    private readonly generateKeyService: GenerateKeysService,
 | 
			
		||||
  ) {}
 | 
			
		||||
 | 
			
		||||
  async validateAdmin(
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,10 +7,13 @@ import {
 | 
			
		|||
  Res,
 | 
			
		||||
  Get,
 | 
			
		||||
} from '@nestjs/common';
 | 
			
		||||
import { AuthService } from './auth.service';
 | 
			
		||||
import { LoginDto } from './dto/login.dto';
 | 
			
		||||
import { AuthService } from '../auth.service';
 | 
			
		||||
import { LoginDto } from '../dto/login.dto';
 | 
			
		||||
import { Request, Response } from 'express';
 | 
			
		||||
import { ChangePasswordDto } from './dto/change-password.dto';
 | 
			
		||||
import { ChangePasswordDto } from '../dto/change-password.dto';
 | 
			
		||||
import AppResponse from '@/response/app-response';
 | 
			
		||||
import { Throttle } from '@nestjs/throttler';
 | 
			
		||||
import { GenerateTokenDto } from '../dto/generate-token.dto';
 | 
			
		||||
 | 
			
		||||
@Controller('admin/auth')
 | 
			
		||||
export class AuthController {
 | 
			
		||||
| 
						 | 
				
			
			@ -26,6 +29,12 @@ export class AuthController {
 | 
			
		|||
      .then((admin) => this.authService.login(admin, response));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Post('check-status')
 | 
			
		||||
  @Throttle({ default: { limit: 1, ttl: 30 * 1000 } })
 | 
			
		||||
  async checkStatus() {
 | 
			
		||||
    return AppResponse.toResponse(true);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Post('logout')
 | 
			
		||||
  async logout(@Res({ passthrough: true }) response: Response) {
 | 
			
		||||
    return await this.authService.logout(response);
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,6 @@
 | 
			
		|||
import { IsString } from 'class-validator';
 | 
			
		||||
 | 
			
		||||
export class GenerateTokenDto {
 | 
			
		||||
  @IsString()
 | 
			
		||||
  client_key: string;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,44 @@
 | 
			
		|||
// auth.middleware.ts
 | 
			
		||||
 | 
			
		||||
import { GenerateKeysService } from '@/modules/admins/services/generate-key.service';
 | 
			
		||||
import AppResponse from '@/response/app-response';
 | 
			
		||||
import {
 | 
			
		||||
  HttpStatus,
 | 
			
		||||
  Injectable,
 | 
			
		||||
  NestMiddleware,
 | 
			
		||||
  UnauthorizedException,
 | 
			
		||||
} from '@nestjs/common';
 | 
			
		||||
import { NextFunction, Request, Response } from 'express';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class ClientAuthenticationMiddleware implements NestMiddleware {
 | 
			
		||||
  constructor(private readonly generateKeysService: GenerateKeysService) {}
 | 
			
		||||
 | 
			
		||||
  async getKey(key: string) {
 | 
			
		||||
    if (!key) return null;
 | 
			
		||||
 | 
			
		||||
    const data = await this.generateKeysService.generateKeyRepo.findOne({
 | 
			
		||||
      where: { client_key: key },
 | 
			
		||||
    });
 | 
			
		||||
    return data;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async use(req: Request, res: Response, next: NextFunction) {
 | 
			
		||||
    const client_key: string = req.headers.authorization;
 | 
			
		||||
 | 
			
		||||
    const key = await this.getKey(client_key);
 | 
			
		||||
 | 
			
		||||
    if (!key) {
 | 
			
		||||
      return next(
 | 
			
		||||
        new UnauthorizedException(
 | 
			
		||||
          AppResponse.toResponse(null, {
 | 
			
		||||
            message: 'Unauthorized',
 | 
			
		||||
            status_code: HttpStatus.UNAUTHORIZED,
 | 
			
		||||
          }),
 | 
			
		||||
        ),
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    next();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,25 @@
 | 
			
		|||
import { GenerateKeysService } from '@/modules/admins/services/generate-key.service';
 | 
			
		||||
import { Socket } from 'socket.io';
 | 
			
		||||
 | 
			
		||||
export async function clientGetWayMiddleware(
 | 
			
		||||
  client: Socket,
 | 
			
		||||
  generateKeysService: GenerateKeysService,
 | 
			
		||||
): Promise<void> {
 | 
			
		||||
  const client_key = client.handshake.headers.authorization;
 | 
			
		||||
 | 
			
		||||
  if (!client_key) {
 | 
			
		||||
    client.disconnect();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const key = await generateKeysService.generateKeyRepo.findOne({
 | 
			
		||||
    where: { client_key },
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  if (!key) {
 | 
			
		||||
    client.disconnect();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  console.log(`✅ WebSocket authenticated: Client ID ${key.client_key}`);
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,50 @@
 | 
			
		|||
import { JwtService, TokenExpiredError } from '@nestjs/jwt';
 | 
			
		||||
import { AdminsService } from '@/modules/admins/services/admins.service';
 | 
			
		||||
import * as cookie from 'cookie';
 | 
			
		||||
import { Socket } from 'socket.io';
 | 
			
		||||
import { plainToClass } from 'class-transformer';
 | 
			
		||||
import Admin from '@/modules/admins/entities/admin.entity';
 | 
			
		||||
 | 
			
		||||
export async function getWayMiddleware(
 | 
			
		||||
  client: Socket,
 | 
			
		||||
  jwtService: JwtService,
 | 
			
		||||
  adminService: AdminsService,
 | 
			
		||||
): Promise<void> {
 | 
			
		||||
  try {
 | 
			
		||||
    const cookies = cookie.parse(client.handshake.headers.cookie || '');
 | 
			
		||||
    const accessToken = cookies['access_token'];
 | 
			
		||||
    const refreshToken = cookies['refresh_token'];
 | 
			
		||||
 | 
			
		||||
    if (!accessToken && !refreshToken) {
 | 
			
		||||
      console.log('🚫 No tokens provided, disconnecting WebSocket');
 | 
			
		||||
      client.disconnect();
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let admin: Admin | null = null;
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      const result = await jwtService.verify(accessToken);
 | 
			
		||||
 | 
			
		||||
      if (!result?._id || result.refresh_key) {
 | 
			
		||||
        throw new Error('Invalid access token');
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      admin = await adminService.adminRepo.findOne({
 | 
			
		||||
        where: { id: result._id },
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      if (!admin) throw new Error('Admin not found');
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.log('❗ Invalid access token, disconnecting WebSocket');
 | 
			
		||||
      client.disconnect();
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    client.data.admin = plainToClass(Admin, admin);
 | 
			
		||||
    console.log(`✅ WebSocket authenticated: Admin ID ${admin.id}`);
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    console.log('❌ WebSocket authentication error:', error);
 | 
			
		||||
    client.disconnect();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -22,6 +22,9 @@ import { GraysApi } from './apis/grays.api';
 | 
			
		|||
import { SendMessageHistory } from './entities/send-message-histories.entity';
 | 
			
		||||
import { SendMessageHistoriesService } from './services/send-message-histories.service';
 | 
			
		||||
import { AdminSendMessageHistoriesController } from './controllers/admin/admin-send-message-histories.controller';
 | 
			
		||||
import { AuthModule } from '../auth/auth.module';
 | 
			
		||||
import { AdminsModule } from '../admins/admins.module';
 | 
			
		||||
import { AdminBidGateway } from './getways/admin-bid-getway';
 | 
			
		||||
 | 
			
		||||
@Module({
 | 
			
		||||
  imports: [
 | 
			
		||||
| 
						 | 
				
			
			@ -35,6 +38,8 @@ import { AdminSendMessageHistoriesController } from './controllers/admin/admin-s
 | 
			
		|||
    EventEmitterModule.forRoot({
 | 
			
		||||
      wildcard: true,
 | 
			
		||||
    }),
 | 
			
		||||
    // AuthModule,
 | 
			
		||||
    AdminsModule,
 | 
			
		||||
  ],
 | 
			
		||||
  controllers: [
 | 
			
		||||
    BidsController,
 | 
			
		||||
| 
						 | 
				
			
			@ -50,6 +55,7 @@ import { AdminSendMessageHistoriesController } from './controllers/admin/admin-s
 | 
			
		|||
    BidsService,
 | 
			
		||||
    BidHistoriesService,
 | 
			
		||||
    BidGateway,
 | 
			
		||||
    AdminBidGateway,
 | 
			
		||||
    OutBidLogsService,
 | 
			
		||||
    WebBidsService,
 | 
			
		||||
    BotTelegramApi,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,57 @@
 | 
			
		|||
import {
 | 
			
		||||
  WebSocketGateway,
 | 
			
		||||
  WebSocketServer,
 | 
			
		||||
  SubscribeMessage,
 | 
			
		||||
  OnGatewayConnection,
 | 
			
		||||
} from '@nestjs/websockets';
 | 
			
		||||
import { Server, Socket } from 'socket.io';
 | 
			
		||||
import { EventEmitter2 } from '@nestjs/event-emitter';
 | 
			
		||||
import { BidsService } from '../services/bids.service';
 | 
			
		||||
import { WebBidsService } from '../services/web-bids.service';
 | 
			
		||||
import { plainToClass } from 'class-transformer';
 | 
			
		||||
import { WebBid } from '../entities/wed-bid.entity';
 | 
			
		||||
import * as cookie from 'cookie';
 | 
			
		||||
import { Constant } from '@/modules/auth/ultils/constant';
 | 
			
		||||
import { getWayMiddleware } from '@/modules/auth/middlewares/get-way.middleware';
 | 
			
		||||
import { AdminsService } from '@/modules/admins/services/admins.service';
 | 
			
		||||
import { JwtService } from '@nestjs/jwt';
 | 
			
		||||
 | 
			
		||||
@WebSocketGateway({
 | 
			
		||||
  namespace: 'admin-bid-ws',
 | 
			
		||||
  cors: {
 | 
			
		||||
    origin: '*',
 | 
			
		||||
    methods: ['GET', 'POST'],
 | 
			
		||||
    credentials: true,
 | 
			
		||||
  },
 | 
			
		||||
})
 | 
			
		||||
export class AdminBidGateway implements OnGatewayConnection {
 | 
			
		||||
  @WebSocketServer()
 | 
			
		||||
  server: Server;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    private eventEmitter: EventEmitter2,
 | 
			
		||||
    private readonly jwtService: JwtService,
 | 
			
		||||
    private webBidsService: WebBidsService,
 | 
			
		||||
    private readonly adminService: AdminsService,
 | 
			
		||||
  ) {}
 | 
			
		||||
 | 
			
		||||
  async onModuleInit() {
 | 
			
		||||
    this.eventEmitter.on('bids.updated', (data) => {
 | 
			
		||||
      this.server.emit('adminBidsUpdated', plainToClass(WebBid, data));
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    this.eventEmitter.on('working', (data) => {
 | 
			
		||||
      this.server.emit('working', data);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async handleConnection(client: Socket) {
 | 
			
		||||
    await getWayMiddleware(client, this.jwtService, this.adminService);
 | 
			
		||||
 | 
			
		||||
    console.log(`📢 Client connected: ${client.id}`);
 | 
			
		||||
 | 
			
		||||
    const data = await this.webBidsService.getDataClient();
 | 
			
		||||
    // Gửi dữ liệu bids ngay khi client kết nối
 | 
			
		||||
    client.emit('adminBidsUpdated', plainToClass(WebBid, data));
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,17 +1,17 @@
 | 
			
		|||
import { GenerateKeysService } from '@/modules/admins/services/generate-key.service';
 | 
			
		||||
import { clientGetWayMiddleware } from '@/modules/auth/middlewares/client-get-way.middleware copy';
 | 
			
		||||
import { EventEmitter2 } from '@nestjs/event-emitter';
 | 
			
		||||
import {
 | 
			
		||||
  OnGatewayConnection,
 | 
			
		||||
  WebSocketGateway,
 | 
			
		||||
  WebSocketServer,
 | 
			
		||||
  SubscribeMessage,
 | 
			
		||||
  OnGatewayConnection,
 | 
			
		||||
} from '@nestjs/websockets';
 | 
			
		||||
import { Server, Socket } from 'socket.io';
 | 
			
		||||
import { EventEmitter2 } from '@nestjs/event-emitter';
 | 
			
		||||
import { BidsService } from '../services/bids.service';
 | 
			
		||||
import { WebBidsService } from '../services/web-bids.service';
 | 
			
		||||
import { plainToClass } from 'class-transformer';
 | 
			
		||||
import { WebBid } from '../entities/wed-bid.entity';
 | 
			
		||||
 | 
			
		||||
@WebSocketGateway({
 | 
			
		||||
  namespace: 'bid-ws',
 | 
			
		||||
  cors: {
 | 
			
		||||
    origin: '*',
 | 
			
		||||
    methods: ['GET', 'POST'],
 | 
			
		||||
| 
						 | 
				
			
			@ -25,30 +25,26 @@ export class BidGateway implements OnGatewayConnection {
 | 
			
		|||
  constructor(
 | 
			
		||||
    private eventEmitter: EventEmitter2,
 | 
			
		||||
    private webBidsService: WebBidsService,
 | 
			
		||||
    private bidsService: BidsService,
 | 
			
		||||
    private generateKeysService: GenerateKeysService,
 | 
			
		||||
  ) {}
 | 
			
		||||
 | 
			
		||||
  async onModuleInit() {
 | 
			
		||||
    this.eventEmitter.on('bids.updated', (data) => {
 | 
			
		||||
      this.server.emit('bidsUpdated', data);
 | 
			
		||||
      this.server.emit('adminBidsUpdated', plainToClass(WebBid, data));
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    this.eventEmitter.on('web.updated', (data) => {
 | 
			
		||||
      this.server.emit('webUpdated', data);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    this.eventEmitter.on('working', (data) => {
 | 
			
		||||
      this.server.emit('working', data);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async handleConnection(client: Socket) {
 | 
			
		||||
    await clientGetWayMiddleware(client, this.generateKeysService);
 | 
			
		||||
 | 
			
		||||
    console.log(`📢 Client connected: ${client.id}`);
 | 
			
		||||
 | 
			
		||||
    const data = await this.webBidsService.getDataClient();
 | 
			
		||||
    // Gửi dữ liệu bids ngay khi client kết nối
 | 
			
		||||
    client.emit('bidsUpdated', data);
 | 
			
		||||
    client.emit('adminBidsUpdated', plainToClass(WebBid, data));
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -12,6 +12,14 @@ export const excludeAuth = [
 | 
			
		|||
  },
 | 
			
		||||
] as (string | RouteInfo)[];
 | 
			
		||||
 | 
			
		||||
export const clientExcludeAuth = [
 | 
			
		||||
  { path: '/admin/(.*)', method: RequestMethod.ALL },
 | 
			
		||||
  {
 | 
			
		||||
    path: 'auth/generate-token',
 | 
			
		||||
    method: RequestMethod.POST,
 | 
			
		||||
  },
 | 
			
		||||
] as (string | RouteInfo)[];
 | 
			
		||||
 | 
			
		||||
export const excludeAuthor = [
 | 
			
		||||
  ...excludeAuth,
 | 
			
		||||
  {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,3 +1,4 @@
 | 
			
		|||
ENVIRONMENT = 'prod'
 | 
			
		||||
SOCKET_URL = 'http://localhost:4000'
 | 
			
		||||
BASE_URL = 'http://localhost:4000/api/v1/'
 | 
			
		||||
BASE_URL = 'http://localhost:4000/api/v1/'
 | 
			
		||||
CLIENT_KEY = ''
 | 
			
		||||
| 
						 | 
				
			
			@ -1,11 +1,11 @@
 | 
			
		|||
import 'dotenv/config';
 | 
			
		||||
import _ from 'lodash';
 | 
			
		||||
import pLimit from 'p-limit';
 | 
			
		||||
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 { delay, isTimeReached, safeClosePage } from './system/utils.js';
 | 
			
		||||
import pLimit from 'p-limit';
 | 
			
		||||
 | 
			
		||||
let MANAGER_BIDS = [];
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -53,7 +53,6 @@ const tracking = async () => {
 | 
			
		|||
            console.log('🔍 Scanning active bids...');
 | 
			
		||||
            const productTabs = _.flatMap(MANAGER_BIDS, 'children');
 | 
			
		||||
 | 
			
		||||
            // Lắng nghe sự kiện của API bids (chạy song song)
 | 
			
		||||
            await Promise.allSettled(
 | 
			
		||||
                MANAGER_BIDS.filter((bid) => !bid.page_context).map((apiBid) => {
 | 
			
		||||
                    console.log(`🎧 Listening to events for API Bid ID: ${apiBid.id}`);
 | 
			
		||||
| 
						 | 
				
			
			@ -196,9 +195,12 @@ const workTracking = async () => {
 | 
			
		|||
};
 | 
			
		||||
 | 
			
		||||
(async () => {
 | 
			
		||||
    const socket = io(configs.SOCKET_URL, {
 | 
			
		||||
    const socket = io(`${configs.SOCKET_URL}/bid-ws`, {
 | 
			
		||||
        transports: ['websocket'],
 | 
			
		||||
        reconnection: true,
 | 
			
		||||
        extraHeaders: {
 | 
			
		||||
            Authorization: process.env.CLIENT_KEY,
 | 
			
		||||
        },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // listen connect
 | 
			
		||||
| 
						 | 
				
			
			@ -207,6 +209,11 @@ const workTracking = async () => {
 | 
			
		|||
        console.log('🔗 Socket ID:', socket.id);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // listen connect
 | 
			
		||||
    socket.on('disconnect', () => {
 | 
			
		||||
        console.log('❌Client key is valid. Disconnected');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // listen event
 | 
			
		||||
    socket.on('bidsUpdated', async (data) => {
 | 
			
		||||
        console.log('📢 Bids Data:', data);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,3 +1,4 @@
 | 
			
		|||
import BID_TYPE from '../system/bid-type.js';
 | 
			
		||||
import CONSTANTS from '../system/constants.js';
 | 
			
		||||
import { takeSnapshot } from '../system/utils.js';
 | 
			
		||||
import _ from 'lodash';
 | 
			
		||||
| 
						 | 
				
			
			@ -20,7 +21,7 @@ export class Bid {
 | 
			
		|||
 | 
			
		||||
        try {
 | 
			
		||||
            // await this.page_context.waitForSelector('#pageContainer', { timeout: 10000 });
 | 
			
		||||
            console.log(`✅ Page fully loaded. Taking snapshot for Product ID: ${this.id}`);
 | 
			
		||||
            console.log(`✅ Page fully loaded. Taking snapshot for ${this.type === BID_TYPE.PRODUCT_TAB ? 'Product ID' : 'Tracking 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);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -218,6 +218,13 @@ export class GraysProductBid extends ProductBid {
 | 
			
		|||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const bidHistoriesItem = _.maxBy(this.histories, 'price');
 | 
			
		||||
 | 
			
		||||
            if (bidHistoriesItem && bidHistoriesItem.price === this.current_price) {
 | 
			
		||||
                console.log(`🔄 You have already bid on this item! (Bid Price: ${bidHistoriesItem.price})`);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (price_value != bid_price) {
 | 
			
		||||
                console.log(`✍️ Updating bid price from ${price_value} → ${bid_price}`);
 | 
			
		||||
                await this.handleWritePrice(page, bid_price);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,6 +3,9 @@ import ax from 'axios';
 | 
			
		|||
const axios = ax.create({
 | 
			
		||||
    // baseURL: 'http://172.18.2.125/api/v1/',
 | 
			
		||||
    baseURL: process.env.BASE_URL,
 | 
			
		||||
    headers: {
 | 
			
		||||
        Authorization: process.env.CLIENT_KEY,
 | 
			
		||||
    },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export default axios;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue