feat: 更新应用标题和描述,调整谷歌验证和登录流程以支持二步验证

This commit is contained in:
2026-01-20 07:09:11 +07:00
parent 220b14be30
commit 322530e45f
6 changed files with 99 additions and 37 deletions

4
.env
View File

@@ -2,9 +2,9 @@
# if use a sub directory, it must be end with "/", like "/admin/" but not "/admin"
VITE_BASE_URL=/
VITE_APP_TITLE=RiwsanAdmin
VITE_APP_TITLE=Financial
VITE_APP_DESC=RiwsanAdmin is trade palatform admin system
VITE_APP_DESC=Financial is trade palatform admin system
# the prefix of the icon name
VITE_ICON_PREFIX=icon

View File

@@ -20,6 +20,7 @@ const totpCode = ref('');
const loading = ref(false);
const verifying = ref(false);
const disabling = ref(false);
const { VITE_APP_TITLE } = import.meta.env;
function generateQRCode(url: string) {
return `https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=${encodeURIComponent(url)}`;
@@ -35,7 +36,7 @@ async function enableTwoFactor() {
const { data } = await safeClient(
authClient.twoFactor.enable({
password: props.password,
issuer: 'my-app-name'
issuer: VITE_APP_TITLE || 'financial-admin'
})
);
loading.value = false;

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import { computed, h } from 'vue';
import { computed, h, ref } from 'vue';
import type { VNode } from 'vue';
import { useDialog } from 'naive-ui';
import { NInput, useDialog } from 'naive-ui';
import { useAuthStore } from '@/store/modules/auth';
import { useRouterPush } from '@/hooks/common/router';
import { useSvgIcon } from '@/hooks/common/icon';
@@ -64,21 +64,35 @@ function logout() {
});
}
function googleAuth() {
const topt = window.prompt('请输入账号登录密码:');
if (!topt) {
window.$message?.error('请输入密码');
return;
}
const d = dialog.create({
const topt = ref('');
dialog.create({
title: '谷歌验证',
content: () =>
h(GoogleAuth, {
password: topt,
onSuccess: () => {
d.destroy();
window.$message?.success('二步验证已开启');
h(NInput, {
type: 'password',
placeholder: '请输入账号登录密码',
onUpdateValue: (value: string) => {
topt.value = value;
}
})
}),
positiveText: '下一步',
negativeText: '取消',
onPositiveClick: () => {
if (!topt.value) {
window.$message?.error('请输入密码');
return false;
}
const d = dialog.create({
title: '谷歌验证',
content: () =>
h(GoogleAuth, {
password: topt.value,
onSuccess: () => {
d.destroy();
}
})
});
}
});
}

View File

@@ -5,8 +5,10 @@ import { request } from '../request';
const { VITE_SERVICE_BASE_URL } = import.meta.env;
const baseURL = import.meta.env.DEV ? window.location.origin : VITE_SERVICE_BASE_URL;
export const authClient = createAuthClient({
baseURL: VITE_SERVICE_BASE_URL,
baseURL,
fetchOptions: {
credentials: 'include',
auth: {

View File

@@ -1,8 +1,9 @@
import { computed, reactive, ref } from 'vue';
import { computed, h, reactive, ref } from 'vue';
import { useRoute } from 'vue-router';
import { defineStore } from 'pinia';
import { NInputOtp } from 'naive-ui';
import { useLoading } from '@sa/hooks';
import { fetchLogin } from '@/service/api';
import { authClient, fetchLogin, safeClient } from '@/service/api';
import { useRouterPush } from '@/hooks/common/router';
import { localStg } from '@/utils/storage';
import { SetupStoreId } from '@/enum';
@@ -102,24 +103,40 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
const res = await fetchLogin(userName, password);
if (!res.error) {
const pass = await loginByToken(res.data);
if ('twoFactorRedirect' in res.data && res.data.twoFactorRedirect) {
const topt = ref('');
window.$dialog?.create({
title: '谷歌验证',
content: () =>
h(NInputOtp, {
style: { margin: '30px 0' },
size: 'large',
allowInput: value => !value || /^\d+$/.test(value),
onUpdateValue: value => {
topt.value = value.join('');
}
}),
positiveText: '下一步',
negativeText: '取消',
onPositiveClick: async () => {
if (!topt.value) {
window.$message?.error('请输入密码');
} else {
const { data } = await safeClient(
authClient.twoFactor.verifyTotp({
code: topt.value, // required
trustDevice: false // 管理员登录不建议信任设备,以提高安全性
})
);
if (pass) {
// Check if the tab needs to be cleared
const isClear = checkTabClear();
let needRedirect = redirect;
if (isClear) {
// If the tab needs to be cleared,it means we don't need to redirect.
needRedirect = false;
}
await redirectFromLogin(needRedirect);
window.$notification?.success({
title: $t('page.login.common.loginSuccess'),
content: $t('page.login.common.welcomeBack', { userName: userInfo.name }),
duration: 4500
if (data.value) {
saveLogin(data.value, redirect);
}
}
}
});
} else {
saveLogin(res.data, redirect);
}
} else {
resetStore();
@@ -128,6 +145,28 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
endLoading();
}
async function saveLogin(data: Api.Auth.LoginToken, redirect = true) {
const pass = await loginByToken(data);
if (pass) {
// Check if the tab needs to be cleared
const isClear = checkTabClear();
let needRedirect = redirect;
if (isClear) {
// If the tab needs to be cleared,it means we don't need to redirect.
needRedirect = false;
}
await redirectFromLogin(needRedirect);
window.$notification?.success({
title: $t('page.login.common.loginSuccess'),
content: $t('page.login.common.welcomeBack', { userName: userInfo.name }),
duration: 4500
});
}
}
async function loginByToken(data: Api.Auth.LoginToken) {
// 1. stored in the localStorage, the later requests need it in headers
localStg.set('token', data.token);

View File

@@ -35,7 +35,13 @@ export default defineConfig(configEnv => {
host: '0.0.0.0',
port: 9527,
open: true,
proxy: createViteProxy(viteEnv, enableProxy)
proxy: {
'/api': {
target: viteEnv.VITE_SERVICE_BASE_URL,
changeOrigin: true,
ws: true
}
}
},
preview: {
port: 9725