From bd7a5776c3b003c7d25bbdc17d1eb3a93c4d97e1 Mon Sep 17 00:00:00 2001 From: moonrailgun Date: Tue, 5 Sep 2023 01:18:43 +0800 Subject: [PATCH] feat: webiste add and list --- nodemon.json | 7 +++ package.json | 3 +- pnpm-lock.yaml | 25 +++++++++ prisma/schema.prisma | 6 +- src/client/App.tsx | 47 ++++++++-------- src/client/api/cache.ts | 10 ++++ src/client/api/model/user.ts | 19 ++++++- src/client/api/model/website.ts | 54 ++++++++++++++++++ src/client/components/DateFilter.tsx | 2 +- src/client/components/Loading.tsx | 11 ++++ src/client/components/NoWorkspaceTip.tsx | 6 ++ src/client/pages/Website.tsx | 70 ++++++++++++++++-------- src/client/store/user.ts | 18 +----- src/server/main.ts | 4 +- src/server/middleware/auth.ts | 2 +- src/server/model/workspace.ts | 52 ++++++++++++++++++ src/server/router/workspace.ts | 64 ++++++++++++++++++++++ src/server/types/global.d.ts | 7 +++ tsconfig.json | 4 +- 19 files changed, 341 insertions(+), 70 deletions(-) create mode 100644 nodemon.json create mode 100644 src/client/api/cache.ts create mode 100644 src/client/api/model/website.ts create mode 100644 src/client/components/Loading.tsx create mode 100644 src/client/components/NoWorkspaceTip.tsx create mode 100644 src/server/model/workspace.ts create mode 100644 src/server/router/workspace.ts create mode 100644 src/server/types/global.d.ts 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 (
-
Servers
+
Websites
- + 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"] }