feat: add profile page and change password form
This commit is contained in:
parent
0c1c72ccab
commit
461d819246
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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();
|
||||
},
|
||||
},
|
||||
],
|
||||
}}
|
||||
|
111
src/client/pages/Settings/Profile.tsx
Normal file
111
src/client/pages/Settings/Profile.tsx
Normal 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';
|
@ -5,12 +5,9 @@ import { WebsiteInfo } from '../../components/WebsiteInfo';
|
||||
import { WebsiteList } from '../../components/WebsiteList';
|
||||
import { useEvent } from '../../hooks/useEvent';
|
||||
import { NotificationList } from './NotificationList';
|
||||
import { Profile } from './Profile';
|
||||
|
||||
export const SettingsPage: React.FC = React.memo(() => {
|
||||
const navigate = useNavigate();
|
||||
const { pathname } = useLocation();
|
||||
|
||||
const items: MenuProps['items'] = [
|
||||
const items: MenuProps['items'] = [
|
||||
{
|
||||
key: 'websites',
|
||||
label: 'Websites',
|
||||
@ -19,7 +16,15 @@ 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>
|
||||
|
@ -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),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -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);
|
||||
}),
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user