perf: 登录支持双重认证

This commit is contained in:
xiaojunnuo
2025-04-17 22:34:21 +08:00
parent 8e50e5dee3
commit 48aef25b3f
16 changed files with 132 additions and 55 deletions

View File

@@ -3,6 +3,17 @@ import { get } from "lodash-es";
import { errorLog, errorCreate } from "./tools";
import { env } from "/src/utils/util.env";
import { useUserStore } from "/@/store/user";
export class CodeError extends Error {
code: number;
data?: any;
constructor(message: string, code: number, data?: any) {
super(message);
this.code = code;
this.data = data;
}
}
/**
* @description 创建请求实例
*/
@@ -56,12 +67,13 @@ function createService() {
const errorMessage = dataAxios.msg || dataAxios.message || "未知错误";
// @ts-ignore
if (response?.config?.onError) {
// @ts-ignore
response.config.onError(new Error(errorMessage));
const err = new CodeError(errorMessage, dataAxios.code, dataAxios.data);
response.config.onError(err);
return;
}
//@ts-ignore
const showErrorNotify = response?.config?.showErrorNotify;
errorCreate(`${errorMessage}: ${response.config.url}`, showErrorNotify);
errorCreate(`${errorMessage}: ${response.config.url}`, showErrorNotify, dataAxios);
return dataAxios;
}
}

View File

