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" # if use a sub directory, it must be end with "/", like "/admin/" but not "/admin"
VITE_BASE_URL=/ 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 # the prefix of the icon name
VITE_ICON_PREFIX=icon VITE_ICON_PREFIX=icon

View File

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

View File

@@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, h } from 'vue'; import { computed, h, ref } from 'vue';
import type { VNode } 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 { useAuthStore } from '@/store/modules/auth';
import { useRouterPush } from '@/hooks/common/router'; import { useRouterPush } from '@/hooks/common/router';
import { useSvgIcon } from '@/hooks/common/icon'; import { useSvgIcon } from '@/hooks/common/icon';
@@ -64,21 +64,35 @@ function logout() {
}); });
} }
function googleAuth() { function googleAuth() {
const topt = window.prompt('请输入账号登录密码:'); const topt = ref('');
if (!topt) { dialog.create({
window.$message?.error('请输入密码');
return;
}
const d = dialog.create({
title: '谷歌验证', title: '谷歌验证',
content: () => content: () =>
h(GoogleAuth, { h(NInput, {
password: topt, type: 'password',
onSuccess: () => { placeholder: '请输入账号登录密码',
d.destroy(); onUpdateValue: (value: string) => {
window.$message?.success('二步验证已开启'); 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 { VITE_SERVICE_BASE_URL } = import.meta.env;
const baseURL = import.meta.env.DEV ? window.location.origin : VITE_SERVICE_BASE_URL;
export const authClient = createAuthClient({ export const authClient = createAuthClient({
baseURL: VITE_SERVICE_BASE_URL, baseURL,
fetchOptions: { fetchOptions: {
credentials: 'include', credentials: 'include',
auth: { 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 { useRoute } from 'vue-router';
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { NInputOtp } from 'naive-ui';
import { useLoading } from '@sa/hooks'; import { useLoading } from '@sa/hooks';
import { fetchLogin } from '@/service/api'; import { authClient, fetchLogin, safeClient } from '@/service/api';
import { useRouterPush } from '@/hooks/common/router'; import { useRouterPush } from '@/hooks/common/router';
import { localStg } from '@/utils/storage'; import { localStg } from '@/utils/storage';
import { SetupStoreId } from '@/enum'; import { SetupStoreId } from '@/enum';
@@ -102,24 +103,40 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
const res = await fetchLogin(userName, password); const res = await fetchLogin(userName, password);
if (!res.error) { 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) { if (data.value) {
// Check if the tab needs to be cleared saveLogin(data.value, redirect);
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
}); });
} else {
saveLogin(res.data, redirect);
} }
} else { } else {
resetStore(); resetStore();
@@ -128,6 +145,28 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
endLoading(); 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) { async function loginByToken(data: Api.Auth.LoginToken) {
// 1. stored in the localStorage, the later requests need it in headers // 1. stored in the localStorage, the later requests need it in headers
localStg.set('token', data.token); localStg.set('token', data.token);

View File

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