From 3412d7009dad9bdeeacfdad05beeae69aca421c8 Mon Sep 17 00:00:00 2001 From: Archer <545436317@qq.com> Date: Wed, 1 Jan 2025 20:54:06 +0800 Subject: [PATCH] perf: password check;perf: image upload check;perf: sso login check (#3509) * perf: password check * perf: image upload check * perf: sso login check --- .../zh-cn/docs/development/upgrading/4818.md | 3 +- packages/global/common/error/code/common.ts | 6 ++ packages/global/common/error/code/user.ts | 6 +- packages/global/common/file/api.d.ts | 5 +- .../global/common/file/image/constants.ts | 61 ------------------ packages/global/common/file/image/type.d.ts | 4 -- .../service/common/file/image/controller.ts | 63 ++++++++++++++++--- packages/service/common/file/image/schema.ts | 23 ++----- packages/service/common/file/read/utils.ts | 4 +- .../common/middle/reqFrequencyLimit.ts | 6 +- packages/service/common/response/index.ts | 1 + .../permission/memberGroup/controllers.ts | 8 ++- .../service/support/user/team/controller.ts | 6 +- packages/web/i18n/en/account_info.json | 2 +- packages/web/i18n/en/common.json | 16 ++--- packages/web/i18n/en/login.json | 11 ++-- packages/web/i18n/en/user.json | 1 - packages/web/i18n/zh-CN/account_info.json | 2 +- packages/web/i18n/zh-CN/common.json | 16 ++--- packages/web/i18n/zh-CN/login.json | 17 ++--- packages/web/i18n/zh-CN/user.json | 1 - packages/web/i18n/zh-Hant/account_info.json | 2 +- packages/web/i18n/zh-Hant/common.json | 16 ++--- packages/web/i18n/zh-Hant/login.json | 11 ++-- packages/web/i18n/zh-Hant/user.json | 1 - .../common/Modal/EditResourceModal.tsx | 44 +++++-------- .../info/components/UpdatePswModal.tsx | 55 ++++++++++------ projects/app/src/pages/account/info/index.tsx | 50 ++++++--------- .../account/team/components/EditInfoModal.tsx | 43 +++++-------- .../components/GroupManage/GroupInfoModal.tsx | 13 ++-- .../components/OrgManage/OrgInfoModal.tsx | 13 ++-- .../src/pages/api/common/file/uploadImage.ts | 30 ++++----- projects/app/src/pages/api/core/app/create.ts | 4 +- projects/app/src/pages/api/core/app/del.ts | 5 +- .../pages/api/core/app/httpPlugin/update.ts | 9 ++- projects/app/src/pages/api/core/app/update.ts | 3 + .../app/src/pages/api/core/dataset/create.ts | 40 +++++++----- .../app/src/pages/api/core/dataset/delete.ts | 5 ++ .../app/src/pages/api/core/dataset/update.ts | 3 + .../support/user/account/loginByPassword.ts | 12 ++-- .../pages/api/support/user/account/update.ts | 31 ++++----- .../pages/app/detail/components/InfoModal.tsx | 41 +++++------- .../pages/app/list/components/CreateModal.tsx | 46 +++++--------- .../list/components/HttpPluginEditModal.tsx | 42 +++++-------- .../dataset/detail/components/Info/index.tsx | 30 --------- .../dataset/list/component/CreateModal.tsx | 45 +++++-------- .../src/pages/dataset/list/component/List.tsx | 4 +- .../login/components/ForgetPasswordForm.tsx | 34 ++++++---- .../login/components/LoginForm/LoginForm.tsx | 40 +++++------- .../pages/login/components/RegisterForm.tsx | 34 ++++++---- projects/app/src/pages/login/provider.tsx | 23 ++++--- .../app/src/web/common/file/controller.ts | 23 +------ .../web/common/file/hooks/useSelectFile.tsx | 43 ++++++++++++- .../src/web/common/system/useSystemStore.ts | 3 +- projects/app/src/web/core/dataset/api.ts | 2 +- .../src/web/support/user/login/constants.ts | 3 + 56 files changed, 501 insertions(+), 564 deletions(-) diff --git a/docSite/content/zh-cn/docs/development/upgrading/4818.md b/docSite/content/zh-cn/docs/development/upgrading/4818.md index 0ca29c252..ae7a5f015 100644 --- a/docSite/content/zh-cn/docs/development/upgrading/4818.md +++ b/docSite/content/zh-cn/docs/development/upgrading/4818.md @@ -10,4 +10,5 @@ weight: 806 ## 完整更新内容 1. -2. 新增 - 支持部门架构权限模式 \ No newline at end of file +2. 新增 - 支持部门架构权限模式 +3. 优化 - 图片上传安全校验。并增加头像图片唯一存储,确保不会累计存储。 \ No newline at end of file diff --git a/packages/global/common/error/code/common.ts b/packages/global/common/error/code/common.ts index 13f74643b..38d76dbec 100644 --- a/packages/global/common/error/code/common.ts +++ b/packages/global/common/error/code/common.ts @@ -1,14 +1,20 @@ +import { i18nT } from '../../../../web/i18n/utils'; import { ErrType } from '../errorCode'; /* dataset: 507000 */ const startCode = 507000; export enum CommonErrEnum { + invalidParams = 'invalidParams', fileNotFound = 'fileNotFound', unAuthFile = 'unAuthFile', missingParams = 'missingParams', inheritPermissionError = 'inheritPermissionError' } const datasetErr = [ + { + statusText: CommonErrEnum.fileNotFound, + message: i18nT('common:error.invalid_params') + }, { statusText: CommonErrEnum.fileNotFound, message: 'error.fileNotFound' diff --git a/packages/global/common/error/code/user.ts b/packages/global/common/error/code/user.ts index e58136041..ebd782ddf 100644 --- a/packages/global/common/error/code/user.ts +++ b/packages/global/common/error/code/user.ts @@ -16,11 +16,7 @@ const errList = [ { statusText: UserErrEnum.binVisitor, message: i18nT('common:code_error.user_error.bin_visitor') - }, // 身份校验未通过 - { - statusText: UserErrEnum.binVisitor, - message: i18nT('common:code_error.user_error.bin_visitor_guest') - }, // 游客身份 + }, { statusText: UserErrEnum.balanceNotEnough, message: i18nT('common:code_error.user_error.balance_not_enough') diff --git a/packages/global/common/file/api.d.ts b/packages/global/common/file/api.d.ts index e26033ae7..5da698b58 100644 --- a/packages/global/common/file/api.d.ts +++ b/packages/global/common/file/api.d.ts @@ -1,10 +1,7 @@ -import { MongoImageTypeEnum } from './image/constants'; import { OutLinkChatAuthProps } from '../../support/permission/chat.d'; export type preUploadImgProps = OutLinkChatAuthProps & { - type: `${MongoImageTypeEnum}`; - - expiredTime?: Date; + // expiredTime?: Date; metadata?: Record; }; export type UploadImgProps = preUploadImgProps & { diff --git a/packages/global/common/file/image/constants.ts b/packages/global/common/file/image/constants.ts index ad516f337..5e511e4b2 100644 --- a/packages/global/common/file/image/constants.ts +++ b/packages/global/common/file/image/constants.ts @@ -1,66 +1,5 @@ export const imageBaseUrl = '/api/system/img/'; -export enum MongoImageTypeEnum { - systemAvatar = 'systemAvatar', - appAvatar = 'appAvatar', - pluginAvatar = 'pluginAvatar', - datasetAvatar = 'datasetAvatar', - userAvatar = 'userAvatar', - teamAvatar = 'teamAvatar', - groupAvatar = 'groupAvatar', - orgAvatar = 'orgAvatar', - - chatImage = 'chatImage', - collectionImage = 'collectionImage' -} -export const mongoImageTypeMap = { - [MongoImageTypeEnum.systemAvatar]: { - label: 'appAvatar', - unique: true - }, - [MongoImageTypeEnum.appAvatar]: { - label: 'appAvatar', - unique: true - }, - [MongoImageTypeEnum.pluginAvatar]: { - label: 'pluginAvatar', - unique: true - }, - [MongoImageTypeEnum.datasetAvatar]: { - label: 'datasetAvatar', - unique: true - }, - [MongoImageTypeEnum.userAvatar]: { - label: 'userAvatar', - unique: true - }, - [MongoImageTypeEnum.teamAvatar]: { - label: 'teamAvatar', - unique: true - }, - [MongoImageTypeEnum.groupAvatar]: { - label: 'groupAvatar', - unique: true - }, - [MongoImageTypeEnum.orgAvatar]: { - label: 'orgAvatar', - unique: true - }, - - [MongoImageTypeEnum.chatImage]: { - label: 'chatImage', - unique: false - }, - [MongoImageTypeEnum.collectionImage]: { - label: 'collectionImage', - unique: false - } -}; - -export const uniqueImageTypeList = Object.entries(mongoImageTypeMap) - .filter(([key, value]) => value.unique) - .map(([key]) => key as `${MongoImageTypeEnum}`); - export const FolderIcon = 'file/fill/folder'; export const FolderImgUrl = '/imgs/files/folder.svg'; export const HttpPluginImgUrl = '/imgs/app/httpPluginFill.svg'; diff --git a/packages/global/common/file/image/type.d.ts b/packages/global/common/file/image/type.d.ts index 6fe274794..320df97fd 100644 --- a/packages/global/common/file/image/type.d.ts +++ b/packages/global/common/file/image/type.d.ts @@ -1,12 +1,8 @@ -import { MongoImageTypeEnum } from './constants'; - export type MongoImageSchemaType = { _id: string; teamId: string; binary: Buffer; - createTime: Date; expiredTime?: Date; - type: `${MongoImageTypeEnum}`; metadata?: { mime?: string; // image mime type. diff --git a/packages/service/common/file/image/controller.ts b/packages/service/common/file/image/controller.ts index 3a8e6f299..27bc7ade5 100644 --- a/packages/service/common/file/image/controller.ts +++ b/packages/service/common/file/image/controller.ts @@ -1,43 +1,92 @@ import { UploadImgProps } from '@fastgpt/global/common/file/api'; import { imageBaseUrl } from '@fastgpt/global/common/file/image/constants'; import { MongoImage } from './schema'; -import { ClientSession } from '../../../common/mongo'; +import { ClientSession, Types } from '../../../common/mongo'; import { guessBase64ImageType } from '../utils'; import { readFromSecondary } from '../../mongo/utils'; +import { addHours } from 'date-fns'; export const maxImgSize = 1024 * 1024 * 12; const base64MimeRegex = /data:image\/([^\)]+);base64/; export async function uploadMongoImg({ - type, base64Img, teamId, - expiredTime, metadata, - shareId + shareId, + forever = false }: UploadImgProps & { teamId: string; + forever?: Boolean; }) { if (base64Img.length > maxImgSize) { return Promise.reject('Image too large'); } const [base64Mime, base64Data] = base64Img.split(','); + // Check if mime type is valid + if (!base64MimeRegex.test(base64Mime)) { + return Promise.reject('Invalid image mime type'); + } + const mime = `image/${base64Mime.match(base64MimeRegex)?.[1] ?? 'image/jpeg'}`; const binary = Buffer.from(base64Data, 'base64'); const extension = mime.split('/')[1]; const { _id } = await MongoImage.create({ - type, teamId, binary, - expiredTime, metadata: Object.assign({ mime }, metadata), - shareId + shareId, + expiredTime: forever ? undefined : addHours(new Date(), 1) }); return `${process.env.FE_DOMAIN || ''}${process.env.NEXT_PUBLIC_BASE_URL || ''}${imageBaseUrl}${String(_id)}.${extension}`; } +const getIdFromPath = (path?: string) => { + if (!path) return; + + const paths = path.split('/'); + const name = paths[paths.length - 1]; + + if (!name) return; + + const id = name.split('.')[0]; + if (!id || !Types.ObjectId.isValid(id)) return; + + return id; +}; +// 删除旧的头像,新的头像去除过期时间 +export const refreshSourceAvatar = async ( + path?: string, + oldPath?: string, + session?: ClientSession +) => { + const newId = getIdFromPath(path); + const oldId = getIdFromPath(oldPath); + + if (!newId) return; + + await MongoImage.updateOne({ _id: newId }, { $unset: { expiredTime: 1 } }, { session }); + + if (oldId) { + await MongoImage.deleteOne({ _id: oldId }, { session }); + } +}; +export const removeImageByPath = (path?: string, session?: ClientSession) => { + if (!path) return; + + const paths = path.split('/'); + const name = paths[paths.length - 1]; + + if (!name) return; + + const id = name.split('.')[0]; + if (!id || !Types.ObjectId.isValid(id)) return; + + return MongoImage.deleteOne({ _id: id }, { session }); +}; + export async function readMongoImg({ id }: { id: string }) { const formatId = id.replace(/\.[^/.]+$/, ''); diff --git a/packages/service/common/file/image/schema.ts b/packages/service/common/file/image/schema.ts index f5c62d5cd..1418fa479 100644 --- a/packages/service/common/file/image/schema.ts +++ b/packages/service/common/file/image/schema.ts @@ -1,8 +1,7 @@ import { TeamCollectionName } from '@fastgpt/global/support/user/team/constant'; -import { connectionMongo, getMongoModel, type Model } from '../../mongo'; +import { connectionMongo, getMongoModel } from '../../mongo'; import { MongoImageSchemaType } from '@fastgpt/global/common/file/image/type.d'; -import { mongoImageTypeMap } from '@fastgpt/global/common/file/image/constants'; -const { Schema, model, models } = connectionMongo; +const { Schema } = connectionMongo; const ImageSchema = new Schema({ teamId: { @@ -14,27 +13,15 @@ const ImageSchema = new Schema({ type: Date, default: () => new Date() }, - expiredTime: { - type: Date - }, - binary: { - type: Buffer - }, - type: { - type: String, - enum: Object.keys(mongoImageTypeMap), - required: true - }, - metadata: { - type: Object - } + expiredTime: Date, + binary: Buffer, + metadata: Object }); try { // tts expired(60 Minutes) ImageSchema.index({ expiredTime: 1 }, { expireAfterSeconds: 60 * 60 }); ImageSchema.index({ type: 1 }); - ImageSchema.index({ createTime: 1 }); // delete related img ImageSchema.index({ teamId: 1, 'metadata.relatedId': 1 }); } catch (error) { diff --git a/packages/service/common/file/read/utils.ts b/packages/service/common/file/read/utils.ts index 0c3cb1ba9..ac3afe1a6 100644 --- a/packages/service/common/file/read/utils.ts +++ b/packages/service/common/file/read/utils.ts @@ -1,5 +1,4 @@ import { uploadMongoImg } from '../image/controller'; -import { MongoImageTypeEnum } from '@fastgpt/global/common/file/image/constants'; import FormData from 'form-data'; import { WorkerNameEnum, runWorker } from '../../../worker/utils'; @@ -114,10 +113,9 @@ export const readRawContentByFileBuffer = async ({ if (imageList) { await batchRun(imageList, async (item) => { const src = await uploadMongoImg({ - type: MongoImageTypeEnum.collectionImage, base64Img: `data:${item.mime};base64,${item.base64}`, teamId, - expiredTime: addHours(new Date(), 1), + // expiredTime: addHours(new Date(), 1), metadata: { ...metadata, mime: item.mime diff --git a/packages/service/common/middle/reqFrequencyLimit.ts b/packages/service/common/middle/reqFrequencyLimit.ts index 8f950c75d..f894d7416 100644 --- a/packages/service/common/middle/reqFrequencyLimit.ts +++ b/packages/service/common/middle/reqFrequencyLimit.ts @@ -9,10 +9,10 @@ import { jsonRes } from '../response'; // unit: times/s // how to use? // export default NextAPI(useQPSLimit(10), handler); // limit 10 times per second for a ip -export function useReqFrequencyLimit(seconds: number, limit: number) { +export function useReqFrequencyLimit(seconds: number, limit: number, force = false) { return async (req: ApiRequestProps, res: NextApiResponse) => { const ip = requestIp.getClientIp(req); - if (!ip || process.env.USE_IP_LIMIT !== 'true') { + if (!ip || (process.env.USE_IP_LIMIT !== 'true' && !force)) { return; } try { @@ -25,7 +25,7 @@ export function useReqFrequencyLimit(seconds: number, limit: number) { res.status(429); jsonRes(res, { code: 429, - message: ERROR_ENUM.tooManyRequest + error: ERROR_ENUM.tooManyRequest }); } }; diff --git a/packages/service/common/response/index.ts b/packages/service/common/response/index.ts index a6586d077..ee2d93d67 100644 --- a/packages/service/common/response/index.ts +++ b/packages/service/common/response/index.ts @@ -33,6 +33,7 @@ export const jsonRes = ( addLog.error(`Api response error: ${url}`, ERROR_RESPONSE[errResponseKey]); + res.status(ERROR_RESPONSE[errResponseKey].code); return res.json(ERROR_RESPONSE[errResponseKey]); } diff --git a/packages/service/support/permission/memberGroup/controllers.ts b/packages/service/support/permission/memberGroup/controllers.ts index 1b672e4eb..fe7b59df4 100644 --- a/packages/service/support/permission/memberGroup/controllers.ts +++ b/packages/service/support/permission/memberGroup/controllers.ts @@ -97,8 +97,12 @@ export const authGroupMemberRole = async ({ tmbId }; } - const groupMember = await MongoGroupMemberModel.findOne({ groupId, tmbId }); - const tmb = await getTmbInfoByTmbId({ tmbId }); + const [groupMember, tmb] = await Promise.all([ + MongoGroupMemberModel.findOne({ groupId, tmbId }), + getTmbInfoByTmbId({ tmbId }) + ]); + + // Team admin or role check if (tmb.permission.hasManagePer || (groupMember && role.includes(groupMember.role))) { return { ...result, diff --git a/packages/service/support/user/team/controller.ts b/packages/service/support/user/team/controller.ts index 2442b0a7b..d4199be06 100644 --- a/packages/service/support/user/team/controller.ts +++ b/packages/service/support/user/team/controller.ts @@ -17,6 +17,7 @@ import { mongoSessionRun } from '../../../common/mongo/sessionRun'; import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant'; import { getAIApi, openaiBaseUrl } from '../../../core/ai/config'; import { createRootOrg } from '../../permission/org/controllers'; +import { refreshSourceAvatar } from '../../../common/file/image/controller'; async function getTeamMember(match: Record): Promise { const tmb = await MongoTeamMember.findOne(match).populate<{ team: TeamSchema }>('team').lean(); @@ -218,7 +219,8 @@ export async function updateTeam({ return obj; })(); - await MongoTeam.findByIdAndUpdate( + // This is where we get the old team + const team = await MongoTeam.findByIdAndUpdate( teamId, { $set: { @@ -244,6 +246,8 @@ export async function updateTeam({ }, { session } ); + + await refreshSourceAvatar(avatar, team?.avatar, session); } }); } diff --git a/packages/web/i18n/en/account_info.json b/packages/web/i18n/en/account_info.json index dbda14415..36cbc15e2 100644 --- a/packages/web/i18n/en/account_info.json +++ b/packages/web/i18n/en/account_info.json @@ -49,8 +49,8 @@ "package_expiry_time": "Expired", "package_usage_rules": "Package usage rules: The system will give priority to using more advanced packages, and the original unused packages will take effect later.", "password": "Password", - "password_length_error": "Password must be at least 4 characters and at most 60 characters", "password_mismatch": "Password Inconsistency: Two passwords are inconsistent", + "password_tip": "Password must be at least 6 characters long and contain at least two combinations: numbers, letters, or special characters", "password_update_error": "Exception when changing password", "password_update_success": "Password changed successfully", "pending_usage": "To be used", diff --git a/packages/web/i18n/en/common.json b/packages/web/i18n/en/common.json index 3a97d8f6d..025bde262 100644 --- a/packages/web/i18n/en/common.json +++ b/packages/web/i18n/en/common.json @@ -74,24 +74,24 @@ "code_error.team_error.ai_points_not_enough": "Insufficient AI Points", "code_error.team_error.app_amount_not_enough": "Application Limit Reached", "code_error.team_error.cannot_delete_default_group": "Cannot delete default group", + "code_error.team_error.cannot_delete_non_empty_org": "Cannot delete non-empty organization", + "code_error.team_error.cannot_modify_root_org": "Cannot modify root organization", + "code_error.team_error.cannot_move_to_sub_path": "Cannot move to same or subdirectory", "code_error.team_error.dataset_amount_not_enough": "Dataset Limit Reached", "code_error.team_error.dataset_size_not_enough": "Insufficient Dataset Capacity, Please Expand", "code_error.team_error.group_name_duplicate": "Duplicate group name", "code_error.team_error.group_name_empty": "Group name cannot be empty", "code_error.team_error.group_not_exist": "Group does not exist", + "code_error.team_error.org_member_duplicated": "Duplicate organization member", + "code_error.team_error.org_member_not_exist": "Organization member does not exist", + "code_error.team_error.org_not_exist": "Organization does not exist", + "code_error.team_error.org_parent_not_exist": "Parent organization does not exist", "code_error.team_error.over_size": "error.team.overSize", "code_error.team_error.plugin_amount_not_enough": "Plugin Limit Reached", "code_error.team_error.re_rank_not_enough": "Unauthorized to Use Re-Rank", "code_error.team_error.un_auth": "Unauthorized to Operate This Team", "code_error.team_error.user_not_active": "The user did not accept or has left the team", "code_error.team_error.website_sync_not_enough": "Unauthorized to Use Website Sync", - "code_error.team_error.org_member_not_exist": "Organization member does not exist", - "code_error.team_error.org_member_duplicated": "Duplicate organization member", - "code_error.team_error.org_not_exist": "Organization does not exist", - "code_error.team_error.org_parent_not_exist": "Parent organization does not exist", - "code_error.team_error.cannot_move_to_sub_path": "Cannot move to same or subdirectory", - "code_error.team_error.cannot_modify_root_org": "Cannot modify root organization", - "code_error.team_error.cannot_delete_non_empty_org": "Cannot delete non-empty organization", "code_error.token_error_code.403": "Invalid Login Status, Please Re-login", "code_error.user_error.balance_not_enough": "Insufficient Account Balance", "code_error.user_error.bin_visitor": "Identity Verification Failed", @@ -880,9 +880,11 @@ "error.code_error": "Verification code error", "error.fileNotFound": "File not found~", "error.inheritPermissionError": "Inherit permission Error", + "error.invalid_params": "Invalid parameter", "error.missingParams": "Insufficient parameters", "error.too_many_request": "Too many request", "error.upload_file_error_filename": "{{name}} Upload Failed", + "error.upload_image_error": "File upload failed", "error.username_empty": "Account cannot be empty", "extraction_results": "Extraction Results", "field_name": "Field Name", diff --git a/packages/web/i18n/en/login.json b/packages/web/i18n/en/login.json index a1256d494..c02cbbb0c 100644 --- a/packages/web/i18n/en/login.json +++ b/packages/web/i18n/en/login.json @@ -1,19 +1,20 @@ { "Chinese_ip_tip": "It is detected that you are a mainland Chinese IP, click to jump to visit the mainland China version.", "Login": "Login", + "agree": "agree", + "cookies_tip": " This website uses cookies to provide a better service experience. By continuing to use the site, you agree to our Cookie Policy.", "forget_password": "Find password", "login_failed": "Login failed", "login_success": "Login successful", "no_remind": "Don't remind again", "password_condition": "Password maximum 60 characters", + "password_tip": "Password must be at least 6 characters long and contain at least two combinations: numbers, letters, or special characters", "policy_tip": "By useing, you agree to our", "privacy": "Privacy policy", + "privacy_policy": "Privacy Policy", "redirect": "Jump", "register": "Register", "root_password_placeholder": "The root user password is the value of the environment variable DEFAULT_ROOT_PSW", "terms": "Terms", - "use_root_login": "Log in as root user", - "agree": "agree", - "cookies_tip": " This website uses cookies to provide a better service experience. By continuing to use the site, you agree to our Cookie Policy.", - "privacy_policy": "Privacy Policy" -} \ No newline at end of file + "use_root_login": "Log in as root user" +} diff --git a/packages/web/i18n/en/user.json b/packages/web/i18n/en/user.json index 5f2ba68ee..814016f08 100644 --- a/packages/web/i18n/en/user.json +++ b/packages/web/i18n/en/user.json @@ -40,7 +40,6 @@ "password.email_phone_void": "Email/Phone Number Cannot Be Empty", "password.get_code": "Get Verification Code", "password.get_code_again": "Get Again in s", - "password.new_password": "New Password (4-20 characters)", "password.not_match": "Passwords Do Not Match", "password.password_condition": "Password must be between 4 and 20 characters", "password.password_required": "Password Cannot Be Empty", diff --git a/packages/web/i18n/zh-CN/account_info.json b/packages/web/i18n/zh-CN/account_info.json index 6059523dd..936e0cdbc 100644 --- a/packages/web/i18n/zh-CN/account_info.json +++ b/packages/web/i18n/zh-CN/account_info.json @@ -47,8 +47,8 @@ "package_expiry_time": "套餐到期时间", "package_usage_rules": "套餐使用规则:系统优先使用更高级的套餐,原未用完的套餐将延后生效", "password": "密码", - "password_length_error": "密码最少 4 位最多 60 位", "password_mismatch": "密码不一致: 两次密码不一致", + "password_tip": "密码至少 6 位,且至少包含两种组合:数字、字母或特殊字符", "password_update_error": "修改密码异常", "password_update_success": "修改密码成功", "pending_usage": "待使用", diff --git a/packages/web/i18n/zh-CN/common.json b/packages/web/i18n/zh-CN/common.json index 23aaaccbe..32b0104fd 100644 --- a/packages/web/i18n/zh-CN/common.json +++ b/packages/web/i18n/zh-CN/common.json @@ -78,24 +78,24 @@ "code_error.team_error.ai_points_not_enough": "", "code_error.team_error.app_amount_not_enough": "应用数量已达上限~", "code_error.team_error.cannot_delete_default_group": "不能删除默认群组", + "code_error.team_error.cannot_delete_non_empty_org": "不能删除非空部门", + "code_error.team_error.cannot_modify_root_org": "不能修改根部门", + "code_error.team_error.cannot_move_to_sub_path": "不能移动到相同或子目录", "code_error.team_error.dataset_amount_not_enough": "知识库数量已达上限~", "code_error.team_error.dataset_size_not_enough": "知识库容量不足,请先扩容~", "code_error.team_error.group_name_duplicate": "群组名称重复", "code_error.team_error.group_name_empty": "群组名称不能为空", "code_error.team_error.group_not_exist": "群组不存在", + "code_error.team_error.org_member_duplicated": "重复的部门成员", + "code_error.team_error.org_member_not_exist": "部门成员不存在", + "code_error.team_error.org_not_exist": "部门不存在", + "code_error.team_error.org_parent_not_exist": "父部门不存在", "code_error.team_error.over_size": "error.team.overSize", "code_error.team_error.plugin_amount_not_enough": "插件数量已达上限~", "code_error.team_error.re_rank_not_enough": "无权使用检索重排~", "code_error.team_error.un_auth": "无权操作该团队", "code_error.team_error.user_not_active": "用户未接受或已离开团队", "code_error.team_error.website_sync_not_enough": "无权使用Web站点同步~", - "code_error.team_error.org_member_not_exist": "部门成员不存在", - "code_error.team_error.org_member_duplicated": "重复的部门成员", - "code_error.team_error.org_not_exist": "部门不存在", - "code_error.team_error.org_parent_not_exist": "父部门不存在", - "code_error.team_error.cannot_move_to_sub_path": "不能移动到相同或子目录", - "code_error.team_error.cannot_modify_root_org": "不能修改根部门", - "code_error.team_error.cannot_delete_non_empty_org": "不能删除非空部门", "code_error.token_error_code.403": "登录状态无效,请重新登录", "code_error.user_error.balance_not_enough": "账号余额不足~", "code_error.user_error.bin_visitor": "您的身份校验未通过", @@ -883,9 +883,11 @@ "error.code_error": "验证码错误", "error.fileNotFound": "文件找不到了~", "error.inheritPermissionError": "权限继承错误", + "error.invalid_params": "参数无效", "error.missingParams": "参数缺失", "error.too_many_request": "请求太频繁了,请稍后重试", "error.upload_file_error_filename": "{{name}} 上传失败", + "error.upload_image_error": "上传文件失败", "error.username_empty": "账号不能为空", "extraction_results": "提取结果", "field_name": "字段名", diff --git a/packages/web/i18n/zh-CN/login.json b/packages/web/i18n/zh-CN/login.json index 92eb9606b..61523b51f 100644 --- a/packages/web/i18n/zh-CN/login.json +++ b/packages/web/i18n/zh-CN/login.json @@ -1,19 +1,20 @@ { + "Chinese_ip_tip": "检测到您是中国大陆 IP,点击跳转访问中国大陆版。", "Login": "登录", + "agree": "同意", + "cookies_tip": "本网站使用 cookies 提供更好的服务体验。继续使用即表示您同意我们的 Cookie 政策。", "forget_password": "忘记密码?", "login_failed": "登录异常", "login_success": "登录成功", + "no_remind": "不再提醒", "password_condition": "密码最多 60 位", + "password_tip": "密码至少 6 位,且至少包含两种组合:数字、字母或特殊字符", "policy_tip": "使用即代表你同意我们的", "privacy": "隐私协议", + "privacy_policy": "隐私政策", + "redirect": "跳转", "register": "注册账号", "root_password_placeholder": "root 用户密码为环境变量 DEFAULT_ROOT_PSW 的值", "terms": "服务协议", - "use_root_login": "使用 root 用户登录", - "redirect": "跳转", - "no_remind": "不再提醒", - "Chinese_ip_tip": "检测到您是中国大陆 IP,点击跳转访问中国大陆版。", - "agree": "同意", - "cookies_tip": "本网站使用 cookies 提供更好的服务体验。继续使用即表示您同意我们的 Cookie 政策。", - "privacy_policy": "隐私政策" -} \ No newline at end of file + "use_root_login": "使用 root 用户登录" +} diff --git a/packages/web/i18n/zh-CN/user.json b/packages/web/i18n/zh-CN/user.json index 1fe72fcee..23b8b7550 100644 --- a/packages/web/i18n/zh-CN/user.json +++ b/packages/web/i18n/zh-CN/user.json @@ -40,7 +40,6 @@ "password.email_phone_void": "邮箱/手机号不能为空", "password.get_code": "获取验证码", "password.get_code_again": "s后重新获取", - "password.new_password": "新密码(4~20位)", "password.not_match": "两次密码不一致", "password.password_condition": "密码最少 4 位最多 20 位", "password.password_required": "密码不能为空", diff --git a/packages/web/i18n/zh-Hant/account_info.json b/packages/web/i18n/zh-Hant/account_info.json index a2792aaad..51831cb94 100644 --- a/packages/web/i18n/zh-Hant/account_info.json +++ b/packages/web/i18n/zh-Hant/account_info.json @@ -49,8 +49,8 @@ "package_expiry_time": "套餐到期時間", "package_usage_rules": "套餐使用規則:系統優先使用更進階的套餐,原未用完的套餐將延遲生效", "password": "密碼", - "password_length_error": "密碼最少 4 位最多 60 位", "password_mismatch": "密碼不一致: 兩次密碼不一致", + "password_tip": "密碼至少 6 位,且至少包含兩種組合:數字、字母或特殊字符", "password_update_error": "修改密碼異常", "password_update_success": "修改密碼成功", "pending_usage": "待使用", diff --git a/packages/web/i18n/zh-Hant/common.json b/packages/web/i18n/zh-Hant/common.json index f5ccd6d67..54cdf3dec 100644 --- a/packages/web/i18n/zh-Hant/common.json +++ b/packages/web/i18n/zh-Hant/common.json @@ -74,24 +74,24 @@ "code_error.team_error.ai_points_not_enough": "AI 點數不足", "code_error.team_error.app_amount_not_enough": "已達應用程式數量上限", "code_error.team_error.cannot_delete_default_group": "無法刪除預設群組", + "code_error.team_error.cannot_delete_non_empty_org": "無法刪除非空組織", + "code_error.team_error.cannot_modify_root_org": "無法修改根組織", + "code_error.team_error.cannot_move_to_sub_path": "無法移動到相同或子目錄", "code_error.team_error.dataset_amount_not_enough": "已達知識庫數量上限", "code_error.team_error.dataset_size_not_enough": "知識庫容量不足,請先擴充容量", "code_error.team_error.group_name_duplicate": "群組名稱重複", "code_error.team_error.group_name_empty": "群組名稱不能為空", "code_error.team_error.group_not_exist": "群組不存在", + "code_error.team_error.org_member_duplicated": "重複的組織成員", + "code_error.team_error.org_member_not_exist": "組織成員不存在", + "code_error.team_error.org_not_exist": "組織不存在", + "code_error.team_error.org_parent_not_exist": "父組織不存在", "code_error.team_error.over_size": "error.team.overSize", "code_error.team_error.plugin_amount_not_enough": "已達外掛程式數量上限", "code_error.team_error.re_rank_not_enough": "無權使用結果重新排名", "code_error.team_error.un_auth": "無權操作此團隊", "code_error.team_error.user_not_active": "使用者未接受或已離開團隊", "code_error.team_error.website_sync_not_enough": "無權使用網站同步", - "code_error.team_error.org_member_not_exist": "組織成員不存在", - "code_error.team_error.org_member_duplicated": "重複的組織成員", - "code_error.team_error.org_not_exist": "組織不存在", - "code_error.team_error.org_parent_not_exist": "父組織不存在", - "code_error.team_error.cannot_move_to_sub_path": "無法移動到相同或子目錄", - "code_error.team_error.cannot_modify_root_org": "無法修改根組織", - "code_error.team_error.cannot_delete_non_empty_org": "無法刪除非空組織", "code_error.token_error_code.403": "登入狀態無效,請重新登入", "code_error.user_error.balance_not_enough": "帳戶餘額不足", "code_error.user_error.bin_visitor": "身份驗證未通過", @@ -881,9 +881,11 @@ "error.code_error": "驗證碼錯誤", "error.fileNotFound": "找不到檔案", "error.inheritPermissionError": "繼承權限錯誤", + "error.invalid_params": "參數無效", "error.missingParams": "參數不足", "error.too_many_request": "請求太頻繁了,請稍後重試", "error.upload_file_error_filename": "{{name}} 上傳失敗", + "error.upload_image_error": "上傳文件失敗", "error.username_empty": "帳號不能為空", "extraction_results": "提取結果", "field_name": "欄位名稱", diff --git a/packages/web/i18n/zh-Hant/login.json b/packages/web/i18n/zh-Hant/login.json index db2689743..51bafc851 100644 --- a/packages/web/i18n/zh-Hant/login.json +++ b/packages/web/i18n/zh-Hant/login.json @@ -1,19 +1,20 @@ { "Chinese_ip_tip": "偵測到您使用中國大陸 IP,點選這裡前往中國大陸版本。", "Login": "登入", + "agree": "同意", + "cookies_tip": "本網站使用 cookies 提供更好的服務體驗。繼續使用即表示您同意我們的 Cookie 政策。", "forget_password": "忘記密碼?", "login_failed": "登入失敗", "login_success": "登入成功", "no_remind": "不再提醒", "password_condition": "密碼最多 60 個字元", + "password_tip": "密碼至少 6 位,且至少包含兩種組合:數字、字母或特殊字符", "policy_tip": "使用即代表您同意我們的", "privacy": "隱私權政策", + "privacy_policy": "隱私權政策", "redirect": "跳轉", "register": "註冊帳號", "root_password_placeholder": "root 使用者密碼為環境變數 DEFAULT_ROOT_PSW 的值", "terms": "服務條款", - "use_root_login": "使用 root 使用者登入", - "agree": "同意", - "cookies_tip": "本網站使用 cookies 提供更好的服務體驗。繼續使用即表示您同意我們的 Cookie 政策。", - "privacy_policy": "隱私權政策" -} \ No newline at end of file + "use_root_login": "使用 root 使用者登入" +} diff --git a/packages/web/i18n/zh-Hant/user.json b/packages/web/i18n/zh-Hant/user.json index f743d765a..695dc164d 100644 --- a/packages/web/i18n/zh-Hant/user.json +++ b/packages/web/i18n/zh-Hant/user.json @@ -40,7 +40,6 @@ "password.email_phone_void": "電子郵件/手機號碼不能空白", "password.get_code": "取得驗證碼", "password.get_code_again": "秒後重新取得", - "password.new_password": "新密碼(4 至 20 字元)", "password.not_match": "兩次輸入的密碼不相符", "password.password_condition": "密碼長度需介於 4 至 20 字元之間", "password.password_required": "密碼不能空白", diff --git a/projects/app/src/components/common/Modal/EditResourceModal.tsx b/projects/app/src/components/common/Modal/EditResourceModal.tsx index 95aaf218c..5fd5e0f4f 100644 --- a/projects/app/src/components/common/Modal/EditResourceModal.tsx +++ b/projects/app/src/components/common/Modal/EditResourceModal.tsx @@ -1,17 +1,13 @@ -import React, { useCallback } from 'react'; +import React from 'react'; import { ModalFooter, ModalBody, Input, Button, Box, Textarea, HStack } from '@chakra-ui/react'; import MyModal from '@fastgpt/web/components/common/MyModal/index'; import { useTranslation } from 'next-i18next'; import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel'; import { useForm } from 'react-hook-form'; -import { compressImgFileAndUpload } from '@/web/common/file/controller'; -import { MongoImageTypeEnum } from '@fastgpt/global/common/file/image/constants'; import { useSelectFile } from '@/web/common/file/hooks/useSelectFile'; -import { getErrText } from '@fastgpt/global/common/error/utils'; import MyTooltip from '@fastgpt/web/components/common/MyTooltip'; import Avatar from '@fastgpt/web/components/common/Avatar'; -import { useToast } from '@fastgpt/web/hooks/useToast'; export type EditResourceInfoFormType = { id: string; @@ -31,7 +27,6 @@ const EditResourceModal = ({ onEdit: (data: EditResourceInfoFormType) => any; }) => { const { t } = useTranslation(); - const { toast } = useToast(); const { register, watch, setValue, handleSubmit } = useForm({ defaultValues: defaultForm }); @@ -46,31 +41,14 @@ const EditResourceModal = ({ } ); - const { File, onOpen: onOpenSelectFile } = useSelectFile({ + const { + File, + onOpen: onOpenSelectFile, + onSelectImage + } = useSelectFile({ fileType: '.jpg,.png', multiple: false }); - const onSelectFile = useCallback( - async (e: File[]) => { - const file = e[0]; - if (!file) return; - try { - const src = await compressImgFileAndUpload({ - type: MongoImageTypeEnum.appAvatar, - file, - maxW: 300, - maxH: 300 - }); - setValue('avatar', src); - } catch (err: any) { - toast({ - title: getErrText(err, t('common:common.error.Select avatar failed')), - status: 'warning' - }); - } - }, - [setValue, t, toast] - ); return ( @@ -108,7 +86,15 @@ const EditResourceModal = ({ - + + onSelectImage(e, { + maxH: 300, + maxW: 300, + callback: (e) => setValue('avatar', e) + }) + } + /> ); }; diff --git a/projects/app/src/pages/account/info/components/UpdatePswModal.tsx b/projects/app/src/pages/account/info/components/UpdatePswModal.tsx index e8518e926..092625db5 100644 --- a/projects/app/src/pages/account/info/components/UpdatePswModal.tsx +++ b/projects/app/src/pages/account/info/components/UpdatePswModal.tsx @@ -3,8 +3,10 @@ import { ModalBody, Box, Flex, Input, ModalFooter, Button } from '@chakra-ui/rea import MyModal from '@fastgpt/web/components/common/MyModal'; import { useTranslation } from 'next-i18next'; import { useForm } from 'react-hook-form'; -import { useRequest } from '@fastgpt/web/hooks/useRequest'; +import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; import { updatePasswordByOld } from '@/web/support/user/api'; +import { PasswordRule } from '@/web/support/user/login/constants'; +import { useToast } from '@fastgpt/web/hooks/useToast'; type FormType = { oldPsw: string; @@ -14,7 +16,9 @@ type FormType = { const UpdatePswModal = ({ onClose }: { onClose: () => void }) => { const { t } = useTranslation(); - const { register, handleSubmit } = useForm({ + const { toast } = useToast(); + + const { register, handleSubmit, getValues } = useForm({ defaultValues: { oldPsw: '', newPsw: '', @@ -22,19 +26,25 @@ const UpdatePswModal = ({ onClose }: { onClose: () => void }) => { } }); - const { mutate: onSubmit, isLoading } = useRequest({ - mutationFn: (data: FormType) => { - if (data.newPsw !== data.confirmPsw) { - return Promise.reject(t('account_info:password_mismatch')); - } - return updatePasswordByOld(data); - }, + const { runAsync: onSubmit, loading: isLoading } = useRequest2(updatePasswordByOld, { onSuccess() { onClose(); }, successToast: t('account_info:password_update_success'), errorToast: t('account_info:password_update_error') }); + const onSubmitErr = (err: Record) => { + const val = Object.values(err)[0]; + if (!val) return; + if (val.message) { + toast({ + status: 'warning', + title: val.message, + duration: 3000, + isClosable: true + }); + } + }; return ( void }) => { > - {t('account_info:old_password') + ':'} + + {t('account_info:old_password') + ':'} + - {t('account_info:new_password') + ':'} + + {t('account_info:new_password') + ':'} + - {t('account_info:confirm_password') + ':'} + + {t('account_info:confirm_password') + ':'} + (getValues('newPsw') === val ? true : t('user:password.not_match')) })} > @@ -81,7 +96,7 @@ const UpdatePswModal = ({ onClose }: { onClose: () => void }) => { - diff --git a/projects/app/src/pages/account/info/index.tsx b/projects/app/src/pages/account/info/index.tsx index d16b3ea13..1d9501339 100644 --- a/projects/app/src/pages/account/info/index.tsx +++ b/projects/app/src/pages/account/info/index.tsx @@ -20,7 +20,6 @@ import type { UserType } from '@fastgpt/global/support/user/type.d'; import { useQuery } from '@tanstack/react-query'; import dynamic from 'next/dynamic'; import { useSelectFile } from '@/web/common/file/hooks/useSelectFile'; -import { compressImgFileAndUpload } from '@/web/common/file/controller'; import { useSystemStore } from '@/web/common/system/useSystemStore'; import { useTranslation } from 'next-i18next'; import Avatar from '@fastgpt/web/components/common/Avatar'; @@ -29,7 +28,6 @@ import MyTooltip from '@fastgpt/web/components/common/MyTooltip'; import { formatStorePrice2Read } from '@fastgpt/global/support/wallet/usage/tools'; import { putUpdateMemberName } from '@/web/support/user/team/api'; import { getDocPath } from '@/web/common/system/doc'; -import { MongoImageTypeEnum } from '@fastgpt/global/common/file/image/constants'; import { StandardSubLevelEnum, standardSubLevelMap @@ -131,7 +129,11 @@ const MyInfo = ({ onOpenContact }: { onOpenContact: () => void }) => { onClose: onCloseUpdateNotification, onOpen: onOpenUpdateNotification } = useDisclosure(); - const { File, onOpen: onOpenSelectFile } = useSelectFile({ + const { + File, + onOpen: onOpenSelectFile, + onSelectImage + } = useSelectFile({ fileType: '.jpg,.png', multiple: false }); @@ -151,32 +153,6 @@ const MyInfo = ({ onOpenContact }: { onOpenContact: () => void }) => { [reset, t, toast, updateUserInfo] ); - const onSelectFile = useCallback( - async (e: File[]) => { - const file = e[0]; - if (!file || !userInfo) return; - try { - const src = await compressImgFileAndUpload({ - type: MongoImageTypeEnum.userAvatar, - file, - maxW: 300, - maxH: 300 - }); - - onclickSave({ - ...userInfo, - avatar: src - }); - } catch (err: any) { - toast({ - title: typeof err === 'string' ? err : t('account_info:avatar_selection_exception'), - status: 'warning' - }); - } - }, - [onclickSave, t, toast, userInfo] - ); - const labelStyles: BoxProps = { flex: '0 0 80px', fontSize: 'sm', @@ -329,7 +305,21 @@ const MyInfo = ({ onOpenContact }: { onOpenContact: () => void }) => { )} {isOpenUpdatePsw && } {isOpenUpdateNotification && } - + + onSelectImage(e, { + maxW: 300, + maxH: 300, + callback: (src) => { + if (!userInfo) return; + onclickSave({ + ...userInfo, + avatar: src + }); + } + }) + } + /> ); }; diff --git a/projects/app/src/pages/account/team/components/EditInfoModal.tsx b/projects/app/src/pages/account/team/components/EditInfoModal.tsx index 120d8a24b..d171089ea 100644 --- a/projects/app/src/pages/account/team/components/EditInfoModal.tsx +++ b/projects/app/src/pages/account/team/components/EditInfoModal.tsx @@ -1,10 +1,8 @@ -import React, { useCallback } from 'react'; +import React from 'react'; import { useForm } from 'react-hook-form'; import { useTranslation } from 'next-i18next'; import { useSelectFile } from '@/web/common/file/hooks/useSelectFile'; -import { compressImgFileAndUpload } from '@/web/common/file/controller'; import { useToast } from '@fastgpt/web/hooks/useToast'; -import { getErrText } from '@fastgpt/global/common/error/utils'; import { useRequest } from '@fastgpt/web/hooks/useRequest'; import MyModal from '@fastgpt/web/components/common/MyModal'; import { Box, Button, Flex, Input, ModalBody, ModalFooter } from '@chakra-ui/react'; @@ -12,7 +10,6 @@ import MyTooltip from '@fastgpt/web/components/common/MyTooltip'; import Avatar from '@fastgpt/web/components/common/Avatar'; import { postCreateTeam, putUpdateTeam } from '@/web/support/user/team/api'; import { CreateTeamProps } from '@fastgpt/global/support/user/team/controller.d'; -import { MongoImageTypeEnum } from '@fastgpt/global/common/file/image/constants'; import { DEFAULT_TEAM_AVATAR } from '@fastgpt/global/common/system/constants'; export type EditTeamFormDataType = CreateTeamProps & { @@ -41,33 +38,15 @@ function EditModal({ }); const avatar = watch('avatar'); - const { File, onOpen: onOpenSelectFile } = useSelectFile({ + const { + File, + onOpen: onOpenSelectFile, + onSelectImage + } = useSelectFile({ fileType: '.jpg,.png,.svg', multiple: false }); - const onSelectFile = useCallback( - async (e: File[]) => { - const file = e[0]; - if (!file) return; - try { - const src = await compressImgFileAndUpload({ - type: MongoImageTypeEnum.teamAvatar, - file, - maxW: 300, - maxH: 300 - }); - setValue('avatar', src); - } catch (err: any) { - toast({ - title: getErrText(err, t('common:common.Select File Failed')), - status: 'warning' - }); - } - }, - [setValue, t, toast] - ); - const { mutate: onclickCreate, isLoading: creating } = useRequest({ mutationFn: async (data: CreateTeamProps) => { return postCreateTeam(data); @@ -154,7 +133,15 @@ function EditModal({ )} - + + onSelectImage(e, { + maxH: 300, + maxW: 300, + callback: (e) => setValue('avatar', e) + }) + } + /> ); } diff --git a/projects/app/src/pages/account/team/components/GroupManage/GroupInfoModal.tsx b/projects/app/src/pages/account/team/components/GroupManage/GroupInfoModal.tsx index 8959d1420..c2fb98e06 100644 --- a/projects/app/src/pages/account/team/components/GroupManage/GroupInfoModal.tsx +++ b/projects/app/src/pages/account/team/components/GroupManage/GroupInfoModal.tsx @@ -6,9 +6,7 @@ import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel'; import { useTranslation } from 'next-i18next'; import React, { useMemo } from 'react'; import { useSelectFile } from '@/web/common/file/hooks/useSelectFile'; -import { compressImgFileAndUpload } from '@/web/common/file/controller'; import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; -import { MongoImageTypeEnum } from '@fastgpt/global/common/file/image/constants'; import { useForm } from 'react-hook-form'; import { useContextSelector } from 'use-context-selector'; import { TeamContext } from '../context'; @@ -23,7 +21,11 @@ export type GroupFormType = { function GroupInfoModal({ onClose, editGroupId }: { onClose: () => void; editGroupId?: string }) { const { refetchGroups, groups, refetchMembers } = useContextSelector(TeamContext, (v) => v); const { t } = useTranslation(); - const { File: AvatarSelect, onOpen: onOpenSelectAvatar } = useSelectFile({ + const { + File: AvatarSelect, + onOpen: onOpenSelectAvatar, + onSelectImage + } = useSelectFile({ fileType: '.jpg, .jpeg, .png', multiple: false }); @@ -41,13 +43,10 @@ function GroupInfoModal({ onClose, editGroupId }: { onClose: () => void; editGro const { loading: uploadingAvatar, run: onSelectAvatar } = useRequest2( async (file: File[]) => { - const src = await compressImgFileAndUpload({ - type: MongoImageTypeEnum.groupAvatar, - file: file[0], + return onSelectImage(file, { maxW: 300, maxH: 300 }); - return src; }, { onSuccess: (src: string) => { diff --git a/projects/app/src/pages/account/team/components/OrgManage/OrgInfoModal.tsx b/projects/app/src/pages/account/team/components/OrgManage/OrgInfoModal.tsx index 2a6673459..bd7b0c7e5 100644 --- a/projects/app/src/pages/account/team/components/OrgManage/OrgInfoModal.tsx +++ b/projects/app/src/pages/account/team/components/OrgManage/OrgInfoModal.tsx @@ -1,8 +1,6 @@ -import { compressImgFileAndUpload } from '@/web/common/file/controller'; import { useSelectFile } from '@/web/common/file/hooks/useSelectFile'; import { postCreateOrg, putUpdateOrg } from '@/web/support/user/team/org/api'; import { Button, HStack, Input, ModalBody, ModalFooter, Textarea } from '@chakra-ui/react'; -import { MongoImageTypeEnum } from '@fastgpt/global/common/file/image/constants'; import { DEFAULT_ORG_AVATAR } from '@fastgpt/global/common/system/constants'; import Avatar from '@fastgpt/web/components/common/Avatar'; import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel'; @@ -89,19 +87,20 @@ function OrgInfoModal({ } ); - const { File: AvatarSelect, onOpen: onOpenSelectAvatar } = useSelectFile({ + const { + File: AvatarSelect, + onOpen: onOpenSelectAvatar, + onSelectImage + } = useSelectFile({ fileType: '.jpg, .jpeg, .png', multiple: false }); const { loading: uploadingAvatar, run: onSelectAvatar } = useRequest2( async (file: File[]) => { - const src = await compressImgFileAndUpload({ - type: MongoImageTypeEnum.groupAvatar, - file: file[0], + return onSelectImage(file, { maxW: 300, maxH: 300 }); - return src; }, { onSuccess: (src: string) => { diff --git a/projects/app/src/pages/api/common/file/uploadImage.ts b/projects/app/src/pages/api/common/file/uploadImage.ts index ebd74fe7f..ef1835c41 100644 --- a/projects/app/src/pages/api/common/file/uploadImage.ts +++ b/projects/app/src/pages/api/common/file/uploadImage.ts @@ -1,38 +1,30 @@ import type { NextApiRequest, NextApiResponse } from 'next'; -import { jsonRes } from '@fastgpt/service/common/response'; import { connectToDatabase } from '@/service/mongo'; import { uploadMongoImg } from '@fastgpt/service/common/file/image/controller'; import { UploadImgProps } from '@fastgpt/global/common/file/api'; import { authCert } from '@fastgpt/service/support/permission/auth/common'; +import { NextAPI } from '@/service/middleware/entry'; /* Upload avatar image */ -export default async function handler(req: NextApiRequest, res: NextApiResponse) { - try { - await connectToDatabase(); - const body = req.body as UploadImgProps; +async function handler(req: NextApiRequest, res: NextApiResponse): Promise { + await connectToDatabase(); + const body = req.body as UploadImgProps; - const { teamId } = await authCert({ req, authToken: true }); + const { teamId } = await authCert({ req, authToken: true }); - const imgId = await uploadMongoImg({ - teamId, - ...body - }); - - jsonRes(res, { data: imgId }); - } catch (error) { - jsonRes(res, { - code: 500, - error - }); - } + return uploadMongoImg({ + teamId, + ...body + }); } +export default NextAPI(handler); export const config = { api: { bodyParser: { - sizeLimit: '16mb' + sizeLimit: '12mb' } } }; diff --git a/projects/app/src/pages/api/core/app/create.ts b/projects/app/src/pages/api/core/app/create.ts index 005b05965..d9f725f50 100644 --- a/projects/app/src/pages/api/core/app/create.ts +++ b/projects/app/src/pages/api/core/app/create.ts @@ -15,8 +15,8 @@ import { ClientSession } from '@fastgpt/service/common/mongo'; import { authApp } from '@fastgpt/service/support/permission/app/auth'; import { CommonErrEnum } from '@fastgpt/global/common/error/code/common'; import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema'; -import { MongoUser } from '@fastgpt/service/support/user/schema'; import { pushTrack } from '@fastgpt/service/common/middle/tracks/utils'; +import { refreshSourceAvatar } from '@fastgpt/service/common/file/image/controller'; export type CreateAppBody = { parentId?: ParentIdType; @@ -148,6 +148,8 @@ export const onCreateApp = async ({ ); } + await refreshSourceAvatar(avatar, undefined, session); + return appId; }; diff --git a/projects/app/src/pages/api/core/app/del.ts b/projects/app/src/pages/api/core/app/del.ts index e21661e43..ff2efff92 100644 --- a/projects/app/src/pages/api/core/app/del.ts +++ b/projects/app/src/pages/api/core/app/del.ts @@ -18,6 +18,7 @@ import { ClientSession } from '@fastgpt/service/common/mongo'; import { deleteChatFiles } from '@fastgpt/service/core/chat/controller'; import { pushTrack } from '@fastgpt/service/common/middle/tracks/utils'; import { MongoOpenApi } from '@fastgpt/service/support/openapi/schema'; +import { removeImageByPath } from '@fastgpt/service/common/file/image/controller'; async function handler(req: NextApiRequest, res: NextApiResponse) { const { appId } = req.query as { appId: string }; @@ -57,7 +58,7 @@ export const onDelOneApp = async ({ const apps = await findAppAndAllChildren({ teamId, appId, - fields: '_id' + fields: '_id avatar' }); const del = async (session: ClientSession) => { @@ -109,6 +110,8 @@ export const onDelOneApp = async ({ }, { session } ); + + await removeImageByPath(app.avatar, session); } }; diff --git a/projects/app/src/pages/api/core/app/httpPlugin/update.ts b/projects/app/src/pages/api/core/app/httpPlugin/update.ts index 0edc9e8f2..2c37b253a 100644 --- a/projects/app/src/pages/api/core/app/httpPlugin/update.ts +++ b/projects/app/src/pages/api/core/app/httpPlugin/update.ts @@ -11,6 +11,7 @@ import { MongoApp } from '@fastgpt/service/core/app/schema'; import { isEqual } from 'lodash'; import { onCreateApp } from '../create'; import { onDelOneApp } from '../del'; +import { refreshSourceAvatar } from '@fastgpt/service/common/file/image/controller'; export type UpdateHttpPluginBody = { appId: string; @@ -49,13 +50,15 @@ async function handler(req: ApiRequestProps, res: NextApiR await MongoApp.findByIdAndUpdate( appId, { - name, - avatar, - intro, + ...(name && { name }), + ...(avatar && { avatar }), + ...(intro !== undefined && { intro }), pluginData }, { session } ); + + await refreshSourceAvatar(avatar, app.avatar, session); }); } diff --git a/projects/app/src/pages/api/core/app/update.ts b/projects/app/src/pages/api/core/app/update.ts index 56cac7987..59b54f010 100644 --- a/projects/app/src/pages/api/core/app/update.ts +++ b/projects/app/src/pages/api/core/app/update.ts @@ -22,6 +22,7 @@ import { getResourceClbsAndGroups } from '@fastgpt/service/support/permission/co import { authUserPer } from '@fastgpt/service/support/permission/user/auth'; import { TeamWritePermissionVal } from '@fastgpt/global/support/permission/user/constant'; import { AppErrEnum } from '@fastgpt/global/common/error/code/app'; +import { refreshSourceAvatar } from '@fastgpt/service/common/file/image/controller'; export type AppUpdateQuery = { appId: string; @@ -95,6 +96,8 @@ async function handler(req: ApiRequestProps) { isPlugin: app.type === AppTypeEnum.plugin }); + await refreshSourceAvatar(avatar, app.avatar, session); + return MongoApp.findByIdAndUpdate( appId, { diff --git a/projects/app/src/pages/api/core/dataset/create.ts b/projects/app/src/pages/api/core/dataset/create.ts index 373cf4fe5..aaf41bf29 100644 --- a/projects/app/src/pages/api/core/dataset/create.ts +++ b/projects/app/src/pages/api/core/dataset/create.ts @@ -11,6 +11,8 @@ import type { ApiRequestProps } from '@fastgpt/service/type/next'; import { parseParentIdInMongo } from '@fastgpt/global/common/parentFolder/utils'; import { authDataset } from '@fastgpt/service/support/permission/dataset/auth'; import { pushTrack } from '@fastgpt/service/common/middle/tracks/utils'; +import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun'; +import { refreshSourceAvatar } from '@fastgpt/service/common/file/image/controller'; export type DatasetCreateQuery = {}; export type DatasetCreateBody = CreateDatasetParams; @@ -63,19 +65,29 @@ async function handler( // check limit await checkTeamDatasetLimit(teamId); - const { _id } = await MongoDataset.create({ - ...parseParentIdInMongo(parentId), - name, - intro, - teamId, - tmbId, - vectorModel, - agentModel, - avatar, - type, - apiServer, - feishuServer, - yuqueServer + const datasetId = await mongoSessionRun(async (session) => { + const [{ _id }] = await MongoDataset.create( + [ + { + ...parseParentIdInMongo(parentId), + name, + intro, + teamId, + tmbId, + vectorModel, + agentModel, + avatar, + type, + apiServer, + feishuServer, + yuqueServer + } + ], + { session } + ); + await refreshSourceAvatar(avatar, undefined, session); + + return _id; }); pushTrack.createDataset({ @@ -85,6 +97,6 @@ async function handler( uid: userId }); - return _id; + return datasetId; } export default NextAPI(handler); diff --git a/projects/app/src/pages/api/core/dataset/delete.ts b/projects/app/src/pages/api/core/dataset/delete.ts index 8cf4f0e05..4e7c47388 100644 --- a/projects/app/src/pages/api/core/dataset/delete.ts +++ b/projects/app/src/pages/api/core/dataset/delete.ts @@ -8,6 +8,7 @@ import { NextAPI } from '@/service/middleware/entry'; import { OwnerPermissionVal } from '@fastgpt/global/support/permission/constant'; import { CommonErrEnum } from '@fastgpt/global/common/error/code/common'; import { MongoDatasetCollectionTags } from '@fastgpt/service/core/dataset/tag/schema'; +import { removeImageByPath } from '@fastgpt/service/common/file/image/controller'; async function handler(req: NextApiRequest) { const { id: datasetId } = req.query as { @@ -51,6 +52,10 @@ async function handler(req: NextApiRequest) { }, { session } ); + + for await (const dataset of datasets) { + await removeImageByPath(dataset.avatar, session); + } }); } diff --git a/projects/app/src/pages/api/core/dataset/update.ts b/projects/app/src/pages/api/core/dataset/update.ts index d57ab7f4e..ea01a4910 100644 --- a/projects/app/src/pages/api/core/dataset/update.ts +++ b/projects/app/src/pages/api/core/dataset/update.ts @@ -28,6 +28,7 @@ import { DatasetErrEnum } from '@fastgpt/global/common/error/code/dataset'; import { MongoDatasetTraining } from '@fastgpt/service/core/dataset/training/schema'; import { MongoDatasetCollection } from '@fastgpt/service/core/dataset/collection/schema'; import { addDays } from 'date-fns'; +import { refreshSourceAvatar } from '@fastgpt/service/common/file/image/controller'; export type DatasetUpdateQuery = {}; export type DatasetUpdateResponse = any; @@ -144,6 +145,8 @@ async function handler( autoSync, session }); + + await refreshSourceAvatar(avatar, dataset.avatar, session); }; await mongoSessionRun(async (session) => { diff --git a/projects/app/src/pages/api/support/user/account/loginByPassword.ts b/projects/app/src/pages/api/support/user/account/loginByPassword.ts index 529d60857..82d1c3e26 100644 --- a/projects/app/src/pages/api/support/user/account/loginByPassword.ts +++ b/projects/app/src/pages/api/support/user/account/loginByPassword.ts @@ -7,12 +7,14 @@ import { UserStatusEnum } from '@fastgpt/global/support/user/constant'; import { NextAPI } from '@/service/middleware/entry'; import { useReqFrequencyLimit } from '@fastgpt/service/common/middle/reqFrequencyLimit'; import { pushTrack } from '@fastgpt/service/common/middle/tracks/utils'; +import { CommonErrEnum } from '@fastgpt/global/common/error/code/common'; +import { UserErrEnum } from '@fastgpt/global/common/error/code/user'; async function handler(req: NextApiRequest, res: NextApiResponse) { const { username, password } = req.body as PostLoginProps; if (!username || !password) { - throw new Error('缺少参数'); + return Promise.reject(CommonErrEnum.invalidParams); } // 检测用户是否存在 @@ -23,11 +25,11 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { 'status' ); if (!authCert) { - throw new Error('用户未注册'); + return Promise.reject(UserErrEnum.unAuthUser); } if (authCert.status === UserStatusEnum.forbidden) { - throw new Error('账号已停用,无法登录'); + return Promise.reject('Invalid account!'); } const user = await MongoUser.findOne({ @@ -36,7 +38,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { }); if (!user) { - throw new Error('密码错误'); + return Promise.reject(UserErrEnum.binVisitor); } const userDetail = await getUserDetail({ @@ -68,4 +70,4 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { }; } -export default NextAPI(useReqFrequencyLimit(120, 10), handler); +export default NextAPI(useReqFrequencyLimit(120, 10, true), handler); diff --git a/projects/app/src/pages/api/support/user/account/update.ts b/projects/app/src/pages/api/support/user/account/update.ts index 990e98f91..34bc06570 100644 --- a/projects/app/src/pages/api/support/user/account/update.ts +++ b/projects/app/src/pages/api/support/user/account/update.ts @@ -6,6 +6,9 @@ import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSc /* update user info */ import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next'; import { NextAPI } from '@/service/middleware/entry'; +import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun'; +import { getUserDetail } from '@fastgpt/service/support/user/controller'; +import { refreshSourceAvatar } from '@fastgpt/service/common/file/image/controller'; export type UserAccountUpdateQuery = {}; export type UserAccountUpdateBody = UserUpdateParams; export type UserAccountUpdateResponse = {}; @@ -16,22 +19,22 @@ async function handler( const { avatar, timezone } = req.body; 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; + const user = await getUserDetail({ tmbId }); // 更新对应的记录 - await MongoUser.updateOne( - { - _id: userId - }, - { - ...(avatar && { avatar }), - ...(timezone && { timezone }) - } - ); + await mongoSessionRun(async (session) => { + await MongoUser.updateOne( + { + _id: user._id + }, + { + ...(avatar && { avatar }), + ...(timezone && { timezone }) + } + ).session(session); + + await refreshSourceAvatar(avatar, user.avatar, session); + }); return {}; } diff --git a/projects/app/src/pages/app/detail/components/InfoModal.tsx b/projects/app/src/pages/app/detail/components/InfoModal.tsx index c9572eab1..b058bbe8b 100644 --- a/projects/app/src/pages/app/detail/components/InfoModal.tsx +++ b/projects/app/src/pages/app/detail/components/InfoModal.tsx @@ -1,7 +1,6 @@ import CollaboratorContextProvider from '@/components/support/permission/MemberManager/context'; import ResumeInherit from '@/components/support/permission/ResumeInheritText'; import { AppContext } from '@/pages/app/detail/components/context'; -import { compressImgFileAndUpload } from '@/web/common/file/controller'; import { useSelectFile } from '@/web/common/file/hooks/useSelectFile'; import { useI18n } from '@/web/context/I18n'; import { resumeInheritPer } from '@/web/core/app/api'; @@ -20,8 +19,6 @@ import { ModalFooter, Textarea } from '@chakra-ui/react'; -import { getErrText } from '@fastgpt/global/common/error/utils'; -import { MongoImageTypeEnum } from '@fastgpt/global/common/file/image/constants'; import type { RequireOnlyOne } from '@fastgpt/global/common/type/utils'; import type { AppSchema } from '@fastgpt/global/core/app/type.d'; import { AppPermissionList } from '@fastgpt/global/support/permission/app/constant'; @@ -42,7 +39,11 @@ const InfoModal = ({ onClose }: { onClose: () => void }) => { const { toast } = useToast(); const { updateAppDetail, appDetail, reloadApp } = useContextSelector(AppContext, (v) => v); - const { File, onOpen: onOpenSelectFile } = useSelectFile({ + const { + File, + onOpen: onOpenSelectFile, + onSelectImage + } = useSelectFile({ fileType: '.jpg,.png', multiple: false }); @@ -101,28 +102,6 @@ const InfoModal = ({ onClose }: { onClose: () => void }) => { [handleSubmit, onClose, saveSubmitError, saveSubmitSuccess] ); - const onSelectFile = useCallback( - async (e: File[]) => { - const file = e[0]; - if (!file) return; - try { - const src = await compressImgFileAndUpload({ - type: MongoImageTypeEnum.appAvatar, - file, - maxW: 300, - maxH: 300 - }); - setValue('avatar', src); - } catch (err: any) { - toast({ - title: getErrText(err, t('common:common.error.Select avatar failed')), - status: 'warning' - }); - } - }, - [setValue, t, toast] - ); - const onUpdateCollaborators = ({ members, groups, @@ -273,7 +252,15 @@ const InfoModal = ({ onClose }: { onClose: () => void }) => { - + + onSelectImage(e, { + maxH: 300, + maxW: 300, + callback: (e) => setValue('avatar', e) + }) + } + /> ); }; diff --git a/projects/app/src/pages/app/list/components/CreateModal.tsx b/projects/app/src/pages/app/list/components/CreateModal.tsx index e9056b28d..e3460af99 100644 --- a/projects/app/src/pages/app/list/components/CreateModal.tsx +++ b/projects/app/src/pages/app/list/components/CreateModal.tsx @@ -1,10 +1,7 @@ -import React, { useCallback, useMemo, useRef } from 'react'; +import React, { useRef } from 'react'; import { Box, Flex, Button, ModalBody, Input, Grid, Card } from '@chakra-ui/react'; import { useSelectFile } from '@/web/common/file/hooks/useSelectFile'; import { useForm } from 'react-hook-form'; -import { compressImgFileAndUpload } from '@/web/common/file/controller'; -import { getErrText } from '@fastgpt/global/common/error/utils'; -import { useToast } from '@fastgpt/web/hooks/useToast'; import { postCreateApp } from '@/web/core/app/api'; import { useRouter } from 'next/router'; import { emptyTemplates } from '@/web/core/app/templates'; @@ -13,7 +10,6 @@ import Avatar from '@fastgpt/web/components/common/Avatar'; import MyTooltip from '@fastgpt/web/components/common/MyTooltip'; import MyModal from '@fastgpt/web/components/common/MyModal'; import { useTranslation } from 'next-i18next'; -import { MongoImageTypeEnum } from '@fastgpt/global/common/file/image/constants'; import { useContextSelector } from 'use-context-selector'; import { AppListContext } from './context'; import { AppTypeEnum } from '@fastgpt/global/core/app/constants'; @@ -25,7 +21,6 @@ import { getTemplateMarketItemList } from '@/web/core/app/api/template'; import { useSystemStore } from '@/web/common/system/useSystemStore'; -import { AppTemplateSchemaType } from '@fastgpt/global/core/app/type'; type FormType = { avatar: string; @@ -44,7 +39,6 @@ const CreateModal = ({ onOpenTemplateModal: (type: AppTypeEnum) => void; }) => { const { t } = useTranslation(); - const { toast } = useToast(); const router = useRouter(); const { parentId, loadMyApps } = useContextSelector(AppListContext, (v) => v); const { isPc } = useSystem(); @@ -86,33 +80,15 @@ const CreateModal = ({ }); const avatar = watch('avatar'); - const { File, onOpen: onOpenSelectFile } = useSelectFile({ + const { + File, + onOpen: onOpenSelectFile, + onSelectImage + } = useSelectFile({ fileType: '.jpg,.png', multiple: false }); - const onSelectFile = useCallback( - async (e: File[]) => { - const file = e[0]; - if (!file) return; - try { - const src = await compressImgFileAndUpload({ - type: MongoImageTypeEnum.appAvatar, - file, - maxW: 300, - maxH: 300 - }); - setValue('avatar', src); - } catch (err: any) { - toast({ - title: getErrText(err, t('common:common.error.Select avatar failed')), - status: 'warning' - }); - } - }, - [setValue, t, toast] - ); - const { runAsync: onclickCreate, loading: isCreating } = useRequest2( async (data: FormType, templateId?: string) => { if (!templateId) { @@ -290,7 +266,15 @@ const CreateModal = ({ ))} - + + onSelectImage(e, { + maxH: 300, + maxW: 300, + callback: (e) => setValue('avatar', e) + }) + } + /> ); }; diff --git a/projects/app/src/pages/app/list/components/HttpPluginEditModal.tsx b/projects/app/src/pages/app/list/components/HttpPluginEditModal.tsx index 9bf703c00..a1cc0dfcd 100644 --- a/projects/app/src/pages/app/list/components/HttpPluginEditModal.tsx +++ b/projects/app/src/pages/app/list/components/HttpPluginEditModal.tsx @@ -17,14 +17,12 @@ import { } from '@chakra-ui/react'; import { useSelectFile } from '@/web/common/file/hooks/useSelectFile'; import { useForm } from 'react-hook-form'; -import { compressImgFileAndUpload } from '@/web/common/file/controller'; -import { getErrText } from '@fastgpt/global/common/error/utils'; import { useToast } from '@fastgpt/web/hooks/useToast'; import { useRequest } from '@fastgpt/web/hooks/useRequest'; import Avatar from '@fastgpt/web/components/common/Avatar'; import MyTooltip from '@fastgpt/web/components/common/MyTooltip'; import { useTranslation } from 'next-i18next'; -import { HttpPluginImgUrl, MongoImageTypeEnum } from '@fastgpt/global/common/file/image/constants'; +import { HttpPluginImgUrl } from '@fastgpt/global/common/file/image/constants'; import { postCreateHttpPlugin, putUpdateHttpPlugin, @@ -124,33 +122,15 @@ const HttpPluginEditModal = ({ errorToast: t('common:common.Update Failed') }); - const { File, onOpen: onOpenSelectFile } = useSelectFile({ + const { + File, + onOpen: onOpenSelectFile, + onSelectImage + } = useSelectFile({ fileType: 'image/*', multiple: false }); - const onSelectFile = useCallback( - async (e: File[]) => { - const file = e[0]; - if (!file) return; - try { - const src = await compressImgFileAndUpload({ - type: MongoImageTypeEnum.pluginAvatar, - file, - maxW: 300, - maxH: 300 - }); - setValue('avatar', src); - } catch (err: any) { - toast({ - title: getErrText(err, t('common:common.Select File Failed')), - status: 'warning' - }); - } - }, - [setValue, t, toast] - ); - /* load api from url */ const { mutate: onClickUrlLoadApi, isLoading: isLoadingUrlApi } = useRequest({ mutationFn: async () => { @@ -473,7 +453,15 @@ const HttpPluginEditModal = ({ )} - + + onSelectImage(e, { + maxH: 300, + maxW: 300, + callback: (e) => setValue('avatar', e) + }) + } + /> ); }; diff --git a/projects/app/src/pages/dataset/detail/components/Info/index.tsx b/projects/app/src/pages/dataset/detail/components/Info/index.tsx index 105005841..d1412cc54 100644 --- a/projects/app/src/pages/dataset/detail/components/Info/index.tsx +++ b/projects/app/src/pages/dataset/detail/components/Info/index.tsx @@ -1,15 +1,12 @@ import React, { useEffect, useState } from 'react'; import { Box, Flex, Switch, Input } from '@chakra-ui/react'; -import { useSelectFile } from '@/web/common/file/hooks/useSelectFile'; import { useConfirm } from '@fastgpt/web/hooks/useConfirm'; import { useForm } from 'react-hook-form'; -import { compressImgFileAndUpload } from '@/web/common/file/controller'; import type { DatasetItemType } from '@fastgpt/global/core/dataset/type.d'; import Avatar from '@fastgpt/web/components/common/Avatar'; import { useTranslation } from 'next-i18next'; import { useSystemStore } from '@/web/common/system/useSystemStore'; import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; -import { MongoImageTypeEnum } from '@fastgpt/global/common/file/image/constants'; import AIModelSelector from '@/components/Select/AIModelSelector'; import { postRebuildEmbedding } from '@/web/core/dataset/api'; import type { VectorModelItemType } from '@fastgpt/global/core/ai/model.d'; @@ -68,11 +65,6 @@ const Info = ({ datasetId }: { datasetId: string }) => { title: t('common:common.confirm.Common Tip') }); - const { File } = useSelectFile({ - fileType: '.jpg,.png', - multiple: false - }); - const { runAsync: onSave } = useRequest2( (data: DatasetItemType) => { return updateDataset({ @@ -87,27 +79,6 @@ const Info = ({ datasetId }: { datasetId: string }) => { } ); - const { runAsync: onSelectFile } = useRequest2( - (e: File[]) => { - const file = e[0]; - if (!file) return Promise.resolve(null); - return compressImgFileAndUpload({ - type: MongoImageTypeEnum.datasetAvatar, - file, - maxW: 300, - maxH: 300 - }); - }, - { - onSuccess(src: string | null) { - if (src) { - setValue('avatar', src); - } - }, - errorToast: t('common:common.avatar.Select Failed') - } - ); - const { runAsync: onRebuilding } = useRequest2( (vectorModel: VectorModelItemType) => { return postRebuildEmbedding({ @@ -416,7 +387,6 @@ const Info = ({ datasetId }: { datasetId: string }) => { )} - diff --git a/projects/app/src/pages/dataset/list/component/CreateModal.tsx b/projects/app/src/pages/dataset/list/component/CreateModal.tsx index ff20d9e68..f6ae59ea9 100644 --- a/projects/app/src/pages/dataset/list/component/CreateModal.tsx +++ b/projects/app/src/pages/dataset/list/component/CreateModal.tsx @@ -1,9 +1,7 @@ -import React, { useCallback, useMemo } from 'react'; +import React, { useMemo } from 'react'; import { Box, Flex, Button, ModalFooter, ModalBody, Input, HStack } from '@chakra-ui/react'; import { useSelectFile } from '@/web/common/file/hooks/useSelectFile'; import { useForm } from 'react-hook-form'; -import { compressImgFileAndUpload } from '@/web/common/file/controller'; -import { getErrText } from '@fastgpt/global/common/error/utils'; import { useToast } from '@fastgpt/web/hooks/useToast'; import { useRouter } from 'next/router'; import { useSystemStore } from '@/web/common/system/useSystemStore'; @@ -15,7 +13,6 @@ import { postCreateDataset } from '@/web/core/dataset/api'; import type { CreateDatasetParams } from '@/global/core/dataset/api.d'; import { useTranslation } from 'next-i18next'; import { DatasetTypeEnum } from '@fastgpt/global/core/dataset/constants'; -import { MongoImageTypeEnum } from '@fastgpt/global/common/file/image/constants'; import AIModelSelector from '@/components/Select/AIModelSelector'; import { useSystem } from '@fastgpt/web/hooks/useSystem'; import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip'; @@ -90,33 +87,15 @@ const CreateModal = ({ const vectorModel = watch('vectorModel'); const agentModel = watch('agentModel'); - const { File, onOpen: onOpenSelectFile } = useSelectFile({ - fileType: '.jpg,.png', + const { + File, + onOpen: onOpenSelectFile, + onSelectImage + } = useSelectFile({ + fileType: 'image/*', multiple: false }); - const onSelectFile = useCallback( - async (e: File[]) => { - const file = e[0]; - if (!file) return; - try { - const src = await compressImgFileAndUpload({ - type: MongoImageTypeEnum.datasetAvatar, - file, - maxW: 300, - maxH: 300 - }); - setValue('avatar' as const, src); - } catch (err: any) { - toast({ - title: getErrText(err, t('common:common.avatar.Select Failed')), - status: 'warning' - }); - } - }, - [setValue, t, toast] - ); - /* create a new kb and router to it */ const { run: onclickCreate, loading: creating } = useRequest2( async (data: CreateDatasetParams) => await postCreateDataset(data), @@ -275,7 +254,15 @@ const CreateModal = ({ - + + onSelectImage(e, { + maxH: 300, + maxW: 300, + callback: (e) => setValue('avatar', e) + }) + } + /> ); }; diff --git a/projects/app/src/pages/dataset/list/component/List.tsx b/projects/app/src/pages/dataset/list/component/List.tsx index 2c1f85818..c2e5d5b73 100644 --- a/projects/app/src/pages/dataset/list/component/List.tsx +++ b/projects/app/src/pages/dataset/list/component/List.tsx @@ -1,5 +1,5 @@ import React, { useMemo, useRef, useState } from 'react'; -import { changeOwner, resumeInheritPer } from '@/web/core/dataset/api'; +import { postChangeOwner, resumeInheritPer } from '@/web/core/dataset/api'; import { Box, Flex, Grid, HStack } from '@chakra-ui/react'; import { DatasetTypeEnum, DatasetTypeMap } from '@fastgpt/global/core/dataset/constants'; import MyMenu from '@fastgpt/web/components/common/MyMenu'; @@ -423,7 +423,7 @@ function List() { {!!editPerDataset && ( - changeOwner({ + postChangeOwner({ datasetId: editPerDataset._id, ownerId: tmbId }).then(() => loadMyDatasets()) diff --git a/projects/app/src/pages/login/components/ForgetPasswordForm.tsx b/projects/app/src/pages/login/components/ForgetPasswordForm.tsx index 7d87c6ef4..43dcfe0f8 100644 --- a/projects/app/src/pages/login/components/ForgetPasswordForm.tsx +++ b/projects/app/src/pages/login/components/ForgetPasswordForm.tsx @@ -1,7 +1,7 @@ import React, { Dispatch } from 'react'; import { FormControl, Box, Input, Button } from '@chakra-ui/react'; import { useForm } from 'react-hook-form'; -import { LoginPageTypeEnum } from '@/web/support/user/login/constants'; +import { LoginPageTypeEnum, PasswordRule } from '@/web/support/user/login/constants'; import { postFindPassword } from '@/web/support/user/api'; import { useSendCode } from '@/web/support/user/hooks/useSendCode'; import type { ResLogin } from '@/global/support/api/userRes.d'; @@ -70,6 +70,18 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => { refreshDeps: [loginSuccess, t, toast] } ); + const onSubmitErr = (err: Record) => { + const val = Object.values(err)[0]; + if (!val) return; + if (val.message) { + toast({ + status: 'warning', + title: val.message, + duration: 3000, + isClosable: true + }); + } + }; return ( <> @@ -79,8 +91,8 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => { { - if (e.keyCode === 13 && !e.shiftKey && !requesting) { - handleSubmit(onclickFindPassword)(); + if (e.key === 'Enter' && !e.shiftKey && !requesting) { + handleSubmit(onclickFindPassword, onSubmitErr)(); } }} > @@ -123,16 +135,12 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => { bg={'myGray.50'} type={'password'} size={'lg'} - placeholder={t('user:password.new_password')} + placeholder={t('login:password_tip')} {...register('password', { - required: t('user:password.password_required'), - minLength: { - value: 4, - message: t('user:password.password_condition') - }, - maxLength: { - value: 20, - message: t('user:password.password_condition') + required: true, + pattern: { + value: PasswordRule, + message: t('login:password_tip') } })} > @@ -160,7 +168,7 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => { fontWeight={['medium', 'medium']} colorScheme="blue" isLoading={requesting} - onClick={handleSubmit(onclickFindPassword)} + onClick={handleSubmit(onclickFindPassword, onSubmitErr)} > {t('user:password.retrieve')} diff --git a/projects/app/src/pages/login/components/LoginForm/LoginForm.tsx b/projects/app/src/pages/login/components/LoginForm/LoginForm.tsx index cfc955a34..ad365e4b2 100644 --- a/projects/app/src/pages/login/components/LoginForm/LoginForm.tsx +++ b/projects/app/src/pages/login/components/LoginForm/LoginForm.tsx @@ -1,4 +1,4 @@ -import React, { useState, Dispatch, useCallback } from 'react'; +import React, { Dispatch } from 'react'; import { FormControl, Flex, Input, Button, Box, Link } from '@chakra-ui/react'; import { useForm } from 'react-hook-form'; import { LoginPageTypeEnum } from '@/web/support/user/login/constants'; @@ -9,6 +9,7 @@ import { useSystemStore } from '@/web/common/system/useSystemStore'; import { getDocPath } from '@/web/common/system/doc'; import { useTranslation } from 'next-i18next'; import FormLayout from './components/FormLayout'; +import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; interface Props { setPageType: Dispatch<`${LoginPageTypeEnum}`>; @@ -30,31 +31,22 @@ const LoginForm = ({ setPageType, loginSuccess }: Props) => { formState: { errors } } = useForm(); - const [requesting, setRequesting] = useState(false); - - const onclickLogin = useCallback( + const { runAsync: onclickLogin, loading: requesting } = useRequest2( async ({ username, password }: LoginFormType) => { - setRequesting(true); - try { - loginSuccess( - await postLogin({ - username, - password - }) - ); - toast({ - title: t('login:login_success'), - status: 'success' - }); - } catch (error: any) { - toast({ - title: error.message || t('login:login_failed'), - status: 'error' - }); - } - setRequesting(false); + loginSuccess( + await postLogin({ + username, + password + }) + ); + toast({ + title: t('login:login_success'), + status: 'success' + }); }, - [loginSuccess, t, toast] + { + refreshDeps: [loginSuccess] + } ); const isCommunityVersion = !!(feConfigs?.register_method && !feConfigs?.isPlus); diff --git a/projects/app/src/pages/login/components/RegisterForm.tsx b/projects/app/src/pages/login/components/RegisterForm.tsx index 9da7a6962..44643b06a 100644 --- a/projects/app/src/pages/login/components/RegisterForm.tsx +++ b/projects/app/src/pages/login/components/RegisterForm.tsx @@ -1,7 +1,7 @@ import React, { Dispatch } from 'react'; import { FormControl, Box, Input, Button } from '@chakra-ui/react'; import { useForm } from 'react-hook-form'; -import { LoginPageTypeEnum } from '@/web/support/user/login/constants'; +import { LoginPageTypeEnum, PasswordRule } from '@/web/support/user/login/constants'; import { postRegister } from '@/web/support/user/api'; import { useSendCode } from '@/web/support/user/hooks/useSendCode'; import type { ResLogin } from '@/global/support/api/userRes'; @@ -87,6 +87,18 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => { refreshDeps: [loginSuccess, t, toast] } ); + const onSubmitErr = (err: Record) => { + const val = Object.values(err)[0]; + if (!val) return; + if (val.message) { + toast({ + status: 'warning', + title: val.message, + duration: 3000, + isClosable: true + }); + } + }; const placeholder = feConfigs?.register_method ?.map((item) => { @@ -108,7 +120,7 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => { mt={9} onKeyDown={(e) => { if (e.key === 'Enter' && !e.shiftKey && !requesting) { - handleSubmit(onclickRegister)(); + handleSubmit(onclickRegister, onSubmitErr)(); } }} > @@ -151,16 +163,12 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => { bg={'myGray.50'} size={'lg'} type={'password'} - placeholder={t('user:password.new_password')} + placeholder={t('login:password_tip')} {...register('password', { - required: t('user:password.password_required'), - minLength: { - value: 4, - message: t('user:password.password_condition') - }, - maxLength: { - value: 20, - message: t('user:password.password_condition') + required: true, + pattern: { + value: PasswordRule, + message: t('login:password_tip') } })} > @@ -175,7 +183,7 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => { validate: (val) => getValues('password') === val ? true : t('user:password.not_match') })} - > + /> diff --git a/projects/app/src/pages/login/provider.tsx b/projects/app/src/pages/login/provider.tsx index 5ac82d7fe..d6f0371eb 100644 --- a/projects/app/src/pages/login/provider.tsx +++ b/projects/app/src/pages/login/provider.tsx @@ -16,7 +16,7 @@ let isOauthLogging = false; const provider = () => { const { t } = useTranslation(); - const { loginStore } = useSystemStore(); + const { initd, loginStore, setLoginStore } = useSystemStore(); const { setUserInfo } = useUserStore(); const router = useRouter(); const { code, state, error } = router.query as { code: string; state: string; error?: string }; @@ -34,13 +34,9 @@ const provider = () => { const authCode = useCallback( async (code: string) => { - if (!loginStore) { - router.replace('/login'); - return; - } try { const res = await oauthLogin({ - type: loginStore?.provider as `${OAuthEnum}`, + type: loginStore?.provider || OAuthEnum.sso, code, callbackUrl: `${location.origin}/login/provider`, inviterId: localStorage.getItem('inviterId') || undefined, @@ -76,8 +72,9 @@ const provider = () => { router.replace('/login'); }, 1000); } + setLoginStore(undefined); }, - [loginStore, loginSuccess, router, t, toast] + [loginStore?.provider, loginSuccess, router, setLoginStore, t, toast] ); useEffect(() => { @@ -90,8 +87,8 @@ const provider = () => { return; } - console.log('SSO', { loginStore, code, state }); - if (!code || !loginStore) return; + console.log('SSO', { initd, loginStore, code, state }); + if (!code || !initd) return; if (isOauthLogging) return; @@ -101,7 +98,7 @@ const provider = () => { await clearToken(); router.prefetch('/app/list'); - if (loginStore.provider !== OAuthEnum.sso && state !== loginStore?.state) { + if (loginStore && state !== loginStore.state) { toast({ status: 'warning', title: t('common:support.user.login.security_failed') @@ -114,7 +111,7 @@ const provider = () => { authCode(code); } })(); - }, [authCode, code, error, loginStore, loginStore?.state, router, state, t, toast]); + }, [initd, authCode, code, error, loginStore, loginStore?.state, router, state, t, toast]); return ; }; @@ -123,6 +120,8 @@ export default provider; export async function getServerSideProps(context: any) { return { - props: { ...(await serviceSideProps(context)) } + props: { + ...(await serviceSideProps(context)) + } }; } diff --git a/projects/app/src/web/common/file/controller.ts b/projects/app/src/web/common/file/controller.ts index 1bbce5936..a02ca0283 100644 --- a/projects/app/src/web/common/file/controller.ts +++ b/projects/app/src/web/common/file/controller.ts @@ -35,30 +35,11 @@ export const uploadFile2DB = ({ }); }; -export const getUploadBase64ImgController = ( - props: CompressImgProps & UploadImgProps, - retry = 3 -): Promise => { - try { - return compressBase64ImgAndUpload({ - maxW: 4000, - maxH: 4000, - maxSize: 1024 * 1024 * 5, - ...props - }); - } catch (error) { - if (retry > 0) { - return getUploadBase64ImgController(props, retry - 1); - } - return Promise.reject(error); - } -}; - /** * compress image. response base64 * @param maxSize The max size of the compressed image */ -export const compressBase64ImgAndUpload = async ({ +const compressBase64ImgAndUpload = async ({ base64Img, maxW, maxH, @@ -89,7 +70,7 @@ export const compressImgFileAndUpload = async ({ reader.readAsDataURL(file); const base64Img = await new Promise((resolve, reject) => { - reader.onload = async () => { + reader.onload = () => { resolve(reader.result as string); }; reader.onerror = (err) => { diff --git a/projects/app/src/web/common/file/hooks/useSelectFile.tsx b/projects/app/src/web/common/file/hooks/useSelectFile.tsx index bc05ca41e..b2abdecb1 100644 --- a/projects/app/src/web/common/file/hooks/useSelectFile.tsx +++ b/projects/app/src/web/common/file/hooks/useSelectFile.tsx @@ -3,12 +3,17 @@ import { Box } from '@chakra-ui/react'; import { useToast } from '@fastgpt/web/hooks/useToast'; import { useI18n } from '@/web/context/I18n'; import { useMemoizedFn } from 'ahooks'; +import { compressImgFileAndUpload } from '../controller'; +import { getErrText } from '@fastgpt/global/common/error/utils'; +import { useTranslation } from 'next-i18next'; +import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; export const useSelectFile = (props?: { fileType?: string; multiple?: boolean; maxCount?: number; }) => { + const { t } = useTranslation(); const { fileT } = useI18n(); const { fileType = '*', multiple = false, maxCount = 10 } = props || {}; const { toast } = useToast(); @@ -48,8 +53,44 @@ export const useSelectFile = (props?: { SelectFileDom.current && SelectFileDom.current.click(); }, []); + const { runAsync: onSelectImage, loading } = useRequest2( + async ( + e: File[], + { + maxW, + maxH, + callback + }: { + maxW?: number; + maxH?: number; + callback?: (e: string) => any; + } + ) => { + const file = e[0]; + if (!file) return Promise.resolve('Can not found image'); + try { + const src = await compressImgFileAndUpload({ + file, + maxW, + maxH + }); + console.log(src, '--'); + callback?.(src); + return src; + } catch (err: any) { + toast({ + title: getErrText(err, t('common:error.upload_image_error')), + status: 'warning' + }); + return Promise.reject(getErrText(err, t('common:error.upload_image_error'))); + } + } + ); + return { File, - onOpen + onOpen, + onSelectImage, + loading }; }; diff --git a/projects/app/src/web/common/system/useSystemStore.ts b/projects/app/src/web/common/system/useSystemStore.ts index a40122c90..e71855945 100644 --- a/projects/app/src/web/common/system/useSystemStore.ts +++ b/projects/app/src/web/common/system/useSystemStore.ts @@ -27,7 +27,8 @@ type State = { setLastAppListRouteType: (e?: string) => void; loginStore?: LoginStoreType; - setLoginStore: (e: LoginStoreType) => void; + setLoginStore: (e?: LoginStoreType) => void; + loading: boolean; setLoading: (val: boolean) => null; gitStar: number; diff --git a/projects/app/src/web/core/dataset/api.ts b/projects/app/src/web/core/dataset/api.ts index 41ff95ae5..5a17b7fce 100644 --- a/projects/app/src/web/core/dataset/api.ts +++ b/projects/app/src/web/core/dataset/api.ts @@ -101,7 +101,7 @@ export const postCreateDatasetFolder = (data: DatasetFolderCreateBody) => export const resumeInheritPer = (datasetId: string) => GET(`/core/dataset/resumeInheritPermission`, { datasetId }); -export const changeOwner = (data: { ownerId: string; datasetId: string }) => +export const postChangeOwner = (data: { ownerId: string; datasetId: string }) => POST(`/proApi/core/dataset/changeOwner`, data); /* =========== search test ============ */ diff --git a/projects/app/src/web/support/user/login/constants.ts b/projects/app/src/web/support/user/login/constants.ts index ca24fc4df..72fa301e9 100644 --- a/projects/app/src/web/support/user/login/constants.ts +++ b/projects/app/src/web/support/user/login/constants.ts @@ -4,3 +4,6 @@ export enum LoginPageTypeEnum { forgetPassword = 'forgetPassword', wechat = 'wechat' } + +export const PasswordRule = + /^(?:(?=.*\d)(?=.*[a-z])|(?=.*\d)(?=.*[A-Z])|(?=.*\d)(?=.*[!@#$%^&*_])|(?=.*[a-z])(?=.*[A-Z])|(?=.*[a-z])(?=.*[!@#$%^&*_])|(?=.*[A-Z])(?=.*[!@#$%^&*_]))[\dA-Za-z!@#$%^&*_]{6,}$/;