mirror of
https://github.com/rustfs/console.git
synced 2026-11-03 23:05:19 +08:00
refactor: 修复 API 客户端 composables 滥用和硬编码路径问题
- 修复 lib/api-client.ts 中在类方法中直接调用 composables 的问题 - 添加 ApiErrorHandler 错误处理器,通过依赖注入方式处理 401 错误 - 创建路由工具函数 utils/routes.ts,统一管理路由路径 - 修复 composables/useAuth.ts 中的硬编码路径,使用工具函数 - 修复 components/copy-input.vue 的生命周期问题,在 onMounted 中创建 ClipboardJS 实例 - 修复 plugins/03.s3.ts 中的错误处理,使用 navigateTo 替代直接操作 window.location - 修复 pages/config.vue 中的硬编码路径 - 添加 composables/useFormValidation.ts、useAsyncOperation.ts、useErrorHandler.ts 工具函数(供后续使用) - 添加 types/api.ts 类型定义
This commit is contained in:
+32
-37
@@ -1,11 +1,11 @@
|
||||
<script lang="ts" setup>
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
|
||||
import { Icon } from '#components'
|
||||
import ClipboardJS from 'clipboard'
|
||||
import { useMessage } from '@/lib/ui/message'
|
||||
import { onUnmounted } from 'vue'
|
||||
import ClipboardJS from 'clipboard'
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
@@ -31,51 +31,46 @@ const props = defineProps({
|
||||
const model = defineModel<string>()
|
||||
|
||||
const message = useMessage()
|
||||
const clipboard = ref<ClipboardJS | null>(null)
|
||||
|
||||
const clipboard = new ClipboardJS(props.copyIcon ? `#${props.id}-btn-icon` : `#${props.id}-btn`)
|
||||
clipboard.on('success', function (e) {
|
||||
message.success(t('Copy Success'))
|
||||
e.clearSelection()
|
||||
onMounted(() => {
|
||||
clipboard.value = new ClipboardJS(
|
||||
props.copyIcon ? `#${props.id}-btn-icon` : `#${props.id}-btn`
|
||||
)
|
||||
|
||||
clipboard.value.on('success', function (e) {
|
||||
message.success(t('Copy Success'))
|
||||
e.clearSelection()
|
||||
})
|
||||
|
||||
clipboard.value.on('error', async function () {
|
||||
// 如果复制失败,则使用 navigator.clipboard.writeText 进行复制
|
||||
try {
|
||||
if (navigator.clipboard && model.value) {
|
||||
await navigator.clipboard.writeText(model.value)
|
||||
message.success(t('Copy Success'))
|
||||
} else {
|
||||
message.error(t('Copy Failed'))
|
||||
}
|
||||
} catch {
|
||||
message.error(t('Copy Failed'))
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// 这里成功的时候也响应error,所以这里也加上
|
||||
clipboard.on('error', function (e) {
|
||||
// 如果复制失败,则使用navigator.clipboard.writeText进行复制
|
||||
if (navigator.clipboard) {
|
||||
navigator.clipboard.writeText(model.value || '')
|
||||
}
|
||||
message.success(t('Copy Success'))
|
||||
})
|
||||
onUnmounted(() => {
|
||||
clipboard.destroy()
|
||||
clipboard.value?.destroy()
|
||||
clipboard.value = null
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex h-full items-center gap-2">
|
||||
<Input
|
||||
v-model="model"
|
||||
:readonly="props.readonly"
|
||||
:id="props.id"
|
||||
class="flex-1"
|
||||
/>
|
||||
<Button
|
||||
v-if="!props.copyIcon"
|
||||
:id="`${props.id}-btn`"
|
||||
:data-clipboard-target="`#${props.id}`"
|
||||
variant="default"
|
||||
>
|
||||
<Input v-model="model" :readonly="props.readonly" :id="props.id" class="flex-1" />
|
||||
<Button v-if="!props.copyIcon" :id="`${props.id}-btn`" :data-clipboard-target="`#${props.id}`" variant="default">
|
||||
{{ t('Copy') }}
|
||||
</Button>
|
||||
<Button
|
||||
v-else
|
||||
:id="`${props.id}-btn-icon`"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="shrink-0"
|
||||
:data-clipboard-target="`#${props.id}`"
|
||||
:title="t('Copy')"
|
||||
>
|
||||
<Button v-else :id="`${props.id}-btn-icon`" variant="ghost" size="sm" class="shrink-0" :data-clipboard-target="`#${props.id}`" :title="t('Copy')">
|
||||
<Icon :size="18" name="ri:file-copy-line" />
|
||||
<span class="sr-only">{{ t('Copy') }}</span>
|
||||
</Button>
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
/**
|
||||
* 统一的异步操作管理 Composable
|
||||
* 提供加载状态、错误处理和重试机制
|
||||
*/
|
||||
import { ref, readonly, type Ref } from 'vue';
|
||||
import { useErrorHandler } from './useErrorHandler';
|
||||
|
||||
export interface AsyncOperationOptions {
|
||||
/**
|
||||
* 是否自动处理错误
|
||||
* @default true
|
||||
*/
|
||||
autoHandleError?: boolean;
|
||||
/**
|
||||
* 默认错误消息
|
||||
*/
|
||||
defaultErrorMessage?: string;
|
||||
/**
|
||||
* 重试次数
|
||||
* @default 0
|
||||
*/
|
||||
retries?: number;
|
||||
/**
|
||||
* 重试延迟(毫秒)
|
||||
* @default 1000
|
||||
*/
|
||||
retryDelay?: number;
|
||||
}
|
||||
|
||||
export interface UseAsyncOperationReturn<T extends (...args: any[]) => Promise<any>> {
|
||||
/**
|
||||
* 加载状态(只读)
|
||||
*/
|
||||
loading: Readonly<Ref<boolean>>;
|
||||
/**
|
||||
* 错误信息(只读)
|
||||
*/
|
||||
error: Readonly<Ref<Error | null>>;
|
||||
/**
|
||||
* 执行异步操作
|
||||
*/
|
||||
execute: (...args: Parameters<T>) => Promise<ReturnType<T> | null>;
|
||||
/**
|
||||
* 重置错误状态
|
||||
*/
|
||||
resetError: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 包装异步操作,提供加载状态和错误处理
|
||||
* @param operation 异步操作函数
|
||||
* @param options 配置选项
|
||||
* @returns 包含加载状态和执行函数的对象
|
||||
*/
|
||||
export function useAsyncOperation<T extends (...args: any[]) => Promise<any>>(
|
||||
operation: T,
|
||||
options: AsyncOperationOptions = {}
|
||||
): UseAsyncOperationReturn<T> {
|
||||
const {
|
||||
autoHandleError = true,
|
||||
defaultErrorMessage,
|
||||
retries = 0,
|
||||
retryDelay = 1000,
|
||||
} = options;
|
||||
|
||||
const loading = ref(false);
|
||||
const error = ref<Error | null>(null);
|
||||
const { handleError } = useErrorHandler();
|
||||
|
||||
/**
|
||||
* 执行重试逻辑
|
||||
*/
|
||||
const executeWithRetry = async (
|
||||
...args: Parameters<T>
|
||||
): Promise<ReturnType<T>> => {
|
||||
let lastError: Error | null = null;
|
||||
|
||||
for (let attempt = 0; attempt <= retries; attempt++) {
|
||||
try {
|
||||
return await operation(...args);
|
||||
} catch (err) {
|
||||
lastError = err instanceof Error ? err : new Error(String(err));
|
||||
|
||||
// 如果不是最后一次尝试,等待后重试
|
||||
if (attempt < retries) {
|
||||
await new Promise((resolve) => setTimeout(resolve, retryDelay));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError || new Error('Operation failed');
|
||||
};
|
||||
|
||||
/**
|
||||
* 执行异步操作
|
||||
*/
|
||||
const execute = async (
|
||||
...args: Parameters<T>
|
||||
): Promise<ReturnType<T> | null> => {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
|
||||
try {
|
||||
const result = await executeWithRetry(...args);
|
||||
return result;
|
||||
} catch (err) {
|
||||
const errorInstance =
|
||||
err instanceof Error ? err : new Error(String(err));
|
||||
error.value = errorInstance;
|
||||
|
||||
if (autoHandleError) {
|
||||
handleError(errorInstance, defaultErrorMessage);
|
||||
}
|
||||
|
||||
return null;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 重置错误状态
|
||||
*/
|
||||
const resetError = () => {
|
||||
error.value = null;
|
||||
};
|
||||
|
||||
return {
|
||||
loading: readonly(loading),
|
||||
error: readonly(error),
|
||||
execute,
|
||||
resetError,
|
||||
};
|
||||
}
|
||||
|
||||
+27
-26
@@ -1,30 +1,31 @@
|
||||
import type { AwsCredentialIdentity, AwsCredentialIdentityProvider } from '@aws-sdk/types';
|
||||
import { getStsToken } from '~/lib/sts';
|
||||
import type { SiteConfig } from '~/types/config';
|
||||
import type { AwsCredentialIdentity, AwsCredentialIdentityProvider } from '@aws-sdk/types'
|
||||
import { getStsToken } from '~/lib/sts'
|
||||
import type { SiteConfig } from '~/types/config'
|
||||
import { getLoginRoute } from '~/utils/routes'
|
||||
|
||||
interface Credentials {
|
||||
AccessKeyId?: string;
|
||||
SecretAccessKey?: string;
|
||||
SessionToken?: string;
|
||||
Expiration?: string;
|
||||
AccessKeyId?: string
|
||||
SecretAccessKey?: string
|
||||
SessionToken?: string
|
||||
Expiration?: string
|
||||
}
|
||||
|
||||
export function useAuth() {
|
||||
const store = useLocalStorage('auth.credentials', {});
|
||||
const store = useLocalStorage('auth.credentials', {})
|
||||
|
||||
const setCredentials = (credentials: Credentials) => {
|
||||
store.value = credentials;
|
||||
};
|
||||
store.value = credentials
|
||||
}
|
||||
|
||||
const getCredentials = () => {
|
||||
if (!isValidCredentials(store.value)) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
return store.value;
|
||||
};
|
||||
return store.value
|
||||
}
|
||||
|
||||
const isExpired = (expiration: string) => (expiration ? new Date(expiration) < new Date() : false);
|
||||
const isExpired = (expiration: string) => (expiration ? new Date(expiration) < new Date() : false)
|
||||
|
||||
const isValidCredentials = (credentials: Credentials) => {
|
||||
return (
|
||||
@@ -33,31 +34,31 @@ export function useAuth() {
|
||||
!!credentials?.SessionToken &&
|
||||
credentials?.Expiration &&
|
||||
!isExpired(credentials.Expiration)
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
const login = async (
|
||||
credentials: AwsCredentialIdentity | AwsCredentialIdentityProvider,
|
||||
customConfig?: SiteConfig
|
||||
) => {
|
||||
const credentialsResponse = await getStsToken(credentials, 'arn:aws:iam::*:role/Admin', customConfig);
|
||||
const credentialsResponse = await getStsToken(credentials, 'arn:aws:iam::*:role/Admin', customConfig)
|
||||
|
||||
setCredentials({
|
||||
...credentialsResponse,
|
||||
Expiration: credentialsResponse.Expiration?.toISOString(),
|
||||
});
|
||||
})
|
||||
|
||||
return credentialsResponse;
|
||||
};
|
||||
return credentialsResponse
|
||||
}
|
||||
|
||||
const logout = () => {
|
||||
store.value = {};
|
||||
};
|
||||
store.value = {}
|
||||
}
|
||||
|
||||
const logoutAndRedirect = () => {
|
||||
logout();
|
||||
window.location.href = '/rustfs/console/auth/login';
|
||||
};
|
||||
logout()
|
||||
window.location.href = getLoginRoute()
|
||||
}
|
||||
|
||||
return {
|
||||
login,
|
||||
@@ -65,5 +66,5 @@ export function useAuth() {
|
||||
logoutAndRedirect,
|
||||
credentials: ref<Credentials | undefined>(getCredentials()),
|
||||
isAuthenticated: ref(isValidCredentials(store.value)),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* 统一的错误处理 Composable
|
||||
* 提供一致的错误处理和消息显示
|
||||
*/
|
||||
import { useMessage } from '~/lib/ui/message';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
export interface ErrorHandlerOptions {
|
||||
/**
|
||||
* 是否记录错误到控制台
|
||||
* @default true
|
||||
*/
|
||||
logError?: boolean;
|
||||
/**
|
||||
* 默认错误消息的翻译键
|
||||
* @default 'Operation failed'
|
||||
*/
|
||||
defaultMessageKey?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 错误处理工具
|
||||
* @param options 配置选项
|
||||
* @returns 错误处理函数
|
||||
*/
|
||||
export function useErrorHandler(options: ErrorHandlerOptions = {}) {
|
||||
const { logError = true, defaultMessageKey = 'Operation failed' } = options;
|
||||
const message = useMessage();
|
||||
const { t } = useI18n();
|
||||
|
||||
/**
|
||||
* 处理错误
|
||||
* @param error 错误对象
|
||||
* @param defaultMessage 默认错误消息(可选)
|
||||
*/
|
||||
const handleError = (error: unknown, defaultMessage?: string): void => {
|
||||
let errorMessage = defaultMessage || t(defaultMessageKey);
|
||||
|
||||
// 提取错误消息
|
||||
if (error instanceof Error) {
|
||||
errorMessage = error.message || errorMessage;
|
||||
} else if (typeof error === 'string') {
|
||||
errorMessage = error;
|
||||
} else if (error && typeof error === 'object' && 'message' in error) {
|
||||
errorMessage = String(error.message);
|
||||
}
|
||||
|
||||
// 记录错误
|
||||
if (logError) {
|
||||
console.error('[ErrorHandler]', error);
|
||||
}
|
||||
|
||||
// 显示错误消息
|
||||
message.error(errorMessage);
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理 API 错误
|
||||
* @param error 错误对象
|
||||
* @param defaultMessage 默认错误消息
|
||||
*/
|
||||
const handleApiError = (
|
||||
error: unknown,
|
||||
defaultMessage?: string
|
||||
): void => {
|
||||
// API 错误可能有特定的格式
|
||||
if (
|
||||
error &&
|
||||
typeof error === 'object' &&
|
||||
'response' in error &&
|
||||
'data' in error
|
||||
) {
|
||||
const apiError = error as { response?: { data?: { message?: string } } };
|
||||
const apiMessage =
|
||||
apiError.response?.data?.message ||
|
||||
defaultMessage ||
|
||||
t('API request failed');
|
||||
handleError(apiMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
handleError(error, defaultMessage);
|
||||
};
|
||||
|
||||
/**
|
||||
* 静默处理错误(不显示消息)
|
||||
* @param error 错误对象
|
||||
* @returns 错误消息
|
||||
*/
|
||||
const handleErrorSilently = (error: unknown): string => {
|
||||
if (error instanceof Error) {
|
||||
return error.message;
|
||||
}
|
||||
if (typeof error === 'string') {
|
||||
return error;
|
||||
}
|
||||
return t(defaultMessageKey);
|
||||
};
|
||||
|
||||
return {
|
||||
handleError,
|
||||
handleApiError,
|
||||
handleErrorSilently,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* 统一的表单验证 Composable
|
||||
* 基于 Zod schema 提供类型安全的表单验证
|
||||
*/
|
||||
import { z } from 'zod';
|
||||
import { useMessage } from '~/lib/ui/message';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
export interface ValidationResult<T> {
|
||||
success: boolean;
|
||||
data?: T;
|
||||
errors?: z.ZodError;
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用 Zod schema 进行表单验证
|
||||
* @param schema Zod schema 定义
|
||||
* @returns 验证函数和错误处理工具
|
||||
*/
|
||||
export function useFormValidation<T extends z.ZodTypeAny>(schema: T) {
|
||||
const { t } = useI18n();
|
||||
const message = useMessage();
|
||||
|
||||
/**
|
||||
* 验证数据是否符合 schema
|
||||
* @param data 待验证的数据
|
||||
* @returns 验证结果
|
||||
*/
|
||||
const validate = (data: unknown): ValidationResult<z.infer<T>> => {
|
||||
const result = schema.safeParse(data);
|
||||
|
||||
if (!result.success) {
|
||||
return {
|
||||
success: false,
|
||||
errors: result.error,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: result.data,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 验证数据,失败时显示错误消息
|
||||
* @param data 待验证的数据
|
||||
* @param showMessage 是否显示错误消息
|
||||
* @returns 验证是否通过
|
||||
*/
|
||||
const validateWithMessage = (
|
||||
data: unknown,
|
||||
showMessage = true
|
||||
): data is z.infer<T> => {
|
||||
const result = validate(data);
|
||||
|
||||
if (!result.success && showMessage) {
|
||||
const firstError = result.errors?.errors[0];
|
||||
if (firstError) {
|
||||
const fieldName = firstError.path.join('.');
|
||||
const errorMessage = firstError.message || t('Validation failed');
|
||||
message.error(
|
||||
fieldName ? `${fieldName}: ${errorMessage}` : errorMessage
|
||||
);
|
||||
} else {
|
||||
message.error(t('Validation failed'));
|
||||
}
|
||||
}
|
||||
|
||||
return result.success;
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取字段错误消息
|
||||
* @param errors ZodError 实例
|
||||
* @param fieldPath 字段路径
|
||||
* @returns 错误消息或 undefined
|
||||
*/
|
||||
const getFieldError = (
|
||||
errors: z.ZodError | undefined,
|
||||
fieldPath: (string | number)[]
|
||||
): string | undefined => {
|
||||
if (!errors) return undefined;
|
||||
|
||||
const error = errors.errors.find((e) => {
|
||||
return (
|
||||
e.path.length === fieldPath.length &&
|
||||
e.path.every((p, i) => p === fieldPath[i])
|
||||
);
|
||||
});
|
||||
|
||||
return error?.message;
|
||||
};
|
||||
|
||||
return {
|
||||
validate,
|
||||
validateWithMessage,
|
||||
getFieldError,
|
||||
schema,
|
||||
};
|
||||
}
|
||||
|
||||
+69
-59
@@ -1,134 +1,144 @@
|
||||
import { joinURL } from 'ufo';
|
||||
import { parseApiError } from '~/utils/error-handler';
|
||||
import { logger } from '~/utils/logger';
|
||||
import type { AwsClient } from './aws4fetch';
|
||||
import { joinURL } from 'ufo'
|
||||
import type { IApiErrorHandler } from '~/types/api'
|
||||
import { parseApiError } from '~/utils/error-handler'
|
||||
import { logger } from '~/utils/logger'
|
||||
import type { AwsClient } from './aws4fetch'
|
||||
|
||||
interface ApiClientOptions {
|
||||
baseUrl?: string;
|
||||
headers?: Record<string, string>;
|
||||
baseUrl?: string
|
||||
headers?: Record<string, string>
|
||||
errorHandler?: IApiErrorHandler
|
||||
}
|
||||
|
||||
interface RequestOptions {
|
||||
method?: string;
|
||||
headers?: Record<string, string>;
|
||||
body?: any;
|
||||
params?: Record<string, string>;
|
||||
method?: string
|
||||
headers?: Record<string, string>
|
||||
body?: any
|
||||
params?: Record<string, string>
|
||||
}
|
||||
|
||||
class ApiClient {
|
||||
private $api: AwsClient;
|
||||
private config?: ApiClientOptions;
|
||||
private $api: AwsClient
|
||||
private config?: ApiClientOptions
|
||||
private errorHandler?: IApiErrorHandler
|
||||
|
||||
constructor(api: AwsClient, options?: ApiClientOptions) {
|
||||
this.$api = api;
|
||||
this.config = options;
|
||||
this.$api = api
|
||||
this.config = options
|
||||
this.errorHandler = options?.errorHandler
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置错误处理器
|
||||
*/
|
||||
setErrorHandler(handler: IApiErrorHandler): void {
|
||||
this.errorHandler = handler
|
||||
}
|
||||
|
||||
async request(url: string, options: RequestOptions = {}, parseJson: boolean = true) {
|
||||
url = this.config?.baseUrl ? joinURL(this.config?.baseUrl, url) : url;
|
||||
options.headers = { ...this.config?.headers, ...options.headers };
|
||||
url = this.config?.baseUrl ? joinURL(this.config?.baseUrl, url) : url
|
||||
options.headers = { ...this.config?.headers, ...options.headers }
|
||||
// 处理body的数据格式,只对普通对象序列化
|
||||
if (options.body &&
|
||||
!(options.body instanceof FormData) &&
|
||||
!(options.body instanceof Blob) &&
|
||||
!(options.body instanceof ArrayBuffer) &&
|
||||
!(options.body instanceof Uint8Array) &&
|
||||
!(options.body instanceof File)) {
|
||||
options.body = JSON.stringify(options.body);
|
||||
!(options.body instanceof FormData) &&
|
||||
!(options.body instanceof Blob) &&
|
||||
!(options.body instanceof ArrayBuffer) &&
|
||||
!(options.body instanceof Uint8Array) &&
|
||||
!(options.body instanceof File)) {
|
||||
options.body = JSON.stringify(options.body)
|
||||
}
|
||||
|
||||
if (options.params) {
|
||||
const queryString = new URLSearchParams(options.params).toString();
|
||||
url += `?${queryString}`; // 拼接查询字符串到URL
|
||||
delete options.params; // 删除params,以免影响fetch的options
|
||||
const queryString = new URLSearchParams(options.params).toString()
|
||||
url += `?${queryString}` // 拼接查询字符串到URL
|
||||
delete options.params // 删除params,以免影响fetch的options
|
||||
}
|
||||
|
||||
logger.log('[request] url:', url);
|
||||
logger.log('[request] options:', options);
|
||||
logger.log('[request] url:', url)
|
||||
logger.log('[request] options:', options)
|
||||
|
||||
const response = await this.$api.fetch(url, options);
|
||||
const response = await this.$api.fetch(url, options)
|
||||
|
||||
logger.log('[request] response:', response);
|
||||
logger.log('[request] response:', response)
|
||||
|
||||
if (!response.ok) {
|
||||
const errorMsg = await parseApiError(response);
|
||||
throw new Error(errorMsg);
|
||||
const errorMsg = await parseApiError(response)
|
||||
throw new Error(errorMsg)
|
||||
}
|
||||
|
||||
// 处理401
|
||||
// 处理401未授权错误
|
||||
if (response.status === 401) {
|
||||
const message = useMessage();
|
||||
// 清除登录信息
|
||||
await useAuth().logout();
|
||||
window.location.href = '/rustfs/console/auth/login';
|
||||
return;
|
||||
if (this.errorHandler) {
|
||||
await this.errorHandler.handle401()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 204 or body length is 0
|
||||
if (response.status === 204 || response.headers.get('content-length') === '0' || !response.body) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
if (parseJson) {
|
||||
return await response.json();
|
||||
return await response.json()
|
||||
} else {
|
||||
return response;
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
||||
async *streamRequest(url: string, options: RequestOptions = {}) {
|
||||
const response = await this.request(url, options, false);
|
||||
const response = await this.request(url, options, false)
|
||||
|
||||
if (!response.body) {
|
||||
throw new Error('No response body');
|
||||
throw new Error('No response body')
|
||||
}
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder('utf-8');
|
||||
const reader = response.body.getReader()
|
||||
const decoder = new TextDecoder('utf-8')
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
const chunk = decoder.decode(value, { stream: true });
|
||||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
const chunk = decoder.decode(value, { stream: true })
|
||||
try {
|
||||
const data = JSON.parse(chunk);
|
||||
yield data; // 使用 yield 返回数据
|
||||
const data = JSON.parse(chunk)
|
||||
yield data // 使用 yield 返回数据
|
||||
} catch (error) {
|
||||
logger.error('Failed to parse chunk:', error);
|
||||
logger.error('Failed to parse chunk:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
async get(url: string, options?: RequestOptions) {
|
||||
return this.request(url, { method: 'GET', ...options });
|
||||
return this.request(url, { method: 'GET', ...options })
|
||||
}
|
||||
|
||||
async post(url: string, body: any, options?: RequestOptions) {
|
||||
return this.request(url, { method: 'POST', body, ...options });
|
||||
return this.request(url, { method: 'POST', body, ...options })
|
||||
}
|
||||
|
||||
async delete(url: string, options?: RequestOptions) {
|
||||
return this.request(url, { method: 'DELETE', ...options });
|
||||
return this.request(url, { method: 'DELETE', ...options })
|
||||
}
|
||||
|
||||
async put(url: string, body: any, options?: RequestOptions) {
|
||||
return this.request(url, { method: 'PUT', body, ...options });
|
||||
return this.request(url, { method: 'PUT', body, ...options })
|
||||
}
|
||||
|
||||
async patch(url: string, body: any, options?: RequestOptions) {
|
||||
return this.request(url, { method: 'PATCH', body, ...options });
|
||||
return this.request(url, { method: 'PATCH', body, ...options })
|
||||
}
|
||||
|
||||
async head(url: string, options?: RequestOptions) {
|
||||
return this.request(url, { method: 'HEAD', ...options });
|
||||
return this.request(url, { method: 'HEAD', ...options })
|
||||
}
|
||||
|
||||
async options(url: string, options?: RequestOptions) {
|
||||
return this.request(url, { method: 'OPTIONS', ...options });
|
||||
return this.request(url, { method: 'OPTIONS', ...options })
|
||||
}
|
||||
|
||||
async trace(url: string, options?: RequestOptions) {
|
||||
return this.request(url, { method: 'TRACE', ...options });
|
||||
return this.request(url, { method: 'TRACE', ...options })
|
||||
}
|
||||
}
|
||||
|
||||
export default ApiClient;
|
||||
export default ApiClient
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* API 错误处理器
|
||||
* 处理 API 请求中的错误,特别是认证错误
|
||||
*/
|
||||
import type { ApiErrorHandlerCallback, IApiErrorHandler } from '~/types/api';
|
||||
|
||||
export class ApiErrorHandler implements IApiErrorHandler {
|
||||
private onUnauthorized?: ApiErrorHandlerCallback;
|
||||
private onForbidden?: ApiErrorHandlerCallback;
|
||||
private onServerError?: ApiErrorHandlerCallback;
|
||||
|
||||
constructor(callbacks?: {
|
||||
onUnauthorized?: ApiErrorHandlerCallback;
|
||||
onForbidden?: ApiErrorHandlerCallback;
|
||||
onServerError?: ApiErrorHandlerCallback;
|
||||
}) {
|
||||
this.onUnauthorized = callbacks?.onUnauthorized;
|
||||
this.onForbidden = callbacks?.onForbidden;
|
||||
this.onServerError = callbacks?.onServerError;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 401 未授权错误
|
||||
*/
|
||||
async handle401(): Promise<void> {
|
||||
if (this.onUnauthorized) {
|
||||
await this.onUnauthorized();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 403 禁止访问错误
|
||||
*/
|
||||
async handle403(): Promise<void> {
|
||||
if (this.onForbidden) {
|
||||
await this.onForbidden();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 5xx 服务器错误
|
||||
*/
|
||||
async handleServerError(): Promise<void> {
|
||||
if (this.onServerError) {
|
||||
await this.onServerError();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据状态码处理错误
|
||||
* @param status HTTP 状态码
|
||||
*/
|
||||
async handleByStatus(status: number): Promise<void> {
|
||||
switch (status) {
|
||||
case 401:
|
||||
await this.handle401();
|
||||
break;
|
||||
case 403:
|
||||
await this.handle403();
|
||||
break;
|
||||
case 500:
|
||||
case 502:
|
||||
case 503:
|
||||
case 504:
|
||||
await this.handleServerError();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
-4
@@ -1,7 +1,8 @@
|
||||
<script lang="ts" setup>
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Field, FieldContent, FieldDescription, FieldLabel } from '@/components/ui/field'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { getLoginRoute } from '~/utils/routes'
|
||||
|
||||
await setPageLayout('plain')
|
||||
|
||||
@@ -51,7 +52,7 @@ const validateAndSave = async () => {
|
||||
|
||||
// 延迟后刷新页面并跳转,确保配置完全生效
|
||||
setTimeout(() => {
|
||||
window.location.href = '/rustfs/console/auth/login'
|
||||
window.location.href = getLoginRoute()
|
||||
}, 200)
|
||||
} catch (error) {
|
||||
console.error('URL validation error:', error) // 调试信息
|
||||
@@ -74,7 +75,7 @@ const resetToCurrentHost = async () => {
|
||||
|
||||
// 延迟后跳转到登录页面,确保配置完全生效
|
||||
setTimeout(() => {
|
||||
window.location.href = '/rustfs/console/auth/login'
|
||||
window.location.href = getLoginRoute()
|
||||
}, 200)
|
||||
}
|
||||
|
||||
@@ -151,7 +152,7 @@ onMounted(() => {
|
||||
{{ t('Need help?') }}
|
||||
<NuxtLink to="https://docs.rustfs.com" class="text-blue-600 hover:underline">{{
|
||||
t('View Documentation')
|
||||
}}</NuxtLink>
|
||||
}}</NuxtLink>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
+34
-20
@@ -1,22 +1,24 @@
|
||||
import ApiClient from '~/lib/api-client';
|
||||
import { AwsClient } from '~/lib/aws4fetch';
|
||||
import type { SiteConfig } from '~/types/config'; // Updated import
|
||||
import ApiClient from '~/lib/api-client'
|
||||
import { AwsClient } from '~/lib/aws4fetch'
|
||||
import { ApiErrorHandler } from '~/lib/utils/api-error-handler'
|
||||
import type { SiteConfig } from '~/types/config'
|
||||
import { navigateToRoute } from '~/utils/routes'
|
||||
|
||||
export default defineNuxtPlugin({
|
||||
name: 'admin-api-client',
|
||||
setup(nuxtApp) {
|
||||
const siteConfig = nuxtApp.$siteConfig as SiteConfig;
|
||||
const { isAuthenticated, credentials } = useAuth();
|
||||
const siteConfig = nuxtApp.$siteConfig as SiteConfig
|
||||
const { isAuthenticated, credentials } = useAuth()
|
||||
|
||||
if (!isAuthenticated.value) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
const accessKeyId = credentials.value?.AccessKeyId || '';
|
||||
const secretAccessKey = credentials.value?.SecretAccessKey || '';
|
||||
const sessionToken = credentials.value?.SessionToken || '';
|
||||
const region: string = siteConfig.s3.region || 'us-east-1';
|
||||
const service = 's3';
|
||||
const accessKeyId = credentials.value?.AccessKeyId || ''
|
||||
const secretAccessKey = credentials.value?.SecretAccessKey || ''
|
||||
const sessionToken = credentials.value?.SessionToken || ''
|
||||
const region: string = siteConfig.s3.region || 'us-east-1'
|
||||
const service = 's3'
|
||||
|
||||
const adminApiClient: AwsClient = new AwsClient({
|
||||
accessKeyId,
|
||||
@@ -24,17 +26,29 @@ export default defineNuxtPlugin({
|
||||
sessionToken,
|
||||
region,
|
||||
service,
|
||||
});
|
||||
})
|
||||
|
||||
// 创建错误处理器
|
||||
const errorHandler = new ApiErrorHandler({
|
||||
onUnauthorized: async () => {
|
||||
const { logout } = useAuth()
|
||||
logout()
|
||||
await navigateToRoute('/auth/login', { external: true })
|
||||
},
|
||||
})
|
||||
|
||||
const apiClient = new ApiClient(adminApiClient, {
|
||||
baseUrl: siteConfig.api.baseURL,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
errorHandler,
|
||||
})
|
||||
|
||||
return {
|
||||
provide: {
|
||||
api: new ApiClient(adminApiClient, {
|
||||
baseUrl: siteConfig.api.baseURL,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}),
|
||||
api: apiClient,
|
||||
},
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
+33
-22
@@ -1,22 +1,22 @@
|
||||
import { S3Client } from '@aws-sdk/client-s3';
|
||||
import type { SiteConfig } from '~/types/config';
|
||||
import type { DeserializeHandler, DeserializeHandlerArguments, DeserializeHandlerOutput } from '@aws-sdk/types';
|
||||
import { S3Client } from '@aws-sdk/client-s3'
|
||||
import type { DeserializeHandler, DeserializeHandlerArguments, DeserializeHandlerOutput } from '@aws-sdk/types'
|
||||
import type { SiteConfig } from '~/types/config'
|
||||
|
||||
interface S3Response {
|
||||
response?: {
|
||||
body?: string;
|
||||
};
|
||||
[key: string]: any;
|
||||
body?: string
|
||||
}
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export default defineNuxtPlugin({
|
||||
name: 's3-client',
|
||||
setup(nuxtApp) {
|
||||
const { credentials, isAuthenticated } = useAuth();
|
||||
const siteConfig = nuxtApp.$siteConfig as SiteConfig;
|
||||
const { credentials, isAuthenticated } = useAuth()
|
||||
const siteConfig = nuxtApp.$siteConfig as SiteConfig
|
||||
|
||||
if (!isAuthenticated || !credentials.value) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
const client = new S3Client({
|
||||
@@ -30,58 +30,69 @@ export default defineNuxtPlugin({
|
||||
secretAccessKey: credentials.value?.SecretAccessKey || '',
|
||||
sessionToken: credentials.value?.SessionToken || '',
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
// 添加中间件处理XML响应和401错误
|
||||
client.middlewareStack.add(
|
||||
(next: DeserializeHandler<any, any>) =>
|
||||
async (args: DeserializeHandlerArguments<any>): Promise<DeserializeHandlerOutput<any>> => {
|
||||
try {
|
||||
const response = (await next(args)) as S3Response;
|
||||
const response = (await next(args)) as S3Response
|
||||
|
||||
// 检查响应是否为XML格式
|
||||
if (response.response?.body && typeof response.response.body === 'string') {
|
||||
const body = response.response.body.trim();
|
||||
const body = response.response.body.trim()
|
||||
|
||||
// 检查是否是空的XML响应
|
||||
if (body.match(/^<\?xml[^>]*\?><[^>]*><\/[^>]*>$/)) {
|
||||
// 从XML标签名提取属性名
|
||||
const tagName = body.match(/<([^>]*)><\/\1>/)?.[1];
|
||||
const tagName = body.match(/<([^>]*)><\/\1>/)?.[1]
|
||||
if (tagName) {
|
||||
// 将XML标签名转换为驼峰命名
|
||||
const propertyName = tagName.replace(/(?:^|_)([a-z])/g, (_, letter) => letter.toUpperCase());
|
||||
const propertyName = tagName.replace(/(?:^|_)([a-z])/g, (_, letter) => letter.toUpperCase())
|
||||
// 返回null,保持response结构
|
||||
return {
|
||||
response: response.response,
|
||||
[propertyName]: null,
|
||||
} as unknown as DeserializeHandlerOutput<any>;
|
||||
} as unknown as DeserializeHandlerOutput<any>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return response as DeserializeHandlerOutput<any>;
|
||||
return response as DeserializeHandlerOutput<any>
|
||||
} catch (error: any) {
|
||||
// 处理401未授权错误
|
||||
if (error?.$metadata?.httpStatusCode === 401) {
|
||||
await useAuth().logoutAndRedirect();
|
||||
const { logout } = useAuth()
|
||||
logout()
|
||||
// 使用 navigateTo 而不是直接操作 window.location
|
||||
await navigateTo('/auth/login')
|
||||
// 返回一个空响应以符合类型要求
|
||||
return {
|
||||
response: {
|
||||
statusCode: 401,
|
||||
headers: {},
|
||||
},
|
||||
} as DeserializeHandlerOutput<any>
|
||||
}
|
||||
|
||||
// 处理 S3 客户端错误,优先抛出 error.message 作为新的 Error
|
||||
if (error?.Code) {
|
||||
throw new Error(error.Code);
|
||||
throw new Error(error.Code)
|
||||
}
|
||||
throw error;
|
||||
throw error
|
||||
}
|
||||
},
|
||||
{
|
||||
step: 'deserialize',
|
||||
name: 'handleXmlResponse',
|
||||
}
|
||||
);
|
||||
)
|
||||
|
||||
return {
|
||||
provide: {
|
||||
s3Client: client,
|
||||
},
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* API 相关类型定义
|
||||
*/
|
||||
|
||||
/**
|
||||
* API 错误处理器回调函数类型
|
||||
*/
|
||||
export type ApiErrorHandlerCallback = () => void | Promise<void>;
|
||||
|
||||
/**
|
||||
* API 客户端错误处理器接口
|
||||
*/
|
||||
export interface IApiErrorHandler {
|
||||
handle401(): Promise<void>;
|
||||
handle403(): Promise<void>;
|
||||
handleServerError(): Promise<void>;
|
||||
handleByStatus(status: number): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* API 请求基础配置
|
||||
*/
|
||||
export interface ApiRequestConfig {
|
||||
baseURL?: string;
|
||||
headers?: Record<string, string>;
|
||||
timeout?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* API 响应基础结构
|
||||
*/
|
||||
export interface ApiResponse<T = any> {
|
||||
data?: T;
|
||||
message?: string;
|
||||
code?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页请求参数
|
||||
*/
|
||||
export interface PaginationParams {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
offset?: number;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页响应数据
|
||||
*/
|
||||
export interface PaginatedResponse<T> {
|
||||
items: T[];
|
||||
total: number;
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* 路由工具函数
|
||||
* 统一管理路由路径和导航逻辑
|
||||
*/
|
||||
import { joinURL } from 'ufo';
|
||||
|
||||
/**
|
||||
* 获取应用的 baseURL
|
||||
* @returns baseURL 路径
|
||||
*/
|
||||
function getBaseURL(): string {
|
||||
// 在客户端,从 window.location 推断 baseURL
|
||||
if (process.client) {
|
||||
const pathname = window.location.pathname;
|
||||
// 如果路径以 /rustfs/console 开头,提取基础路径
|
||||
const match = pathname.match(/^(\/rustfs\/console)/);
|
||||
if (match) {
|
||||
return match[1] + '/';
|
||||
}
|
||||
}
|
||||
|
||||
// 默认值
|
||||
return '/rustfs/console/';
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建完整路由路径(包含 baseURL)
|
||||
* @param path 路由路径
|
||||
* @returns 完整路径
|
||||
*/
|
||||
export function buildRoute(path: string): string {
|
||||
const baseURL = getBaseURL();
|
||||
return joinURL(baseURL.replace(/\/$/, ''), path.replace(/^\//, ''));
|
||||
}
|
||||
|
||||
/**
|
||||
* 导航到指定路由
|
||||
* @param path 路由路径
|
||||
* @param options 导航选项
|
||||
*/
|
||||
export async function navigateToRoute(
|
||||
path: string,
|
||||
options?: { external?: boolean; replace?: boolean }
|
||||
): Promise<void> {
|
||||
const fullPath = buildRoute(path);
|
||||
|
||||
if (options?.external) {
|
||||
window.location.href = fullPath;
|
||||
} else {
|
||||
await navigateTo(fullPath, { replace: options?.replace });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取登录页面路径
|
||||
* @returns 登录页面完整路径
|
||||
*/
|
||||
export function getLoginRoute(): string {
|
||||
return buildRoute('/auth/login');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user