mirror of
https://github.com/labring/FastGPT.git
synced 2025-07-23 13:03:50 +00:00
Password security policy (#4765)
* Psw (#4748) * feat: 添加重置密码功能及相关接口 - 在用户模型中新增 passwordUpdateTime 字段以记录密码更新时间。 - 更新用户模式以支持密码更新时间的存储。 - 新增重置密码的模态框组件,允许用户重置密码。 - 实现重置密码的 API 接口,支持根据用户 ID 更新密码。 - 更新相关国际化文件,添加重置密码的提示信息。 * 更新国际化文件,添加重置密码相关提示信息,并优化重置密码模态框的实现。修复部分代码逻辑,确保用户体验流畅。 * 更新国际化文件,添加重置密码相关提示信息,优化重置密码模态框的实现,修复部分代码逻辑,确保用户体验流畅。新增获取用户密码更新时间的API接口,并调整相关逻辑以支持密码重置功能。 * update * fix * fix * Added environment variables NEXT_PUBLIC_PASSWORD_UPDATETIME to support password update time configuration, update related logic to implement password mandatory update function, and optimize the implementation of reset password modal box to improve user experience. * update index * 更新用户密码重置功能,调整相关API接口,优化重置密码模态框的实现,确保用户体验流畅。修复部分代码逻辑,更新国际化提示信息。 * 删除获取用户密码更新时间的API接口,并在布局组件中移除不必要的重置密码模态框。优化代码结构,提升可维护性。 * update * perf: reset expired password code * perf: layout child components * doc * remove invalid env * perf: update password code --------- Co-authored-by: dreamer6680 <1468683855@qq.com>
This commit is contained in:
@@ -63,6 +63,8 @@ WORKFLOW_MAX_LOOP_TIMES=50
|
||||
CHECK_INTERNAL_IP=false
|
||||
# 密码错误锁时长:s
|
||||
PASSWORD_LOGIN_LOCK_SECONDS=
|
||||
# 密码过期月份,不设置则不会过期
|
||||
PASSWORD_EXPIRED_MONTH=
|
||||
|
||||
# 特殊配置
|
||||
# 自定义跨域,不配置时,默认都允许跨域(逗号分割)
|
||||
|
@@ -18,12 +18,26 @@ import WorkorderButton from './WorkorderButton';
|
||||
|
||||
const Navbar = dynamic(() => import('./navbar'));
|
||||
const NavbarPhone = dynamic(() => import('./navbarPhone'));
|
||||
const NotSufficientModal = dynamic(() => import('@/components/support/wallet/NotSufficientModal'));
|
||||
const SystemMsgModal = dynamic(() => import('@/components/support/user/inform/SystemMsgModal'));
|
||||
const ImportantInform = dynamic(() => import('@/components/support/user/inform/ImportantInform'));
|
||||
const UpdateContact = dynamic(() => import('@/components/support/user/inform/UpdateContactModal'));
|
||||
const ManualCopyModal = dynamic(() =>
|
||||
import('@fastgpt/web/hooks/useCopyData').then((mod) => mod.ManualCopyModal)
|
||||
|
||||
const ResetExpiredPswModal = dynamic(
|
||||
() => import('@/components/support/user/safe/ResetExpiredPswModal'),
|
||||
{ ssr: false }
|
||||
);
|
||||
const NotSufficientModal = dynamic(() => import('@/components/support/wallet/NotSufficientModal'), {
|
||||
ssr: false
|
||||
});
|
||||
const SystemMsgModal = dynamic(() => import('@/components/support/user/inform/SystemMsgModal'), {
|
||||
ssr: false
|
||||
});
|
||||
const ImportantInform = dynamic(() => import('@/components/support/user/inform/ImportantInform'), {
|
||||
ssr: false
|
||||
});
|
||||
const UpdateContact = dynamic(() => import('@/components/support/user/inform/UpdateContactModal'), {
|
||||
ssr: false
|
||||
});
|
||||
const ManualCopyModal = dynamic(
|
||||
() => import('@fastgpt/web/hooks/useCopyData').then((mod) => mod.ManualCopyModal),
|
||||
{ ssr: false }
|
||||
);
|
||||
|
||||
const pcUnShowLayoutRoute: Record<string, boolean> = {
|
||||
@@ -56,8 +70,7 @@ const Layout = ({ children }: { children: JSX.Element }) => {
|
||||
const { toast } = useToast();
|
||||
const { t } = useTranslation();
|
||||
const { Loading } = useLoading();
|
||||
const { loading, feConfigs, notSufficientModalType, llmModelList, embeddingModelList } =
|
||||
useSystemStore();
|
||||
const { loading, feConfigs, llmModelList, embeddingModelList } = useSystemStore();
|
||||
const { isPc } = useSystem();
|
||||
const { userInfo, isUpdateNotification, setIsUpdateNotification } = useUserStore();
|
||||
const { setUserDefaultLng } = useI18nLng();
|
||||
@@ -66,6 +79,7 @@ const Layout = ({ children }: { children: JSX.Element }) => {
|
||||
() => router.pathname === '/chat' && Object.values(router.query).join('').length !== 0,
|
||||
[router.pathname, router.query]
|
||||
);
|
||||
const isHideNavbar = !!pcUnShowLayoutRoute[router.pathname];
|
||||
|
||||
// System hook
|
||||
const { data, refetch: refetchUnRead } = useQuery(['getUnreadCount'], getUnreadCount, {
|
||||
@@ -75,8 +89,6 @@ const Layout = ({ children }: { children: JSX.Element }) => {
|
||||
const unread = data?.unReadCount || 0;
|
||||
const importantInforms = data?.importantInforms || [];
|
||||
|
||||
const isHideNavbar = !!pcUnShowLayoutRoute[router.pathname];
|
||||
|
||||
const showUpdateNotification =
|
||||
isUpdateNotification &&
|
||||
feConfigs?.bind_notification_method &&
|
||||
@@ -153,14 +165,15 @@ const Layout = ({ children }: { children: JSX.Element }) => {
|
||||
</Box>
|
||||
{feConfigs?.isPlus && (
|
||||
<>
|
||||
{notSufficientModalType && <NotSufficientModal type={notSufficientModalType} />}
|
||||
{!!userInfo && <SystemMsgModal />}
|
||||
<NotSufficientModal />
|
||||
<SystemMsgModal />
|
||||
{showUpdateNotification && (
|
||||
<UpdateContact onClose={() => setIsUpdateNotification(false)} mode="contact" />
|
||||
)}
|
||||
{!!userInfo && importantInforms.length > 0 && (
|
||||
<ImportantInform informs={importantInforms} refetch={refetchUnRead} />
|
||||
)}
|
||||
<ResetExpiredPswModal />
|
||||
<WorkorderButton />
|
||||
</>
|
||||
)}
|
||||
|
@@ -11,19 +11,27 @@ const Markdown = dynamic(() => import('@/components/Markdown'), { ssr: false });
|
||||
|
||||
const SystemMsgModal = ({}: {}) => {
|
||||
const { t } = useTranslation();
|
||||
const { systemMsgReadId, setSysMsgReadId } = useUserStore();
|
||||
const { userInfo, systemMsgReadId, setSysMsgReadId } = useUserStore();
|
||||
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
|
||||
const { data } = useRequest2(getSystemMsgModalData, {
|
||||
refreshDeps: [systemMsgReadId],
|
||||
manual: false,
|
||||
onSuccess(res) {
|
||||
if (res?.content && (!systemMsgReadId || res.id !== systemMsgReadId)) {
|
||||
onOpen();
|
||||
const { data } = useRequest2(
|
||||
async () => {
|
||||
if (!userInfo?._id) {
|
||||
return;
|
||||
}
|
||||
return getSystemMsgModalData();
|
||||
},
|
||||
{
|
||||
refreshDeps: [systemMsgReadId, userInfo?._id],
|
||||
manual: false,
|
||||
onSuccess(res) {
|
||||
if (res?.content && (!systemMsgReadId || res.id !== systemMsgReadId)) {
|
||||
onOpen();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
const onclickRead = useCallback(() => {
|
||||
if (!data) return;
|
||||
@@ -31,12 +39,8 @@ const SystemMsgModal = ({}: {}) => {
|
||||
onClose();
|
||||
}, [data, onClose, setSysMsgReadId]);
|
||||
|
||||
return (
|
||||
<MyModal
|
||||
isOpen={isOpen}
|
||||
iconSrc={LOGO_ICON}
|
||||
title={t('common:support.user.inform.System message')}
|
||||
>
|
||||
return isOpen ? (
|
||||
<MyModal isOpen iconSrc={LOGO_ICON} title={t('common:support.user.inform.System message')}>
|
||||
<ModalBody overflow={'auto'}>
|
||||
<Markdown source={data?.content} />
|
||||
</ModalBody>
|
||||
@@ -44,7 +48,7 @@ const SystemMsgModal = ({}: {}) => {
|
||||
<Button onClick={onclickRead}>{t('common:support.inform.Read')}</Button>
|
||||
</ModalFooter>
|
||||
</MyModal>
|
||||
);
|
||||
) : null;
|
||||
};
|
||||
|
||||
export default React.memo(SystemMsgModal);
|
||||
|
@@ -0,0 +1,121 @@
|
||||
import React from 'react';
|
||||
import { ModalBody, Box, Flex, Input, ModalFooter, Button, HStack } from '@chakra-ui/react';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { resetPassword, getCheckPswExpired } from '@/web/support/user/api';
|
||||
import { checkPasswordRule } from '@fastgpt/global/common/string/password';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import { useUserStore } from '@/web/support/user/useUserStore';
|
||||
import Icon from '@fastgpt/web/components/common/Icon';
|
||||
|
||||
type FormType = {
|
||||
newPsw: string;
|
||||
confirmPsw: string;
|
||||
};
|
||||
|
||||
const ResetPswModal = () => {
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToast();
|
||||
const { userInfo } = useUserStore();
|
||||
|
||||
const { register, handleSubmit, getValues } = useForm<FormType>({
|
||||
defaultValues: {
|
||||
newPsw: '',
|
||||
confirmPsw: ''
|
||||
}
|
||||
});
|
||||
|
||||
const {
|
||||
data: passwordExpired = false,
|
||||
runAsync,
|
||||
loading: isFetching
|
||||
} = useRequest2(
|
||||
async () => {
|
||||
if (!userInfo?._id) {
|
||||
return false;
|
||||
}
|
||||
return getCheckPswExpired();
|
||||
},
|
||||
{
|
||||
manual: false,
|
||||
refreshDeps: [userInfo?._id]
|
||||
}
|
||||
);
|
||||
|
||||
const { runAsync: onSubmit, loading: isSubmitting } = useRequest2(resetPassword, {
|
||||
onSuccess() {
|
||||
runAsync();
|
||||
},
|
||||
successToast: t('common:user.Update password successful'),
|
||||
errorToast: t('common:user.Update password failed')
|
||||
});
|
||||
|
||||
const onSubmitErr = (err: Record<string, any>) => {
|
||||
const val = Object.values(err)[0];
|
||||
if (!val) return;
|
||||
if (val.message) {
|
||||
toast({
|
||||
status: 'warning',
|
||||
title: val.message,
|
||||
duration: 3000,
|
||||
isClosable: true
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return passwordExpired ? (
|
||||
<MyModal isOpen iconSrc="/imgs/modal/password.svg" title={t('common:user.reset_password')}>
|
||||
<ModalBody>
|
||||
<HStack p="3" color="primary.600" bgColor="primary.50" borderRadius="md">
|
||||
<Icon name="common/info" w="1rem" />
|
||||
<Box fontSize={'xs'}>{t('common:user.reset_password_tip')}</Box>
|
||||
</HStack>
|
||||
<Flex alignItems={'center'} mt={5}>
|
||||
<Box flex={'0 0 70px'} fontSize={'sm'}>
|
||||
{t('common:user.new_password') + ':'}
|
||||
</Box>
|
||||
<Input
|
||||
flex={1}
|
||||
type={'password'}
|
||||
placeholder={t('common:user.password_tip')}
|
||||
{...register('newPsw', {
|
||||
required: true,
|
||||
validate: (val) => {
|
||||
if (!checkPasswordRule(val)) {
|
||||
return t('common:user.password_tip');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
})}
|
||||
></Input>
|
||||
</Flex>
|
||||
<Flex alignItems={'center'} mt={5}>
|
||||
<Box flex={'0 0 70px'} fontSize={'sm'}>
|
||||
{t('common:user.confirm_password') + ':'}
|
||||
</Box>
|
||||
<Input
|
||||
flex={1}
|
||||
type={'password'}
|
||||
placeholder={t('common:user.confirm_password')}
|
||||
{...register('confirmPsw', {
|
||||
required: true,
|
||||
validate: (val) => (getValues('newPsw') === val ? true : t('user:password.not_match'))
|
||||
})}
|
||||
></Input>
|
||||
</Flex>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button
|
||||
isLoading={isSubmitting || isFetching}
|
||||
onClick={handleSubmit((data) => onSubmit(data.newPsw), onSubmitErr)}
|
||||
>
|
||||
{t('common:Confirm')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</MyModal>
|
||||
) : null;
|
||||
};
|
||||
|
||||
export default React.memo(ResetPswModal);
|
@@ -12,9 +12,9 @@ import { standardSubLevelMap } from '@fastgpt/global/support/wallet/sub/constant
|
||||
import { TeamErrEnum } from '@fastgpt/global/common/error/code/team';
|
||||
import { useMount } from 'ahooks';
|
||||
|
||||
const NotSufficientModal = ({ type }: { type: NotSufficientModalType }) => {
|
||||
const NotSufficientModal = () => {
|
||||
const { t } = useTranslation();
|
||||
const { setNotSufficientModalType } = useSystemStore();
|
||||
const { notSufficientModalType: type, setNotSufficientModalType } = useSystemStore();
|
||||
|
||||
const onClose = () => setNotSufficientModalType(undefined);
|
||||
|
||||
@@ -35,7 +35,7 @@ const NotSufficientModal = ({ type }: { type: NotSufficientModalType }) => {
|
||||
[TeamErrEnum.reRankNotEnough]: t('common:code_error.team_error.re_rank_not_enough')
|
||||
};
|
||||
|
||||
return (
|
||||
return type ? (
|
||||
<>
|
||||
<MyModal isOpen iconSrc="common/confirm/deleteTip" title={t('common:Warning')} w={'420px'}>
|
||||
<ModalBody>{textMap[type]}</ModalBody>
|
||||
@@ -57,7 +57,7 @@ const NotSufficientModal = ({ type }: { type: NotSufficientModalType }) => {
|
||||
<RechargeModal onClose={onRechargeModalClose} onPaySuccess={onClose} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
) : null;
|
||||
};
|
||||
|
||||
export default NotSufficientModal;
|
||||
|
@@ -0,0 +1,28 @@
|
||||
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import { checkPswExpired } from '@/service/support/user/account/password';
|
||||
import { authCert } from '@fastgpt/service/support/permission/auth/common';
|
||||
import { MongoUser } from '@fastgpt/service/support/user/schema';
|
||||
|
||||
export type getTimeQuery = {};
|
||||
|
||||
export type getTimeBody = {};
|
||||
|
||||
export type getTimeResponse = boolean;
|
||||
|
||||
async function handler(
|
||||
req: ApiRequestProps<getTimeBody, getTimeQuery>,
|
||||
res: ApiResponseType<getTimeResponse>
|
||||
): Promise<getTimeResponse> {
|
||||
const { userId } = await authCert({ req, authToken: true });
|
||||
|
||||
const user = await MongoUser.findById(userId, 'passwordUpdateTime');
|
||||
|
||||
if (!user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return checkPswExpired({ updateTime: user.passwordUpdateTime });
|
||||
}
|
||||
|
||||
export default NextAPI(handler);
|
@@ -0,0 +1,49 @@
|
||||
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
|
||||
import { authCert } from '@fastgpt/service/support/permission/auth/common';
|
||||
import { MongoUser } from '@fastgpt/service/support/user/schema';
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import { i18nT } from '@fastgpt/web/i18n/utils';
|
||||
import { checkPswExpired } from '@/service/support/user/account/password';
|
||||
|
||||
export type resetExpiredPswQuery = {};
|
||||
|
||||
export type resetExpiredPswBody = {
|
||||
newPsw: string;
|
||||
};
|
||||
|
||||
export type resetExpiredPswResponse = {};
|
||||
|
||||
async function resetExpiredPswHandler(
|
||||
req: ApiRequestProps<resetExpiredPswBody, resetExpiredPswQuery>,
|
||||
res: ApiResponseType<resetExpiredPswResponse>
|
||||
): Promise<resetExpiredPswResponse> {
|
||||
const newPsw = req.body.newPsw;
|
||||
const { userId } = await authCert({ req, authToken: true });
|
||||
const user = await MongoUser.findById(userId, 'passwordUpdateTime').lean();
|
||||
|
||||
if (!user) {
|
||||
return Promise.reject('The password has not expired');
|
||||
}
|
||||
|
||||
// check if can reset password
|
||||
const canReset = checkPswExpired({ updateTime: user.passwordUpdateTime });
|
||||
|
||||
if (!canReset) {
|
||||
return Promise.reject(i18nT('common:user.No_right_to_reset_password'));
|
||||
}
|
||||
|
||||
// 更新对应的记录
|
||||
await MongoUser.updateOne(
|
||||
{
|
||||
_id: userId
|
||||
},
|
||||
{
|
||||
password: newPsw,
|
||||
passwordUpdateTime: new Date()
|
||||
}
|
||||
);
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
export default NextAPI(resetExpiredPswHandler);
|
@@ -1,48 +1,45 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@fastgpt/service/common/response';
|
||||
import { authCert } from '@fastgpt/service/support/permission/auth/common';
|
||||
import { MongoUser } from '@fastgpt/service/support/user/schema';
|
||||
|
||||
import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema';
|
||||
import { i18nT } from '@fastgpt/web/i18n/utils';
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
try {
|
||||
const { oldPsw, newPsw } = req.body as { oldPsw: string; newPsw: string };
|
||||
async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
const { oldPsw, newPsw } = req.body as { oldPsw: string; newPsw: string };
|
||||
|
||||
if (!oldPsw || !newPsw) {
|
||||
throw new Error('Params is missing');
|
||||
}
|
||||
|
||||
const { tmbId } = await authCert({ req, authToken: true });
|
||||
const tmb = await MongoTeamMember.findById(tmbId);
|
||||
if (!tmb) {
|
||||
throw new Error('can not find it');
|
||||
}
|
||||
const userId = tmb.userId;
|
||||
// auth old password
|
||||
const user = await MongoUser.findOne({
|
||||
_id: userId,
|
||||
password: oldPsw
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new Error('user.Old password is error');
|
||||
}
|
||||
|
||||
// 更新对应的记录
|
||||
await MongoUser.findByIdAndUpdate(userId, {
|
||||
password: newPsw
|
||||
});
|
||||
|
||||
jsonRes(res, {
|
||||
data: {
|
||||
user
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
if (!oldPsw || !newPsw) {
|
||||
return Promise.reject('Params is missing');
|
||||
}
|
||||
|
||||
const { tmbId } = await authCert({ req, authToken: true });
|
||||
const tmb = await MongoTeamMember.findById(tmbId);
|
||||
if (!tmb) {
|
||||
return Promise.reject('can not find it');
|
||||
}
|
||||
const userId = tmb.userId;
|
||||
// auth old password
|
||||
const user = await MongoUser.findOne({
|
||||
_id: userId,
|
||||
password: oldPsw
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return Promise.reject(i18nT('common:user.Old password is error'));
|
||||
}
|
||||
|
||||
if (oldPsw === newPsw) {
|
||||
return Promise.reject(i18nT('common:user.Password has no change'));
|
||||
}
|
||||
|
||||
// 更新对应的记录
|
||||
await MongoUser.findByIdAndUpdate(userId, {
|
||||
password: newPsw,
|
||||
passwordUpdateTime: new Date()
|
||||
});
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
export default NextAPI(handler);
|
||||
|
@@ -60,14 +60,14 @@ const FastLogin = ({
|
||||
}, 1000);
|
||||
}
|
||||
},
|
||||
[loginSuccess, router, toast]
|
||||
[loginSuccess, router, t, toast]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
clearToken();
|
||||
router.prefetch(callbackUrl);
|
||||
authCode(code, token);
|
||||
}, []);
|
||||
}, [authCode, callbackUrl, code, router, token]);
|
||||
|
||||
return <Loading />;
|
||||
};
|
||||
|
18
projects/app/src/service/support/user/account/password.ts
Normal file
18
projects/app/src/service/support/user/account/password.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
export const checkPswExpired = ({ updateTime }: { updateTime?: Date }) => {
|
||||
if (!process.env.PASSWORD_EXPIRED_MONTH) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!updateTime) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const expiredMonth = Number(process.env.PASSWORD_EXPIRED_MONTH);
|
||||
if (expiredMonth === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const time = new Date().getTime() - new Date(updateTime).getTime();
|
||||
|
||||
return time > 1000 * 60 * 60 * 24 * 30 * expiredMonth;
|
||||
};
|
@@ -68,6 +68,14 @@ export const updatePasswordByOld = ({ oldPsw, newPsw }: { oldPsw: string; newPsw
|
||||
newPsw: hashStr(newPsw)
|
||||
});
|
||||
|
||||
export const resetPassword = (newPsw: string) =>
|
||||
POST('/support/user/account/resetExpiredPsw', {
|
||||
newPsw: hashStr(newPsw)
|
||||
});
|
||||
|
||||
/* Check the whether password has expired */
|
||||
export const getCheckPswExpired = () => GET<boolean>('/support/user/account/checkPswExpired');
|
||||
|
||||
export const updateNotificationAccount = (data: { account: string; verifyCode: string }) =>
|
||||
PUT('/proApi/support/user/team/updateNotificationAccount', data);
|
||||
|
||||
|
Reference in New Issue
Block a user