Commit c75f1547 by Archer Committed by GitHub

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>
parent 96e7dd58
...@@ -14,6 +14,7 @@ weight: 792 ...@@ -14,6 +14,7 @@ weight: 792
2. 将所有内置任务,从非 stream 模式调整成 stream 模式,避免部分模型不支持非 stream 模式。如需覆盖,则可以在模型`额外 Body`参数中,强制指定`stream=false`。 2. 将所有内置任务,从非 stream 模式调整成 stream 模式,避免部分模型不支持非 stream 模式。如需覆盖,则可以在模型`额外 Body`参数中,强制指定`stream=false`。
3. qwen3 模型预设 3. qwen3 模型预设
4. 语雀知识库支持设置根目录。 4. 语雀知识库支持设置根目录。
5. 可配置密码过期时间,过期后下次登录会强制要求修改密码。
## ⚙️ 优化 ## ⚙️ 优化
......
...@@ -36,6 +36,7 @@ declare global { ...@@ -36,6 +36,7 @@ declare global {
SHOW_COUPON?: string; SHOW_COUPON?: string;
CONFIG_JSON_PATH?: string; CONFIG_JSON_PATH?: string;
PASSWORD_LOGIN_LOCK_SECONDS?: string; PASSWORD_LOGIN_LOCK_SECONDS?: string;
PASSWORD_EXPIRED_MONTH?: string;
} }
} }
} }
......
...@@ -14,6 +14,7 @@ export type UserModelSchema = { ...@@ -14,6 +14,7 @@ export type UserModelSchema = {
timezone: string; timezone: string;
status: `${UserStatusEnum}`; status: `${UserStatusEnum}`;
lastLoginTmbId?: string; lastLoginTmbId?: string;
passwordUpdateTime?: Date;
fastgpt_sem?: { fastgpt_sem?: {
keyword: string; keyword: string;
}; };
......
...@@ -3,6 +3,7 @@ const { Schema } = connectionMongo; ...@@ -3,6 +3,7 @@ const { Schema } = connectionMongo;
import { hashStr } from '@fastgpt/global/common/string/tools'; import { hashStr } from '@fastgpt/global/common/string/tools';
import type { UserModelSchema } from '@fastgpt/global/support/user/type'; import type { UserModelSchema } from '@fastgpt/global/support/user/type';
import { UserStatusEnum, userStatusMap } from '@fastgpt/global/support/user/constant'; import { UserStatusEnum, userStatusMap } from '@fastgpt/global/support/user/constant';
import { TeamMemberCollectionName } from '@fastgpt/global/support/user/team/constant';
export const userCollectionName = 'users'; export const userCollectionName = 'users';
...@@ -18,9 +19,7 @@ const UserSchema = new Schema({ ...@@ -18,9 +19,7 @@ const UserSchema = new Schema({
required: true, required: true,
unique: true // 唯一 unique: true // 唯一
}, },
phonePrefix: { phonePrefix: Number,
type: Number
},
password: { password: {
type: String, type: String,
required: true, required: true,
...@@ -28,13 +27,14 @@ const UserSchema = new Schema({ ...@@ -28,13 +27,14 @@ const UserSchema = new Schema({
get: (val: string) => hashStr(val), get: (val: string) => hashStr(val),
select: false select: false
}, },
passwordUpdateTime: Date,
createTime: { createTime: {
type: Date, type: Date,
default: () => new Date() default: () => new Date()
}, },
promotionRate: { promotionRate: {
type: Number, type: Number,
default: 15 default: 0
}, },
openaiAccount: { openaiAccount: {
type: { type: {
...@@ -47,7 +47,8 @@ const UserSchema = new Schema({ ...@@ -47,7 +47,8 @@ const UserSchema = new Schema({
default: 'Asia/Shanghai' default: 'Asia/Shanghai'
}, },
lastLoginTmbId: { lastLoginTmbId: {
type: Schema.Types.ObjectId type: Schema.Types.ObjectId,
ref: TeamMemberCollectionName
}, },
inviterId: { inviterId: {
......
...@@ -55,6 +55,7 @@ ...@@ -55,6 +55,7 @@
"please_bind_notification_receiving_path": "Please bind the notification receiving method first", "please_bind_notification_receiving_path": "Please bind the notification receiving method first",
"purchase_extra_package": "Upgrade", "purchase_extra_package": "Upgrade",
"reminder_create_bound_notification_account": "Remind the creator to bind the notification account", "reminder_create_bound_notification_account": "Remind the creator to bind the notification account",
"reset_password": "reset password",
"resource_usage": "Usages", "resource_usage": "Usages",
"select_avatar": "Click to select avatar", "select_avatar": "Click to select avatar",
"standard_package_and_extra_resource_package": "Includes standard and extra plans", "standard_package_and_extra_resource_package": "Includes standard and extra plans",
......
...@@ -28,6 +28,7 @@ ...@@ -28,6 +28,7 @@
"login.success": "Login Successful", "login.success": "Login Successful",
"manage_team": "Manage team", "manage_team": "Manage team",
"name": "Name", "name": "Name",
"new_password": "New Password",
"notification.remind_owner_bind": "Please remind the creator to bind a notification account", "notification.remind_owner_bind": "Please remind the creator to bind a notification account",
"operations": "Actions", "operations": "Actions",
"owner": "owner", "owner": "owner",
......
...@@ -65,6 +65,7 @@ ...@@ -65,6 +65,7 @@
"type": "类型", "type": "类型",
"unlimited": "无限制", "unlimited": "无限制",
"update_password": "修改密码", "update_password": "修改密码",
"reset_password": "重置密码",
"update_success_tip": "更新数据成功", "update_success_tip": "更新数据成功",
"upgrade_package": "升级套餐", "upgrade_package": "升级套餐",
"usage_balance": "使用余额: 使用余额", "usage_balance": "使用余额: 使用余额",
......
...@@ -28,6 +28,7 @@ ...@@ -28,6 +28,7 @@
"login.success": "登录成功", "login.success": "登录成功",
"manage_team": "管理团队", "manage_team": "管理团队",
"name": "名称", "name": "名称",
"new_password": "新密码",
"notification.remind_owner_bind": "请提醒创建者绑定通知账号", "notification.remind_owner_bind": "请提醒创建者绑定通知账号",
"operations": "操作", "operations": "操作",
"owner": "所有者", "owner": "所有者",
......
...@@ -55,6 +55,7 @@ ...@@ -55,6 +55,7 @@
"please_bind_notification_receiving_path": "請先綁定通知接收途徑", "please_bind_notification_receiving_path": "請先綁定通知接收途徑",
"purchase_extra_package": "購買額外套餐", "purchase_extra_package": "購買額外套餐",
"reminder_create_bound_notification_account": "提醒建立者綁定通知帳號", "reminder_create_bound_notification_account": "提醒建立者綁定通知帳號",
"reset_password": "重置密碼",
"resource_usage": "資源用量", "resource_usage": "資源用量",
"select_avatar": "點選選擇頭像", "select_avatar": "點選選擇頭像",
"standard_package_and_extra_resource_package": "包含標準套餐與額外資源包", "standard_package_and_extra_resource_package": "包含標準套餐與額外資源包",
......
...@@ -28,6 +28,7 @@ ...@@ -28,6 +28,7 @@
"login.success": "登入成功", "login.success": "登入成功",
"manage_team": "管理團隊", "manage_team": "管理團隊",
"name": "名稱", "name": "名稱",
"new_password": "新密碼",
"notification.remind_owner_bind": "請提醒建立者綁定通知帳號", "notification.remind_owner_bind": "請提醒建立者綁定通知帳號",
"operations": "操作", "operations": "操作",
"owner": "擁有者", "owner": "擁有者",
......
...@@ -63,6 +63,8 @@ WORKFLOW_MAX_LOOP_TIMES=50 ...@@ -63,6 +63,8 @@ WORKFLOW_MAX_LOOP_TIMES=50
CHECK_INTERNAL_IP=false CHECK_INTERNAL_IP=false
# 密码错误锁时长:s # 密码错误锁时长:s
PASSWORD_LOGIN_LOCK_SECONDS= PASSWORD_LOGIN_LOCK_SECONDS=
# 密码过期月份,不设置则不会过期
PASSWORD_EXPIRED_MONTH=
# 特殊配置 # 特殊配置
# 自定义跨域,不配置时,默认都允许跨域(逗号分割) # 自定义跨域,不配置时,默认都允许跨域(逗号分割)
......
...@@ -18,12 +18,26 @@ import WorkorderButton from './WorkorderButton'; ...@@ -18,12 +18,26 @@ import WorkorderButton from './WorkorderButton';
const Navbar = dynamic(() => import('./navbar')); const Navbar = dynamic(() => import('./navbar'));
const NavbarPhone = dynamic(() => import('./navbarPhone')); const NavbarPhone = dynamic(() => import('./navbarPhone'));
const NotSufficientModal = dynamic(() => import('@/components/support/wallet/NotSufficientModal'));
const SystemMsgModal = dynamic(() => import('@/components/support/user/inform/SystemMsgModal')); const ResetExpiredPswModal = dynamic(
const ImportantInform = dynamic(() => import('@/components/support/user/inform/ImportantInform')); () => import('@/components/support/user/safe/ResetExpiredPswModal'),
const UpdateContact = dynamic(() => import('@/components/support/user/inform/UpdateContactModal')); { ssr: false }
const ManualCopyModal = dynamic(() => );
import('@fastgpt/web/hooks/useCopyData').then((mod) => mod.ManualCopyModal) 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> = { const pcUnShowLayoutRoute: Record<string, boolean> = {
...@@ -56,8 +70,7 @@ const Layout = ({ children }: { children: JSX.Element }) => { ...@@ -56,8 +70,7 @@ const Layout = ({ children }: { children: JSX.Element }) => {
const { toast } = useToast(); const { toast } = useToast();
const { t } = useTranslation(); const { t } = useTranslation();
const { Loading } = useLoading(); const { Loading } = useLoading();
const { loading, feConfigs, notSufficientModalType, llmModelList, embeddingModelList } = const { loading, feConfigs, llmModelList, embeddingModelList } = useSystemStore();
useSystemStore();
const { isPc } = useSystem(); const { isPc } = useSystem();
const { userInfo, isUpdateNotification, setIsUpdateNotification } = useUserStore(); const { userInfo, isUpdateNotification, setIsUpdateNotification } = useUserStore();
const { setUserDefaultLng } = useI18nLng(); const { setUserDefaultLng } = useI18nLng();
...@@ -66,6 +79,7 @@ const Layout = ({ children }: { children: JSX.Element }) => { ...@@ -66,6 +79,7 @@ const Layout = ({ children }: { children: JSX.Element }) => {
() => router.pathname === '/chat' && Object.values(router.query).join('').length !== 0, () => router.pathname === '/chat' && Object.values(router.query).join('').length !== 0,
[router.pathname, router.query] [router.pathname, router.query]
); );
const isHideNavbar = !!pcUnShowLayoutRoute[router.pathname];
// System hook // System hook
const { data, refetch: refetchUnRead } = useQuery(['getUnreadCount'], getUnreadCount, { const { data, refetch: refetchUnRead } = useQuery(['getUnreadCount'], getUnreadCount, {
...@@ -75,8 +89,6 @@ const Layout = ({ children }: { children: JSX.Element }) => { ...@@ -75,8 +89,6 @@ const Layout = ({ children }: { children: JSX.Element }) => {
const unread = data?.unReadCount || 0; const unread = data?.unReadCount || 0;
const importantInforms = data?.importantInforms || []; const importantInforms = data?.importantInforms || [];
const isHideNavbar = !!pcUnShowLayoutRoute[router.pathname];
const showUpdateNotification = const showUpdateNotification =
isUpdateNotification && isUpdateNotification &&
feConfigs?.bind_notification_method && feConfigs?.bind_notification_method &&
...@@ -153,14 +165,15 @@ const Layout = ({ children }: { children: JSX.Element }) => { ...@@ -153,14 +165,15 @@ const Layout = ({ children }: { children: JSX.Element }) => {
</Box> </Box>
{feConfigs?.isPlus && ( {feConfigs?.isPlus && (
<> <>
{notSufficientModalType && <NotSufficientModal type={notSufficientModalType} />} <NotSufficientModal />
{!!userInfo && <SystemMsgModal />} <SystemMsgModal />
{showUpdateNotification && ( {showUpdateNotification && (
<UpdateContact onClose={() => setIsUpdateNotification(false)} mode="contact" /> <UpdateContact onClose={() => setIsUpdateNotification(false)} mode="contact" />
)} )}
{!!userInfo && importantInforms.length > 0 && ( {!!userInfo && importantInforms.length > 0 && (
<ImportantInform informs={importantInforms} refetch={refetchUnRead} /> <ImportantInform informs={importantInforms} refetch={refetchUnRead} />
)} )}
<ResetExpiredPswModal />
<WorkorderButton /> <WorkorderButton />
</> </>
)} )}
......
...@@ -11,19 +11,27 @@ const Markdown = dynamic(() => import('@/components/Markdown'), { ssr: false }); ...@@ -11,19 +11,27 @@ const Markdown = dynamic(() => import('@/components/Markdown'), { ssr: false });
const SystemMsgModal = ({}: {}) => { const SystemMsgModal = ({}: {}) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { systemMsgReadId, setSysMsgReadId } = useUserStore(); const { userInfo, systemMsgReadId, setSysMsgReadId } = useUserStore();
const { isOpen, onOpen, onClose } = useDisclosure(); const { isOpen, onOpen, onClose } = useDisclosure();
const { data } = useRequest2(getSystemMsgModalData, { const { data } = useRequest2(
refreshDeps: [systemMsgReadId], async () => {
manual: false, if (!userInfo?._id) {
onSuccess(res) { return;
if (res?.content && (!systemMsgReadId || res.id !== systemMsgReadId)) { }
onOpen(); return getSystemMsgModalData();
},
{
refreshDeps: [systemMsgReadId, userInfo?._id],
manual: false,
onSuccess(res) {
if (res?.content && (!systemMsgReadId || res.id !== systemMsgReadId)) {
onOpen();
}
} }
} }
}); );
const onclickRead = useCallback(() => { const onclickRead = useCallback(() => {
if (!data) return; if (!data) return;
...@@ -31,12 +39,8 @@ const SystemMsgModal = ({}: {}) => { ...@@ -31,12 +39,8 @@ const SystemMsgModal = ({}: {}) => {
onClose(); onClose();
}, [data, onClose, setSysMsgReadId]); }, [data, onClose, setSysMsgReadId]);
return ( return isOpen ? (
<MyModal <MyModal isOpen iconSrc={LOGO_ICON} title={t('common:support.user.inform.System message')}>
isOpen={isOpen}
iconSrc={LOGO_ICON}
title={t('common:support.user.inform.System message')}
>
<ModalBody overflow={'auto'}> <ModalBody overflow={'auto'}>
<Markdown source={data?.content} /> <Markdown source={data?.content} />
</ModalBody> </ModalBody>
...@@ -44,7 +48,7 @@ const SystemMsgModal = ({}: {}) => { ...@@ -44,7 +48,7 @@ const SystemMsgModal = ({}: {}) => {
<Button onClick={onclickRead}>{t('common:support.inform.Read')}</Button> <Button onClick={onclickRead}>{t('common:support.inform.Read')}</Button>
</ModalFooter> </ModalFooter>
</MyModal> </MyModal>
); ) : null;
}; };
export default React.memo(SystemMsgModal); export default React.memo(SystemMsgModal);
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 ...@@ -12,9 +12,9 @@ import { standardSubLevelMap } from '@fastgpt/global/support/wallet/sub/constant
import { TeamErrEnum } from '@fastgpt/global/common/error/code/team'; import { TeamErrEnum } from '@fastgpt/global/common/error/code/team';
import { useMount } from 'ahooks'; import { useMount } from 'ahooks';
const NotSufficientModal = ({ type }: { type: NotSufficientModalType }) => { const NotSufficientModal = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { setNotSufficientModalType } = useSystemStore(); const { notSufficientModalType: type, setNotSufficientModalType } = useSystemStore();
const onClose = () => setNotSufficientModalType(undefined); const onClose = () => setNotSufficientModalType(undefined);
...@@ -35,7 +35,7 @@ const NotSufficientModal = ({ type }: { type: NotSufficientModalType }) => { ...@@ -35,7 +35,7 @@ const NotSufficientModal = ({ type }: { type: NotSufficientModalType }) => {
[TeamErrEnum.reRankNotEnough]: t('common:code_error.team_error.re_rank_not_enough') [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'}> <MyModal isOpen iconSrc="common/confirm/deleteTip" title={t('common:Warning')} w={'420px'}>
<ModalBody>{textMap[type]}</ModalBody> <ModalBody>{textMap[type]}</ModalBody>
...@@ -57,7 +57,7 @@ const NotSufficientModal = ({ type }: { type: NotSufficientModalType }) => { ...@@ -57,7 +57,7 @@ const NotSufficientModal = ({ type }: { type: NotSufficientModalType }) => {
<RechargeModal onClose={onRechargeModalClose} onPaySuccess={onClose} /> <RechargeModal onClose={onRechargeModalClose} onPaySuccess={onClose} />
)} )}
</> </>
); ) : null;
}; };
export default NotSufficientModal; export default NotSufficientModal;
......
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);
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);
import type { NextApiRequest, NextApiResponse } from 'next'; import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { authCert } from '@fastgpt/service/support/permission/auth/common'; import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { MongoUser } from '@fastgpt/service/support/user/schema'; import { MongoUser } from '@fastgpt/service/support/user/schema';
import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema'; 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>) { async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try { const { oldPsw, newPsw } = req.body as { oldPsw: string; newPsw: string };
const { oldPsw, newPsw } = req.body as { oldPsw: string; newPsw: string };
if (!oldPsw || !newPsw) {
if (!oldPsw || !newPsw) { return Promise.reject('Params is missing');
throw new Error('Params is missing'); }
}
const { tmbId } = await authCert({ req, authToken: true });
const { tmbId } = await authCert({ req, authToken: true }); const tmb = await MongoTeamMember.findById(tmbId);
const tmb = await MongoTeamMember.findById(tmbId); if (!tmb) {
if (!tmb) { return Promise.reject('can not find it');
throw new Error('can not find it'); }
} const userId = tmb.userId;
const userId = tmb.userId; // auth old password
// auth old password const user = await MongoUser.findOne({
const user = await MongoUser.findOne({ _id: userId,
_id: userId, password: oldPsw
password: oldPsw });
});
if (!user) {
if (!user) { return Promise.reject(i18nT('common:user.Old password is error'));
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(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 = ({ ...@@ -60,14 +60,14 @@ const FastLogin = ({
}, 1000); }, 1000);
} }
}, },
[loginSuccess, router, toast] [loginSuccess, router, t, toast]
); );
useEffect(() => { useEffect(() => {
clearToken(); clearToken();
router.prefetch(callbackUrl); router.prefetch(callbackUrl);
authCode(code, token); authCode(code, token);
}, []); }, [authCode, callbackUrl, code, router, token]);
return <Loading />; return <Loading />;
}; };
......
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 ...@@ -68,6 +68,14 @@ export const updatePasswordByOld = ({ oldPsw, newPsw }: { oldPsw: string; newPsw
newPsw: hashStr(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 }) => export const updateNotificationAccount = (data: { account: string; verifyCode: string }) =>
PUT('/proApi/support/user/team/updateNotificationAccount', data); PUT('/proApi/support/user/team/updateNotificationAccount', data);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment