Browse Source

Merge pull request #1219 from colinin/vben-feat-login

feat(vben5): enhance the login page
pull/1238/head
yx lin 10 months ago
committed by GitHub
parent
commit
c25d1cdddc
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 2
      apps/vben5/apps/app-antd/src/locales/langs/zh-CN/abp.json
  2. 31
      apps/vben5/apps/app-antd/src/store/auth.ts
  3. 42
      apps/vben5/apps/app-antd/src/views/_core/authentication/code-login.vue
  4. 144
      apps/vben5/apps/app-antd/src/views/_core/authentication/forget-password.vue
  5. 5
      apps/vben5/apps/app-antd/src/views/_core/authentication/login.vue
  6. 1
      apps/vben5/packages/@abp/account/src/api/index.ts
  7. 28
      apps/vben5/packages/@abp/account/src/api/useAccountApi.ts
  8. 46
      apps/vben5/packages/@abp/account/src/api/usePhoneLoginApi.ts
  9. 2
      apps/vben5/packages/@abp/account/src/api/useQrCodeLoginApi.ts
  10. 9
      apps/vben5/packages/@abp/account/src/hooks/useOAuthError.ts
  11. 12
      apps/vben5/packages/@abp/account/src/types/account.ts
  12. 8
      apps/vben5/packages/@abp/account/src/types/token.ts

2
apps/vben5/apps/app-antd/src/locales/langs/zh-CN/abp.json

