import type { LoginPayload, LoginRequestBody, LoginRequestFormat, 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' const defaultLoginRequestFormat: LoginRequestFormat = 'formData' interface LoginOptions { requestFormat?: LoginRequestFormat } function createLoginRequestBody(payload: LoginPayload): LoginRequestBody { return { UserID: payload.UserID, Password: payload.Password, ...(payload.captcha ? { DNTCaptchaInputText: payload.captcha.DNTCaptchaInputText, DNTCaptchaText: payload.captcha.DNTCaptchaText, DNTCaptchaToken: payload.captcha.DNTCaptchaToken, } : {}), } } function createLoginFormData(payload: LoginRequestBody) { const formData = new FormData() formData.append('UserID', payload.UserID) formData.append('Password', payload.Password) if (payload.DNTCaptchaInputText) { formData.append('DNTCaptchaInputText', payload.DNTCaptchaInputText) } if (payload.DNTCaptchaText) { formData.append('DNTCaptchaText', payload.DNTCaptchaText) } if (payload.DNTCaptchaToken) { formData.append('DNTCaptchaToken', payload.DNTCaptchaToken) } return formData } // - 只在 store 管理登入狀態:user/token/loading/error // - Component 不直接呼叫 API,避免狀態散落 // - token 單一來源:透過 tokenService 同步 ref + localStorage // - store 負責寫入/清除 token(login/logout) // - HTTP hooks 只讀 tokenService export const useAuthStore = defineStore('auth', () => { const user = ref(null) const token = tokenService.token const loading = ref(false) const error = ref(null) // 只針對 login 取消重複請求,避免競態與重複提交 const loginController = ref(null) const isAuthenticated = computed(() => !!token.value) const roles = computed(() => (user.value?.role ? [user.value.role] : [])) const login = async (payload: LoginPayload, options: LoginOptions = {}) => { loginController.value?.abort() loginController.value = new AbortController() loading.value = true error.value = null try { const requestBody = createLoginRequestBody(payload) const requestFormat = options.requestFormat ?? defaultLoginRequestFormat const requestOptions = { signal: loginController.value.signal } const { data } = requestFormat === 'json' ? await authApi.loginWithJson(requestBody, requestOptions) : await authApi.loginWithFormData(createLoginFormData(requestBody), requestOptions) const parseUser = (val: unknown): User | undefined => { if (!val || typeof val !== 'object') return const obj = val as Record 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 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 // 使用者token寫入 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 { user, token, loading, error, isAuthenticated, roles, login, logout, } })