mirror of
https://github.com/samanhappy/mcphub.git
synced 2025-12-23 18:29:21 -05:00
feat: Refactor API URL handling and add base path support (#131)
This commit is contained in:
@@ -18,8 +18,8 @@ RUN npm install -g pnpm
|
|||||||
ARG REQUEST_TIMEOUT=60000
|
ARG REQUEST_TIMEOUT=60000
|
||||||
ENV REQUEST_TIMEOUT=$REQUEST_TIMEOUT
|
ENV REQUEST_TIMEOUT=$REQUEST_TIMEOUT
|
||||||
|
|
||||||
ARG VITE_BASE_PATH=""
|
ARG BASE_PATH=""
|
||||||
ENV VITE_BASE_PATH=$VITE_BASE_PATH
|
ENV BASE_PATH=$BASE_PATH
|
||||||
|
|
||||||
ENV PNPM_HOME=/usr/local/share/pnpm
|
ENV PNPM_HOME=/usr/local/share/pnpm
|
||||||
ENV PATH=$PNPM_HOME:$PATH
|
ENV PATH=$PNPM_HOME:$PATH
|
||||||
|
|||||||
@@ -1,16 +1,13 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>MCP Hub Dashboard</title>
|
<title>MCP Hub Dashboard</title>
|
||||||
<link rel="icon" type="image/x-icon" href="./favicon.ico">
|
<link rel="icon" type="image/x-icon" href="/favicon.ico">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="bg-gray-100">
|
<body class="bg-gray-100">
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<script type="module" src="./src/main.tsx"></script>
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@@ -15,13 +15,16 @@ import LogsPage from './pages/LogsPage';
|
|||||||
|
|
||||||
// Get base path from environment variable or default to empty string
|
// Get base path from environment variable or default to empty string
|
||||||
const getBasePath = (): string => {
|
const getBasePath = (): string => {
|
||||||
const basePath = import.meta.env.VITE_BASE_PATH || '';
|
const basePath = import.meta.env.BASE_PATH || '';
|
||||||
return basePath.startsWith('/') ? basePath : '';
|
// Ensure the path starts with / if it's not empty and doesn't already start with /
|
||||||
|
if (basePath && !basePath.startsWith('/')) {
|
||||||
|
return '/' + basePath;
|
||||||
|
}
|
||||||
|
return basePath;
|
||||||
};
|
};
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const basename = getBasePath();
|
const basename = getBasePath();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import ServerForm from './ServerForm'
|
import ServerForm from './ServerForm'
|
||||||
|
import { getApiUrl } from '../utils/api'
|
||||||
|
|
||||||
interface AddServerFormProps {
|
interface AddServerFormProps {
|
||||||
onAdd: () => void
|
onAdd: () => void
|
||||||
@@ -20,7 +21,7 @@ const AddServerForm = ({ onAdd }: AddServerFormProps) => {
|
|||||||
try {
|
try {
|
||||||
setError(null)
|
setError(null)
|
||||||
const token = localStorage.getItem('mcphub_token');
|
const token = localStorage.getItem('mcphub_token');
|
||||||
const response = await fetch('/api/servers', {
|
const response = await fetch(getApiUrl('/servers'), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Group, ApiResponse } from '@/types';
|
import { Group, ApiResponse } from '@/types';
|
||||||
|
import { getApiUrl } from '../utils/api';
|
||||||
|
|
||||||
export const useGroupData = () => {
|
export const useGroupData = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -13,25 +14,25 @@ export const useGroupData = () => {
|
|||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const token = localStorage.getItem('mcphub_token');
|
const token = localStorage.getItem('mcphub_token');
|
||||||
const response = await fetch('/api/groups', {
|
const response = await fetch(getApiUrl('/groups'), {
|
||||||
headers: {
|
headers: {
|
||||||
'x-auth-token': token || ''
|
'x-auth-token': token || '',
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Status: ${response.status}`);
|
throw new Error(`Status: ${response.status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const data: ApiResponse<Group[]> = await response.json();
|
const data: ApiResponse<Group[]> = await response.json();
|
||||||
|
|
||||||
if (data && data.success && Array.isArray(data.data)) {
|
if (data && data.success && Array.isArray(data.data)) {
|
||||||
setGroups(data.data);
|
setGroups(data.data);
|
||||||
} else {
|
} else {
|
||||||
console.error('Invalid group data format:', data);
|
console.error('Invalid group data format:', data);
|
||||||
setGroups([]);
|
setGroups([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
setError(null);
|
setError(null);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error fetching groups:', err);
|
console.error('Error fetching groups:', err);
|
||||||
@@ -44,29 +45,29 @@ export const useGroupData = () => {
|
|||||||
|
|
||||||
// Trigger a refresh of the groups data
|
// Trigger a refresh of the groups data
|
||||||
const triggerRefresh = useCallback(() => {
|
const triggerRefresh = useCallback(() => {
|
||||||
setRefreshKey(prev => prev + 1);
|
setRefreshKey((prev) => prev + 1);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Create a new group with server associations
|
// Create a new group with server associations
|
||||||
const createGroup = async (name: string, description?: string, servers: string[] = []) => {
|
const createGroup = async (name: string, description?: string, servers: string[] = []) => {
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('mcphub_token');
|
const token = localStorage.getItem('mcphub_token');
|
||||||
const response = await fetch('/api/groups', {
|
const response = await fetch(getApiUrl('/groups'), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'x-auth-token': token || ''
|
'x-auth-token': token || '',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ name, description, servers }),
|
body: JSON.stringify({ name, description, servers }),
|
||||||
});
|
});
|
||||||
|
|
||||||
const result: ApiResponse<Group> = await response.json();
|
const result: ApiResponse<Group> = await response.json();
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
setError(result.message || t('groups.createError'));
|
setError(result.message || t('groups.createError'));
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
triggerRefresh();
|
triggerRefresh();
|
||||||
return result.data || null;
|
return result.data || null;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -76,25 +77,28 @@ export const useGroupData = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Update an existing group with server associations
|
// Update an existing group with server associations
|
||||||
const updateGroup = async (id: string, data: { name?: string; description?: string; servers?: string[] }) => {
|
const updateGroup = async (
|
||||||
|
id: string,
|
||||||
|
data: { name?: string; description?: string; servers?: string[] },
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('mcphub_token');
|
const token = localStorage.getItem('mcphub_token');
|
||||||
const response = await fetch(`/api/groups/${id}`, {
|
const response = await fetch(getApiUrl(`/groups/${id}`), {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'x-auth-token': token || ''
|
'x-auth-token': token || '',
|
||||||
},
|
},
|
||||||
body: JSON.stringify(data),
|
body: JSON.stringify(data),
|
||||||
});
|
});
|
||||||
|
|
||||||
const result: ApiResponse<Group> = await response.json();
|
const result: ApiResponse<Group> = await response.json();
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
setError(result.message || t('groups.updateError'));
|
setError(result.message || t('groups.updateError'));
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
triggerRefresh();
|
triggerRefresh();
|
||||||
return result.data || null;
|
return result.data || null;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -107,22 +111,22 @@ export const useGroupData = () => {
|
|||||||
const updateGroupServers = async (groupId: string, servers: string[]) => {
|
const updateGroupServers = async (groupId: string, servers: string[]) => {
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('mcphub_token');
|
const token = localStorage.getItem('mcphub_token');
|
||||||
const response = await fetch(`/api/groups/${groupId}/servers/batch`, {
|
const response = await fetch(getApiUrl(`/groups/${groupId}/servers/batch`), {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'x-auth-token': token || ''
|
'x-auth-token': token || '',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ servers }),
|
body: JSON.stringify({ servers }),
|
||||||
});
|
});
|
||||||
|
|
||||||
const result: ApiResponse<Group> = await response.json();
|
const result: ApiResponse<Group> = await response.json();
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
setError(result.message || t('groups.updateError'));
|
setError(result.message || t('groups.updateError'));
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
triggerRefresh();
|
triggerRefresh();
|
||||||
return result.data || null;
|
return result.data || null;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -135,20 +139,20 @@ export const useGroupData = () => {
|
|||||||
const deleteGroup = async (id: string) => {
|
const deleteGroup = async (id: string) => {
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('mcphub_token');
|
const token = localStorage.getItem('mcphub_token');
|
||||||
const response = await fetch(`/api/groups/${id}`, {
|
const response = await fetch(getApiUrl(`/groups/${id}`), {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: {
|
headers: {
|
||||||
'x-auth-token': token || ''
|
'x-auth-token': token || '',
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
setError(result.message || t('groups.deleteError'));
|
setError(result.message || t('groups.deleteError'));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
triggerRefresh();
|
triggerRefresh();
|
||||||
return true;
|
return true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -161,22 +165,22 @@ export const useGroupData = () => {
|
|||||||
const addServerToGroup = async (groupId: string, serverName: string) => {
|
const addServerToGroup = async (groupId: string, serverName: string) => {
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('mcphub_token');
|
const token = localStorage.getItem('mcphub_token');
|
||||||
const response = await fetch(`/api/groups/${groupId}/servers`, {
|
const response = await fetch(getApiUrl(`/groups/${groupId}/servers`), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'x-auth-token': token || ''
|
'x-auth-token': token || '',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ serverName }),
|
body: JSON.stringify({ serverName }),
|
||||||
});
|
});
|
||||||
|
|
||||||
const result: ApiResponse<Group> = await response.json();
|
const result: ApiResponse<Group> = await response.json();
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
setError(result.message || t('groups.serverAddError'));
|
setError(result.message || t('groups.serverAddError'));
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
triggerRefresh();
|
triggerRefresh();
|
||||||
return result.data || null;
|
return result.data || null;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -189,20 +193,20 @@ export const useGroupData = () => {
|
|||||||
const removeServerFromGroup = async (groupId: string, serverName: string) => {
|
const removeServerFromGroup = async (groupId: string, serverName: string) => {
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('mcphub_token');
|
const token = localStorage.getItem('mcphub_token');
|
||||||
const response = await fetch(`/api/groups/${groupId}/servers/${serverName}`, {
|
const response = await fetch(getApiUrl(`/groups/${groupId}/servers/${serverName}`), {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: {
|
headers: {
|
||||||
'x-auth-token': token || ''
|
'x-auth-token': token || '',
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const result: ApiResponse<Group> = await response.json();
|
const result: ApiResponse<Group> = await response.json();
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
setError(result.message || t('groups.serverRemoveError'));
|
setError(result.message || t('groups.serverRemoveError'));
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
triggerRefresh();
|
triggerRefresh();
|
||||||
return result.data || null;
|
return result.data || null;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -227,6 +231,6 @@ export const useGroupData = () => {
|
|||||||
updateGroupServers,
|
updateGroupServers,
|
||||||
deleteGroup,
|
deleteGroup,
|
||||||
addServerToGroup,
|
addServerToGroup,
|
||||||
removeServerFromGroup
|
removeServerFromGroup,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { MarketServer, ApiResponse } from '@/types';
|
import { MarketServer, ApiResponse } from '@/types';
|
||||||
|
import { getApiUrl } from '../utils/api';
|
||||||
|
|
||||||
export const useMarketData = () => {
|
export const useMarketData = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -15,7 +16,7 @@ export const useMarketData = () => {
|
|||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [currentServer, setCurrentServer] = useState<MarketServer | null>(null);
|
const [currentServer, setCurrentServer] = useState<MarketServer | null>(null);
|
||||||
const [installedServers, setInstalledServers] = useState<string[]>([]);
|
const [installedServers, setInstalledServers] = useState<string[]>([]);
|
||||||
|
|
||||||
// Pagination states
|
// Pagination states
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const [serversPerPage, setServersPerPage] = useState(9);
|
const [serversPerPage, setServersPerPage] = useState(9);
|
||||||
@@ -26,18 +27,18 @@ export const useMarketData = () => {
|
|||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const token = localStorage.getItem('mcphub_token');
|
const token = localStorage.getItem('mcphub_token');
|
||||||
const response = await fetch('/api/market/servers', {
|
const response = await fetch(getApiUrl('/market/servers'), {
|
||||||
headers: {
|
headers: {
|
||||||
'x-auth-token': token || ''
|
'x-auth-token': token || '',
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Status: ${response.status}`);
|
throw new Error(`Status: ${response.status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const data: ApiResponse<MarketServer[]> = await response.json();
|
const data: ApiResponse<MarketServer[]> = await response.json();
|
||||||
|
|
||||||
if (data && data.success && Array.isArray(data.data)) {
|
if (data && data.success && Array.isArray(data.data)) {
|
||||||
setAllServers(data.data);
|
setAllServers(data.data);
|
||||||
// Apply pagination to the fetched data
|
// Apply pagination to the fetched data
|
||||||
@@ -55,44 +56,50 @@ export const useMarketData = () => {
|
|||||||
}, [t]);
|
}, [t]);
|
||||||
|
|
||||||
// Apply pagination to data
|
// Apply pagination to data
|
||||||
const applyPagination = useCallback((data: MarketServer[], page: number, itemsPerPage = serversPerPage) => {
|
const applyPagination = useCallback(
|
||||||
const totalItems = data.length;
|
(data: MarketServer[], page: number, itemsPerPage = serversPerPage) => {
|
||||||
const calculatedTotalPages = Math.ceil(totalItems / itemsPerPage);
|
const totalItems = data.length;
|
||||||
setTotalPages(calculatedTotalPages);
|
const calculatedTotalPages = Math.ceil(totalItems / itemsPerPage);
|
||||||
|
setTotalPages(calculatedTotalPages);
|
||||||
|
|
||||||
// Ensure current page is valid
|
// Ensure current page is valid
|
||||||
const validPage = Math.max(1, Math.min(page, calculatedTotalPages));
|
const validPage = Math.max(1, Math.min(page, calculatedTotalPages));
|
||||||
if (validPage !== page) {
|
if (validPage !== page) {
|
||||||
setCurrentPage(validPage);
|
setCurrentPage(validPage);
|
||||||
}
|
}
|
||||||
|
|
||||||
const startIndex = (validPage - 1) * itemsPerPage;
|
const startIndex = (validPage - 1) * itemsPerPage;
|
||||||
const paginatedServers = data.slice(startIndex, startIndex + itemsPerPage);
|
const paginatedServers = data.slice(startIndex, startIndex + itemsPerPage);
|
||||||
setServers(paginatedServers);
|
setServers(paginatedServers);
|
||||||
}, [serversPerPage]);
|
},
|
||||||
|
[serversPerPage],
|
||||||
|
);
|
||||||
|
|
||||||
// Change page
|
// Change page
|
||||||
const changePage = useCallback((page: number) => {
|
const changePage = useCallback(
|
||||||
setCurrentPage(page);
|
(page: number) => {
|
||||||
applyPagination(allServers, page, serversPerPage);
|
setCurrentPage(page);
|
||||||
}, [allServers, applyPagination, serversPerPage]);
|
applyPagination(allServers, page, serversPerPage);
|
||||||
|
},
|
||||||
|
[allServers, applyPagination, serversPerPage],
|
||||||
|
);
|
||||||
|
|
||||||
// Fetch all categories
|
// Fetch all categories
|
||||||
const fetchCategories = useCallback(async () => {
|
const fetchCategories = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('mcphub_token');
|
const token = localStorage.getItem('mcphub_token');
|
||||||
const response = await fetch('/api/market/categories', {
|
const response = await fetch(getApiUrl('/market/categories'), {
|
||||||
headers: {
|
headers: {
|
||||||
'x-auth-token': token || ''
|
'x-auth-token': token || '',
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Status: ${response.status}`);
|
throw new Error(`Status: ${response.status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const data: ApiResponse<string[]> = await response.json();
|
const data: ApiResponse<string[]> = await response.json();
|
||||||
|
|
||||||
if (data && data.success && Array.isArray(data.data)) {
|
if (data && data.success && Array.isArray(data.data)) {
|
||||||
setCategories(data.data);
|
setCategories(data.data);
|
||||||
} else {
|
} else {
|
||||||
@@ -107,18 +114,18 @@ export const useMarketData = () => {
|
|||||||
const fetchTags = useCallback(async () => {
|
const fetchTags = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('mcphub_token');
|
const token = localStorage.getItem('mcphub_token');
|
||||||
const response = await fetch('/api/market/tags', {
|
const response = await fetch(getApiUrl('/market/tags'), {
|
||||||
headers: {
|
headers: {
|
||||||
'x-auth-token': token || ''
|
'x-auth-token': token || '',
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Status: ${response.status}`);
|
throw new Error(`Status: ${response.status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const data: ApiResponse<string[]> = await response.json();
|
const data: ApiResponse<string[]> = await response.json();
|
||||||
|
|
||||||
if (data && data.success && Array.isArray(data.data)) {
|
if (data && data.success && Array.isArray(data.data)) {
|
||||||
setTags(data.data);
|
setTags(data.data);
|
||||||
} else {
|
} else {
|
||||||
@@ -130,178 +137,196 @@ export const useMarketData = () => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Fetch server by name
|
// Fetch server by name
|
||||||
const fetchServerByName = useCallback(async (name: string) => {
|
const fetchServerByName = useCallback(
|
||||||
try {
|
async (name: string) => {
|
||||||
setLoading(true);
|
try {
|
||||||
const token = localStorage.getItem('mcphub_token');
|
setLoading(true);
|
||||||
const response = await fetch(`/api/market/servers/${name}`, {
|
const token = localStorage.getItem('mcphub_token');
|
||||||
headers: {
|
const response = await fetch(getApiUrl(`/market/servers/${name}`), {
|
||||||
'x-auth-token': token || ''
|
headers: {
|
||||||
|
'x-auth-token': token || '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Status: ${response.status}`);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
const data: ApiResponse<MarketServer> = await response.json();
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Status: ${response.status}`);
|
if (data && data.success && data.data) {
|
||||||
}
|
setCurrentServer(data.data);
|
||||||
|
return data.data;
|
||||||
const data: ApiResponse<MarketServer> = await response.json();
|
} else {
|
||||||
|
console.error('Invalid server data format:', data);
|
||||||
if (data && data.success && data.data) {
|
setError(t('market.serverNotFound'));
|
||||||
setCurrentServer(data.data);
|
return null;
|
||||||
return data.data;
|
}
|
||||||
} else {
|
} catch (err) {
|
||||||
console.error('Invalid server data format:', data);
|
console.error(`Error fetching server ${name}:`, err);
|
||||||
setError(t('market.serverNotFound'));
|
setError(err instanceof Error ? err.message : String(err));
|
||||||
return null;
|
return null;
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
},
|
||||||
console.error(`Error fetching server ${name}:`, err);
|
[t],
|
||||||
setError(err instanceof Error ? err.message : String(err));
|
);
|
||||||
return null;
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}, [t]);
|
|
||||||
|
|
||||||
// Search servers by query
|
// Search servers by query
|
||||||
const searchServers = useCallback(async (query: string) => {
|
const searchServers = useCallback(
|
||||||
try {
|
async (query: string) => {
|
||||||
setLoading(true);
|
try {
|
||||||
setSearchQuery(query);
|
setLoading(true);
|
||||||
|
setSearchQuery(query);
|
||||||
if (!query.trim()) {
|
|
||||||
// Fetch fresh data from server instead of just applying pagination
|
if (!query.trim()) {
|
||||||
fetchMarketServers();
|
// Fetch fresh data from server instead of just applying pagination
|
||||||
return;
|
fetchMarketServers();
|
||||||
}
|
return;
|
||||||
|
|
||||||
const token = localStorage.getItem('mcphub_token');
|
|
||||||
const response = await fetch(`/api/market/servers/search?query=${encodeURIComponent(query)}`, {
|
|
||||||
headers: {
|
|
||||||
'x-auth-token': token || ''
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
const token = localStorage.getItem('mcphub_token');
|
||||||
if (!response.ok) {
|
const response = await fetch(
|
||||||
throw new Error(`Status: ${response.status}`);
|
getApiUrl(`/market/servers/search?query=${encodeURIComponent(query)}`),
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'x-auth-token': token || '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data: ApiResponse<MarketServer[]> = await response.json();
|
||||||
|
|
||||||
|
if (data && data.success && Array.isArray(data.data)) {
|
||||||
|
setAllServers(data.data);
|
||||||
|
setCurrentPage(1);
|
||||||
|
applyPagination(data.data, 1);
|
||||||
|
} else {
|
||||||
|
console.error('Invalid search results format:', data);
|
||||||
|
setError(t('market.searchError'));
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error searching servers:', err);
|
||||||
|
setError(err instanceof Error ? err.message : String(err));
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
const data: ApiResponse<MarketServer[]> = await response.json();
|
[t, allServers, applyPagination, fetchMarketServers],
|
||||||
|
);
|
||||||
if (data && data.success && Array.isArray(data.data)) {
|
|
||||||
setAllServers(data.data);
|
|
||||||
setCurrentPage(1);
|
|
||||||
applyPagination(data.data, 1);
|
|
||||||
} else {
|
|
||||||
console.error('Invalid search results format:', data);
|
|
||||||
setError(t('market.searchError'));
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error searching servers:', err);
|
|
||||||
setError(err instanceof Error ? err.message : String(err));
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}, [t, allServers, applyPagination, fetchMarketServers]);
|
|
||||||
|
|
||||||
// Filter servers by category
|
// Filter servers by category
|
||||||
const filterByCategory = useCallback(async (category: string) => {
|
const filterByCategory = useCallback(
|
||||||
try {
|
async (category: string) => {
|
||||||
setLoading(true);
|
try {
|
||||||
setSelectedCategory(category);
|
setLoading(true);
|
||||||
setSelectedTag(''); // Reset tag filter when filtering by category
|
setSelectedCategory(category);
|
||||||
|
setSelectedTag(''); // Reset tag filter when filtering by category
|
||||||
if (!category) {
|
|
||||||
fetchMarketServers();
|
if (!category) {
|
||||||
return;
|
fetchMarketServers();
|
||||||
}
|
return;
|
||||||
|
|
||||||
const token = localStorage.getItem('mcphub_token');
|
|
||||||
const response = await fetch(`/api/market/categories/${encodeURIComponent(category)}`, {
|
|
||||||
headers: {
|
|
||||||
'x-auth-token': token || ''
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
const token = localStorage.getItem('mcphub_token');
|
||||||
if (!response.ok) {
|
const response = await fetch(
|
||||||
throw new Error(`Status: ${response.status}`);
|
getApiUrl(`/market/categories/${encodeURIComponent(category)}`),
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'x-auth-token': token || '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data: ApiResponse<MarketServer[]> = await response.json();
|
||||||
|
|
||||||
|
if (data && data.success && Array.isArray(data.data)) {
|
||||||
|
setAllServers(data.data);
|
||||||
|
setCurrentPage(1);
|
||||||
|
applyPagination(data.data, 1);
|
||||||
|
} else {
|
||||||
|
console.error('Invalid category filter results format:', data);
|
||||||
|
setError(t('market.filterError'));
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error filtering servers by category:', err);
|
||||||
|
setError(err instanceof Error ? err.message : String(err));
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
const data: ApiResponse<MarketServer[]> = await response.json();
|
[t, fetchMarketServers, applyPagination],
|
||||||
|
);
|
||||||
if (data && data.success && Array.isArray(data.data)) {
|
|
||||||
setAllServers(data.data);
|
|
||||||
setCurrentPage(1);
|
|
||||||
applyPagination(data.data, 1);
|
|
||||||
} else {
|
|
||||||
console.error('Invalid category filter results format:', data);
|
|
||||||
setError(t('market.filterError'));
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error filtering servers by category:', err);
|
|
||||||
setError(err instanceof Error ? err.message : String(err));
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}, [t, fetchMarketServers, applyPagination]);
|
|
||||||
|
|
||||||
// Filter servers by tag
|
// Filter servers by tag
|
||||||
const filterByTag = useCallback(async (tag: string) => {
|
const filterByTag = useCallback(
|
||||||
try {
|
async (tag: string) => {
|
||||||
setLoading(true);
|
try {
|
||||||
setSelectedTag(tag);
|
setLoading(true);
|
||||||
setSelectedCategory(''); // Reset category filter when filtering by tag
|
setSelectedTag(tag);
|
||||||
|
setSelectedCategory(''); // Reset category filter when filtering by tag
|
||||||
if (!tag) {
|
|
||||||
fetchMarketServers();
|
if (!tag) {
|
||||||
return;
|
fetchMarketServers();
|
||||||
}
|
return;
|
||||||
|
|
||||||
const token = localStorage.getItem('mcphub_token');
|
|
||||||
const response = await fetch(`/api/market/tags/${encodeURIComponent(tag)}`, {
|
|
||||||
headers: {
|
|
||||||
'x-auth-token': token || ''
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
const token = localStorage.getItem('mcphub_token');
|
||||||
if (!response.ok) {
|
const response = await fetch(getApiUrl(`/market/tags/${encodeURIComponent(tag)}`), {
|
||||||
throw new Error(`Status: ${response.status}`);
|
headers: {
|
||||||
|
'x-auth-token': token || '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data: ApiResponse<MarketServer[]> = await response.json();
|
||||||
|
|
||||||
|
if (data && data.success && Array.isArray(data.data)) {
|
||||||
|
setAllServers(data.data);
|
||||||
|
setCurrentPage(1);
|
||||||
|
applyPagination(data.data, 1);
|
||||||
|
} else {
|
||||||
|
console.error('Invalid tag filter results format:', data);
|
||||||
|
setError(t('market.tagFilterError'));
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error filtering servers by tag:', err);
|
||||||
|
setError(err instanceof Error ? err.message : String(err));
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
const data: ApiResponse<MarketServer[]> = await response.json();
|
[t, fetchMarketServers, applyPagination],
|
||||||
|
);
|
||||||
if (data && data.success && Array.isArray(data.data)) {
|
|
||||||
setAllServers(data.data);
|
|
||||||
setCurrentPage(1);
|
|
||||||
applyPagination(data.data, 1);
|
|
||||||
} else {
|
|
||||||
console.error('Invalid tag filter results format:', data);
|
|
||||||
setError(t('market.tagFilterError'));
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error filtering servers by tag:', err);
|
|
||||||
setError(err instanceof Error ? err.message : String(err));
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}, [t, fetchMarketServers, applyPagination]);
|
|
||||||
|
|
||||||
// Fetch installed servers
|
// Fetch installed servers
|
||||||
const fetchInstalledServers = useCallback(async () => {
|
const fetchInstalledServers = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('mcphub_token');
|
const token = localStorage.getItem('mcphub_token');
|
||||||
const response = await fetch('/api/servers', {
|
const response = await fetch(getApiUrl('/servers'), {
|
||||||
headers: {
|
headers: {
|
||||||
'x-auth-token': token || ''
|
'x-auth-token': token || '',
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Status: ${response.status}`);
|
throw new Error(`Status: ${response.status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (data && data.success && Array.isArray(data.data)) {
|
if (data && data.success && Array.isArray(data.data)) {
|
||||||
// Extract server names
|
// Extract server names
|
||||||
const installedServerNames = data.data.map((server: any) => server.name);
|
const installedServerNames = data.data.map((server: any) => server.name);
|
||||||
@@ -313,64 +338,77 @@ export const useMarketData = () => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Check if a server is already installed
|
// Check if a server is already installed
|
||||||
const isServerInstalled = useCallback((serverName: string) => {
|
const isServerInstalled = useCallback(
|
||||||
return installedServers.includes(serverName);
|
(serverName: string) => {
|
||||||
}, [installedServers]);
|
return installedServers.includes(serverName);
|
||||||
|
},
|
||||||
|
[installedServers],
|
||||||
|
);
|
||||||
|
|
||||||
// Install server to the local environment
|
// Install server to the local environment
|
||||||
const installServer = useCallback(async (server: MarketServer) => {
|
const installServer = useCallback(
|
||||||
try {
|
async (server: MarketServer) => {
|
||||||
const installType = server.installations?.npm ? 'npm' : Object.keys(server.installations || {}).length > 0 ? Object.keys(server.installations)[0] : null;
|
try {
|
||||||
|
const installType = server.installations?.npm
|
||||||
if (!installType || !server.installations?.[installType]) {
|
? 'npm'
|
||||||
setError(t('market.noInstallationMethod'));
|
: Object.keys(server.installations || {}).length > 0
|
||||||
|
? Object.keys(server.installations)[0]
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (!installType || !server.installations?.[installType]) {
|
||||||
|
setError(t('market.noInstallationMethod'));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const installation = server.installations[installType];
|
||||||
|
|
||||||
|
// Prepare server configuration
|
||||||
|
const serverConfig = {
|
||||||
|
name: server.name,
|
||||||
|
config: {
|
||||||
|
command: installation.command,
|
||||||
|
args: installation.args,
|
||||||
|
env: installation.env || {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Call the createServer API
|
||||||
|
const token = localStorage.getItem('mcphub_token');
|
||||||
|
const response = await fetch(getApiUrl('/servers'), {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'x-auth-token': token || '',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(serverConfig),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json();
|
||||||
|
throw new Error(errorData.message || `Status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update installed servers list after successful installation
|
||||||
|
await fetchInstalledServers();
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error installing server:', err);
|
||||||
|
setError(err instanceof Error ? err.message : String(err));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
},
|
||||||
const installation = server.installations[installType];
|
[t, fetchInstalledServers],
|
||||||
|
);
|
||||||
// Prepare server configuration
|
|
||||||
const serverConfig = {
|
|
||||||
name: server.name,
|
|
||||||
config: {
|
|
||||||
command: installation.command,
|
|
||||||
args: installation.args,
|
|
||||||
env: installation.env || {}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Call the createServer API
|
|
||||||
const token = localStorage.getItem('mcphub_token');
|
|
||||||
const response = await fetch('/api/servers', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'x-auth-token': token || ''
|
|
||||||
},
|
|
||||||
body: JSON.stringify(serverConfig),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorData = await response.json();
|
|
||||||
throw new Error(errorData.message || `Status: ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update installed servers list after successful installation
|
|
||||||
await fetchInstalledServers();
|
|
||||||
return true;
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error installing server:', err);
|
|
||||||
setError(err instanceof Error ? err.message : String(err));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}, [t, fetchInstalledServers]);
|
|
||||||
|
|
||||||
// Change servers per page
|
// Change servers per page
|
||||||
const changeServersPerPage = useCallback((perPage: number) => {
|
const changeServersPerPage = useCallback(
|
||||||
setServersPerPage(perPage);
|
(perPage: number) => {
|
||||||
setCurrentPage(1);
|
setServersPerPage(perPage);
|
||||||
applyPagination(allServers, 1, perPage);
|
setCurrentPage(1);
|
||||||
}, [allServers, applyPagination]);
|
applyPagination(allServers, 1, perPage);
|
||||||
|
},
|
||||||
|
[allServers, applyPagination],
|
||||||
|
);
|
||||||
|
|
||||||
// Load initial data
|
// Load initial data
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -405,6 +443,6 @@ export const useMarketData = () => {
|
|||||||
changePage,
|
changePage,
|
||||||
changeServersPerPage,
|
changeServersPerPage,
|
||||||
// Installed servers methods
|
// Installed servers methods
|
||||||
isServerInstalled
|
isServerInstalled,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,18 +1,19 @@
|
|||||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Server, ApiResponse } from '@/types';
|
import { Server, ApiResponse } from '@/types';
|
||||||
|
import { getApiUrl } from '../utils/api';
|
||||||
|
|
||||||
// Configuration options
|
// Configuration options
|
||||||
const CONFIG = {
|
const CONFIG = {
|
||||||
// Initialization phase configuration
|
// Initialization phase configuration
|
||||||
startup: {
|
startup: {
|
||||||
maxAttempts: 60, // Maximum number of attempts during initialization
|
maxAttempts: 60, // Maximum number of attempts during initialization
|
||||||
pollingInterval: 3000 // Polling interval during initialization (3 seconds)
|
pollingInterval: 3000, // Polling interval during initialization (3 seconds)
|
||||||
},
|
},
|
||||||
// Normal operation phase configuration
|
// Normal operation phase configuration
|
||||||
normal: {
|
normal: {
|
||||||
pollingInterval: 10000 // Polling interval during normal operation (10 seconds)
|
pollingInterval: 10000, // Polling interval during normal operation (10 seconds)
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useServerData = () => {
|
export const useServerData = () => {
|
||||||
@@ -22,7 +23,7 @@ export const useServerData = () => {
|
|||||||
const [refreshKey, setRefreshKey] = useState(0);
|
const [refreshKey, setRefreshKey] = useState(0);
|
||||||
const [isInitialLoading, setIsInitialLoading] = useState(true);
|
const [isInitialLoading, setIsInitialLoading] = useState(true);
|
||||||
const [fetchAttempts, setFetchAttempts] = useState(0);
|
const [fetchAttempts, setFetchAttempts] = useState(0);
|
||||||
|
|
||||||
// Timer reference for polling
|
// Timer reference for polling
|
||||||
const intervalRef = useRef<NodeJS.Timeout | null>(null);
|
const intervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
// Track current attempt count to avoid dependency cycles
|
// Track current attempt count to avoid dependency cycles
|
||||||
@@ -40,17 +41,17 @@ export const useServerData = () => {
|
|||||||
const startNormalPolling = useCallback(() => {
|
const startNormalPolling = useCallback(() => {
|
||||||
// Ensure no other timers are running
|
// Ensure no other timers are running
|
||||||
clearTimer();
|
clearTimer();
|
||||||
|
|
||||||
const fetchServers = async () => {
|
const fetchServers = async () => {
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('mcphub_token');
|
const token = localStorage.getItem('mcphub_token');
|
||||||
const response = await fetch('/api/servers', {
|
const response = await fetch(getApiUrl('/servers'), {
|
||||||
headers: {
|
headers: {
|
||||||
'x-auth-token': token || ''
|
'x-auth-token': token || '',
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (data && data.success && Array.isArray(data.data)) {
|
if (data && data.success && Array.isArray(data.data)) {
|
||||||
setServers(data.data);
|
setServers(data.data);
|
||||||
} else if (data && Array.isArray(data)) {
|
} else if (data && Array.isArray(data)) {
|
||||||
@@ -59,29 +60,29 @@ export const useServerData = () => {
|
|||||||
console.error('Invalid server data format:', data);
|
console.error('Invalid server data format:', data);
|
||||||
setServers([]);
|
setServers([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset error state
|
// Reset error state
|
||||||
setError(null);
|
setError(null);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error fetching servers during normal polling:', err);
|
console.error('Error fetching servers during normal polling:', err);
|
||||||
|
|
||||||
// Use friendly error message
|
// Use friendly error message
|
||||||
if (!navigator.onLine) {
|
if (!navigator.onLine) {
|
||||||
setError(t('errors.network'));
|
setError(t('errors.network'));
|
||||||
} else if (err instanceof TypeError && (
|
} else if (
|
||||||
err.message.includes('NetworkError') ||
|
err instanceof TypeError &&
|
||||||
err.message.includes('Failed to fetch')
|
(err.message.includes('NetworkError') || err.message.includes('Failed to fetch'))
|
||||||
)) {
|
) {
|
||||||
setError(t('errors.serverConnection'));
|
setError(t('errors.serverConnection'));
|
||||||
} else {
|
} else {
|
||||||
setError(t('errors.serverFetch'));
|
setError(t('errors.serverFetch'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Execute immediately
|
// Execute immediately
|
||||||
fetchServers();
|
fetchServers();
|
||||||
|
|
||||||
// Set up regular polling
|
// Set up regular polling
|
||||||
intervalRef.current = setInterval(fetchServers, CONFIG.normal.pollingInterval);
|
intervalRef.current = setInterval(fetchServers, CONFIG.normal.pollingInterval);
|
||||||
}, [t]);
|
}, [t]);
|
||||||
@@ -92,18 +93,18 @@ export const useServerData = () => {
|
|||||||
attemptsRef.current = 0;
|
attemptsRef.current = 0;
|
||||||
setFetchAttempts(0);
|
setFetchAttempts(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialization phase request function
|
// Initialization phase request function
|
||||||
const fetchInitialData = async () => {
|
const fetchInitialData = async () => {
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('mcphub_token');
|
const token = localStorage.getItem('mcphub_token');
|
||||||
const response = await fetch('/api/servers', {
|
const response = await fetch(getApiUrl('/servers'), {
|
||||||
headers: {
|
headers: {
|
||||||
'x-auth-token': token || ''
|
'x-auth-token': token || '',
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
// Handle API response wrapper object, extract data field
|
// Handle API response wrapper object, extract data field
|
||||||
if (data && data.success && Array.isArray(data.data)) {
|
if (data && data.success && Array.isArray(data.data)) {
|
||||||
setServers(data.data);
|
setServers(data.data);
|
||||||
@@ -131,17 +132,17 @@ export const useServerData = () => {
|
|||||||
// Increment attempt count, use ref to avoid triggering effect rerun
|
// Increment attempt count, use ref to avoid triggering effect rerun
|
||||||
attemptsRef.current += 1;
|
attemptsRef.current += 1;
|
||||||
console.error(`Initial loading attempt ${attemptsRef.current} failed:`, err);
|
console.error(`Initial loading attempt ${attemptsRef.current} failed:`, err);
|
||||||
|
|
||||||
// Update state for display
|
// Update state for display
|
||||||
setFetchAttempts(attemptsRef.current);
|
setFetchAttempts(attemptsRef.current);
|
||||||
|
|
||||||
// Set appropriate error message
|
// Set appropriate error message
|
||||||
if (!navigator.onLine) {
|
if (!navigator.onLine) {
|
||||||
setError(t('errors.network'));
|
setError(t('errors.network'));
|
||||||
} else {
|
} else {
|
||||||
setError(t('errors.initialStartup'));
|
setError(t('errors.initialStartup'));
|
||||||
}
|
}
|
||||||
|
|
||||||
// If maximum attempt count is exceeded, give up initialization and switch to normal polling
|
// If maximum attempt count is exceeded, give up initialization and switch to normal polling
|
||||||
if (attemptsRef.current >= CONFIG.startup.maxAttempts) {
|
if (attemptsRef.current >= CONFIG.startup.maxAttempts) {
|
||||||
console.log('Maximum startup attempts reached, switching to normal polling');
|
console.log('Maximum startup attempts reached, switching to normal polling');
|
||||||
@@ -151,19 +152,19 @@ export const useServerData = () => {
|
|||||||
// Switch to normal polling mode
|
// Switch to normal polling mode
|
||||||
startNormalPolling();
|
startNormalPolling();
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// On component mount, set appropriate polling based on current state
|
// On component mount, set appropriate polling based on current state
|
||||||
if (isInitialLoading) {
|
if (isInitialLoading) {
|
||||||
// Ensure no other timers are running
|
// Ensure no other timers are running
|
||||||
clearTimer();
|
clearTimer();
|
||||||
|
|
||||||
// Execute initial request immediately
|
// Execute initial request immediately
|
||||||
fetchInitialData();
|
fetchInitialData();
|
||||||
|
|
||||||
// Set polling interval for initialization phase
|
// Set polling interval for initialization phase
|
||||||
intervalRef.current = setInterval(fetchInitialData, CONFIG.startup.pollingInterval);
|
intervalRef.current = setInterval(fetchInitialData, CONFIG.startup.pollingInterval);
|
||||||
console.log(`Started initial polling with interval: ${CONFIG.startup.pollingInterval}ms`);
|
console.log(`Started initial polling with interval: ${CONFIG.startup.pollingInterval}ms`);
|
||||||
@@ -171,7 +172,7 @@ export const useServerData = () => {
|
|||||||
// Initialization completed, start normal polling
|
// Initialization completed, start normal polling
|
||||||
startNormalPolling();
|
startNormalPolling();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cleanup function
|
// Cleanup function
|
||||||
return () => {
|
return () => {
|
||||||
clearTimer();
|
clearTimer();
|
||||||
@@ -182,21 +183,21 @@ export const useServerData = () => {
|
|||||||
const triggerRefresh = () => {
|
const triggerRefresh = () => {
|
||||||
// Clear current timer
|
// Clear current timer
|
||||||
clearTimer();
|
clearTimer();
|
||||||
|
|
||||||
// If in initialization phase, reset initialization state
|
// If in initialization phase, reset initialization state
|
||||||
if (isInitialLoading) {
|
if (isInitialLoading) {
|
||||||
setIsInitialLoading(true);
|
setIsInitialLoading(true);
|
||||||
attemptsRef.current = 0;
|
attemptsRef.current = 0;
|
||||||
setFetchAttempts(0);
|
setFetchAttempts(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Change in refreshKey will trigger useEffect to run again
|
// Change in refreshKey will trigger useEffect to run again
|
||||||
setRefreshKey(prevKey => prevKey + 1);
|
setRefreshKey((prevKey) => prevKey + 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Server related operations
|
// Server related operations
|
||||||
const handleServerAdd = () => {
|
const handleServerAdd = () => {
|
||||||
setRefreshKey(prevKey => prevKey + 1);
|
setRefreshKey((prevKey) => prevKey + 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleServerEdit = async (server: Server) => {
|
const handleServerEdit = async (server: Server) => {
|
||||||
@@ -205,12 +206,12 @@ export const useServerData = () => {
|
|||||||
const token = localStorage.getItem('mcphub_token');
|
const token = localStorage.getItem('mcphub_token');
|
||||||
const response = await fetch(`/api/settings`, {
|
const response = await fetch(`/api/settings`, {
|
||||||
headers: {
|
headers: {
|
||||||
'x-auth-token': token || ''
|
'x-auth-token': token || '',
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const settingsData: ApiResponse<{ mcpServers: Record<string, any> }> = await response.json();
|
const settingsData: ApiResponse<{ mcpServers: Record<string, any> }> = await response.json();
|
||||||
|
|
||||||
if (
|
if (
|
||||||
settingsData &&
|
settingsData &&
|
||||||
settingsData.success &&
|
settingsData.success &&
|
||||||
@@ -243,8 +244,8 @@ export const useServerData = () => {
|
|||||||
const response = await fetch(`/api/servers/${serverName}`, {
|
const response = await fetch(`/api/servers/${serverName}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: {
|
headers: {
|
||||||
'x-auth-token': token || ''
|
'x-auth-token': token || '',
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|
||||||
@@ -253,7 +254,7 @@ export const useServerData = () => {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
setRefreshKey(prevKey => prevKey + 1);
|
setRefreshKey((prevKey) => prevKey + 1);
|
||||||
return true;
|
return true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(t('errors.general') + ': ' + (err instanceof Error ? err.message : String(err)));
|
setError(t('errors.general') + ': ' + (err instanceof Error ? err.message : String(err)));
|
||||||
@@ -268,7 +269,7 @@ export const useServerData = () => {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'x-auth-token': token || ''
|
'x-auth-token': token || '',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ enabled }),
|
body: JSON.stringify({ enabled }),
|
||||||
});
|
});
|
||||||
@@ -282,7 +283,7 @@ export const useServerData = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update the UI immediately to reflect the change
|
// Update the UI immediately to reflect the change
|
||||||
setRefreshKey(prevKey => prevKey + 1);
|
setRefreshKey((prevKey) => prevKey + 1);
|
||||||
return true;
|
return true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error toggling server:', err);
|
console.error('Error toggling server:', err);
|
||||||
@@ -301,6 +302,6 @@ export const useServerData = () => {
|
|||||||
handleServerAdd,
|
handleServerAdd,
|
||||||
handleServerEdit,
|
handleServerEdit,
|
||||||
handleServerRemove,
|
handleServerRemove,
|
||||||
handleServerToggle
|
handleServerToggle,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useState, useCallback, useEffect } from 'react';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { ApiResponse } from '@/types';
|
import { ApiResponse } from '@/types';
|
||||||
import { useToast } from '@/contexts/ToastContext';
|
import { useToast } from '@/contexts/ToastContext';
|
||||||
|
import { getApiUrl } from '../utils/api';
|
||||||
|
|
||||||
// Define types for the settings data
|
// Define types for the settings data
|
||||||
interface RoutingConfig {
|
interface RoutingConfig {
|
||||||
@@ -80,7 +81,7 @@ export const useSettingsData = () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('mcphub_token');
|
const token = localStorage.getItem('mcphub_token');
|
||||||
const response = await fetch('/api/settings', {
|
const response = await fetch(getApiUrl('/settings'), {
|
||||||
headers: {
|
headers: {
|
||||||
'x-auth-token': token || '',
|
'x-auth-token': token || '',
|
||||||
},
|
},
|
||||||
@@ -136,7 +137,7 @@ export const useSettingsData = () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('mcphub_token');
|
const token = localStorage.getItem('mcphub_token');
|
||||||
const response = await fetch('/api/system-config', {
|
const response = await fetch(getApiUrl('/system-config'), {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@@ -183,7 +184,7 @@ export const useSettingsData = () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('mcphub_token');
|
const token = localStorage.getItem('mcphub_token');
|
||||||
const response = await fetch('/api/system-config', {
|
const response = await fetch(getApiUrl('/system-config'), {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@@ -233,7 +234,7 @@ export const useSettingsData = () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('mcphub_token');
|
const token = localStorage.getItem('mcphub_token');
|
||||||
const response = await fetch('/api/system-config', {
|
const response = await fetch(getApiUrl('/system-config'), {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@@ -283,7 +284,7 @@ export const useSettingsData = () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('mcphub_token');
|
const token = localStorage.getItem('mcphub_token');
|
||||||
const response = await fetch('/api/system-config', {
|
const response = await fetch(getApiUrl('/system-config'), {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import { AuthResponse, LoginCredentials, RegisterCredentials, ChangePasswordCredentials } from '../types';
|
import {
|
||||||
|
AuthResponse,
|
||||||
// Base URL for API requests
|
LoginCredentials,
|
||||||
const API_URL = '';
|
RegisterCredentials,
|
||||||
|
ChangePasswordCredentials,
|
||||||
|
} from '../types';
|
||||||
|
import { getApiUrl } from '../utils/api';
|
||||||
|
|
||||||
// Token key in localStorage
|
// Token key in localStorage
|
||||||
const TOKEN_KEY = 'mcphub_token';
|
const TOKEN_KEY = 'mcphub_token';
|
||||||
@@ -24,7 +27,8 @@ export const removeToken = (): void => {
|
|||||||
// Login user
|
// Login user
|
||||||
export const login = async (credentials: LoginCredentials): Promise<AuthResponse> => {
|
export const login = async (credentials: LoginCredentials): Promise<AuthResponse> => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_URL}/auth/login`, {
|
console.log(getApiUrl('/auth/login'));
|
||||||
|
const response = await fetch(getApiUrl('/auth/login'), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@@ -33,11 +37,11 @@ export const login = async (credentials: LoginCredentials): Promise<AuthResponse
|
|||||||
});
|
});
|
||||||
|
|
||||||
const data: AuthResponse = await response.json();
|
const data: AuthResponse = await response.json();
|
||||||
|
|
||||||
if (data.success && data.token) {
|
if (data.success && data.token) {
|
||||||
setToken(data.token);
|
setToken(data.token);
|
||||||
}
|
}
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Login error:', error);
|
console.error('Login error:', error);
|
||||||
@@ -51,7 +55,7 @@ export const login = async (credentials: LoginCredentials): Promise<AuthResponse
|
|||||||
// Register user
|
// Register user
|
||||||
export const register = async (credentials: RegisterCredentials): Promise<AuthResponse> => {
|
export const register = async (credentials: RegisterCredentials): Promise<AuthResponse> => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_URL}/auth/register`, {
|
const response = await fetch(getApiUrl('/auth/register'), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@@ -60,11 +64,11 @@ export const register = async (credentials: RegisterCredentials): Promise<AuthRe
|
|||||||
});
|
});
|
||||||
|
|
||||||
const data: AuthResponse = await response.json();
|
const data: AuthResponse = await response.json();
|
||||||
|
|
||||||
if (data.success && data.token) {
|
if (data.success && data.token) {
|
||||||
setToken(data.token);
|
setToken(data.token);
|
||||||
}
|
}
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Register error:', error);
|
console.error('Register error:', error);
|
||||||
@@ -78,16 +82,16 @@ export const register = async (credentials: RegisterCredentials): Promise<AuthRe
|
|||||||
// Get current user
|
// Get current user
|
||||||
export const getCurrentUser = async (): Promise<AuthResponse> => {
|
export const getCurrentUser = async (): Promise<AuthResponse> => {
|
||||||
const token = getToken();
|
const token = getToken();
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: 'No authentication token',
|
message: 'No authentication token',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_URL}/auth/user`, {
|
const response = await fetch(getApiUrl('/auth/user'), {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
'x-auth-token': token,
|
'x-auth-token': token,
|
||||||
@@ -105,18 +109,20 @@ export const getCurrentUser = async (): Promise<AuthResponse> => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Change password
|
// Change password
|
||||||
export const changePassword = async (credentials: ChangePasswordCredentials): Promise<AuthResponse> => {
|
export const changePassword = async (
|
||||||
|
credentials: ChangePasswordCredentials,
|
||||||
|
): Promise<AuthResponse> => {
|
||||||
const token = getToken();
|
const token = getToken();
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: 'No authentication token',
|
message: 'No authentication token',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_URL}/auth/change-password`, {
|
const response = await fetch(getApiUrl('/auth/change-password'), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@@ -138,4 +144,4 @@ export const changePassword = async (credentials: ChangePasswordCredentials): Pr
|
|||||||
// Logout user
|
// Logout user
|
||||||
export const logout = (): void => {
|
export const logout = (): void => {
|
||||||
removeToken();
|
removeToken();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { getToken } from './authService'; // Import getToken function
|
import { getToken } from './authService'; // Import getToken function
|
||||||
|
import { getApiUrl } from '../utils/api';
|
||||||
|
|
||||||
export interface LogEntry {
|
export interface LogEntry {
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
@@ -18,18 +19,18 @@ export const fetchLogs = async (): Promise<LogEntry[]> => {
|
|||||||
throw new Error('Authentication token not found. Please log in.');
|
throw new Error('Authentication token not found. Please log in.');
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch('/api/logs', {
|
const response = await fetch(getApiUrl('/logs'), {
|
||||||
headers: {
|
headers: {
|
||||||
'x-auth-token': token
|
'x-auth-token': token,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
throw new Error(result.error || 'Failed to fetch logs');
|
throw new Error(result.error || 'Failed to fetch logs');
|
||||||
}
|
}
|
||||||
|
|
||||||
return result.data;
|
return result.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching logs:', error);
|
console.error('Error fetching logs:', error);
|
||||||
@@ -46,15 +47,15 @@ export const clearLogs = async (): Promise<void> => {
|
|||||||
throw new Error('Authentication token not found. Please log in.');
|
throw new Error('Authentication token not found. Please log in.');
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch('/api/logs', {
|
const response = await fetch(getApiUrl('/logs'), {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: {
|
headers: {
|
||||||
'x-auth-token': token
|
'x-auth-token': token,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
throw new Error(result.error || 'Failed to clear logs');
|
throw new Error(result.error || 'Failed to clear logs');
|
||||||
}
|
}
|
||||||
@@ -90,19 +91,19 @@ export const useLogs = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Connect to SSE endpoint with auth token in URL
|
// Connect to SSE endpoint with auth token in URL
|
||||||
eventSource = new EventSource(`/api/logs/stream?token=${token}`);
|
eventSource = new EventSource(getApiUrl(`/logs/stream?token=${token}`));
|
||||||
|
|
||||||
eventSource.onmessage = (event) => {
|
eventSource.onmessage = (event) => {
|
||||||
if (!isMounted) return;
|
if (!isMounted) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(event.data);
|
const data = JSON.parse(event.data);
|
||||||
|
|
||||||
if (data.type === 'initial') {
|
if (data.type === 'initial') {
|
||||||
setLogs(data.logs);
|
setLogs(data.logs);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
} else if (data.type === 'log') {
|
} else if (data.type === 'log') {
|
||||||
setLogs(prevLogs => [...prevLogs, data.log]);
|
setLogs((prevLogs) => [...prevLogs, data.log]);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error parsing SSE message:', err);
|
console.error('Error parsing SSE message:', err);
|
||||||
@@ -111,13 +112,13 @@ export const useLogs = () => {
|
|||||||
|
|
||||||
eventSource.onerror = () => {
|
eventSource.onerror = () => {
|
||||||
if (!isMounted) return;
|
if (!isMounted) return;
|
||||||
|
|
||||||
if (eventSource) {
|
if (eventSource) {
|
||||||
eventSource.close();
|
eventSource.close();
|
||||||
// Attempt to reconnect after a delay
|
// Attempt to reconnect after a delay
|
||||||
setTimeout(connectToLogStream, 5000);
|
setTimeout(connectToLogStream, 5000);
|
||||||
}
|
}
|
||||||
|
|
||||||
setError(new Error('Connection to log stream lost, attempting to reconnect...'));
|
setError(new Error('Connection to log stream lost, attempting to reconnect...'));
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -149,4 +150,4 @@ export const useLogs = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return { logs, loading, error, clearLogs: clearAllLogs };
|
return { logs, loading, error, clearLogs: clearAllLogs };
|
||||||
};
|
};
|
||||||
|
|||||||
27
frontend/src/utils/api.ts
Normal file
27
frontend/src/utils/api.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
/**
|
||||||
|
* API utility functions for constructing URLs with proper base path support
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the API base URL including base path and /api prefix
|
||||||
|
* @returns The complete API base URL
|
||||||
|
*/
|
||||||
|
export const getApiBaseUrl = (): string => {
|
||||||
|
const basePath = import.meta.env.BASE_PATH || '';
|
||||||
|
// Ensure the path starts with / if it's not empty and doesn't already start with /
|
||||||
|
const normalizedBasePath = basePath && !basePath.startsWith('/') ? '/' + basePath : basePath;
|
||||||
|
// Always append /api to the base path for API endpoints
|
||||||
|
return normalizedBasePath + '/api';
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a full API URL with the given endpoint
|
||||||
|
* @param endpoint - The API endpoint (should start with /, e.g., '/auth/login')
|
||||||
|
* @returns The complete API URL
|
||||||
|
*/
|
||||||
|
export const getApiUrl = (endpoint: string): string => {
|
||||||
|
const baseUrl = getApiBaseUrl();
|
||||||
|
// Ensure endpoint starts with /
|
||||||
|
const normalizedEndpoint = endpoint.startsWith('/') ? endpoint : '/' + endpoint;
|
||||||
|
return baseUrl + normalizedEndpoint;
|
||||||
|
};
|
||||||
2
frontend/src/vite-env.d.ts
vendored
2
frontend/src/vite-env.d.ts
vendored
@@ -3,7 +3,7 @@
|
|||||||
interface ImportMeta {
|
interface ImportMeta {
|
||||||
readonly env: {
|
readonly env: {
|
||||||
readonly PACKAGE_VERSION: string;
|
readonly PACKAGE_VERSION: string;
|
||||||
readonly VITE_BASE_PATH?: string; // Add base path environment variable
|
readonly BASE_PATH?: string; // Add base path environment variable
|
||||||
// Add other custom env variables here if needed
|
// Add other custom env variables here if needed
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,9 +8,12 @@ import { readFileSync } from 'fs';
|
|||||||
// Get package.json version
|
// Get package.json version
|
||||||
const packageJson = JSON.parse(readFileSync(path.resolve(__dirname, '../package.json'), 'utf-8'));
|
const packageJson = JSON.parse(readFileSync(path.resolve(__dirname, '../package.json'), 'utf-8'));
|
||||||
|
|
||||||
|
// Get base path from environment variable
|
||||||
|
const basePath = process.env.BASE_PATH || '';
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
base: './', // Use relative paths for assets
|
base: basePath || './', // Use base path or relative paths for assets
|
||||||
plugins: [react(), tailwindcss()],
|
plugins: [react(), tailwindcss()],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
@@ -18,19 +21,20 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
define: {
|
define: {
|
||||||
// Make package version available as global variable
|
// Make package version and base path available as global variables
|
||||||
'import.meta.env.PACKAGE_VERSION': JSON.stringify(packageJson.version),
|
'import.meta.env.PACKAGE_VERSION': JSON.stringify(packageJson.version),
|
||||||
|
'import.meta.env.BASE_PATH': JSON.stringify(basePath),
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
sourcemap: true, // Enable source maps for production build
|
sourcemap: true, // Enable source maps for production build
|
||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
[`${basePath}/api`]: {
|
||||||
target: 'http://localhost:3000',
|
target: 'http://localhost:3000',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
},
|
},
|
||||||
'/auth': {
|
[`${basePath}/auth`]: {
|
||||||
target: 'http://localhost:3000',
|
target: 'http://localhost:3000',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
},
|
},
|
||||||
|
|||||||
72
nginx.conf.example
Normal file
72
nginx.conf.example
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
# Nginx configuration example for MCPHub with subpath routing
|
||||||
|
# This example shows how to deploy MCPHub under a subpath like /mcphub
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name your-domain.com;
|
||||||
|
|
||||||
|
# MCPHub under /mcphub subpath
|
||||||
|
location /mcphub/ {
|
||||||
|
# Remove the subpath prefix before forwarding to MCPHub
|
||||||
|
rewrite ^/mcphub/(.*)$ /$1 break;
|
||||||
|
|
||||||
|
proxy_pass http://mcphub:3000/;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection 'upgrade';
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_cache_bypass $http_upgrade;
|
||||||
|
|
||||||
|
# Important: Disable buffering for SSE connections
|
||||||
|
proxy_buffering off;
|
||||||
|
proxy_cache off;
|
||||||
|
|
||||||
|
# Support for Server-Sent Events (SSE)
|
||||||
|
proxy_read_timeout 24h;
|
||||||
|
proxy_send_timeout 24h;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Alternative configuration if you want to keep the subpath in the backend
|
||||||
|
# In this case, set BASE_PATH=/mcphub
|
||||||
|
# location /mcphub/ {
|
||||||
|
# proxy_pass http://mcphub:3000/mcphub/;
|
||||||
|
# proxy_http_version 1.1;
|
||||||
|
# proxy_set_header Upgrade $http_upgrade;
|
||||||
|
# proxy_set_header Connection 'upgrade';
|
||||||
|
# proxy_set_header Host $host;
|
||||||
|
# proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
# proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
# proxy_cache_bypass $http_upgrade;
|
||||||
|
#
|
||||||
|
# # Important: Disable buffering for SSE connections
|
||||||
|
# proxy_buffering off;
|
||||||
|
# proxy_cache off;
|
||||||
|
#
|
||||||
|
# # Support for Server-Sent Events (SSE)
|
||||||
|
# proxy_read_timeout 24h;
|
||||||
|
# proxy_send_timeout 24h;
|
||||||
|
# }
|
||||||
|
}
|
||||||
|
|
||||||
|
# Docker Compose example with subpath
|
||||||
|
# version: '3.8'
|
||||||
|
# services:
|
||||||
|
# mcphub:
|
||||||
|
# image: samanhappy/mcphub
|
||||||
|
# environment:
|
||||||
|
# - BASE_PATH=/mcphub
|
||||||
|
# volumes:
|
||||||
|
# - ./mcp_settings.json:/app/mcp_settings.json
|
||||||
|
#
|
||||||
|
# nginx:
|
||||||
|
# image: nginx:alpine
|
||||||
|
# ports:
|
||||||
|
# - "80:80"
|
||||||
|
# volumes:
|
||||||
|
# - ./nginx.conf:/etc/nginx/conf.d/default.conf
|
||||||
|
# depends_on:
|
||||||
|
# - mcphub
|
||||||
@@ -10,6 +10,7 @@ const defaultConfig = {
|
|||||||
port: process.env.PORT || 3000,
|
port: process.env.PORT || 3000,
|
||||||
initTimeout: process.env.INIT_TIMEOUT || 300000,
|
initTimeout: process.env.INIT_TIMEOUT || 300000,
|
||||||
timeout: process.env.REQUEST_TIMEOUT || 60000,
|
timeout: process.env.REQUEST_TIMEOUT || 60000,
|
||||||
|
basePath: process.env.BASE_PATH || '',
|
||||||
mcpHubName: 'mcphub',
|
mcpHubName: 'mcphub',
|
||||||
mcpHubVersion: getPackageVersion(),
|
mcpHubVersion: getPackageVersion(),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { dirname } from 'path';
|
|||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import { auth } from './auth.js';
|
import { auth } from './auth.js';
|
||||||
import { initializeDefaultUser } from '../models/User.js';
|
import { initializeDefaultUser } from '../models/User.js';
|
||||||
|
import config from '../config/index.js';
|
||||||
|
|
||||||
// Create __dirname equivalent for ES modules
|
// Create __dirname equivalent for ES modules
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
@@ -17,13 +18,13 @@ const findFrontendPath = (): string => {
|
|||||||
if (fs.existsSync(devPath)) {
|
if (fs.existsSync(devPath)) {
|
||||||
return path.join(dirname(__dirname), 'frontend', 'dist');
|
return path.join(dirname(__dirname), 'frontend', 'dist');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try npm/npx installed path (remove /dist directory)
|
// Try npm/npx installed path (remove /dist directory)
|
||||||
const npmPath = path.join(dirname(dirname(__dirname)), 'frontend', 'dist', 'index.html');
|
const npmPath = path.join(dirname(dirname(__dirname)), 'frontend', 'dist', 'index.html');
|
||||||
if (fs.existsSync(npmPath)) {
|
if (fs.existsSync(npmPath)) {
|
||||||
return path.join(dirname(dirname(__dirname)), 'frontend', 'dist');
|
return path.join(dirname(dirname(__dirname)), 'frontend', 'dist');
|
||||||
}
|
}
|
||||||
|
|
||||||
// If none of the above paths exist, return the most reasonable default path and log a warning
|
// If none of the above paths exist, return the most reasonable default path and log a warning
|
||||||
console.warn('Warning: Could not locate frontend files. Using default path.');
|
console.warn('Warning: Could not locate frontend files. Using default path.');
|
||||||
return path.join(dirname(__dirname), 'frontend', 'dist');
|
return path.join(dirname(__dirname), 'frontend', 'dist');
|
||||||
@@ -32,10 +33,10 @@ const findFrontendPath = (): string => {
|
|||||||
const frontendPath = findFrontendPath();
|
const frontendPath = findFrontendPath();
|
||||||
|
|
||||||
export const errorHandler = (
|
export const errorHandler = (
|
||||||
err: Error,
|
err: Error,
|
||||||
_req: Request,
|
_req: Request,
|
||||||
res: Response,
|
res: Response,
|
||||||
_next: NextFunction
|
_next: NextFunction,
|
||||||
): void => {
|
): void => {
|
||||||
console.error('Unhandled error:', err);
|
console.error('Unhandled error:', err);
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
@@ -46,10 +47,17 @@ export const errorHandler = (
|
|||||||
|
|
||||||
export const initMiddlewares = (app: express.Application): void => {
|
export const initMiddlewares = (app: express.Application): void => {
|
||||||
// Serve static files from the dynamically determined frontend path
|
// Serve static files from the dynamically determined frontend path
|
||||||
app.use(express.static(frontendPath));
|
// Note: Static files will be handled by the server directly, not here
|
||||||
|
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
if (req.path !== '/sse' && req.path !== '/messages') {
|
const basePath = config.basePath;
|
||||||
|
// Only apply JSON parsing for API and auth routes, not for SSE or message endpoints
|
||||||
|
if (
|
||||||
|
req.path !== `${basePath}/sse` &&
|
||||||
|
req.path !== `${basePath}/messages` &&
|
||||||
|
!req.path.startsWith(`${basePath}/sse/`) &&
|
||||||
|
!req.path.startsWith(`${basePath}/mcp/`)
|
||||||
|
) {
|
||||||
express.json()(req, res, next);
|
express.json()(req, res, next);
|
||||||
} else {
|
} else {
|
||||||
next();
|
next();
|
||||||
@@ -57,16 +65,18 @@ export const initMiddlewares = (app: express.Application): void => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Initialize default admin user if no users exist
|
// Initialize default admin user if no users exist
|
||||||
initializeDefaultUser().catch(err => {
|
initializeDefaultUser().catch((err) => {
|
||||||
console.error('Error initializing default user:', err);
|
console.error('Error initializing default user:', err);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Protect all API routes with authentication middleware
|
// Protect API routes with authentication middleware, but exclude auth endpoints
|
||||||
app.use('/api', auth);
|
app.use(`${config.basePath}/api`, (req, res, next) => {
|
||||||
|
// Skip authentication for login and register endpoints
|
||||||
app.get('/', (_req: Request, res: Response) => {
|
if (req.path === '/auth/login' || req.path === '/auth/register') {
|
||||||
// Serve the frontend application
|
next();
|
||||||
res.sendFile(path.join(frontendPath, 'index.html'));
|
} else {
|
||||||
|
auth(req, res, next);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.use(errorHandler);
|
app.use(errorHandler);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import express from 'express';
|
import express from 'express';
|
||||||
import { check } from 'express-validator';
|
import { check } from 'express-validator';
|
||||||
|
import config from '../config/index.js';
|
||||||
import {
|
import {
|
||||||
getAllServers,
|
getAllServers,
|
||||||
getAllSettings,
|
getAllSettings,
|
||||||
@@ -7,7 +8,7 @@ import {
|
|||||||
updateServer,
|
updateServer,
|
||||||
deleteServer,
|
deleteServer,
|
||||||
toggleServer,
|
toggleServer,
|
||||||
updateSystemConfig
|
updateSystemConfig,
|
||||||
} from '../controllers/serverController.js';
|
} from '../controllers/serverController.js';
|
||||||
import {
|
import {
|
||||||
getGroups,
|
getGroups,
|
||||||
@@ -18,7 +19,7 @@ import {
|
|||||||
addServerToExistingGroup,
|
addServerToExistingGroup,
|
||||||
removeServerFromExistingGroup,
|
removeServerFromExistingGroup,
|
||||||
getGroupServers,
|
getGroupServers,
|
||||||
updateGroupServersBatch
|
updateGroupServersBatch,
|
||||||
} from '../controllers/groupController.js';
|
} from '../controllers/groupController.js';
|
||||||
import {
|
import {
|
||||||
getAllMarketServers,
|
getAllMarketServers,
|
||||||
@@ -27,19 +28,10 @@ import {
|
|||||||
getAllMarketTags,
|
getAllMarketTags,
|
||||||
searchMarketServersByQuery,
|
searchMarketServersByQuery,
|
||||||
getMarketServersByCategory,
|
getMarketServersByCategory,
|
||||||
getMarketServersByTag
|
getMarketServersByTag,
|
||||||
} from '../controllers/marketController.js';
|
} from '../controllers/marketController.js';
|
||||||
import {
|
import { login, register, getCurrentUser, changePassword } from '../controllers/authController.js';
|
||||||
login,
|
import { getAllLogs, clearLogs, streamLogs } from '../controllers/logController.js';
|
||||||
register,
|
|
||||||
getCurrentUser,
|
|
||||||
changePassword
|
|
||||||
} from '../controllers/authController.js';
|
|
||||||
import {
|
|
||||||
getAllLogs,
|
|
||||||
clearLogs,
|
|
||||||
streamLogs
|
|
||||||
} from '../controllers/logController.js';
|
|
||||||
import { auth } from '../middlewares/auth.js';
|
import { auth } from '../middlewares/auth.js';
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
@@ -53,7 +45,7 @@ export const initRoutes = (app: express.Application): void => {
|
|||||||
router.delete('/servers/:name', deleteServer);
|
router.delete('/servers/:name', deleteServer);
|
||||||
router.post('/servers/:name/toggle', toggleServer);
|
router.post('/servers/:name/toggle', toggleServer);
|
||||||
router.put('/system-config', updateSystemConfig);
|
router.put('/system-config', updateSystemConfig);
|
||||||
|
|
||||||
// Group management routes
|
// Group management routes
|
||||||
router.get('/groups', getGroups);
|
router.get('/groups', getGroups);
|
||||||
router.get('/groups/:id', getGroup);
|
router.get('/groups/:id', getGroup);
|
||||||
@@ -65,7 +57,7 @@ export const initRoutes = (app: express.Application): void => {
|
|||||||
router.get('/groups/:id/servers', getGroupServers);
|
router.get('/groups/:id/servers', getGroupServers);
|
||||||
// New route for batch updating servers in a group
|
// New route for batch updating servers in a group
|
||||||
router.put('/groups/:id/servers/batch', updateGroupServersBatch);
|
router.put('/groups/:id/servers/batch', updateGroupServersBatch);
|
||||||
|
|
||||||
// Market routes
|
// Market routes
|
||||||
router.get('/market/servers', getAllMarketServers);
|
router.get('/market/servers', getAllMarketServers);
|
||||||
router.get('/market/servers/search', searchMarketServersByQuery);
|
router.get('/market/servers/search', searchMarketServersByQuery);
|
||||||
@@ -74,33 +66,45 @@ export const initRoutes = (app: express.Application): void => {
|
|||||||
router.get('/market/categories/:category', getMarketServersByCategory);
|
router.get('/market/categories/:category', getMarketServersByCategory);
|
||||||
router.get('/market/tags', getAllMarketTags);
|
router.get('/market/tags', getAllMarketTags);
|
||||||
router.get('/market/tags/:tag', getMarketServersByTag);
|
router.get('/market/tags/:tag', getMarketServersByTag);
|
||||||
|
|
||||||
// Log routes
|
// Log routes
|
||||||
router.get('/logs', getAllLogs);
|
router.get('/logs', getAllLogs);
|
||||||
router.delete('/logs', clearLogs);
|
router.delete('/logs', clearLogs);
|
||||||
router.get('/logs/stream', streamLogs);
|
router.get('/logs/stream', streamLogs);
|
||||||
|
|
||||||
// Auth routes (these will NOT be protected by auth middleware)
|
|
||||||
app.post('/auth/login', [
|
|
||||||
check('username', 'Username is required').not().isEmpty(),
|
|
||||||
check('password', 'Password is required').not().isEmpty(),
|
|
||||||
], login);
|
|
||||||
|
|
||||||
app.post('/auth/register', [
|
|
||||||
check('username', 'Username is required').not().isEmpty(),
|
|
||||||
check('password', 'Password must be at least 6 characters').isLength({ min: 6 }),
|
|
||||||
], register);
|
|
||||||
|
|
||||||
app.get('/auth/user', auth, getCurrentUser);
|
|
||||||
|
|
||||||
// Add change password route
|
|
||||||
app.post('/auth/change-password', [
|
|
||||||
auth,
|
|
||||||
check('currentPassword', 'Current password is required').not().isEmpty(),
|
|
||||||
check('newPassword', 'New password must be at least 6 characters').isLength({ min: 6 }),
|
|
||||||
], changePassword);
|
|
||||||
|
|
||||||
app.use('/api', router);
|
// Auth routes - move to router instead of app directly
|
||||||
|
router.post(
|
||||||
|
'/auth/login',
|
||||||
|
[
|
||||||
|
check('username', 'Username is required').not().isEmpty(),
|
||||||
|
check('password', 'Password is required').not().isEmpty(),
|
||||||
|
],
|
||||||
|
login,
|
||||||
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/auth/register',
|
||||||
|
[
|
||||||
|
check('username', 'Username is required').not().isEmpty(),
|
||||||
|
check('password', 'Password must be at least 6 characters').isLength({ min: 6 }),
|
||||||
|
],
|
||||||
|
register,
|
||||||
|
);
|
||||||
|
|
||||||
|
router.get('/auth/user', auth, getCurrentUser);
|
||||||
|
|
||||||
|
// Add change password route
|
||||||
|
router.post(
|
||||||
|
'/auth/change-password',
|
||||||
|
[
|
||||||
|
auth,
|
||||||
|
check('currentPassword', 'Current password is required').not().isEmpty(),
|
||||||
|
check('newPassword', 'New password must be at least 6 characters').isLength({ min: 6 }),
|
||||||
|
],
|
||||||
|
changePassword,
|
||||||
|
);
|
||||||
|
|
||||||
|
app.use(`${config.basePath}/api`, router);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
@@ -22,10 +22,12 @@ export class AppServer {
|
|||||||
private app: express.Application;
|
private app: express.Application;
|
||||||
private port: number | string;
|
private port: number | string;
|
||||||
private frontendPath: string | null = null;
|
private frontendPath: string | null = null;
|
||||||
|
private basePath: string;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.app = express();
|
this.app = express();
|
||||||
this.port = config.port;
|
this.port = config.port;
|
||||||
|
this.basePath = config.basePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
async initialize(): Promise<void> {
|
async initialize(): Promise<void> {
|
||||||
@@ -40,11 +42,11 @@ export class AppServer {
|
|||||||
initUpstreamServers()
|
initUpstreamServers()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
console.log('MCP server initialized successfully');
|
console.log('MCP server initialized successfully');
|
||||||
this.app.get('/sse/:group?', (req, res) => handleSseConnection(req, res));
|
this.app.get(`${this.basePath}/sse/:group?`, (req, res) => handleSseConnection(req, res));
|
||||||
this.app.post('/messages', handleSseMessage);
|
this.app.post(`${this.basePath}/messages`, handleSseMessage);
|
||||||
this.app.post('/mcp/:group?', handleMcpPostRequest);
|
this.app.post(`${this.basePath}/mcp/:group?`, handleMcpPostRequest);
|
||||||
this.app.get('/mcp/:group?', handleMcpOtherRequest);
|
this.app.get(`${this.basePath}/mcp/:group?`, handleMcpOtherRequest);
|
||||||
this.app.delete('/mcp/:group?', handleMcpOtherRequest);
|
this.app.delete(`${this.basePath}/mcp/:group?`, handleMcpOtherRequest);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Error initializing MCP server:', error);
|
console.error('Error initializing MCP server:', error);
|
||||||
@@ -66,17 +68,26 @@ export class AppServer {
|
|||||||
|
|
||||||
if (this.frontendPath) {
|
if (this.frontendPath) {
|
||||||
console.log(`Serving frontend from: ${this.frontendPath}`);
|
console.log(`Serving frontend from: ${this.frontendPath}`);
|
||||||
this.app.use(express.static(this.frontendPath));
|
// Serve static files with base path
|
||||||
|
this.app.use(this.basePath, express.static(this.frontendPath));
|
||||||
|
|
||||||
// Add the wildcard route for SPA
|
// Add the wildcard route for SPA with base path
|
||||||
if (fs.existsSync(path.join(this.frontendPath, 'index.html'))) {
|
if (fs.existsSync(path.join(this.frontendPath, 'index.html'))) {
|
||||||
this.app.get('*', (_req, res) => {
|
this.app.get(`${this.basePath}/*`, (_req, res) => {
|
||||||
res.sendFile(path.join(this.frontendPath!, 'index.html'));
|
res.sendFile(path.join(this.frontendPath!, 'index.html'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Also handle root redirect if base path is set
|
||||||
|
if (this.basePath) {
|
||||||
|
this.app.get('/', (_req, res) => {
|
||||||
|
res.redirect(this.basePath);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.warn('Frontend dist directory not found. Server will run without frontend.');
|
console.warn('Frontend dist directory not found. Server will run without frontend.');
|
||||||
this.app.get('/', (_req, res) => {
|
const rootPath = this.basePath || '/';
|
||||||
|
this.app.get(rootPath, (_req, res) => {
|
||||||
res
|
res
|
||||||
.status(404)
|
.status(404)
|
||||||
.send('Frontend not found. MCPHub API is running, but the UI is not available.');
|
.send('Frontend not found. MCPHub API is running, but the UI is not available.');
|
||||||
|
|||||||
Reference in New Issue
Block a user