mirror of
https://github.com/samanhappy/mcphub.git
synced 2025-12-24 02:39:19 -05:00
Add Chinese localization support and i18n middleware (#253)
This commit is contained in:
@@ -21,6 +21,9 @@ ENV REQUEST_TIMEOUT=$REQUEST_TIMEOUT
|
|||||||
ARG BASE_PATH=""
|
ARG BASE_PATH=""
|
||||||
ENV BASE_PATH=$BASE_PATH
|
ENV BASE_PATH=$BASE_PATH
|
||||||
|
|
||||||
|
ARG READONLY=false
|
||||||
|
ENV READONLY=$READONLY
|
||||||
|
|
||||||
ENV PNPM_HOME=/usr/local/share/pnpm
|
ENV PNPM_HOME=/usr/local/share/pnpm
|
||||||
ENV PATH=$PNPM_HOME:$PATH
|
ENV PATH=$PNPM_HOME:$PATH
|
||||||
RUN mkdir -p $PNPM_HOME && \
|
RUN mkdir -p $PNPM_HOME && \
|
||||||
|
|||||||
@@ -50,9 +50,8 @@ const AddGroupForm = ({ onAdd, onCancel }: AddGroupFormProps) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const result = await createGroup(formData.name, formData.description, formData.servers)
|
const result = await createGroup(formData.name, formData.description, formData.servers)
|
||||||
|
if (!result || !result.success) {
|
||||||
if (!result) {
|
setError(result?.message || t('groups.createError'))
|
||||||
setError(t('groups.createError'))
|
|
||||||
setIsSubmitting(false)
|
setIsSubmitting(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +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/runtime'
|
import { apiPost } from '../utils/fetchInterceptor'
|
||||||
import { detectVariables } from '../utils/variableDetection'
|
import { detectVariables } from '../utils/variableDetection'
|
||||||
|
|
||||||
interface AddServerFormProps {
|
interface AddServerFormProps {
|
||||||
@@ -34,26 +34,12 @@ const AddServerForm = ({ onAdd }: AddServerFormProps) => {
|
|||||||
const submitServer = async (payload: any) => {
|
const submitServer = async (payload: any) => {
|
||||||
try {
|
try {
|
||||||
setError(null)
|
setError(null)
|
||||||
const token = localStorage.getItem('mcphub_token');
|
const result = await apiPost('/servers', payload)
|
||||||
const response = await fetch(getApiUrl('/servers'), {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'x-auth-token': token || ''
|
|
||||||
},
|
|
||||||
body: JSON.stringify(payload),
|
|
||||||
})
|
|
||||||
|
|
||||||
const result = await response.json()
|
if (!result.success) {
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
// Use specific error message from the response if available
|
// Use specific error message from the response if available
|
||||||
if (result && result.message) {
|
if (result && result.message) {
|
||||||
setError(result.message)
|
setError(result.message)
|
||||||
} else if (response.status === 400) {
|
|
||||||
setError(t('server.invalidData'))
|
|
||||||
} else if (response.status === 409) {
|
|
||||||
setError(t('server.alreadyExists', { serverName: payload.name }))
|
|
||||||
} else {
|
} else {
|
||||||
setError(t('server.addError'))
|
setError(t('server.addError'))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { apiPost, apiGet, apiPut, fetchWithInterceptors } from '@/utils/fetchInterceptor';
|
||||||
import { getApiUrl } from '@/utils/runtime';
|
import { getApiUrl } from '@/utils/runtime';
|
||||||
import ConfirmDialog from '@/components/ui/ConfirmDialog';
|
import ConfirmDialog from '@/components/ui/ConfirmDialog';
|
||||||
|
|
||||||
@@ -81,12 +82,8 @@ const DxtUploadForm: React.FC<DxtUploadFormProps> = ({ onSuccess, onCancel }) =>
|
|||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('dxtFile', selectedFile);
|
formData.append('dxtFile', selectedFile);
|
||||||
|
|
||||||
const token = localStorage.getItem('mcphub_token');
|
const response = await fetchWithInterceptors(getApiUrl('/dxt/upload'), {
|
||||||
const response = await fetch(getApiUrl('/dxt/upload'), {
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
|
||||||
'x-auth-token': token || '',
|
|
||||||
},
|
|
||||||
body: formData,
|
body: formData,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -119,19 +116,11 @@ const DxtUploadForm: React.FC<DxtUploadFormProps> = ({ onSuccess, onCancel }) =>
|
|||||||
// Convert DXT manifest to MCPHub stdio server configuration
|
// Convert DXT manifest to MCPHub stdio server configuration
|
||||||
const serverConfig = convertDxtToMcpConfig(manifestData, extractDir, serverName);
|
const serverConfig = convertDxtToMcpConfig(manifestData, extractDir, serverName);
|
||||||
|
|
||||||
const token = localStorage.getItem('mcphub_token');
|
|
||||||
|
|
||||||
// First, check if server exists
|
// First, check if server exists
|
||||||
if (!forceOverride) {
|
if (!forceOverride) {
|
||||||
const checkResponse = await fetch(getApiUrl('/servers'), {
|
const checkResult = await apiGet('/servers');
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'x-auth-token': token || '',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (checkResponse.ok) {
|
if (checkResult.success) {
|
||||||
const checkResult = await checkResponse.json();
|
|
||||||
const existingServer = checkResult.data?.find((server: any) => server.name === serverName);
|
const existingServer = checkResult.data?.find((server: any) => server.name === serverName);
|
||||||
|
|
||||||
if (existingServer) {
|
if (existingServer) {
|
||||||
@@ -145,25 +134,17 @@ const DxtUploadForm: React.FC<DxtUploadFormProps> = ({ onSuccess, onCancel }) =>
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Install or override the server
|
// Install or override the server
|
||||||
const method = forceOverride ? 'PUT' : 'POST';
|
let result;
|
||||||
const url = forceOverride ? getApiUrl(`/servers/${encodeURIComponent(serverName)}`) : getApiUrl('/servers');
|
if (forceOverride) {
|
||||||
|
result = await apiPut(`/servers/${encodeURIComponent(serverName)}`, {
|
||||||
const response = await fetch(url, {
|
|
||||||
method,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'x-auth-token': token || '',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
name: serverName,
|
name: serverName,
|
||||||
config: serverConfig,
|
config: serverConfig,
|
||||||
}),
|
});
|
||||||
});
|
} else {
|
||||||
|
result = await apiPost('/servers', {
|
||||||
const result = await response.json();
|
name: serverName,
|
||||||
|
config: serverConfig,
|
||||||
if (!response.ok) {
|
});
|
||||||
throw new Error(result.message || `HTTP error! Status: ${response.status}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
|
|||||||
@@ -56,8 +56,8 @@ const EditGroupForm = ({ group, onEdit, onCancel }: EditGroupFormProps) => {
|
|||||||
servers: formData.servers
|
servers: formData.servers
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!result) {
|
if (!result || !result.success) {
|
||||||
setError(t('groups.updateError'))
|
setError(result?.message || t('groups.updateError'))
|
||||||
setIsSubmitting(false)
|
setIsSubmitting(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Server } from '@/types'
|
import { Server } from '@/types'
|
||||||
import { getApiUrl } from '../utils/runtime'
|
import { apiPut } from '../utils/fetchInterceptor'
|
||||||
import ServerForm from './ServerForm'
|
import ServerForm from './ServerForm'
|
||||||
|
|
||||||
interface EditServerFormProps {
|
interface EditServerFormProps {
|
||||||
@@ -17,26 +17,12 @@ const EditServerForm = ({ server, onEdit, onCancel }: EditServerFormProps) => {
|
|||||||
const handleSubmit = async (payload: any) => {
|
const handleSubmit = async (payload: any) => {
|
||||||
try {
|
try {
|
||||||
setError(null)
|
setError(null)
|
||||||
const token = localStorage.getItem('mcphub_token');
|
const result = await apiPut(`/servers/${server.name}`, payload)
|
||||||
const response = await fetch(getApiUrl(`/servers/${server.name}`), {
|
|
||||||
method: 'PUT',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'x-auth-token': token || ''
|
|
||||||
},
|
|
||||||
body: JSON.stringify(payload),
|
|
||||||
})
|
|
||||||
|
|
||||||
const result = await response.json()
|
if (!result.success) {
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
// Use specific error message from the response if available
|
// Use specific error message from the response if available
|
||||||
if (result && result.message) {
|
if (result && result.message) {
|
||||||
setError(result.message)
|
setError(result.message)
|
||||||
} else if (response.status === 404) {
|
|
||||||
setError(t('server.notFound', { serverName: server.name }))
|
|
||||||
} else if (response.status === 400) {
|
|
||||||
setError(t('server.invalidData'))
|
|
||||||
} else {
|
} else {
|
||||||
setError(t('server.updateError', { serverName: server.name }))
|
setError(t('server.updateError', { serverName: server.name }))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +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, IGroupServerConfig } from '@/types';
|
import { Group, ApiResponse, IGroupServerConfig } from '@/types';
|
||||||
import { getApiUrl } from '../utils/runtime';
|
import { apiGet, apiPost, apiPut, apiDelete } from '../utils/fetchInterceptor';
|
||||||
|
|
||||||
export const useGroupData = () => {
|
export const useGroupData = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -13,18 +13,7 @@ export const useGroupData = () => {
|
|||||||
const fetchGroups = useCallback(async () => {
|
const fetchGroups = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const token = localStorage.getItem('mcphub_token');
|
const data: ApiResponse<Group[]> = await apiGet('/groups');
|
||||||
const response = await fetch(getApiUrl('/groups'), {
|
|
||||||
headers: {
|
|
||||||
'x-auth-token': token || '',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Status: ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
||||||
@@ -55,25 +44,16 @@ export const useGroupData = () => {
|
|||||||
servers: string[] | IGroupServerConfig[] = [],
|
servers: string[] | IGroupServerConfig[] = [],
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('mcphub_token');
|
const result: ApiResponse<Group> = await apiPost('/groups', { name, description, servers });
|
||||||
const response = await fetch(getApiUrl('/groups'), {
|
console.log('Group created successfully:', result);
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'x-auth-token': token || '',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ name, description, servers }),
|
|
||||||
});
|
|
||||||
|
|
||||||
const result: ApiResponse<Group> = await response.json();
|
if (!result || !result.success) {
|
||||||
|
setError(result?.message || t('groups.createError'));
|
||||||
if (!response.ok) {
|
return result;
|
||||||
setError(result.message || t('groups.createError'));
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
triggerRefresh();
|
triggerRefresh();
|
||||||
return result.data || null;
|
return result || null;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : 'Failed to create group');
|
setError(err instanceof Error ? err.message : 'Failed to create group');
|
||||||
return null;
|
return null;
|
||||||
@@ -86,25 +66,14 @@ export const useGroupData = () => {
|
|||||||
data: { name?: string; description?: string; servers?: string[] | IGroupServerConfig[] },
|
data: { name?: string; description?: string; servers?: string[] | IGroupServerConfig[] },
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('mcphub_token');
|
const result: ApiResponse<Group> = await apiPut(`/groups/${id}`, data);
|
||||||
const response = await fetch(getApiUrl(`/groups/${id}`), {
|
if (!result || !result.success) {
|
||||||
method: 'PUT',
|
setError(result?.message || t('groups.updateError'));
|
||||||
headers: {
|
return result;
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'x-auth-token': token || '',
|
|
||||||
},
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
});
|
|
||||||
|
|
||||||
const result: ApiResponse<Group> = await response.json();
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
setError(result.message || t('groups.updateError'));
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
triggerRefresh();
|
triggerRefresh();
|
||||||
return result.data || null;
|
return result || null;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : 'Failed to update group');
|
setError(err instanceof Error ? err.message : 'Failed to update group');
|
||||||
return null;
|
return null;
|
||||||
@@ -114,20 +83,12 @@ export const useGroupData = () => {
|
|||||||
// Update servers in a group (for batch updates)
|
// Update servers in a group (for batch updates)
|
||||||
const updateGroupServers = async (groupId: string, servers: string[] | IGroupServerConfig[]) => {
|
const updateGroupServers = async (groupId: string, servers: string[] | IGroupServerConfig[]) => {
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('mcphub_token');
|
const result: ApiResponse<Group> = await apiPut(`/groups/${groupId}/servers/batch`, {
|
||||||
const response = await fetch(getApiUrl(`/groups/${groupId}/servers/batch`), {
|
servers,
|
||||||
method: 'PUT',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'x-auth-token': token || '',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ servers }),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const result: ApiResponse<Group> = await response.json();
|
if (!result || !result.success) {
|
||||||
|
setError(result?.message || t('groups.updateError'));
|
||||||
if (!response.ok) {
|
|
||||||
setError(result.message || t('groups.updateError'));
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,46 +103,29 @@ export const useGroupData = () => {
|
|||||||
// Delete a group
|
// Delete a group
|
||||||
const deleteGroup = async (id: string) => {
|
const deleteGroup = async (id: string) => {
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('mcphub_token');
|
const result = await apiDelete(`/groups/${id}`);
|
||||||
const response = await fetch(getApiUrl(`/groups/${id}`), {
|
if (!result || !result.success) {
|
||||||
method: 'DELETE',
|
setError(result?.message || t('groups.deleteError'));
|
||||||
headers: {
|
return result;
|
||||||
'x-auth-token': token || '',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await response.json();
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
setError(result.message || t('groups.deleteError'));
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
triggerRefresh();
|
triggerRefresh();
|
||||||
return true;
|
return result;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : 'Failed to delete group');
|
setError(err instanceof Error ? err.message : 'Failed to delete group');
|
||||||
return false;
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add server to a group
|
// Add server to a group
|
||||||
const addServerToGroup = async (groupId: string, serverName: string) => {
|
const addServerToGroup = async (groupId: string, serverName: string) => {
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('mcphub_token');
|
const result: ApiResponse<Group> = await apiPost(`/groups/${groupId}/servers`, {
|
||||||
const response = await fetch(getApiUrl(`/groups/${groupId}/servers`), {
|
serverName,
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'x-auth-token': token || '',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ serverName }),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const result: ApiResponse<Group> = await response.json();
|
if (!result || !result.success) {
|
||||||
|
setError(result?.message || t('groups.serverAddError'));
|
||||||
if (!response.ok) {
|
|
||||||
setError(result.message || t('groups.serverAddError'));
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,18 +140,12 @@ export const useGroupData = () => {
|
|||||||
// Remove server from group
|
// Remove server from group
|
||||||
const removeServerFromGroup = async (groupId: string, serverName: string) => {
|
const removeServerFromGroup = async (groupId: string, serverName: string) => {
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('mcphub_token');
|
const result: ApiResponse<Group> = await apiDelete(
|
||||||
const response = await fetch(getApiUrl(`/groups/${groupId}/servers/${serverName}`), {
|
`/groups/${groupId}/servers/${serverName}`,
|
||||||
method: 'DELETE',
|
);
|
||||||
headers: {
|
|
||||||
'x-auth-token': token || '',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const result: ApiResponse<Group> = await response.json();
|
if (!result || !result.success) {
|
||||||
|
setError(result?.message || t('groups.serverRemoveError'));
|
||||||
if (!response.ok) {
|
|
||||||
setError(result.message || t('groups.serverRemoveError'));
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +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, ServerConfig } from '@/types';
|
import { MarketServer, ApiResponse, ServerConfig } from '@/types';
|
||||||
import { getApiUrl } from '../utils/runtime';
|
import { apiGet, apiPost } from '../utils/fetchInterceptor';
|
||||||
|
|
||||||
export const useMarketData = () => {
|
export const useMarketData = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -26,18 +26,7 @@ export const useMarketData = () => {
|
|||||||
const fetchMarketServers = useCallback(async () => {
|
const fetchMarketServers = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const token = localStorage.getItem('mcphub_token');
|
const data: ApiResponse<MarketServer[]> = await apiGet('/market/servers');
|
||||||
const response = await fetch(getApiUrl('/market/servers'), {
|
|
||||||
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)) {
|
if (data && data.success && Array.isArray(data.data)) {
|
||||||
setAllServers(data.data);
|
setAllServers(data.data);
|
||||||
@@ -87,18 +76,7 @@ export const useMarketData = () => {
|
|||||||
// Fetch all categories
|
// Fetch all categories
|
||||||
const fetchCategories = useCallback(async () => {
|
const fetchCategories = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('mcphub_token');
|
const data: ApiResponse<string[]> = await apiGet('/market/categories');
|
||||||
const response = await fetch(getApiUrl('/market/categories'), {
|
|
||||||
headers: {
|
|
||||||
'x-auth-token': token || '',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Status: ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
||||||
@@ -113,18 +91,7 @@ export const useMarketData = () => {
|
|||||||
// Fetch all tags
|
// Fetch all tags
|
||||||
const fetchTags = useCallback(async () => {
|
const fetchTags = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('mcphub_token');
|
const data: ApiResponse<string[]> = await apiGet('/market/tags');
|
||||||
const response = await fetch(getApiUrl('/market/tags'), {
|
|
||||||
headers: {
|
|
||||||
'x-auth-token': token || '',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Status: ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
||||||
@@ -141,18 +108,7 @@ export const useMarketData = () => {
|
|||||||
async (name: string) => {
|
async (name: string) => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const token = localStorage.getItem('mcphub_token');
|
const data: ApiResponse<MarketServer> = await apiGet(`/market/servers/${name}`);
|
||||||
const response = await fetch(getApiUrl(`/market/servers/${name}`), {
|
|
||||||
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 && data.data) {
|
if (data && data.success && data.data) {
|
||||||
setCurrentServer(data.data);
|
setCurrentServer(data.data);
|
||||||
@@ -186,22 +142,10 @@ export const useMarketData = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const token = localStorage.getItem('mcphub_token');
|
const data: ApiResponse<MarketServer[]> = await apiGet(
|
||||||
const response = await fetch(
|
`/market/servers/search?query=${encodeURIComponent(query)}`,
|
||||||
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)) {
|
if (data && data.success && Array.isArray(data.data)) {
|
||||||
setAllServers(data.data);
|
setAllServers(data.data);
|
||||||
setCurrentPage(1);
|
setCurrentPage(1);
|
||||||
@@ -233,22 +177,10 @@ export const useMarketData = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const token = localStorage.getItem('mcphub_token');
|
const data: ApiResponse<MarketServer[]> = await apiGet(
|
||||||
const response = await fetch(
|
`/market/categories/${encodeURIComponent(category)}`,
|
||||||
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)) {
|
if (data && data.success && Array.isArray(data.data)) {
|
||||||
setAllServers(data.data);
|
setAllServers(data.data);
|
||||||
setCurrentPage(1);
|
setCurrentPage(1);
|
||||||
@@ -280,18 +212,9 @@ export const useMarketData = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const token = localStorage.getItem('mcphub_token');
|
const data: ApiResponse<MarketServer[]> = await apiGet(
|
||||||
const response = await fetch(getApiUrl(`/market/tags/${encodeURIComponent(tag)}`), {
|
`/market/tags/${encodeURIComponent(tag)}`,
|
||||||
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)) {
|
if (data && data.success && Array.isArray(data.data)) {
|
||||||
setAllServers(data.data);
|
setAllServers(data.data);
|
||||||
@@ -314,18 +237,7 @@ export const useMarketData = () => {
|
|||||||
// Fetch installed servers
|
// Fetch installed servers
|
||||||
const fetchInstalledServers = useCallback(async () => {
|
const fetchInstalledServers = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('mcphub_token');
|
const data = await apiGet<{ success: boolean; data: any[] }>('/servers');
|
||||||
const response = await fetch(getApiUrl('/servers'), {
|
|
||||||
headers: {
|
|
||||||
'x-auth-token': token || '',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Status: ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
||||||
@@ -365,27 +277,24 @@ export const useMarketData = () => {
|
|||||||
// Prepare server configuration, merging with customConfig
|
// Prepare server configuration, merging with customConfig
|
||||||
const serverConfig = {
|
const serverConfig = {
|
||||||
name: server.name,
|
name: server.name,
|
||||||
config: customConfig.type === 'stdio' ? {
|
config:
|
||||||
command: customConfig.command || installation.command || '',
|
customConfig.type === 'stdio'
|
||||||
args: customConfig.args || installation.args || [],
|
? {
|
||||||
env: { ...installation.env, ...customConfig.env },
|
command: customConfig.command || installation.command || '',
|
||||||
} : customConfig
|
args: customConfig.args || installation.args || [],
|
||||||
|
env: { ...installation.env, ...customConfig.env },
|
||||||
|
}
|
||||||
|
: customConfig,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Call the createServer API
|
// Call the createServer API
|
||||||
const token = localStorage.getItem('mcphub_token');
|
const result = await apiPost<{ success: boolean; message?: string }>(
|
||||||
const response = await fetch(getApiUrl('/servers'), {
|
'/servers',
|
||||||
method: 'POST',
|
serverConfig,
|
||||||
headers: {
|
);
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'x-auth-token': token || '',
|
|
||||||
},
|
|
||||||
body: JSON.stringify(serverConfig),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!result.success) {
|
||||||
const errorData = await response.json();
|
throw new Error(result.message || 'Failed to install server');
|
||||||
throw new Error(errorData.message || `Status: ${response.status}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update installed servers list after successful installation
|
// Update installed servers list after successful installation
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
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/runtime';
|
import { apiGet, apiPost, apiDelete } from '../utils/fetchInterceptor';
|
||||||
|
|
||||||
// Configuration options
|
// Configuration options
|
||||||
const CONFIG = {
|
const CONFIG = {
|
||||||
@@ -44,13 +44,7 @@ export const useServerData = () => {
|
|||||||
|
|
||||||
const fetchServers = async () => {
|
const fetchServers = async () => {
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('mcphub_token');
|
const data = await apiGet('/servers');
|
||||||
const response = await fetch(getApiUrl('/servers'), {
|
|
||||||
headers: {
|
|
||||||
'x-auth-token': token || '',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
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);
|
||||||
@@ -97,13 +91,7 @@ export const useServerData = () => {
|
|||||||
// Initialization phase request function
|
// Initialization phase request function
|
||||||
const fetchInitialData = async () => {
|
const fetchInitialData = async () => {
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('mcphub_token');
|
const data = await apiGet('/servers');
|
||||||
const response = await fetch(getApiUrl('/servers'), {
|
|
||||||
headers: {
|
|
||||||
'x-auth-token': token || '',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
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)) {
|
||||||
@@ -203,14 +191,8 @@ export const useServerData = () => {
|
|||||||
const handleServerEdit = async (server: Server) => {
|
const handleServerEdit = async (server: Server) => {
|
||||||
try {
|
try {
|
||||||
// Fetch settings to get the full server config before editing
|
// Fetch settings to get the full server config before editing
|
||||||
const token = localStorage.getItem('mcphub_token');
|
const settingsData: ApiResponse<{ mcpServers: Record<string, any> }> =
|
||||||
const response = await fetch(getApiUrl('/settings'), {
|
await apiGet('/settings');
|
||||||
headers: {
|
|
||||||
'x-auth-token': token || '',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const settingsData: ApiResponse<{ mcpServers: Record<string, any> }> = await response.json();
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
settingsData &&
|
settingsData &&
|
||||||
@@ -240,17 +222,10 @@ export const useServerData = () => {
|
|||||||
|
|
||||||
const handleServerRemove = async (serverName: string) => {
|
const handleServerRemove = async (serverName: string) => {
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('mcphub_token');
|
const result = await apiDelete(`/servers/${serverName}`);
|
||||||
const response = await fetch(getApiUrl(`/servers/${serverName}`), {
|
|
||||||
method: 'DELETE',
|
|
||||||
headers: {
|
|
||||||
'x-auth-token': token || '',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const result = await response.json();
|
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!result || !result.success) {
|
||||||
setError(result.message || t('server.deleteError', { serverName }));
|
setError(result?.message || t('server.deleteError', { serverName }));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,21 +239,11 @@ export const useServerData = () => {
|
|||||||
|
|
||||||
const handleServerToggle = async (server: Server, enabled: boolean) => {
|
const handleServerToggle = async (server: Server, enabled: boolean) => {
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('mcphub_token');
|
const result = await apiPost(`/servers/${server.name}/toggle`, { enabled });
|
||||||
const response = await fetch(getApiUrl(`/servers/${server.name}/toggle`), {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'x-auth-token': token || '',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ enabled }),
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await response.json();
|
if (!result || !result.success) {
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
console.error('Failed to toggle server:', result);
|
console.error('Failed to toggle server:', result);
|
||||||
setError(t('server.toggleError', { serverName: server.name }));
|
setError(result?.message || t('server.toggleError', { serverName: server.name }));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +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/runtime';
|
import { apiGet, apiPut } from '../utils/fetchInterceptor';
|
||||||
|
|
||||||
// Define types for the settings data
|
// Define types for the settings data
|
||||||
interface RoutingConfig {
|
interface RoutingConfig {
|
||||||
@@ -84,18 +84,7 @@ export const useSettingsData = () => {
|
|||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('mcphub_token');
|
const data: ApiResponse<SystemSettings> = await apiGet('/settings');
|
||||||
const response = await fetch(getApiUrl('/settings'), {
|
|
||||||
headers: {
|
|
||||||
'x-auth-token': token || '',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP error! Status: ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data: ApiResponse<SystemSettings> = await response.json();
|
|
||||||
|
|
||||||
if (data.success && data.data?.systemConfig?.routing) {
|
if (data.success && data.data?.systemConfig?.routing) {
|
||||||
setRoutingConfig({
|
setRoutingConfig({
|
||||||
@@ -134,34 +123,17 @@ export const useSettingsData = () => {
|
|||||||
}, [t]); // 移除 showToast 依赖
|
}, [t]); // 移除 showToast 依赖
|
||||||
|
|
||||||
// Update routing configuration
|
// Update routing configuration
|
||||||
const updateRoutingConfig = async <T extends keyof RoutingConfig>(
|
const updateRoutingConfig = async (key: keyof RoutingConfig, value: any) => {
|
||||||
key: T,
|
|
||||||
value: RoutingConfig[T],
|
|
||||||
) => {
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('mcphub_token');
|
const data = await apiPut('/system-config', {
|
||||||
const response = await fetch(getApiUrl('/system-config'), {
|
routing: {
|
||||||
method: 'PUT',
|
[key]: value,
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'x-auth-token': token || '',
|
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
|
||||||
routing: {
|
|
||||||
[key]: value,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP error! Status: ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
setRoutingConfig({
|
setRoutingConfig({
|
||||||
...routingConfig,
|
...routingConfig,
|
||||||
@@ -170,7 +142,7 @@ export const useSettingsData = () => {
|
|||||||
showToast(t('settings.systemConfigUpdated'));
|
showToast(t('settings.systemConfigUpdated'));
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
showToast(t('errors.failedToUpdateRouteConfig'));
|
showToast(data.message || t('errors.failedToUpdateRouteConfig'));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -189,26 +161,12 @@ export const useSettingsData = () => {
|
|||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('mcphub_token');
|
const data = await apiPut('/system-config', {
|
||||||
const response = await fetch(getApiUrl('/system-config'), {
|
install: {
|
||||||
method: 'PUT',
|
[key]: value,
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'x-auth-token': token || '',
|
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
|
||||||
install: {
|
|
||||||
[key]: value,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP error! Status: ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
setInstallConfig({
|
setInstallConfig({
|
||||||
...installConfig,
|
...installConfig,
|
||||||
@@ -217,7 +175,7 @@ export const useSettingsData = () => {
|
|||||||
showToast(t('settings.systemConfigUpdated'));
|
showToast(t('settings.systemConfigUpdated'));
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
showToast(t('errors.failedToUpdateSystemConfig'));
|
showToast(data.message || t('errors.failedToUpdateSystemConfig'));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -239,27 +197,12 @@ export const useSettingsData = () => {
|
|||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('mcphub_token');
|
const data = await apiPut('/system-config', {
|
||||||
const response = await fetch(getApiUrl('/system-config'), {
|
smartRouting: {
|
||||||
method: 'PUT',
|
[key]: value,
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'x-auth-token': token || '',
|
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
|
||||||
smartRouting: {
|
|
||||||
[key]: value,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorData = await response.json();
|
|
||||||
throw new Error(errorData.message || `HTTP error! Status: ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
setSmartRoutingConfig({
|
setSmartRoutingConfig({
|
||||||
...smartRoutingConfig,
|
...smartRoutingConfig,
|
||||||
@@ -289,25 +232,10 @@ export const useSettingsData = () => {
|
|||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('mcphub_token');
|
const data = await apiPut('/system-config', {
|
||||||
const response = await fetch(getApiUrl('/system-config'), {
|
smartRouting: updates,
|
||||||
method: 'PUT',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'x-auth-token': token || '',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
smartRouting: updates,
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorData = await response.json();
|
|
||||||
throw new Error(errorData.message || `HTTP error! Status: ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
setSmartRoutingConfig({
|
setSmartRoutingConfig({
|
||||||
...smartRoutingConfig,
|
...smartRoutingConfig,
|
||||||
@@ -337,24 +265,10 @@ export const useSettingsData = () => {
|
|||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('mcphub_token');
|
const data = await apiPut('/system-config', {
|
||||||
const response = await fetch(getApiUrl('/system-config'), {
|
routing: updates,
|
||||||
method: 'PUT',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'x-auth-token': token || '',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
routing: updates,
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP error! Status: ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
setRoutingConfig({
|
setRoutingConfig({
|
||||||
...routingConfig,
|
...routingConfig,
|
||||||
@@ -363,7 +277,7 @@ export const useSettingsData = () => {
|
|||||||
showToast(t('settings.systemConfigUpdated'));
|
showToast(t('settings.systemConfigUpdated'));
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
showToast(t('errors.failedToUpdateRouteConfig'));
|
showToast(data.message || t('errors.failedToUpdateRouteConfig'));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import i18n from 'i18next';
|
|||||||
import { initReactI18next } from 'react-i18next';
|
import { initReactI18next } from 'react-i18next';
|
||||||
import LanguageDetector from 'i18next-browser-languagedetector';
|
import LanguageDetector from 'i18next-browser-languagedetector';
|
||||||
|
|
||||||
// Import translations
|
// Import shared translations from root locales directory
|
||||||
import enTranslation from './locales/en.json';
|
import enTranslation from '../../locales/en.json';
|
||||||
import zhTranslation from './locales/zh.json';
|
import zhTranslation from '../../locales/zh.json';
|
||||||
|
|
||||||
i18n
|
i18n
|
||||||
// Detect user language
|
// Detect user language
|
||||||
@@ -15,11 +15,11 @@ i18n
|
|||||||
.init({
|
.init({
|
||||||
resources: {
|
resources: {
|
||||||
en: {
|
en: {
|
||||||
translation: enTranslation
|
translation: enTranslation,
|
||||||
},
|
},
|
||||||
zh: {
|
zh: {
|
||||||
translation: zhTranslation
|
translation: zhTranslation,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
fallbackLng: 'en',
|
fallbackLng: 'en',
|
||||||
debug: process.env.NODE_ENV === 'development',
|
debug: process.env.NODE_ENV === 'development',
|
||||||
@@ -36,7 +36,7 @@ i18n
|
|||||||
order: ['localStorage', 'cookie', 'htmlTag', 'navigator'],
|
order: ['localStorage', 'cookie', 'htmlTag', 'navigator'],
|
||||||
// Cache the language in localStorage
|
// Cache the language in localStorage
|
||||||
caches: ['localStorage', 'cookie'],
|
caches: ['localStorage', 'cookie'],
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default i18n;
|
export default i18n;
|
||||||
@@ -4,6 +4,8 @@ import App from './App';
|
|||||||
import './index.css';
|
import './index.css';
|
||||||
// Import the i18n configuration
|
// Import the i18n configuration
|
||||||
import './i18n';
|
import './i18n';
|
||||||
|
// Setup fetch interceptors
|
||||||
|
import './utils/setupInterceptors';
|
||||||
import { loadRuntimeConfig } from './utils/runtime';
|
import { loadRuntimeConfig } from './utils/runtime';
|
||||||
|
|
||||||
// Load runtime configuration before starting the app
|
// Load runtime configuration before starting the app
|
||||||
|
|||||||
@@ -32,9 +32,9 @@ const GroupsPage: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteGroup = async (groupId: string) => {
|
const handleDeleteGroup = async (groupId: string) => {
|
||||||
const success = await deleteGroup(groupId);
|
const result = await deleteGroup(groupId);
|
||||||
if (!success) {
|
if (!result || !result.success) {
|
||||||
setGroupError(t('groups.deleteError'));
|
setGroupError(result?.message || t('groups.deleteError'));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -103,7 +103,6 @@ const ServersPage: React.FC = () => {
|
|||||||
<div className="mb-6 bg-red-50 border-l-4 border-red-500 p-4 rounded shadow-sm error-box">
|
<div className="mb-6 bg-red-50 border-l-4 border-red-500 p-4 rounded shadow-sm error-box">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-red-600 text-lg font-medium">{t('app.error')}</h3>
|
|
||||||
<p className="text-gray-600 mt-1">{error}</p>
|
<p className="text-gray-600 mt-1">{error}</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -4,45 +4,27 @@ import {
|
|||||||
RegisterCredentials,
|
RegisterCredentials,
|
||||||
ChangePasswordCredentials,
|
ChangePasswordCredentials,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import { getApiUrl } from '../utils/runtime';
|
import { apiPost, apiGet } from '../utils/fetchInterceptor';
|
||||||
|
import { getToken, setToken, removeToken } from '../utils/interceptors';
|
||||||
|
|
||||||
// Token key in localStorage
|
// Export token management functions
|
||||||
const TOKEN_KEY = 'mcphub_token';
|
export { getToken, setToken, removeToken };
|
||||||
|
|
||||||
// Get token from localStorage
|
|
||||||
export const getToken = (): string | null => {
|
|
||||||
return localStorage.getItem(TOKEN_KEY);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Set token in localStorage
|
|
||||||
export const setToken = (token: string): void => {
|
|
||||||
localStorage.setItem(TOKEN_KEY, token);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Remove token from localStorage
|
|
||||||
export const removeToken = (): void => {
|
|
||||||
localStorage.removeItem(TOKEN_KEY);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Login user
|
// Login user
|
||||||
export const login = async (credentials: LoginCredentials): Promise<AuthResponse> => {
|
export const login = async (credentials: LoginCredentials): Promise<AuthResponse> => {
|
||||||
try {
|
try {
|
||||||
console.log(getApiUrl('/auth/login'));
|
const response = await apiPost<AuthResponse>('/auth/login', credentials);
|
||||||
const response = await fetch(getApiUrl('/auth/login'), {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify(credentials),
|
|
||||||
});
|
|
||||||
|
|
||||||
const data: AuthResponse = await response.json();
|
// The auth API returns data directly, not wrapped in a data field
|
||||||
|
if (response.success && response.token) {
|
||||||
if (data.success && data.token) {
|
setToken(response.token);
|
||||||
setToken(data.token);
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
return data;
|
return {
|
||||||
|
success: false,
|
||||||
|
message: response.message || 'Login failed',
|
||||||
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Login error:', error);
|
console.error('Login error:', error);
|
||||||
return {
|
return {
|
||||||
@@ -55,21 +37,17 @@ 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(getApiUrl('/auth/register'), {
|
const response = await apiPost<AuthResponse>('/auth/register', credentials);
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify(credentials),
|
|
||||||
});
|
|
||||||
|
|
||||||
const data: AuthResponse = await response.json();
|
if (response.success && response.token) {
|
||||||
|
setToken(response.token);
|
||||||
if (data.success && data.token) {
|
return response;
|
||||||
setToken(data.token);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return data;
|
return {
|
||||||
|
success: false,
|
||||||
|
message: response.message || 'Registration failed',
|
||||||
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Register error:', error);
|
console.error('Register error:', error);
|
||||||
return {
|
return {
|
||||||
@@ -91,14 +69,8 @@ export const getCurrentUser = async (): Promise<AuthResponse> => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(getApiUrl('/auth/user'), {
|
const response = await apiGet<AuthResponse>('/auth/user');
|
||||||
method: 'GET',
|
return response;
|
||||||
headers: {
|
|
||||||
'x-auth-token': token,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return await response.json();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Get current user error:', error);
|
console.error('Get current user error:', error);
|
||||||
return {
|
return {
|
||||||
@@ -122,16 +94,8 @@ export const changePassword = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(getApiUrl('/auth/change-password'), {
|
const response = await apiPost<AuthResponse>('/auth/change-password', credentials);
|
||||||
method: 'POST',
|
return response;
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'x-auth-token': token,
|
|
||||||
},
|
|
||||||
body: JSON.stringify(credentials),
|
|
||||||
});
|
|
||||||
|
|
||||||
return await response.json();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Change password error:', error);
|
console.error('Change password error:', error);
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { getApiUrl, getBasePath } from '../utils/runtime';
|
import { apiGet, fetchWithInterceptors } from '../utils/fetchInterceptor';
|
||||||
|
import { getBasePath } from '../utils/runtime';
|
||||||
|
|
||||||
export interface SystemConfig {
|
export interface SystemConfig {
|
||||||
routing?: {
|
routing?: {
|
||||||
@@ -43,7 +44,7 @@ export interface SystemConfigResponse {
|
|||||||
export const getPublicConfig = async (): Promise<{ skipAuth: boolean }> => {
|
export const getPublicConfig = async (): Promise<{ skipAuth: boolean }> => {
|
||||||
try {
|
try {
|
||||||
const basePath = getBasePath();
|
const basePath = getBasePath();
|
||||||
const response = await fetch(`${basePath}/public-config`, {
|
const response = await fetchWithInterceptors(`${basePath}/public-config`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@@ -69,16 +70,10 @@ export const getPublicConfig = async (): Promise<{ skipAuth: boolean }> => {
|
|||||||
*/
|
*/
|
||||||
export const getSystemConfigPublic = async (): Promise<SystemConfig | null> => {
|
export const getSystemConfigPublic = async (): Promise<SystemConfig | null> => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(getApiUrl('/settings'), {
|
const response = await apiGet<SystemConfigResponse>('/settings');
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.success) {
|
||||||
const data: SystemConfigResponse = await response.json();
|
return response.data?.systemConfig || null;
|
||||||
return data.data?.systemConfig || null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { getToken } from './authService'; // Import getToken function
|
import { apiGet, apiDelete } from '../utils/fetchInterceptor';
|
||||||
import { getApiUrl } from '../utils/runtime';
|
import { getApiUrl } from '../utils/runtime';
|
||||||
|
import { getToken } from '../utils/interceptors';
|
||||||
|
|
||||||
export interface LogEntry {
|
export interface LogEntry {
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
@@ -13,21 +14,13 @@ export interface LogEntry {
|
|||||||
// Fetch all logs
|
// Fetch all logs
|
||||||
export const fetchLogs = async (): Promise<LogEntry[]> => {
|
export const fetchLogs = async (): Promise<LogEntry[]> => {
|
||||||
try {
|
try {
|
||||||
// Get authentication token
|
const response = await apiGet<{ success: boolean; data: LogEntry[]; error?: string }>('/logs');
|
||||||
const token = getToken();
|
|
||||||
const response = await fetch(getApiUrl('/logs'), {
|
|
||||||
headers: {
|
|
||||||
'x-auth-token': token || '',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await response.json();
|
if (!response.success) {
|
||||||
|
throw new Error(response.error || 'Failed to fetch logs');
|
||||||
if (!result.success) {
|
|
||||||
throw new Error(result.error || 'Failed to fetch logs');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching logs:', error);
|
console.error('Error fetching logs:', error);
|
||||||
throw error;
|
throw error;
|
||||||
@@ -37,19 +30,10 @@ export const fetchLogs = async (): Promise<LogEntry[]> => {
|
|||||||
// Clear all logs
|
// Clear all logs
|
||||||
export const clearLogs = async (): Promise<void> => {
|
export const clearLogs = async (): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
// Get authentication token
|
const response = await apiDelete<{ success: boolean; error?: string }>('/logs');
|
||||||
const token = getToken();
|
|
||||||
const response = await fetch(getApiUrl('/logs'), {
|
|
||||||
method: 'DELETE',
|
|
||||||
headers: {
|
|
||||||
'x-auth-token': token || '',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await response.json();
|
if (!response.success) {
|
||||||
|
throw new Error(response.error || 'Failed to clear logs');
|
||||||
if (!result.success) {
|
|
||||||
throw new Error(result.error || 'Failed to clear logs');
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error clearing logs:', error);
|
console.error('Error clearing logs:', error);
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { getApiUrl } from '../utils/runtime';
|
import { apiPost, apiPut } from '../utils/fetchInterceptor';
|
||||||
import { getToken } from './authService';
|
|
||||||
|
|
||||||
export interface ToolCallRequest {
|
export interface ToolCallRequest {
|
||||||
toolName: string;
|
toolName: string;
|
||||||
@@ -25,38 +24,32 @@ export const callTool = async (
|
|||||||
server?: string,
|
server?: string,
|
||||||
): Promise<ToolCallResult> => {
|
): Promise<ToolCallResult> => {
|
||||||
try {
|
try {
|
||||||
const token = getToken();
|
|
||||||
// Construct the URL with optional server parameter
|
// Construct the URL with optional server parameter
|
||||||
const url = server ? `/tools/call/${server}` : '/tools/call';
|
const url = server ? `/tools/call/${server}` : '/tools/call';
|
||||||
|
|
||||||
const response = await fetch(getApiUrl(url), {
|
const response = await apiPost<any>(
|
||||||
method: 'POST',
|
url,
|
||||||
headers: {
|
{
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'x-auth-token': token || '', // Include token for authentication
|
|
||||||
Authorization: `Bearer ${token}`, // Add bearer auth for MCP routing
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
toolName: request.toolName,
|
toolName: request.toolName,
|
||||||
arguments: request.arguments,
|
arguments: request.arguments,
|
||||||
}),
|
},
|
||||||
});
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${localStorage.getItem('mcphub_token')}`, // Add bearer auth for MCP routing
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.success) {
|
||||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
if (!data.success) {
|
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: data.message || 'Tool call failed',
|
error: response.message || 'Tool call failed',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
content: data.data.content || [],
|
content: response.data?.content || [],
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error calling tool:', error);
|
console.error('Error calling tool:', error);
|
||||||
@@ -76,25 +69,19 @@ export const toggleTool = async (
|
|||||||
enabled: boolean,
|
enabled: boolean,
|
||||||
): Promise<{ success: boolean; error?: string }> => {
|
): Promise<{ success: boolean; error?: string }> => {
|
||||||
try {
|
try {
|
||||||
const token = getToken();
|
const response = await apiPost<any>(
|
||||||
const response = await fetch(getApiUrl(`/servers/${serverName}/tools/${toolName}/toggle`), {
|
`/servers/${serverName}/tools/${toolName}/toggle`,
|
||||||
method: 'POST',
|
{ enabled },
|
||||||
headers: {
|
{
|
||||||
'Content-Type': 'application/json',
|
headers: {
|
||||||
'x-auth-token': token || '',
|
Authorization: `Bearer ${localStorage.getItem('mcphub_token')}`,
|
||||||
Authorization: `Bearer ${token}`,
|
},
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ enabled }),
|
);
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
return {
|
return {
|
||||||
success: data.success,
|
success: response.success,
|
||||||
error: data.success ? undefined : data.message,
|
error: response.success ? undefined : response.message,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error toggling tool:', error);
|
console.error('Error toggling tool:', error);
|
||||||
@@ -114,28 +101,19 @@ export const updateToolDescription = async (
|
|||||||
description: string,
|
description: string,
|
||||||
): Promise<{ success: boolean; error?: string }> => {
|
): Promise<{ success: boolean; error?: string }> => {
|
||||||
try {
|
try {
|
||||||
const token = getToken();
|
const response = await apiPut<any>(
|
||||||
const response = await fetch(
|
`/servers/${serverName}/tools/${toolName}/description`,
|
||||||
getApiUrl(`/servers/${serverName}/tools/${toolName}/description`),
|
{ description },
|
||||||
{
|
{
|
||||||
method: 'PUT',
|
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
Authorization: `Bearer ${localStorage.getItem('mcphub_token')}`,
|
||||||
'x-auth-token': token || '',
|
|
||||||
Authorization: `Bearer ${token || ''}`,
|
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ description }),
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
return {
|
return {
|
||||||
success: data.success,
|
success: response.success,
|
||||||
error: data.success ? undefined : data.message,
|
error: response.success ? undefined : response.message,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error updating tool description:', error);
|
console.error('Error updating tool description:', error);
|
||||||
|
|||||||
174
frontend/src/utils/fetchInterceptor.ts
Normal file
174
frontend/src/utils/fetchInterceptor.ts
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
import { getApiUrl } from './runtime';
|
||||||
|
|
||||||
|
// Define the interceptor interface
|
||||||
|
export interface FetchInterceptor {
|
||||||
|
request?: (url: string, config: RequestInit) => Promise<{ url: string; config: RequestInit }>;
|
||||||
|
response?: (response: Response) => Promise<Response>;
|
||||||
|
error?: (error: Error) => Promise<Error>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define the enhanced fetch response interface
|
||||||
|
export interface ApiResponse<T = any> {
|
||||||
|
success: boolean;
|
||||||
|
data?: T;
|
||||||
|
message?: string;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global interceptors store
|
||||||
|
const interceptors: FetchInterceptor[] = [];
|
||||||
|
|
||||||
|
// Add an interceptor
|
||||||
|
export const addInterceptor = (interceptor: FetchInterceptor): void => {
|
||||||
|
interceptors.push(interceptor);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Remove an interceptor
|
||||||
|
export const removeInterceptor = (interceptor: FetchInterceptor): void => {
|
||||||
|
const index = interceptors.indexOf(interceptor);
|
||||||
|
if (index > -1) {
|
||||||
|
interceptors.splice(index, 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Clear all interceptors
|
||||||
|
export const clearInterceptors = (): void => {
|
||||||
|
interceptors.length = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Enhanced fetch function with interceptors
|
||||||
|
export const fetchWithInterceptors = async (
|
||||||
|
input: string | URL | Request,
|
||||||
|
init: RequestInit = {},
|
||||||
|
): Promise<Response> => {
|
||||||
|
let url = input.toString();
|
||||||
|
let config = { ...init };
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Apply request interceptors
|
||||||
|
for (const interceptor of interceptors) {
|
||||||
|
if (interceptor.request) {
|
||||||
|
const result = await interceptor.request(url, config);
|
||||||
|
url = result.url;
|
||||||
|
config = result.config;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make the actual fetch request
|
||||||
|
let response = await fetch(url, config);
|
||||||
|
|
||||||
|
// Apply response interceptors
|
||||||
|
for (const interceptor of interceptors) {
|
||||||
|
if (interceptor.response) {
|
||||||
|
response = await interceptor.response(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
let processedError = error as Error;
|
||||||
|
|
||||||
|
// Apply error interceptors
|
||||||
|
for (const interceptor of interceptors) {
|
||||||
|
if (interceptor.error) {
|
||||||
|
processedError = await interceptor.error(processedError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw processedError;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Convenience function for API calls with automatic URL construction
|
||||||
|
export const apiRequest = async <T = any>(endpoint: string, init: RequestInit = {}): Promise<T> => {
|
||||||
|
try {
|
||||||
|
const url = getApiUrl(endpoint);
|
||||||
|
const response = await fetchWithInterceptors(url, init);
|
||||||
|
|
||||||
|
// Try to parse JSON response
|
||||||
|
let data: T;
|
||||||
|
try {
|
||||||
|
data = await response.json();
|
||||||
|
} catch (parseError) {
|
||||||
|
// If JSON parsing fails, create a generic response
|
||||||
|
const genericResponse = {
|
||||||
|
success: response.ok,
|
||||||
|
message: response.ok
|
||||||
|
? 'Request successful'
|
||||||
|
: `HTTP ${response.status}: ${response.statusText}`,
|
||||||
|
};
|
||||||
|
data = genericResponse as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If response is not ok, but no explicit error in parsed data
|
||||||
|
if (!response.ok && typeof data === 'object' && data !== null) {
|
||||||
|
const responseObj = data as any;
|
||||||
|
if (responseObj.success !== false) {
|
||||||
|
responseObj.success = false;
|
||||||
|
responseObj.message =
|
||||||
|
responseObj.message || `HTTP ${response.status}: ${response.statusText}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('API request error:', error);
|
||||||
|
const errorResponse = {
|
||||||
|
success: false,
|
||||||
|
message: error instanceof Error ? error.message : 'An unknown error occurred',
|
||||||
|
};
|
||||||
|
return errorResponse as T;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Convenience methods for common HTTP methods
|
||||||
|
export const apiGet = <T = any>(endpoint: string, init: Omit<RequestInit, 'method'> = {}) =>
|
||||||
|
apiRequest<T>(endpoint, { ...init, method: 'GET' });
|
||||||
|
|
||||||
|
export const apiPost = <T = any>(
|
||||||
|
endpoint: string,
|
||||||
|
data?: any,
|
||||||
|
init: Omit<RequestInit, 'method' | 'body'> = {},
|
||||||
|
) =>
|
||||||
|
apiRequest<T>(endpoint, {
|
||||||
|
...init,
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...init.headers,
|
||||||
|
},
|
||||||
|
body: data ? JSON.stringify(data) : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const apiPut = <T = any>(
|
||||||
|
endpoint: string,
|
||||||
|
data?: any,
|
||||||
|
init: Omit<RequestInit, 'method' | 'body'> = {},
|
||||||
|
) =>
|
||||||
|
apiRequest<T>(endpoint, {
|
||||||
|
...init,
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...init.headers,
|
||||||
|
},
|
||||||
|
body: data ? JSON.stringify(data) : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const apiDelete = <T = any>(endpoint: string, init: Omit<RequestInit, 'method'> = {}) =>
|
||||||
|
apiRequest<T>(endpoint, { ...init, method: 'DELETE' });
|
||||||
|
|
||||||
|
export const apiPatch = <T = any>(
|
||||||
|
endpoint: string,
|
||||||
|
data?: any,
|
||||||
|
init: Omit<RequestInit, 'method' | 'body'> = {},
|
||||||
|
) =>
|
||||||
|
apiRequest<T>(endpoint, {
|
||||||
|
...init,
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...init.headers,
|
||||||
|
},
|
||||||
|
body: data ? JSON.stringify(data) : undefined,
|
||||||
|
});
|
||||||
99
frontend/src/utils/interceptors.ts
Normal file
99
frontend/src/utils/interceptors.ts
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
import { addInterceptor, removeInterceptor, type FetchInterceptor } from './fetchInterceptor';
|
||||||
|
|
||||||
|
// Token key in localStorage
|
||||||
|
const TOKEN_KEY = 'mcphub_token';
|
||||||
|
|
||||||
|
// Get token from localStorage
|
||||||
|
export const getToken = (): string | null => {
|
||||||
|
return localStorage.getItem(TOKEN_KEY);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set token in localStorage
|
||||||
|
export const setToken = (token: string): void => {
|
||||||
|
localStorage.setItem(TOKEN_KEY, token);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Remove token from localStorage
|
||||||
|
export const removeToken = (): void => {
|
||||||
|
localStorage.removeItem(TOKEN_KEY);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Auth interceptor for automatically adding authorization headers
|
||||||
|
export const authInterceptor: FetchInterceptor = {
|
||||||
|
request: async (url: string, config: RequestInit) => {
|
||||||
|
const headers = new Headers(config.headers);
|
||||||
|
const language = localStorage.getItem('i18nextLng') || 'en';
|
||||||
|
headers.set('Accept-Language', language);
|
||||||
|
|
||||||
|
const token = getToken();
|
||||||
|
if (token) {
|
||||||
|
headers.set('x-auth-token', token);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
url,
|
||||||
|
config: {
|
||||||
|
...config,
|
||||||
|
headers,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
response: async (response: Response) => {
|
||||||
|
// Handle unauthorized responses
|
||||||
|
if (response.status === 401) {
|
||||||
|
// Token might be expired or invalid, remove it
|
||||||
|
removeToken();
|
||||||
|
|
||||||
|
// You could also trigger a redirect to login page here
|
||||||
|
// window.location.href = '/login';
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
|
||||||
|
error: async (error: Error) => {
|
||||||
|
console.error('Auth interceptor error:', error);
|
||||||
|
return error;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Install the auth interceptor
|
||||||
|
export const installAuthInterceptor = (): void => {
|
||||||
|
addInterceptor(authInterceptor);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Uninstall the auth interceptor
|
||||||
|
export const uninstallAuthInterceptor = (): void => {
|
||||||
|
removeInterceptor(authInterceptor);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Logging interceptor for development
|
||||||
|
export const loggingInterceptor: FetchInterceptor = {
|
||||||
|
request: async (url: string, config: RequestInit) => {
|
||||||
|
console.log(`🚀 [${config.method || 'GET'}] ${url}`, config);
|
||||||
|
return { url, config };
|
||||||
|
},
|
||||||
|
|
||||||
|
response: async (response: Response) => {
|
||||||
|
console.log(`✅ [${response.status}] ${response.url}`);
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
|
||||||
|
error: async (error: Error) => {
|
||||||
|
console.error(`❌ Fetch error:`, error);
|
||||||
|
return error;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Install the logging interceptor (only in development)
|
||||||
|
export const installLoggingInterceptor = (): void => {
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
addInterceptor(loggingInterceptor);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Uninstall the logging interceptor
|
||||||
|
export const uninstallLoggingInterceptor = (): void => {
|
||||||
|
removeInterceptor(loggingInterceptor);
|
||||||
|
};
|
||||||
19
frontend/src/utils/setupInterceptors.ts
Normal file
19
frontend/src/utils/setupInterceptors.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { installAuthInterceptor, installLoggingInterceptor } from './interceptors';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup all default interceptors for the application
|
||||||
|
* This should be called once when the app initializes
|
||||||
|
*/
|
||||||
|
export const setupInterceptors = (): void => {
|
||||||
|
// Install auth interceptor for automatic token handling
|
||||||
|
installAuthInterceptor();
|
||||||
|
|
||||||
|
// Install logging interceptor in development mode
|
||||||
|
installLoggingInterceptor();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize interceptors automatically when this module is imported
|
||||||
|
* This ensures interceptors are set up as early as possible
|
||||||
|
*/
|
||||||
|
setupInterceptors();
|
||||||
@@ -456,5 +456,63 @@
|
|||||||
"deleteConfirmation": "Are you sure you want to delete user '{{username}}'? This action cannot be undone.",
|
"deleteConfirmation": "Are you sure you want to delete user '{{username}}'? This action cannot be undone.",
|
||||||
"confirmDelete": "Delete User",
|
"confirmDelete": "Delete User",
|
||||||
"deleteWarning": "Are you sure you want to delete user '{{username}}'? This action cannot be undone."
|
"deleteWarning": "Are you sure you want to delete user '{{username}}'? This action cannot be undone."
|
||||||
|
},
|
||||||
|
"api": {
|
||||||
|
"errors": {
|
||||||
|
"readonly": "Readonly for demo environment",
|
||||||
|
"serverNameRequired": "Server name is required",
|
||||||
|
"serverConfigRequired": "Server configuration is required",
|
||||||
|
"serverConfigInvalid": "Server configuration must include either a URL, OpenAPI specification URL or schema, or command with arguments",
|
||||||
|
"serverTypeInvalid": "Server type must be one of: stdio, sse, streamable-http, openapi",
|
||||||
|
"urlRequiredForType": "URL is required for {{type}} server type",
|
||||||
|
"openapiSpecRequired": "OpenAPI specification URL or schema is required for openapi server type",
|
||||||
|
"headersInvalidFormat": "Headers must be an object",
|
||||||
|
"headersNotSupportedForStdio": "Headers are not supported for stdio server type",
|
||||||
|
"serverNotFound": "Server not found",
|
||||||
|
"failedToRemoveServer": "Server not found or failed to remove",
|
||||||
|
"internalServerError": "Internal server error",
|
||||||
|
"failedToGetServers": "Failed to get servers information",
|
||||||
|
"failedToGetServerSettings": "Failed to get server settings",
|
||||||
|
"failedToGetServerConfig": "Failed to get server configuration",
|
||||||
|
"failedToSaveSettings": "Failed to save settings",
|
||||||
|
"toolNameRequired": "Server name and tool name are required",
|
||||||
|
"descriptionMustBeString": "Description must be a string",
|
||||||
|
"groupIdRequired": "Group ID is required",
|
||||||
|
"groupNameRequired": "Group name is required",
|
||||||
|
"groupNotFound": "Group not found",
|
||||||
|
"groupIdAndServerNameRequired": "Group ID and server name are required",
|
||||||
|
"groupOrServerNotFound": "Group or server not found",
|
||||||
|
"toolsMustBeAllOrArray": "Tools must be \"all\" or an array of strings",
|
||||||
|
"serverNameAndToolNameRequired": "Server name and tool name are required",
|
||||||
|
"usernameRequired": "Username is required",
|
||||||
|
"userNotFound": "User not found",
|
||||||
|
"failedToGetUsers": "Failed to get users information",
|
||||||
|
"failedToGetUserInfo": "Failed to get user information",
|
||||||
|
"failedToGetUserStats": "Failed to get user statistics",
|
||||||
|
"marketServerNameRequired": "Server name is required",
|
||||||
|
"marketServerNotFound": "Market server not found",
|
||||||
|
"failedToGetMarketServers": "Failed to get market servers information",
|
||||||
|
"failedToGetMarketServer": "Failed to get market server information",
|
||||||
|
"failedToGetMarketCategories": "Failed to get market categories",
|
||||||
|
"failedToGetMarketTags": "Failed to get market tags",
|
||||||
|
"failedToSearchMarketServers": "Failed to search market servers",
|
||||||
|
"failedToFilterMarketServers": "Failed to filter market servers",
|
||||||
|
"failedToProcessDxtFile": "Failed to process DXT file"
|
||||||
|
},
|
||||||
|
"success": {
|
||||||
|
"serverCreated": "Server created successfully",
|
||||||
|
"serverUpdated": "Server updated successfully",
|
||||||
|
"serverRemoved": "Server removed successfully",
|
||||||
|
"serverToggled": "Server status toggled successfully",
|
||||||
|
"toolToggled": "Tool {{name}} {{action}} successfully",
|
||||||
|
"toolDescriptionUpdated": "Tool {{name}} description updated successfully",
|
||||||
|
"systemConfigUpdated": "System configuration updated successfully",
|
||||||
|
"groupCreated": "Group created successfully",
|
||||||
|
"groupUpdated": "Group updated successfully",
|
||||||
|
"groupDeleted": "Group deleted successfully",
|
||||||
|
"serverAddedToGroup": "Server added to group successfully",
|
||||||
|
"serverRemovedFromGroup": "Server removed from group successfully",
|
||||||
|
"serverToolsUpdated": "Server tools updated successfully"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -458,5 +458,63 @@
|
|||||||
"deleteConfirmation": "您确定要删除用户 '{{username}}' 吗?此操作无法撤消。",
|
"deleteConfirmation": "您确定要删除用户 '{{username}}' 吗?此操作无法撤消。",
|
||||||
"confirmDelete": "删除用户",
|
"confirmDelete": "删除用户",
|
||||||
"deleteWarning": "您确定要删除用户 '{{username}}' 吗?此操作无法撤消。"
|
"deleteWarning": "您确定要删除用户 '{{username}}' 吗?此操作无法撤消。"
|
||||||
|
},
|
||||||
|
"api": {
|
||||||
|
"errors": {
|
||||||
|
"readonly": "演示环境无法修改数据",
|
||||||
|
"serverNameRequired": "服务器名称是必需的",
|
||||||
|
"serverConfigRequired": "服务器配置是必需的",
|
||||||
|
"serverConfigInvalid": "服务器配置必须包含 URL、OpenAPI 规范 URL 或模式,或者带参数的命令",
|
||||||
|
"serverTypeInvalid": "服务器类型必须是以下之一:stdio、sse、streamable-http、openapi",
|
||||||
|
"urlRequiredForType": "{{type}} 服务器类型需要 URL",
|
||||||
|
"openapiSpecRequired": "openapi 服务器类型需要 OpenAPI 规范 URL 或模式",
|
||||||
|
"headersInvalidFormat": "请求头必须是对象格式",
|
||||||
|
"headersNotSupportedForStdio": "stdio 服务器类型不支持请求头",
|
||||||
|
"serverNotFound": "找不到服务器",
|
||||||
|
"failedToRemoveServer": "找不到服务器或删除失败",
|
||||||
|
"internalServerError": "服务器内部错误",
|
||||||
|
"failedToGetServers": "获取服务器信息失败",
|
||||||
|
"failedToGetServerSettings": "获取服务器设置失败",
|
||||||
|
"failedToGetServerConfig": "获取服务器配置失败",
|
||||||
|
"failedToSaveSettings": "保存设置失败",
|
||||||
|
"toolNameRequired": "服务器名称和工具名称是必需的",
|
||||||
|
"descriptionMustBeString": "描述必须是字符串",
|
||||||
|
"groupIdRequired": "分组 ID 是必需的",
|
||||||
|
"groupNameRequired": "分组名称是必需的",
|
||||||
|
"groupNotFound": "找不到分组",
|
||||||
|
"groupIdAndServerNameRequired": "分组 ID 和服务器名称是必需的",
|
||||||
|
"groupOrServerNotFound": "找不到分组或服务器",
|
||||||
|
"toolsMustBeAllOrArray": "工具必须是 \"all\" 或字符串数组",
|
||||||
|
"serverNameAndToolNameRequired": "服务器名称和工具名称是必需的",
|
||||||
|
"usernameRequired": "用户名是必需的",
|
||||||
|
"userNotFound": "找不到用户",
|
||||||
|
"failedToGetUsers": "获取用户信息失败",
|
||||||
|
"failedToGetUserInfo": "获取用户信息失败",
|
||||||
|
"failedToGetUserStats": "获取用户统计信息失败",
|
||||||
|
"marketServerNameRequired": "服务器名称是必需的",
|
||||||
|
"marketServerNotFound": "找不到市场服务器",
|
||||||
|
"failedToGetMarketServers": "获取市场服务器信息失败",
|
||||||
|
"failedToGetMarketServer": "获取市场服务器信息失败",
|
||||||
|
"failedToGetMarketCategories": "获取市场类别失败",
|
||||||
|
"failedToGetMarketTags": "获取市场标签失败",
|
||||||
|
"failedToSearchMarketServers": "搜索市场服务器失败",
|
||||||
|
"failedToFilterMarketServers": "过滤市场服务器失败",
|
||||||
|
"failedToProcessDxtFile": "处理 DXT 文件失败"
|
||||||
|
},
|
||||||
|
"success": {
|
||||||
|
"serverCreated": "服务器创建成功",
|
||||||
|
"serverUpdated": "服务器更新成功",
|
||||||
|
"serverRemoved": "服务器删除成功",
|
||||||
|
"serverToggled": "服务器状态切换成功",
|
||||||
|
"toolToggled": "工具 {{name}} {{action}} 成功",
|
||||||
|
"toolDescriptionUpdated": "工具 {{name}} 描述更新成功",
|
||||||
|
"systemConfigUpdated": "系统配置更新成功",
|
||||||
|
"groupCreated": "分组创建成功",
|
||||||
|
"groupUpdated": "分组更新成功",
|
||||||
|
"groupDeleted": "分组删除成功",
|
||||||
|
"serverAddedToGroup": "服务器添加到分组成功",
|
||||||
|
"serverRemovedFromGroup": "服务器从分组移除成功",
|
||||||
|
"serverToolsUpdated": "服务器工具更新成功"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -57,6 +57,7 @@
|
|||||||
"dotenv-expand": "^12.0.2",
|
"dotenv-expand": "^12.0.2",
|
||||||
"express": "^4.21.2",
|
"express": "^4.21.2",
|
||||||
"express-validator": "^7.2.1",
|
"express-validator": "^7.2.1",
|
||||||
|
"i18next-fs-backend": "^2.6.0",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"multer": "^2.0.1",
|
"multer": "^2.0.1",
|
||||||
"openai": "^4.103.0",
|
"openai": "^4.103.0",
|
||||||
|
|||||||
8
pnpm-lock.yaml
generated
8
pnpm-lock.yaml
generated
@@ -44,6 +44,9 @@ importers:
|
|||||||
express-validator:
|
express-validator:
|
||||||
specifier: ^7.2.1
|
specifier: ^7.2.1
|
||||||
version: 7.2.1
|
version: 7.2.1
|
||||||
|
i18next-fs-backend:
|
||||||
|
specifier: ^2.6.0
|
||||||
|
version: 2.6.0
|
||||||
jsonwebtoken:
|
jsonwebtoken:
|
||||||
specifier: ^9.0.2
|
specifier: ^9.0.2
|
||||||
version: 9.0.2
|
version: 9.0.2
|
||||||
@@ -2669,6 +2672,9 @@ packages:
|
|||||||
i18next-browser-languagedetector@8.2.0:
|
i18next-browser-languagedetector@8.2.0:
|
||||||
resolution: {integrity: sha512-P+3zEKLnOF0qmiesW383vsLdtQVyKtCNA9cjSoKCppTKPQVfKd2W8hbVo5ZhNJKDqeM7BOcvNoKJOjpHh4Js9g==}
|
resolution: {integrity: sha512-P+3zEKLnOF0qmiesW383vsLdtQVyKtCNA9cjSoKCppTKPQVfKd2W8hbVo5ZhNJKDqeM7BOcvNoKJOjpHh4Js9g==}
|
||||||
|
|
||||||
|
i18next-fs-backend@2.6.0:
|
||||||
|
resolution: {integrity: sha512-3ZlhNoF9yxnM8pa8bWp5120/Ob6t4lVl1l/tbLmkml/ei3ud8IWySCHt2lrY5xWRlSU5D9IV2sm5bEbGuTqwTw==}
|
||||||
|
|
||||||
i18next@24.2.3:
|
i18next@24.2.3:
|
||||||
resolution: {integrity: sha512-lfbf80OzkocvX7nmZtu7nSTNbrTYR52sLWxPtlXX1zAhVw8WEnFk4puUkCR4B1dNQwbSpEHHHemcZu//7EcB7A==}
|
resolution: {integrity: sha512-lfbf80OzkocvX7nmZtu7nSTNbrTYR52sLWxPtlXX1zAhVw8WEnFk4puUkCR4B1dNQwbSpEHHHemcZu//7EcB7A==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -6902,6 +6908,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 7.27.0
|
'@babel/runtime': 7.27.0
|
||||||
|
|
||||||
|
i18next-fs-backend@2.6.0: {}
|
||||||
|
|
||||||
i18next@24.2.3(typescript@5.8.3):
|
i18next@24.2.3(typescript@5.8.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 7.27.0
|
'@babel/runtime': 7.27.0
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ const defaultConfig = {
|
|||||||
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 || '',
|
basePath: process.env.BASE_PATH || '',
|
||||||
|
readonly: 'true' === process.env.READONLY || false,
|
||||||
mcpHubName: 'mcphub',
|
mcpHubName: 'mcphub',
|
||||||
mcpHubVersion: getPackageVersion(),
|
mcpHubVersion: getPackageVersion(),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,10 +18,17 @@ const TOKEN_EXPIRY = '24h';
|
|||||||
|
|
||||||
// Login user
|
// Login user
|
||||||
export const login = async (req: Request, res: Response): Promise<void> => {
|
export const login = async (req: Request, res: Response): Promise<void> => {
|
||||||
|
// Get translation function from request
|
||||||
|
const t = (req as any).t;
|
||||||
|
|
||||||
// Validate request
|
// Validate request
|
||||||
const errors = validationResult(req);
|
const errors = validationResult(req);
|
||||||
if (!errors.isEmpty()) {
|
if (!errors.isEmpty()) {
|
||||||
res.status(400).json({ success: false, errors: errors.array() });
|
res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: t('api.errors.validation_failed'),
|
||||||
|
errors: errors.array(),
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,7 +39,10 @@ export const login = async (req: Request, res: Response): Promise<void> => {
|
|||||||
const user = findUserByUsername(username);
|
const user = findUserByUsername(username);
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
res.status(401).json({ success: false, message: 'Invalid credentials' });
|
res.status(401).json({
|
||||||
|
success: false,
|
||||||
|
message: t('api.errors.invalid_credentials'),
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,7 +50,10 @@ export const login = async (req: Request, res: Response): Promise<void> => {
|
|||||||
const isPasswordValid = await verifyPassword(password, user.password);
|
const isPasswordValid = await verifyPassword(password, user.password);
|
||||||
|
|
||||||
if (!isPasswordValid) {
|
if (!isPasswordValid) {
|
||||||
res.status(401).json({ success: false, message: 'Invalid credentials' });
|
res.status(401).json({
|
||||||
|
success: false,
|
||||||
|
message: t('api.errors.invalid_credentials'),
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,6 +69,7 @@ export const login = async (req: Request, res: Response): Promise<void> => {
|
|||||||
if (err) throw err;
|
if (err) throw err;
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
|
message: t('api.success.login_successful'),
|
||||||
token,
|
token,
|
||||||
user: {
|
user: {
|
||||||
username: user.username,
|
username: user.username,
|
||||||
@@ -66,16 +80,26 @@ export const login = async (req: Request, res: Response): Promise<void> => {
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Login error:', error);
|
console.error('Login error:', error);
|
||||||
res.status(500).json({ success: false, message: 'Server error' });
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: t('api.errors.server_error'),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Register new user
|
// Register new user
|
||||||
export const register = async (req: Request, res: Response): Promise<void> => {
|
export const register = async (req: Request, res: Response): Promise<void> => {
|
||||||
|
// Get translation function from request
|
||||||
|
const t = (req as any).t;
|
||||||
|
|
||||||
// Validate request
|
// Validate request
|
||||||
const errors = validationResult(req);
|
const errors = validationResult(req);
|
||||||
if (!errors.isEmpty()) {
|
if (!errors.isEmpty()) {
|
||||||
res.status(400).json({ success: false, errors: errors.array() });
|
res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: t('api.errors.validation_failed'),
|
||||||
|
errors: errors.array(),
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Request, Response, NextFunction } from 'express';
|
import { Request, Response, NextFunction } from 'express';
|
||||||
import jwt from 'jsonwebtoken';
|
import jwt from 'jsonwebtoken';
|
||||||
import { loadSettings } from '../config/index.js';
|
import { loadSettings } from '../config/index.js';
|
||||||
|
import defaultConfig from '../config/index.js';
|
||||||
|
|
||||||
// Default secret key - in production, use an environment variable
|
// Default secret key - in production, use an environment variable
|
||||||
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key-change-this';
|
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key-change-this';
|
||||||
@@ -18,8 +19,30 @@ const validateBearerAuth = (req: Request, routingConfig: any): boolean => {
|
|||||||
return authHeader.substring(7) === routingConfig.bearerAuthKey;
|
return authHeader.substring(7) === routingConfig.bearerAuthKey;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const readonlyAllowPaths = ['/tools/call/'];
|
||||||
|
|
||||||
|
const checkReadonly = (req: Request): boolean => {
|
||||||
|
if (!defaultConfig.readonly) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const path of readonlyAllowPaths) {
|
||||||
|
if (req.path.startsWith(defaultConfig.basePath + path)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return req.method === 'GET';
|
||||||
|
};
|
||||||
|
|
||||||
// Middleware to authenticate JWT token
|
// Middleware to authenticate JWT token
|
||||||
export const auth = (req: Request, res: Response, next: NextFunction): void => {
|
export const auth = (req: Request, res: Response, next: NextFunction): void => {
|
||||||
|
const t = (req as any).t;
|
||||||
|
if (!checkReadonly(req)) {
|
||||||
|
res.status(403).json({ success: false, message: t('api.errors.readonly') });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if authentication is disabled globally
|
// Check if authentication is disabled globally
|
||||||
const routingConfig = loadSettings().systemConfig?.routing || {
|
const routingConfig = loadSettings().systemConfig?.routing || {
|
||||||
enableGlobalRoute: true,
|
enableGlobalRoute: true,
|
||||||
|
|||||||
41
src/middlewares/i18n.ts
Normal file
41
src/middlewares/i18n.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
import { getT } from '../utils/i18n.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* i18n middleware to detect user language and attach translation function to request
|
||||||
|
*/
|
||||||
|
export const i18nMiddleware = (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
// Detect language from various sources (prioritized)
|
||||||
|
const acceptLanguage = req.headers['accept-language'];
|
||||||
|
const customLanguageHeader = req.headers['x-language'] as string;
|
||||||
|
const languageFromQuery = req.query.lang as string;
|
||||||
|
|
||||||
|
// Default to English
|
||||||
|
let detectedLanguage = 'en';
|
||||||
|
|
||||||
|
// Priority order: query parameter > custom header > accept-language header
|
||||||
|
if (languageFromQuery) {
|
||||||
|
detectedLanguage = languageFromQuery;
|
||||||
|
} else if (customLanguageHeader) {
|
||||||
|
detectedLanguage = customLanguageHeader;
|
||||||
|
} else if (acceptLanguage) {
|
||||||
|
// Parse accept-language header and get primary language
|
||||||
|
const primaryLanguage = acceptLanguage.split(',')[0].split('-')[0].trim();
|
||||||
|
detectedLanguage = primaryLanguage;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize language code (ensure we support it)
|
||||||
|
const supportedLanguages = ['en', 'zh'];
|
||||||
|
if (!supportedLanguages.includes(detectedLanguage)) {
|
||||||
|
detectedLanguage = 'en'; // fallback to English
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set language in request (using any type to avoid TypeScript issues)
|
||||||
|
(req as any).language = detectedLanguage;
|
||||||
|
|
||||||
|
// Get translation function for the detected language
|
||||||
|
const t = getT(detectedLanguage);
|
||||||
|
(req as any).t = t;
|
||||||
|
|
||||||
|
next();
|
||||||
|
};
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import express, { Request, Response, NextFunction } from 'express';
|
import express, { Request, Response, NextFunction } from 'express';
|
||||||
import { auth } from './auth.js';
|
import { auth } from './auth.js';
|
||||||
import { userContextMiddleware } from './userContext.js';
|
import { userContextMiddleware } from './userContext.js';
|
||||||
|
import { i18nMiddleware } from './i18n.js';
|
||||||
import { initializeDefaultUser } from '../models/User.js';
|
import { initializeDefaultUser } from '../models/User.js';
|
||||||
import config from '../config/index.js';
|
import config from '../config/index.js';
|
||||||
|
|
||||||
@@ -18,6 +19,9 @@ export const errorHandler = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const initMiddlewares = (app: express.Application): void => {
|
export const initMiddlewares = (app: express.Application): void => {
|
||||||
|
// Apply i18n middleware first to detect language for all requests
|
||||||
|
app.use(i18nMiddleware);
|
||||||
|
|
||||||
// Serve static files from the dynamically determined frontend path
|
// Serve static files from the dynamically determined frontend path
|
||||||
// Note: Static files will be handled by the server directly, not here
|
// Note: Static files will be handled by the server directly, not here
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import fs from 'fs';
|
|||||||
import { initUpstreamServers, connected } from './services/mcpService.js';
|
import { initUpstreamServers, connected } from './services/mcpService.js';
|
||||||
import { initMiddlewares } from './middlewares/index.js';
|
import { initMiddlewares } from './middlewares/index.js';
|
||||||
import { initRoutes } from './routes/index.js';
|
import { initRoutes } from './routes/index.js';
|
||||||
|
import { initI18n } from './utils/i18n.js';
|
||||||
import {
|
import {
|
||||||
handleSseConnection,
|
handleSseConnection,
|
||||||
handleSseMessage,
|
handleSseMessage,
|
||||||
@@ -31,6 +32,10 @@ export class AppServer {
|
|||||||
|
|
||||||
async initialize(): Promise<void> {
|
async initialize(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
|
// Initialize i18n before other components
|
||||||
|
await initI18n();
|
||||||
|
console.log('i18n initialized successfully');
|
||||||
|
|
||||||
// Initialize default admin user if no users exist
|
// Initialize default admin user if no users exist
|
||||||
await initializeDefaultUser();
|
await initializeDefaultUser();
|
||||||
|
|
||||||
|
|||||||
5
src/types/express.d.ts
vendored
Normal file
5
src/types/express.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
// Custom types for Express Request
|
||||||
|
export interface I18nRequest extends Request {
|
||||||
|
language?: string;
|
||||||
|
t: (key: string, options?: any) => string;
|
||||||
|
}
|
||||||
41
src/utils/i18n.ts
Normal file
41
src/utils/i18n.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import i18n from 'i18next';
|
||||||
|
import Backend from 'i18next-fs-backend';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
// Initialize i18n for backend
|
||||||
|
const initI18n = async () => {
|
||||||
|
return i18n.use(Backend).init({
|
||||||
|
lng: 'en', // default language
|
||||||
|
fallbackLng: 'en',
|
||||||
|
|
||||||
|
backend: {
|
||||||
|
// Path to translation files
|
||||||
|
loadPath: path.join(process.cwd(), 'locales', '{{lng}}.json'),
|
||||||
|
},
|
||||||
|
|
||||||
|
interpolation: {
|
||||||
|
escapeValue: false, // not needed for server side
|
||||||
|
},
|
||||||
|
|
||||||
|
// Enable debug mode for development
|
||||||
|
debug: false,
|
||||||
|
|
||||||
|
// Preload languages
|
||||||
|
preload: ['en', 'zh'],
|
||||||
|
|
||||||
|
// Use sync mode for server
|
||||||
|
initImmediate: false,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get translation function for a specific language
|
||||||
|
export const getT = (language?: string) => {
|
||||||
|
if (language && language !== i18n.language) {
|
||||||
|
i18n.changeLanguage(language);
|
||||||
|
}
|
||||||
|
return i18n.t.bind(i18n);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize and export
|
||||||
|
export { initI18n };
|
||||||
|
export default i18n;
|
||||||
Reference in New Issue
Block a user