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:
@@ -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,7 +10,7 @@ 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,
|
||||||
@@ -18,4 +18,12 @@ export const authApi = {
|
|||||||
})
|
})
|
||||||
.json<unknown>(),
|
.json<unknown>(),
|
||||||
}),
|
}),
|
||||||
|
loginWithJson: async (payload: LoginRequestBody, options?: RequestOptions) => ({
|
||||||
|
data: await httpClient
|
||||||
|
.post('Auth/login', {
|
||||||
|
json: payload,
|
||||||
|
signal: options?.signal,
|
||||||
|
})
|
||||||
|
.json<unknown>(),
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
|
|||||||
+50
-15
@@ -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_)
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
Reference in New Issue
Block a user