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 {
|
export interface BusinessLine {
|
||||||
id: number
|
id: number
|
||||||
@@ -21,41 +21,31 @@ export interface SinaOrganization {
|
|||||||
|
|
||||||
export const businessLineApi = {
|
export const businessLineApi = {
|
||||||
async listMine(): Promise<BusinessLine[]> {
|
async listMine(): Promise<BusinessLine[]> {
|
||||||
const token = getToken()
|
const data = await authRequest('/auth/api/v1/business-lines')
|
||||||
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}`)
|
|
||||||
}
|
|
||||||
return Array.isArray(data.items) ? data.items : []
|
return Array.isArray(data.items) ? data.items : []
|
||||||
},
|
},
|
||||||
|
|
||||||
async listAll(): Promise<BusinessLine[]> {
|
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 : []
|
return Array.isArray(data.items) ? data.items : []
|
||||||
},
|
},
|
||||||
|
|
||||||
async create(name: string): Promise<BusinessLine> {
|
async create(name: string): Promise<BusinessLine> {
|
||||||
return request('/auth/api/v1/business-lines', {
|
return authRequest('/auth/api/v1/business-lines', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({ name }),
|
body: JSON.stringify({ name }),
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
async update(id: number, name: string): Promise<BusinessLine> {
|
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',
|
method: 'PUT',
|
||||||
body: JSON.stringify({ name }),
|
body: JSON.stringify({ name }),
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
async remove(id: number): Promise<void> {
|
async remove(id: number): Promise<void> {
|
||||||
await request(`/auth/api/v1/business-lines/${id}`, {
|
await authRequest(`/auth/api/v1/business-lines/${id}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -65,14 +55,14 @@ export const businessLineApi = {
|
|||||||
target_user_id: number
|
target_user_id: number
|
||||||
target_business_line_id: number
|
target_business_line_id: number
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
await request('/auth/api/v1/business-lines/authorizations', {
|
await authRequest('/auth/api/v1/business-lines/authorizations', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify(payload),
|
body: JSON.stringify(payload),
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
async listWayneNamespaces(): Promise<WayneNamespace[]> {
|
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 : []
|
const items = Array.isArray(data.data?.list) ? data.data.list : []
|
||||||
return items.map((item: any) => ({
|
return items.map((item: any) => ({
|
||||||
id: Number(item.id),
|
id: Number(item.id),
|
||||||
@@ -82,12 +72,12 @@ export const businessLineApi = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async listMappedWayneNamespaces(businessLineId: number): Promise<WayneNamespace[]> {
|
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 : []
|
return Array.isArray(data.items) ? data.items : []
|
||||||
},
|
},
|
||||||
|
|
||||||
async replaceMappedWayneNamespaces(businessLineId: number, namespaces: WayneNamespace[]): Promise<void> {
|
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',
|
method: 'PUT',
|
||||||
body: JSON.stringify({ namespaces }),
|
body: JSON.stringify({ namespaces }),
|
||||||
})
|
})
|
||||||
@@ -99,37 +89,19 @@ export const businessLineApi = {
|
|||||||
params.set('keyword', keyword.trim())
|
params.set('keyword', keyword.trim())
|
||||||
}
|
}
|
||||||
const suffix = params.toString() ? `?${params.toString()}` : ''
|
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 : []
|
return Array.isArray(data.items) ? data.items : []
|
||||||
},
|
},
|
||||||
|
|
||||||
async listMappedSinaOrganizations(businessLineId: number): Promise<SinaOrganization[]> {
|
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 : []
|
return Array.isArray(data.items) ? data.items : []
|
||||||
},
|
},
|
||||||
|
|
||||||
async replaceMappedSinaOrganizations(businessLineId: number, organizations: SinaOrganization[]): Promise<void> {
|
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',
|
method: 'PUT',
|
||||||
body: JSON.stringify({ organizations }),
|
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 {
|
export interface ContainerServiceSummary {
|
||||||
businessLineId: number
|
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 { getToken } from '@/utils/auth'
|
||||||
|
import { authRequest } from '@/api/authRequest'
|
||||||
|
|
||||||
export interface DeliveryTarget {
|
export interface DeliveryTarget {
|
||||||
id: number
|
id: number
|
||||||
@@ -213,34 +214,3 @@ function createIdempotencyKey(payload: CreateMySQLDeliveryPayload) {
|
|||||||
Date.now(),
|
Date.now(),
|
||||||
].join(':')
|
].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 {
|
export interface MachineOverview {
|
||||||
total: number
|
total: number
|
||||||
@@ -114,34 +114,3 @@ export const machineApi = {
|
|||||||
await authRequest('/auth/api/v1/machines/sync', { method: 'POST' })
|
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 {
|
export interface SubsystemAuthSystem {
|
||||||
key: string
|
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 { getToken } from '@/utils/auth'
|
||||||
|
import { authRequest } from '@/api/authRequest'
|
||||||
|
|
||||||
export interface TaskLogSummary {
|
export interface TaskLogSummary {
|
||||||
id: string
|
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 {
|
export interface TaskLogStreamCallbacks {
|
||||||
onInit?: (lines: TaskLogLine[]) => void
|
onInit?: (lines: TaskLogLine[]) => void
|
||||||
onUpdate?: (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 { useAuthStore } from '@/stores/auth'
|
||||||
import { useBusinessLineStore } from '@/stores/businessLine'
|
import { useBusinessLineStore } from '@/stores/businessLine'
|
||||||
import { useBusinessLineMockProfile } from '@/utils/businessLineMock'
|
import { useBusinessLineMockProfile } from '@/utils/businessLineMock'
|
||||||
|
import { isAuthenticationRequiredError } from '@/utils/session'
|
||||||
|
|
||||||
type FlowView = 'config' | 'execution' | 'result'
|
type FlowView = 'config' | 'execution' | 'result'
|
||||||
type StepState = 'pending' | 'running' | 'done' | 'failed'
|
type StepState = 'pending' | 'running' | 'done' | 'failed'
|
||||||
@@ -1295,6 +1296,7 @@ async function precheck() {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
precheckPassed.value = false
|
precheckPassed.value = false
|
||||||
|
if (isAuthenticationRequiredError(error)) return
|
||||||
ElMessage.error(error instanceof Error ? `实例名称可用性检查失败:${error.message}` : '实例名称可用性检查失败')
|
ElMessage.error(error instanceof Error ? `实例名称可用性检查失败:${error.message}` : '实例名称可用性检查失败')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user