feat: add profile page and change password form

This commit is contained in:
moonrailgun 2023-10-20 23:12:42 +08:00
parent 0c1c72ccab
commit 461d819246
7 changed files with 206 additions and 13 deletions

View File

@ -9,3 +9,7 @@ export function getJWT(): string | null {
export function setJWT(jwt: string) {
window.localStorage.setItem(TOKEN_STORAGE_KEY, jwt);
}
export function clearJWT() {
window.localStorage.removeItem(TOKEN_STORAGE_KEY);
}

View File

@ -1,4 +1,8 @@
import dayjs from 'dayjs';
import { useUserStore } from '../../store/user';
import { useEvent } from '../../hooks/useEvent';
import { useNavigate } from 'react-router';
import { clearJWT } from '../auth';
/**
* Mock
@ -7,3 +11,15 @@ import dayjs from 'dayjs';
export function getUserTimezone(): string {
return dayjs.tz.guess() ?? 'utc';
}
export function useLogout() {
const navigate = useNavigate();
const logout = useEvent(() => {
useUserStore.setState({ info: null });
clearJWT();
navigate('/login');
});
return logout;
}

View File

@ -4,6 +4,7 @@ import { NavItem } from '../components/NavItem';
import { UserOutlined } from '@ant-design/icons';
import { Button, Dropdown } from 'antd';
import { useUserStore } from '../store/user';
import { useLogout } from '../api/model/user';
export const Layout: React.FC = React.memo(() => {
const [params] = useSearchParams();
@ -14,12 +15,13 @@ export const Layout: React.FC = React.memo(() => {
id: w.workspace.id,
name: w.workspace.name,
role: w.role,
current: userInfo.currentWorkspace.id === w.workspace.id,
current: userInfo.currentWorkspace?.id === w.workspace.id,
}));
}
return [];
});
const logout = useLogout();
const showHeader = !params.has('hideHeader');
return (
@ -57,6 +59,9 @@ export const Layout: React.FC = React.memo(() => {
{
key: 'logout',
label: 'Logout',
onClick: () => {
logout();
},
},
],
}}

View File

@ -0,0 +1,111 @@
import { Button, Card, Form, Input, Modal, Typography } from 'antd';
import React, { useState } from 'react';
import { useUserStore } from '../../store/user';
import { PageHeader } from '../../components/PageHeader';
import {
defaultErrorHandler,
defaultSuccessHandler,
trpc,
} from '../../api/trpc';
import { useLogout } from '../../api/model/user';
export const Profile: React.FC = React.memo(() => {
const userInfo = useUserStore((state) => state.info);
const [openChangePassword, setOpenChangePassword] = useState(false);
const changePasswordMutation = trpc.user.changePassword.useMutation({
onSuccess: defaultSuccessHandler,
onError: defaultErrorHandler,
});
const logout = useLogout();
return (
<div>
<PageHeader title="Profile" />
<Card>
<Form layout="vertical">
<Form.Item label="User Id">
<Typography.Text copyable={true} code={true}>
{userInfo?.id}
</Typography.Text>
</Form.Item>
<Form.Item label="Current Workspace Id">
<Typography.Text copyable={true} code={true}>
{userInfo?.currentWorkspace?.id}
</Typography.Text>
</Form.Item>
<Form.Item label="Password">
<Button danger={true} onClick={() => setOpenChangePassword(true)}>
Change Password
</Button>
</Form.Item>
</Form>
</Card>
<Modal
open={openChangePassword}
footer={null}
maskClosable={false}
onCancel={() => setOpenChangePassword(false)}
destroyOnClose={true}
>
<Form
layout="vertical"
onFinish={async (values) => {
const { oldPassword, newPassword } = values;
await changePasswordMutation.mutateAsync({
oldPassword,
newPassword,
});
logout();
}}
>
<Form.Item
label="Old Password"
name="oldPassword"
rules={[{ required: true }]}
>
<Input.Password />
</Form.Item>
<Form.Item
label="New Password"
name="newPassword"
rules={[{ required: true }]}
>
<Input.Password />
</Form.Item>
<Form.Item
label="Old Password"
name="newPasswordRepeat"
rules={[
{ required: true },
(form) => ({
validator(rule, value) {
if (!value || form.getFieldValue('newPassword') === value) {
return Promise.resolve();
}
return Promise.reject('The two passwords are not consistent');
},
}),
]}
>
<Input.Password />
</Form.Item>
<Form.Item className="text-right">
<Button
type="primary"
htmlType="submit"
loading={changePasswordMutation.isLoading}
>
Submit
</Button>
</Form.Item>
</Form>
</Modal>
</div>
);
});
Profile.displayName = 'Profile';

View File

@ -5,10 +5,7 @@ import { WebsiteInfo } from '../../components/WebsiteInfo';
import { WebsiteList } from '../../components/WebsiteList';
import { useEvent } from '../../hooks/useEvent';
import { NotificationList } from './NotificationList';
export const SettingsPage: React.FC = React.memo(() => {
const navigate = useNavigate();
const { pathname } = useLocation();
import { Profile } from './Profile';
const items: MenuProps['items'] = [
{
@ -19,8 +16,16 @@ export const SettingsPage: React.FC = React.memo(() => {
key: 'notifications',
label: 'Notifications',
},
{
key: 'profile',
label: 'Profile',
},
];
export const SettingsPage: React.FC = React.memo(() => {
const navigate = useNavigate();
const { pathname } = useLocation();
const onClick: MenuProps['onClick'] = useEvent((e) => {
navigate(`/settings/${e.key}`);
});
@ -46,6 +51,7 @@ export const SettingsPage: React.FC = React.memo(() => {
<Route path="/websites" element={<WebsiteList />} />
<Route path="/website/:websiteId" element={<WebsiteInfo />} />
<Route path="/notifications" element={<NotificationList />} />
<Route path="/profile" element={<Profile />} />
</Routes>
</div>
</div>

View File

@ -2,6 +2,7 @@ import { prisma } from './_client';
import bcryptjs from 'bcryptjs';
import { ROLES, SYSTEM_ROLES } from '../utils/const';
import { jwtVerify } from '../middleware/auth';
import { TRPCError } from '@trpc/server';
async function hashPassword(password: string) {
return await bcryptjs.hash(password, 10);
@ -191,3 +192,39 @@ export async function findUser(userId: string) {
return user;
}
export async function changeUserPassword(
userId: string,
oldPassword: string,
newPassword: string
) {
const user = await prisma.user.findUnique({
where: {
id: userId,
},
});
if (!user) {
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: 'user not found',
});
}
const checkPassword = await comparePassword(oldPassword, user.password);
if (!checkPassword) {
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: 'old password not correct',
});
}
return prisma.user.update({
where: {
id: userId,
},
data: {
password: await hashPassword(newPassword),
},
});
}

View File

@ -1,8 +1,9 @@
import { publicProcedure, router } from '../trpc';
import { protectProedure, publicProcedure, router } from '../trpc';
import { z } from 'zod';
import {
authUser,
authUserWithToken,
changeUserPassword,
createAdminUser,
createUser,
getUserCount,
@ -74,4 +75,17 @@ export const userRouter = router({
return { info: user, token };
}
}),
changePassword: protectProedure
.input(
z.object({
oldPassword: z.string(),
newPassword: z.string(),
})
)
.mutation(async ({ input, ctx }) => {
const userId = ctx.user.id;
const { oldPassword, newPassword } = input;
return changeUserPassword(userId, oldPassword, newPassword);
}),
});