diff --git a/nodemon.json b/nodemon.json
new file mode 100644
index 0000000..30c46b8
--- /dev/null
+++ b/nodemon.json
@@ -0,0 +1,7 @@
+{
+ "verbose": true,
+ "watch": ["./src/server"],
+ "ext": "ts",
+ "delay": 1000,
+ "exec": "ts-node --transpileOnly ./src/server/main.ts"
+}
diff --git a/package.json b/package.json
index 0ef098a..b632e1d 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index e7cd97e..ebd6d1a 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -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==}
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 3efd51b..cc18b13 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -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 {
diff --git a/src/client/App.tsx b/src/client/App.tsx
index 84b27af..81fa01d 100644
--- a/src/client/App.tsx
+++ b/src/client/App.tsx
@@ -8,35 +8,38 @@ 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 (
-
-
- {info && (
- }>
- } />
- } />
- } />
- } />
- } />
-
- )}
+
+
+
+ {info && (
+ }>
+ } />
+ } />
+ } />
+ } />
+ } />
+
+ )}
- } />
- } />
+ } />
+ } />
-
- }
- />
-
-
+
+ }
+ />
+
+
+
);
}
diff --git a/src/client/api/cache.ts b/src/client/api/cache.ts
new file mode 100644
index 0000000..053f6a8
--- /dev/null
+++ b/src/client/api/cache.ts
@@ -0,0 +1,10 @@
+import { QueryClient } from '@tanstack/react-query';
+
+export const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ retry: false,
+ refetchOnWindowFocus: false,
+ },
+ },
+});
diff --git a/src/client/api/model/user.ts b/src/client/api/model/user.ts
index 5e87278..36be374 100644
--- a/src/client/api/model/user.ts
+++ b/src/client/api/model/user.ts
@@ -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,
diff --git a/src/client/api/model/website.ts b/src/client/api/model/website.ts
new file mode 100644
index 0000000..eec40ae
--- /dev/null
+++ b/src/client/api/model/website.ts
@@ -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 {
+ 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,
+ });
+}
diff --git a/src/client/components/DateFilter.tsx b/src/client/components/DateFilter.tsx
index b94be57..9cdffa3 100644
--- a/src/client/components/DateFilter.tsx
+++ b/src/client/components/DateFilter.tsx
@@ -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<{
diff --git a/src/client/components/Loading.tsx b/src/client/components/Loading.tsx
new file mode 100644
index 0000000..68e7ec2
--- /dev/null
+++ b/src/client/components/Loading.tsx
@@ -0,0 +1,11 @@
+import { LoadingOutlined } from '@ant-design/icons';
+import React from 'react';
+
+export const Loading: React.FC = React.memo(() => {
+ return (
+
+
+
+ );
+});
+Loading.displayName = 'Loading';
diff --git a/src/client/components/NoWorkspaceTip.tsx b/src/client/components/NoWorkspaceTip.tsx
new file mode 100644
index 0000000..01f9b70
--- /dev/null
+++ b/src/client/components/NoWorkspaceTip.tsx
@@ -0,0 +1,6 @@
+import React from 'react';
+
+export const NoWorkspaceTip: React.FC = React.memo(() => {
+ return Please Select Workspace
;
+});
+NoWorkspaceTip.displayName = 'NoWorkspaceTip';
diff --git a/src/client/pages/Website.tsx b/src/client/pages/Website.tsx
index 96d7f69..b5af3c9 100644
--- a/src/client/pages/Website.tsx
+++ b/src/client/pages/Website.tsx
@@ -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 ;
+ }
return (
-
+
handleAddWebsite()}
onCancel={() => setIsModalOpen(false)}
>
-
+
-
+
@@ -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 => {
+ const columns = useMemo((): ColumnsType => {
return [
{
dataIndex: 'name',
@@ -89,6 +111,10 @@ const WebsiteList: React.FC = React.memo(() => {
];
}, []);
- return ;
+ if (isLoading) {
+ return ;
+ }
+
+ return ;
});
WebsiteList.displayName = 'WebsiteList';
diff --git a/src/client/store/user.ts b/src/client/store/user.ts
index 59c78c9..efafce1 100644
--- a/src/client/store/user.ts
+++ b/src/client/store/user.ts
@@ -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;
diff --git a/src/server/main.ts b/src/server/main.ts
index 911d3ab..e3993ef 100644
--- a/src/server/main.ts
+++ b/src/server/main.ts
@@ -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);
diff --git a/src/server/middleware/auth.ts b/src/server/middleware/auth.ts
index 1dae4aa..447661b 100644
--- a/src/server/middleware/auth.ts
+++ b/src/server/middleware/auth.ts
@@ -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;
diff --git a/src/server/model/workspace.ts b/src/server/model/workspace.ts
new file mode 100644
index 0000000..4ec74f0
--- /dev/null
+++ b/src/server/model/workspace.ts
@@ -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;
+}
diff --git a/src/server/router/workspace.ts b/src/server/router/workspace.ts
new file mode 100644
index 0000000..3e42450
--- /dev/null
+++ b/src/server/router/workspace.ts
@@ -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 });
+ }
+);
diff --git a/src/server/types/global.d.ts b/src/server/types/global.d.ts
new file mode 100644
index 0000000..d10dab8
--- /dev/null
+++ b/src/server/types/global.d.ts
@@ -0,0 +1,7 @@
+import type { JWTPayload } from '../middleware/auth';
+
+declare global {
+ namespace Express {
+ interface User extends JWTPayload {}
+ }
+}
diff --git a/tsconfig.json b/tsconfig.json
index 48916c0..91ff255 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -13,7 +13,7 @@
"moduleResolution": "Node",
"resolveJsonModule": true,
"isolatedModules": true,
- "noEmit": true
+ "noEmit": true,
},
- "include": ["src"]
+ "include": ["src", "types"]
}