Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion document/content/self-host/config/env.en.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ This page describes the environment variables commonly used in a self-hosted Fas
- `projects/code-sandbox`: the code execution sandbox service. It exposes the `/sandbox` endpoint and is called by App through `CODE_SANDBOX_URL`.
- `packages/service/env.ts` exports `serviceEnv`; `projects/app/src/env.ts` exports `appEnv`.
- Shared App/Admin boolean variables use `true`, `1`, `yes`, or `y` to enable a feature. Other values are treated as disabled.
- `FILE_TOKEN_KEY`, `AES256_SECRET_KEY`, and `INVOKE_TOKEN_SECRET` are required at runtime. Use strong random secrets and do not use the example values in production.
- `FILE_TOKEN_KEY`, `AES256_SECRET_KEY`, and `INVOKE_TOKEN_SECRET` are required at runtime. Use separate strong random secrets and do not use the example values in production.

## Shared App/Admin Variables

Expand Down
14 changes: 13 additions & 1 deletion packages/global/common/error/code/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ export enum UserErrEnum {
verifyCodeTooFrequently = 'verifyCodeTooFrequently',
invalidAccount = 'invalidAccount',
accountCancellationPending = 'accountCancellationPending',
registrationMethodNotSupported = 'registrationMethodNotSupported'
registrationMethodNotSupported = 'registrationMethodNotSupported',
passwordChangeAuthorizationInvalid = 'passwordChangeAuthorizationInvalid',
newPasswordSameAsOld = 'newPasswordSameAsOld'
}
const errList = [
{
Expand Down Expand Up @@ -59,6 +61,16 @@ const errList = [
statusText: UserErrEnum.registrationMethodNotSupported,
message: i18nT('common:error.registration_method_not_supported'),
httpStatus: 403
},
{
statusText: UserErrEnum.passwordChangeAuthorizationInvalid,
message: 'Password change authorization is invalid',
httpStatus: 403
},
{
statusText: UserErrEnum.newPasswordSameAsOld,
message: i18nT('common:user.Password has no change'),
httpStatus: 400
}
];
export default errList.reduce((acc, cur, index) => {
Expand Down
28 changes: 11 additions & 17 deletions packages/global/openapi/support/user/account/cancellation/api.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { z } from 'zod';
import {
AccountCancellationAllowedMethodSchema,
AccountCancellationUnavailableReasonSchema
} from '../../../../../support/user/account/cancellation/type';
import { AccountExternalVerificationMethodSchema } from '../../../../../support/user/account/verification/type';
import { oauthAccountVerificationMethods } from '../../../../../support/user/account/verification/constants';
import type { OAuthAccountVerificationMethod } from '../../../../../support/user/account/verification/type';
import { AccountCancellationUnavailableReasonSchema } from '../../../../../support/user/account/cancellation/type';

/* ============================================================================
* API: 账号注销
Expand All @@ -22,7 +22,7 @@ export const AccountCancellationStatusResponseSchema = z
description: '是否允许发起注销申请',
example: true
}),
verificationMethod: AccountCancellationAllowedMethodSchema.optional().meta({
verificationMethod: AccountExternalVerificationMethodSchema.optional().meta({
description: '当前账号可用的注销验证方式',
example: 'code'
}),
Expand Down Expand Up @@ -75,18 +75,14 @@ const OAuthCreatePayloadSchema = z.object({
isWecomWorkTerminal: z.boolean().optional().meta({ description: '是否来自企业微信工作台' })
});

const OAuthCreateMethodSchemas = [
'oauth/github',
'oauth/google',
'oauth/microsoft',
'oauth/wecom',
'oauth/sso'
] as const;
const createOAuthVerificationSchemaTuple = <Schema extends z.ZodType>(
createSchema: (method: OAuthAccountVerificationMethod) => Schema
) => oauthAccountVerificationMethods.map(createSchema) as [Schema, Schema, Schema, Schema, Schema];

export const CreateAccountCancellationVerificationBodySchema = z.discriminatedUnion('method', [
CodeVerificationCreateSchema,
WechatVerificationCreateSchema,
...OAuthCreateMethodSchemas.map((method) =>
...createOAuthVerificationSchemaTuple((method) =>
z.object({
method: z.literal(method).meta({ description: 'OAuth 验证方式', example: method }),
payload: OAuthCreatePayloadSchema
Expand All @@ -113,7 +109,7 @@ export const CreateAccountCancellationVerificationResponseSchema = z.discriminat
.meta({ description: '微信二维码地址', example: 'https://mp.weixin.qq.com/...' }),
expiredAt: DateTimeSchema.optional().meta({ description: '二维码过期时间' })
}),
...OAuthCreateMethodSchemas.map((method) =>
...createOAuthVerificationSchemaTuple((method) =>
z.object({
method: z.literal(method),
state: z.string().min(16).meta({ description: '一次性 OAuth state', example: 'state' }),
Expand Down Expand Up @@ -172,7 +168,7 @@ const OAuthSubmitPayloadSchema = z.object({
export const SubmitAccountCancellationBodySchema = z.discriminatedUnion('method', [
CodeSubmitSchema,
WechatSubmitSchema,
...OAuthCreateMethodSchemas.map((method) =>
...createOAuthVerificationSchemaTuple((method) =>
z.object({ method: z.literal(method), payload: OAuthSubmitPayloadSchema })
)
] as [typeof CodeSubmitSchema, typeof WechatSubmitSchema, ...any[]]);
Expand All @@ -198,5 +194,3 @@ export const CancelAccountCancellationResponseSchema = z
export type CancelAccountCancellationResponse = z.infer<
typeof CancelAccountCancellationResponseSchema
>;

export { AccountCancellationAllowedMethodSchema };
252 changes: 214 additions & 38 deletions packages/global/openapi/support/user/account/password/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,20 @@ import { LanguageSchema } from '../../../../../common/i18n/type';
import {
AccountContactUsernameSchema,
AccountPasswordSchema,
ShortAuthStringSchema
AccountVerificationMethodSchema,
ShortAuthStringSchema,
type OAuthAccountVerificationMethod
} from '../../../../../support/user/account/verification/type';
import { oauthAccountVerificationMethods } from '../../../../../support/user/account/verification/constants';

// ===== Update password by old password =====
export const UpdatePasswordByOldBodySchema = z
.object({
oldPsw: AccountPasswordSchema.meta({
example: 'hashed_old_password',
description: '旧密码(已加密)'
}),
newPsw: AccountPasswordSchema.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>;
/* ============================================================================
* API: 安全修改密码
* Routes: POST /proApi/support/user/account/password/verification/create
* POST /proApi/support/user/account/password/authorization
* POST /support/user/account/password/update
* Description: 创建身份验证材料、一次性改密 Session 并更新当前用户密码
* Tags: ['User Login', 'Account Verification']
* ============================================================================ */

// ===== Check password expired =====
export const CheckPswExpiredResponseSchema = z.boolean().meta({
Expand All @@ -37,25 +25,213 @@ export const CheckPswExpiredResponseSchema = z.boolean().meta({
});
export type CheckPswExpiredResponseType = z.infer<typeof CheckPswExpiredResponseSchema>;

// ===== Reset expired password =====
export const ResetExpiredPswBodySchema = z
const DateTimeSchema = z.iso.datetime({ offset: true });
const createOAuthVerificationSchemaTuple = <Schema extends z.ZodType>(
createSchema: (method: OAuthAccountVerificationMethod) => Schema
) => oauthAccountVerificationMethods.map(createSchema) as [Schema, Schema, Schema, Schema, Schema];

const OAuthCreatePayloadSchema = z
.object({
newPsw: AccountPasswordSchema.meta({
example: 'hashed_new_password',
description: '新密码(已加密)'
callbackUrl: z.url().max(2048).meta({
description: 'OAuth 回调地址',
example: 'https://fastgpt.example.com/login/provider'
}),
isWecomWorkTerminal: z.boolean().optional().meta({
description: '是否来自企业微信工作台',
example: false
})
})
.meta({
example: {
newPsw: 'hashed_new_password'
}
.strict();
const OAuthPropsSchema = z
.record(
z
.string()
.regex(/^[A-Za-z0-9_.-]+$/)
.max(64),
z.string().max(4096)
)
.refine((props) => Object.keys(props).length <= 20, {
message: 'OAuth props contain too many keys'
});
export type ResetExpiredPswBodyType = z.infer<typeof ResetExpiredPswBodySchema>;
const OAuthConsumePayloadSchema = z
.object({
callbackUrl: z.url().max(2048).meta({
description: 'OAuth 回调地址',
example: 'https://fastgpt.example.com/login/provider'
}),
code: z.string().min(1).max(4096).meta({
description: 'Provider 返回的一次性授权码',
example: 'provider-code'
}),
state: z.string().min(16).max(256).optional().meta({
description: '创建验证材料时签发的 OAuth state',
example: 'state-abcdefghijklmnopqrstuvwxyz'
}),
props: OAuthPropsSchema.optional().meta({ description: 'SSO Provider 附加回调参数' })
})
.strict();

export const ResetExpiredPswResponseSchema = z.undefined().meta({
description: '重置成功'
});
export type ResetExpiredPswResponseType = z.infer<typeof ResetExpiredPswResponseSchema>;
const CodeVerificationCreateSchema = z
.object({
method: z.literal('code').meta({ description: '邮箱或手机验证码', example: 'code' }),
payload: z
.object({
captcha: z.string().min(1).max(64).meta({
description: '图片验证码答案',
example: 'A1B2C3'
})
})
.strict()
})
.strict();
const OldPasswordVerificationCreateSchema = z
.object({
method: z.literal('oldPassword'),
payload: z.object({}).strict()
})
.strict();
const WechatVerificationCreateSchema = z
.object({
method: z.literal('wechat'),
payload: z.object({}).strict()
})
.strict();
const OAuthVerificationCreateSchemas = createOAuthVerificationSchemaTuple((method) =>
z.object({ method: z.literal(method), payload: OAuthCreatePayloadSchema }).strict()
);

export const CreatePasswordVerificationBodySchema = z.discriminatedUnion('method', [
CodeVerificationCreateSchema,
OldPasswordVerificationCreateSchema,
WechatVerificationCreateSchema,
...OAuthVerificationCreateSchemas
]);
export type CreatePasswordVerificationBody = z.infer<typeof CreatePasswordVerificationBodySchema>;

const OAuthVerificationResponseSchemas = createOAuthVerificationSchemaTuple((method) =>
z.object({
method: z.literal(method),
state: z.string().min(16).meta({ description: 'OAuth state', example: 'state-value' }),
url: z.url().meta({ description: 'Provider 重新认证地址' })
})
);
export const CreatePasswordVerificationResponseSchema = z.discriminatedUnion('method', [
z.object({
method: z.literal('code'),
sent: z.literal(true),
maskedTarget: z.string().meta({ description: '验证码接收目标脱敏值' })
}),
z.object({
method: z.literal('oldPassword'),
preLoginCode: z.string().min(1).meta({ description: '绑定当前密码验证的短期材料' })
}),
z.object({
method: z.literal('wechat'),
code: z.string().min(16).meta({ description: '微信二维码场景码' }),
codeUrl: z.url().meta({ description: '微信二维码图片地址' }),
expiredAt: DateTimeSchema.optional().meta({ description: '二维码过期时间' })
}),
...OAuthVerificationResponseSchemas
]);
export type CreatePasswordVerificationResponse = z.infer<
typeof CreatePasswordVerificationResponseSchema
>;

const CodeVerificationConsumeSchema = z
.object({
method: z.literal('code'),
payload: z.object({ code: z.string().min(1).max(32) }).strict()
})
.strict();
const OldPasswordVerificationConsumeSchema = z
.object({
method: z.literal('oldPassword'),
payload: z
.object({
password: z.string().length(64),
preLoginCode: z.string().min(1).max(128)
})
.strict()
})
.strict();
const WechatVerificationConsumeSchema = z
.object({
method: z.literal('wechat'),
payload: z.object({ code: z.string().min(1).max(128) }).strict()
})
.strict();
const OAuthVerificationConsumeSchemas = createOAuthVerificationSchemaTuple((method) =>
z.object({ method: z.literal(method), payload: OAuthConsumePayloadSchema }).strict()
);
export const SensitiveAccountVerificationBodySchema = z.discriminatedUnion('method', [
CodeVerificationConsumeSchema,
OldPasswordVerificationConsumeSchema,
WechatVerificationConsumeSchema,
...OAuthVerificationConsumeSchemas
]);
export type SensitiveAccountVerificationBody = z.infer<
typeof SensitiveAccountVerificationBodySchema
>;

export const PasswordAuthorizationBodySchema = z.discriminatedUnion('source', [
z
.object({
source: z.literal('verificationMethod').meta({
description: '请求服务端解析唯一验证方式',
example: 'verificationMethod'
})
})
.strict(),
z
.object({
source: z.literal('accountVerification').meta({
description: '消费账号身份验证材料',
example: 'accountVerification'
}),
verification: SensitiveAccountVerificationBodySchema.meta({ description: '身份验证材料' })
})
.strict()
]);
export type PasswordAuthorizationBody = z.infer<typeof PasswordAuthorizationBodySchema>;

export const PasswordAuthorizationResponseSchema = z.discriminatedUnion('status', [
z.object({
status: z.literal('authorized'),
sessionId: z.string().min(1).max(128).meta({ description: '五分钟有效的一次性改密 Session' }),
expiredAt: DateTimeSchema.meta({ description: '改密授权过期时间' })
}),
z.object({
status: z.literal('verificationRequired'),
method: AccountVerificationMethodSchema.meta({ description: '服务端选择的唯一验证方式' })
}),
z.object({ status: z.literal('verificationPending') }),
z.object({ status: z.literal('verificationExpired') }),
z.object({
status: z.literal('verificationUnavailable'),
reason: z.literal('no_available_verification_method')
})
]);
export type PasswordAuthorizationResponse = z.infer<typeof PasswordAuthorizationResponseSchema>;

export const UpdatePasswordBodySchema = z
.object({
newPsw: z
.string()
.length(64)
.meta({
description: '客户端 SHA-256 处理后的新密码摘要',
example: 'a'.repeat(64)
}),
passwordChangeSession: z.string().min(1).max(128).meta({
description: '身份验证成功后签发的一次性改密 Session',
example: 'password-change-session'
})
})
.strict();
export type UpdatePasswordBody = z.infer<typeof UpdatePasswordBodySchema>;

export const UpdatePasswordResponseSchema = z.undefined().meta({ description: '密码设置成功' });
export type UpdatePasswordResponse = z.infer<typeof UpdatePasswordResponseSchema>;

// ===== Find Password (update by code) =====
export const UpdatePasswordByCodeBodySchema = z.object({
Expand Down
Loading
Loading