@ -18,7 +18,7 @@
"invalidUserNameOrPassword": "用户名或密码错误!", "invalidUserNameOrPassword": "用户名或密码错误!",
"tokenHasExpired": "您的请求会话已过期,请重新登录!", "tokenHasExpired": "您的请求会话已过期,请重新登录!",
"requiresTwoFactor": "需要验证身份,请选择一种验证方式!", "requiresTwoFactor": "需要验证身份,请选择一种验证方式!",
"shouldChangePassword": "您的密码已失效,请修改密码后登录!" "shouldChangePassword": "您的密码已过期,请修改密码后登录!"
} }
}, },
"manage": { "manage": {

31
apps/vben5/apps/app-antd/src/store/auth.ts

@ -2,13 +2,14 @@ import type { TokenResult } from '@abp/account';
import type { Recordable, UserInfo } from '@vben/types'; import type { Recordable, UserInfo } from '@vben/types';
import { ref } from 'vue'; import { ref, watch } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { DEFAULT_HOME_PATH, LOGIN_PATH } from '@vben/constants'; import { DEFAULT_HOME_PATH, LOGIN_PATH } from '@vben/constants';
import { resetAllStores, useAccessStore, useUserStore } from '@vben/stores'; import { resetAllStores, useAccessStore, useUserStore } from '@vben/stores';
import { import {
usePhoneLoginApi,
useProfileApi, useProfileApi,
useQrCodeLoginApi, useQrCodeLoginApi,
useTokenApi, useTokenApi,
@ -25,6 +26,7 @@ export const useAuthStore = defineStore('auth', () => {
const { publish } = useEventBus(); const { publish } = useEventBus();
const { loginApi } = useTokenApi(); const { loginApi } = useTokenApi();
const { loginApi: qrcodeLoginApi } = useQrCodeLoginApi(); const { loginApi: qrcodeLoginApi } = useQrCodeLoginApi();
const { loginApi: phoneLoginApi } = usePhoneLoginApi();
const { getUserInfoApi } = useUserInfoApi(); const { getUserInfoApi } = useUserInfoApi();
const { getConfigApi } = useAbpConfigApi(); const { getConfigApi } = useAbpConfigApi();
const { getPictureApi } = useProfileApi(); const { getPictureApi } = useProfileApi();
@ -35,6 +37,17 @@ export const useAuthStore = defineStore('auth', () => {
const loginLoading = ref(false); const loginLoading = ref(false);
watch(
() => accessStore.accessToken,
(accessToken) => {
if (accessToken && !loginLoading.value) {
loginLoading.value = true;
fetchUserInfo();
loginLoading.value = false;
}
},
);
async function qrcodeLogin( async function qrcodeLogin(
key: string, key: string,
tenantId?: string, tenantId?: string,
@ -49,6 +62,20 @@ export const useAuthStore = defineStore('auth', () => {
} }
} }
async function phoneLogin(
phoneNumber: string,
code: string,
onSuccess?: () => Promise<void> | void,
) {
try {
loginLoading.value = true;
const result = await phoneLoginApi({ phoneNumber, code });
return await _loginSuccess(result, onSuccess);
} finally {
loginLoading.value = false;
}
}
/** /**
* *
* Asynchronously handle the login process * Asynchronously handle the login process
@ -126,7 +153,6 @@ export const useAuthStore = defineStore('auth', () => {
) { ) {
// 异步处理用户登录操作并获取 accessToken // 异步处理用户登录操作并获取 accessToken
let userInfo: null | UserInfo = null; let userInfo: null | UserInfo = null;
loginLoading.value = true;
const { accessToken, tokenType, refreshToken } = loginResult; const { accessToken, tokenType, refreshToken } = loginResult;
// 如果成功获取到 accessToken // 如果成功获取到 accessToken
if (accessToken) { if (accessToken) {
@ -172,6 +198,7 @@ export const useAuthStore = defineStore('auth', () => {
return { return {
$reset, $reset,
authLogin, authLogin,
phoneLogin,
qrcodeLogin, qrcodeLogin,
fetchUserInfo, fetchUserInfo,
loginLoading, loginLoading,

42
apps/vben5/apps/app-antd/src/views/_core/authentication/code-login.vue

@ -1,29 +1,42 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { VbenFormSchema } from '@vben/common-ui'; import type { ExtendedFormApi, VbenFormSchema } from '@vben/common-ui';
import type { Recordable } from '@vben/types'; import type { Recordable } from '@vben/types';
import { computed, ref } from 'vue'; import { computed, ref, useTemplateRef } from 'vue';
import { AuthenticationCodeLogin, z } from '@vben/common-ui'; import { AuthenticationCodeLogin, z } from '@vben/common-ui';
import { $t } from '@vben/locales'; import { $t } from '@vben/locales';
import { useAccountApi } from '@abp/account';
import { isPhone } from '@abp/core';
import { useAuthStore } from '#/store/auth';
interface CodeLoginExpose {
getFormApi(): ExtendedFormApi;
}
defineOptions({ name: 'CodeLogin' }); defineOptions({ name: 'CodeLogin' });
const authStore = useAuthStore();
const { sendPhoneSigninCodeApi } = useAccountApi();
const loading = ref(false); const loading = ref(false);
const codeLogin = useTemplateRef<CodeLoginExpose>('codeLogin');
const formSchema = computed((): VbenFormSchema[] => { const formSchema = computed((): VbenFormSchema[] => {
return [ return [
{ {
component: 'VbenInput', component: 'Input',
componentProps: { componentProps: {
placeholder: $t('authentication.mobile'), placeholder: $t('AbpAccount.DisplayName:PhoneNumber'),
}, },
fieldName: 'phoneNumber', fieldName: 'phoneNumber',
label: $t('authentication.mobile'), label: $t('AbpAccount.DisplayName:PhoneNumber'),
rules: z rules: z
.string() .string()
.min(1, { message: $t('authentication.mobileTip') }) .min(1, { message: $t('authentication.mobileTip') })
.refine((v) => /^\d{11}$/.test(v), { .refine((v) => isPhone(v), {
message: $t('authentication.mobileErrortip'), message: $t('authentication.mobileErrortip'),
}), }),
}, },
@ -37,6 +50,7 @@ const formSchema = computed((): VbenFormSchema[] => {
: $t('authentication.sendCode'); : $t('authentication.sendCode');
return text; return text;
}, },
handleSendCode: onSendCode,
placeholder: $t('authentication.code'), placeholder: $t('authentication.code'),
}, },
fieldName: 'code', fieldName: 'code',
@ -45,19 +59,31 @@ const formSchema = computed((): VbenFormSchema[] => {
}, },
]; ];
}); });
async function onSendCode() {
const formApi = codeLogin.value?.getFormApi();
const input = await formApi?.getValues();
await sendPhoneSigninCodeApi({
phoneNumber: input!.phoneNumber,
});
}
/** /**
* 异步处理登录操作 * 异步处理登录操作
* Asynchronously handle the login process * Asynchronously handle the login process
* @param values 登录表单数据 * @param values 登录表单数据
*/ */
async function handleLogin(values: Recordable<any>) { async function handleLogin(values: Recordable<any>) {
// eslint-disable-next-line no-console try {
console.log(values); loading.value = true;
await authStore.phoneLogin(values.phoneNumber, values.code);
} finally {
loading.value = false;
}
} }
</script> </script>
<template> <template>
<AuthenticationCodeLogin <AuthenticationCodeLogin
ref="codeLogin"
:form-schema="formSchema" :form-schema="formSchema"
:loading="loading" :loading="loading"
@submit="handleLogin" @submit="handleLogin"

144
apps/vben5/apps/app-antd/src/views/_core/authentication/forget-password.vue

@ -1,42 +1,164 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { VbenFormSchema } from '@vben/common-ui'; import type { ExtendedFormApi, VbenFormSchema } from '@vben/common-ui';
import type { Recordable } from '@vben/types'; import type { Recordable } from '@vben/types';
import { computed, ref } from 'vue'; import { computed, ref, useTemplateRef } from 'vue';
import { useRouter } from 'vue-router';
import { AuthenticationForgetPassword, z } from '@vben/common-ui'; import { AuthenticationForgetPassword, z } from '@vben/common-ui';
import { $t } from '@vben/locales'; import { $t } from '@vben/locales';
import { useAccountApi } from '@abp/account';
import { isPhone } from '@abp/core';
import { usePasswordValidator } from '@abp/identity';
import { message } from 'ant-design-vue';
interface FormModel {
currentPassword: string;
newPassword: string;
newPasswordConfirm: string;
}
interface ForgetPasswordExpose {
getFormApi(): ExtendedFormApi;
}
defineOptions({ name: 'ForgetPassword' }); defineOptions({ name: 'ForgetPassword' });
const router = useRouter();
const { validate } = usePasswordValidator();
const { resetPasswordApi, sendPhoneResetPasswordCodeApi } = useAccountApi();
const loading = ref(false); const loading = ref(false);
const forgetPassword = useTemplateRef<ForgetPasswordExpose>('forgetPassword');
const formSchema = computed((): VbenFormSchema[] => { const formSchema = computed((): VbenFormSchema[] => {
return [ return [
{ {
component: 'VbenInput', component: 'Input',
componentProps: {
placeholder: $t('AbpAccount.DisplayName:PhoneNumber'),
},
fieldName: 'phoneNumber',
label: $t('AbpAccount.DisplayName:PhoneNumber'),
rules: z
.string()
.min(1, { message: $t('authentication.mobileTip') })
.refine((v) => isPhone(v), {
message: $t('authentication.mobileErrortip'),
}),
},
{
component: 'VbenPinInput',
componentProps: {
createText: (countdown: number) => {
const text =
countdown > 0
? $t('authentication.sendText', [countdown])
: $t('authentication.sendCode');
return text;
},
handleSendCode: onSendCode,
placeholder: $t('authentication.code'),
},
fieldName: 'code',
label: $t('authentication.code'),
rules: z.string().min(1, { message: $t('authentication.codeTip') }),
},
{
component: 'InputPassword',
componentProps: { componentProps: {
placeholder: 'example@example.com', placeholder: $t('AbpAccount.DisplayName:NewPassword'),
}, },
fieldName: 'email', fieldName: 'newPassword',
label: $t('authentication.email'), label: $t('AbpAccount.DisplayName:NewPassword'),
rules: z rules: z
.string() .string()
.min(1, { message: $t('authentication.emailTip') }) .superRefine(async (newPassword, ctx) => {
.email($t('authentication.emailValidErrorTip')), try {
await validate(newPassword);
} catch (error) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: String(error),
});
}
})
.refine(
async (newPassword) => {
const formApi = forgetPassword.value?.getFormApi();
const input = (await formApi?.getValues()) as FormModel;
return input.currentPassword !== newPassword;
},
{
message: $t('AbpAccount.NewPasswordSameAsOld'),
},
)
.refine(
async (newPassword) => {
const formApi = forgetPassword.value?.getFormApi();
const input = (await formApi?.getValues()) as FormModel;
return input.newPasswordConfirm === newPassword;
},
{
message: $t(
'AbpIdentity.Volo_Abp_Identity:PasswordConfirmationFailed',
),
},
),
},
{
component: 'InputPassword',
componentProps: {
placeholder: $t('AbpAccount.DisplayName:NewPasswordConfirm'),
},
fieldName: 'newPasswordConfirm',
label: $t('AbpAccount.DisplayName:NewPasswordConfirm'),
rules: z.string().refine(
async (newPasswordConfirm) => {
const formApi = forgetPassword.value?.getFormApi();
const input = (await formApi?.getValues()) as FormModel;
return input.newPassword === newPasswordConfirm;
},
{
message: $t(
'AbpIdentity.Volo_Abp_Identity:PasswordConfirmationFailed',
),
},
),
}, },
]; ];
}); });
function handleSubmit(value: Recordable<any>) { async function onSendCode() {
// eslint-disable-next-line no-console const formApi = forgetPassword.value?.getFormApi();
console.log('reset email:', value); const input = await formApi?.getValues();
await sendPhoneResetPasswordCodeApi({
phoneNumber: input!.phoneNumber,
});
}
async function handleSubmit(values: Recordable<any>) {
loading.value = true;
try {
await resetPasswordApi({
code: values.code,
phoneNumber: values.phoneNumber,
newPassword: values.newPassword,
});
message.success($t('AbpAccount.YourPasswordIsSuccessfullyReset'));
router.push('/auth/login');
} finally {
loading.value = false;
}
} }
</script> </script>
<template> <template>
<AuthenticationForgetPassword <AuthenticationForgetPassword
ref="forgetPassword"
:form-schema="formSchema" :form-schema="formSchema"
:submit-button-text="$t('AbpAccount.ResetPassword')"
:loading="loading" :loading="loading"
@submit="handleSubmit" @submit="handleSubmit"
/> />

5
apps/vben5/apps/app-antd/src/views/_core/authentication/login.vue

@ -9,7 +9,7 @@ import { computed, nextTick, onMounted, useTemplateRef } from 'vue';
import { AuthenticationLogin, useVbenModal, z } from '@vben/common-ui'; import { AuthenticationLogin, useVbenModal, z } from '@vben/common-ui';
import { $t } from '@vben/locales'; import { $t } from '@vben/locales';
import { useAbpStore } from '@abp/core'; import { useAbpStore, useSettings } from '@abp/core';
import { useAbpConfigApi } from '#/api/core/useAbpConfigApi'; import { useAbpConfigApi } from '#/api/core/useAbpConfigApi';
import { useAuthStore } from '#/store'; import { useAuthStore } from '#/store';
@ -27,6 +27,8 @@ defineOptions({ name: 'Login' });
const abpStore = useAbpStore(); const abpStore = useAbpStore();
const authStore = useAuthStore(); const authStore = useAuthStore();
const { isTrue } = useSettings();
const { getConfigApi } = useAbpConfigApi(); const { getConfigApi } = useAbpConfigApi();
const login = useTemplateRef<LoginInstance>('login'); const login = useTemplateRef<LoginInstance>('login');
@ -118,6 +120,7 @@ onMounted(onInit);
ref="login" ref="login"
:form-schema="formSchema" :form-schema="formSchema"
:loading="authStore.loginLoading" :loading="authStore.loginLoading"
:show-register="isTrue('Abp.Account.IsSelfRegistrationEnabled')"
@submit="onLogin" @submit="onLogin"
> >
<!-- 第三方登录 --> <!-- 第三方登录 -->

1
apps/vben5/packages/@abp/account/src/api/index.ts

@ -1,5 +1,6 @@
export { useAccountApi } from './useAccountApi'; export { useAccountApi } from './useAccountApi';
export { useMySessionApi } from './useMySessionApi'; export { useMySessionApi } from './useMySessionApi';
export { usePhoneLoginApi } from './usePhoneLoginApi';
export { useProfileApi } from './useProfileApi'; export { useProfileApi } from './useProfileApi';
export { useQrCodeLoginApi } from './useQrCodeLoginApi'; export { useQrCodeLoginApi } from './useQrCodeLoginApi';
export { useTokenApi } from './useTokenApi'; export { useTokenApi } from './useTokenApi';

28
apps/vben5/packages/@abp/account/src/api/useAccountApi.ts

@ -2,7 +2,9 @@ import type { ListResultDto } from '@abp/core';
import type { import type {
GetTwoFactorProvidersInput, GetTwoFactorProvidersInput,
PhoneResetPasswordDto,
SendEmailSigninCodeDto, SendEmailSigninCodeDto,
SendPhoneResetPasswordCodeDto,
SendPhoneSigninCodeDto, SendPhoneSigninCodeDto,
TwoFactorProvider, TwoFactorProvider,
} from '../types/account'; } from '../types/account';
@ -54,10 +56,36 @@ export function useAccountApi() {
}); });
} }
/**
*
* @param input
*/
function sendPhoneResetPasswordCodeApi(
input: SendPhoneResetPasswordCodeDto,
): Promise<void> {
return request('/api/account/phone/send-password-reset-code', {
data: input,
method: 'POST',
});
}
/**
*
* @param input
*/
function resetPasswordApi(input: PhoneResetPasswordDto): Promise<void> {
return request('/api/account/phone/reset-password', {
data: input,
method: 'PUT',
});
}
return { return {
cancel, cancel,
getTwoFactorProvidersApi, getTwoFactorProvidersApi,
resetPasswordApi,
sendEmailSigninCodeApi, sendEmailSigninCodeApi,
sendPhoneResetPasswordCodeApi,
sendPhoneSigninCodeApi, sendPhoneSigninCodeApi,
}; };
} }

