From 63b356b8d747d2697abec8fe5c87c2c09ff99ec3 Mon Sep 17 00:00:00 2001 From: samanhappy Date: Sun, 3 Aug 2025 11:53:04 +0800 Subject: [PATCH] Add Chinese localization support and i18n middleware (#253) --- Dockerfile | 3 + frontend/src/components/AddGroupForm.tsx | 5 +- frontend/src/components/AddServerForm.tsx | 20 +-- frontend/src/components/DxtUploadForm.tsx | 45 ++---- frontend/src/components/EditGroupForm.tsx | 4 +- frontend/src/components/EditServerForm.tsx | 20 +-- frontend/src/hooks/useGroupData.ts | 126 ++++----------- frontend/src/hooks/useMarketData.ts | 145 ++++------------- frontend/src/hooks/useServerData.ts | 57 ++----- frontend/src/hooks/useSettingsData.ts | 124 +++------------ frontend/src/i18n.ts | 20 +-- frontend/src/main.tsx | 2 + frontend/src/pages/GroupsPage.tsx | 6 +- frontend/src/pages/ServersPage.tsx | 1 - frontend/src/services/authService.ts | 86 +++------- frontend/src/services/configService.ts | 17 +- frontend/src/services/logService.ts | 34 ++-- frontend/src/services/toolService.ts | 82 ++++------ frontend/src/utils/fetchInterceptor.ts | 174 +++++++++++++++++++++ frontend/src/utils/interceptors.ts | 99 ++++++++++++ frontend/src/utils/setupInterceptors.ts | 19 +++ {frontend/src/locales => locales}/en.json | 58 +++++++ {frontend/src/locales => locales}/zh.json | 58 +++++++ package.json | 1 + pnpm-lock.yaml | 8 + src/config/index.ts | 1 + src/controllers/authController.ts | 34 +++- src/middlewares/auth.ts | 23 +++ src/middlewares/i18n.ts | 41 +++++ src/middlewares/index.ts | 4 + src/server.ts | 5 + src/types/express.d.ts | 5 + src/utils/i18n.ts | 41 +++++ 33 files changed, 766 insertions(+), 602 deletions(-) create mode 100644 frontend/src/utils/fetchInterceptor.ts create mode 100644 frontend/src/utils/interceptors.ts create mode 100644 frontend/src/utils/setupInterceptors.ts rename {frontend/src/locales => locales}/en.json (83%) rename {frontend/src/locales => locales}/zh.json (84%) create mode 100644 src/middlewares/i18n.ts create mode 100644 src/types/express.d.ts create mode 100644 src/utils/i18n.ts diff --git a/Dockerfile b/Dockerfile index 7eff4a5..271ddb0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,6 +21,9 @@ ENV REQUEST_TIMEOUT=$REQUEST_TIMEOUT ARG BASE_PATH="" ENV BASE_PATH=$BASE_PATH +ARG READONLY=false +ENV READONLY=$READONLY + ENV PNPM_HOME=/usr/local/share/pnpm ENV PATH=$PNPM_HOME:$PATH RUN mkdir -p $PNPM_HOME && \ diff --git a/frontend/src/components/AddGroupForm.tsx b/frontend/src/components/AddGroupForm.tsx index 98c6c85..bffc09b 100644 --- a/frontend/src/components/AddGroupForm.tsx +++ b/frontend/src/components/AddGroupForm.tsx @@ -50,9 +50,8 @@ const AddGroupForm = ({ onAdd, onCancel }: AddGroupFormProps) => { } const result = await createGroup(formData.name, formData.description, formData.servers) - - if (!result) { - setError(t('groups.createError')) + if (!result || !result.success) { + setError(result?.message || t('groups.createError')) setIsSubmitting(false) return } diff --git a/frontend/src/components/AddServerForm.tsx b/frontend/src/components/AddServerForm.tsx index 05169e7..d6fbed4 100644 --- a/frontend/src/components/AddServerForm.tsx +++ b/frontend/src/components/AddServerForm.tsx @@ -1,7 +1,7 @@ import { useState } from 'react' import { useTranslation } from 'react-i18next' import ServerForm from './ServerForm' -import { getApiUrl } from '../utils/runtime' +import { apiPost } from '../utils/fetchInterceptor' import { detectVariables } from '../utils/variableDetection' interface AddServerFormProps { @@ -34,26 +34,12 @@ const AddServerForm = ({ onAdd }: AddServerFormProps) => { const submitServer = async (payload: any) => { try { setError(null) - const token = localStorage.getItem('mcphub_token'); - const response = await fetch(getApiUrl('/servers'), { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'x-auth-token': token || '' - }, - body: JSON.stringify(payload), - }) + const result = await apiPost('/servers', payload) - const result = await response.json() - - if (!response.ok) { + if (!result.success) { // Use specific error message from the response if available if (result && 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 { setError(t('server.addError')) } diff --git a/frontend/src/components/DxtUploadForm.tsx b/frontend/src/components/DxtUploadForm.tsx index 4b5d760..490a50b 100644 --- a/frontend/src/components/DxtUploadForm.tsx +++ b/frontend/src/components/DxtUploadForm.tsx @@ -1,5 +1,6 @@ import React, { useState } from 'react'; import { useTranslation } from 'react-i18next'; +import { apiPost, apiGet, apiPut, fetchWithInterceptors } from '@/utils/fetchInterceptor'; import { getApiUrl } from '@/utils/runtime'; import ConfirmDialog from '@/components/ui/ConfirmDialog'; @@ -81,12 +82,8 @@ const DxtUploadForm: React.FC = ({ onSuccess, onCancel }) => const formData = new FormData(); formData.append('dxtFile', selectedFile); - const token = localStorage.getItem('mcphub_token'); - const response = await fetch(getApiUrl('/dxt/upload'), { + const response = await fetchWithInterceptors(getApiUrl('/dxt/upload'), { method: 'POST', - headers: { - 'x-auth-token': token || '', - }, body: formData, }); @@ -119,19 +116,11 @@ const DxtUploadForm: React.FC = ({ onSuccess, onCancel }) => // Convert DXT manifest to MCPHub stdio server configuration const serverConfig = convertDxtToMcpConfig(manifestData, extractDir, serverName); - const token = localStorage.getItem('mcphub_token'); - // First, check if server exists if (!forceOverride) { - const checkResponse = await fetch(getApiUrl('/servers'), { - method: 'GET', - headers: { - 'x-auth-token': token || '', - }, - }); + const checkResult = await apiGet('/servers'); - if (checkResponse.ok) { - const checkResult = await checkResponse.json(); + if (checkResult.success) { const existingServer = checkResult.data?.find((server: any) => server.name === serverName); if (existingServer) { @@ -145,25 +134,17 @@ const DxtUploadForm: React.FC = ({ onSuccess, onCancel }) => } // Install or override the server - const method = forceOverride ? 'PUT' : 'POST'; - const url = forceOverride ? getApiUrl(`/servers/${encodeURIComponent(serverName)}`) : getApiUrl('/servers'); - - const response = await fetch(url, { - method, - headers: { - 'Content-Type': 'application/json', - 'x-auth-token': token || '', - }, - body: JSON.stringify({ + let result; + if (forceOverride) { + result = await apiPut(`/servers/${encodeURIComponent(serverName)}`, { name: serverName, config: serverConfig, - }), - }); - - const result = await response.json(); - - if (!response.ok) { - throw new Error(result.message || `HTTP error! Status: ${response.status}`); + }); + } else { + result = await apiPost('/servers', { + name: serverName, + config: serverConfig, + }); } if (result.success) { diff --git a/frontend/src/components/EditGroupForm.tsx b/frontend/src/components/EditGroupForm.tsx index 3413e8a..75cf71c 100644 --- a/frontend/src/components/EditGroupForm.tsx +++ b/frontend/src/components/EditGroupForm.tsx @@ -56,8 +56,8 @@ const EditGroupForm = ({ group, onEdit, onCancel }: EditGroupFormProps) => { servers: formData.servers }) - if (!result) { - setError(t('groups.updateError')) + if (!result || !result.success) { + setError(result?.message || t('groups.updateError')) setIsSubmitting(false) return } diff --git a/frontend/src/components/EditServerForm.tsx b/frontend/src/components/EditServerForm.tsx index fce9cb4..47bf39b 100644 --- a/frontend/src/components/EditServerForm.tsx +++ b/frontend/src/components/EditServerForm.tsx @@ -1,7 +1,7 @@ import { useState } from 'react' import { useTranslation } from 'react-i18next' import { Server } from '@/types' -import { getApiUrl } from '../utils/runtime' +import { apiPut } from '../utils/fetchInterceptor' import ServerForm from './ServerForm' interface EditServerFormProps { @@ -17,26 +17,12 @@ const EditServerForm = ({ server, onEdit, onCancel }: EditServerFormProps) => { const handleSubmit = async (payload: any) => { try { setError(null) - const token = localStorage.getItem('mcphub_token'); - 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 apiPut(`/servers/${server.name}`, payload) - const result = await response.json() - - if (!response.ok) { + if (!result.success) { // Use specific error message from the response if available if (result && 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 { setError(t('server.updateError', { serverName: server.name })) } diff --git a/frontend/src/hooks/useGroupData.ts b/frontend/src/hooks/useGroupData.ts index a550b2a..dc7f5c9 100644 --- a/frontend/src/hooks/useGroupData.ts +++ b/frontend/src/hooks/useGroupData.ts @@ -1,7 +1,7 @@ import { useState, useEffect, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { Group, ApiResponse, IGroupServerConfig } from '@/types'; -import { getApiUrl } from '../utils/runtime'; +import { apiGet, apiPost, apiPut, apiDelete } from '../utils/fetchInterceptor'; export const useGroupData = () => { const { t } = useTranslation(); @@ -13,18 +13,7 @@ export const useGroupData = () => { const fetchGroups = useCallback(async () => { try { setLoading(true); - const token = localStorage.getItem('mcphub_token'); - const response = await fetch(getApiUrl('/groups'), { - headers: { - 'x-auth-token': token || '', - }, - }); - - if (!response.ok) { - throw new Error(`Status: ${response.status}`); - } - - const data: ApiResponse = await response.json(); + const data: ApiResponse = await apiGet('/groups'); if (data && data.success && Array.isArray(data.data)) { setGroups(data.data); @@ -55,25 +44,16 @@ export const useGroupData = () => { servers: string[] | IGroupServerConfig[] = [], ) => { try { - const token = localStorage.getItem('mcphub_token'); - const response = await fetch(getApiUrl('/groups'), { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'x-auth-token': token || '', - }, - body: JSON.stringify({ name, description, servers }), - }); + const result: ApiResponse = await apiPost('/groups', { name, description, servers }); + console.log('Group created successfully:', result); - const result: ApiResponse = await response.json(); - - if (!response.ok) { - setError(result.message || t('groups.createError')); - return null; + if (!result || !result.success) { + setError(result?.message || t('groups.createError')); + return result; } triggerRefresh(); - return result.data || null; + return result || null; } catch (err) { setError(err instanceof Error ? err.message : 'Failed to create group'); return null; @@ -86,25 +66,14 @@ export const useGroupData = () => { data: { name?: string; description?: string; servers?: string[] | IGroupServerConfig[] }, ) => { try { - const token = localStorage.getItem('mcphub_token'); - const response = await fetch(getApiUrl(`/groups/${id}`), { - method: 'PUT', - headers: { - 'Content-Type': 'application/json', - 'x-auth-token': token || '', - }, - body: JSON.stringify(data), - }); - - const result: ApiResponse = await response.json(); - - if (!response.ok) { - setError(result.message || t('groups.updateError')); - return null; + const result: ApiResponse = await apiPut(`/groups/${id}`, data); + if (!result || !result.success) { + setError(result?.message || t('groups.updateError')); + return result; } triggerRefresh(); - return result.data || null; + return result || null; } catch (err) { setError(err instanceof Error ? err.message : 'Failed to update group'); return null; @@ -114,20 +83,12 @@ export const useGroupData = () => { // Update servers in a group (for batch updates) const updateGroupServers = async (groupId: string, servers: string[] | IGroupServerConfig[]) => { try { - const token = localStorage.getItem('mcphub_token'); - const response = await fetch(getApiUrl(`/groups/${groupId}/servers/batch`), { - method: 'PUT', - headers: { - 'Content-Type': 'application/json', - 'x-auth-token': token || '', - }, - body: JSON.stringify({ servers }), + const result: ApiResponse = await apiPut(`/groups/${groupId}/servers/batch`, { + servers, }); - const result: ApiResponse = await response.json(); - - if (!response.ok) { - setError(result.message || t('groups.updateError')); + if (!result || !result.success) { + setError(result?.message || t('groups.updateError')); return null; } @@ -142,46 +103,29 @@ export const useGroupData = () => { // Delete a group const deleteGroup = async (id: string) => { try { - const token = localStorage.getItem('mcphub_token'); - const response = await fetch(getApiUrl(`/groups/${id}`), { - method: 'DELETE', - headers: { - 'x-auth-token': token || '', - }, - }); - - const result = await response.json(); - - if (!response.ok) { - setError(result.message || t('groups.deleteError')); - return false; + const result = await apiDelete(`/groups/${id}`); + if (!result || !result.success) { + setError(result?.message || t('groups.deleteError')); + return result; } triggerRefresh(); - return true; + return result; } catch (err) { setError(err instanceof Error ? err.message : 'Failed to delete group'); - return false; + return null; } }; // Add server to a group const addServerToGroup = async (groupId: string, serverName: string) => { try { - const token = localStorage.getItem('mcphub_token'); - const response = await fetch(getApiUrl(`/groups/${groupId}/servers`), { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'x-auth-token': token || '', - }, - body: JSON.stringify({ serverName }), + const result: ApiResponse = await apiPost(`/groups/${groupId}/servers`, { + serverName, }); - const result: ApiResponse = await response.json(); - - if (!response.ok) { - setError(result.message || t('groups.serverAddError')); + if (!result || !result.success) { + setError(result?.message || t('groups.serverAddError')); return null; } @@ -196,18 +140,12 @@ export const useGroupData = () => { // Remove server from group const removeServerFromGroup = async (groupId: string, serverName: string) => { try { - const token = localStorage.getItem('mcphub_token'); - const response = await fetch(getApiUrl(`/groups/${groupId}/servers/${serverName}`), { - method: 'DELETE', - headers: { - 'x-auth-token': token || '', - }, - }); + const result: ApiResponse = await apiDelete( + `/groups/${groupId}/servers/${serverName}`, + ); - const result: ApiResponse = await response.json(); - - if (!response.ok) { - setError(result.message || t('groups.serverRemoveError')); + if (!result || !result.success) { + setError(result?.message || t('groups.serverRemoveError')); return null; } diff --git a/frontend/src/hooks/useMarketData.ts b/frontend/src/hooks/useMarketData.ts index a6a6c87..2b54c52 100644 --- a/frontend/src/hooks/useMarketData.ts +++ b/frontend/src/hooks/useMarketData.ts @@ -1,7 +1,7 @@ import { useState, useEffect, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { MarketServer, ApiResponse, ServerConfig } from '@/types'; -import { getApiUrl } from '../utils/runtime'; +import { apiGet, apiPost } from '../utils/fetchInterceptor'; export const useMarketData = () => { const { t } = useTranslation(); @@ -26,18 +26,7 @@ export const useMarketData = () => { const fetchMarketServers = useCallback(async () => { try { setLoading(true); - const token = localStorage.getItem('mcphub_token'); - const response = await fetch(getApiUrl('/market/servers'), { - headers: { - 'x-auth-token': token || '', - }, - }); - - if (!response.ok) { - throw new Error(`Status: ${response.status}`); - } - - const data: ApiResponse = await response.json(); + const data: ApiResponse = await apiGet('/market/servers'); if (data && data.success && Array.isArray(data.data)) { setAllServers(data.data); @@ -87,18 +76,7 @@ export const useMarketData = () => { // Fetch all categories const fetchCategories = useCallback(async () => { try { - const token = localStorage.getItem('mcphub_token'); - const response = await fetch(getApiUrl('/market/categories'), { - headers: { - 'x-auth-token': token || '', - }, - }); - - if (!response.ok) { - throw new Error(`Status: ${response.status}`); - } - - const data: ApiResponse = await response.json(); + const data: ApiResponse = await apiGet('/market/categories'); if (data && data.success && Array.isArray(data.data)) { setCategories(data.data); @@ -113,18 +91,7 @@ export const useMarketData = () => { // Fetch all tags const fetchTags = useCallback(async () => { try { - const token = localStorage.getItem('mcphub_token'); - const response = await fetch(getApiUrl('/market/tags'), { - headers: { - 'x-auth-token': token || '', - }, - }); - - if (!response.ok) { - throw new Error(`Status: ${response.status}`); - } - - const data: ApiResponse = await response.json(); + const data: ApiResponse = await apiGet('/market/tags'); if (data && data.success && Array.isArray(data.data)) { setTags(data.data); @@ -141,18 +108,7 @@ export const useMarketData = () => { async (name: string) => { try { setLoading(true); - const token = localStorage.getItem('mcphub_token'); - 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 = await response.json(); + const data: ApiResponse = await apiGet(`/market/servers/${name}`); if (data && data.success && data.data) { setCurrentServer(data.data); @@ -186,22 +142,10 @@ export const useMarketData = () => { return; } - const token = localStorage.getItem('mcphub_token'); - const response = await fetch( - getApiUrl(`/market/servers/search?query=${encodeURIComponent(query)}`), - { - headers: { - 'x-auth-token': token || '', - }, - }, + const data: ApiResponse = await apiGet( + `/market/servers/search?query=${encodeURIComponent(query)}`, ); - if (!response.ok) { - throw new Error(`Status: ${response.status}`); - } - - const data: ApiResponse = await response.json(); - if (data && data.success && Array.isArray(data.data)) { setAllServers(data.data); setCurrentPage(1); @@ -233,22 +177,10 @@ export const useMarketData = () => { return; } - const token = localStorage.getItem('mcphub_token'); - const response = await fetch( - getApiUrl(`/market/categories/${encodeURIComponent(category)}`), - { - headers: { - 'x-auth-token': token || '', - }, - }, + const data: ApiResponse = await apiGet( + `/market/categories/${encodeURIComponent(category)}`, ); - if (!response.ok) { - throw new Error(`Status: ${response.status}`); - } - - const data: ApiResponse = await response.json(); - if (data && data.success && Array.isArray(data.data)) { setAllServers(data.data); setCurrentPage(1); @@ -280,18 +212,9 @@ export const useMarketData = () => { return; } - const token = localStorage.getItem('mcphub_token'); - const response = await fetch(getApiUrl(`/market/tags/${encodeURIComponent(tag)}`), { - headers: { - 'x-auth-token': token || '', - }, - }); - - if (!response.ok) { - throw new Error(`Status: ${response.status}`); - } - - const data: ApiResponse = await response.json(); + const data: ApiResponse = await apiGet( + `/market/tags/${encodeURIComponent(tag)}`, + ); if (data && data.success && Array.isArray(data.data)) { setAllServers(data.data); @@ -314,18 +237,7 @@ export const useMarketData = () => { // Fetch installed servers const fetchInstalledServers = useCallback(async () => { try { - const token = localStorage.getItem('mcphub_token'); - 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(); + const data = await apiGet<{ success: boolean; data: any[] }>('/servers'); if (data && data.success && Array.isArray(data.data)) { // Extract server names @@ -365,27 +277,24 @@ export const useMarketData = () => { // Prepare server configuration, merging with customConfig const serverConfig = { name: server.name, - config: customConfig.type === 'stdio' ? { - command: customConfig.command || installation.command || '', - args: customConfig.args || installation.args || [], - env: { ...installation.env, ...customConfig.env }, - } : customConfig + config: + customConfig.type === 'stdio' + ? { + command: customConfig.command || installation.command || '', + args: customConfig.args || installation.args || [], + env: { ...installation.env, ...customConfig.env }, + } + : customConfig, }; // Call the createServer API - const token = localStorage.getItem('mcphub_token'); - const response = await fetch(getApiUrl('/servers'), { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'x-auth-token': token || '', - }, - body: JSON.stringify(serverConfig), - }); + const result = await apiPost<{ success: boolean; message?: string }>( + '/servers', + serverConfig, + ); - if (!response.ok) { - const errorData = await response.json(); - throw new Error(errorData.message || `Status: ${response.status}`); + if (!result.success) { + throw new Error(result.message || 'Failed to install server'); } // Update installed servers list after successful installation diff --git a/frontend/src/hooks/useServerData.ts b/frontend/src/hooks/useServerData.ts index 49db4e6..4804d46 100644 --- a/frontend/src/hooks/useServerData.ts +++ b/frontend/src/hooks/useServerData.ts @@ -1,7 +1,7 @@ import { useState, useEffect, useRef, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { Server, ApiResponse } from '@/types'; -import { getApiUrl } from '../utils/runtime'; +import { apiGet, apiPost, apiDelete } from '../utils/fetchInterceptor'; // Configuration options const CONFIG = { @@ -44,13 +44,7 @@ export const useServerData = () => { const fetchServers = async () => { try { - const token = localStorage.getItem('mcphub_token'); - const response = await fetch(getApiUrl('/servers'), { - headers: { - 'x-auth-token': token || '', - }, - }); - const data = await response.json(); + const data = await apiGet('/servers'); if (data && data.success && Array.isArray(data.data)) { setServers(data.data); @@ -97,13 +91,7 @@ export const useServerData = () => { // Initialization phase request function const fetchInitialData = async () => { try { - const token = localStorage.getItem('mcphub_token'); - const response = await fetch(getApiUrl('/servers'), { - headers: { - 'x-auth-token': token || '', - }, - }); - const data = await response.json(); + const data = await apiGet('/servers'); // Handle API response wrapper object, extract data field if (data && data.success && Array.isArray(data.data)) { @@ -203,14 +191,8 @@ export const useServerData = () => { const handleServerEdit = async (server: Server) => { try { // Fetch settings to get the full server config before editing - const token = localStorage.getItem('mcphub_token'); - const response = await fetch(getApiUrl('/settings'), { - headers: { - 'x-auth-token': token || '', - }, - }); - - const settingsData: ApiResponse<{ mcpServers: Record }> = await response.json(); + const settingsData: ApiResponse<{ mcpServers: Record }> = + await apiGet('/settings'); if ( settingsData && @@ -240,17 +222,10 @@ export const useServerData = () => { const handleServerRemove = async (serverName: string) => { try { - const token = localStorage.getItem('mcphub_token'); - const response = await fetch(getApiUrl(`/servers/${serverName}`), { - method: 'DELETE', - headers: { - 'x-auth-token': token || '', - }, - }); - const result = await response.json(); + const result = await apiDelete(`/servers/${serverName}`); - if (!response.ok) { - setError(result.message || t('server.deleteError', { serverName })); + if (!result || !result.success) { + setError(result?.message || t('server.deleteError', { serverName })); return false; } @@ -264,21 +239,11 @@ export const useServerData = () => { const handleServerToggle = async (server: Server, enabled: boolean) => { try { - const token = localStorage.getItem('mcphub_token'); - 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 apiPost(`/servers/${server.name}/toggle`, { enabled }); - const result = await response.json(); - - if (!response.ok) { + if (!result || !result.success) { 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; } diff --git a/frontend/src/hooks/useSettingsData.ts b/frontend/src/hooks/useSettingsData.ts index 411de9e..ea8beab 100644 --- a/frontend/src/hooks/useSettingsData.ts +++ b/frontend/src/hooks/useSettingsData.ts @@ -2,7 +2,7 @@ import { useState, useCallback, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { ApiResponse } from '@/types'; import { useToast } from '@/contexts/ToastContext'; -import { getApiUrl } from '../utils/runtime'; +import { apiGet, apiPut } from '../utils/fetchInterceptor'; // Define types for the settings data interface RoutingConfig { @@ -84,18 +84,7 @@ export const useSettingsData = () => { setError(null); try { - const token = localStorage.getItem('mcphub_token'); - 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 = await response.json(); + const data: ApiResponse = await apiGet('/settings'); if (data.success && data.data?.systemConfig?.routing) { setRoutingConfig({ @@ -134,34 +123,17 @@ export const useSettingsData = () => { }, [t]); // 移除 showToast 依赖 // Update routing configuration - const updateRoutingConfig = async ( - key: T, - value: RoutingConfig[T], - ) => { + const updateRoutingConfig = async (key: keyof RoutingConfig, value: any) => { setLoading(true); setError(null); try { - const token = localStorage.getItem('mcphub_token'); - const response = await fetch(getApiUrl('/system-config'), { - method: 'PUT', - headers: { - 'Content-Type': 'application/json', - 'x-auth-token': token || '', + const data = await apiPut('/system-config', { + routing: { + [key]: value, }, - 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) { setRoutingConfig({ ...routingConfig, @@ -170,7 +142,7 @@ export const useSettingsData = () => { showToast(t('settings.systemConfigUpdated')); return true; } else { - showToast(t('errors.failedToUpdateRouteConfig')); + showToast(data.message || t('errors.failedToUpdateRouteConfig')); return false; } } catch (error) { @@ -189,26 +161,12 @@ export const useSettingsData = () => { setError(null); try { - const token = localStorage.getItem('mcphub_token'); - const response = await fetch(getApiUrl('/system-config'), { - method: 'PUT', - headers: { - 'Content-Type': 'application/json', - 'x-auth-token': token || '', + const data = await apiPut('/system-config', { + install: { + [key]: value, }, - 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) { setInstallConfig({ ...installConfig, @@ -217,7 +175,7 @@ export const useSettingsData = () => { showToast(t('settings.systemConfigUpdated')); return true; } else { - showToast(t('errors.failedToUpdateSystemConfig')); + showToast(data.message || t('errors.failedToUpdateSystemConfig')); return false; } } catch (error) { @@ -239,27 +197,12 @@ export const useSettingsData = () => { setError(null); try { - const token = localStorage.getItem('mcphub_token'); - const response = await fetch(getApiUrl('/system-config'), { - method: 'PUT', - headers: { - 'Content-Type': 'application/json', - 'x-auth-token': token || '', + const data = await apiPut('/system-config', { + smartRouting: { + [key]: value, }, - 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) { setSmartRoutingConfig({ ...smartRoutingConfig, @@ -289,25 +232,10 @@ export const useSettingsData = () => { setError(null); try { - const token = localStorage.getItem('mcphub_token'); - const response = await fetch(getApiUrl('/system-config'), { - method: 'PUT', - headers: { - 'Content-Type': 'application/json', - 'x-auth-token': token || '', - }, - body: JSON.stringify({ - smartRouting: updates, - }), + const data = await apiPut('/system-config', { + 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) { setSmartRoutingConfig({ ...smartRoutingConfig, @@ -337,24 +265,10 @@ export const useSettingsData = () => { setError(null); try { - const token = localStorage.getItem('mcphub_token'); - const response = await fetch(getApiUrl('/system-config'), { - method: 'PUT', - headers: { - 'Content-Type': 'application/json', - 'x-auth-token': token || '', - }, - body: JSON.stringify({ - routing: updates, - }), + const data = await apiPut('/system-config', { + routing: updates, }); - if (!response.ok) { - throw new Error(`HTTP error! Status: ${response.status}`); - } - - const data = await response.json(); - if (data.success) { setRoutingConfig({ ...routingConfig, @@ -363,7 +277,7 @@ export const useSettingsData = () => { showToast(t('settings.systemConfigUpdated')); return true; } else { - showToast(t('errors.failedToUpdateRouteConfig')); + showToast(data.message || t('errors.failedToUpdateRouteConfig')); return false; } } catch (error) { diff --git a/frontend/src/i18n.ts b/frontend/src/i18n.ts index 5a542ee..6840055 100644 --- a/frontend/src/i18n.ts +++ b/frontend/src/i18n.ts @@ -2,9 +2,9 @@ import i18n from 'i18next'; import { initReactI18next } from 'react-i18next'; import LanguageDetector from 'i18next-browser-languagedetector'; -// Import translations -import enTranslation from './locales/en.json'; -import zhTranslation from './locales/zh.json'; +// Import shared translations from root locales directory +import enTranslation from '../../locales/en.json'; +import zhTranslation from '../../locales/zh.json'; i18n // Detect user language @@ -15,18 +15,18 @@ i18n .init({ resources: { en: { - translation: enTranslation + translation: enTranslation, }, zh: { - translation: zhTranslation - } + translation: zhTranslation, + }, }, fallbackLng: 'en', debug: process.env.NODE_ENV === 'development', - + // Common namespace used for all translations defaultNS: 'translation', - + interpolation: { escapeValue: false, // React already safe from XSS }, @@ -36,7 +36,7 @@ i18n order: ['localStorage', 'cookie', 'htmlTag', 'navigator'], // Cache the language in localStorage caches: ['localStorage', 'cookie'], - } + }, }); -export default i18n; \ No newline at end of file +export default i18n; diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index f1cd579..194354b 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -4,6 +4,8 @@ import App from './App'; import './index.css'; // Import the i18n configuration import './i18n'; +// Setup fetch interceptors +import './utils/setupInterceptors'; import { loadRuntimeConfig } from './utils/runtime'; // Load runtime configuration before starting the app diff --git a/frontend/src/pages/GroupsPage.tsx b/frontend/src/pages/GroupsPage.tsx index 4e8579f..c9e724c 100644 --- a/frontend/src/pages/GroupsPage.tsx +++ b/frontend/src/pages/GroupsPage.tsx @@ -32,9 +32,9 @@ const GroupsPage: React.FC = () => { }; const handleDeleteGroup = async (groupId: string) => { - const success = await deleteGroup(groupId); - if (!success) { - setGroupError(t('groups.deleteError')); + const result = await deleteGroup(groupId); + if (!result || !result.success) { + setGroupError(result?.message || t('groups.deleteError')); } }; diff --git a/frontend/src/pages/ServersPage.tsx b/frontend/src/pages/ServersPage.tsx index dd14169..a79f3ea 100644 --- a/frontend/src/pages/ServersPage.tsx +++ b/frontend/src/pages/ServersPage.tsx @@ -103,7 +103,6 @@ const ServersPage: React.FC = () => {
-

{t('app.error')}

{error}