mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-02-18 02:30:51 +08:00
319 lines
10 KiB
TypeScript
319 lines
10 KiB
TypeScript
import { useCallback, useRef, useState, type ChangeEvent, type RefObject } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { authFilesApi } from '@/services/api';
|
|
import { apiClient } from '@/services/api/client';
|
|
import { useNotificationStore } from '@/stores';
|
|
import type { AuthFileItem } from '@/types';
|
|
import { formatFileSize } from '@/utils/format';
|
|
import { MAX_AUTH_FILE_SIZE, getTypeLabel, isRuntimeOnlyAuthFile } from '@/features/authFiles/constants';
|
|
|
|
type DeleteAllOptions = {
|
|
filter: string;
|
|
onResetFilterToAll: () => void;
|
|
};
|
|
|
|
export type UseAuthFilesDataResult = {
|
|
files: AuthFileItem[];
|
|
loading: boolean;
|
|
error: string;
|
|
uploading: boolean;
|
|
deleting: string | null;
|
|
deletingAll: boolean;
|
|
statusUpdating: Record<string, boolean>;
|
|
fileInputRef: RefObject<HTMLInputElement | null>;
|
|
loadFiles: () => Promise<void>;
|
|
handleUploadClick: () => void;
|
|
handleFileChange: (event: ChangeEvent<HTMLInputElement>) => Promise<void>;
|
|
handleDelete: (name: string) => void;
|
|
handleDeleteAll: (options: DeleteAllOptions) => void;
|
|
handleDownload: (name: string) => Promise<void>;
|
|
handleStatusToggle: (item: AuthFileItem, enabled: boolean) => Promise<void>;
|
|
};
|
|
|
|
export type UseAuthFilesDataOptions = {
|
|
refreshKeyStats: () => Promise<void>;
|
|
};
|
|
|
|
export function useAuthFilesData(options: UseAuthFilesDataOptions): UseAuthFilesDataResult {
|
|
const { refreshKeyStats } = options;
|
|
const { t } = useTranslation();
|
|
const { showNotification, showConfirmation } = useNotificationStore();
|
|
|
|
const [files, setFiles] = useState<AuthFileItem[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState('');
|
|
const [uploading, setUploading] = useState(false);
|
|
const [deleting, setDeleting] = useState<string | null>(null);
|
|
const [deletingAll, setDeletingAll] = useState(false);
|
|
const [statusUpdating, setStatusUpdating] = useState<Record<string, boolean>>({});
|
|
|
|
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
|
|
|
const loadFiles = useCallback(async () => {
|
|
setLoading(true);
|
|
setError('');
|
|
try {
|
|
const data = await authFilesApi.list();
|
|
setFiles(data?.files || []);
|
|
} catch (err: unknown) {
|
|
const errorMessage = err instanceof Error ? err.message : t('notification.refresh_failed');
|
|
setError(errorMessage);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [t]);
|
|
|
|
const handleUploadClick = useCallback(() => {
|
|
fileInputRef.current?.click();
|
|
}, []);
|
|
|
|
const handleFileChange = useCallback(
|
|
async (event: ChangeEvent<HTMLInputElement>) => {
|
|
const fileList = event.target.files;
|
|
if (!fileList || fileList.length === 0) return;
|
|
|
|
const filesToUpload = Array.from(fileList);
|
|
const validFiles: File[] = [];
|
|
const invalidFiles: string[] = [];
|
|
const oversizedFiles: string[] = [];
|
|
|
|
filesToUpload.forEach((file) => {
|
|
if (!file.name.endsWith('.json')) {
|
|
invalidFiles.push(file.name);
|
|
return;
|
|
}
|
|
if (file.size > MAX_AUTH_FILE_SIZE) {
|
|
oversizedFiles.push(file.name);
|
|
return;
|
|
}
|
|
validFiles.push(file);
|
|
});
|
|
|
|
if (invalidFiles.length > 0) {
|
|
showNotification(t('auth_files.upload_error_json'), 'error');
|
|
}
|
|
if (oversizedFiles.length > 0) {
|
|
showNotification(
|
|
t('auth_files.upload_error_size', { maxSize: formatFileSize(MAX_AUTH_FILE_SIZE) }),
|
|
'error'
|
|
);
|
|
}
|
|
|
|
if (validFiles.length === 0) {
|
|
event.target.value = '';
|
|
return;
|
|
}
|
|
|
|
setUploading(true);
|
|
let successCount = 0;
|
|
const failed: { name: string; message: string }[] = [];
|
|
|
|
for (const file of validFiles) {
|
|
try {
|
|
await authFilesApi.upload(file);
|
|
successCount++;
|
|
} catch (err: unknown) {
|
|
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
|
|
failed.push({ name: file.name, message: errorMessage });
|
|
}
|
|
}
|
|
|
|
if (successCount > 0) {
|
|
const suffix = validFiles.length > 1 ? ` (${successCount}/${validFiles.length})` : '';
|
|
showNotification(
|
|
`${t('auth_files.upload_success')}${suffix}`,
|
|
failed.length ? 'warning' : 'success'
|
|
);
|
|
await loadFiles();
|
|
await refreshKeyStats();
|
|
}
|
|
|
|
if (failed.length > 0) {
|
|
const details = failed.map((item) => `${item.name}: ${item.message}`).join('; ');
|
|
showNotification(`${t('notification.upload_failed')}: ${details}`, 'error');
|
|
}
|
|
|
|
setUploading(false);
|
|
event.target.value = '';
|
|
},
|
|
[loadFiles, refreshKeyStats, showNotification, t]
|
|
);
|
|
|
|
const handleDelete = useCallback(
|
|
(name: string) => {
|
|
showConfirmation({
|
|
title: t('auth_files.delete_title', { defaultValue: 'Delete File' }),
|
|
message: `${t('auth_files.delete_confirm')} "${name}" ?`,
|
|
variant: 'danger',
|
|
confirmText: t('common.confirm'),
|
|
onConfirm: async () => {
|
|
setDeleting(name);
|
|
try {
|
|
await authFilesApi.deleteFile(name);
|
|
showNotification(t('auth_files.delete_success'), 'success');
|
|
setFiles((prev) => prev.filter((item) => item.name !== name));
|
|
} catch (err: unknown) {
|
|
const errorMessage = err instanceof Error ? err.message : '';
|
|
showNotification(`${t('notification.delete_failed')}: ${errorMessage}`, 'error');
|
|
} finally {
|
|
setDeleting(null);
|
|
}
|
|
}
|
|
});
|
|
},
|
|
[showConfirmation, showNotification, t]
|
|
);
|
|
|
|
const handleDeleteAll = useCallback(
|
|
(deleteAllOptions: DeleteAllOptions) => {
|
|
const { filter, onResetFilterToAll } = deleteAllOptions;
|
|
const isFiltered = filter !== 'all';
|
|
const typeLabel = isFiltered ? getTypeLabel(t, filter) : t('auth_files.filter_all');
|
|
const confirmMessage = isFiltered
|
|
? t('auth_files.delete_filtered_confirm', { type: typeLabel })
|
|
: t('auth_files.delete_all_confirm');
|
|
|
|
showConfirmation({
|
|
title: t('auth_files.delete_all_title', { defaultValue: 'Delete All Files' }),
|
|
message: confirmMessage,
|
|
variant: 'danger',
|
|
confirmText: t('common.confirm'),
|
|
onConfirm: async () => {
|
|
setDeletingAll(true);
|
|
try {
|
|
if (!isFiltered) {
|
|
await authFilesApi.deleteAll();
|
|
showNotification(t('auth_files.delete_all_success'), 'success');
|
|
setFiles((prev) => prev.filter((file) => isRuntimeOnlyAuthFile(file)));
|
|
} else {
|
|
const filesToDelete = files.filter(
|
|
(f) => f.type === filter && !isRuntimeOnlyAuthFile(f)
|
|
);
|
|
|
|
if (filesToDelete.length === 0) {
|
|
showNotification(t('auth_files.delete_filtered_none', { type: typeLabel }), 'info');
|
|
setDeletingAll(false);
|
|
return;
|
|
}
|
|
|
|
let success = 0;
|
|
let failed = 0;
|
|
const deletedNames: string[] = [];
|
|
|
|
for (const file of filesToDelete) {
|
|
try {
|
|
await authFilesApi.deleteFile(file.name);
|
|
success++;
|
|
deletedNames.push(file.name);
|
|
} catch {
|
|
failed++;
|
|
}
|
|
}
|
|
|
|
setFiles((prev) => prev.filter((f) => !deletedNames.includes(f.name)));
|
|
|
|
if (failed === 0) {
|
|
showNotification(
|
|
t('auth_files.delete_filtered_success', { count: success, type: typeLabel }),
|
|
'success'
|
|
);
|
|
} else {
|
|
showNotification(
|
|
t('auth_files.delete_filtered_partial', { success, failed, type: typeLabel }),
|
|
'warning'
|
|
);
|
|
}
|
|
onResetFilterToAll();
|
|
}
|
|
} catch (err: unknown) {
|
|
const errorMessage = err instanceof Error ? err.message : '';
|
|
showNotification(`${t('notification.delete_failed')}: ${errorMessage}`, 'error');
|
|
} finally {
|
|
setDeletingAll(false);
|
|
}
|
|
}
|
|
});
|
|
},
|
|
[files, showConfirmation, showNotification, t]
|
|
);
|
|
|
|
const handleDownload = useCallback(
|
|
async (name: string) => {
|
|
try {
|
|
const response = await apiClient.getRaw(
|
|
`/auth-files/download?name=${encodeURIComponent(name)}`,
|
|
{ responseType: 'blob' }
|
|
);
|
|
const blob = new Blob([response.data]);
|
|
const url = window.URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = name;
|
|
a.click();
|
|
window.URL.revokeObjectURL(url);
|
|
showNotification(t('auth_files.download_success'), 'success');
|
|
} catch (err: unknown) {
|
|
const errorMessage = err instanceof Error ? err.message : '';
|
|
showNotification(`${t('notification.download_failed')}: ${errorMessage}`, 'error');
|
|
}
|
|
},
|
|
[showNotification, t]
|
|
);
|
|
|
|
const handleStatusToggle = useCallback(
|
|
async (item: AuthFileItem, enabled: boolean) => {
|
|
const name = item.name;
|
|
const nextDisabled = !enabled;
|
|
const previousDisabled = item.disabled === true;
|
|
|
|
setStatusUpdating((prev) => ({ ...prev, [name]: true }));
|
|
setFiles((prev) => prev.map((f) => (f.name === name ? { ...f, disabled: nextDisabled } : f)));
|
|
|
|
try {
|
|
const res = await authFilesApi.setStatus(name, nextDisabled);
|
|
setFiles((prev) =>
|
|
prev.map((f) => (f.name === name ? { ...f, disabled: res.disabled } : f))
|
|
);
|
|
showNotification(
|
|
enabled
|
|
? t('auth_files.status_enabled_success', { name })
|
|
: t('auth_files.status_disabled_success', { name }),
|
|
'success'
|
|
);
|
|
} catch (err: unknown) {
|
|
const errorMessage = err instanceof Error ? err.message : '';
|
|
setFiles((prev) =>
|
|
prev.map((f) => (f.name === name ? { ...f, disabled: previousDisabled } : f))
|
|
);
|
|
showNotification(`${t('notification.update_failed')}: ${errorMessage}`, 'error');
|
|
} finally {
|
|
setStatusUpdating((prev) => {
|
|
if (!prev[name]) return prev;
|
|
const next = { ...prev };
|
|
delete next[name];
|
|
return next;
|
|
});
|
|
}
|
|
},
|
|
[showNotification, t]
|
|
);
|
|
|
|
return {
|
|
files,
|
|
loading,
|
|
error,
|
|
uploading,
|
|
deleting,
|
|
deletingAll,
|
|
statusUpdating,
|
|
fileInputRef,
|
|
loadFiles,
|
|
handleUploadClick,
|
|
handleFileChange,
|
|
handleDelete,
|
|
handleDeleteAll,
|
|
handleDownload,
|
|
handleStatusToggle
|
|
};
|
|
}
|