diff --git a/apps/vben5/apps/app-antd/.env.development b/apps/vben5/apps/app-antd/.env.development index 7327d9a14..ea36fef54 100644 --- a/apps/vben5/apps/app-antd/.env.development +++ b/apps/vben5/apps/app-antd/.env.development @@ -10,7 +10,7 @@ VITE_GLOB_API_URL=/ VITE_NITRO_MOCK=true # 是否打开 devtools,true 为打开,false 为关闭 -VITE_DEVTOOLS=false +VITE_DEVTOOLS=true # 是否注入全局loading VITE_INJECT_APP_LOADING=true diff --git a/apps/vben5/apps/app-antd/src/adapter/form.ts b/apps/vben5/apps/app-antd/src/adapter/form.ts new file mode 100644 index 000000000..65ff793b6 --- /dev/null +++ b/apps/vben5/apps/app-antd/src/adapter/form.ts @@ -0,0 +1,47 @@ +import type { + VbenFormSchema as FormSchema, + VbenFormProps, +} from '@vben/common-ui'; + +import type { ComponentType } from './component'; + +import { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui'; +import { $t } from '@vben/locales'; + +setupVbenForm({ + config: { + // ant design vue组件库默认都是 v-model:value + baseModelPropName: 'value', + + // 一些组件是 v-model:checked 或者 v-model:fileList + modelPropNameMap: { + Checkbox: 'checked', + Radio: 'checked', + Switch: 'checked', + Upload: 'fileList', + }, + }, + defineRules: { + // 输入项目必填国际化适配 + required: (value, _params, ctx) => { + if (value === undefined || value === null || value.length === 0) { + return $t('ui.formRules.required', [ctx.label]); + } + return true; + }, + // 选择项目必填国际化适配 + selectRequired: (value, _params, ctx) => { + if (value === undefined || value === null) { + return $t('ui.formRules.selectRequired', [ctx.label]); + } + return true; + }, + }, +}); + +const useVbenForm = useForm; + +export { useVbenForm, z }; + +export type VbenFormSchema = FormSchema; +export type { VbenFormProps }; diff --git a/apps/vben5/apps/app-antd/src/adapter/request/index.ts b/apps/vben5/apps/app-antd/src/adapter/request/index.ts index f75d2aea1..f3d1b3286 100644 --- a/apps/vben5/apps/app-antd/src/adapter/request/index.ts +++ b/apps/vben5/apps/app-antd/src/adapter/request/index.ts @@ -1,3 +1,4 @@ +import { $t } from '@vben/locales'; import { preferences } from '@vben/preferences'; import { authenticateResponseInterceptor, @@ -105,6 +106,10 @@ export function initRequestClient() { // 这里可以根据业务进行定制,你可以拿到 error 内的信息进行定制化处理,根据不同的 code 做不同的提示,而不是直接使用 message.error 提示 msg // 当前mock接口返回的错误字段是 error 或者 message const responseData = error?.response?.data ?? {}; + if (responseData?.error_description) { + message.error($t(`abp.oauth.${responseData.error_description}`) || msg); + return; + } const errorMessage = responseData?.error ?? responseData?.message ?? ''; // 如果没有错误信息,则会根据状态码进行提示 message.error(errorMessage || msg); diff --git a/apps/vben5/apps/app-antd/src/locales/langs/en-US/abp.json b/apps/vben5/apps/app-antd/src/locales/langs/en-US/abp.json index 111ddf7c6..1b110751a 100644 --- a/apps/vben5/apps/app-antd/src/locales/langs/en-US/abp.json +++ b/apps/vben5/apps/app-antd/src/locales/langs/en-US/abp.json @@ -1,5 +1,19 @@ { "title": "Abp Framework", + "oauth": { + "Invalid username or password!": "Invalid username or password!", + "Invalid authenticator code!": "Invalid authenticator code!", + "The specified refresh token is no longer valid.": "The session has expired. Please log in again!", + "RequiresTwoFactor": "Requires Two Factor", + "twoFactor": { + "title": "Two Factor", + "authenticator": "Authenticator", + "emailAddress": "Email Address", + "phoneNumber": "Phone Number", + "getCode": "Get Code", + "code": "Code" + } + }, "manage": { "title": "Manage", "identity": { @@ -38,6 +52,7 @@ }, "security": { "title": "Security Settings", + "unSet": "Not Set", "verified": "Verified", "unVerified": "Not Verified", "email": "Email", diff --git a/apps/vben5/apps/app-antd/src/locales/langs/zh-CN/abp.json b/apps/vben5/apps/app-antd/src/locales/langs/zh-CN/abp.json index 8d0a0f3ee..68d50daa0 100644 --- a/apps/vben5/apps/app-antd/src/locales/langs/zh-CN/abp.json +++ b/apps/vben5/apps/app-antd/src/locales/langs/zh-CN/abp.json @@ -1,5 +1,19 @@ { "title": "Abp框架", + "oauth": { + "Invalid username or password!": "用户名或密码错误!", + "Invalid authenticator code!": "无效的验证器代码!", + "The specified refresh token is no longer valid.": "会话已过期,请重新登陆!", + "RequiresTwoFactor": "需要二次认证", + "twoFactor": { + "title": "二次认证", + "authenticator": "验证方式", + "emailAddress": "电子邮件", + "phoneNumber": "手机号码", + "getCode": "获取验证码", + "code": "验证码" + } + }, "manage": { "title": "管理", "identity": { @@ -38,6 +52,7 @@ }, "security": { "title": "安全设置", + "unSet": "未设置", "verified": "已验证", "unVerified": "未验证", "email": "电子邮件", diff --git a/apps/vben5/apps/app-antd/src/views/_core/authentication/login.vue b/apps/vben5/apps/app-antd/src/views/_core/authentication/login.vue index 7fee812fd..d0cd7f002 100644 --- a/apps/vben5/apps/app-antd/src/views/_core/authentication/login.vue +++ b/apps/vben5/apps/app-antd/src/views/_core/authentication/login.vue @@ -1,13 +1,17 @@ diff --git a/apps/vben5/apps/app-antd/src/views/_core/authentication/two-factor-login.vue b/apps/vben5/apps/app-antd/src/views/_core/authentication/two-factor-login.vue new file mode 100644 index 000000000..cf9270deb --- /dev/null +++ b/apps/vben5/apps/app-antd/src/views/_core/authentication/two-factor-login.vue @@ -0,0 +1,206 @@ + + + + + diff --git a/apps/vben5/packages/@abp/account/package.json b/apps/vben5/packages/@abp/account/package.json index c0b1ddb0e..a663164aa 100644 --- a/apps/vben5/packages/@abp/account/package.json +++ b/apps/vben5/packages/@abp/account/package.json @@ -34,6 +34,7 @@ "@vueuse/core": "catalog:", "@vueuse/integrations": "catalog:", "ant-design-vue": "catalog:", - "vue": "catalog:*" + "vue": "catalog:*", + "vue-router": "catalog:" } } diff --git a/apps/vben5/packages/@abp/account/src/api/index.ts b/apps/vben5/packages/@abp/account/src/api/index.ts index 48ec78a52..90f884fc6 100644 --- a/apps/vben5/packages/@abp/account/src/api/index.ts +++ b/apps/vben5/packages/@abp/account/src/api/index.ts @@ -1,3 +1,4 @@ +export { useAccountApi } from './useAccountApi'; export { useProfileApi } from './useProfileApi'; export { useTokenApi } from './useTokenApi'; export { useUserInfoApi } from './useUserInfoApi'; diff --git a/apps/vben5/packages/@abp/account/src/api/useAccountApi.ts b/apps/vben5/packages/@abp/account/src/api/useAccountApi.ts new file mode 100644 index 000000000..a6c04c4fa --- /dev/null +++ b/apps/vben5/packages/@abp/account/src/api/useAccountApi.ts @@ -0,0 +1,63 @@ +import type { ListResultDto } from '@abp/core'; + +import type { + GetTwoFactorProvidersInput, + SendEmailSigninCodeDto, + SendPhoneSigninCodeDto, + TwoFactorProvider, +} from '../types/account'; + +import { useRequest } from '@abp/request'; + +export function useAccountApi() { + const { cancel, request } = useRequest(); + + /** + * 获取可用的二次认证验证器 + * @param input 参数 + */ + function getTwoFactorProvidersApi( + input: GetTwoFactorProvidersInput, + ): Promise> { + return request>( + '/api/account/two-factor-providers', + { + method: 'GET', + params: input, + }, + ); + } + + /** + * 发送登陆验证邮件 + * @param input 参数 + */ + function sendEmailSigninCodeApi( + input: SendEmailSigninCodeDto, + ): Promise { + return request('/api/account/email/send-signin-code', { + data: input, + method: 'POST', + }); + } + + /** + * 发送登陆验证短信 + * @param input 参数 + */ + function sendPhoneSigninCodeApi( + input: SendPhoneSigninCodeDto, + ): Promise { + return request('/api/account/phone/send-signin-code', { + data: input, + method: 'POST', + }); + } + + return { + cancel, + getTwoFactorProvidersApi, + sendEmailSigninCodeApi, + sendPhoneSigninCodeApi, + }; +} diff --git a/apps/vben5/packages/@abp/account/src/api/useProfileApi.ts b/apps/vben5/packages/@abp/account/src/api/useProfileApi.ts index 53aaaf54a..c0351e2d6 100644 --- a/apps/vben5/packages/@abp/account/src/api/useProfileApi.ts +++ b/apps/vben5/packages/@abp/account/src/api/useProfileApi.ts @@ -2,7 +2,9 @@ import type { AuthenticatorDto, AuthenticatorRecoveryCodeDto, ChangePasswordInput, + ConfirmEmailInput, ProfileDto, + SendEmailConfirmCodeDto, TwoFactorEnabledDto, UpdateProfileDto, VerifyAuthenticatorCodeInput, @@ -103,14 +105,40 @@ export function useProfileApi() { }); } + /** + * 发送邮件确认链接 + * @param input 参数 + */ + function sendEmailConfirmLinkApi( + input: SendEmailConfirmCodeDto, + ): Promise { + return request('/api/account/my-profile/send-email-confirm-link', { + data: input, + method: 'POST', + }); + } + + /** + * 确认邮件 + * @param input 参数 + */ + function confirmEmailApi(input: ConfirmEmailInput) { + return request('/api/account/my-profile/confirm-email', { + data: input, + method: 'PUT', + }); + } + return { cancel, changePasswordApi, changeTwoFactorEnabledApi, + confirmEmailApi, getApi, getAuthenticatorApi, getTwoFactorEnabledApi, resetAuthenticatorApi, + sendEmailConfirmLinkApi, updateApi, verifyAuthenticatorCodeApi, }; diff --git a/apps/vben5/packages/@abp/account/src/api/useTokenApi.ts b/apps/vben5/packages/@abp/account/src/api/useTokenApi.ts index eeec1df4c..e41e84c10 100644 --- a/apps/vben5/packages/@abp/account/src/api/useTokenApi.ts +++ b/apps/vben5/packages/@abp/account/src/api/useTokenApi.ts @@ -28,9 +28,8 @@ export function useTokenApi() { client_id: clientId, client_secret: clientSecret, grant_type: 'password', - password: input.password, scope: audience, - username: input.username, + ...input, }, headers: { 'Content-Type': 'application/x-www-form-urlencoded', diff --git a/apps/vben5/packages/@abp/account/src/components/MySetting.vue b/apps/vben5/packages/@abp/account/src/components/MySetting.vue index f014c90fa..eda031603 100644 --- a/apps/vben5/packages/@abp/account/src/components/MySetting.vue +++ b/apps/vben5/packages/@abp/account/src/components/MySetting.vue @@ -2,8 +2,10 @@ import type { ProfileDto, UpdateProfileDto } from '../types/profile'; import type { UserInfo } from '../types/user'; -import { computed, onMounted, reactive, ref } from 'vue'; +import { computed, defineAsyncComponent, onMounted, reactive, ref } from 'vue'; +import { useRoute } from 'vue-router'; +import { useVbenModal } from '@vben/common-ui'; import { $t } from '@vben/locales'; import { useUserStore } from '@vben/stores'; @@ -18,6 +20,7 @@ import SecuritySettings from './components/SecuritySettings.vue'; const { getApi, updateApi } = useProfileApi(); const userStore = useUserStore(); +const { query } = useRoute(); const selectedMenuKeys = ref(['basic']); const myProfile = ref({} as ProfileDto); @@ -59,6 +62,20 @@ const getUserInfo = computed((): null | UserInfo => { uniqueName: userStore.userInfo.username, }; }); +const [EmailConfirmModal, emailConfirmModalApi] = useVbenModal({ + connectedComponent: defineAsyncComponent( + () => import('./components/EmailConfirmModal.vue'), + ), +}); +function onEmailConfirm() { + if (query?.confirmToken) { + emailConfirmModalApi.setData({ + email: myProfile.value.email, + ...query, + }); + emailConfirmModalApi.open(); + } +} async function onGetProfile() { const profile = await getApi(); myProfile.value = profile; @@ -68,11 +85,12 @@ async function onUpdateProfile(input: UpdateProfileDto) { centered: true, content: $t('AbpAccount.PersonalSettingsSaved'), onOk: async () => { - const profile = await updateApi(input); + await updateApi(input); message.success( $t('AbpAccount.PersonalSettingsChangedConfirmationModalTitle'), ); - myProfile.value = profile; + // 刷新页面重载用户信息 + window.location.reload(); }, title: $t('AbpUi.AreYouSure'), }); @@ -85,44 +103,45 @@ function onChangePhoneNumber() { // TODO: onChangePhoneNumber 暂时未实现! console.warn('onChangePhoneNumber 暂时未实现!'); } -function onValidateEmail() { - // TODO: onValidateEmail 暂时未实现! - console.warn('onValidateEmail 暂时未实现!'); -} -onMounted(onGetProfile); +onMounted(async () => { + await onGetProfile(); + onEmailConfirm(); +}); diff --git a/apps/vben5/packages/@abp/account/src/components/components/BasicSettings.vue b/apps/vben5/packages/@abp/account/src/components/components/BasicSettings.vue index f7c402b93..eec631aa4 100644 --- a/apps/vben5/packages/@abp/account/src/components/components/BasicSettings.vue +++ b/apps/vben5/packages/@abp/account/src/components/components/BasicSettings.vue @@ -78,12 +78,6 @@ watchEffect(() => { type="email" /> - - - +import { ref } from 'vue'; + +import { useVbenModal } from '@vben/common-ui'; +import { $t } from '@vben/locales'; + +import { Form, Input, message } from 'ant-design-vue'; + +import { useProfileApi } from '../../api/useProfileApi'; + +const FormItem = Form.Item; + +interface State { + confirmToken: string; + email: string; + returnUrl?: string; + userId: string; +} +const formModel = ref({} as State); + +const { cancel, confirmEmailApi } = useProfileApi(); +const [Modal, modalApi] = useVbenModal({ + onClosed: cancel, + onConfirm: onSubmit, + onOpenChange(isOpen) { + if (isOpen) { + const state = modalApi.getData(); + formModel.value = state; + } + }, + title: $t('AbpAccount.EmailConfirm'), +}); +async function onSubmit() { + try { + modalApi.setState({ confirmLoading: true }); + await confirmEmailApi({ + confirmToken: encodeURIComponent(formModel.value.confirmToken), + }); + message.success($t('AbpAccount.YourEmailIsSuccessfullyConfirm')); + modalApi.close(); + if (formModel.value.returnUrl) { + window.location.href = formModel.value.returnUrl; + } + } finally { + modalApi.setState({ confirmLoading: false }); + } +} + + + + + diff --git a/apps/vben5/packages/@abp/account/src/components/components/SecuritySettings.vue b/apps/vben5/packages/@abp/account/src/components/components/SecuritySettings.vue index bf5f5ebd6..65ac3940b 100644 --- a/apps/vben5/packages/@abp/account/src/components/components/SecuritySettings.vue +++ b/apps/vben5/packages/@abp/account/src/components/components/SecuritySettings.vue @@ -2,7 +2,7 @@ import type { TwoFactorEnabledDto } from '../../types'; import type { UserInfo } from '../../types/user'; -import { onMounted, ref } from 'vue'; +import { computed, onMounted, ref } from 'vue'; import { $t } from '@vben/locales'; @@ -16,15 +16,29 @@ defineProps<{ const emits = defineEmits<{ (event: 'changePassword'): void; (event: 'changePhoneNumber'): void; - (event: 'validateEmail'): void; }>(); const ListItem = List.Item; const ListItemMeta = List.Item.Meta; -const { changeTwoFactorEnabledApi, getTwoFactorEnabledApi } = useProfileApi(); +const { + changeTwoFactorEnabledApi, + getTwoFactorEnabledApi, + sendEmailConfirmLinkApi, +} = useProfileApi(); const twoFactor = ref(); const loading = ref(false); +const sendMailInternal = ref(0); +const getSendMailLoading = computed(() => { + return sendMailInternal.value > 0; +}); +const getSendMailTitle = computed(() => { + if (sendMailInternal.value > 0) { + return `${sendMailInternal.value} s`; + } + return $t('AbpAccount.ClickToValidation'); +}); + async function onGet() { const dto = await getTwoFactorEnabledApi(); twoFactor.value = dto; @@ -39,6 +53,24 @@ async function onTwoFactorChange(enabled: boolean) { loading.value = false; } } +async function onValidateEmail(email: string) { + sendMailInternal.value = 60; + try { + await sendEmailConfirmLinkApi({ + appName: 'VueVben5', + email, + returnUrl: window.location.href, + }); + setInterval(() => { + if (sendMailInternal.value <= 0) { + return; + } + sendMailInternal.value -= 1; + }, 1000); + } catch { + sendMailInternal.value = 0; + } +} onMounted(onGet); @@ -75,12 +107,19 @@ onMounted(onGet); @@ -89,10 +128,11 @@ onMounted(onGet); diff --git a/apps/vben5/packages/@abp/account/src/types/account.ts b/apps/vben5/packages/@abp/account/src/types/account.ts new file mode 100644 index 000000000..e5e66d37c --- /dev/null +++ b/apps/vben5/packages/@abp/account/src/types/account.ts @@ -0,0 +1,22 @@ +import type { NameValue } from '@abp/core'; + +interface GetTwoFactorProvidersInput { + userId: string; +} + +interface SendEmailSigninCodeDto { + emailAddress: string; +} + +interface SendPhoneSigninCodeDto { + phoneNumber: string; +} + +type TwoFactorProvider = NameValue; + +export type { + GetTwoFactorProvidersInput, + SendEmailSigninCodeDto, + SendPhoneSigninCodeDto, + TwoFactorProvider, +}; diff --git a/apps/vben5/packages/@abp/account/src/types/index.ts b/apps/vben5/packages/@abp/account/src/types/index.ts index 8a346ac61..1dd5f3324 100644 --- a/apps/vben5/packages/@abp/account/src/types/index.ts +++ b/apps/vben5/packages/@abp/account/src/types/index.ts @@ -1,3 +1,4 @@ +export * from './account'; export * from './profile'; export * from './token'; export * from './user'; diff --git a/apps/vben5/packages/@abp/account/src/types/profile.ts b/apps/vben5/packages/@abp/account/src/types/profile.ts index 3ce01df4f..e614f818d 100644 --- a/apps/vben5/packages/@abp/account/src/types/profile.ts +++ b/apps/vben5/packages/@abp/account/src/types/profile.ts @@ -55,11 +55,23 @@ interface AuthenticatorRecoveryCodeDto { recoveryCodes: string[]; } +interface SendEmailConfirmCodeDto { + appName: string; + email: string; + returnUrl?: string; +} + +interface ConfirmEmailInput { + confirmToken: string; +} + export type { AuthenticatorDto, AuthenticatorRecoveryCodeDto, ChangePasswordInput, + ConfirmEmailInput, ProfileDto, + SendEmailConfirmCodeDto, TwoFactorEnabledDto, UpdateProfileDto, VerifyAuthenticatorCodeInput, diff --git a/apps/vben5/packages/@abp/account/src/types/token.ts b/apps/vben5/packages/@abp/account/src/types/token.ts index e3436012c..646681e9c 100644 --- a/apps/vben5/packages/@abp/account/src/types/token.ts +++ b/apps/vben5/packages/@abp/account/src/types/token.ts @@ -48,11 +48,27 @@ interface OAuthTokenResult { token_type: string; } +interface OAuthError { + /** 错误类型 */ + error: string; + /** 错误描述 */ + error_description: string; + /** 错误描述链接 */ + error_uri?: string; +} + +interface TwoFactorError extends OAuthError { + twoFactorToken: string; + userId: string; +} + export type { + OAuthError, OAuthTokenRefreshModel, OAuthTokenResult, PasswordTokenRequest, PasswordTokenRequestModel, TokenRequest, TokenResult, + TwoFactorError, }; diff --git a/apps/vben5/packages/@abp/settings/src/components/settings/SettingForm.vue b/apps/vben5/packages/@abp/settings/src/components/settings/SettingForm.vue index 1100322f7..59b7b7fc9 100644 --- a/apps/vben5/packages/@abp/settings/src/components/settings/SettingForm.vue +++ b/apps/vben5/packages/@abp/settings/src/components/settings/SettingForm.vue @@ -88,6 +88,9 @@ function onDateChange(e: any, setting: SettingDetail) { } function onValueChange(setting: SettingDetail) { + if (setting.valueType === ValueType.NoSet) { + return; + } const index = settingsUpdateInput.value.settings.findIndex( (s) => s.name === setting.name, ); diff --git a/apps/vben5/packages/@abp/settings/src/types/settings.ts b/apps/vben5/packages/@abp/settings/src/types/settings.ts index b29cb6a0b..07616db73 100644 --- a/apps/vben5/packages/@abp/settings/src/types/settings.ts +++ b/apps/vben5/packages/@abp/settings/src/types/settings.ts @@ -18,6 +18,7 @@ export enum ValueType { Array = 4, Boolean = 2, Date = 3, + NoSet = -1, Number = 1, Object = 10, Option = 5,