46
apps/vben5/packages/@abp/account/src/api/usePhoneLoginApi.ts

@ -0,0 +1,46 @@
import type { OAuthTokenResult, PhoneNumberTokenRequest } from '../types/token';
import { useAppConfig } from '@vben/hooks';
import { useRequest } from '@abp/request';
export function usePhoneLoginApi() {
const { cancel, request } = useRequest();
/**
*
* @param input
* @returns token
*/
async function loginApi(input: PhoneNumberTokenRequest) {
const { audience, clientId, clientSecret } = useAppConfig(
import.meta.env,
import.meta.env.PROD,
);
const result = await request<OAuthTokenResult>('/connect/token', {
data: {
client_id: clientId,
client_secret: clientSecret,
grant_type: 'phone_verify',
phone_number: input.phoneNumber,
phone_verify_code: input.code,
scope: audience,
},
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
method: 'POST',
});
return {
accessToken: result.access_token,
expiresIn: result.expires_in,
refreshToken: result.refresh_token,
tokenType: result.token_type,
};
}
return {
cancel,
loginApi,
};
}

2
apps/vben5/packages/@abp/account/src/api/useQrCodeLoginApi.ts

@ -34,7 +34,7 @@ export function useQrCodeLoginApi() {
/** /**
* *
* @param key Key * @param input
* @returns token * @returns token
*/ */
async function loginApi(input: QrCodeTokenRequest) { async function loginApi(input: QrCodeTokenRequest) {

9
apps/vben5/packages/@abp/account/src/hooks/useOAuthError.ts

@ -13,14 +13,15 @@ export function useOAuthError() {
case 'Invalid username or password!': { case 'Invalid username or password!': {
return $t('abp.oauth.errors.invalidUserNameOrPassword'); return $t('abp.oauth.errors.invalidUserNameOrPassword');
} }
// 需要二次认证
case 'RequiresTwoFactor': {
return $t('abp.oauth.errors.requiresTwoFactor');
}
// 需要更改密码 // 需要更改密码
case 'PeriodicallyChangePassword':
case 'ShouldChangePasswordOnNextLogin': { case 'ShouldChangePasswordOnNextLogin': {
return $t('abp.oauth.errors.shouldChangePassword'); return $t('abp.oauth.errors.shouldChangePassword');
} }
// 需要二次认证
case 'RequiresTwoFactor': {
return $t('abp.oauth.errors.requiresTwoFactor');
}
// Token已失效 // Token已失效
case 'The token is no longer valid.': { case 'The token is no longer valid.': {
return $t('abp.oauth.errors.tokenHasExpired'); return $t('abp.oauth.errors.tokenHasExpired');

12
apps/vben5/packages/@abp/account/src/types/account.ts

@ -12,11 +12,23 @@ interface SendPhoneSigninCodeDto {
phoneNumber: string; phoneNumber: string;
} }
interface SendPhoneResetPasswordCodeDto {
phoneNumber: string;
}
interface PhoneResetPasswordDto {
code: string;
newPassword: string;
phoneNumber: string;
}
type TwoFactorProvider = NameValue<string>; type TwoFactorProvider = NameValue<string>;
export type { export type {
GetTwoFactorProvidersInput, GetTwoFactorProvidersInput,
PhoneResetPasswordDto,
SendEmailSigninCodeDto, SendEmailSigninCodeDto,
SendPhoneResetPasswordCodeDto,
SendPhoneSigninCodeDto, SendPhoneSigninCodeDto,
TwoFactorProvider, TwoFactorProvider,
}; };

8
apps/vben5/packages/@abp/account/src/types/token.ts

@ -14,6 +14,13 @@ interface PasswordTokenRequest extends TokenRequest {
/** 用户名 */ /** 用户名 */
userName: string; userName: string;
} }
/** 手机号授权请求数据模型 */
interface PhoneNumberTokenRequest {
/** 验证码 */
code: string;
/** 手机号 */
phoneNumber: string;
}
/** 扫码登录授权请求数据模型 */ /** 扫码登录授权请求数据模型 */
interface QrCodeTokenRequest { interface QrCodeTokenRequest {
/** 二维码Key */ /** 二维码Key */
@ -80,6 +87,7 @@ export type {
OAuthTokenResult, OAuthTokenResult,
PasswordTokenRequest, PasswordTokenRequest,
PasswordTokenRequestModel, PasswordTokenRequestModel,
PhoneNumberTokenRequest,
QrCodeTokenRequest, QrCodeTokenRequest,
ShouldChangePasswordError, ShouldChangePasswordError,
TokenRequest, TokenRequest,

Loading…
Cancel
Save