Files
skt-vuetify-templates/src/stores/auth.ts
T
skytek_xinliang 71683482e1 refactor: ky
2026-05-07 11:17:30 +08:00

147 lines
4.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { CaptchaResponse, LoginPayload, User } from '@/types/api'
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
import { normalizeError } from '@/services/error'
import { authApi } from '@/services/modules/auth'
import { tokenService } from '@/services/token'
import { useMenuStore } from '@/stores/menu'
// - 只在 store 管理登入狀態:user/token/loading/error
// - Component 不直接呼叫 API,避免狀態散落
// - token 單一來源:透過 tokenService 同步 ref + localStorage
// - store 負責寫入/清除 tokenlogin/logout
// - HTTP hooks 只讀 tokenService
export const useAuthStore = defineStore('auth', () => {
const user = ref<User | null>(null)
const token = tokenService.token
const loading = ref(false)
const error = ref<string | null>(null)
const captcha = ref<CaptchaResponse | null>(null)
const captchaLoading = ref(false)
const captchaErrorMessage = ref<string | null>(null)
// 只針對 login 取消重複請求,避免競態與重複提交
const loginController = ref<AbortController | null>(null)
const isAuthenticated = computed(() => !!token.value)
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) => {
loginController.value?.abort()
loginController.value = new AbortController()
loading.value = true
error.value = null
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()
formData.append('UserID', requestPayload.UserID)
formData.append('Password', requestPayload.Password)
formData.append('DNTCaptchaInputText', requestPayload.DNTCaptchaInputText)
formData.append('DNTCaptchaText', requestPayload.DNTCaptchaText)
formData.append('DNTCaptchaToken', requestPayload.DNTCaptchaToken)
const { data } = await authApi.login(formData, {
signal: loginController.value.signal,
})
const parseUser = (val: unknown): User | undefined => {
if (!val || typeof val !== 'object') return
const obj = val as Record<string, unknown>
const id = obj.id
const name = obj.name
const role = obj.role
if (typeof id !== 'string' || typeof name !== 'string' || typeof role !== 'string') return
return { id, name, role }
}
const parseLoginResult = (
raw: unknown
): {
accessToken?: string
tokenType?: string
expiresIn?: number
user?: User
message?: string
} => {
if (!raw || typeof raw !== 'object') return {}
const obj = raw as Record<string, unknown>
const accessToken = typeof obj.access_token === 'string' ? obj.access_token : undefined
const tokenType = typeof obj.token_type === 'string' ? obj.token_type : undefined
const expiresIn = typeof obj.expires_in === 'number' ? obj.expires_in : undefined
const user = parseUser(obj.user)
const message = typeof obj.message === 'string' ? obj.message : undefined
return { accessToken, tokenType, expiresIn, user, message }
}
const result = parseLoginResult(data)
if (!result.accessToken) {
throw new Error(result.message || '登入回傳缺少 access_token')
}
user.value = result.user ?? null
tokenService.setToken(result.accessToken)
} catch (error_) {
const normalizedError = normalizeError(error_)
if (normalizedError.name !== 'CanceledRequestError') {
error.value = normalizedError.message
}
throw normalizedError
} finally {
loading.value = false
loginController.value = null
}
}
const logout = () => {
user.value = null
tokenService.clearToken()
useMenuStore().clear()
}
return {
getCaptcha,
captcha,
captchaLoading,
captchaErrorMessage,
user,
token,
loading,
error,
isAuthenticated,
roles,
login,
logout,
}
})