@@ -4,6 +4,7 @@
* @param {String} defaultValue 默认值
*/
import { uiContext } from "@fast-crud/fast-crud";
import { CodeError } from "/@/api/service";
export function parse(jsonString = "{}", defaultValue = {}) {
let result = defaultValue;
@@ -68,8 +69,8 @@ export function errorLog(error: any, notify = true) {
* @description 创建一个错误
* @param {String} msg 错误信息
*/
export function errorCreate(msg: string, notify = true) {
const err = new Error(msg);
export function errorCreate(msg: string, notify = true, data?: any) {
const err = new CodeError(msg, data.code, data.data);
console.error("errorCreate", err);
if (notify) {
uiContext.get().notification.error({ message: err.message });

View File

@@ -149,7 +149,7 @@ export const certdResources = [
path: "/certd/mine/security",
component: "/certd/mine/security/index.vue",
meta: {
icon: "ion:locked-outline",
icon: "fluent:shield-keyhole-16-regular",
auth: true,
isMenu: true,
},

View File

@@ -66,3 +66,11 @@ export async function mine(): Promise<UserInfoRes> {
method: "post",
});
}
export async function loginByTwoFactor(data: any) {
return await request({
url: "/loginByTwoFactor",
method: "post",
data,
});
}

View File

@@ -51,7 +51,7 @@ export const useUserStore = defineStore({
setUserInfo(info: UserInfoRes) {
this.userInfo = info;
const userStore = vbenUserStore();
userStore.setUserInfo(info);
userStore.setUserInfo(info as any);
LocalStorage.set(USER_INFO_KEY, info);
},
resetState() {
@@ -71,23 +71,18 @@ export const useUserStore = defineStore({
* @description: login
*/
async login(loginType: string, params: LoginReq | SmsLoginReq): Promise<any> {
try {
let loginRes: any = null;
if (loginType === "sms") {
loginRes = await UserApi.loginBySms(params as SmsLoginReq);
} else {
loginRes = await UserApi.login(params as LoginReq);
}
const { token, expire } = loginRes;
// save token
this.setToken(token, expire);
// get user info
return await this.onLoginSuccess(loginRes);
} catch (error) {
console.error(error);
return null;
let loginRes: any = null;
if (loginType === "sms") {
loginRes = await UserApi.loginBySms(params as SmsLoginReq);
} else {
loginRes = await UserApi.login(params as LoginReq);
}
return await this.onLoginSuccess(loginRes);
},
async loginByTwoFactor(form: any) {
const loginRes = await UserApi.loginByTwoFactor(form);
return await this.onLoginSuccess(loginRes);
},
async getUserInfoAction(): Promise<UserInfoRes> {
const userInfo = await UserApi.mine();
@@ -100,9 +95,13 @@ export const useUserStore = defineStore({
},
async onLoginSuccess(loginData: any) {
const { token, expire } = loginData;
// save token
this.setToken(token, expire);
// get user info
// await this.getUserInfoAction();
// const userInfo = await this.getUserInfoAction();
mitter.emit("app.login", { token: loginData });
mitter.emit("app.login", { ...loginData });
await router.replace("/");
},

View File

@@ -9,7 +9,14 @@
<div class="flex mt-5">
<a-switch v-model:checked="formState.authenticator.enabled" :disabled="!settingsStore.isPlus" @change="onAuthenticatorEnabledChanged" />
<a-button v-if="formState.authenticator.enabled && formState.authenticator.verified" :disabled="authenticatorOpenRef" size="small" class="ml-5" type="primary" @click="authenticatorForm.open = true">
<a-button
v-if="formState.authenticator.enabled && formState.authenticator.verified"
:disabled="authenticatorOpenRef || !settingsStore.isPlus"
size="small"
class="ml-5"
type="primary"
@click="authenticatorForm.open = true"
>
重新绑定
</a-button>
@@ -53,7 +60,12 @@ defineOptions({
name: "UserSecurity",
});
const formState = reactive<Partial<UserTwoFactorSetting>>({});
const formState = reactive<Partial<UserTwoFactorSetting>>({
authenticator: {
enabled: false,
verified: false,
},
});
const authenticatorForm = reactive({
qrcodeSrc: "",

View File

@@ -1,6 +1,6 @@
<template>
<div class="main login-page">
<a-form ref="formRef" class="user-layout-login" name="custom-validation" :model="formState" v-bind="layout" @finish="handleFinish" @finish-failed="handleFinishFailed">
<a-form v-if="!twoFactor.loginId" ref="formRef" class="user-layout-login" name="custom-validation" :model="formState" v-bind="layout" @finish="handleFinish" @finish-failed="handleFinishFailed">
<!-- <div class="login-title">登录</div>-->
<a-tabs v-model:active-key="formState.loginType" :tab-bar-style="{ textAlign: 'center', borderBottom: 'unset' }">
<a-tab-pane key="password" tab="密码登录" :disabled="sysPublicSettings.passwordLoginEnabled !== true">
@@ -49,6 +49,23 @@
<router-link v-if="hasRegisterTypeEnabled()" class="register" :to="{ name: 'register' }"> 注册 </router-link>
</a-form-item>
</a-form>
<a-form v-else ref="twoFactorFormRef" class="user-layout-login" :model="twoFactor" v-bind="layout">
<div class="mb-10 flex flex-center">请打开您的Authenticator APP获取动态验证码</div>
<a-form-item name="verifyCode">
<a-input v-model:value="twoFactor.verifyCode" placeholder="请输入动态验证码" allow-clear>
<template #prefix>
<fs-icon icon="ion:lock-closed-outline"></fs-icon>
</template>
</a-input>
</a-form-item>
<a-form-item>
<loading-button type="primary" size="large" html-type="button" class="login-button" :click="handleTwoFactorSubmit">OTP验证登录</loading-button>
</a-form-item>
<a-form-item class="user-login-other">
<a class="register" @click="twoFactor.loginId = null"> 返回 </a>
</a-form-item>
</a-form>
</div>
</template>
<script lang="ts">
@@ -113,11 +130,28 @@ export default defineComponent({
},
};
const twoFactor = reactive({
loginId: "",
verifyCode: "",
});
const handleTwoFactorSubmit = async () => {
await userStore.loginByTwoFactor(twoFactor);
};
const handleFinish = async (values: any) => {
loading.value = true;
try {
const loginType = formState.loginType;
await userStore.login(loginType, toRaw(formState));
} catch (e: any) {
//@ts-ignore
if (e.code === 10020) {
//@ts-ignore
twoFactor.loginId = e.data;
} else {
throw e;
}
} finally {
loading.value = false;
}
@@ -150,6 +184,8 @@ export default defineComponent({
isLoginError,
sysPublicSettings,
hasRegisterTypeEnabled,
twoFactor,
handleTwoFactorSubmit,
};
},
});

View File

@@ -4,8 +4,7 @@
<h2>站点隐藏</h2>
<a-form-item label="启用站点隐藏" :name="['hidden', 'enabled']" :required="true">
<div class="flex">
<a-switch v-model:checked="formState.hidden.enabled" :disabled="!settingsStore.isPlus" />
<vip-button class="ml-5" mode="button"></vip-button>
<a-switch v-model:checked="formState.hidden.enabled" />
</div>
<div class="helper">