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": "用户名或密码错误!",
"tokenHasExpired": "您的请求会话已过期,请重新登录!",
"requiresTwoFactor": "需要验证身份,请选择一种验证方式!",
"shouldChangePassword": "您的密码已失效,请修改密码后登录!"
"shouldChangePassword": "您的密码已过期,请修改密码后登录!"
}
},
"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 { ref } from 'vue';
import { ref, watch } from 'vue';
import { useRouter } from 'vue-router';
import { DEFAULT_HOME_PATH, LOGIN_PATH } from '@vben/constants';
import { resetAllStores, useAccessStore, useUserStore } from '@vben/stores';
import {
usePhoneLoginApi,
useProfileApi,
useQrCodeLoginApi,
useTokenApi,
@ -25,6 +26,7 @@ export const useAuthStore = defineStore('auth', () => {
const { publish } = useEventBus();
const { loginApi } = useTokenApi();
const { loginApi: qrcodeLoginApi } = useQrCodeLoginApi();
const { loginApi: phoneLoginApi } = usePhoneLoginApi();
const { getUserInfoApi } = useUserInfoApi();
const { getConfigApi } = useAbpConfigApi();
const { getPictureApi } = useProfileApi();
@ -35,6 +37,17 @@ export const useAuthStore = defineStore('auth', () => {
const loginLoading = ref(false);
watch(
() => accessStore.accessToken,
(accessToken) => {
if (accessToken && !loginLoading.value) {
loginLoading.value = true;
fetchUserInfo();
loginLoading.value = false;
}
},
);
async function qrcodeLogin(
key: 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
@ -126,7 +153,6 @@ export const useAuthStore = defineStore('auth', () => {
) {
// 异步处理用户登录操作并获取 accessToken
let userInfo: null | UserInfo = null;
loginLoading.value = true;
const { accessToken, tokenType, refreshToken } = loginResult;
// 如果成功获取到 accessToken
if (accessToken) {
@ -172,6 +198,7 @@ export const useAuthStore = defineStore('auth', () => {
return {
$reset,
authLogin,
phoneLogin,
qrcodeLogin,
fetchUserInfo,
loginLoading,

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

@ -1,29 +1,42 @@
<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 { computed, ref } from 'vue';
import { computed, ref, useTemplateRef } from 'vue';
import { AuthenticationCodeLogin, z } from '@vben/common-ui';
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' });
const authStore = useAuthStore();
const { sendPhoneSigninCodeApi } = useAccountApi();
const loading = ref(false);
const codeLogin = useTemplateRef<CodeLoginExpose>('codeLogin');
const formSchema = computed((): VbenFormSchema[] => {
return [
{
component: 'VbenInput',
component: 'Input',
componentProps: {
placeholder: $t('authentication.mobile'),
placeholder: $t('AbpAccount.DisplayName:PhoneNumber'),
},
fieldName: 'phoneNumber',
label: $t('authentication.mobile'),
label: $t('AbpAccount.DisplayName:PhoneNumber'),
rules: z
.string()
.min(1, { message: $t('authentication.mobileTip') })
.refine((v) => /^\d{11}$/.test(v), {
.refine((v) => isPhone(v), {
message: $t('authentication.mobileErrortip'),
}),
},
@ -37,6 +50,7 @@ const formSchema = computed((): VbenFormSchema[] => {
: $t('authentication.sendCode');
return text;
},
handleSendCode: onSendCode,
placeholder: $t('authentication.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
* @param values 登录表单数据
*/
async function handleLogin(values: Recordable<any>) {
// eslint-disable-next-line no-console
console.log(values);
try {
loading.value = true;
await authStore.phoneLogin(values.phoneNumber, values.code);
} finally {
loading.value = false;
}
}
</script>
<template>
<AuthenticationCodeLogin
ref="codeLogin"
:form-schema="formSchema"
:loading="loading"
@submit="handleLogin"

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

@ -1,42 +1,164 @@
<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 { computed, ref } from 'vue';
import { computed, ref, useTemplateRef } from 'vue';
import { useRouter } from 'vue-router';
import { AuthenticationForgetPassword, z } from '@vben/common-ui';
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' });
const router = useRouter();
const { validate } = usePasswordValidator();
const { resetPasswordApi, sendPhoneResetPasswordCodeApi } = useAccountApi();
const loading = ref(false);
const forgetPassword = useTemplateRef<ForgetPasswordExpose>('forgetPassword');
const formSchema = computed((): VbenFormSchema[] => {
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: {
placeholder: 'example@example.com',
placeholder: $t('AbpAccount.DisplayName:NewPassword'),
},
fieldName: 'email',
label: $t('authentication.email'),
fieldName: 'newPassword',
label: $t('AbpAccount.DisplayName:NewPassword'),
rules: z
.string()
.min(1, { message: $t('authentication.emailTip') })
.email($t('authentication.emailValidErrorTip')),
.superRefine(async (newPassword, ctx) => {
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>) {
// eslint-disable-next-line no-console
console.log('reset email:', value);
async function onSendCode() {
const formApi = forgetPassword.value?.getFormApi();
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>
<template>
<AuthenticationForgetPassword
ref="forgetPassword"
:form-schema="formSchema"
:submit-button-text="$t('AbpAccount.ResetPassword')"
:loading="loading"
@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 { $t } from '@vben/locales';
import { useAbpStore } from '@abp/core';
import { useAbpStore, useSettings } from '@abp/core';
import { useAbpConfigApi } from '#/api/core/useAbpConfigApi';
import { useAuthStore } from '#/store';
@ -27,6 +27,8 @@ defineOptions({ name: 'Login' });
const abpStore = useAbpStore();
const authStore = useAuthStore();
const { isTrue } = useSettings();
const { getConfigApi } = useAbpConfigApi();
const login = useTemplateRef<LoginInstance>('login');
@ -118,6 +120,7 @@ onMounted(onInit);
ref="login"
:form-schema="formSchema"
:loading="authStore.loginLoading"
:show-register="isTrue('Abp.Account.IsSelfRegistrationEnabled')"
@submit="onLogin"
>
<!-- 第三方登录 -->

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

@ -1,5 +1,6 @@
export { useAccountApi } from './useAccountApi';
export { useMySessionApi } from './useMySessionApi';
export { usePhoneLoginApi } from './usePhoneLoginApi';
export { useProfileApi } from './useProfileApi';
export { useQrCodeLoginApi } from './useQrCodeLoginApi';
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 {
GetTwoFactorProvidersInput,
PhoneResetPasswordDto,
SendEmailSigninCodeDto,
SendPhoneResetPasswordCodeDto,
SendPhoneSigninCodeDto,
TwoFactorProvider,
} 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 {
cancel,
getTwoFactorProvidersApi,
resetPasswordApi,
sendEmailSigninCodeApi,
sendPhoneResetPasswordCodeApi,
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
*/
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!': {
return $t('abp.oauth.errors.invalidUserNameOrPassword');
}
// 需要二次认证
case 'RequiresTwoFactor': {
return $t('abp.oauth.errors.requiresTwoFactor');
}
// 需要更改密码
case 'PeriodicallyChangePassword':
case 'ShouldChangePasswordOnNextLogin': {
return $t('abp.oauth.errors.shouldChangePassword');
}
// 需要二次认证
case 'RequiresTwoFactor': {
return $t('abp.oauth.errors.requiresTwoFactor');
}
// Token已失效
case 'The token is no longer valid.': {
return $t('abp.oauth.errors.tokenHasExpired');

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

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

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

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

Loading…
Cancel
Save