From 0e669a2ca1d928af7b5d6363e6b8441a9829a179 Mon Sep 17 00:00:00 2001 From: moonrailgun Date: Mon, 16 Oct 2023 22:34:01 +0800 Subject: [PATCH] refactor: migrate user api to trpc --- src/client/api/model/user.ts | 49 ---------- src/client/api/trpc.ts | 4 +- src/client/components/TokenLoginContainer.tsx | 21 ++++- .../components/website/MetricsTable.tsx | 4 +- src/client/pages/Login.tsx | 13 ++- src/client/pages/Register.tsx | 14 ++- src/client/store/user.ts | 25 +++-- src/server/main.ts | 2 - src/server/router/user.ts | 93 ------------------- src/server/trpc/routers/index.ts | 2 + src/server/trpc/routers/user.ts | 77 +++++++++++++++ src/types/utils.ts | 3 + 12 files changed, 144 insertions(+), 163 deletions(-) delete mode 100644 src/server/router/user.ts create mode 100644 src/server/trpc/routers/user.ts diff --git a/src/client/api/model/user.ts b/src/client/api/model/user.ts index fa81933..485456d 100644 --- a/src/client/api/model/user.ts +++ b/src/client/api/model/user.ts @@ -1,53 +1,4 @@ import dayjs from 'dayjs'; -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, - password, - }); - - setJWT(data.token); - setUserInfo(data.info as UserLoginInfo); -} - -export async function loginWithToken() { - const { data } = await request.post('/api/user/loginWithToken', { - token: getJWT(), - }); - - setJWT(data.token); - setUserInfo(data.info as UserLoginInfo); -} - -export async function register(username: string, password: string) { - const { data } = await request.post('/api/user/register', { - username, - password, - }); - - setJWT(data.token); - setUserInfo(data.info as UserLoginInfo); -} /** * Mock diff --git a/src/client/api/trpc.ts b/src/client/api/trpc.ts index 28a9491..24f3c11 100644 --- a/src/client/api/trpc.ts +++ b/src/client/api/trpc.ts @@ -10,8 +10,8 @@ export { getQueryKey }; export const trpc = createTRPCReact(); -export type RouterInput = inferRouterInputs; -export type RouterOutput = inferRouterOutputs; +export type AppRouterInput = inferRouterInputs; +export type AppRouterOutput = inferRouterOutputs; export const trpcClient = trpc.createClient({ links: [ diff --git a/src/client/components/TokenLoginContainer.tsx b/src/client/components/TokenLoginContainer.tsx index 0dd2187..9147075 100644 --- a/src/client/components/TokenLoginContainer.tsx +++ b/src/client/components/TokenLoginContainer.tsx @@ -1,18 +1,29 @@ import React, { useEffect, useState } from 'react'; -import { getJWT } from '../api/auth'; -import { loginWithToken } from '../api/model/user'; +import { getJWT, setJWT } from '../api/auth'; import { Loading } from './Loading'; +import { trpc } from '../api/trpc'; +import { setUserInfo } from '../store/user'; export const TokenLoginContainer: React.FC = React.memo((props) => { const [loading, setLoading] = useState(true); + const mutation = trpc.user.loginWithToken.useMutation(); useEffect(() => { const token = getJWT(); if (token) { - loginWithToken().finally(() => { - setLoading(false); - }); + mutation + .mutateAsync({ + token, + }) + .then((res) => { + setJWT(res.token); + setUserInfo(res.info); + }) + .catch((err) => {}) + .finally(() => { + setLoading(false); + }); } else { setLoading(false); } diff --git a/src/client/components/website/MetricsTable.tsx b/src/client/components/website/MetricsTable.tsx index 7299249..17fc9bd 100644 --- a/src/client/components/website/MetricsTable.tsx +++ b/src/client/components/website/MetricsTable.tsx @@ -1,12 +1,12 @@ import { Table } from 'antd'; import { ColumnsType } from 'antd/es/table/interface'; import React from 'react'; -import { RouterOutput, trpc } from '../../api/trpc'; +import { AppRouterOutput, trpc } from '../../api/trpc'; import { useCurrentWorkspaceId } from '../../store/user'; import { sum } from 'lodash-es'; import millify from 'millify'; -type MetricsItemType = RouterOutput['website']['metrics'][number]; +type MetricsItemType = AppRouterOutput['website']['metrics'][number]; interface MetricsTableProps { websiteId: string; diff --git a/src/client/pages/Login.tsx b/src/client/pages/Login.tsx index c5898bd..48e915e 100644 --- a/src/client/pages/Login.tsx +++ b/src/client/pages/Login.tsx @@ -1,14 +1,23 @@ import { Button, Form, Input, Typography } from 'antd'; import React from 'react'; -import { model } from '../api/model'; import { useNavigate } from 'react-router'; import { useRequest } from '../hooks/useRequest'; +import { trpc } from '../api/trpc'; +import { setJWT } from '../api/auth'; +import { setUserInfo } from '../store/user'; export const Login: React.FC = React.memo(() => { const navigate = useNavigate(); + const mutation = trpc.user.login.useMutation(); const [{ loading }, handleLogin] = useRequest(async (values: any) => { - await model.user.login(values.username, values.password); + const res = await mutation.mutateAsync({ + username: values.username, + password: values.password, + }); + + setJWT(res.token); + setUserInfo(res.info); navigate('/dashboard'); }); diff --git a/src/client/pages/Register.tsx b/src/client/pages/Register.tsx index 22e83de..8eb967e 100644 --- a/src/client/pages/Register.tsx +++ b/src/client/pages/Register.tsx @@ -1,14 +1,24 @@ import { Button, Form, Input, Typography } from 'antd'; import React from 'react'; -import { model } from '../api/model'; import { useNavigate } from 'react-router'; import { useRequest } from '../hooks/useRequest'; +import { trpc } from '../api/trpc'; +import { setJWT } from '../api/auth'; +import { setUserInfo } from '../store/user'; export const Register: React.FC = React.memo(() => { const navigate = useNavigate(); + const mutation = trpc.user.register.useMutation(); + const [{ loading }, handleRegister] = useRequest(async (values: any) => { - await model.user.register(values.username, values.password); + const res = await mutation.mutateAsync({ + username: values.username, + password: values.password, + }); + setJWT(res.token); + setUserInfo(res.info); + navigate('/dashboard'); }); diff --git a/src/client/store/user.ts b/src/client/store/user.ts index a08dc73..ad86001 100644 --- a/src/client/store/user.ts +++ b/src/client/store/user.ts @@ -1,6 +1,8 @@ import { create } from 'zustand'; -import { UserLoginInfo } from '../api/model/user'; import { createSocketIOClient } from '../api/socketio'; +import { AppRouterOutput } from '../api/trpc'; + +type UserLoginInfo = AppRouterOutput['user']['loginWithToken']['info']; interface UserState { info: UserLoginInfo | null; @@ -13,10 +15,7 @@ export const useUserStore = create(() => ({ export function setUserInfo(info: UserLoginInfo) { if (!info.currentWorkspace && info.workspaces[0]) { // Make sure currentWorkspace existed - info.currentWorkspace = { - id: info.workspaces[0].workspace.id, - name: info.workspaces[0].workspace.name, - }; + info.currentWorkspace = info.workspaces[0].workspace; } useUserStore.setState({ @@ -24,7 +23,21 @@ export function setUserInfo(info: UserLoginInfo) { }); // create socketio after login - createSocketIOClient(info.currentWorkspace.id); + if (info.currentWorkspace) { + createSocketIOClient(info.currentWorkspace.id); + } +} + +export function useCurrentWorkspace() { + const currentWorkspace = useUserStore( + (state) => state.info?.currentWorkspace + ); + + if (!currentWorkspace) { + throw new Error('No Workspace Id'); + } + + return currentWorkspace; } export function useCurrentWorkspaceId() { diff --git a/src/server/main.ts b/src/server/main.ts index ed728c7..1b1dfa9 100644 --- a/src/server/main.ts +++ b/src/server/main.ts @@ -6,7 +6,6 @@ import ViteExpress from 'vite-express'; import compression from 'compression'; import passport from 'passport'; import morgan from 'morgan'; -import { userRouter } from './router/user'; import { websiteRouter } from './router/website'; import { workspaceRouter } from './router/workspace'; import { telemetryRouter } from './router/telemetry'; @@ -36,7 +35,6 @@ app.use(passport.initialize()); // http://expressjs.com/en/advanced/best-practice-security.html#at-a-minimum-disable-x-powered-by-header app.disable('x-powered-by'); -app.use('/api/user', userRouter); app.use('/api/website', websiteRouter); app.use('/api/workspace', workspaceRouter); app.use('/telemetry', telemetryRouter); diff --git a/src/server/router/user.ts b/src/server/router/user.ts deleted file mode 100644 index b101932..0000000 --- a/src/server/router/user.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { Router } from 'express'; -import { body, validate } from '../middleware/validate'; -import { - authUser, - authUserWithToken, - createAdminUser, - createUser, - getUserCount, -} from '../model/user'; -import { auth, jwtSign } from '../middleware/auth'; - -export const userRouter = Router(); - -userRouter.post( - '/login', - validate( - body('username').exists().withMessage('Username should be existed'), - body('password').exists().withMessage('Password should be existed') - ), - async (req, res) => { - const { username, password } = req.body; - - const user = await authUser(username, password); - - const token = jwtSign(user); - - res.json({ info: user, token }); - } -); - -userRouter.post( - '/register', - validate( - body('username').exists().withMessage('Username should be existed'), - body('password').exists().withMessage('Password should be existed') - ), - async (req, res) => { - const { username, password } = req.body; - - const userCount = await getUserCount(); - if (userCount === 0) { - const user = await createAdminUser(username, password); - - const token = jwtSign(user); - - res.json({ info: user, token }); - } else { - const user = await createUser(username, password); - - const token = jwtSign(user); - - res.json({ info: user, token }); - } - } -); - -userRouter.post( - '/loginWithToken', - validate(body('token').exists().withMessage('Token should be existed')), - async (req, res) => { - const { token } = req.body; - - if (!token) { - throw new Error('Cannot get token'); - } - - try { - const user = await authUserWithToken(token); - - const newToken = jwtSign(user); - - res.json({ info: user, token: newToken }); - } catch (err) { - res.status(401).json({ message: 'Invalid token' }); - } - } -); - -userRouter.post( - '/createAdmin', - auth(), - validate( - body('username').exists().withMessage('Username should be existed'), - body('password').exists().withMessage('Password should be existed') - ), - async (req, res) => { - const { username, password } = req.body; - - await createAdminUser(username, password); - - res.json({ result: true }); - } -); diff --git a/src/server/trpc/routers/index.ts b/src/server/trpc/routers/index.ts index 8d10d5f..65e8bfc 100644 --- a/src/server/trpc/routers/index.ts +++ b/src/server/trpc/routers/index.ts @@ -2,8 +2,10 @@ import { router } from '../trpc'; import { notificationRouter } from './notification'; import { websiteRouter } from './website'; import { monitorRouter } from './monitor'; +import { userRouter } from './user'; export const appRouter = router({ + user: userRouter, website: websiteRouter, notification: notificationRouter, monitor: monitorRouter, diff --git a/src/server/trpc/routers/user.ts b/src/server/trpc/routers/user.ts new file mode 100644 index 0000000..573126d --- /dev/null +++ b/src/server/trpc/routers/user.ts @@ -0,0 +1,77 @@ +import { publicProcedure, router } from '../trpc'; +import { z } from 'zod'; +import { + authUser, + authUserWithToken, + createAdminUser, + createUser, + getUserCount, +} from '../../model/user'; +import { jwtSign } from '../../middleware/auth'; +import { TRPCError } from '@trpc/server'; + +export const userRouter = router({ + login: publicProcedure + .input( + z.object({ + username: z.string(), + password: z.string(), + }) + ) + .mutation(async ({ input }) => { + const { username, password } = input; + const user = await authUser(username, password); + + const token = jwtSign(user); + + return { info: user, token }; + }), + loginWithToken: publicProcedure + .input( + z.object({ + token: z.string(), + }) + ) + .mutation(async ({ input }) => { + const { token } = input; + + if (!token) { + throw new Error('Cannot get token'); + } + + try { + const user = await authUserWithToken(token); + + const newToken = jwtSign(user); + + return { info: user, token: newToken }; + } catch (err) { + throw new TRPCError({ code: 'FORBIDDEN', message: 'Invalid token' }); + } + }), + register: publicProcedure + .input( + z.object({ + username: z.string(), + password: z.string(), + }) + ) + .mutation(async ({ input }) => { + const { username, password } = input; + + const userCount = await getUserCount(); + if (userCount === 0) { + const user = await createAdminUser(username, password); + + const token = jwtSign(user); + + return { info: user, token }; + } else { + const user = await createUser(username, password); + + const token = jwtSign(user); + + return { info: user, token }; + } + }), +}); diff --git a/src/types/utils.ts b/src/types/utils.ts index b10b829..f33a9e6 100644 --- a/src/types/utils.ts +++ b/src/types/utils.ts @@ -1 +1,4 @@ export type ExactType> = Omit & U; + +export type PickRequired = Omit & + Required>;