refactor: migrate user api to trpc

This commit is contained in:
moonrailgun 2023-10-16 22:34:01 +08:00
parent 7ba22f62a9
commit 0e669a2ca1
12 changed files with 144 additions and 163 deletions

View File

@ -1,53 +1,4 @@
import dayjs from 'dayjs'; 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 * Mock

View File

@ -10,8 +10,8 @@ export { getQueryKey };
export const trpc = createTRPCReact<AppRouter>(); export const trpc = createTRPCReact<AppRouter>();
export type RouterInput = inferRouterInputs<AppRouter>; export type AppRouterInput = inferRouterInputs<AppRouter>;
export type RouterOutput = inferRouterOutputs<AppRouter>; export type AppRouterOutput = inferRouterOutputs<AppRouter>;
export const trpcClient = trpc.createClient({ export const trpcClient = trpc.createClient({
links: [ links: [

View File

@ -1,16 +1,27 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { getJWT } from '../api/auth'; import { getJWT, setJWT } from '../api/auth';
import { loginWithToken } from '../api/model/user';
import { Loading } from './Loading'; import { Loading } from './Loading';
import { trpc } from '../api/trpc';
import { setUserInfo } from '../store/user';
export const TokenLoginContainer: React.FC<React.PropsWithChildren> = export const TokenLoginContainer: React.FC<React.PropsWithChildren> =
React.memo((props) => { React.memo((props) => {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const mutation = trpc.user.loginWithToken.useMutation();
useEffect(() => { useEffect(() => {
const token = getJWT(); const token = getJWT();
if (token) { if (token) {
loginWithToken().finally(() => { mutation
.mutateAsync({
token,
})
.then((res) => {
setJWT(res.token);
setUserInfo(res.info);
})
.catch((err) => {})
.finally(() => {
setLoading(false); setLoading(false);
}); });
} else { } else {

View File

@ -1,12 +1,12 @@
import { Table } from 'antd'; import { Table } from 'antd';
import { ColumnsType } from 'antd/es/table/interface'; import { ColumnsType } from 'antd/es/table/interface';
import React from 'react'; import React from 'react';
import { RouterOutput, trpc } from '../../api/trpc'; import { AppRouterOutput, trpc } from '../../api/trpc';
import { useCurrentWorkspaceId } from '../../store/user'; import { useCurrentWorkspaceId } from '../../store/user';
import { sum } from 'lodash-es'; import { sum } from 'lodash-es';
import millify from 'millify'; import millify from 'millify';
type MetricsItemType = RouterOutput['website']['metrics'][number]; type MetricsItemType = AppRouterOutput['website']['metrics'][number];
interface MetricsTableProps { interface MetricsTableProps {
websiteId: string; websiteId: string;

View File

@ -1,14 +1,23 @@
import { Button, Form, Input, Typography } from 'antd'; import { Button, Form, Input, Typography } from 'antd';
import React from 'react'; import React from 'react';
import { model } from '../api/model';
import { useNavigate } from 'react-router'; import { useNavigate } from 'react-router';
import { useRequest } from '../hooks/useRequest'; 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(() => { export const Login: React.FC = React.memo(() => {
const navigate = useNavigate(); const navigate = useNavigate();
const mutation = trpc.user.login.useMutation();
const [{ loading }, handleLogin] = useRequest(async (values: any) => { 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'); navigate('/dashboard');
}); });

View File

@ -1,14 +1,24 @@
import { Button, Form, Input, Typography } from 'antd'; import { Button, Form, Input, Typography } from 'antd';
import React from 'react'; import React from 'react';
import { model } from '../api/model';
import { useNavigate } from 'react-router'; import { useNavigate } from 'react-router';
import { useRequest } from '../hooks/useRequest'; 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(() => { export const Register: React.FC = React.memo(() => {
const navigate = useNavigate(); const navigate = useNavigate();
const mutation = trpc.user.register.useMutation();
const [{ loading }, handleRegister] = useRequest(async (values: any) => { 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'); navigate('/dashboard');
}); });

View File

@ -1,6 +1,8 @@
import { create } from 'zustand'; import { create } from 'zustand';
import { UserLoginInfo } from '../api/model/user';
import { createSocketIOClient } from '../api/socketio'; import { createSocketIOClient } from '../api/socketio';
import { AppRouterOutput } from '../api/trpc';
type UserLoginInfo = AppRouterOutput['user']['loginWithToken']['info'];
interface UserState { interface UserState {
info: UserLoginInfo | null; info: UserLoginInfo | null;
@ -13,10 +15,7 @@ export const useUserStore = create<UserState>(() => ({
export function setUserInfo(info: UserLoginInfo) { export function setUserInfo(info: UserLoginInfo) {
if (!info.currentWorkspace && info.workspaces[0]) { if (!info.currentWorkspace && info.workspaces[0]) {
// Make sure currentWorkspace existed // Make sure currentWorkspace existed
info.currentWorkspace = { info.currentWorkspace = info.workspaces[0].workspace;
id: info.workspaces[0].workspace.id,
name: info.workspaces[0].workspace.name,
};
} }
useUserStore.setState({ useUserStore.setState({
@ -24,7 +23,21 @@ export function setUserInfo(info: UserLoginInfo) {
}); });
// create socketio after login // create socketio after login
if (info.currentWorkspace) {
createSocketIOClient(info.currentWorkspace.id); 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() { export function useCurrentWorkspaceId() {

View File

@ -6,7 +6,6 @@ import ViteExpress from 'vite-express';
import compression from 'compression'; import compression from 'compression';
import passport from 'passport'; import passport from 'passport';
import morgan from 'morgan'; import morgan from 'morgan';
import { userRouter } from './router/user';
import { websiteRouter } from './router/website'; import { websiteRouter } from './router/website';
import { workspaceRouter } from './router/workspace'; import { workspaceRouter } from './router/workspace';
import { telemetryRouter } from './router/telemetry'; 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 // http://expressjs.com/en/advanced/best-practice-security.html#at-a-minimum-disable-x-powered-by-header
app.disable('x-powered-by'); app.disable('x-powered-by');
app.use('/api/user', userRouter);
app.use('/api/website', websiteRouter); app.use('/api/website', websiteRouter);
app.use('/api/workspace', workspaceRouter); app.use('/api/workspace', workspaceRouter);
app.use('/telemetry', telemetryRouter); app.use('/telemetry', telemetryRouter);

View File

@ -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 });
}
);

View File

@ -2,8 +2,10 @@ import { router } from '../trpc';
import { notificationRouter } from './notification'; import { notificationRouter } from './notification';
import { websiteRouter } from './website'; import { websiteRouter } from './website';
import { monitorRouter } from './monitor'; import { monitorRouter } from './monitor';
import { userRouter } from './user';
export const appRouter = router({ export const appRouter = router({
user: userRouter,
website: websiteRouter, website: websiteRouter,
notification: notificationRouter, notification: notificationRouter,
monitor: monitorRouter, monitor: monitorRouter,

View File

@ -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 };
}
}),
});

View File

@ -1 +1,4 @@
export type ExactType<T, U extends Partial<T>> = Omit<T, keyof U> & U; export type ExactType<T, U extends Partial<T>> = Omit<T, keyof U> & U;
export type PickRequired<T, U extends keyof T> = Omit<T, keyof U> &
Required<Pick<T, U>>;