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 () => {
|
export const logout = async () => {
|
||||||
try {
|
try {
|
||||||
const { data } = await axios({
|
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 { Avatar, Button, LoadingOverlay, Menu, Modal, PasswordInput } from '@mantine/core';
|
||||||
import { useForm, zodResolver } from '@mantine/form';
|
import { useForm, zodResolver } from '@mantine/form';
|
||||||
import { useDisclosure } from '@mantine/hooks';
|
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 { useNavigate } from 'react-router';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { changePassword, logout } from '../apis/auth';
|
import { changePassword, logout } from '../apis/auth';
|
||||||
import { useConfirmStore } from '../lib/zustand/use-confirm';
|
import { useConfirmStore } from '../lib/zustand/use-confirm';
|
||||||
import Links from '../system/links';
|
import Links from '../system/links';
|
||||||
import { useState } from 'react';
|
|
||||||
|
|
||||||
const schema = z
|
const schema = z
|
||||||
.object({
|
.object({
|
||||||
|
|
@ -92,6 +93,9 @@ export default function UserMenu() {
|
||||||
<Menu.Item onClick={open} leftSection={<IconSettings size={14} />}>
|
<Menu.Item onClick={open} leftSection={<IconSettings size={14} />}>
|
||||||
Change password
|
Change password
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
|
<Menu.Item component={Link} to={Links.GENERATE_KEYS} leftSection={<IconKey size={14} />}>
|
||||||
|
Keys
|
||||||
|
</Menu.Item>
|
||||||
|
|
||||||
<Menu.Divider />
|
<Menu.Divider />
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ export default function PrivateLayout() {
|
||||||
<Logo />
|
<Logo />
|
||||||
|
|
||||||
<Box className="flex items-center gap-4">
|
<Box className="flex items-center gap-4">
|
||||||
{Links.MENUS.map((menu, index) => (
|
{Links.MENUS.filter((i) => i.show).map((menu, index) => (
|
||||||
<Button
|
<Button
|
||||||
size="xs"
|
size="xs"
|
||||||
component={Link}
|
component={Link}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { Box, Text, Title } from '@mantine/core';
|
import { Box, Text, Title } from '@mantine/core';
|
||||||
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import io from 'socket.io-client';
|
import io from 'socket.io-client';
|
||||||
import { WorkingPage } from '../components/dashboard';
|
import { WorkingPage } from '../components/dashboard';
|
||||||
import { IBid, IWebBid } from '../system/type';
|
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,
|
autoConnect: true,
|
||||||
transports: ['websocket'],
|
transports: ['websocket'],
|
||||||
});
|
});
|
||||||
|
|
@ -13,18 +14,24 @@ const socket = io(import.meta.env.VITE_SOCKET_URL, {
|
||||||
export default function DashBoard() {
|
export default function DashBoard() {
|
||||||
const [workingData, setWorkingData] = useState<(IWebBid & { type: string })[] | (IBid & { type: string })[]>([]);
|
const [workingData, setWorkingData] = useState<(IWebBid & { type: string })[] | (IBid & { type: string })[]>([]);
|
||||||
|
|
||||||
|
const RETRY_CONNECT = useRef(2);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
socket.connect();
|
socket.connect();
|
||||||
|
|
||||||
socket.on('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.emit('getBidsData');
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('disconnect', () => {
|
socket.on('disconnect', async () => {
|
||||||
console.log('❌ WebSocket disconnected');
|
if (RETRY_CONNECT.current > 0) {
|
||||||
|
await checkStatus();
|
||||||
|
|
||||||
|
socket.connect();
|
||||||
|
|
||||||
|
RETRY_CONNECT.current--;
|
||||||
|
return;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('adminBidsUpdated', (data: IWebBid[]) => {
|
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 { Bids, Dashboard, OutBidsLog } from '../pages';
|
||||||
import WebBids from '../pages/web-bids';
|
import WebBids from '../pages/web-bids';
|
||||||
import SendMessageHistories from '../pages/send-message-histories';
|
import SendMessageHistories from '../pages/send-message-histories';
|
||||||
import Admins from '../pages/admins';
|
import Admins from '../pages/admins';
|
||||||
|
import GenerateKeys from '../pages/generate-keys';
|
||||||
export default class Links {
|
export default class Links {
|
||||||
public static DASHBOARD = '/dashboard';
|
public static DASHBOARD = '/dashboard';
|
||||||
public static BIDS = '/bids';
|
public static BIDS = '/bids';
|
||||||
public static WEBS = '/webs';
|
public static WEBS = '/webs';
|
||||||
public static OUT_BIDS_LOG = '/out-bids-log';
|
public static OUT_BIDS_LOG = '/out-bids-log';
|
||||||
public static SEND_MESSAGE_HISTORIES = '/send-message-histories';
|
public static SEND_MESSAGE_HISTORIES = '/send-message-histories';
|
||||||
|
public static GENERATE_KEYS = '/generate-keys';
|
||||||
public static ADMINS = '/admins';
|
public static ADMINS = '/admins';
|
||||||
|
|
||||||
public static HOME = '/';
|
public static HOME = '/';
|
||||||
|
|
@ -20,36 +22,49 @@ export default class Links {
|
||||||
title: 'Dashboard',
|
title: 'Dashboard',
|
||||||
icon: IconHome2,
|
icon: IconHome2,
|
||||||
element: Dashboard,
|
element: Dashboard,
|
||||||
|
show: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: this.ADMINS,
|
path: this.ADMINS,
|
||||||
title: 'Admins',
|
title: 'Admins',
|
||||||
icon: IconUserCheck,
|
icon: IconUserCheck,
|
||||||
element: Admins,
|
element: Admins,
|
||||||
|
show: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: this.WEBS,
|
path: this.WEBS,
|
||||||
title: 'Webs',
|
title: 'Webs',
|
||||||
icon: IconPageBreak,
|
icon: IconPageBreak,
|
||||||
element: WebBids,
|
element: WebBids,
|
||||||
|
show: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: this.BIDS,
|
path: this.BIDS,
|
||||||
title: 'Bids',
|
title: 'Bids',
|
||||||
icon: IconHammer,
|
icon: IconHammer,
|
||||||
element: Bids,
|
element: Bids,
|
||||||
|
show: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: this.OUT_BIDS_LOG,
|
path: this.OUT_BIDS_LOG,
|
||||||
title: 'Out bids log',
|
title: 'Out bids log',
|
||||||
icon: IconOutlet,
|
icon: IconOutlet,
|
||||||
element: OutBidsLog,
|
element: OutBidsLog,
|
||||||
|
show: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: this.SEND_MESSAGE_HISTORIES,
|
path: this.SEND_MESSAGE_HISTORIES,
|
||||||
title: 'Send message histories',
|
title: 'Send message histories',
|
||||||
icon: IconMessage,
|
icon: IconMessage,
|
||||||
element: SendMessageHistories,
|
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: [];
|
permissions: [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IKey extends ITimestamp {
|
||||||
|
id: number;
|
||||||
|
client_key: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ITimestamp {
|
export interface ITimestamp {
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@
|
||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.1",
|
"class-validator": "^0.14.1",
|
||||||
|
"cookie": "^1.0.2",
|
||||||
"cookie-parser": "^1.4.7",
|
"cookie-parser": "^1.4.7",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
|
|
@ -32,13 +33,15 @@
|
||||||
"reflect-metadata": "^0.2.0",
|
"reflect-metadata": "^0.2.0",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
"sharp": "^0.33.5",
|
"sharp": "^0.33.5",
|
||||||
"typeorm": "^0.3.21"
|
"typeorm": "^0.3.21",
|
||||||
|
"uuid": "^11.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nestjs/cli": "^10.0.0",
|
"@nestjs/cli": "^10.0.0",
|
||||||
"@nestjs/schematics": "^10.0.0",
|
"@nestjs/schematics": "^10.0.0",
|
||||||
"@nestjs/testing": "^10.0.0",
|
"@nestjs/testing": "^10.0.0",
|
||||||
"@types/bcrypt": "^5.0.2",
|
"@types/bcrypt": "^5.0.2",
|
||||||
|
"@types/cookie": "^0.6.0",
|
||||||
"@types/cookie-parser": "^1.4.8",
|
"@types/cookie-parser": "^1.4.8",
|
||||||
"@types/express": "^4.17.17",
|
"@types/express": "^4.17.17",
|
||||||
"@types/jest": "^29.5.2",
|
"@types/jest": "^29.5.2",
|
||||||
|
|
@ -2823,6 +2826,13 @@
|
||||||
"@types/node": "*"
|
"@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": {
|
"node_modules/@types/cookie-parser": {
|
||||||
"version": "1.4.8",
|
"version": "1.4.8",
|
||||||
"resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.8.tgz",
|
"resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.8.tgz",
|
||||||
|
|
@ -4719,12 +4729,12 @@
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/cookie": {
|
"node_modules/cookie": {
|
||||||
"version": "0.7.1",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
|
||||||
"integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
|
"integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.6"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/cookie-parser": {
|
"node_modules/cookie-parser": {
|
||||||
|
|
@ -5736,6 +5746,15 @@
|
||||||
"url": "https://opencollective.com/express"
|
"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": {
|
"node_modules/express/node_modules/debug": {
|
||||||
"version": "2.6.9",
|
"version": "2.6.9",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||||
|
|
@ -8088,16 +8107,6 @@
|
||||||
"set-cookie-parser": "^2.6.0"
|
"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": {
|
"node_modules/lines-and-columns": {
|
||||||
"version": "1.2.4",
|
"version": "1.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@
|
||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.1",
|
"class-validator": "^0.14.1",
|
||||||
|
"cookie": "^1.0.2",
|
||||||
"cookie-parser": "^1.4.7",
|
"cookie-parser": "^1.4.7",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
|
|
@ -48,13 +49,15 @@
|
||||||
"reflect-metadata": "^0.2.0",
|
"reflect-metadata": "^0.2.0",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
"sharp": "^0.33.5",
|
"sharp": "^0.33.5",
|
||||||
"typeorm": "^0.3.21"
|
"typeorm": "^0.3.21",
|
||||||
|
"uuid": "^11.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nestjs/cli": "^10.0.0",
|
"@nestjs/cli": "^10.0.0",
|
||||||
"@nestjs/schematics": "^10.0.0",
|
"@nestjs/schematics": "^10.0.0",
|
||||||
"@nestjs/testing": "^10.0.0",
|
"@nestjs/testing": "^10.0.0",
|
||||||
"@types/bcrypt": "^5.0.2",
|
"@types/bcrypt": "^5.0.2",
|
||||||
|
"@types/cookie": "^0.6.0",
|
||||||
"@types/cookie-parser": "^1.4.8",
|
"@types/cookie-parser": "^1.4.8",
|
||||||
"@types/express": "^4.17.17",
|
"@types/express": "^4.17.17",
|
||||||
"@types/jest": "^29.5.2",
|
"@types/jest": "^29.5.2",
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,13 @@ import { AppValidatorsModule } from './modules/app-validators/app-validators.mod
|
||||||
import { AuthModule } from './modules/auth/auth.module';
|
import { AuthModule } from './modules/auth/auth.module';
|
||||||
import { AdminsModule } from './modules/admins/admins.module';
|
import { AdminsModule } from './modules/admins/admins.module';
|
||||||
import { AuthenticationMiddleware } from './modules/auth/middlewares/authentication.middleware';
|
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 { AuthorizationMiddleware } from './modules/admins/middlewares/authorization.middleware';
|
||||||
|
import { ClientAuthenticationMiddleware } from './modules/auth/middlewares/client-authentication.middleware';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
|
@ -32,5 +37,10 @@ export class AppModule {
|
||||||
.apply(AuthorizationMiddleware)
|
.apply(AuthorizationMiddleware)
|
||||||
.exclude(...excludeAuthor)
|
.exclude(...excludeAuthor)
|
||||||
.forRoutes({ path: 'admin/*', method: RequestMethod.ALL });
|
.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';
|
const prefix_version = '/api/v1';
|
||||||
|
|
||||||
app.enableCors({
|
app.enableCors({
|
||||||
origin: process.env.CORS.split(', '),
|
origin: process.env.CORS.split(', ') || '*',
|
||||||
methods: ['GET', 'PUT', 'POST', 'DELETE', 'PATCH'],
|
methods: ['GET', 'PUT', 'POST', 'DELETE', 'PATCH'],
|
||||||
allowedHeaders: 'Content-Type, Authorization',
|
allowedHeaders: 'Content-Type, Authorization',
|
||||||
credentials: true,
|
credentials: true,
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,18 @@ import { AdminsController } from './controllers/admins.controller';
|
||||||
import Permission from './entities/permission.entity';
|
import Permission from './entities/permission.entity';
|
||||||
import { PermissionService } from './services/permission.service';
|
import { PermissionService } from './services/permission.service';
|
||||||
import { PermissionsController } from './controllers/permissions.controller';
|
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({
|
@Module({
|
||||||
imports: [TypeOrmModule.forFeature([Admin, Permission])],
|
imports: [TypeOrmModule.forFeature([Admin, Permission, GenerateKey])],
|
||||||
providers: [AdminsService, PermissionService],
|
providers: [AdminsService, PermissionService, GenerateKeysService],
|
||||||
exports: [AdminsService, PermissionService],
|
exports: [AdminsService, PermissionService, GenerateKeysService],
|
||||||
controllers: [AdminsController, PermissionsController],
|
controllers: [
|
||||||
|
AdminsController,
|
||||||
|
PermissionsController,
|
||||||
|
GenerateKeysController,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class AdminsModule {}
|
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 { 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 Permission from './permission.entity';
|
||||||
import { Timestamp } from './timestamp';
|
import { Timestamp } from './timestamp';
|
||||||
|
import { GenerateKey } from './generate-key.entity';
|
||||||
|
|
||||||
@Entity('admins')
|
@Entity('admins')
|
||||||
export default class Admin extends Timestamp {
|
export default class Admin extends Timestamp {
|
||||||
|
|
@ -29,4 +37,9 @@ export default class Admin extends Timestamp {
|
||||||
onDelete: 'CASCADE',
|
onDelete: 'CASCADE',
|
||||||
})
|
})
|
||||||
permissions: Permission[];
|
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'
|
| 'is_system_account'
|
||||||
| 'permissions'
|
| 'permissions'
|
||||||
| 'posts'
|
| 'posts'
|
||||||
|
| 'generateKeys'
|
||||||
>) {
|
>) {
|
||||||
const hashPassword = await bcrypt.hash(password, 10);
|
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 { Module } from '@nestjs/common';
|
||||||
import { JwtModule } from '@nestjs/jwt';
|
import { JwtModule } from '@nestjs/jwt';
|
||||||
import { AuthController } from './auth.controller';
|
import { AuthController } from './controllers/auth.controller';
|
||||||
import { AuthService } from './auth.service';
|
import { AuthService } from './auth.service';
|
||||||
import { AdminsModule } from '../admins/admins.module';
|
import { AdminsModule } from '../admins/admins.module';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,12 +14,14 @@ import { Request, Response } from 'express';
|
||||||
import { Constant } from './ultils/constant';
|
import { Constant } from './ultils/constant';
|
||||||
import { ChangePasswordDto } from './dto/change-password.dto';
|
import { ChangePasswordDto } from './dto/change-password.dto';
|
||||||
import AppResponse from '@/response/app-response';
|
import AppResponse from '@/response/app-response';
|
||||||
|
import { GenerateKeysService } from '../admins/services/generate-key.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuthService {
|
export class AuthService {
|
||||||
constructor(
|
constructor(
|
||||||
private jwtService: JwtService,
|
private jwtService: JwtService,
|
||||||
private readonly adminService: AdminsService,
|
private readonly adminService: AdminsService,
|
||||||
|
private readonly generateKeyService: GenerateKeysService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async validateAdmin(
|
async validateAdmin(
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,13 @@ import {
|
||||||
Res,
|
Res,
|
||||||
Get,
|
Get,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { AuthService } from './auth.service';
|
import { AuthService } from '../auth.service';
|
||||||
import { LoginDto } from './dto/login.dto';
|
import { LoginDto } from '../dto/login.dto';
|
||||||
import { Request, Response } from 'express';
|
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')
|
@Controller('admin/auth')
|
||||||
export class AuthController {
|
export class AuthController {
|
||||||
|
|
@ -26,6 +29,12 @@ export class AuthController {
|
||||||
.then((admin) => this.authService.login(admin, response));
|
.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')
|
@Post('logout')
|
||||||
async logout(@Res({ passthrough: true }) response: Response) {
|
async logout(@Res({ passthrough: true }) response: Response) {
|
||||||
return await this.authService.logout(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 { SendMessageHistory } from './entities/send-message-histories.entity';
|
||||||
import { SendMessageHistoriesService } from './services/send-message-histories.service';
|
import { SendMessageHistoriesService } from './services/send-message-histories.service';
|
||||||
import { AdminSendMessageHistoriesController } from './controllers/admin/admin-send-message-histories.controller';
|
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({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
|
@ -35,6 +38,8 @@ import { AdminSendMessageHistoriesController } from './controllers/admin/admin-s
|
||||||
EventEmitterModule.forRoot({
|
EventEmitterModule.forRoot({
|
||||||
wildcard: true,
|
wildcard: true,
|
||||||
}),
|
}),
|
||||||
|
// AuthModule,
|
||||||
|
AdminsModule,
|
||||||
],
|
],
|
||||||
controllers: [
|
controllers: [
|
||||||
BidsController,
|
BidsController,
|
||||||
|
|
@ -50,6 +55,7 @@ import { AdminSendMessageHistoriesController } from './controllers/admin/admin-s
|
||||||
BidsService,
|
BidsService,
|
||||||
BidHistoriesService,
|
BidHistoriesService,
|
||||||
BidGateway,
|
BidGateway,
|
||||||
|
AdminBidGateway,
|
||||||
OutBidLogsService,
|
OutBidLogsService,
|
||||||
WebBidsService,
|
WebBidsService,
|
||||||
BotTelegramApi,
|
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 {
|
import {
|
||||||
|
OnGatewayConnection,
|
||||||
WebSocketGateway,
|
WebSocketGateway,
|
||||||
WebSocketServer,
|
WebSocketServer,
|
||||||
SubscribeMessage,
|
|
||||||
OnGatewayConnection,
|
|
||||||
} from '@nestjs/websockets';
|
} from '@nestjs/websockets';
|
||||||
import { Server, Socket } from 'socket.io';
|
import { Server, Socket } from 'socket.io';
|
||||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
|
||||||
import { BidsService } from '../services/bids.service';
|
import { BidsService } from '../services/bids.service';
|
||||||
import { WebBidsService } from '../services/web-bids.service';
|
import { WebBidsService } from '../services/web-bids.service';
|
||||||
import { plainToClass } from 'class-transformer';
|
|
||||||
import { WebBid } from '../entities/wed-bid.entity';
|
|
||||||
|
|
||||||
@WebSocketGateway({
|
@WebSocketGateway({
|
||||||
|
namespace: 'bid-ws',
|
||||||
cors: {
|
cors: {
|
||||||
origin: '*',
|
origin: '*',
|
||||||
methods: ['GET', 'POST'],
|
methods: ['GET', 'POST'],
|
||||||
|
|
@ -25,30 +25,26 @@ export class BidGateway implements OnGatewayConnection {
|
||||||
constructor(
|
constructor(
|
||||||
private eventEmitter: EventEmitter2,
|
private eventEmitter: EventEmitter2,
|
||||||
private webBidsService: WebBidsService,
|
private webBidsService: WebBidsService,
|
||||||
private bidsService: BidsService,
|
private generateKeysService: GenerateKeysService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async onModuleInit() {
|
async onModuleInit() {
|
||||||
this.eventEmitter.on('bids.updated', (data) => {
|
this.eventEmitter.on('bids.updated', (data) => {
|
||||||
this.server.emit('bidsUpdated', data);
|
this.server.emit('bidsUpdated', data);
|
||||||
this.server.emit('adminBidsUpdated', plainToClass(WebBid, data));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.eventEmitter.on('web.updated', (data) => {
|
this.eventEmitter.on('web.updated', (data) => {
|
||||||
this.server.emit('webUpdated', data);
|
this.server.emit('webUpdated', data);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.eventEmitter.on('working', (data) => {
|
|
||||||
this.server.emit('working', data);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleConnection(client: Socket) {
|
async handleConnection(client: Socket) {
|
||||||
|
await clientGetWayMiddleware(client, this.generateKeysService);
|
||||||
|
|
||||||
console.log(`📢 Client connected: ${client.id}`);
|
console.log(`📢 Client connected: ${client.id}`);
|
||||||
|
|
||||||
const data = await this.webBidsService.getDataClient();
|
const data = await this.webBidsService.getDataClient();
|
||||||
// Gửi dữ liệu bids ngay khi client kết nối
|
// Gửi dữ liệu bids ngay khi client kết nối
|
||||||
client.emit('bidsUpdated', data);
|
client.emit('bidsUpdated', data);
|
||||||
client.emit('adminBidsUpdated', plainToClass(WebBid, data));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,14 @@ export const excludeAuth = [
|
||||||
},
|
},
|
||||||
] as (string | RouteInfo)[];
|
] as (string | RouteInfo)[];
|
||||||
|
|
||||||
|
export const clientExcludeAuth = [
|
||||||
|
{ path: '/admin/(.*)', method: RequestMethod.ALL },
|
||||||
|
{
|
||||||
|
path: 'auth/generate-token',
|
||||||
|
method: RequestMethod.POST,
|
||||||
|
},
|
||||||
|
] as (string | RouteInfo)[];
|
||||||
|
|
||||||
export const excludeAuthor = [
|
export const excludeAuthor = [
|
||||||
...excludeAuth,
|
...excludeAuth,
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
ENVIRONMENT = 'prod'
|
ENVIRONMENT = 'prod'
|
||||||
SOCKET_URL = 'http://localhost:4000'
|
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 'dotenv/config';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
import pLimit from 'p-limit';
|
||||||
import { io } from 'socket.io-client';
|
import { io } from 'socket.io-client';
|
||||||
import { createApiBid, createBidProduct, deleteProfile, shouldUpdateProductTab } from './service/app-service.js';
|
import { createApiBid, createBidProduct, deleteProfile, shouldUpdateProductTab } from './service/app-service.js';
|
||||||
import browser from './system/browser.js';
|
import browser from './system/browser.js';
|
||||||
import configs from './system/config.js';
|
import configs from './system/config.js';
|
||||||
import { delay, isTimeReached, safeClosePage } from './system/utils.js';
|
import { delay, isTimeReached, safeClosePage } from './system/utils.js';
|
||||||
import pLimit from 'p-limit';
|
|
||||||
|
|
||||||
let MANAGER_BIDS = [];
|
let MANAGER_BIDS = [];
|
||||||
|
|
||||||
|
|
@ -53,7 +53,6 @@ const tracking = async () => {
|
||||||
console.log('🔍 Scanning active bids...');
|
console.log('🔍 Scanning active bids...');
|
||||||
const productTabs = _.flatMap(MANAGER_BIDS, 'children');
|
const productTabs = _.flatMap(MANAGER_BIDS, 'children');
|
||||||
|
|
||||||
// Lắng nghe sự kiện của API bids (chạy song song)
|
|
||||||
await Promise.allSettled(
|
await Promise.allSettled(
|
||||||
MANAGER_BIDS.filter((bid) => !bid.page_context).map((apiBid) => {
|
MANAGER_BIDS.filter((bid) => !bid.page_context).map((apiBid) => {
|
||||||
console.log(`🎧 Listening to events for API Bid ID: ${apiBid.id}`);
|
console.log(`🎧 Listening to events for API Bid ID: ${apiBid.id}`);
|
||||||
|
|
@ -196,9 +195,12 @@ const workTracking = async () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
const socket = io(configs.SOCKET_URL, {
|
const socket = io(`${configs.SOCKET_URL}/bid-ws`, {
|
||||||
transports: ['websocket'],
|
transports: ['websocket'],
|
||||||
reconnection: true,
|
reconnection: true,
|
||||||
|
extraHeaders: {
|
||||||
|
Authorization: process.env.CLIENT_KEY,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// listen connect
|
// listen connect
|
||||||
|
|
@ -207,6 +209,11 @@ const workTracking = async () => {
|
||||||
console.log('🔗 Socket ID:', socket.id);
|
console.log('🔗 Socket ID:', socket.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// listen connect
|
||||||
|
socket.on('disconnect', () => {
|
||||||
|
console.log('❌Client key is valid. Disconnected');
|
||||||
|
});
|
||||||
|
|
||||||
// listen event
|
// listen event
|
||||||
socket.on('bidsUpdated', async (data) => {
|
socket.on('bidsUpdated', async (data) => {
|
||||||
console.log('📢 Bids Data:', 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 CONSTANTS from '../system/constants.js';
|
||||||
import { takeSnapshot } from '../system/utils.js';
|
import { takeSnapshot } from '../system/utils.js';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
|
@ -20,7 +21,7 @@ export class Bid {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// await this.page_context.waitForSelector('#pageContainer', { timeout: 10000 });
|
// 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);
|
takeSnapshot(this.page_context, this, 'working', CONSTANTS.TYPE_IMAGE.WORK);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`❌ Error taking snapshot for Product ID: ${this.id}:`, error.message);
|
console.error(`❌ Error taking snapshot for Product ID: ${this.id}:`, error.message);
|
||||||
|
|
|
||||||
|
|
@ -218,6 +218,13 @@ export class GraysProductBid extends ProductBid {
|
||||||
return;
|
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) {
|
if (price_value != bid_price) {
|
||||||
console.log(`✍️ Updating bid price from ${price_value} → ${bid_price}`);
|
console.log(`✍️ Updating bid price from ${price_value} → ${bid_price}`);
|
||||||
await this.handleWritePrice(page, bid_price);
|
await this.handleWritePrice(page, bid_price);
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,9 @@ import ax from 'axios';
|
||||||
const axios = ax.create({
|
const axios = ax.create({
|
||||||
// baseURL: 'http://172.18.2.125/api/v1/',
|
// baseURL: 'http://172.18.2.125/api/v1/',
|
||||||
baseURL: process.env.BASE_URL,
|
baseURL: process.env.BASE_URL,
|
||||||
|
headers: {
|
||||||
|
Authorization: process.env.CLIENT_KEY,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default axios;
|
export default axios;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue