mirror of
https://github.com/Noooste/garage-ui.git
synced 2026-07-26 07:48:13 +00:00
94ad142edf
Signed-off-by: Noste <83548733+Noooste@users.noreply.github.com>
411 lines
12 KiB
TypeScript
411 lines
12 KiB
TypeScript
import axios from 'axios';
|
|
import {toast} from 'sonner';
|
|
import type {
|
|
AccessKey,
|
|
Bucket,
|
|
BucketDetails,
|
|
ClusterHealth,
|
|
ClusterStatistics,
|
|
ClusterStatus,
|
|
GarageMetrics,
|
|
MultiNodeResponse,
|
|
MultiNodeStatisticsResponse,
|
|
ObjectListResponse,
|
|
ObjectMetadata,
|
|
S3Object,
|
|
StorageMetrics,
|
|
} from '@/types';
|
|
import type { AuthUser } from '@/types/auth';
|
|
|
|
// Helper function to encode object keys for URLs
|
|
// Encodes the entire key including slashes to ensure proper handling of special characters
|
|
const encodeObjectKey = (key: string): string => {
|
|
return encodeURIComponent(key);
|
|
};
|
|
|
|
const api = axios.create({
|
|
baseURL: '/api',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
});
|
|
|
|
// Separate axios instance for auth endpoints (which are not under /api)
|
|
const authApiClient = axios.create({
|
|
baseURL: '/auth',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
});
|
|
|
|
api.interceptors.request.use((config) => {
|
|
const token = localStorage.getItem('auth-token');
|
|
if (token) {
|
|
config.headers.Authorization = `Bearer ${token}`;
|
|
}
|
|
return config;
|
|
});
|
|
|
|
authApiClient.interceptors.request.use((config) => {
|
|
const token = localStorage.getItem('auth-token');
|
|
if (token) {
|
|
config.headers.Authorization = `Bearer ${token}`;
|
|
}
|
|
return config;
|
|
});
|
|
|
|
api.interceptors.response.use(
|
|
(response) => {
|
|
// If response has success=false in data, treat it as an error
|
|
if (response.data && response.data.success === false && response.data.error) {
|
|
const error = response.data.error;
|
|
const errorMessage = error.message || 'An error occurred';
|
|
const errorCode = error.code || 'UNKNOWN_ERROR';
|
|
|
|
// Display toast with error details
|
|
toast.error(errorMessage, {
|
|
description: `Error Code: ${errorCode}`,
|
|
});
|
|
|
|
// Reject the promise so it's treated as an error
|
|
return Promise.reject(new Error(errorMessage));
|
|
}
|
|
return response;
|
|
},
|
|
(error) => {
|
|
// Handle 401 Unauthorized - redirect to login
|
|
if (error.response?.status === 401) {
|
|
// Clear auth token
|
|
localStorage.removeItem('auth-token');
|
|
|
|
// Only redirect if not already on login page
|
|
if (window.location.pathname !== '/login') {
|
|
window.location.href = '/login';
|
|
}
|
|
|
|
return Promise.reject(error);
|
|
}
|
|
|
|
// Handle axios errors
|
|
if (error.response) {
|
|
// Server responded with error status
|
|
const data = error.response.data;
|
|
|
|
if (data && data.error) {
|
|
const errorMessage = data.error.message || 'An error occurred';
|
|
const errorCode = data.error.code || 'UNKNOWN_ERROR';
|
|
|
|
toast.error(errorMessage, {
|
|
description: `Error Code: ${errorCode}`,
|
|
});
|
|
} else {
|
|
// Generic HTTP error
|
|
toast.error(`Request failed: ${error.response.status}`, {
|
|
description: error.response.statusText || 'Unknown error',
|
|
});
|
|
}
|
|
} else if (error.request) {
|
|
// Request made but no response received
|
|
toast.error('Network Error', {
|
|
description: 'Unable to reach the server. Please check your connection.',
|
|
});
|
|
} else {
|
|
// Something else happened
|
|
toast.error('Error', {
|
|
description: error.message || 'An unexpected error occurred',
|
|
});
|
|
}
|
|
|
|
return Promise.reject(error);
|
|
}
|
|
);
|
|
|
|
// Auth API
|
|
export const authApi = {
|
|
getConfig: async () => {
|
|
const response = await authApiClient.get<{
|
|
admin: { enabled: boolean };
|
|
oidc: { enabled: boolean; provider?: string };
|
|
}>('/config');
|
|
return response;
|
|
},
|
|
|
|
loginAdmin: async (username: string, password: string) => {
|
|
const response = await authApiClient.post<{ success: boolean; token: string; user: AuthUser }>('/login', {
|
|
username,
|
|
password,
|
|
});
|
|
return response;
|
|
},
|
|
|
|
me: async () => {
|
|
const response = await authApiClient.get<{ success: boolean; user: AuthUser }>('/me');
|
|
return response;
|
|
},
|
|
|
|
logoutAdmin: async () => {
|
|
// For admin, just clear local storage (no server logout needed)
|
|
return Promise.resolve();
|
|
},
|
|
|
|
logoutOIDC: async () => {
|
|
const response = await authApiClient.post('/oidc/logout');
|
|
return response;
|
|
},
|
|
|
|
loginOIDC: () => {
|
|
window.location.href = '/auth/oidc/login';
|
|
},
|
|
};
|
|
|
|
// Health API
|
|
export const healthApi = {
|
|
getVersion: async (): Promise<string> => {
|
|
const response = await api.get('/v1/health');
|
|
return response.data.data.version as string;
|
|
},
|
|
};
|
|
|
|
// Bucket API
|
|
export const bucketsApi = {
|
|
list: async (): Promise<Bucket[]> => {
|
|
const response = await api.get('/v1/buckets');
|
|
return response.data.data.buckets || [];
|
|
},
|
|
|
|
get: async (name: string): Promise<BucketDetails> => {
|
|
const response = await api.get(`/v1/buckets/${name}`);
|
|
return response.data.data;
|
|
},
|
|
|
|
create: async (bucketName: string, bucketRegion?: string): Promise<void> => {
|
|
await api.post('/v1/buckets', { name: bucketName, region: bucketRegion });
|
|
},
|
|
|
|
delete: async (name: string): Promise<void> => {
|
|
await api.delete(`/v1/buckets/${name}`);
|
|
},
|
|
|
|
grantPermission: async (
|
|
bucketName: string,
|
|
accessKeyId: string,
|
|
permissions: { read: boolean; write: boolean; owner: boolean }
|
|
): Promise<void> => {
|
|
await api.post(`/v1/buckets/${bucketName}/permissions`, {
|
|
accessKeyId,
|
|
permissions,
|
|
});
|
|
},
|
|
|
|
updateSettings: async (name: string, settings: Partial<BucketDetails>): Promise<void> => {
|
|
await api.patch(`/v1/buckets/${name}/settings`, settings);
|
|
},
|
|
};
|
|
|
|
// Objects API
|
|
export const objectsApi = {
|
|
list: async (bucket: string, prefix?: string, maxKeys?: number, continuationToken?: string): Promise<ObjectListResponse> => {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const params: any = {};
|
|
if (prefix) params.prefix = prefix;
|
|
if (maxKeys) params.max_keys = maxKeys;
|
|
if (continuationToken) params.continuation_token = continuationToken;
|
|
|
|
const response = await api.get(`/v1/buckets/${bucket}/objects`, { params });
|
|
const data = response.data.data;
|
|
|
|
// Combine objects and prefixes (folders)
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const objects: S3Object[] = data.objects?.map((obj: any) => ({
|
|
key: obj.key,
|
|
size: obj.size,
|
|
lastModified: obj.last_modified,
|
|
etag: obj.etag,
|
|
contentType: obj.content_type,
|
|
storageClass: obj.storage_class,
|
|
isFolder: false,
|
|
})) || [];
|
|
|
|
const folders: S3Object[] = data.prefixes?.map((prefix: string) => ({
|
|
key: prefix,
|
|
size: 0,
|
|
lastModified: null,
|
|
isFolder: true,
|
|
})) || [];
|
|
|
|
return {
|
|
bucket: data.bucket,
|
|
objects: [...folders, ...objects],
|
|
prefixes: data.prefixes || [],
|
|
count: data.count,
|
|
isTruncated: data.is_truncated || false,
|
|
nextContinuationToken: data.next_continuation_token,
|
|
};
|
|
},
|
|
|
|
get: async (bucket: string, key: string): Promise<Blob> => {
|
|
const response = await api.get(`/v1/buckets/${bucket}/objects/${encodeObjectKey(key)}`, {
|
|
responseType: 'blob'
|
|
});
|
|
return response.data;
|
|
},
|
|
|
|
getMetadata: async (bucket: string, key: string): Promise<ObjectMetadata> => {
|
|
const response = await api.get(`/v1/buckets/${bucket}/objects/${encodeObjectKey(key)}/metadata`);
|
|
const data = response.data.data;
|
|
return {
|
|
key: data.key,
|
|
size: data.size,
|
|
lastModified: data.last_modified,
|
|
contentType: data.content_type,
|
|
etag: data.etag,
|
|
storageClass: data.storage_class,
|
|
metadata: data.metadata,
|
|
};
|
|
},
|
|
|
|
upload: async (bucket: string, key: string, file: File, onProgress?: (progress: number) => void): Promise<void> => {
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
formData.append('key', key);
|
|
await api.post(`/v1/buckets/${bucket}/objects`, formData, {
|
|
headers: { 'Content-Type': 'multipart/form-data' },
|
|
onUploadProgress: (progressEvent) => {
|
|
if (onProgress && progressEvent.total) {
|
|
const progress = Math.round((progressEvent.loaded * 100) / progressEvent.total);
|
|
onProgress(progress);
|
|
}
|
|
},
|
|
});
|
|
},
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
uploadMultiple: async (bucket: string, files: File[]): Promise<any> => {
|
|
const formData = new FormData();
|
|
files.forEach(file => {
|
|
formData.append('files', file);
|
|
});
|
|
const response = await api.post(`/v1/buckets/${bucket}/objects/upload-multiple`, formData, {
|
|
headers: { 'Content-Type': 'multipart/form-data' },
|
|
});
|
|
return response.data.data;
|
|
},
|
|
|
|
delete: async (bucket: string, key: string): Promise<void> => {
|
|
await api.delete(`/v1/buckets/${bucket}/objects/${encodeObjectKey(key)}`);
|
|
},
|
|
|
|
deleteMultiple: async (bucket: string, keys: string[], prefix?: string): Promise<void> => {
|
|
const payload = { keys, ...(prefix && { prefix }) };
|
|
await api.post(`/v1/buckets/${bucket}/objects/delete-multiple`, payload);
|
|
},
|
|
|
|
getPresignedUrl: async (bucket: string, key: string, expiresIn: number = 3600): Promise<string> => {
|
|
const response = await api.get(`/v1/buckets/${bucket}/objects/${encodeObjectKey(key)}/presign`, {
|
|
params: { expires_in: expiresIn }
|
|
});
|
|
return response.data.data.url;
|
|
},
|
|
};
|
|
|
|
// Access Control API (Users/Keys)
|
|
export const accessApi = {
|
|
listKeys: async (): Promise<AccessKey[]> => {
|
|
const response = await api.get('/v1/users');
|
|
return response.data.data.users || [];
|
|
},
|
|
|
|
getKey: async (accessKey: string): Promise<AccessKey> => {
|
|
const response = await api.get(`/v1/users/${accessKey}`);
|
|
return response.data.data;
|
|
},
|
|
|
|
getSecretKey: async (accessKey: string): Promise<string> => {
|
|
const response = await api.get(`/v1/users/${accessKey}/secret`);
|
|
return response.data.data.secretKey;
|
|
},
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
createKey: async (name: string, permissions?: any[]): Promise<AccessKey> => {
|
|
const response = await api.post('/v1/users', { name, permissions });
|
|
return response.data.data;
|
|
},
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
updateKey: async (accessKey: string, updates: any): Promise<void> => {
|
|
await api.patch(`/v1/users/${accessKey}`, updates);
|
|
},
|
|
|
|
deleteKey: async (accessKey: string): Promise<void> => {
|
|
await api.delete(`/v1/users/${accessKey}`);
|
|
},
|
|
};
|
|
|
|
// Analytics API
|
|
export const analyticsApi = {
|
|
getMetrics: async (): Promise<StorageMetrics> => {
|
|
const response = await api.get('/v1/monitoring/dashboard');
|
|
return response.data.data;
|
|
},
|
|
};
|
|
|
|
// Garage Cluster & Monitoring API
|
|
export const garageApi = {
|
|
getClusterHealth: async (): Promise<ClusterHealth> => {
|
|
const response = await api.get('/v1/cluster/health');
|
|
return response.data.data;
|
|
},
|
|
|
|
getClusterStatus: async (): Promise<ClusterStatus> => {
|
|
const response = await api.get('/v1/cluster/status');
|
|
return response.data.data;
|
|
},
|
|
|
|
getClusterStatistics: async (): Promise<ClusterStatistics> => {
|
|
const response = await api.get('/v1/cluster/statistics');
|
|
return response.data.data;
|
|
},
|
|
|
|
getNodeInfo: async (nodeId: string = 'self'): Promise<MultiNodeResponse> => {
|
|
const response = await api.get(`/v1/cluster/nodes/${nodeId}`);
|
|
return response.data.data;
|
|
},
|
|
|
|
getNodeStatistics: async (nodeId: string): Promise<MultiNodeStatisticsResponse> => {
|
|
const response = await api.get(`/v1/cluster/nodes/${nodeId}/statistics`);
|
|
return response.data.data;
|
|
},
|
|
|
|
getFullMetrics: async (): Promise<GarageMetrics> => {
|
|
// Fetch all cluster-related metrics
|
|
const [health, statistics, storageMetrics] = await Promise.all([
|
|
garageApi.getClusterHealth(),
|
|
garageApi.getClusterStatistics(),
|
|
analyticsApi.getMetrics(),
|
|
]);
|
|
|
|
return {
|
|
...storageMetrics,
|
|
clusterHealth: health,
|
|
clusterStatistics: statistics,
|
|
};
|
|
},
|
|
};
|
|
|
|
// Monitoring API
|
|
export const monitoringApi = {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
getMetrics: async (): Promise<any> => {
|
|
const response = await api.get('/v1/monitoring/metrics');
|
|
return response.data.data;
|
|
},
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
checkAdminHealth: async (): Promise<any> => {
|
|
const response = await api.get('/v1/monitoring/admin-health');
|
|
return response.data.data;
|
|
},
|
|
};
|
|
|
|
export default api;
|