Commit bd966d47 by Archer Committed by GitHub

fix: login secret (#6635)

* fix: login secret

* lock

* env template

* fix: ts

* fix: ts

* fix: ts
parent e48a037f
...@@ -5,11 +5,12 @@ ...@@ -5,11 +5,12 @@
## 输出要求 ## 输出要求
1. 输出语言:中文 1. 输出语言:中文
2. 输出的设计文档位置:.claude/design,问题分析文档位置: .claude/issue,以 Markdown 文件为主。 2. 输出文档位置:
3. 输出 Plan 时,均需写入 .claude/plan 目录下,以 Markdown 文件为主。 1. 设计文档:.claude/design,todo 跟在设计文档后面。
4. 相同需求文档,尽量写在一起(内容超过 300 行,可以分批写入),或者创建要给目录一起管理,不要随意平铺一堆不同版本的相同问题的文档。 2. 问题分析文档: .claude/issue
5. 文件输出,使用正确的编码格式,例如UTF-8。 3. 相同需求文档,尽量写在一起(内容超过 300 行,可以分批写入),或者创建要给目录一起管理,不要随意平铺一堆不同版本的相同问题的文档。
6. 如果用户未指明,不要随意编写总结报告。 4. 文件输出,使用正确的编码格式,例如UTF-8。
5. 如果用户未指明,不要随意编写总结报告。
## 项目概述 ## 项目概述
......
# Context
当前 `opensandbox` 分支已经暴露了 OpenSandbox provider 环境变量,并在运行时通过 `SandboxClient` 走 provider 分支,但 OpenSandbox 的“持久化配置”仍然散落在 `SandboxClient` 内部,没有形成独立的配置抽象,也没有 volume-manager / `createConfig.volumes` / 实例详情落库这条完整链路。
相比之下,`agent-skill-dev` 已经把这部分能力拆成了独立配置模块,并通过 volume-manager 为 session/runtime 准备持久化卷,再将存储与运行时详情写入 sandbox 实例记录。
本次推荐方案的目标不是整包迁入 `agentSkills` 上层编排,而是:**保留当前分支以 `SandboxClient` 为唯一消费入口的形态,将 OpenSandbox 持久化配置能力下沉到 `packages/service/core/ai/sandbox` 域内完成抽离**。这样可以在最小扰动现有 workflow/tool 调用链的前提下,把持久化卷、配置解析和实例详情持久化补齐。
# Recommended Approach
## 1. 先把配置抽象从 `SandboxClient` 构造函数中抽离
新增统一配置模块,建议放在:
- `packages/service/core/ai/sandbox/config.ts`
`agent-skill-dev` 复用并改造以下能力,统一收敛到 sandbox 域:
- `getSandboxProviderConfig`
- `getVolumeManagerConfig`
- `buildVolumeConfig`
- `buildBaseContainerEnv`
这样 `packages/service/core/ai/sandbox/controller.ts` 不再直接读取 env 并内联拼 provider 参数,而是只消费配置模块输出。
## 2. 补齐 provider 类型与实例详情结构
当前事实:
- `packages/service/env.ts` 已支持 `opensandbox`
- `packages/service/core/ai/sandbox/type.ts``SandboxProviderSchema` 仍只有 `sealosdevbox`
需要在:
- `packages/service/core/ai/sandbox/type.ts`
- `packages/service/core/ai/sandbox/schema.ts`
补齐以下结构:
- `SandboxProviderSchema` 至少包含 `opensandbox`
- 实例 `detail`:provider 运行时返回信息、endpoint、连接信息
- 实例 `storage`:volume 标识、claimName、mountPath、provider storage payload
- 实例 `metadata`:session 维度标识、createConfig 摘要、迁移/兼容标记
要求:新字段全部可选,保证旧记录仍可读。
## 3. 在 `SandboxClient` 内接入持久化卷准备与详情落库
核心改造文件:
- `packages/service/core/ai/sandbox/controller.ts`
推荐保留 `SandboxClient` 作为唯一消费入口,内部改造为:
1. 根据业务标识(`appId/userId/chatId``sandboxId`)构建稳定 session key
2. 调用 `config.ts` 读取 provider 配置
3. 如果 provider 为 `opensandbox`,则调用 volume-manager 配置与卷构建逻辑
4. 将 volume 信息塞入 `createConfig.volumes`
5. 创建/恢复实例后,把 `detail/storage/metadata` 写入 `MongoSandboxInstance`
复用目标分支的关键能力时,优先迁“配置与卷构建逻辑”,不要直接把 `agentSkills` 生命周期编排整包搬入。
## 4. 保持业务入口不变,只做最小上下文透传
现有消费入口继续复用 `SandboxClient`
- `packages/service/core/workflow/dispatch/ai/agent/sub/sandbox/index.ts`
- `packages/service/core/workflow/dispatch/ai/tool/toolCall.ts`
这两个入口只做最小修改:
- 确保传入稳定的 `appId/userId/chatId`
- 不感知 provider 配置、volume-manager、存储细节
原则:**底层配置与持久化逻辑留在 sandbox 域,业务入口不扩散基础设施细节。**
## 5. 分两阶段实施
### 第一阶段(必须先迁)
1. 新增 `packages/service/core/ai/sandbox/config.ts`
2. 扩展 `packages/service/env.ts` 中 OpenSandbox 持久化相关 env
3. 扩展 `packages/service/core/ai/sandbox/type.ts`
4. 扩展 `packages/service/core/ai/sandbox/schema.ts`
5. 改造 `packages/service/core/ai/sandbox/controller.ts`
6. 最小化调整 workflow / tool 入口透传上下文
### 第二阶段(可后补)
1. 引入独立 lifecycle 管理(参考 `agent-skill-dev``.../sandbox/lifecycle.ts`
2. 区分 edit-debug 与 session-runtime 的 volume 策略
3. 补充 volume 清理、回滚、观测与旧数据回填能力
# Critical Files to Modify
必须修改:
- `packages/service/env.ts`
- `packages/service/core/ai/sandbox/type.ts`
- `packages/service/core/ai/sandbox/schema.ts`
- `packages/service/core/ai/sandbox/controller.ts`
- `packages/service/core/workflow/dispatch/ai/agent/sub/sandbox/index.ts`
- `packages/service/core/workflow/dispatch/ai/tool/toolCall.ts`
建议新增:
- `packages/service/core/ai/sandbox/config.ts`
目标分支中应重点参考、复用逻辑的来源文件:
- `packages/service/core/agentSkills/sandboxConfig.ts`
- `packages/service/core/agentSkills/sandboxController.ts`
- `packages/service/core/workflow/dispatch/ai/agent/sub/sandbox/lifecycle.ts`
# Reuse Existing Functions and Patterns
优先复用当前仓库已验证的模式:
- 现有单一消费入口模式:`packages/service/core/ai/sandbox/controller.ts` 中的 `SandboxClient`
- 现有运行时消费入口:
- `packages/service/core/workflow/dispatch/ai/agent/sub/sandbox/index.ts`
- `packages/service/core/workflow/dispatch/ai/tool/toolCall.ts`
- 目标分支中可迁入的配置抽离函数:
- `getSandboxProviderConfig`
- `getVolumeManagerConfig`
- `buildVolumeConfig`
- `buildBaseContainerEnv`
复用原则:
- 复用“能力”与“结构”,不直接复制 `agentSkills` 上层调用链
- 将通用配置逻辑沉入 `core/ai/sandbox`,避免基础设施层依赖业务层
# Key Risks and Compatibility Notes
1. **类型不一致风险**
- 现在 env 支持 `opensandbox`,但 `SandboxProviderSchema` 不支持
- 必须先统一类型层,再改控制器逻辑
2. **旧数据兼容风险**
- 历史 `agent_sandbox_instances` 没有 `detail/storage/metadata`
- 新字段必须 optional,读取逻辑必须支持旧数据回退到 `provider + sandboxId`
3. **provider 分支串扰风险**
- OpenSandbox 的 volume 逻辑不能污染 `sealosdevbox` / `e2b`
- `createConfig.volumes` 只能在对应 provider 下启用
4. **依赖反转风险**
- 不要让 `core/ai/sandbox` 反向依赖 `agentSkills`
- 通用配置能力必须落在 sandbox 域内
5. **卷幂等与清理风险**
- 第一阶段至少保证 ensure volume 幂等
- 清理/回滚放第二阶段补齐
# Verification
## 单测
1. `config.ts`
- `getSandboxProviderConfig`:不同 provider 输出正确;缺失关键 env 时给出明确错误
- `getVolumeManagerConfig`:能正确解析 volume-manager 配置
- `buildVolumeConfig`:相同 session 上下文生成稳定 volume 配置
- `buildBaseContainerEnv`:容器基础 env 正确且不泄漏无关敏感信息
2. `type.ts` / `schema.ts`
- 旧记录仅有基础字段时可通过读取
- 新记录包含 `detail/storage/metadata` 时可通过校验
3. `controller.ts`
- OpenSandbox 创建时会注入 `createConfig.volumes`
- 创建后会写入 `detail/storage/metadata`
- 旧实例记录仍可兼容
- 非 OpenSandbox provider 行为不变
## 集成测试
1. 通过 `dispatchSandboxShell` 触发首次 session sandbox 创建
2. 检查 `agent_sandbox_instances` 中是否写入 `detail/storage/metadata`
3. 同一 `appId + userId + chatId` 再次进入时,验证 volume 信息可复用
4.`toolCall.ts` 链路触发一次 sandbox 执行,确认入口无需感知底层 volume 配置
## 人工验证
1. 配齐 OpenSandbox + volume-manager 环境变量后启动服务
2. 首次会话进入 sandbox,写入一个测试文件
3. 结束请求后使用相同 `appId/userId/chatId` 再次进入
4. 验证测试文件仍然存在,且实例记录中保留 storage/detail 信息
5. 切换到 `sealosdevbox` / `e2b` 时,验证旧链路行为不变
...@@ -7,6 +7,7 @@ description: 'FastGPT V4.14.10 更新说明' ...@@ -7,6 +7,7 @@ description: 'FastGPT V4.14.10 更新说明'
## 🚀 新增内容 ## 🚀 新增内容
1. 飞书发布渠道,支持流输出。 1. 飞书发布渠道,支持流输出。
2. 目录最大上限,可通过环境变量配置。
## ⚙️ 优化 ## ⚙️ 优化
......
...@@ -67,6 +67,7 @@ CODE_SANDBOX_TOKEN=代码运行沙盒的凭证 ...@@ -67,6 +67,7 @@ CODE_SANDBOX_TOKEN=代码运行沙盒的凭证
13. 修复视频音频自定义文件类型流程开始无文件链接变量 13. 修复视频音频自定义文件类型流程开始无文件链接变量
14. 用户输入框消息不转义成 markdown 格式 14. 用户输入框消息不转义成 markdown 格式
15. 修复 AgentV2 部分上下文拼接错误。 15. 修复 AgentV2 部分上下文拼接错误。
16. login 接口安全风险。
## 代码优化 ## 代码优化
......
...@@ -220,7 +220,7 @@ ...@@ -220,7 +220,7 @@
"document/content/docs/self-host/upgrading/4-14/4140.mdx": "2026-03-03T17:39:47+08:00", "document/content/docs/self-host/upgrading/4-14/4140.mdx": "2026-03-03T17:39:47+08:00",
"document/content/docs/self-host/upgrading/4-14/4141.en.mdx": "2026-03-03T17:39:47+08:00", "document/content/docs/self-host/upgrading/4-14/4141.en.mdx": "2026-03-03T17:39:47+08:00",
"document/content/docs/self-host/upgrading/4-14/4141.mdx": "2026-03-03T17:39:47+08:00", "document/content/docs/self-host/upgrading/4-14/4141.mdx": "2026-03-03T17:39:47+08:00",
"document/content/docs/self-host/upgrading/4-14/41410.mdx": "2026-03-24T09:59:30+08:00", "document/content/docs/self-host/upgrading/4-14/41410.mdx": "2026-03-24T18:02:38+08:00",
"document/content/docs/self-host/upgrading/4-14/4142.en.mdx": "2026-03-03T17:39:47+08:00", "document/content/docs/self-host/upgrading/4-14/4142.en.mdx": "2026-03-03T17:39:47+08:00",
"document/content/docs/self-host/upgrading/4-14/4142.mdx": "2026-03-03T17:39:47+08:00", "document/content/docs/self-host/upgrading/4-14/4142.mdx": "2026-03-03T17:39:47+08:00",
"document/content/docs/self-host/upgrading/4-14/4143.en.mdx": "2026-03-03T17:39:47+08:00", "document/content/docs/self-host/upgrading/4-14/4143.en.mdx": "2026-03-03T17:39:47+08:00",
...@@ -240,7 +240,7 @@ ...@@ -240,7 +240,7 @@
"document/content/docs/self-host/upgrading/4-14/41481.en.mdx": "2026-03-09T12:02:02+08:00", "document/content/docs/self-host/upgrading/4-14/41481.en.mdx": "2026-03-09T12:02:02+08:00",
"document/content/docs/self-host/upgrading/4-14/41481.mdx": "2026-03-09T17:39:53+08:00", "document/content/docs/self-host/upgrading/4-14/41481.mdx": "2026-03-09T17:39:53+08:00",
"document/content/docs/self-host/upgrading/4-14/4149.en.mdx": "2026-03-23T12:17:04+08:00", "document/content/docs/self-host/upgrading/4-14/4149.en.mdx": "2026-03-23T12:17:04+08:00",
"document/content/docs/self-host/upgrading/4-14/4149.mdx": "2026-03-23T23:37:12+08:00", "document/content/docs/self-host/upgrading/4-14/4149.mdx": "2026-03-25T13:54:15+08:00",
"document/content/docs/self-host/upgrading/outdated/40.en.mdx": "2026-03-03T17:39:47+08:00", "document/content/docs/self-host/upgrading/outdated/40.en.mdx": "2026-03-03T17:39:47+08:00",
"document/content/docs/self-host/upgrading/outdated/40.mdx": "2026-03-03T17:39:47+08:00", "document/content/docs/self-host/upgrading/outdated/40.mdx": "2026-03-03T17:39:47+08:00",
"document/content/docs/self-host/upgrading/outdated/41.en.mdx": "2026-03-03T17:39:47+08:00", "document/content/docs/self-host/upgrading/outdated/41.en.mdx": "2026-03-03T17:39:47+08:00",
......
...@@ -16,6 +16,8 @@ export enum LangEnum { ...@@ -16,6 +16,8 @@ export enum LangEnum {
export type localeType = `${LangEnum}`; export type localeType = `${LangEnum}`;
export const LocaleList = ['en', 'zh-CN', 'zh-Hant'] as const; export const LocaleList = ['en', 'zh-CN', 'zh-Hant'] as const;
export const LanguageSchema = z.enum(LocaleList).meta({ description: '用户语言偏好' });
export const langMap = { export const langMap = {
[LangEnum.en]: { [LangEnum.en]: {
label: 'English(US)', label: 'English(US)',
......
import { AdminInformPath } from './inform'; import { AdminInformPath } from './inform';
import { AdminLoginPath } from './login';
import type { OpenAPIPath } from '../../../type'; import type { OpenAPIPath } from '../../../type';
export const AdminUserPath: OpenAPIPath = { export const AdminUserPath: OpenAPIPath = {
...AdminInformPath ...AdminInformPath,
...AdminLoginPath
}; };
import { z } from 'zod';
// Admin login request body
export const AdminLoginBodySchema = z.object({
username: z.string().meta({ description: '用户名' }),
password: z.string().meta({ description: '密码' })
});
export type AdminLoginBodyType = z.infer<typeof AdminLoginBodySchema>;
// Admin login response
export const AdminLoginResponseSchema = z.object({
user: z.any().meta({ description: '用户信息' }),
token: z.string().meta({ description: '登录令牌' })
});
export type AdminLoginResponseType = z.infer<typeof AdminLoginResponseSchema>;
import type { OpenAPIPath } from '../../../../type';
import { AdminLoginBodySchema, AdminLoginResponseSchema } from './api';
import { TagsMap } from '../../../../tag';
export const AdminLoginPath: OpenAPIPath = {
'/admin/support/user/login': {
post: {
summary: '管理员登录',
description: '管理员使用用户名和密码登录',
tags: [TagsMap.userLogin],
requestBody: {
content: {
'application/json': {
schema: AdminLoginBodySchema
}
}
},
responses: {
200: {
description: '登录成功',
content: {
'application/json': {
schema: AdminLoginResponseSchema
}
}
}
}
}
}
};
...@@ -54,7 +54,12 @@ export const openAPIDocument = createDocument({ ...@@ -54,7 +54,12 @@ export const openAPIDocument = createDocument({
}, },
{ {
name: '用户体系', name: '用户体系',
tags: [TagsMap.userInform, TagsMap.walletBill, TagsMap.walletDiscountCoupon] tags: [
TagsMap.userInform,
TagsMap.walletBill,
TagsMap.walletDiscountCoupon,
TagsMap.userLogin
]
}, },
{ {
name: '通用-核心功能', name: '通用-核心功能',
......
import type { OpenAPIPath } from '../../../type';
import { LoginPath } from './login';
import { RegisterPath } from './register';
import { PasswordPath } from './password';
export const UserAccountPath: OpenAPIPath = {
...LoginPath,
...RegisterPath,
...PasswordPath
};
import { z } from 'zod';
import { OAuthEnum } from '../../../../../support/user/constant';
import { TrackRegisterParamsSchema } from '../../../../../support/marketing/type';
import { LanguageSchema } from '../../../../../common/i18n/type';
export const LoginSuccessResponseSchema = z.object({
user: z.any().meta({
description: '用户详情'
}),
token: z.string().meta({
example: 'eyJhbGciOiJIUzI1NiIs...',
description: '登录令牌'
})
});
export type LoginSuccessResponseType = z.infer<typeof LoginSuccessResponseSchema>;
// ===== Pre login - get login verification code =====
export const PreLoginQuerySchema = z.object({
username: z.string().meta({
example: 'admin',
description: '用户名'
})
});
export type PreLoginQueryType = z.infer<typeof PreLoginQuerySchema>;
export const PreLoginResponseSchema = z
.object({
code: z.string().meta({
example: 'a1b2c3',
description: '预登录验证码'
})
})
.meta({
example: {
code: 'a1b2c3'
}
});
export type PreLoginResponseType = z.infer<typeof PreLoginResponseSchema>;
// ===== Login by password =====
export const LoginByPasswordBodySchema = z
.object({
username: z.string().meta({
example: 'admin',
description: '用户名'
}),
password: z.string().meta({
example: 'hashed_password',
description: '密码'
}),
code: z.string().meta({
example: '123456',
description: '预登录验证码'
}),
language: LanguageSchema.optional().default('zh-CN').meta({
example: 'zh-CN',
description: '用户语言偏好'
})
})
.meta({
example: {
username: 'admin',
password: 'hashed_password',
code: '123456',
language: 'zh-CN'
}
});
export type LoginByPasswordBodyType = z.infer<typeof LoginByPasswordBodySchema>;
/* ===== Wecom Login ===== */
export const WecomGetRedirectURLBodySchema = z.object({
redirectUri: z.string(),
state: z.string(),
isWecomWorkTerminal: z.boolean()
});
export const WecomGetRedirectURLResponseSchema = z.string();
export type WecomGetRedirectURLBodyType = z.infer<typeof WecomGetRedirectURLBodySchema>;
export type WecomGetRedirectURLResponseType = z.infer<typeof WecomGetRedirectURLResponseSchema>;
// ===== OAuth Login =====
export const OauthLoginBodySchema = TrackRegisterParamsSchema.extend({
type: z.enum(OAuthEnum).meta({ description: 'OAuth 登录类型' }),
callbackUrl: z.string().meta({ description: '回调 URL' }),
props: z.record(z.string(), z.string()).meta({ description: '附加属性' }),
language: LanguageSchema.optional().meta({ description: '语言' })
});
export type OauthLoginBodyType = z.infer<typeof OauthLoginBodySchema>;
// ===== Fast Login =====
export const FastLoginBodySchema = z.object({
token: z.string().meta({ description: 'Token' }),
code: z.string().meta({ description: 'Code' })
});
export type FastLoginBodyType = z.infer<typeof FastLoginBodySchema>;
// ===== WeChat Login Result =====
export const WxLoginBodySchema = z.object({
inviterId: z.string().optional().meta({ description: '邀请人 ID' }),
code: z.string().meta({ description: '微信登录 Code' }),
bd_vid: z.string().optional(),
msclkid: z.string().optional(),
fastgpt_sem: z.string().optional(),
sourceDomain: z.string().optional()
});
export type WxLoginBodyType = z.infer<typeof WxLoginBodySchema>;
export const GetWXLoginQRResponseSchema = z.object({
code: z.string().meta({ description: '微信登录 Code' }),
codeUrl: z.string().meta({ description: '微信登录二维码 URL' })
});
export type GetWXLoginQRResponseType = z.infer<typeof GetWXLoginQRResponseSchema>;
import type { OpenAPIPath } from '../../../../type';
import { TagsMap } from '../../../../tag';
import {
LoginByPasswordBodySchema,
PreLoginQuerySchema,
PreLoginResponseSchema,
OauthLoginBodySchema,
FastLoginBodySchema,
WxLoginBodySchema,
GetWXLoginQRResponseSchema,
LoginSuccessResponseSchema
} from './api';
import { UserSchema } from '../../../../../support/user/type';
export const LoginPath: OpenAPIPath = {
'/support/user/account/tokenLogin': {
get: {
summary: 'Token 登录',
description: '通过已有的登录令牌获取用户信息',
tags: [TagsMap.userLogin],
responses: {
200: {
description: '成功获取用户信息',
content: {
'application/json': {
schema: UserSchema
}
}
}
}
}
},
'/support/user/account/preLogin': {
get: {
summary: '预登录获取验证码',
description: '通过用户名获取预登录验证码,用于密码登录时的验证',
tags: [TagsMap.userLogin],
requestParams: {
query: PreLoginQuerySchema
},
responses: {
200: {
description: '成功获取预登录验证码',
content: {
'application/json': {
schema: PreLoginResponseSchema
}
}
}
}
}
},
'/support/user/account/loginByPassword': {
post: {
summary: '用户密码登录',
description: '通过用户名和密码进行登录,需要先获取预登录验证码',
tags: [TagsMap.userLogin],
requestBody: {
content: {
'application/json': {
schema: LoginByPasswordBodySchema
}
}
},
responses: {
200: {
description: '登录成功,返回用户信息和令牌',
content: {
'application/json': {
schema: LoginSuccessResponseSchema
}
}
}
}
}
},
'/proApi/support/user/account/login/oauth': {
post: {
summary: 'OAuth 登录',
description: '使用第三方 OAuth 授权登录',
tags: [TagsMap.userLogin],
requestBody: {
content: {
'application/json': {
schema: OauthLoginBodySchema
}
}
},
responses: {
200: {
description: '登录成功',
content: {
'application/json': {
schema: LoginSuccessResponseSchema
}
}
}
}
}
},
'/proApi/support/user/account/login/fastLogin': {
post: {
summary: '快捷登录',
description: '使用 Token 和 Code 进行快捷登录',
tags: [TagsMap.userLogin],
requestBody: {
content: {
'application/json': {
schema: FastLoginBodySchema
}
}
},
responses: {
200: {
description: '登录成功',
content: {
'application/json': {
schema: LoginSuccessResponseSchema
}
}
}
}
}
},
'/proApi/support/user/account/login/wx/getQR': {
get: {
summary: '获取微信登录二维码',
description: '获取微信登录二维码',
tags: [TagsMap.userLogin],
responses: {
200: {
description: '获取微信登录二维码成功',
content: {
'application/json': {
schema: GetWXLoginQRResponseSchema
}
}
}
}
}
},
'/proApi/support/user/account/login/wx/getResult': {
post: {
summary: '获取微信登录结果',
description: '提交微信登录 Code 以获取登录结果',
tags: [TagsMap.userLogin],
requestBody: {
content: {
'application/json': {
schema: WxLoginBodySchema
}
}
},
responses: {
200: {
description: '登录成功',
content: {
'application/json': {
schema: LoginSuccessResponseSchema
}
}
}
}
}
},
'/support/user/account/loginout': {
get: {
summary: '退出登录',
description: '退出当前用户的所有会话并清除登录凭证',
tags: [TagsMap.userLogin],
responses: {
200: {
description: '退出登录成功'
}
}
}
}
};
import { z } from 'zod';
export const WecomGetRedirectURLBodySchema = z.object({
redirectUri: z.string(),
state: z.string(),
isWecomWorkTerminal: z.boolean()
});
export const WecomGetRedirectURLResponseSchema = z.string();
export type WecomGetRedirectURLBodyType = z.infer<typeof WecomGetRedirectURLBodySchema>;
export type WecomGetRedirectURLResponseType = z.infer<typeof WecomGetRedirectURLResponseSchema>;
import { z } from 'zod';
// ===== Update password by old password =====
export const UpdatePasswordByOldBodySchema = z
.object({
oldPsw: z.string().trim().min(1).meta({
example: 'hashed_old_password',
description: '旧密码(已加密)'
}),
newPsw: z.string().trim().min(1).meta({
example: 'hashed_new_password',
description: '新密码(已加密)'
})
})
.meta({
example: {
oldPsw: 'hashed_old_password',
newPsw: 'hashed_new_password'
}
});
export type UpdatePasswordByOldBodyType = z.infer<typeof UpdatePasswordByOldBodySchema>;
export const UpdatePasswordByOldResponseSchema = z.any().meta({
description: '用户信息'
});
export type UpdatePasswordByOldResponseType = z.infer<typeof UpdatePasswordByOldResponseSchema>;
// ===== Check password expired =====
export const CheckPswExpiredResponseSchema = z.boolean().meta({
example: false,
description: '密码是否已过期'
});
export type CheckPswExpiredResponseType = z.infer<typeof CheckPswExpiredResponseSchema>;
// ===== Reset expired password =====
export const ResetExpiredPswBodySchema = z
.object({
newPsw: z.string().trim().min(1).meta({
example: 'hashed_new_password',
description: '新密码(已加密)'
})
})
.meta({
example: {
newPsw: 'hashed_new_password'
}
});
export type ResetExpiredPswBodyType = z.infer<typeof ResetExpiredPswBodySchema>;
export const ResetExpiredPswResponseSchema = z.object({}).meta({
description: '重置成功'
});
export type ResetExpiredPswResponseType = z.infer<typeof ResetExpiredPswResponseSchema>;
// ===== Find Password (update by code) =====
export const UpdatePasswordByCodeBodySchema = z.object({
username: z.string().trim().min(1).meta({ description: '用户名' }),
code: z.string().meta({ description: '验证码' }),
password: z.string().trim().min(1).meta({ description: '新密码' }),
tmbId: z.string().optional().meta({ description: '团队成员 ID(可选)' })
});
export type UpdatePasswordByCodeBodyType = z.infer<typeof UpdatePasswordByCodeBodySchema>;
import type { OpenAPIPath } from '../../../../type';
import { TagsMap } from '../../../../tag';
import {
UpdatePasswordByOldBodySchema,
UpdatePasswordByOldResponseSchema,
CheckPswExpiredResponseSchema,
ResetExpiredPswBodySchema,
ResetExpiredPswResponseSchema,
UpdatePasswordByCodeBodySchema
} from './api';
export const PasswordPath: OpenAPIPath = {
'/support/user/account/updatePasswordByOld': {
post: {
summary: '通过旧密码修改密码',
description: '使用旧密码验证后修改为新密码,修改成功后其他会话将被注销',
tags: [TagsMap.userLogin],
requestBody: {
content: {
'application/json': {
schema: UpdatePasswordByOldBodySchema
}
}
},
responses: {
200: {
description: '密码修改成功',
content: {
'application/json': {
schema: UpdatePasswordByOldResponseSchema
}
}
}
}
}
},
'/support/user/account/checkPswExpired': {
get: {
summary: '检查密码是否过期',
description: '检查当前用户的密码是否已过期,需要强制修改',
tags: [TagsMap.userLogin],
responses: {
200: {
description: '返回密码是否过期',
content: {
'application/json': {
schema: CheckPswExpiredResponseSchema
}
}
}
}
}
},
'/support/user/account/resetExpiredPsw': {
post: {
summary: '重置过期密码',
description: '当密码过期时,使用此接口重置密码,重置后其他会话将被注销',
tags: [TagsMap.userLogin],
requestBody: {
content: {
'application/json': {
schema: ResetExpiredPswBodySchema
}
}
},
responses: {
200: {
description: '密码重置成功',
content: {
'application/json': {
schema: ResetExpiredPswResponseSchema
}
}
}
}
}
},
'/support/user/account/password/updateByCode': {
post: {
summary: '通过验证码找回/修改密码',
description: '通过邮箱/手机验证码找回或修改密码',
tags: [TagsMap.userLogin],
requestBody: {
content: {
'application/json': {
schema: UpdatePasswordByCodeBodySchema
}
}
},
responses: {
200: {
description: '修改成功',
content: {
'application/json': {
schema: {}
}
}
}
}
}
}
};
import { z } from 'zod';
import { TrackRegisterParamsSchema } from '../../../../../support/marketing/type';
import { LanguageSchema } from '../../../../../common/i18n/type';
// ===== Register by email or phone =====
export const AccountRegisterBodySchema = TrackRegisterParamsSchema.extend({
username: z.string().meta({ description: '用户名(邮箱或手机号)' }),
code: z.string().meta({ description: '验证码' }),
password: z.string().meta({ description: '密码(已加密)' }),
language: LanguageSchema.optional().meta({ description: '语言' })
});
export type AccountRegisterBodyType = z.infer<typeof AccountRegisterBodySchema>;
import type { OpenAPIPath } from '../../../../type';
import { TagsMap } from '../../../../tag';
import { AccountRegisterBodySchema } from './api';
export const RegisterPath: OpenAPIPath = {
'/support/user/account/register/emailAndPhone': {
post: {
summary: '邮箱/手机号注册',
description: '使用邮箱或手机号验证码注册新账号',
tags: [TagsMap.userLogin],
requestBody: {
content: {
'application/json': {
schema: AccountRegisterBodySchema
}
}
},
responses: {
200: {
description: '注册成功',
content: {
'application/json': {
schema: {}
}
}
}
}
}
}
};
import { UserInformPath } from './inform'; import { UserInformPath } from './inform';
import type { OpenAPIPath } from '../../type'; import type { OpenAPIPath } from '../../type';
import { UserAccountPath } from './account';
export const UserPath: OpenAPIPath = { export const UserPath: OpenAPIPath = {
...UserInformPath ...UserInformPath,
...UserAccountPath
}; };
...@@ -42,6 +42,7 @@ export const TagsMap = { ...@@ -42,6 +42,7 @@ export const TagsMap = {
customDomain: '自定义域名', customDomain: '自定义域名',
// User // User
userInform: '用户通知', userInform: '用户通知',
userLogin: '用户账号',
/* Common */ /* Common */
// APIKey // APIKey
......
import type { MemberGroupSchemaType } from '../permission/memberGroup/type'; import type { MemberGroupSchemaType } from '../permission/memberGroup/type';
import { MemberGroupListItemType } from '../permission/memberGroup/type';
import type { OAuthEnum } from './constant';
import { TeamMemberStatusEnum } from './team/constant';
import type { OrgType } from './team/org/type'; import type { OrgType } from './team/org/type';
import type { TeamMemberItemType } from './team/type'; import type { TeamMemberItemType } from './team/type';
import type { LangEnum } from '../../common/i18n/type';
import type { TrackRegisterParams } from '../marketing/type';
export type PostLoginProps = {
username: string;
password: string;
code: string;
language?: `${LangEnum}`;
};
export type OauthLoginProps = {
type: `${OAuthEnum}`;
callbackUrl: string;
props: Record<string, string>;
language?: `${LangEnum}`;
} & TrackRegisterParams;
export type WxLoginProps = {
inviterId?: string;
code: string;
bd_vid?: string;
msclkid?: string;
fastgpt_sem?: string;
sourceDomain?: string;
};
export type FastLoginProps = {
token: string;
code: string;
};
export type SearchResult = { export type SearchResult = {
members: Omit<TeamMemberItemType, 'teamId' | 'permission'>[]; members: Omit<TeamMemberItemType, 'teamId' | 'permission'>[];
......
import type { LangEnum } from '../../../common/i18n/type';
import type { TrackRegisterParams } from '../../marketing/type';
export type GetWXLoginQRResponse = {
code: string;
codeUrl: string;
};
export type AccountRegisterBody = {
username: string;
code: string;
password: string;
language?: `${LangEnum}`;
} & TrackRegisterParams;
import type { TeamMetaType, UserModelSchema } from '../type'; import type { TeamMetaType, UserModelSchema } from '../type';
import type { TeamMemberRoleEnum, TeamMemberStatusEnum } from './constant'; import { TeamMemberRoleEnum, TeamMemberStatusEnum } from './constant';
import type { GroupMemberRole } from '../../permission/memberGroup/constant'; import type { GroupMemberRole } from '../../permission/memberGroup/constant';
import type { TeamPermission } from '../../permission/user/controller'; import { TeamPermission } from '../../permission/user/controller';
import { z } from 'zod';
export type ThirdPartyAccountType = { export const LafAccountSchema = z.object({
lafAccount?: LafAccountType; appid: z.string(),
openaiAccount?: OpenaiAccountType; token: z.string(),
externalWorkflowVariables?: Record<string, string>; pat: z.string()
}; });
export type LafAccountType = z.infer<typeof LafAccountSchema>;
export const OpenaiAccountSchema = z.object({
key: z.string(),
baseUrl: z.string()
});
export type OpenaiAccountType = z.infer<typeof OpenaiAccountSchema>;
export const ThidPartyAccountSchema = z.object({
lafAccount: LafAccountSchema.optional(),
openaiAccount: OpenaiAccountSchema.optional(),
externalWorkflowVariables: z.record(z.string(), z.string()).optional()
});
export type ThirdPartyAccountType = z.infer<typeof ThidPartyAccountSchema>;
export type TeamSchema = { export type TeamSchema = {
_id: string; _id: string;
...@@ -45,7 +60,7 @@ export type TeamMemberSchema = { ...@@ -45,7 +60,7 @@ export type TeamMemberSchema = {
createTime: Date; createTime: Date;
updateTime?: Date; updateTime?: Date;
name: string; name: string;
role: `${TeamMemberRoleEnum}`; role: TeamMemberRoleEnum;
status: TeamMemberStatusEnum; status: TeamMemberStatusEnum;
avatar: string; avatar: string;
}; };
...@@ -55,22 +70,23 @@ export type TeamMemberWithTeamAndUserSchema = TeamMemberSchema & { ...@@ -55,22 +70,23 @@ export type TeamMemberWithTeamAndUserSchema = TeamMemberSchema & {
user: UserModelSchema; user: UserModelSchema;
}; };
export type TeamTmbItemType = { export const TeamTmbItemSchema = ThidPartyAccountSchema.extend({
userId: string; userId: z.string(),
teamId: string; teamId: z.string(),
teamAvatar?: string; teamAvatar: z.string().optional(),
teamName: string; teamName: z.string(),
memberName: string; memberName: z.string(),
avatar: string; avatar: z.string(),
balance?: number; balance: z.number().optional(),
tmbId: string; tmbId: z.string(),
teamDomain: string; teamDomain: z.string(),
role: `${TeamMemberRoleEnum}`; role: z.enum(TeamMemberRoleEnum),
status: `${TeamMemberStatusEnum}`; status: z.enum(TeamMemberStatusEnum),
notificationAccount?: string; notificationAccount: z.string().optional(),
permission: TeamPermission; permission: z.instanceof(TeamPermission),
isWecomTeam?: boolean; isWecomTeam: z.boolean().optional()
} & ThirdPartyAccountType; });
export type TeamTmbItemType = z.infer<typeof TeamTmbItemSchema>;
export type TeamMemberItemType< export type TeamMemberItemType<
Options extends { Options extends {
...@@ -110,17 +126,6 @@ export type TeamTagItemType = { ...@@ -110,17 +126,6 @@ export type TeamTagItemType = {
key: string; key: string;
}; };
export type LafAccountType = {
appid: string;
token: string;
pat: string;
};
export type OpenaiAccountType = {
key: string;
baseUrl: string;
};
export type TeamInvoiceHeaderType = { export type TeamInvoiceHeaderType = {
teamName: string; teamName: string;
unifiedCreditCode: string; unifiedCreditCode: string;
......
import z from 'zod'; import z from 'zod';
import type { LangEnum } from '../../common/i18n/type'; import { LanguageSchema, type LangEnum } from '../../common/i18n/type';
import type { TeamPermission } from '../permission/user/controller'; import { TeamPermission } from '../permission/user/controller';
import type { UserStatusEnum } from './constant'; import type { UserStatusEnum } from './constant';
import { TeamMemberStatusEnum } from './team/constant'; import { TeamMemberStatusEnum } from './team/constant';
import type { TeamTmbItemType } from './team/type'; import { TeamTmbItemSchema } from './team/type';
export const UserTagsEnum = z.enum(['wecom']); export const UserTagsEnum = z.enum(['wecom']);
export type UserTagsEnum = z.infer<typeof UserTagsEnum>; export type UserTagsEnum = z.infer<typeof UserTagsEnum>;
...@@ -33,18 +33,19 @@ export type UserModelSchema = { ...@@ -33,18 +33,19 @@ export type UserModelSchema = {
meta?: UserMetaType; meta?: UserMetaType;
}; };
export type UserType = { export const UserSchema = z.object({
_id: string; _id: z.string(),
username: string; username: z.string(),
avatar: string; // it should be team member's avatar after 4.8.18 avatar: z.string(),
timezone: string; timezone: z.string(),
language?: `${LangEnum}`; language: LanguageSchema.optional(),
promotionRate: UserModelSchema['promotionRate']; promotionRate: z.number(),
team: TeamTmbItemType; team: TeamTmbItemSchema,
permission: TeamPermission; permission: z.instanceof(TeamPermission),
contact?: string; contact: z.string().optional(),
tags?: UserTagsEnum[]; tags: z.array(UserTagsEnum).optional()
}; });
export type UserType = z.infer<typeof UserSchema>;
export const SourceMemberSchema = z.object({ export const SourceMemberSchema = z.object({
name: z.string().meta({ example: '张三', description: '成员名称' }), name: z.string().meta({ example: '张三', description: '成员名称' }),
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
}, },
"dependencies": { "dependencies": {
"@apidevtools/json-schema-ref-parser": "^11.7.2", "@apidevtools/json-schema-ref-parser": "^11.7.2",
"@fastgpt-sdk/sandbox-adapter": "^0.0.28", "@fastgpt-sdk/sandbox-adapter": "^0.0.31",
"@fastgpt-sdk/otel": "catalog:", "@fastgpt-sdk/otel": "catalog:",
"@fastgpt-sdk/storage": "catalog:", "@fastgpt-sdk/storage": "catalog:",
"@fastgpt/global": "workspace:*", "@fastgpt/global": "workspace:*",
......
...@@ -4,120 +4,6 @@ settings: ...@@ -4,120 +4,6 @@ settings:
autoInstallPeers: true autoInstallPeers: true
excludeLinksFromLockfile: false excludeLinksFromLockfile: false
catalogs:
default:
'@chakra-ui/anatomy':
specifier: ^2
version: 2.3.6
'@chakra-ui/icons':
specifier: ^2
version: 2.1.1
'@chakra-ui/next-js':
specifier: ^2
version: 2.4.2
'@chakra-ui/react':
specifier: ^2
version: 2.10.7
'@chakra-ui/styled-system':
specifier: ^2
version: 2.12.2
'@chakra-ui/system':
specifier: ^2
version: 2.6.1
'@emotion/react':
specifier: ^11
version: 11.11.1
'@emotion/styled':
specifier: ^11
version: 11.11.0
'@fastgpt-sdk/logger':
specifier: 0.1.2
version: 0.1.2
'@fastgpt-sdk/otel':
specifier: 0.1.0
version: 0.1.0
'@fastgpt-sdk/storage':
specifier: 0.6.15
version: 0.6.15
'@modelcontextprotocol/sdk':
specifier: ^1
version: 1.26.0
'@types/lodash':
specifier: ^4
version: 4.17.16
'@types/node':
specifier: ^20
version: 20.17.24
'@types/react':
specifier: ^18
version: 18.3.1
'@types/react-dom':
specifier: ^18
version: 18.3.0
axios:
specifier: 1.13.6
version: 1.13.6
date-fns:
specifier: ^3.6.0
version: 3.6.0
dayjs:
specifier: 1.11.19
version: 1.11.19
eslint:
specifier: ^8
version: 8.57.1
eslint-config-next:
specifier: 15.5.12
version: 15.5.12
express:
specifier: ^4
version: 4.22.1
i18next:
specifier: 23.16.8
version: 23.16.8
js-yaml:
specifier: ^4.1.1
version: 4.1.1
json5:
specifier: ^2.2.3
version: 2.2.3
lodash:
specifier: 4.17.23
version: 4.17.23
minio:
specifier: 8.0.7
version: 8.0.7
next:
specifier: 16.1.6
version: 16.1.6
next-i18next:
specifier: 15.4.2
version: 15.4.2
next-rspack:
specifier: 16.1.6
version: 16.1.6
proxy-agent:
specifier: ^6
version: 6.5.0
react:
specifier: ^18
version: 18.3.1
react-dom:
specifier: ^18
version: 18.3.1
react-i18next:
specifier: 14.1.2
version: 14.1.2
tsdown:
specifier: 0.21.4
version: 0.21.4
typescript:
specifier: ^5.9.3
version: 5.9.3
zod:
specifier: ^4
version: 4.1.12
importers: importers:
.: .:
...@@ -250,8 +136,8 @@ importers: ...@@ -250,8 +136,8 @@ importers:
specifier: 'catalog:' specifier: 'catalog:'
version: 0.1.0 version: 0.1.0
'@fastgpt-sdk/sandbox-adapter': '@fastgpt-sdk/sandbox-adapter':
specifier: ^0.0.28 specifier: ^0.0.31
version: 0.0.28 version: 0.0.31
'@fastgpt-sdk/storage': '@fastgpt-sdk/storage':
specifier: 'catalog:' specifier: 'catalog:'
version: 0.6.15(@opentelemetry/api@1.9.0)(@types/node@24.0.13)(jiti@2.6.0)(lightningcss@1.30.1)(proxy-agent@6.5.0)(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.8.1) version: 0.6.15(@opentelemetry/api@1.9.0)(@types/node@24.0.13)(jiti@2.6.0)(lightningcss@1.30.1)(proxy-agent@6.5.0)(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.8.1)
...@@ -1185,10 +1071,6 @@ importers: ...@@ -1185,10 +1071,6 @@ importers:
packages: packages:
'@alibaba-group/opensandbox@0.1.4':
resolution: {integrity: sha512-hTgzsBRYCoNM5A3cM0Rgsq4q996pWzHNk2MDClhXsPoeg7ArYXkqYJAylh2c2iM8dV6IB7PwCe7/y9eeYn9kOA==}
engines: {node: '>=20'}
'@alloc/quick-lru@5.2.0': '@alloc/quick-lru@5.2.0':
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
engines: {node: '>=10'} engines: {node: '>=10'}
...@@ -2723,8 +2605,8 @@ packages: ...@@ -2723,8 +2605,8 @@ packages:
'@fastgpt-sdk/plugin@0.3.8': '@fastgpt-sdk/plugin@0.3.8':
resolution: {integrity: sha512-GjKrXMHxeF5UMkYGXawrUpzZjVRw3DICNYODeYwsUVOy+/ltu5zuwsqLkuuGQ7Arp/SBCmYRjG/MHmeNp4xxfw==} resolution: {integrity: sha512-GjKrXMHxeF5UMkYGXawrUpzZjVRw3DICNYODeYwsUVOy+/ltu5zuwsqLkuuGQ7Arp/SBCmYRjG/MHmeNp4xxfw==}
'@fastgpt-sdk/sandbox-adapter@0.0.28': '@fastgpt-sdk/sandbox-adapter@0.0.31':
resolution: {integrity: sha512-sqOnv/4hnxbjE8HGNqauHnhuzHxsXTHvkNDIFz+XV/we/Jj7h25nt+hnEtO1zOYmXKcuhbGC0zmrYwBPoIE3tQ==} resolution: {integrity: sha512-Ex1nQNJo0BCFhy1FqsjOyTys1b+tDvzYexDHXUo+34rVF0AFYXhp7KRcoKlCyF2LFvFtHxPK2ggbUUP3Mr5lRQ==}
engines: {node: '>=18'} engines: {node: '>=18'}
'@fastgpt-sdk/storage@0.6.15': '@fastgpt-sdk/storage@0.6.15':
...@@ -11673,12 +11555,121 @@ packages: ...@@ -11673,12 +11555,121 @@ packages:
zwitch@2.0.4: zwitch@2.0.4:
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
snapshots: catalogs:
default:
'@chakra-ui/anatomy':
specifier: ^2
version: 2.3.6
'@chakra-ui/icons':
specifier: ^2
version: 2.1.1
'@chakra-ui/next-js':
specifier: ^2
version: 2.4.2
'@chakra-ui/react':
specifier: ^2
version: 2.10.7
'@chakra-ui/styled-system':
specifier: ^2
version: 2.12.2
'@chakra-ui/system':
specifier: ^2
version: 2.6.1
'@emotion/react':
specifier: ^11
version: 11.11.1
'@emotion/styled':
specifier: ^11
version: 11.11.0
'@fastgpt-sdk/logger':
specifier: 0.1.2
version: 0.1.2
'@fastgpt-sdk/otel':
specifier: 0.1.0
version: 0.1.0
'@fastgpt-sdk/storage':
specifier: 0.6.15
version: 0.6.15
'@modelcontextprotocol/sdk':
specifier: ^1
version: 1.26.0
'@types/lodash':
specifier: ^4
version: 4.17.16
'@types/node':
specifier: ^20
version: 20.17.24
'@types/react':
specifier: ^18
version: 18.3.1
'@types/react-dom':
specifier: ^18
version: 18.3.0
axios:
specifier: 1.13.6
version: 1.13.6
date-fns:
specifier: ^3
version: 3.6.0
dayjs:
specifier: 1.11.19
version: 1.11.19
eslint:
specifier: ^8
version: 8.57.1
eslint-config-next:
specifier: 15.5.12
version: 15.5.12
express:
specifier: ^4
version: 4.22.1
i18next:
specifier: 23.16.8
version: 23.16.8
js-yaml:
specifier: ^4.1.1
version: 4.1.1
json5:
specifier: ^2.2.3
version: 2.2.3
lodash:
specifier: 4.17.23
version: 4.17.23
minio:
specifier: 8.0.7
version: 8.0.7
next:
specifier: 16.1.6
version: 16.1.6
next-i18next:
specifier: 15.4.2
version: 15.4.2
next-rspack:
specifier: 16.1.6
version: 16.1.6
proxy-agent:
specifier: ^6
version: 6.5.0
react:
specifier: ^18
version: 18.3.1
react-dom:
specifier: ^18
version: 18.3.1
react-i18next:
specifier: 14.1.2
version: 14.1.2
tsdown:
specifier: 0.21.4
version: 0.21.4
typescript:
specifier: ^5.9.3
version: 5.9.3
zod:
specifier: ^4
version: 4.1.12
'@alibaba-group/opensandbox@0.1.4': snapshots:
dependencies:
openapi-fetch: 0.14.1
undici: 7.18.2
'@alloc/quick-lru@5.2.0': {} '@alloc/quick-lru@5.2.0': {}
...@@ -13675,9 +13666,8 @@ snapshots: ...@@ -13675,9 +13666,8 @@ snapshots:
'@fortaine/fetch-event-source': 3.0.6 '@fortaine/fetch-event-source': 3.0.6
zod: 4.1.12 zod: 4.1.12
'@fastgpt-sdk/sandbox-adapter@0.0.28': '@fastgpt-sdk/sandbox-adapter@0.0.31':
dependencies: dependencies:
'@alibaba-group/opensandbox': 0.1.4
'@e2b/code-interpreter': 2.3.3 '@e2b/code-interpreter': 2.3.3
'@fastgpt-sdk/storage@0.6.15(@opentelemetry/api@1.9.0)(@types/node@20.17.24)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.8.1)': '@fastgpt-sdk/storage@0.6.15(@opentelemetry/api@1.9.0)(@types/node@20.17.24)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.8.1)':
...@@ -18557,7 +18547,7 @@ snapshots: ...@@ -18557,7 +18547,7 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
eslint-module-utils@2.12.1(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.9.0)(eslint@8.57.1): eslint-module-utils@2.12.1(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.9.0(eslint-plugin-import@2.32.0)(eslint@8.57.1))(eslint@8.57.1):
dependencies: dependencies:
debug: 3.2.7 debug: 3.2.7
optionalDependencies: optionalDependencies:
...@@ -18579,7 +18569,7 @@ snapshots: ...@@ -18579,7 +18569,7 @@ snapshots:
doctrine: 2.1.0 doctrine: 2.1.0
eslint: 8.57.1 eslint: 8.57.1
eslint-import-resolver-node: 0.3.9 eslint-import-resolver-node: 0.3.9
eslint-module-utils: 2.12.1(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.9.0)(eslint@8.57.1) eslint-module-utils: 2.12.1(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.9.0(eslint-plugin-import@2.32.0)(eslint@8.57.1))(eslint@8.57.1)
hasown: 2.0.2 hasown: 2.0.2
is-core-module: 2.16.1 is-core-module: 2.16.1
is-glob: 4.0.3 is-glob: 4.0.3
......
...@@ -126,6 +126,10 @@ WORKFLOW_MAX_LOOP_TIMES=50 ...@@ -126,6 +126,10 @@ WORKFLOW_MAX_LOOP_TIMES=50
SERVICE_REQUEST_MAX_CONTENT_LENGTH=10 SERVICE_REQUEST_MAX_CONTENT_LENGTH=10
# 启用内网 IP 检查 # 启用内网 IP 检查
CHECK_INTERNAL_IP=false CHECK_INTERNAL_IP=false
# 应用文件夹最大数量
APP_FOLDER_MAX_AMOUNT=1000
# 数据集文件夹最大数量
DATASET_FOLDER_MAX_AMOUNT=1000
# ==================== 上传与账号策略 ==================== # ==================== 上传与账号策略 ====================
# 最大上传文件大小(MB) # 最大上传文件大小(MB)
......
import type { UserType } from '@fastgpt/global/support/user/type'; import type { UserType } from '@fastgpt/global/support/user/type';
import type { PromotionRecordSchema } from '@fastgpt/global/support/activity/type'; import type { PromotionRecordSchema } from '@fastgpt/global/support/activity/type';
export interface LoginSuccessResponse {
user: UserType;
token: string;
}
export interface PromotionRecordType { export interface PromotionRecordType {
_id: PromotionRecordSchema['_id']; _id: PromotionRecordSchema['_id'];
......
...@@ -4,16 +4,16 @@ import { useForm } from 'react-hook-form'; ...@@ -4,16 +4,16 @@ import { useForm } from 'react-hook-form';
import { LoginPageTypeEnum } from '@/web/support/user/login/constants'; import { LoginPageTypeEnum } from '@/web/support/user/login/constants';
import { postFindPassword } from '@/web/support/user/api'; import { postFindPassword } from '@/web/support/user/api';
import { useSendCode } from '@/web/support/user/hooks/useSendCode'; import { useSendCode } from '@/web/support/user/hooks/useSendCode';
import type { LoginSuccessResponse } from '@/global/support/api/userRes';
import { useToast } from '@fastgpt/web/hooks/useToast'; import { useToast } from '@fastgpt/web/hooks/useToast';
import { useSystemStore } from '@/web/common/system/useSystemStore'; import { useSystemStore } from '@/web/common/system/useSystemStore';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { useRequest } from '@fastgpt/web/hooks/useRequest'; import { useRequest } from '@fastgpt/web/hooks/useRequest';
import { checkPasswordRule } from '@fastgpt/global/common/string/password'; import { checkPasswordRule } from '@fastgpt/global/common/string/password';
import type { LoginSuccessResponseType } from '@fastgpt/global/openapi/support/user/account/login/api';
interface Props { interface Props {
setPageType: Dispatch<`${LoginPageTypeEnum}`>; setPageType: Dispatch<`${LoginPageTypeEnum}`>;
loginSuccess: (e: LoginSuccessResponse) => void; loginSuccess: (e: LoginSuccessResponseType) => void;
} }
interface RegisterType { interface RegisterType {
......
...@@ -3,7 +3,6 @@ import { FormControl, Flex, Input, Button, Box } from '@chakra-ui/react'; ...@@ -3,7 +3,6 @@ import { FormControl, Flex, Input, Button, Box } from '@chakra-ui/react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { LoginPageTypeEnum } from '@/web/support/user/login/constants'; import { LoginPageTypeEnum } from '@/web/support/user/login/constants';
import { postLogin, getPreLogin } from '@/web/support/user/api'; import { postLogin, getPreLogin } from '@/web/support/user/api';
import type { LoginSuccessResponse } from '@/global/support/api/userRes';
import { useToast } from '@fastgpt/web/hooks/useToast'; import { useToast } from '@fastgpt/web/hooks/useToast';
import { useSystemStore } from '@/web/common/system/useSystemStore'; import { useSystemStore } from '@/web/common/system/useSystemStore';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
...@@ -15,10 +14,11 @@ import { UserErrEnum } from '@fastgpt/global/common/error/code/user'; ...@@ -15,10 +14,11 @@ import { UserErrEnum } from '@fastgpt/global/common/error/code/user';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useMount } from 'ahooks'; import { useMount } from 'ahooks';
import type { LangEnum } from '@fastgpt/global/common/i18n/type'; import type { LangEnum } from '@fastgpt/global/common/i18n/type';
import type { LoginSuccessResponseType } from '@fastgpt/global/openapi/support/user/account/login/api';
interface Props { interface Props {
setPageType: Dispatch<`${LoginPageTypeEnum}`>; setPageType: Dispatch<`${LoginPageTypeEnum}`>;
loginSuccess: (e: LoginSuccessResponse) => void; loginSuccess: (e: LoginSuccessResponseType) => void;
} }
interface LoginFormType { interface LoginFormType {
......
import React, { type Dispatch } from 'react'; import React, { type Dispatch } from 'react';
import { LoginPageTypeEnum } from '@/web/support/user/login/constants'; import { LoginPageTypeEnum } from '@/web/support/user/login/constants';
import type { LoginSuccessResponse } from '@/global/support/api/userRes'; import { Box, Center } from '@chakra-ui/react';
import { Box, Center, Flex, Link } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { getWXLoginQR, getWXLoginResult } from '@/web/support/user/api'; import { getWXLoginQR, getWXLoginResult } from '@/web/support/user/api';
import { getErrText } from '@fastgpt/global/common/error/utils'; import { getErrText } from '@fastgpt/global/common/error/utils';
...@@ -18,12 +17,12 @@ import { ...@@ -18,12 +17,12 @@ import {
removeFastGPTSem, removeFastGPTSem,
getInviterId getInviterId
} from '@/web/support/marketing/utils'; } from '@/web/support/marketing/utils';
import { getDocPath } from '@/web/common/system/doc';
import { useSystemStore } from '@/web/common/system/useSystemStore'; import { useSystemStore } from '@/web/common/system/useSystemStore';
import PolicyTip from './PolicyTip'; import PolicyTip from './PolicyTip';
import type { LoginSuccessResponseType } from '@fastgpt/global/openapi/support/user/account/login/api';
interface Props { interface Props {
loginSuccess: (e: LoginSuccessResponse) => void; loginSuccess: (e: LoginSuccessResponseType) => void;
setPageType: Dispatch<`${LoginPageTypeEnum}`>; setPageType: Dispatch<`${LoginPageTypeEnum}`>;
} }
...@@ -55,7 +54,7 @@ const WechatForm = ({ setPageType, loginSuccess }: Props) => { ...@@ -55,7 +54,7 @@ const WechatForm = ({ setPageType, loginSuccess }: Props) => {
{ {
refetchInterval: 3 * 1000, refetchInterval: 3 * 1000,
enabled: !!wechatInfo?.code, enabled: !!wechatInfo?.code,
onSuccess(data: LoginSuccessResponse | undefined) { onSuccess(data: LoginSuccessResponseType | undefined) {
if (data) { if (data) {
removeFastGPTSem(); removeFastGPTSem();
loginSuccess(data); loginSuccess(data);
......
...@@ -4,10 +4,10 @@ import { LoginContainer } from '@/pageComponents/login'; ...@@ -4,10 +4,10 @@ import { LoginContainer } from '@/pageComponents/login';
import I18nLngSelector from '@/components/Select/I18nLngSelector'; import I18nLngSelector from '@/components/Select/I18nLngSelector';
import { useSystem } from '@fastgpt/web/hooks/useSystem'; import { useSystem } from '@fastgpt/web/hooks/useSystem';
import { getWebReqUrl } from '@fastgpt/web/common/system/utils'; import { getWebReqUrl } from '@fastgpt/web/common/system/utils';
import type { LoginSuccessResponse } from '@/global/support/api/userRes'; import { type LoginSuccessResponseType } from '@fastgpt/global/openapi/support/user/account/login/api';
type LoginModalProps = { type LoginModalProps = {
onSuccess: (e: LoginSuccessResponse) => any; onSuccess: (e: LoginSuccessResponseType) => any;
}; };
const LoginModal = ({ onSuccess }: LoginModalProps) => { const LoginModal = ({ onSuccess }: LoginModalProps) => {
......
...@@ -4,7 +4,6 @@ import { useForm } from 'react-hook-form'; ...@@ -4,7 +4,6 @@ import { useForm } from 'react-hook-form';
import { LoginPageTypeEnum } from '@/web/support/user/login/constants'; import { LoginPageTypeEnum } from '@/web/support/user/login/constants';
import { postRegister } from '@/web/support/user/api'; import { postRegister } from '@/web/support/user/api';
import { useSendCode } from '@/web/support/user/hooks/useSendCode'; import { useSendCode } from '@/web/support/user/hooks/useSendCode';
import type { LoginSuccessResponse } from '@/global/support/api/userRes';
import { useToast } from '@fastgpt/web/hooks/useToast'; import { useToast } from '@fastgpt/web/hooks/useToast';
import { useSystemStore } from '@/web/common/system/useSystemStore'; import { useSystemStore } from '@/web/common/system/useSystemStore';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
...@@ -18,9 +17,10 @@ import { ...@@ -18,9 +17,10 @@ import {
removeFastGPTSem removeFastGPTSem
} from '@/web/support/marketing/utils'; } from '@/web/support/marketing/utils';
import { checkPasswordRule } from '@fastgpt/global/common/string/password'; import { checkPasswordRule } from '@fastgpt/global/common/string/password';
import type { LoginSuccessResponseType } from '@fastgpt/global/openapi/support/user/account/login/api';
interface Props { interface Props {
loginSuccess: (e: LoginSuccessResponse) => void; loginSuccess: (e: LoginSuccessResponseType) => void;
setPageType: Dispatch<`${LoginPageTypeEnum}`>; setPageType: Dispatch<`${LoginPageTypeEnum}`>;
} }
......
...@@ -10,7 +10,6 @@ import { ...@@ -10,7 +10,6 @@ import {
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { LoginPageTypeEnum } from '@/web/support/user/login/constants'; import { LoginPageTypeEnum } from '@/web/support/user/login/constants';
import { useSystemStore } from '@/web/common/system/useSystemStore'; import { useSystemStore } from '@/web/common/system/useSystemStore';
import type { LoginSuccessResponse } from '@/global/support/api/userRes';
import { useChatStore } from '@/web/core/chat/context/useChatStore'; import { useChatStore } from '@/web/core/chat/context/useChatStore';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import Script from 'next/script'; import Script from 'next/script';
...@@ -20,6 +19,7 @@ import { useTranslation } from 'next-i18next'; ...@@ -20,6 +19,7 @@ import { useTranslation } from 'next-i18next';
import LoginForm from '@/pageComponents/login/LoginForm/LoginForm'; import LoginForm from '@/pageComponents/login/LoginForm/LoginForm';
import { GET } from '@/web/common/api/request'; import { GET } from '@/web/common/api/request';
import { getDocPath } from '@/web/common/system/doc'; import { getDocPath } from '@/web/common/system/doc';
import type { LoginSuccessResponseType } from '@fastgpt/global/openapi/support/user/account/login/api';
const RegisterForm = dynamic(() => import('@/pageComponents/login/RegisterForm')); const RegisterForm = dynamic(() => import('@/pageComponents/login/RegisterForm'));
const ForgetPasswordForm = dynamic(() => import('@/pageComponents/login/ForgetPasswordForm')); const ForgetPasswordForm = dynamic(() => import('@/pageComponents/login/ForgetPasswordForm'));
...@@ -184,7 +184,7 @@ export const LoginContainer = ({ ...@@ -184,7 +184,7 @@ export const LoginContainer = ({
onSuccess onSuccess
}: { }: {
children?: React.ReactNode; children?: React.ReactNode;
onSuccess: (res: LoginSuccessResponse) => void; onSuccess: (res: LoginSuccessResponseType) => void;
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { feConfigs } = useSystemStore(); const { feConfigs } = useSystemStore();
...@@ -195,7 +195,7 @@ export const LoginContainer = ({ ...@@ -195,7 +195,7 @@ export const LoginContainer = ({
// login success handler // login success handler
const loginSuccess = useCallback( const loginSuccess = useCallback(
(res: LoginSuccessResponse) => { (res: LoginSuccessResponseType) => {
onSuccess?.(res); onSuccess?.(res);
}, },
[onSuccess] [onSuccess]
......
...@@ -3,17 +3,12 @@ import { NextAPI } from '@/service/middleware/entry'; ...@@ -3,17 +3,12 @@ import { NextAPI } from '@/service/middleware/entry';
import { checkPswExpired } from '@/service/support/user/account/password'; import { checkPswExpired } from '@/service/support/user/account/password';
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 type { CheckPswExpiredResponseType } from '@fastgpt/global/openapi/support/user/account/password/api';
export type getTimeQuery = {};
export type getTimeBody = {};
export type getTimeResponse = boolean;
async function handler( async function handler(
req: ApiRequestProps<getTimeBody, getTimeQuery>, req: ApiRequestProps,
res: ApiResponseType<getTimeResponse> res: ApiResponseType
): Promise<getTimeResponse> { ): Promise<CheckPswExpiredResponseType> {
const { userId } = await authCert({ req, authToken: true }); const { userId } = await authCert({ req, authToken: true });
const user = await MongoUser.findById(userId, 'passwordUpdateTime'); const user = await MongoUser.findById(userId, 'passwordUpdateTime');
......
import type { NextApiRequest, NextApiResponse } from 'next';
import { MongoUser } from '@fastgpt/service/support/user/schema'; import { MongoUser } from '@fastgpt/service/support/user/schema';
import { getUserDetail } from '@fastgpt/service/support/user/controller'; import { getUserDetail } from '@fastgpt/service/support/user/controller';
import type { PostLoginProps } from '@fastgpt/global/support/user/api';
import { UserStatusEnum } from '@fastgpt/global/support/user/constant'; import { UserStatusEnum } from '@fastgpt/global/support/user/constant';
import { NextAPI } from '@/service/middleware/entry'; import { NextAPI } from '@/service/middleware/entry';
import { useIPFrequencyLimit } from '@fastgpt/service/common/middle/reqFrequencyLimit'; import { useIPFrequencyLimit } from '@fastgpt/service/common/middle/reqFrequencyLimit';
import { pushTrack } from '@fastgpt/service/common/middle/tracks/utils'; 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'; import { UserErrEnum } from '@fastgpt/global/common/error/code/user';
import { addAuditLog } from '@fastgpt/service/support/user/audit/util'; import { addAuditLog } from '@fastgpt/service/support/user/audit/util';
import { AuditEventEnum } from '@fastgpt/global/support/user/audit/constants'; import { AuditEventEnum } from '@fastgpt/global/support/user/audit/constants';
...@@ -16,13 +13,18 @@ import { createUserSession } from '@fastgpt/service/support/user/session'; ...@@ -16,13 +13,18 @@ import { createUserSession } from '@fastgpt/service/support/user/session';
import requestIp from 'request-ip'; import requestIp from 'request-ip';
import { setCookie } from '@fastgpt/service/support/permission/auth/common'; import { setCookie } from '@fastgpt/service/support/permission/auth/common';
import { UserError } from '@fastgpt/global/common/error/utils'; import { UserError } from '@fastgpt/global/common/error/utils';
import {
LoginByPasswordBodySchema,
type LoginByPasswordBodyType,
type LoginSuccessResponseType
} from '@fastgpt/global/openapi/support/user/account/login/api';
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
async function handler(req: NextApiRequest, res: NextApiResponse) { async function handler(
const { username, password, code, language = 'zh-CN' } = req.body as PostLoginProps; req: ApiRequestProps<LoginByPasswordBodyType>,
res: ApiResponseType
if (!username || !password || !code) { ): Promise<LoginSuccessResponseType> {
return Promise.reject(CommonErrEnum.invalidParams); const { username, password, code, language } = LoginByPasswordBodySchema.parse(req.body);
}
// Auth prelogin code // Auth prelogin code
await authCode({ await authCode({
......
...@@ -4,25 +4,17 @@ import { UserAuthTypeEnum } from '@fastgpt/global/support/user/auth/constants'; ...@@ -4,25 +4,17 @@ import { UserAuthTypeEnum } from '@fastgpt/global/support/user/auth/constants';
import { getNanoid } from '@fastgpt/global/common/string/tools'; import { getNanoid } from '@fastgpt/global/common/string/tools';
import { addSeconds } from 'date-fns'; import { addSeconds } from 'date-fns';
import { addAuthCode } from '@fastgpt/service/support/user/auth/controller'; import { addAuthCode } from '@fastgpt/service/support/user/auth/controller';
import { UserError } from '@fastgpt/global/common/error/utils'; import {
PreLoginQuerySchema,
export type preLoginQuery = { type PreLoginQueryType,
username: string; type PreLoginResponseType
}; } from '@fastgpt/global/openapi/support/user/account/login/api';
export type preLoginBody = {};
export type preLoginResponse = { code: string };
async function handler( async function handler(
req: ApiRequestProps<preLoginBody, preLoginQuery>, req: ApiRequestProps<{}, PreLoginQueryType>,
res: ApiResponseType<any> res: ApiResponseType<any>
): Promise<preLoginResponse> { ): Promise<PreLoginResponseType> {
const { username } = req.query; const { username } = PreLoginQuerySchema.parse(req.query);
if (!username) {
return Promise.reject(new UserError('username is required'));
}
const code = getNanoid(6); const code = getNanoid(6);
......
...@@ -5,20 +5,16 @@ import { NextAPI } from '@/service/middleware/entry'; ...@@ -5,20 +5,16 @@ import { NextAPI } from '@/service/middleware/entry';
import { i18nT } from '@fastgpt/web/i18n/utils'; import { i18nT } from '@fastgpt/web/i18n/utils';
import { checkPswExpired } from '@/service/support/user/account/password'; import { checkPswExpired } from '@/service/support/user/account/password';
import { delUserAllSession } from '@fastgpt/service/support/user/session'; import { delUserAllSession } from '@fastgpt/service/support/user/session';
import {
export type resetExpiredPswQuery = {}; ResetExpiredPswBodySchema,
type ResetExpiredPswResponseType
export type resetExpiredPswBody = { } from '@fastgpt/global/openapi/support/user/account/password/api';
newPsw: string;
};
export type resetExpiredPswResponse = {};
async function resetExpiredPswHandler( async function resetExpiredPswHandler(
req: ApiRequestProps<resetExpiredPswBody, resetExpiredPswQuery>, req: ApiRequestProps,
res: ApiResponseType<resetExpiredPswResponse> res: ApiResponseType
): Promise<resetExpiredPswResponse> { ): Promise<ResetExpiredPswResponseType> {
const newPsw = req.body.newPsw; const { newPsw } = ResetExpiredPswBodySchema.parse(req.body);
const { userId, sessionId } = await authCert({ req, authToken: true }); const { userId, sessionId } = await authCert({ req, authToken: true });
const user = await MongoUser.findById(userId, 'passwordUpdateTime').lean(); const user = await MongoUser.findById(userId, 'passwordUpdateTime').lean();
......
...@@ -2,17 +2,10 @@ import { authCert } from '@fastgpt/service/support/permission/auth/common'; ...@@ -2,17 +2,10 @@ import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { getUserDetail } from '@fastgpt/service/support/user/controller'; import { getUserDetail } from '@fastgpt/service/support/user/controller';
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next'; import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
import { NextAPI } from '@/service/middleware/entry'; import { NextAPI } from '@/service/middleware/entry';
import { type UserType } from '@fastgpt/global/support/user/type';
import { pushTrack } from '@fastgpt/service/common/middle/tracks/utils'; import { pushTrack } from '@fastgpt/service/common/middle/tracks/utils';
import type { UserType } from '@fastgpt/global/support/user/type';
export type TokenLoginQuery = {}; async function handler(req: ApiRequestProps, _res: ApiResponseType): Promise<UserType> {
export type TokenLoginBody = {};
export type TokenLoginResponse = UserType;
async function handler(
req: ApiRequestProps<TokenLoginBody, TokenLoginQuery>,
_res: ApiResponseType<any>
): Promise<TokenLoginResponse> {
const { tmbId, userId, teamId } = await authCert({ req, authToken: true }); const { tmbId, userId, teamId } = await authCert({ req, authToken: true });
const user = await getUserDetail({ tmbId }); const user = await getUserDetail({ tmbId });
......
import type { NextApiRequest, NextApiResponse } from 'next';
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';
...@@ -8,12 +7,18 @@ import { NextAPI } from '@/service/middleware/entry'; ...@@ -8,12 +7,18 @@ import { NextAPI } from '@/service/middleware/entry';
import { addAuditLog } from '@fastgpt/service/support/user/audit/util'; import { addAuditLog } from '@fastgpt/service/support/user/audit/util';
import { AuditEventEnum } from '@fastgpt/global/support/user/audit/constants'; import { AuditEventEnum } from '@fastgpt/global/support/user/audit/constants';
import { delUserAllSession } from '@fastgpt/service/support/user/session'; import { delUserAllSession } from '@fastgpt/service/support/user/session';
async function handler(req: NextApiRequest, res: NextApiResponse<any>) { import {
const { oldPsw, newPsw } = req.body as { oldPsw: string; newPsw: string }; UpdatePasswordByOldBodySchema,
type UpdatePasswordByOldBodyType,
if (!oldPsw || !newPsw) { type UpdatePasswordByOldResponseType
return Promise.reject('Params is missing'); } from '@fastgpt/global/openapi/support/user/account/password/api';
} import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
async function handler(
req: ApiRequestProps<UpdatePasswordByOldBodyType>,
res: ApiResponseType<any>
): Promise<UpdatePasswordByOldResponseType> {
const { oldPsw, newPsw } = UpdatePasswordByOldBodySchema.parse(req.body);
const { tmbId, teamId, sessionId } = await authCert({ req, authToken: true }); const { tmbId, teamId, sessionId } = await authCert({ req, authToken: true });
const tmb = await MongoTeamMember.findById(tmbId); const tmb = await MongoTeamMember.findById(tmbId);
......
...@@ -23,10 +23,10 @@ import { ChatPageContext, ChatPageContextProvider } from '@/web/core/chat/contex ...@@ -23,10 +23,10 @@ import { ChatPageContext, ChatPageContextProvider } from '@/web/core/chat/contex
import ChatTeamApp from '@/pageComponents/chat/ChatTeamApp'; import ChatTeamApp from '@/pageComponents/chat/ChatTeamApp';
import ChatFavouriteApp from '@/pageComponents/chat/ChatFavouriteApp'; import ChatFavouriteApp from '@/pageComponents/chat/ChatFavouriteApp';
import { useUserStore } from '@/web/support/user/useUserStore'; import { useUserStore } from '@/web/support/user/useUserStore';
import type { LoginSuccessResponse } from '@/global/support/api/userRes';
import { MongoOutLink } from '@fastgpt/service/support/outLink/schema'; import { MongoOutLink } from '@fastgpt/service/support/outLink/schema';
import { getLogger, LogCategories } from '@fastgpt/service/common/logger'; import { getLogger, LogCategories } from '@fastgpt/service/common/logger';
import { PublishChannelEnum } from '@fastgpt/global/support/outLink/constant'; import { PublishChannelEnum } from '@fastgpt/global/support/outLink/constant';
import type { LoginSuccessResponseType } from '@fastgpt/global/openapi/support/user/account/login/api';
const logger = getLogger(LogCategories.MODULE.CHAT.ITEM); const logger = getLogger(LogCategories.MODULE.CHAT.ITEM);
...@@ -121,7 +121,7 @@ const ChatContent = (props: ChatPageProps) => { ...@@ -121,7 +121,7 @@ const ChatContent = (props: ChatPageProps) => {
}, [appId, chatId]); }, [appId, chatId]);
const loginSuccess = useCallback( const loginSuccess = useCallback(
async (res: LoginSuccessResponse) => { async (res: LoginSuccessResponseType) => {
setUserInfo(res.user); setUserInfo(res.user);
}, },
[setUserInfo] [setUserInfo]
......
import React, { useCallback, useEffect } from 'react'; import React, { useCallback, useEffect } from 'react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import type { LoginSuccessResponse } from '@/global/support/api/userRes';
import { useUserStore } from '@/web/support/user/useUserStore'; import { useUserStore } from '@/web/support/user/useUserStore';
import { clearToken } from '@/web/support/user/auth'; import { clearToken } from '@/web/support/user/auth';
import { postFastLogin } from '@/web/support/user/api'; import { postFastLogin } from '@/web/support/user/api';
...@@ -10,6 +9,7 @@ import { serviceSideProps } from '@/web/common/i18n/utils'; ...@@ -10,6 +9,7 @@ import { serviceSideProps } from '@/web/common/i18n/utils';
import { getErrText } from '@fastgpt/global/common/error/utils'; import { getErrText } from '@fastgpt/global/common/error/utils';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { validateRedirectUrl } from '@/web/common/utils/uri'; import { validateRedirectUrl } from '@/web/common/utils/uri';
import type { LoginSuccessResponseType } from '@fastgpt/global/openapi/support/user/account/login/api';
const FastLogin = ({ const FastLogin = ({
code, code,
...@@ -25,7 +25,7 @@ const FastLogin = ({ ...@@ -25,7 +25,7 @@ const FastLogin = ({
const { toast } = useToast(); const { toast } = useToast();
const { t } = useTranslation(); const { t } = useTranslation();
const loginSuccess = useCallback( const loginSuccess = useCallback(
(res: LoginSuccessResponse) => { (res: LoginSuccessResponseType) => {
setUserInfo(res.user); setUserInfo(res.user);
setTimeout(() => { setTimeout(() => {
......
...@@ -5,12 +5,12 @@ import { clearToken } from '@/web/support/user/auth'; ...@@ -5,12 +5,12 @@ import { clearToken } from '@/web/support/user/auth';
import { useMount } from 'ahooks'; import { useMount } from 'ahooks';
import LoginModal from '@/pageComponents/login/LoginModal'; import LoginModal from '@/pageComponents/login/LoginModal';
import { postAcceptInvitationLink } from '@/web/support/user/team/api'; import { postAcceptInvitationLink } from '@/web/support/user/team/api';
import type { LoginSuccessResponse } from '@/global/support/api/userRes';
import { useToast } from '@fastgpt/web/hooks/useToast'; import { useToast } from '@fastgpt/web/hooks/useToast';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { useUserStore } from '@/web/support/user/useUserStore'; import { useUserStore } from '@/web/support/user/useUserStore';
import { subRoute } from '@fastgpt/web/common/system/utils'; import { subRoute } from '@fastgpt/web/common/system/utils';
import { validateRedirectUrl } from '@/web/common/utils/uri'; import { validateRedirectUrl } from '@/web/common/utils/uri';
import type { LoginSuccessResponseType } from '@fastgpt/global/openapi/support/user/account/login/api';
const Login = () => { const Login = () => {
const router = useRouter(); const router = useRouter();
...@@ -20,7 +20,7 @@ const Login = () => { ...@@ -20,7 +20,7 @@ const Login = () => {
const { setUserInfo } = useUserStore(); const { setUserInfo } = useUserStore();
const loginSuccess = useCallback( const loginSuccess = useCallback(
async (res: LoginSuccessResponse) => { async (res: LoginSuccessResponseType) => {
setUserInfo(res.user); setUserInfo(res.user);
const decodeLastRoute = validateRedirectUrl(lastRoute); const decodeLastRoute = validateRedirectUrl(lastRoute);
......
import React, { useCallback, useEffect } from 'react'; import React, { useCallback, useEffect } from 'react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useSystemStore } from '@/web/common/system/useSystemStore'; import { useSystemStore } from '@/web/common/system/useSystemStore';
import type { LoginSuccessResponse } from '@/global/support/api/userRes';
import { useUserStore } from '@/web/support/user/useUserStore'; import { useUserStore } from '@/web/support/user/useUserStore';
import { clearToken } from '@/web/support/user/auth'; import { clearToken } from '@/web/support/user/auth';
import { oauthLogin } from '@/web/support/user/api'; import { oauthLogin } from '@/web/support/user/api';
...@@ -23,6 +22,7 @@ import { postAcceptInvitationLink } from '@/web/support/user/team/api'; ...@@ -23,6 +22,7 @@ import { postAcceptInvitationLink } from '@/web/support/user/team/api';
import { retryFn } from '@fastgpt/global/common/system/utils'; import { retryFn } from '@fastgpt/global/common/system/utils';
import type { LangEnum } from '@fastgpt/global/common/i18n/type'; import type { LangEnum } from '@fastgpt/global/common/i18n/type';
import { validateRedirectUrl } from '@/web/common/utils/uri'; import { validateRedirectUrl } from '@/web/common/utils/uri';
import type { LoginSuccessResponseType } from '@fastgpt/global/openapi/support/user/account/login/api';
let isOauthLogging = false; let isOauthLogging = false;
...@@ -40,7 +40,7 @@ const provider = () => { ...@@ -40,7 +40,7 @@ const provider = () => {
const errorRedirectPage = lastRoute.startsWith('/chat') ? lastRoute : '/login'; const errorRedirectPage = lastRoute.startsWith('/chat') ? lastRoute : '/login';
const loginSuccess = useCallback( const loginSuccess = useCallback(
async (res: LoginSuccessResponse) => { async (res: LoginSuccessResponseType) => {
const decodeLastRoute = validateRedirectUrl(lastRoute); const decodeLastRoute = validateRedirectUrl(lastRoute);
setUserInfo(res.user); setUserInfo(res.user);
......
...@@ -22,7 +22,7 @@ import { ...@@ -22,7 +22,7 @@ import {
} from '@fastgpt/global/core/ai/provider'; } from '@fastgpt/global/core/ai/provider';
import { getMyModels, getOperationalAd } from './api'; import { getMyModels, getOperationalAd } from './api';
type LoginStoreType = { provider: `${OAuthEnum}`; lastRoute: string; state: string }; type LoginStoreType = { provider: OAuthEnum; lastRoute: string; state: string };
export type NotSufficientModalType = export type NotSufficientModalType =
| TeamErrEnum.datasetSizeNotEnough | TeamErrEnum.datasetSizeNotEnough
......
import { GET, POST, PUT } from '@/web/common/api/request'; import { GET, POST, PUT } from '@/web/common/api/request';
import { hashStr } from '@fastgpt/global/common/string/tools'; import { hashStr } from '@fastgpt/global/common/string/tools';
import type { LoginSuccessResponse } from '@/global/support/api/userRes';
import type { UserAuthTypeEnum } from '@fastgpt/global/support/user/auth/constants'; import type { UserAuthTypeEnum } from '@fastgpt/global/support/user/auth/constants';
import type { UserUpdateParams } from '@/types/user'; import type { UserUpdateParams } from '@/types/user';
import type { UserType } from '@fastgpt/global/support/user/type'; import type { UserType } from '@fastgpt/global/support/user/type';
import type { SearchResult } from '@fastgpt/global/support/user/api';
import type { import type {
FastLoginProps, PreLoginResponseType,
OauthLoginProps, LoginByPasswordBodyType,
PostLoginProps, OauthLoginBodyType,
SearchResult FastLoginBodyType,
} from '@fastgpt/global/support/user/api'; WxLoginBodyType,
import type { GetWXLoginQRResponseType
AccountRegisterBody, } from '@fastgpt/global/openapi/support/user/account/login/api';
GetWXLoginQRResponse import type { UpdatePasswordByOldBodyType } from '@fastgpt/global/openapi/support/user/account/password/api';
} from '@fastgpt/global/support/user/login/api'; import type { AccountRegisterBodyType } from '@fastgpt/global/openapi/support/user/account/register/api';
import type { preLoginResponse } from '@/pages/api/support/user/account/preLogin';
import type { WxLoginProps } from '@fastgpt/global/support/user/api';
import type { LangEnum } from '@fastgpt/global/common/i18n/type'; import type { LangEnum } from '@fastgpt/global/common/i18n/type';
import type { LoginSuccessResponseType } from '@fastgpt/global/openapi/support/user/account/login/api';
/* ===== Auth code ===== */
export const sendAuthCode = (data: { export const sendAuthCode = (data: {
username: string; username: string;
type: `${UserAuthTypeEnum}`; type: `${UserAuthTypeEnum}`;
...@@ -25,16 +25,37 @@ export const sendAuthCode = (data: { ...@@ -25,16 +25,37 @@ export const sendAuthCode = (data: {
captcha: string; captcha: string;
lang: `${LangEnum}`; lang: `${LangEnum}`;
}) => POST(`/proApi/support/user/inform/sendAuthCode`, data); }) => POST(`/proApi/support/user/inform/sendAuthCode`, data);
export const getCaptchaPic = (username: string) =>
GET<{
captchaImage: string;
}>('/proApi/support/user/account/captcha/getImgCaptcha', { username });
/* ===== login ===== */
export const getPreLogin = (username: string) =>
GET<PreLoginResponseType>('/support/user/account/preLogin', { username });
export const getTokenLogin = () => export const getTokenLogin = () =>
GET<UserType>('/support/user/account/tokenLogin', {}, { maxQuantity: 1 }); GET<UserType>('/support/user/account/tokenLogin', {}, { maxQuantity: 1 });
export const oauthLogin = (params: OauthLoginProps) => export const oauthLogin = (params: OauthLoginBodyType) =>
POST<LoginSuccessResponse>('/proApi/support/user/account/login/oauth', params); POST<LoginSuccessResponseType>('/proApi/support/user/account/login/oauth', params);
export const postFastLogin = (params: FastLoginProps) => export const postFastLogin = (params: FastLoginBodyType) =>
POST<LoginSuccessResponse>('/proApi/support/user/account/login/fastLogin', params); POST<LoginSuccessResponseType>('/proApi/support/user/account/login/fastLogin', params);
export const ssoLogin = (params: any) => export const ssoLogin = (params: any) =>
GET<LoginSuccessResponse>('/proApi/support/user/account/sso', params); GET<LoginSuccessResponseType>('/proApi/support/user/account/sso', params);
export const postLogin = ({ password, ...props }: LoginByPasswordBodyType) =>
POST<LoginSuccessResponseType>('/support/user/account/loginByPassword', {
...props,
password: hashStr(password)
});
// wx login
export const getWXLoginQR = () =>
GET<GetWXLoginQRResponseType>('/proApi/support/user/account/login/wx/getQR');
export const getWXLoginResult = (params: WxLoginBodyType) =>
POST<LoginSuccessResponseType>(`/proApi/support/user/account/login/wx/getResult`, params);
export const loginOut = () => GET('/support/user/account/loginout');
/* ===== register ===== */
export const postRegister = ({ export const postRegister = ({
username, username,
password, password,
...@@ -43,8 +64,8 @@ export const postRegister = ({ ...@@ -43,8 +64,8 @@ export const postRegister = ({
bd_vid, bd_vid,
msclkid, msclkid,
fastgpt_sem fastgpt_sem
}: AccountRegisterBody) => }: AccountRegisterBodyType) =>
POST<LoginSuccessResponse>(`/proApi/support/user/account/register/emailAndPhone`, { POST<LoginSuccessResponseType>(`/proApi/support/user/account/register/emailAndPhone`, {
username, username,
code, code,
inviterId, inviterId,
...@@ -54,6 +75,7 @@ export const postRegister = ({ ...@@ -54,6 +75,7 @@ export const postRegister = ({
password: hashStr(password) password: hashStr(password)
}); });
/* ===== password ===== */
export const postFindPassword = ({ export const postFindPassword = ({
username, username,
code, code,
...@@ -63,57 +85,33 @@ export const postFindPassword = ({ ...@@ -63,57 +85,33 @@ export const postFindPassword = ({
code: string; code: string;
password: string; password: string;
}) => }) =>
POST<LoginSuccessResponse>(`/proApi/support/user/account/password/updateByCode`, { POST<LoginSuccessResponseType>(`/proApi/support/user/account/password/updateByCode`, {
username, username,
code, code,
password: hashStr(password) password: hashStr(password)
}); });
export const updatePasswordByOld = ({ oldPsw, newPsw }: UpdatePasswordByOldBodyType) =>
export const updatePasswordByOld = ({ oldPsw, newPsw }: { oldPsw: string; newPsw: string }) =>
POST('/support/user/account/updatePasswordByOld', { POST('/support/user/account/updatePasswordByOld', {
oldPsw: hashStr(oldPsw), oldPsw: hashStr(oldPsw),
newPsw: hashStr(newPsw) newPsw: hashStr(newPsw)
}); });
export const resetPassword = (newPsw: string) => export const resetPassword = (newPsw: string) =>
POST('/support/user/account/resetExpiredPsw', { POST('/support/user/account/resetExpiredPsw', {
newPsw: hashStr(newPsw) newPsw: hashStr(newPsw)
}); });
// Check the whether password has expired
/* Check the whether password has expired */
export const getCheckPswExpired = () => GET<boolean>('/support/user/account/checkPswExpired'); export const getCheckPswExpired = () => GET<boolean>('/support/user/account/checkPswExpired');
/* ===== notification account ===== */
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);
export const updateContact = (data: { contact: string; verifyCode: string }) => { export const updateContact = (data: { contact: string; verifyCode: string }) => {
return PUT('/proApi/support/user/account/updateContact', data); return PUT('/proApi/support/user/account/updateContact', data);
}; };
export const postLogin = ({ password, ...props }: PostLoginProps) => /* ===== user info ===== */
POST<LoginSuccessResponse>('/support/user/account/loginByPassword', {
...props,
password: hashStr(password)
});
export const loginOut = () => GET('/support/user/account/loginout');
export const putUserInfo = (data: UserUpdateParams) => PUT('/support/user/account/update', data); export const putUserInfo = (data: UserUpdateParams) => PUT('/support/user/account/update', data);
export const getWXLoginQR = () =>
GET<GetWXLoginQRResponse>('/proApi/support/user/account/login/wx/getQR');
export const getWXLoginResult = (params: WxLoginProps) =>
POST<LoginSuccessResponse>(`/proApi/support/user/account/login/wx/getResult`, params);
export const getCaptchaPic = (username: string) =>
GET<{
captchaImage: string;
}>('/proApi/support/user/account/captcha/getImgCaptcha', { username });
export const getPreLogin = (username: string) =>
GET<preLoginResponse>('/support/user/account/preLogin', { username });
export const postSyncMembers = () => POST('/proApi/support/user/sync'); export const postSyncMembers = () => POST('/proApi/support/user/sync');
export const GetSearchUserGroupOrg = ( export const GetSearchUserGroupOrg = (
......
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import * as checkPswExpiredApi from '@/pages/api/support/user/account/checkPswExpired';
import { MongoUser } from '@fastgpt/service/support/user/schema';
import { MongoTeam } from '@fastgpt/service/support/user/team/teamSchema';
import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema';
import { UserStatusEnum } from '@fastgpt/global/support/user/constant';
import { initTeamFreePlan } from '@fastgpt/service/support/wallet/sub/utils';
import { Call } from '@test/utils/request';
describe('checkPswExpired API', () => {
let testUser: any;
let testTeam: any;
let testTmb: any;
const originalEnv = process.env.PASSWORD_EXPIRED_MONTH;
beforeEach(async () => {
testUser = await MongoUser.create({
username: 'testuser',
password: 'testpassword',
status: UserStatusEnum.active,
passwordUpdateTime: new Date()
});
testTeam = await MongoTeam.create({
name: 'Test Team',
ownerId: testUser._id
});
await initTeamFreePlan({ teamId: String(testTeam._id) });
testTmb = await MongoTeamMember.create({
teamId: testTeam._id,
userId: testUser._id,
status: 'active',
role: 'owner'
});
vi.clearAllMocks();
});
afterEach(() => {
process.env.PASSWORD_EXPIRED_MONTH = originalEnv;
});
it('should return false when PASSWORD_EXPIRED_MONTH is not set', async () => {
delete process.env.PASSWORD_EXPIRED_MONTH;
const res = await Call(checkPswExpiredApi.default, {
auth: {
userId: String(testUser._id),
teamId: String(testTeam._id),
tmbId: String(testTmb._id),
isRoot: false,
sessionId: 'session123'
} as any
});
expect(res.code).toBe(200);
expect(res.data).toBe(false);
});
it('should return false when PASSWORD_EXPIRED_MONTH=0', async () => {
process.env.PASSWORD_EXPIRED_MONTH = '0';
const res = await Call(checkPswExpiredApi.default, {
auth: {
userId: String(testUser._id),
teamId: String(testTeam._id),
tmbId: String(testTmb._id),
isRoot: false,
sessionId: 'session123'
} as any
});
expect(res.code).toBe(200);
expect(res.data).toBe(false);
});
it('should return false when password was updated recently', async () => {
process.env.PASSWORD_EXPIRED_MONTH = '3';
// Update password time to now
await MongoUser.findByIdAndUpdate(testUser._id, {
passwordUpdateTime: new Date()
});
const res = await Call(checkPswExpiredApi.default, {
auth: {
userId: String(testUser._id),
teamId: String(testTeam._id),
tmbId: String(testTmb._id),
isRoot: false,
sessionId: 'session123'
} as any
});
expect(res.code).toBe(200);
expect(res.data).toBe(false);
});
it('should return true when password has expired (update time older than expiry period)', async () => {
process.env.PASSWORD_EXPIRED_MONTH = '1';
// Set password update time to 2 months ago
const twoMonthsAgo = new Date();
twoMonthsAgo.setMonth(twoMonthsAgo.getMonth() - 2);
await MongoUser.findByIdAndUpdate(testUser._id, {
passwordUpdateTime: twoMonthsAgo
});
const res = await Call(checkPswExpiredApi.default, {
auth: {
userId: String(testUser._id),
teamId: String(testTeam._id),
tmbId: String(testTmb._id),
isRoot: false,
sessionId: 'session123'
} as any
});
expect(res.code).toBe(200);
expect(res.data).toBe(true);
});
it('should return true when passwordUpdateTime is not set and env is configured', async () => {
process.env.PASSWORD_EXPIRED_MONTH = '3';
// Remove passwordUpdateTime
await MongoUser.findByIdAndUpdate(testUser._id, {
$unset: { passwordUpdateTime: '' }
});
const res = await Call(checkPswExpiredApi.default, {
auth: {
userId: String(testUser._id),
teamId: String(testTeam._id),
tmbId: String(testTmb._id),
isRoot: false,
sessionId: 'session123'
} as any
});
expect(res.code).toBe(200);
expect(res.data).toBe(true);
});
it('should return false when user is not found', async () => {
const nonExistentId = '000000000000000000000001';
const res = await Call(checkPswExpiredApi.default, {
auth: {
userId: nonExistentId,
teamId: String(testTeam._id),
tmbId: String(testTmb._id),
isRoot: false,
sessionId: 'session123'
} as any
});
expect(res.code).toBe(200);
expect(res.data).toBe(false);
});
it('should reject request without authentication', async () => {
const res = await Call(checkPswExpiredApi.default, {});
expect(res.code).toBe(500);
});
});
import { describe, it, expect, vi, beforeEach } from 'vitest';
import * as loginoutApi from '@/pages/api/support/user/account/loginout';
import { MongoUser } from '@fastgpt/service/support/user/schema';
import { MongoTeam } from '@fastgpt/service/support/user/team/teamSchema';
import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema';
import { UserStatusEnum } from '@fastgpt/global/support/user/constant';
import { initTeamFreePlan } from '@fastgpt/service/support/wallet/sub/utils';
import { Call } from '@test/utils/request';
describe('loginout API', () => {
let testUser: any;
let testTeam: any;
let testTmb: any;
beforeEach(async () => {
testUser = await MongoUser.create({
username: 'testuser',
password: 'testpassword',
status: UserStatusEnum.active
});
testTeam = await MongoTeam.create({
name: 'Test Team',
ownerId: testUser._id
});
await initTeamFreePlan({ teamId: String(testTeam._id) });
testTmb = await MongoTeamMember.create({
teamId: testTeam._id,
userId: testUser._id,
status: 'active',
role: 'owner'
});
vi.clearAllMocks();
});
it('should logout successfully with valid auth', async () => {
const res = await Call(loginoutApi.default, {
auth: {
userId: String(testUser._id),
teamId: String(testTeam._id),
tmbId: String(testTmb._id),
isRoot: false,
sessionId: 'session123'
} as any
});
expect(res.code).toBe(200);
});
it('should succeed even when unauthenticated (auth errors are caught)', async () => {
// loginout catches auth errors and always calls clearCookie
const res = await Call(loginoutApi.default, {});
expect(res.code).toBe(200);
});
});
import { describe, it, expect, beforeEach } from 'vitest';
import * as preLoginApi from '@/pages/api/support/user/account/preLogin';
import { MongoUserAuth } from '@fastgpt/service/support/user/auth/schema';
import { UserAuthTypeEnum } from '@fastgpt/global/support/user/auth/constants';
import { Call } from '@test/utils/request';
describe('preLogin API', () => {
beforeEach(async () => {
await MongoUserAuth.deleteMany({});
});
it('should return a 6-char verification code for valid username', async () => {
const res = await Call(preLoginApi.default, {
query: { username: 'testuser' }
});
expect(res.code).toBe(200);
expect(res.data).toBeDefined();
expect(typeof res.data.code).toBe('string');
expect(res.data.code.length).toBe(6);
});
it('should store the verification code in database', async () => {
const res = await Call(preLoginApi.default, {
query: { username: 'testuser' }
});
expect(res.code).toBe(200);
const record = await MongoUserAuth.findOne({
key: 'testuser',
type: UserAuthTypeEnum.login
});
expect(record).toBeDefined();
expect(record?.code).toBe(res.data.code);
});
it('should generate different codes for different usernames', async () => {
const res1 = await Call(preLoginApi.default, {
query: { username: 'user1' }
});
const res2 = await Call(preLoginApi.default, {
query: { username: 'user2' }
});
expect(res1.code).toBe(200);
expect(res2.code).toBe(200);
const record1 = await MongoUserAuth.findOne({ key: 'user1', type: UserAuthTypeEnum.login });
const record2 = await MongoUserAuth.findOne({ key: 'user2', type: UserAuthTypeEnum.login });
expect(record1?.key).toBe('user1');
expect(record2?.key).toBe('user2');
});
it('should overwrite previous code for the same username', async () => {
await Call(preLoginApi.default, { query: { username: 'testuser' } });
const res2 = await Call(preLoginApi.default, { query: { username: 'testuser' } });
const records = await MongoUserAuth.find({
key: 'testuser',
type: UserAuthTypeEnum.login
});
// upsert: only one record per key+type
expect(records.length).toBe(1);
expect(records[0].code).toBe(res2.data.code);
});
it('should set code expiredTime about 30 seconds from now', async () => {
const before = new Date();
const res = await Call(preLoginApi.default, {
query: { username: 'testuser' }
});
const after = new Date();
expect(res.code).toBe(200);
const record = await MongoUserAuth.findOne({ key: 'testuser', type: UserAuthTypeEnum.login });
expect(record?.expiredTime).toBeDefined();
const expiredTime = new Date(record!.expiredTime!).getTime();
// Should expire ~30 seconds from now (allow ±2s for test execution)
expect(expiredTime).toBeGreaterThanOrEqual(before.getTime() + 28000);
expect(expiredTime).toBeLessThanOrEqual(after.getTime() + 32000);
});
it('should reject when username is missing', async () => {
const res = await Call(preLoginApi.default, {
query: {}
});
expect(res.code).toBe(500);
});
it('should handle root username', async () => {
const res = await Call(preLoginApi.default, {
query: { username: 'root' }
});
expect(res.code).toBe(200);
expect(res.data.code).toBeDefined();
expect(res.data.code.length).toBe(6);
});
});
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import * as resetExpiredPswApi from '@/pages/api/support/user/account/resetExpiredPsw';
import { MongoUser } from '@fastgpt/service/support/user/schema';
import { MongoTeam } from '@fastgpt/service/support/user/team/teamSchema';
import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema';
import { UserStatusEnum } from '@fastgpt/global/support/user/constant';
import { initTeamFreePlan } from '@fastgpt/service/support/wallet/sub/utils';
import type { ResetExpiredPswBodyType } from '@fastgpt/global/openapi/support/user/account/password/api';
import { Call } from '@test/utils/request';
describe('resetExpiredPsw API', () => {
let testUser: any;
let testTeam: any;
let testTmb: any;
const originalEnv = process.env.PASSWORD_EXPIRED_MONTH;
beforeEach(async () => {
testUser = await MongoUser.create({
username: 'testuser',
password: 'oldpassword',
status: UserStatusEnum.active
});
testTeam = await MongoTeam.create({
name: 'Test Team',
ownerId: testUser._id
});
await initTeamFreePlan({ teamId: String(testTeam._id) });
testTmb = await MongoTeamMember.create({
teamId: testTeam._id,
userId: testUser._id,
status: 'active',
role: 'owner'
});
vi.clearAllMocks();
});
afterEach(() => {
process.env.PASSWORD_EXPIRED_MONTH = originalEnv;
});
it('should successfully reset password when expired', async () => {
process.env.PASSWORD_EXPIRED_MONTH = '1';
// Set password update time to 2 months ago (expired)
const twoMonthsAgo = new Date();
twoMonthsAgo.setMonth(twoMonthsAgo.getMonth() - 2);
await MongoUser.findByIdAndUpdate(testUser._id, {
passwordUpdateTime: twoMonthsAgo
});
const res = await Call<ResetExpiredPswBodyType, {}, any>(resetExpiredPswApi.default, {
body: { newPsw: 'newhashedpassword' },
auth: {
userId: String(testUser._id),
teamId: String(testTeam._id),
tmbId: String(testTmb._id),
isRoot: false,
sessionId: 'session123'
} as any
});
expect(res.code).toBe(200);
// Verify password was updated
const updatedUser = await MongoUser.findById(testUser._id).select(
'+password +passwordUpdateTime'
);
expect(updatedUser?.password).toBeDefined();
expect(updatedUser?.passwordUpdateTime).toBeDefined();
const newUpdateTime = new Date(updatedUser!.passwordUpdateTime!).getTime();
expect(newUpdateTime).toBeGreaterThan(twoMonthsAgo.getTime());
});
it('should reject when password is not expired (PASSWORD_EXPIRED_MONTH not set)', async () => {
delete process.env.PASSWORD_EXPIRED_MONTH;
await MongoUser.findByIdAndUpdate(testUser._id, {
passwordUpdateTime: new Date()
});
const res = await Call<ResetExpiredPswBodyType, {}, any>(resetExpiredPswApi.default, {
body: { newPsw: 'newhashedpassword' },
auth: {
userId: String(testUser._id),
teamId: String(testTeam._id),
tmbId: String(testTmb._id),
isRoot: false,
sessionId: 'session123'
} as any
});
expect(res.code).toBe(500);
expect(res.error).toBeDefined();
});
it('should reject when password is not expired (still within expiry period)', async () => {
process.env.PASSWORD_EXPIRED_MONTH = '3';
// Update password just now — not expired
await MongoUser.findByIdAndUpdate(testUser._id, {
passwordUpdateTime: new Date()
});
const res = await Call<ResetExpiredPswBodyType, {}, any>(resetExpiredPswApi.default, {
body: { newPsw: 'newhashedpassword' },
auth: {
userId: String(testUser._id),
teamId: String(testTeam._id),
tmbId: String(testTmb._id),
isRoot: false,
sessionId: 'session123'
} as any
});
expect(res.code).toBe(500);
});
it('should reject when newPsw is missing', async () => {
process.env.PASSWORD_EXPIRED_MONTH = '1';
const res = await Call<any, {}, any>(resetExpiredPswApi.default, {
body: {},
auth: {
userId: String(testUser._id),
teamId: String(testTeam._id),
tmbId: String(testTmb._id),
isRoot: false,
sessionId: 'session123'
} as any
});
expect(res.code).toBe(500);
});
it('should reject when user is not found', async () => {
process.env.PASSWORD_EXPIRED_MONTH = '1';
const nonExistentId = '000000000000000000000001';
const res = await Call<ResetExpiredPswBodyType, {}, any>(resetExpiredPswApi.default, {
body: { newPsw: 'newhashedpassword' },
auth: {
userId: nonExistentId,
teamId: String(testTeam._id),
tmbId: String(testTmb._id),
isRoot: false,
sessionId: 'session123'
} as any
});
expect(res.code).toBe(500);
expect(res.error).toBe('The password has not expired');
});
it('should reject request without authentication', async () => {
const res = await Call<ResetExpiredPswBodyType, {}, any>(resetExpiredPswApi.default, {
body: { newPsw: 'newhashedpassword' }
});
expect(res.code).toBe(500);
});
it('should reject newPsw as non-string (injection guard)', async () => {
process.env.PASSWORD_EXPIRED_MONTH = '1';
const twoMonthsAgo = new Date();
twoMonthsAgo.setMonth(twoMonthsAgo.getMonth() - 2);
await MongoUser.findByIdAndUpdate(testUser._id, {
passwordUpdateTime: twoMonthsAgo
});
const res = await Call<any, {}, any>(resetExpiredPswApi.default, {
body: { newPsw: { $ne: '' } },
auth: {
userId: String(testUser._id),
teamId: String(testTeam._id),
tmbId: String(testTmb._id),
isRoot: false,
sessionId: 'session123'
} as any
});
expect(res.code).toBe(500);
});
});
import { describe, it, expect, vi, beforeEach } from 'vitest';
import * as tokenLoginApi from '@/pages/api/support/user/account/tokenLogin';
import { MongoUser } from '@fastgpt/service/support/user/schema';
import { MongoTeam } from '@fastgpt/service/support/user/team/teamSchema';
import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema';
import { UserStatusEnum } from '@fastgpt/global/support/user/constant';
import { pushTrack } from '@fastgpt/service/common/middle/tracks/utils';
import { initTeamFreePlan } from '@fastgpt/service/support/wallet/sub/utils';
import { Call } from '@test/utils/request';
describe('tokenLogin API', () => {
let testUser: any;
let testTeam: any;
let testTmb: any;
beforeEach(async () => {
testUser = await MongoUser.create({
username: 'testuser',
password: 'testpassword',
status: UserStatusEnum.active
});
testTeam = await MongoTeam.create({
name: 'Test Team',
ownerId: testUser._id
});
await initTeamFreePlan({ teamId: String(testTeam._id) });
testTmb = await MongoTeamMember.create({
teamId: testTeam._id,
userId: testUser._id,
status: 'active',
role: 'owner'
});
await MongoUser.findByIdAndUpdate(testUser._id, {
lastLoginTmbId: testTmb._id
});
vi.clearAllMocks();
});
it('should return user detail on valid token', async () => {
const res = await Call(tokenLoginApi.default, {
auth: {
userId: String(testUser._id),
teamId: String(testTeam._id),
tmbId: String(testTmb._id),
isRoot: false,
sessionId: 'session123'
} as any
});
expect(res.code).toBe(200);
expect(res.data).toBeDefined();
expect(res.data.team).toBeDefined();
expect(res.data.team.teamId).toBe(String(testTeam._id));
expect(res.data.team.tmbId).toBe(String(testTmb._id));
});
it('should call pushTrack.dailyUserActive', async () => {
await Call(tokenLoginApi.default, {
auth: {
userId: String(testUser._id),
teamId: String(testTeam._id),
tmbId: String(testTmb._id),
isRoot: false,
sessionId: 'session123'
} as any
});
expect(pushTrack.dailyUserActive).toHaveBeenCalledWith({
uid: String(testUser._id),
teamId: String(testTeam._id),
tmbId: String(testTmb._id)
});
});
it('should mask openaiAccount key but keep baseUrl', async () => {
await MongoTeamMember.findByIdAndUpdate(testTmb._id, {
openaiAccount: { key: 'sk-secret-key', baseUrl: 'https://api.openai.com' }
});
const res = await Call(tokenLoginApi.default, {
auth: {
userId: String(testUser._id),
teamId: String(testTeam._id),
tmbId: String(testTmb._id),
isRoot: false,
sessionId: 'session123'
} as any
});
expect(res.code).toBe(200);
if (res.data.team.openaiAccount) {
expect(res.data.team.openaiAccount.key).toBe('');
expect(res.data.team.openaiAccount.baseUrl).toBe('https://api.openai.com');
}
});
it('should mask all values in externalWorkflowVariables', async () => {
await MongoTeamMember.findByIdAndUpdate(testTmb._id, {
externalWorkflowVariables: { SECRET: 'top-secret', API_KEY: 'sk-123' }
});
const res = await Call(tokenLoginApi.default, {
auth: {
userId: String(testUser._id),
teamId: String(testTeam._id),
tmbId: String(testTmb._id),
isRoot: false,
sessionId: 'session123'
} as any
});
expect(res.code).toBe(200);
if (res.data.team.externalWorkflowVariables) {
Object.values(res.data.team.externalWorkflowVariables).forEach((val) => {
expect(val).toBe('');
});
}
});
it('should reject request without authentication', async () => {
const res = await Call(tokenLoginApi.default, {});
expect(res.code).toBe(500);
});
});
import { describe, it, expect, vi, beforeEach } from 'vitest';
import * as updateApi from '@/pages/api/support/user/account/update';
import { MongoUser } from '@fastgpt/service/support/user/schema';
import { MongoTeam } from '@fastgpt/service/support/user/team/teamSchema';
import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema';
import { UserStatusEnum } from '@fastgpt/global/support/user/constant';
import { initTeamFreePlan } from '@fastgpt/service/support/wallet/sub/utils';
import { Call } from '@test/utils/request';
describe('update (user account) API', () => {
let testUser: any;
let testTeam: any;
let testTmb: any;
beforeEach(async () => {
testUser = await MongoUser.create({
username: 'testuser',
password: 'testpassword',
status: UserStatusEnum.active,
language: 'zh-CN',
timezone: 'Asia/Shanghai'
});
testTeam = await MongoTeam.create({
name: 'Test Team',
ownerId: testUser._id
});
await initTeamFreePlan({ teamId: String(testTeam._id) });
testTmb = await MongoTeamMember.create({
teamId: testTeam._id,
userId: testUser._id,
status: 'active',
role: 'owner'
});
vi.clearAllMocks();
});
const makeAuth = (user: any, team: any, tmb: any) => ({
userId: String(user._id),
teamId: String(team._id),
tmbId: String(tmb._id),
isRoot: false,
sessionId: 'session123'
});
it('should update language successfully', async () => {
const res = await Call(updateApi.default, {
body: { language: 'en' },
auth: makeAuth(testUser, testTeam, testTmb) as any
});
expect(res.code).toBe(200);
const updatedUser = await MongoUser.findById(testUser._id);
expect(updatedUser?.language).toBe('en');
});
it('should update timezone successfully', async () => {
const res = await Call(updateApi.default, {
body: { timezone: 'America/New_York' },
auth: makeAuth(testUser, testTeam, testTmb) as any
});
expect(res.code).toBe(200);
const updatedUser = await MongoUser.findById(testUser._id);
expect(updatedUser?.timezone).toBe('America/New_York');
});
it('should update both language and timezone simultaneously', async () => {
const res = await Call(updateApi.default, {
body: { language: 'zh-Hant', timezone: 'Asia/Tokyo' },
auth: makeAuth(testUser, testTeam, testTmb) as any
});
expect(res.code).toBe(200);
const updatedUser = await MongoUser.findById(testUser._id);
expect(updatedUser?.language).toBe('zh-Hant');
expect(updatedUser?.timezone).toBe('Asia/Tokyo');
});
it('should update avatar on team member', async () => {
const newAvatar = '/avatar/test-avatar.png';
const res = await Call(updateApi.default, {
body: { avatar: newAvatar },
auth: makeAuth(testUser, testTeam, testTmb) as any
});
expect(res.code).toBe(200);
const updatedTmb = await MongoTeamMember.findById(testTmb._id);
expect(updatedTmb?.avatar).toBe(newAvatar);
});
it('should return empty object on success', async () => {
const res = await Call(updateApi.default, {
body: { language: 'en' },
auth: makeAuth(testUser, testTeam, testTmb) as any
});
expect(res.code).toBe(200);
expect(res.data).toEqual({});
});
it('should handle empty body without error', async () => {
const res = await Call(updateApi.default, {
body: {},
auth: makeAuth(testUser, testTeam, testTmb) as any
});
expect(res.code).toBe(200);
// Nothing should change
const updatedUser = await MongoUser.findById(testUser._id);
expect(updatedUser?.language).toBe('zh-CN');
expect(updatedUser?.timezone).toBe('Asia/Shanghai');
});
it('should reject request without authentication', async () => {
const res = await Call(updateApi.default, {
body: { language: 'en' }
});
expect(res.code).toBe(500);
});
});
import { describe, it, expect, vi, beforeEach } from 'vitest';
import * as updatePasswordApi from '@/pages/api/support/user/account/updatePasswordByOld';
import { MongoUser } from '@fastgpt/service/support/user/schema';
import { MongoTeam } from '@fastgpt/service/support/user/team/teamSchema';
import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema';
import { UserStatusEnum } from '@fastgpt/global/support/user/constant';
import { initTeamFreePlan } from '@fastgpt/service/support/wallet/sub/utils';
import type { UpdatePasswordByOldBodyType } from '@fastgpt/global/openapi/support/user/account/password/api';
import { Call } from '@test/utils/request';
describe('updatePasswordByOld API', () => {
let testUser: any;
let testTeam: any;
let testTmb: any;
beforeEach(async () => {
testUser = await MongoUser.create({
username: 'testuser',
password: 'oldhashpassword',
status: UserStatusEnum.active
});
testTeam = await MongoTeam.create({
name: 'Test Team',
ownerId: testUser._id
});
await initTeamFreePlan({ teamId: String(testTeam._id) });
testTmb = await MongoTeamMember.create({
teamId: testTeam._id,
userId: testUser._id,
status: 'active',
role: 'owner'
});
vi.clearAllMocks();
});
const makeAuth = (user: any, team: any, tmb: any) => ({
userId: String(user._id),
teamId: String(team._id),
tmbId: String(tmb._id),
isRoot: false,
sessionId: 'session123'
});
it('should update password successfully with correct old password', async () => {
const res = await Call<UpdatePasswordByOldBodyType, {}, any>(updatePasswordApi.default, {
body: { oldPsw: 'oldhashpassword', newPsw: 'newhashpassword' },
auth: makeAuth(testUser, testTeam, testTmb) as any
});
expect(res.code).toBe(200);
const updatedUser = await MongoUser.findById(testUser._id).select('+password');
expect(updatedUser?.password).toBeDefined();
expect(updatedUser?.passwordUpdateTime).toBeDefined();
});
it('should reject when old password is incorrect', async () => {
const res = await Call<UpdatePasswordByOldBodyType, {}, any>(updatePasswordApi.default, {
body: { oldPsw: 'wrongpassword', newPsw: 'newhashpassword' },
auth: makeAuth(testUser, testTeam, testTmb) as any
});
expect(res.code).toBe(500);
// Password should not change
const user = await MongoUser.findById(testUser._id).select('+passwordUpdateTime');
expect(user?.passwordUpdateTime).toBeUndefined(); // we didn't set it initially
});
it('should reject when old and new passwords are the same', async () => {
const res = await Call<UpdatePasswordByOldBodyType, {}, any>(updatePasswordApi.default, {
body: { oldPsw: 'oldhashpassword', newPsw: 'oldhashpassword' },
auth: makeAuth(testUser, testTeam, testTmb) as any
});
expect(res.code).toBe(500);
});
it('should reject when oldPsw is missing', async () => {
const res = await Call<any, {}, any>(updatePasswordApi.default, {
body: { newPsw: 'newhashpassword' },
auth: makeAuth(testUser, testTeam, testTmb) as any
});
expect(res.code).toBe(500);
});
it('should reject when newPsw is missing', async () => {
const res = await Call<any, {}, any>(updatePasswordApi.default, {
body: { oldPsw: 'oldhashpassword' },
auth: makeAuth(testUser, testTeam, testTmb) as any
});
expect(res.code).toBe(500);
});
it('should reject request without authentication', async () => {
const res = await Call<UpdatePasswordByOldBodyType, {}, any>(updatePasswordApi.default, {
body: { oldPsw: 'oldhashpassword', newPsw: 'newhashpassword' }
});
expect(res.code).toBe(500);
});
// ===== Security: NoSQL injection prevention (GHSA-jxvr-h2vx-p73r Step 3) =====
it('should reject oldPsw as MongoDB operator object ($ne injection)', async () => {
// GHSA-jxvr-h2vx-p73r Step 3: oldPsw: {"$ne": ""} bypasses old password check
const res = await Call<any, {}, any>(updatePasswordApi.default, {
body: { oldPsw: { $ne: '' }, newPsw: 'newhashpassword' },
auth: makeAuth(testUser, testTeam, testTmb) as any
});
// Zod z.string() must reject object-type oldPsw
expect(res.code).toBe(500);
// Password must NOT be changed
const user = await MongoUser.findById(testUser._id).select('+passwordUpdateTime');
expect(user?.passwordUpdateTime).toBeUndefined();
});
it('should reject oldPsw with $regex injection', async () => {
const res = await Call<any, {}, any>(updatePasswordApi.default, {
body: { oldPsw: { $regex: '.*' }, newPsw: 'newhashpassword' },
auth: makeAuth(testUser, testTeam, testTmb) as any
});
expect(res.code).toBe(500);
});
it('should reject newPsw as non-string type', async () => {
const res = await Call<any, {}, any>(updatePasswordApi.default, {
body: { oldPsw: 'oldhashpassword', newPsw: { $ne: '' } },
auth: makeAuth(testUser, testTeam, testTmb) as any
});
expect(res.code).toBe(500);
});
});
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