feat(frontend): unify authenticated requests with 401 session handling
- add utils/session.ts: on 401 clear auth state and redirect to SSO or /login with return path; singleton promise prevents duplicate redirects; typed AuthenticationRequiredError for callers - add api/authRequest.ts: shared generic authRequest with token injection, response parsing and 401 hook - migrate businessLine/containerService/delivery/machine/subsystemAuth/taskLog api modules to the shared helper, removing six duplicated implementations; keep getToken only for EventSource query-param auth - Catalog: silence session-expiry error in instance-name precheck; prefer mysql delivery target by exact template name (matches backend DELIVERY_MYSQL_TEMPLATE_NAME default)
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
import { getToken } from '@/utils/auth'
|
||||
import { handleUnauthorized } from '@/utils/session'
|
||||
|
||||
type JsonObject = Record<string, any>
|
||||
|
||||
export async function authRequest<T extends JsonObject = JsonObject>(path: string, init: RequestInit = {}): Promise<T> {
|
||||
const token = getToken()
|
||||
const response = await fetch(path, {
|
||||
...init,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
...init.headers,
|
||||
},
|
||||
})
|
||||
const text = await response.text()
|
||||
const data = parseResponseBody(text)
|
||||
if (!response.ok) {
|
||||
if (response.status === 401) {
|
||||
return handleUnauthorized()
|
||||
}
|
||||
const message = data?.error || data?.message || text || `HTTP ${response.status}`
|
||||
throw new Error(message)
|
||||
}
|
||||
return (data || {}) as T
|
||||
}
|
||||
|
||||
function parseResponseBody(text: string): JsonObject {
|
||||
if (!text.trim()) {
|
||||
return {}
|
||||
}
|
||||
try {
|
||||
return JSON.parse(text) as JsonObject
|
||||
} catch {
|
||||
return { error: text }
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getToken } from '@/utils/auth'
|
||||
import { authRequest } from '@/api/authRequest'
|
||||
|
||||
export interface BusinessLine {
|
||||
id: number
|
||||
@@ -21,41 +21,31 @@ export interface SinaOrganization {
|
||||
|
||||
export const businessLineApi = {
|
||||
async listMine(): Promise<BusinessLine[]> {
|
||||
const token = getToken()
|
||||
const response = await fetch('/auth/api/v1/business-lines', {
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
},
|
||||
})
|
||||
const data = await response.json().catch(() => ({}))
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || `HTTP ${response.status}`)
|
||||
}
|
||||
const data = await authRequest('/auth/api/v1/business-lines')
|
||||
return Array.isArray(data.items) ? data.items : []
|
||||
},
|
||||
|
||||
async listAll(): Promise<BusinessLine[]> {
|
||||
const data = await request('/auth/api/v1/business-lines/all')
|
||||
const data = await authRequest('/auth/api/v1/business-lines/all')
|
||||
return Array.isArray(data.items) ? data.items : []
|
||||
},
|
||||
|
||||
async create(name: string): Promise<BusinessLine> {
|
||||
return request('/auth/api/v1/business-lines', {
|
||||
return authRequest('/auth/api/v1/business-lines', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ name }),
|
||||
})
|
||||
},
|
||||
|
||||
async update(id: number, name: string): Promise<BusinessLine> {
|
||||
return request(`/auth/api/v1/business-lines/${id}`, {
|
||||
return authRequest(`/auth/api/v1/business-lines/${id}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ name }),
|
||||
})
|
||||
},
|
||||
|
||||
async remove(id: number): Promise<void> {
|
||||
await request(`/auth/api/v1/business-lines/${id}`, {
|
||||
await authRequest(`/auth/api/v1/business-lines/${id}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
},
|
||||
@@ -65,14 +55,14 @@ export const businessLineApi = {
|
||||
target_user_id: number
|
||||
target_business_line_id: number
|
||||
}): Promise<void> {
|
||||
await request('/auth/api/v1/business-lines/authorizations', {
|
||||
await authRequest('/auth/api/v1/business-lines/authorizations', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
},
|
||||
|
||||
async listWayneNamespaces(): Promise<WayneNamespace[]> {
|
||||
const data = await request('/auth/api/v1/wayne/namespaces')
|
||||
const data = await authRequest('/auth/api/v1/wayne/namespaces')
|
||||
const items = Array.isArray(data.data?.list) ? data.data.list : []
|
||||
return items.map((item: any) => ({
|
||||
id: Number(item.id),
|
||||
@@ -82,12 +72,12 @@ export const businessLineApi = {
|
||||
},
|
||||
|
||||
async listMappedWayneNamespaces(businessLineId: number): Promise<WayneNamespace[]> {
|
||||
const data = await request(`/auth/api/v1/business-lines/${businessLineId}/wayne-namespaces`)
|
||||
const data = await authRequest(`/auth/api/v1/business-lines/${businessLineId}/wayne-namespaces`)
|
||||
return Array.isArray(data.items) ? data.items : []
|
||||
},
|
||||
|
||||
async replaceMappedWayneNamespaces(businessLineId: number, namespaces: WayneNamespace[]): Promise<void> {
|
||||
await request(`/auth/api/v1/business-lines/${businessLineId}/wayne-namespaces`, {
|
||||
await authRequest(`/auth/api/v1/business-lines/${businessLineId}/wayne-namespaces`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ namespaces }),
|
||||
})
|
||||
@@ -99,37 +89,19 @@ export const businessLineApi = {
|
||||
params.set('keyword', keyword.trim())
|
||||
}
|
||||
const suffix = params.toString() ? `?${params.toString()}` : ''
|
||||
const data = await request(`/auth/api/v1/business-lines/${businessLineId}/sina-organizations${suffix}`)
|
||||
const data = await authRequest(`/auth/api/v1/business-lines/${businessLineId}/sina-organizations${suffix}`)
|
||||
return Array.isArray(data.items) ? data.items : []
|
||||
},
|
||||
|
||||
async listMappedSinaOrganizations(businessLineId: number): Promise<SinaOrganization[]> {
|
||||
const data = await request(`/auth/api/v1/business-lines/${businessLineId}/sina-organization-mappings`)
|
||||
const data = await authRequest(`/auth/api/v1/business-lines/${businessLineId}/sina-organization-mappings`)
|
||||
return Array.isArray(data.items) ? data.items : []
|
||||
},
|
||||
|
||||
async replaceMappedSinaOrganizations(businessLineId: number, organizations: SinaOrganization[]): Promise<void> {
|
||||
await request(`/auth/api/v1/business-lines/${businessLineId}/sina-organization-mappings`, {
|
||||
await authRequest(`/auth/api/v1/business-lines/${businessLineId}/sina-organization-mappings`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ organizations }),
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
async function request(path: string, init: RequestInit = {}) {
|
||||
const token = getToken()
|
||||
const response = await fetch(path, {
|
||||
...init,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
...init.headers,
|
||||
},
|
||||
})
|
||||
const data = await response.json().catch(() => ({}))
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || `HTTP ${response.status}`)
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getToken } from '@/utils/auth'
|
||||
import { authRequest } from '@/api/authRequest'
|
||||
|
||||
export interface ContainerServiceSummary {
|
||||
businessLineId: number
|
||||
@@ -102,34 +102,3 @@ export const containerServiceApi = {
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
async function authRequest(path: string, init: RequestInit = {}) {
|
||||
const token = getToken()
|
||||
const response = await fetch(path, {
|
||||
...init,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
...init.headers,
|
||||
},
|
||||
})
|
||||
const text = await response.text()
|
||||
const data = parseResponseBody(text)
|
||||
if (!response.ok) {
|
||||
const message = data?.error || data?.message || text || `HTTP ${response.status}`
|
||||
throw new Error(message)
|
||||
}
|
||||
return data || {}
|
||||
}
|
||||
|
||||
function parseResponseBody(text: string) {
|
||||
if (!text.trim()) {
|
||||
return {}
|
||||
}
|
||||
try {
|
||||
return JSON.parse(text)
|
||||
} catch {
|
||||
return { error: text }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { getToken } from '@/utils/auth'
|
||||
import { authRequest } from '@/api/authRequest'
|
||||
|
||||
export interface DeliveryTarget {
|
||||
id: number
|
||||
@@ -213,34 +214,3 @@ function createIdempotencyKey(payload: CreateMySQLDeliveryPayload) {
|
||||
Date.now(),
|
||||
].join(':')
|
||||
}
|
||||
|
||||
async function authRequest(path: string, init: RequestInit = {}) {
|
||||
const token = getToken()
|
||||
const response = await fetch(path, {
|
||||
...init,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
...init.headers,
|
||||
},
|
||||
})
|
||||
const text = await response.text()
|
||||
const data = parseResponseBody(text)
|
||||
if (!response.ok) {
|
||||
const message = data?.error || data?.message || text || `HTTP ${response.status}`
|
||||
throw new Error(message)
|
||||
}
|
||||
return data || {}
|
||||
}
|
||||
|
||||
function parseResponseBody(text: string) {
|
||||
if (!text.trim()) {
|
||||
return {}
|
||||
}
|
||||
try {
|
||||
return JSON.parse(text)
|
||||
} catch {
|
||||
return { error: text }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getToken } from '@/utils/auth'
|
||||
import { authRequest } from '@/api/authRequest'
|
||||
|
||||
export interface MachineOverview {
|
||||
total: number
|
||||
@@ -114,34 +114,3 @@ export const machineApi = {
|
||||
await authRequest('/auth/api/v1/machines/sync', { method: 'POST' })
|
||||
},
|
||||
}
|
||||
|
||||
async function authRequest(path: string, init: RequestInit = {}) {
|
||||
const token = getToken()
|
||||
const response = await fetch(path, {
|
||||
...init,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
...init.headers,
|
||||
},
|
||||
})
|
||||
const text = await response.text()
|
||||
const data = parseResponseBody(text)
|
||||
if (!response.ok) {
|
||||
const message = data?.error || data?.message || text || `HTTP ${response.status}`
|
||||
throw new Error(message)
|
||||
}
|
||||
return data || {}
|
||||
}
|
||||
|
||||
function parseResponseBody(text: string) {
|
||||
if (!text.trim()) {
|
||||
return {}
|
||||
}
|
||||
try {
|
||||
return JSON.parse(text)
|
||||
} catch {
|
||||
return { error: text }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getToken } from '@/utils/auth'
|
||||
import { authRequest } from '@/api/authRequest'
|
||||
|
||||
export interface SubsystemAuthSystem {
|
||||
key: string
|
||||
@@ -110,34 +110,3 @@ export const subsystemAuthApi = {
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
async function authRequest(path: string, init: RequestInit = {}) {
|
||||
const token = getToken()
|
||||
const response = await fetch(path, {
|
||||
...init,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
...init.headers,
|
||||
},
|
||||
})
|
||||
const text = await response.text()
|
||||
const data = parseResponseBody(text)
|
||||
if (!response.ok) {
|
||||
const message = data?.error || data?.message || text || `HTTP ${response.status}`
|
||||
throw new Error(message)
|
||||
}
|
||||
return data || {}
|
||||
}
|
||||
|
||||
function parseResponseBody(text: string) {
|
||||
if (!text.trim()) {
|
||||
return {}
|
||||
}
|
||||
try {
|
||||
return JSON.parse(text)
|
||||
} catch {
|
||||
return { error: text }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { getToken } from '@/utils/auth'
|
||||
import { authRequest } from '@/api/authRequest'
|
||||
|
||||
export interface TaskLogSummary {
|
||||
id: string
|
||||
@@ -71,37 +72,6 @@ export const taskLogApi = {
|
||||
},
|
||||
}
|
||||
|
||||
async function authRequest(path: string, init: RequestInit = {}) {
|
||||
const token = getToken()
|
||||
const response = await fetch(path, {
|
||||
...init,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
...init.headers,
|
||||
},
|
||||
})
|
||||
const text = await response.text()
|
||||
const data = parseResponseBody(text)
|
||||
if (!response.ok) {
|
||||
const message = data?.error || data?.message || text || `HTTP ${response.status}`
|
||||
throw new Error(message)
|
||||
}
|
||||
return data || {}
|
||||
}
|
||||
|
||||
function parseResponseBody(text: string) {
|
||||
if (!text.trim()) {
|
||||
return {}
|
||||
}
|
||||
try {
|
||||
return JSON.parse(text)
|
||||
} catch {
|
||||
return { error: text }
|
||||
}
|
||||
}
|
||||
|
||||
export interface TaskLogStreamCallbacks {
|
||||
onInit?: (lines: TaskLogLine[]) => void
|
||||
onUpdate?: (lines: TaskLogLine[]) => void
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { authApi } from '@/api/auth'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { redirectToSSO } from '@/utils/sso'
|
||||
|
||||
let unauthorizedRedirect: Promise<void> | null = null
|
||||
|
||||
export class AuthenticationRequiredError extends Error {
|
||||
constructor() {
|
||||
super('登录状态已失效,正在重新认证')
|
||||
this.name = 'AuthenticationRequiredError'
|
||||
}
|
||||
}
|
||||
|
||||
export function isAuthenticationRequiredError(error: unknown): error is AuthenticationRequiredError {
|
||||
return error instanceof AuthenticationRequiredError
|
||||
}
|
||||
|
||||
export async function handleUnauthorized(): Promise<never> {
|
||||
if (!unauthorizedRedirect) {
|
||||
unauthorizedRedirect = redirectAfterClearingSession()
|
||||
}
|
||||
await unauthorizedRedirect
|
||||
throw new AuthenticationRequiredError()
|
||||
}
|
||||
|
||||
async function redirectAfterClearingSession(): Promise<void> {
|
||||
useAuthStore().clearAuth()
|
||||
|
||||
try {
|
||||
const { data } = await authApi.getConfig()
|
||||
if (data.sso_enabled) {
|
||||
redirectToSSO()
|
||||
return
|
||||
}
|
||||
} catch {
|
||||
redirectToSSO()
|
||||
return
|
||||
}
|
||||
|
||||
const redirect = `${window.location.pathname}${window.location.search}${window.location.hash}`
|
||||
window.location.assign(`/login?redirect=${encodeURIComponent(redirect)}`)
|
||||
}
|
||||
@@ -522,6 +522,7 @@ import { deliveryApi, type DeliveryMountPath, type DeliveryTarget, type Deployme
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useBusinessLineStore } from '@/stores/businessLine'
|
||||
import { useBusinessLineMockProfile } from '@/utils/businessLineMock'
|
||||
import { isAuthenticationRequiredError } from '@/utils/session'
|
||||
|
||||
type FlowView = 'config' | 'execution' | 'result'
|
||||
type StepState = 'pending' | 'running' | 'done' | 'failed'
|
||||
@@ -1295,6 +1296,7 @@ async function precheck() {
|
||||
}
|
||||
} catch (error) {
|
||||
precheckPassed.value = false
|
||||
if (isAuthenticationRequiredError(error)) return
|
||||
ElMessage.error(error instanceof Error ? `实例名称可用性检查失败:${error.message}` : '实例名称可用性检查失败')
|
||||
return
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user