diff --git a/app/common/library/AdminTotp.php b/app/common/library/AdminTotp.php index df569b4..06fc2af 100644 --- a/app/common/library/AdminTotp.php +++ b/app/common/library/AdminTotp.php @@ -53,13 +53,13 @@ class AdminTotp return self::tfa()->getQRCodeImageAsDataUri($label, $secret); } - public static function verifyCode(string $plainSecret, string $code): bool + public static function verifyCode(string $plainSecret, string $code, int $discrepancy = 2): bool { $code = trim($code); if (!preg_match('/^\d{6}$/', $code)) { return false; } - return self::tfa()->verifyCode($plainSecret, $code); + return self::tfa()->verifyCode($plainSecret, $code, $discrepancy); } public static function encryptSecret(string $plainSecret): string diff --git a/web/src/api/backend/index.ts b/web/src/api/backend/index.ts index 951b444..796a79a 100644 --- a/web/src/api/backend/index.ts +++ b/web/src/api/backend/index.ts @@ -27,19 +27,29 @@ export function login(method: 'get' | 'post', params: object = {}, axiosConfig: } export function totpVerify(params: object = {}) { - return createAxios({ - url: url + 'totpVerify', - data: params, - method: 'post', - }) + return createAxios( + { + url: url + 'totpVerify', + data: params, + method: 'post', + }, + { + cancelDuplicateRequest: false, + } + ) } export function totpBindInit(params: object = {}) { - return createAxios({ - url: url + 'totpBindInit', - data: params, - method: 'post', - }) + return createAxios( + { + url: url + 'totpBindInit', + data: params, + method: 'post', + }, + { + cancelDuplicateRequest: false, + } + ) } export function totpBindConfirm(params: object = {}) { @@ -50,6 +60,7 @@ export function totpBindConfirm(params: object = {}) { method: 'post', }, { + cancelDuplicateRequest: false, showSuccessMessage: true, } ) diff --git a/web/src/lang/backend/en/login.ts b/web/src/lang/backend/en/login.ts index 5d5648a..601f756 100644 --- a/web/src/lang/backend/en/login.ts +++ b/web/src/lang/backend/en/login.ts @@ -13,4 +13,5 @@ export default { 'Back to login': 'Back to login', 'Binding...': 'Binding...', 'Verifying...': 'Verifying...', + 'Login failed, please try again': 'Sign-in could not be completed. Please try again or contact support.', } diff --git a/web/src/lang/backend/zh-cn/login.ts b/web/src/lang/backend/zh-cn/login.ts index c66f249..d2043cb 100644 --- a/web/src/lang/backend/zh-cn/login.ts +++ b/web/src/lang/backend/zh-cn/login.ts @@ -13,4 +13,5 @@ export default { 'Back to login': '返回登录', 'Binding...': '绑定中...', 'Verifying...': '验证中...', + 'Login failed, please try again': '登录未完成,请重试或联系管理员', } diff --git a/web/src/router/index.ts b/web/src/router/index.ts index 354b9a1..8e3454d 100644 --- a/web/src/router/index.ts +++ b/web/src/router/index.ts @@ -91,6 +91,8 @@ router.afterEach(() => { ensureStandardHashUrl() if (window.existLoading) { loading.hide() + } else { + window.existLoading = false } NProgress.done() }) diff --git a/web/src/utils/loading.ts b/web/src/utils/loading.ts index 937cfa5..92b65d1 100644 --- a/web/src/utils/loading.ts +++ b/web/src/utils/loading.ts @@ -26,9 +26,11 @@ export const loading = { hide: () => { nextTick(() => { setTimeout(() => { - const el = document.querySelector('.block-loading') - el && el.parentNode?.removeChild(el) - }, 1000) + document.querySelectorAll('.block-loading').forEach((el) => { + el.parentNode?.removeChild(el) + }) + window.existLoading = false + }, 100) }) }, } diff --git a/web/src/views/backend/login.vue b/web/src/views/backend/login.vue index 88adcb6..ed16dc7 100644 --- a/web/src/views/backend/login.vue +++ b/web/src/views/backend/login.vue @@ -194,6 +194,28 @@ const unwrapLoginApiData = (res: anyObj | undefined): anyObj => { return res as anyObj } +/** 从 login / totpVerify / totpBindConfirm 响应中解析 userInfo */ +const extractLoginUserInfo = (res: anyObj | undefined): anyObj | undefined => { + const data = unwrapLoginApiData(res) + if (data.userInfo && typeof data.userInfo === 'object' && data.userInfo.token) { + return data.userInfo as anyObj + } + if (data.token) { + return data as anyObj + } + return undefined +} + +const finishLoginWithApiResponse = (res: anyObj | undefined) => { + const userInfo = extractLoginUserInfo(res) + if (userInfo) { + navigateAfterLogin(userInfo) + return true + } + handleLoginApiResult(res) + return false +} + const handleLoginApiResult = (res: anyObj | undefined) => { if (!res || typeof res !== 'object') { return @@ -211,7 +233,6 @@ const handleLoginApiResult = (res: anyObj | undefined) => { navigateAfterLogin(data.userInfo) return } - const tempToken = data.tempToken || data.temp_token if (data.type === 'need_totp' && tempToken) { state.step = 'totp' @@ -311,13 +332,16 @@ const resetToLogin = () => { const navigateAfterLogin = (userInfo: anyObj) => { if (!userInfo?.token) { + ElMessage.error(t('login.Login failed, please try again')) return } abortLoginPrefetch() - adminInfo.dataFill(userInfo, false) - nextTick(() => { - router.replace({ path: adminBaseRoutePath }) - }) + adminInfo.setToken(userInfo.token, 'auth') + if (userInfo.refresh_token) { + adminInfo.setToken(userInfo.refresh_token, 'refresh') + } + adminInfo.dataFill(userInfo, ['token', 'refresh_token']) + router.replace({ path: adminBaseRoutePath }) } onMounted(() => { @@ -365,13 +389,17 @@ const onSubmit = (captchaInfo = '') => { form.captchaInfo = captchaInfo login('post', form) .then((res) => { - handleLoginApiResult(res) + finishLoginWithApiResponse(res) }) .catch((err) => { if (err?.code === 'ERR_CANCELED' || err?.message === 'canceled') { return } - handleLoginApiResult(err as anyObj) + if (err && typeof err === 'object' && 'code' in err) { + handleLoginApiResult(err as anyObj) + return + } + console.log(err) }) .finally(() => { state.submitLoading = false @@ -400,7 +428,17 @@ const onTotpVerify = () => { keep: form.keep, }) .then((res) => { - navigateAfterLogin(res?.data?.userInfo) + if (!finishLoginWithApiResponse(res)) { + const code = res?.code + if (code === 1) { + ElMessage.error(t('login.Login failed, please try again')) + } + } + }) + .catch((err) => { + if (err && typeof err === 'object' && 'msg' in err && err.msg) { + ElMessage.error(String(err.msg)) + } }) .finally(() => { state.submitLoading = false @@ -419,7 +457,17 @@ const onBindConfirm = () => { keep: form.keep, }) .then((res) => { - navigateAfterLogin(res?.data?.userInfo) + if (!finishLoginWithApiResponse(res)) { + const code = res?.code + if (code === 1) { + ElMessage.error(t('login.Login failed, please try again')) + } + } + }) + .catch((err) => { + if (err && typeof err === 'object' && 'msg' in err && err.msg) { + ElMessage.error(String(err.msg)) + } }) .finally(() => { state.submitLoading = false diff --git a/web/src/views/backend/mall/playxConfig/index.vue b/web/src/views/backend/mall/playxConfig/index.vue index e0a499c..99587a0 100644 --- a/web/src/views/backend/mall/playxConfig/index.vue +++ b/web/src/views/backend/mall/playxConfig/index.vue @@ -21,8 +21,8 @@ v-model="form.unlock_ratio" :min="0" :max="100" - :step="0.01" - :precision="2" + :step="0.001" + :precision="3" controls-position="right" class="w100" /> @@ -82,8 +82,8 @@ v-model="form.return_ratio" :min="0" :max="100" - :step="0.01" - :precision="2" + :step="0.001" + :precision="3" controls-position="right" class="w100" :disabled="!form.daily_points_enabled" @@ -101,8 +101,8 @@ v-model="form.lifetime_points_ratio" :min="0" :max="100" - :step="0.01" - :precision="2" + :step="0.001" + :precision="3" controls-position="right" class="w100" :disabled="!form.lifetime_points_enabled"