fix: captcha 開關
This commit is contained in:
@@ -0,0 +1,84 @@
|
|||||||
|
import type { CaptchaResponse } from '@/types/api'
|
||||||
|
import { computed, ref, toValue, type MaybeRefOrGetter } from 'vue'
|
||||||
|
import { normalizeError } from '@/services/error'
|
||||||
|
import { authApi } from '@/services/modules/auth'
|
||||||
|
|
||||||
|
interface UseLoginCaptchaOptions {
|
||||||
|
enabled: MaybeRefOrGetter<boolean>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useLoginCaptcha(options: UseLoginCaptchaOptions) {
|
||||||
|
const captcha = ref<CaptchaResponse | null>(null)
|
||||||
|
const captchaValue = ref('')
|
||||||
|
const captchaLoading = ref(false)
|
||||||
|
const captchaErrorMessage = ref<string | null>(null)
|
||||||
|
const enabled = computed(() => toValue(options.enabled))
|
||||||
|
|
||||||
|
const formCaptcha = computed(() => {
|
||||||
|
if (!enabled.value || !captcha.value) return undefined
|
||||||
|
|
||||||
|
return {
|
||||||
|
imgUrl: captcha.value.dntCaptchaImgUrl,
|
||||||
|
id: captcha.value.dntCaptchaId,
|
||||||
|
tokenValue: captcha.value.dntCaptchaTokenValue,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
async function loadCaptcha() {
|
||||||
|
if (!enabled.value) return null
|
||||||
|
|
||||||
|
captchaLoading.value = true
|
||||||
|
captchaErrorMessage.value = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data } = await authApi.getCaptcha()
|
||||||
|
captcha.value = data
|
||||||
|
return data
|
||||||
|
} catch (error_) {
|
||||||
|
const normalizedError = normalizeError(error_)
|
||||||
|
captcha.value = null
|
||||||
|
captchaErrorMessage.value = normalizedError.message
|
||||||
|
throw normalizedError
|
||||||
|
} finally {
|
||||||
|
captchaLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshCaptcha() {
|
||||||
|
if (!enabled.value) return null
|
||||||
|
|
||||||
|
captchaValue.value = ''
|
||||||
|
return await loadCaptcha()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setCaptchaValue(value: string) {
|
||||||
|
if (!enabled.value) return
|
||||||
|
|
||||||
|
captchaValue.value = value
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLoginCaptchaPayload() {
|
||||||
|
if (!enabled.value) return undefined
|
||||||
|
|
||||||
|
if (!captcha.value?.dntCaptchaTextValue || !captcha.value?.dntCaptchaTokenValue) {
|
||||||
|
throw new Error('驗證碼資料缺失,請先刷新驗證碼')
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
DNTCaptchaInputText: captchaValue.value,
|
||||||
|
DNTCaptchaText: captcha.value.dntCaptchaTextValue,
|
||||||
|
DNTCaptchaToken: captcha.value.dntCaptchaTokenValue,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
captchaValue,
|
||||||
|
captchaLoading,
|
||||||
|
captchaErrorMessage,
|
||||||
|
formCaptcha,
|
||||||
|
loadCaptcha,
|
||||||
|
refreshCaptcha,
|
||||||
|
setCaptchaValue,
|
||||||
|
getLoginCaptchaPayload,
|
||||||
|
}
|
||||||
|
}
|
||||||
+9
-42
@@ -1,4 +1,4 @@
|
|||||||
import type { CaptchaResponse, LoginPayload, User } from '@/types/api'
|
import type { LoginPayload, User } from '@/types/api'
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { normalizeError } from '@/services/error'
|
import { normalizeError } from '@/services/error'
|
||||||
@@ -16,32 +16,12 @@ export const useAuthStore = defineStore('auth', () => {
|
|||||||
const token = tokenService.token
|
const token = tokenService.token
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const error = ref<string | null>(null)
|
const error = ref<string | null>(null)
|
||||||
const captcha = ref<CaptchaResponse | null>(null)
|
|
||||||
const captchaLoading = ref(false)
|
|
||||||
const captchaErrorMessage = ref<string | null>(null)
|
|
||||||
// 只針對 login 取消重複請求,避免競態與重複提交
|
// 只針對 login 取消重複請求,避免競態與重複提交
|
||||||
const loginController = ref<AbortController | null>(null)
|
const loginController = ref<AbortController | null>(null)
|
||||||
|
|
||||||
const isAuthenticated = computed(() => !!token.value)
|
const isAuthenticated = computed(() => !!token.value)
|
||||||
const roles = computed(() => (user.value?.role ? [user.value.role] : []))
|
const roles = computed(() => (user.value?.role ? [user.value.role] : []))
|
||||||
|
|
||||||
const getCaptcha = async () => {
|
|
||||||
captchaLoading.value = true
|
|
||||||
captchaErrorMessage.value = null
|
|
||||||
try {
|
|
||||||
const { data } = await authApi.getCaptcha()
|
|
||||||
captcha.value = data
|
|
||||||
return data
|
|
||||||
} catch (error_) {
|
|
||||||
const normalizedError = normalizeError(error_)
|
|
||||||
captcha.value = null
|
|
||||||
captchaErrorMessage.value = normalizedError.message
|
|
||||||
throw normalizedError
|
|
||||||
} finally {
|
|
||||||
captchaLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const login = async (payload: LoginPayload) => {
|
const login = async (payload: LoginPayload) => {
|
||||||
loginController.value?.abort()
|
loginController.value?.abort()
|
||||||
loginController.value = new AbortController()
|
loginController.value = new AbortController()
|
||||||
@@ -49,24 +29,15 @@ export const useAuthStore = defineStore('auth', () => {
|
|||||||
error.value = null
|
error.value = null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!captcha.value?.dntCaptchaTextValue || !captcha.value?.dntCaptchaTokenValue) {
|
|
||||||
throw new Error('驗證碼資料缺失,請先刷新驗證碼')
|
|
||||||
}
|
|
||||||
|
|
||||||
const requestPayload = {
|
|
||||||
UserID: payload.UserID,
|
|
||||||
Password: payload.Password,
|
|
||||||
DNTCaptchaInputText: payload.DNTCaptchaInputText,
|
|
||||||
DNTCaptchaText: captcha.value.dntCaptchaTextValue,
|
|
||||||
DNTCaptchaToken: captcha.value.dntCaptchaTokenValue,
|
|
||||||
}
|
|
||||||
|
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
formData.append('UserID', requestPayload.UserID)
|
formData.append('UserID', payload.UserID)
|
||||||
formData.append('Password', requestPayload.Password)
|
formData.append('Password', payload.Password)
|
||||||
formData.append('DNTCaptchaInputText', requestPayload.DNTCaptchaInputText)
|
|
||||||
formData.append('DNTCaptchaText', requestPayload.DNTCaptchaText)
|
if (payload.captcha) {
|
||||||
formData.append('DNTCaptchaToken', requestPayload.DNTCaptchaToken)
|
formData.append('DNTCaptchaInputText', payload.captcha.DNTCaptchaInputText)
|
||||||
|
formData.append('DNTCaptchaText', payload.captcha.DNTCaptchaText)
|
||||||
|
formData.append('DNTCaptchaToken', payload.captcha.DNTCaptchaToken)
|
||||||
|
}
|
||||||
|
|
||||||
const { data } = await authApi.login(formData, {
|
const { data } = await authApi.login(formData, {
|
||||||
signal: loginController.value.signal,
|
signal: loginController.value.signal,
|
||||||
@@ -130,10 +101,6 @@ export const useAuthStore = defineStore('auth', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getCaptcha,
|
|
||||||
captcha,
|
|
||||||
captchaLoading,
|
|
||||||
captchaErrorMessage,
|
|
||||||
user,
|
user,
|
||||||
token,
|
token,
|
||||||
loading,
|
loading,
|
||||||
|
|||||||
+7
-1
@@ -16,8 +16,14 @@ export interface CaptchaResponse {
|
|||||||
dntCaptchaTextValue: string
|
dntCaptchaTextValue: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface LoginCaptchaPayload {
|
||||||
|
DNTCaptchaInputText: string
|
||||||
|
DNTCaptchaText: string
|
||||||
|
DNTCaptchaToken: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface LoginPayload {
|
export interface LoginPayload {
|
||||||
UserID: string
|
UserID: string
|
||||||
Password: string
|
Password: string
|
||||||
DNTCaptchaInputText: string
|
captcha?: LoginCaptchaPayload
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-19
@@ -58,6 +58,7 @@ import { useI18n } from 'vue-i18n'
|
|||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import HyakkaouAcademyImage from '@/assets/logo.png'
|
import HyakkaouAcademyImage from '@/assets/logo.png'
|
||||||
import PageLogin from '@/components/PageLogin.vue'
|
import PageLogin from '@/components/PageLogin.vue'
|
||||||
|
import { useLoginCaptcha } from '@/composables/useLoginCaptcha'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
import {
|
import {
|
||||||
type LoginAnnouncementListItem,
|
type LoginAnnouncementListItem,
|
||||||
@@ -94,6 +95,7 @@ const formPositionLayout = ref<LayoutType>('side-left')
|
|||||||
|
|
||||||
// 功能開關:是否啟用驗證碼
|
// 功能開關:是否啟用驗證碼
|
||||||
const withCaptcha = ref(true)
|
const withCaptcha = ref(true)
|
||||||
|
const loginCaptcha = useLoginCaptcha({ enabled: withCaptcha })
|
||||||
|
|
||||||
// 文字內容(i18n)
|
// 文字內容(i18n)
|
||||||
const title = computed(() => t('pages.login.title'))
|
const title = computed(() => t('pages.login.title'))
|
||||||
@@ -117,9 +119,6 @@ const forgotPasswordHref = ref('/forgot-password')
|
|||||||
const forgotPasswordTarget = ref<string | undefined>(undefined)
|
const forgotPasswordTarget = ref<string | undefined>(undefined)
|
||||||
// 記住帳號的 localStorage key
|
// 記住帳號的 localStorage key
|
||||||
const rememberStorageKey = ref('login.remember.username')
|
const rememberStorageKey = ref('login.remember.username')
|
||||||
// 驗證碼 API
|
|
||||||
const captchaValue = ref('')
|
|
||||||
|
|
||||||
// 驗證與對話框狀態
|
// 驗證與對話框狀態
|
||||||
const dialogVisible = ref(false)
|
const dialogVisible = ref(false)
|
||||||
const dialogTitle = ref('')
|
const dialogTitle = ref('')
|
||||||
@@ -155,16 +154,10 @@ const form = computed(() => ({
|
|||||||
rememberStorageKey: rememberStorageKey.value,
|
rememberStorageKey: rememberStorageKey.value,
|
||||||
// 功能開關:是否顯示驗證碼
|
// 功能開關:是否顯示驗證碼
|
||||||
withCaptcha: withCaptcha.value,
|
withCaptcha: withCaptcha.value,
|
||||||
captcha: authStore.captcha
|
captcha: loginCaptcha.formCaptcha.value,
|
||||||
? {
|
captchaValue: loginCaptcha.captchaValue.value,
|
||||||
imgUrl: authStore.captcha.dntCaptchaImgUrl,
|
captchaLoading: loginCaptcha.captchaLoading.value,
|
||||||
id: authStore.captcha.dntCaptchaId,
|
captchaErrorMessage: loginCaptcha.captchaErrorMessage.value ?? '',
|
||||||
tokenValue: authStore.captcha.dntCaptchaTokenValue,
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
captchaValue: captchaValue.value,
|
|
||||||
captchaLoading: authStore.captchaLoading,
|
|
||||||
captchaErrorMessage: authStore.captchaErrorMessage ?? '',
|
|
||||||
captchaVerified: false,
|
captchaVerified: false,
|
||||||
forgotPassword: {
|
forgotPassword: {
|
||||||
text: forgotPasswordText.value,
|
text: forgotPasswordText.value,
|
||||||
@@ -192,12 +185,11 @@ function handleChangeLocale(nextLocale: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleCaptchaRefresh() {
|
async function handleCaptchaRefresh() {
|
||||||
captchaValue.value = ''
|
await loginCaptcha.refreshCaptcha().catch(() => undefined)
|
||||||
await authStore.getCaptcha()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCaptchaChange(value: string) {
|
function handleCaptchaChange(value: string) {
|
||||||
captchaValue.value = value
|
loginCaptcha.setCaptchaValue(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleToggleLayout() {
|
function handleToggleLayout() {
|
||||||
@@ -213,7 +205,7 @@ function handleSelectAnnouncement(item: LoginAnnouncementListItem) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function onLogin(data: Record<string, unknown>) {
|
async function onLogin(data: Record<string, unknown>) {
|
||||||
if (withCaptcha.value && !captchaValue.value) {
|
if (withCaptcha.value && !loginCaptcha.captchaValue.value) {
|
||||||
dialogTitle.value = t('common.notice')
|
dialogTitle.value = t('common.notice')
|
||||||
dialogMessage.value = t('pages.login.alert.verifyRequired')
|
dialogMessage.value = t('pages.login.alert.verifyRequired')
|
||||||
dialogVisible.value = true
|
dialogVisible.value = true
|
||||||
@@ -234,7 +226,7 @@ async function onLogin(data: Record<string, unknown>) {
|
|||||||
await authStore.login({
|
await authStore.login({
|
||||||
UserID: userId,
|
UserID: userId,
|
||||||
Password: password,
|
Password: password,
|
||||||
DNTCaptchaInputText: captchaValue.value,
|
captcha: loginCaptcha.getLoginCaptchaPayload(),
|
||||||
})
|
})
|
||||||
|
|
||||||
menuStore.getMenu(authStore.user?.id ?? '')
|
menuStore.getMenu(authStore.user?.id ?? '')
|
||||||
@@ -262,6 +254,6 @@ async function onLogin(data: Record<string, unknown>) {
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loginAnnouncementsStore.hydrate()
|
loginAnnouncementsStore.hydrate()
|
||||||
loginAnnouncementsStore.fetchMobileAnnouncements()
|
loginAnnouncementsStore.fetchMobileAnnouncements()
|
||||||
authStore.getCaptcha()
|
void loginCaptcha.loadCaptcha().catch(() => undefined)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user