update authentication
This commit is contained in:
parent
1e214a8162
commit
46979c6b9e
|
|
@ -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