feat(auth): support JSON and form-data login requests

Split the auth login API into format-specific methods and add request
format selection in the auth store. Build a shared login request body so
captcha fields can be sent consistently as either JSON or FormData.feat(auth): support JSON and form-data login requests

Split the auth login API into format-specific methods and add request
format selection in the auth store. Build a shared login request body so
captcha fields can be sent consistently as either JSON or FormData.
This commit is contained in:
skytek_xinliang
2026-05-25 13:55:47 +08:00
parent ec62fcee51
commit b5be5b4448
3 changed files with 71 additions and 18 deletions
+11 -3
View File
@@ -1,4 +1,4 @@
import type { CaptchaResponse } from '@/types/api' import type { CaptchaResponse, LoginRequestBody } from '@/types/api'
import { httpClient } from '../client' import { httpClient } from '../client'
export interface RequestOptions { export interface RequestOptions {
@@ -10,11 +10,19 @@ export const authApi = {
getCaptcha: async () => ({ getCaptcha: async () => ({
data: await httpClient.get('Auth/get-captcha').json<CaptchaResponse>(), data: await httpClient.get('Auth/get-captcha').json<CaptchaResponse>(),
}), }),
login: async (payload: FormData, options?: RequestOptions) => ({ loginWithFormData: async (payload: FormData, options?: RequestOptions) => ({
data: await httpClient data: await httpClient
.post('Auth/login', { .post('Auth/login', {
body: payload, body: payload,
signal: options?.signal, signal: options?.signal,
})
.json<unknown>(),
}),
loginWithJson: async (payload: LoginRequestBody, options?: RequestOptions) => ({
data: await httpClient
.post('Auth/login', {
json: payload,
signal: options?.signal,
}) })
.json<unknown>(), .json<unknown>(),
}), }),
+50 -15
View File
@@ -1,4 +1,4 @@
import type { LoginPayload, User } from '@/types/api' import type { LoginPayload, LoginRequestBody, LoginRequestFormat, 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'
@@ -6,6 +6,46 @@ import { authApi } from '@/services/modules/auth'
import { tokenService } from '@/services/token' import { tokenService } from '@/services/token'
import { useMenuStore } from '@/stores/menu' 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 // - 只在 store 管理登入狀態:user/token/loading/error
// - Component 不直接呼叫 API,避免狀態散落 // - Component 不直接呼叫 API,避免狀態散落
// - token 單一來源:透過 tokenService 同步 ref + localStorage // - token 單一來源:透過 tokenService 同步 ref + localStorage
@@ -22,26 +62,20 @@ export const useAuthStore = defineStore('auth', () => {
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 login = async (payload: LoginPayload) => { const login = async (payload: LoginPayload, options: LoginOptions = {}) => {
loginController.value?.abort() loginController.value?.abort()
loginController.value = new AbortController() loginController.value = new AbortController()
loading.value = true loading.value = true
error.value = null error.value = null
try { try {
const formData = new FormData() const requestBody = createLoginRequestBody(payload)
formData.append('UserID', payload.UserID) const requestFormat = options.requestFormat ?? defaultLoginRequestFormat
formData.append('Password', payload.Password) const requestOptions = { signal: loginController.value.signal }
const { data } =
if (payload.captcha) { requestFormat === 'json'
formData.append('DNTCaptchaInputText', payload.captcha.DNTCaptchaInputText) ? await authApi.loginWithJson(requestBody, requestOptions)
formData.append('DNTCaptchaText', payload.captcha.DNTCaptchaText) : await authApi.loginWithFormData(createLoginFormData(requestBody), requestOptions)
formData.append('DNTCaptchaToken', payload.captcha.DNTCaptchaToken)
}
const { data } = await authApi.login(formData, {
signal: loginController.value.signal,
})
const parseUser = (val: unknown): User | undefined => { const parseUser = (val: unknown): User | undefined => {
if (!val || typeof val !== 'object') return if (!val || typeof val !== 'object') return
@@ -81,6 +115,7 @@ export const useAuthStore = defineStore('auth', () => {
} }
user.value = result.user ?? null user.value = result.user ?? null
// 使用者token寫入
tokenService.setToken(result.accessToken) tokenService.setToken(result.accessToken)
} catch (error_) { } catch (error_) {
const normalizedError = normalizeError(error_) const normalizedError = normalizeError(error_)
+10
View File
@@ -27,3 +27,13 @@ export interface LoginPayload {
Password: string Password: string
captcha?: LoginCaptchaPayload captcha?: LoginCaptchaPayload
} }
export interface LoginRequestBody {
UserID: string
Password: string
DNTCaptchaInputText?: string
DNTCaptchaText?: string
DNTCaptchaToken?: string
}
export type LoginRequestFormat = 'formData' | 'json'