feat: webiste add and list
This commit is contained in:
parent
2ccb084959
commit
bd7a5776c3
7
nodemon.json
Normal file
7
nodemon.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"verbose": true,
|
||||
"watch": ["./src/server"],
|
||||
"ext": "ts",
|
||||
"delay": 1000,
|
||||
"exec": "ts-node --transpileOnly ./src/server/main.ts"
|
||||
}
|
@ -3,7 +3,7 @@
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"dev": "nodemon src/server/main.ts -w src/server",
|
||||
"dev": "nodemon",
|
||||
"start": "NODE_ENV=production ts-node src/server/main.ts",
|
||||
"build": "vite build && pnpm build:tracker && pnpm build:geo",
|
||||
"build:tracker": "ts-node scripts/build-tracker.ts",
|
||||
@ -16,6 +16,7 @@
|
||||
"@ant-design/charts": "^1.4.2",
|
||||
"@ant-design/icons": "^5.2.5",
|
||||
"@prisma/client": "^5.2.0",
|
||||
"@tanstack/react-query": "^4.33.0",
|
||||
"@types/uuid": "^9.0.3",
|
||||
"antd": "^5.8.5",
|
||||
"axios": "^1.5.0",
|
||||
|
@ -10,6 +10,9 @@ dependencies:
|
||||
'@prisma/client':
|
||||
specifier: ^5.2.0
|
||||
version: 5.2.0(prisma@5.2.0)
|
||||
'@tanstack/react-query':
|
||||
specifier: ^4.33.0
|
||||
version: 4.33.0(react-dom@18.2.0)(react@18.2.0)
|
||||
'@types/uuid':
|
||||
specifier: ^9.0.3
|
||||
version: 9.0.3
|
||||
@ -1760,6 +1763,28 @@ packages:
|
||||
resolution: {integrity: sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==}
|
||||
dev: false
|
||||
|
||||
/@tanstack/query-core@4.33.0:
|
||||
resolution: {integrity: sha512-qYu73ptvnzRh6se2nyBIDHGBQvPY1XXl3yR769B7B6mIDD7s+EZhdlWHQ67JI6UOTFRaI7wupnTnwJ3gE0Mr/g==}
|
||||
dev: false
|
||||
|
||||
/@tanstack/react-query@4.33.0(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-97nGbmDK0/m0B86BdiXzx3EW9RcDYKpnyL2+WwyuLHEgpfThYAnXFaMMmnTDuAO4bQJXEhflumIEUfKmP7ESGA==}
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||
react-native: '*'
|
||||
peerDependenciesMeta:
|
||||
react-dom:
|
||||
optional: true
|
||||
react-native:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@tanstack/query-core': 4.33.0
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
use-sync-external-store: 1.2.0(react@18.2.0)
|
||||
dev: false
|
||||
|
||||
/@tsconfig/node10@1.0.9:
|
||||
resolution: {integrity: sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==}
|
||||
|
||||
|
@ -29,8 +29,10 @@ model Workspace {
|
||||
updatedAt DateTime? @updatedAt @db.Timestamptz(6)
|
||||
|
||||
users WorkspacesOnUsers[]
|
||||
website Website[]
|
||||
User User[]
|
||||
websites Website[]
|
||||
|
||||
// for user currentWorkspace
|
||||
selectedUsers User[]
|
||||
}
|
||||
|
||||
model WorkspacesOnUsers {
|
||||
|
@ -8,12 +8,14 @@ import { Settings } from './pages/Settings';
|
||||
import { Servers } from './pages/Servers';
|
||||
import { useUserStore } from './store/user';
|
||||
import { Register } from './pages/Register';
|
||||
|
||||
import { QueryClientProvider } from '@tanstack/react-query';
|
||||
import { queryClient } from './api/cache';
|
||||
function App() {
|
||||
const { info } = useUserStore();
|
||||
|
||||
return (
|
||||
<div className="App">
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
{info && (
|
||||
@ -37,6 +39,7 @@ function App() {
|
||||
/>
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
</QueryClientProvider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
10
src/client/api/cache.ts
Normal file
10
src/client/api/cache.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { QueryClient } from '@tanstack/react-query';
|
||||
|
||||
export const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
refetchOnWindowFocus: false,
|
||||
},
|
||||
},
|
||||
});
|
@ -1,7 +1,24 @@
|
||||
import { setUserInfo, UserLoginInfo } from '../../store/user';
|
||||
import { setUserInfo } from '../../store/user';
|
||||
import { getJWT, setJWT } from '../auth';
|
||||
import { request } from '../request';
|
||||
|
||||
export interface UserLoginInfo {
|
||||
id: string;
|
||||
username: string;
|
||||
role: string;
|
||||
currentWorkspace: {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
workspaces: {
|
||||
role: string;
|
||||
workspace: {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
}[];
|
||||
}
|
||||
|
||||
export async function login(username: string, password: string) {
|
||||
const { data } = await request.post('/api/user/login', {
|
||||
username,
|
||||
|
54
src/client/api/model/website.ts
Normal file
54
src/client/api/model/website.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { queryClient } from '../cache';
|
||||
import { request } from '../request';
|
||||
|
||||
export interface WebsiteInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
domain: string | null;
|
||||
shareId: string | null;
|
||||
resetAt: string | null;
|
||||
workspaceId: string;
|
||||
createdAt: string | null;
|
||||
updatedAt: string | null;
|
||||
deletedAt: string | null;
|
||||
}
|
||||
|
||||
export async function getWorkspaceWebsites(
|
||||
workspaceId: string
|
||||
): Promise<WebsiteInfo[]> {
|
||||
const { data } = await request.get('/api/workspace/websites', {
|
||||
params: {
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
return data.websites;
|
||||
}
|
||||
|
||||
export function useWorspaceWebsites(workspaceId: string) {
|
||||
const { data: websites = [], isLoading } = useQuery(
|
||||
['websites', workspaceId],
|
||||
() => {
|
||||
return getWorkspaceWebsites(workspaceId);
|
||||
}
|
||||
);
|
||||
|
||||
return { websites, isLoading };
|
||||
}
|
||||
|
||||
export function refreshWorkspaceWebsites(workspaceId: string) {
|
||||
queryClient.refetchQueries(['websites', workspaceId]);
|
||||
}
|
||||
|
||||
export async function addWorkspaceWebsite(
|
||||
workspaceId: string,
|
||||
name: string,
|
||||
domain: string
|
||||
) {
|
||||
await request.post('/api/workspace/website', {
|
||||
workspaceId,
|
||||
name,
|
||||
domain,
|
||||
});
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Select } from 'antd';
|
||||
import { Dropdown, Select } from 'antd';
|
||||
import { compact } from 'lodash-es';
|
||||
|
||||
export const DateFilter: React.FC<{
|
||||
|
11
src/client/components/Loading.tsx
Normal file
11
src/client/components/Loading.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import React from 'react';
|
||||
|
||||
export const Loading: React.FC = React.memo(() => {
|
||||
return (
|
||||
<div>
|
||||
<LoadingOutlined />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
Loading.displayName = 'Loading';
|
6
src/client/components/NoWorkspaceTip.tsx
Normal file
6
src/client/components/NoWorkspaceTip.tsx
Normal file
@ -0,0 +1,6 @@
|
||||
import React from 'react';
|
||||
|
||||
export const NoWorkspaceTip: React.FC = React.memo(() => {
|
||||
return <div>Please Select Workspace</div>;
|
||||
});
|
||||
NoWorkspaceTip.displayName = 'NoWorkspaceTip';
|
@ -6,18 +6,43 @@ import {
|
||||
import { Button, Form, Input, Modal, Table } from 'antd';
|
||||
import { ColumnsType } from 'antd/es/table';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import {
|
||||
addWorkspaceWebsite,
|
||||
refreshWorkspaceWebsites,
|
||||
useWorspaceWebsites,
|
||||
WebsiteInfo,
|
||||
} from '../api/model/website';
|
||||
import { Loading } from '../components/Loading';
|
||||
import { NoWorkspaceTip } from '../components/NoWorkspaceTip';
|
||||
import { useRequest } from '../hooks/useRequest';
|
||||
import { useUserStore } from '../store/user';
|
||||
|
||||
export const Website: React.FC = React.memo(() => {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const currentWorkspace = useUserStore(
|
||||
(state) => state.info?.currentWorkspace
|
||||
);
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const handleOk = () => {
|
||||
const [{ loading }, handleAddWebsite] = useRequest(async () => {
|
||||
await form.validateFields();
|
||||
const values = form.getFieldsValue();
|
||||
|
||||
await addWorkspaceWebsite(currentWorkspace!.id, values.name, values.domain);
|
||||
refreshWorkspaceWebsites(currentWorkspace!.id);
|
||||
setIsModalOpen(false);
|
||||
};
|
||||
|
||||
form.resetFields();
|
||||
});
|
||||
|
||||
if (!currentWorkspace) {
|
||||
return <NoWorkspaceTip />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="h-24 flex items-center">
|
||||
<div className="text-2xl flex-1">Servers</div>
|
||||
<div className="text-2xl flex-1">Websites</div>
|
||||
<div>
|
||||
<Button
|
||||
type="primary"
|
||||
@ -30,19 +55,26 @@ export const Website: React.FC = React.memo(() => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<WebsiteList />
|
||||
<WebsiteList workspaceId={currentWorkspace.id} />
|
||||
|
||||
<Modal
|
||||
title="Add Server"
|
||||
open={isModalOpen}
|
||||
onOk={handleOk}
|
||||
okButtonProps={{
|
||||
loading,
|
||||
}}
|
||||
onOk={() => handleAddWebsite()}
|
||||
onCancel={() => setIsModalOpen(false)}
|
||||
>
|
||||
<Form layout="vertical">
|
||||
<Form.Item label="Server Name">
|
||||
<Form layout="vertical" form={form}>
|
||||
<Form.Item
|
||||
label="Server Name"
|
||||
name="name"
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label="Domain">
|
||||
<Form.Item label="Domain" name="domain" rules={[{ required: true }]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
@ -52,20 +84,10 @@ export const Website: React.FC = React.memo(() => {
|
||||
});
|
||||
Website.displayName = 'Website';
|
||||
|
||||
interface WebsiteInfoRecordType {
|
||||
name: string;
|
||||
domain: string;
|
||||
}
|
||||
const WebsiteList: React.FC<{ workspaceId: string }> = React.memo((props) => {
|
||||
const { websites, isLoading } = useWorspaceWebsites(props.workspaceId);
|
||||
|
||||
const WebsiteList: React.FC = React.memo(() => {
|
||||
const dataSource: WebsiteInfoRecordType[] = [
|
||||
{
|
||||
name: 'tianji',
|
||||
domain: 'tianji.msgbyte.com',
|
||||
},
|
||||
];
|
||||
|
||||
const columns = useMemo((): ColumnsType<WebsiteInfoRecordType> => {
|
||||
const columns = useMemo((): ColumnsType<WebsiteInfo> => {
|
||||
return [
|
||||
{
|
||||
dataIndex: 'name',
|
||||
@ -89,6 +111,10 @@ const WebsiteList: React.FC = React.memo(() => {
|
||||
];
|
||||
}, []);
|
||||
|
||||
return <Table columns={columns} dataSource={dataSource} pagination={false} />;
|
||||
if (isLoading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
return <Table columns={columns} dataSource={websites} pagination={false} />;
|
||||
});
|
||||
WebsiteList.displayName = 'WebsiteList';
|
||||
|
@ -1,21 +1,5 @@
|
||||
import { create } from 'zustand';
|
||||
|
||||
export interface UserLoginInfo {
|
||||
id: string;
|
||||
username: string;
|
||||
role: string;
|
||||
currentWorkspace: {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
workspaces: {
|
||||
role: string;
|
||||
workspace: {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
}[];
|
||||
}
|
||||
import { UserLoginInfo } from '../api/model/user';
|
||||
|
||||
interface UserState {
|
||||
info: UserLoginInfo | null;
|
||||
|
@ -6,8 +6,9 @@ import compression from 'compression';
|
||||
import passport from 'passport';
|
||||
import { userRouter } from './router/user';
|
||||
import { websiteRouter } from './router/website';
|
||||
import { workspaceRouter } from './router/workspace';
|
||||
|
||||
const port = Number(process.env.PORT || 3000);
|
||||
const port = Number(process.env.PORT || 12345);
|
||||
|
||||
const app = express();
|
||||
|
||||
@ -20,6 +21,7 @@ app.disable('x-powered-by');
|
||||
|
||||
app.use('/api/user', userRouter);
|
||||
app.use('/api/website', websiteRouter);
|
||||
app.use('/api/workspace', workspaceRouter);
|
||||
|
||||
app.use((err: any, req: any, res: any, next: any) => {
|
||||
console.error(err);
|
||||
|
@ -11,7 +11,7 @@ export const jwtSecret =
|
||||
export const jwtIssuer = process.env.JWT_ISSUER || 'tianji.msgbyte.com';
|
||||
export const jwtAudience = process.env.JWT_AUDIENCE || 'msgbyte.com';
|
||||
|
||||
interface JWTPayload {
|
||||
export interface JWTPayload {
|
||||
id: string;
|
||||
username: string;
|
||||
role: string;
|
||||
|
52
src/server/model/workspace.ts
Normal file
52
src/server/model/workspace.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import { prisma } from './_client';
|
||||
|
||||
export async function checkIsWorkspaceUser(
|
||||
workspaceId: string,
|
||||
userId: string
|
||||
) {
|
||||
const workspace = await prisma.workspace.findUnique({
|
||||
where: {
|
||||
id: workspaceId,
|
||||
users: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (workspace) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getWorkspaceWebsites(workspaceId: string) {
|
||||
const workspace = await prisma.workspace.findUnique({
|
||||
where: {
|
||||
id: workspaceId,
|
||||
},
|
||||
select: {
|
||||
websites: true,
|
||||
},
|
||||
});
|
||||
|
||||
return workspace?.websites ?? [];
|
||||
}
|
||||
|
||||
export async function addWorkspaceWebsite(
|
||||
workspaceId: string,
|
||||
name: string,
|
||||
domain: string
|
||||
) {
|
||||
const website = await prisma.website.create({
|
||||
data: {
|
||||
name,
|
||||
domain,
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
return website;
|
||||
}
|
64
src/server/router/workspace.ts
Normal file
64
src/server/router/workspace.ts
Normal file
@ -0,0 +1,64 @@
|
||||
import { Router } from 'express';
|
||||
import { auth } from '../middleware/auth';
|
||||
import { body, param, query, validate } from '../middleware/validate';
|
||||
import {
|
||||
addWorkspaceWebsite,
|
||||
checkIsWorkspaceUser,
|
||||
getWorkspaceWebsites,
|
||||
} from '../model/workspace';
|
||||
|
||||
export const workspaceRouter = Router();
|
||||
|
||||
workspaceRouter.get(
|
||||
'/websites',
|
||||
validate(
|
||||
query('workspaceId')
|
||||
.isString()
|
||||
.withMessage('workspaceId should be string')
|
||||
.isUUID()
|
||||
.withMessage('workspaceId should be UUID')
|
||||
),
|
||||
auth(),
|
||||
async (req, res) => {
|
||||
const userId = req.user!.id;
|
||||
const workspaceId = req.query.workspaceId as string;
|
||||
|
||||
const isWorkspaceUser = await checkIsWorkspaceUser(workspaceId, userId);
|
||||
|
||||
if (!isWorkspaceUser) {
|
||||
throw new Error('Is not workspace user');
|
||||
}
|
||||
|
||||
const websites = await getWorkspaceWebsites(workspaceId);
|
||||
|
||||
res.json({ websites });
|
||||
}
|
||||
);
|
||||
|
||||
workspaceRouter.post(
|
||||
'/website',
|
||||
validate(
|
||||
body('workspaceId')
|
||||
.isString()
|
||||
.withMessage('workspaceId should be string')
|
||||
.isUUID()
|
||||
.withMessage('workspaceId should be UUID'),
|
||||
body('name').isString().withMessage('name should be a string'),
|
||||
body('domain').isURL().withMessage('domain should be URL')
|
||||
),
|
||||
auth(),
|
||||
async (req, res) => {
|
||||
const userId = req.user!.id;
|
||||
const { workspaceId, name, domain } = req.body;
|
||||
|
||||
const isWorkspaceUser = await checkIsWorkspaceUser(workspaceId, userId);
|
||||
|
||||
if (!isWorkspaceUser) {
|
||||
throw new Error('Is not workspace user');
|
||||
}
|
||||
|
||||
const website = await addWorkspaceWebsite(workspaceId, name, domain);
|
||||
|
||||
res.json({ website });
|
||||
}
|
||||
);
|
7
src/server/types/global.d.ts
vendored
Normal file
7
src/server/types/global.d.ts
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
import type { JWTPayload } from '../middleware/auth';
|
||||
|
||||
declare global {
|
||||
namespace Express {
|
||||
interface User extends JWTPayload {}
|
||||
}
|
||||
}
|
@ -13,7 +13,7 @@
|
||||
"moduleResolution": "Node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true
|
||||
"noEmit": true,
|
||||
},
|
||||
"include": ["src"]
|
||||
"include": ["src", "types"]
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user