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 { 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

View File

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

View File

@ -1,16 +1,27 @@
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.PropsWithChildren> =
React.memo((props) => {
const [loading, setLoading] = useState(true);
const mutation = trpc.user.loginWithToken.useMutation();
useEffect(() => {
const token = getJWT();
if (token) {
loginWithToken().finally(() => {
mutation
.mutateAsync({
token,
})
.then((res) => {
setJWT(res.token);
setUserInfo(res.info);
})
.catch((err) => {})
.finally(() => {
setLoading(false);
});
} else {

View File

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

View File

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

View File

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

View File

@ -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<UserState>(() => ({
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,8 +23,22 @@ export function setUserInfo(info: UserLoginInfo) {
});
// create socketio after login
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() {
const currentWorkspaceId = useUserStore(

View File

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

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 { websiteRouter } from './website';
import { monitorRouter } from './monitor';
import { userRouter } from './user';
export const appRouter = router({
user: userRouter,
website: websiteRouter,
notification: notificationRouter,
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 PickRequired<T, U extends keyof T> = Omit<T, keyof U> &
Required<Pick<T, U>>;