feat: Implement bearer token validation in auth middleware (#186)

This commit is contained in:
samanhappy
2025-06-19 12:11:35 +08:00
committed by GitHub
parent 1e308ec4c5
commit d119be0f82
15 changed files with 248 additions and 56 deletions

View File

@@ -7,20 +7,20 @@ interface ProtectedRouteProps {
redirectPath?: string;
}
const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
redirectPath = '/login'
const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
redirectPath = '/login'
}) => {
const { t } = useTranslation();
const { auth } = useAuth();
if (auth.loading) {
return <div className="flex items-center justify-center h-screen">{t('app.loading')}</div>;
}
if (!auth.isAuthenticated) {
return <Navigate to={redirectPath} replace />;
}
return <Outlet />;
};

View File

@@ -1,10 +1,10 @@
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
import { AuthState, IUser } from '../types';
import { AuthState } from '../types';
import * as authService from '../services/authService';
import { shouldSkipAuth } from '../services/configService';
// Initial auth state
const initialState: AuthState = {
token: null,
isAuthenticated: false,
loading: true,
user: null,
@@ -21,7 +21,7 @@ const AuthContext = createContext<{
auth: initialState,
login: async () => false,
register: async () => false,
logout: () => {},
logout: () => { },
});
// Auth provider component
@@ -31,8 +31,26 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
// Load user if token exists
useEffect(() => {
const loadUser = async () => {
// First check if authentication should be skipped
const skipAuth = await shouldSkipAuth();
if (skipAuth) {
// If authentication is disabled, set user as authenticated with a dummy user
setAuth({
isAuthenticated: true,
loading: false,
user: {
username: 'guest',
isAdmin: true,
},
error: null,
});
return;
}
// Normal authentication flow
const token = authService.getToken();
if (!token) {
setAuth({
...initialState,
@@ -40,13 +58,12 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
});
return;
}
try {
const response = await authService.getCurrentUser();
if (response.success && response.user) {
setAuth({
token,
isAuthenticated: true,
loading: false,
user: response.user,
@@ -67,7 +84,7 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
});
}
};
loadUser();
}, []);
@@ -75,10 +92,9 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
const login = async (username: string, password: string): Promise<boolean> => {
try {
const response = await authService.login({ username, password });
if (response.success && response.token && response.user) {
setAuth({
token: response.token,
isAuthenticated: true,
loading: false,
user: response.user,
@@ -105,16 +121,15 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
// Register function
const register = async (
username: string,
password: string,
username: string,
password: string,
isAdmin = false
): Promise<boolean> => {
try {
const response = await authService.register({ username, password, isAdmin });
if (response.success && response.token && response.user) {
setAuth({
token: response.token,
isAuthenticated: true,
loading: false,
user: response.user,

View File

@@ -10,6 +10,7 @@ interface RoutingConfig {
enableGroupNameRoute: boolean;
enableBearerAuth: boolean;
bearerAuthKey: string;
skipAuth: boolean;
}
interface InstallConfig {
@@ -46,6 +47,7 @@ export const useSettingsData = () => {
enableGroupNameRoute: true,
enableBearerAuth: false,
bearerAuthKey: '',
skipAuth: false,
});
const [tempRoutingConfig, setTempRoutingConfig] = useState<TempRoutingConfig>({
@@ -99,6 +101,7 @@ export const useSettingsData = () => {
enableGroupNameRoute: data.data.systemConfig.routing.enableGroupNameRoute ?? true,
enableBearerAuth: data.data.systemConfig.routing.enableBearerAuth ?? false,
bearerAuthKey: data.data.systemConfig.routing.bearerAuthKey || '',
skipAuth: data.data.systemConfig.routing.skipAuth ?? false,
});
}
if (data.success && data.data?.systemConfig?.install) {

View File

@@ -343,6 +343,8 @@
"bearerAuthKey": "Bearer Authentication Key",
"bearerAuthKeyDescription": "The authentication key that will be required in the Bearer token",
"bearerAuthKeyPlaceholder": "Enter bearer authentication key",
"skipAuth": "Skip Authentication",
"skipAuthDescription": "Bypass login requirement for frontend and API access (DEFAULT OFF for security)",
"pythonIndexUrl": "Python Package Repository URL",
"pythonIndexUrlDescription": "Set UV_DEFAULT_INDEX environment variable for Python package installation",
"pythonIndexUrlPlaceholder": "e.g. https://pypi.org/simple",

View File

@@ -344,6 +344,8 @@
"bearerAuthKey": "Bearer 认证密钥",
"bearerAuthKeyDescription": "Bearer 令牌中需要携带的认证密钥",
"bearerAuthKeyPlaceholder": "请输入 Bearer 认证密钥",
"skipAuth": "免登录开关",
"skipAuthDescription": "跳过前端和 API 访问的登录要求(默认关闭确保安全性)",
"pythonIndexUrl": "Python 包仓库地址",
"pythonIndexUrlDescription": "设置 UV_DEFAULT_INDEX 环境变量,用于 Python 包安装",
"pythonIndexUrlPlaceholder": "例如: https://mirrors.aliyun.com/pypi/simple",

View File

@@ -85,7 +85,7 @@ const SettingsPage: React.FC = () => {
}));
};
const handleRoutingConfigChange = async (key: 'enableGlobalRoute' | 'enableGroupNameRoute' | 'enableBearerAuth' | 'bearerAuthKey', value: boolean | string) => {
const handleRoutingConfigChange = async (key: 'enableGlobalRoute' | 'enableGroupNameRoute' | 'enableBearerAuth' | 'bearerAuthKey' | 'skipAuth', value: boolean | string) => {
// If enableBearerAuth is turned on and there's no key, generate one first
if (key === 'enableBearerAuth' && value === true) {
if (!tempRoutingConfig.bearerAuthKey && !routingConfig.bearerAuthKey) {
@@ -430,6 +430,18 @@ const SettingsPage: React.FC = () => {
/>
</div>
<div className="flex items-center justify-between p-3 bg-gray-50 rounded-md">
<div>
<h3 className="font-medium text-gray-700">{t('settings.skipAuth')}</h3>
<p className="text-sm text-gray-500">{t('settings.skipAuthDescription')}</p>
</div>
<Switch
disabled={loading}
checked={routingConfig.skipAuth}
onCheckedChange={(checked) => handleRoutingConfigChange('skipAuth', checked)}
/>
</div>
</div>
)}
</div>

View File

@@ -0,0 +1,102 @@
import { getApiUrl, getBasePath } from '../utils/runtime';
export interface SystemConfig {
routing?: {
enableGlobalRoute?: boolean;
enableGroupNameRoute?: boolean;
enableBearerAuth?: boolean;
bearerAuthKey?: string;
skipAuth?: boolean;
};
install?: {
pythonIndexUrl?: string;
npmRegistry?: string;
};
smartRouting?: {
enabled?: boolean;
dbUrl?: string;
openaiApiBaseUrl?: string;
openaiApiKey?: string;
openaiApiEmbeddingModel?: string;
};
}
export interface PublicConfigResponse {
success: boolean;
data?: {
skipAuth?: boolean;
};
message?: string;
}
export interface SystemConfigResponse {
success: boolean;
data?: {
systemConfig?: SystemConfig;
};
message?: string;
}
/**
* Get public configuration (skipAuth setting) without authentication
*/
export const getPublicConfig = async (): Promise<{ skipAuth: boolean }> => {
try {
const basePath = getBasePath();
const response = await fetch(`${basePath}/public-config`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
if (response.ok) {
const data: PublicConfigResponse = await response.json();
return { skipAuth: data.data?.skipAuth === true };
}
return { skipAuth: false };
} catch (error) {
console.debug('Failed to get public config:', error);
return { skipAuth: false };
}
};
/**
* Get system configuration without authentication
* This function tries to get the system configuration first without auth,
* and if that fails (likely due to auth requirements), it returns null
*/
export const getSystemConfigPublic = async (): Promise<SystemConfig | null> => {
try {
const response = await fetch(getApiUrl('/settings'), {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
if (response.ok) {
const data: SystemConfigResponse = await response.json();
return data.data?.systemConfig || null;
}
return null;
} catch (error) {
console.debug('Failed to get system config without auth:', error);
return null;
}
};
/**
* Check if authentication should be skipped based on system configuration
*/
export const shouldSkipAuth = async (): Promise<boolean> => {
try {
const config = await getPublicConfig();
return config.skipAuth;
} catch (error) {
console.debug('Failed to check skipAuth setting:', error);
return false;
}
};

View File

@@ -15,13 +15,9 @@ export const fetchLogs = async (): Promise<LogEntry[]> => {
try {
// Get authentication token
const token = getToken();
if (!token) {
throw new Error('Authentication token not found. Please log in.');
}
const response = await fetch(getApiUrl('/logs'), {
headers: {
'x-auth-token': token,
'x-auth-token': token || '',
},
});
@@ -43,14 +39,10 @@ export const clearLogs = async (): Promise<void> => {
try {
// Get authentication token
const token = getToken();
if (!token) {
throw new Error('Authentication token not found. Please log in.');
}
const response = await fetch(getApiUrl('/logs'), {
method: 'DELETE',
headers: {
'x-auth-token': token,
'x-auth-token': token || '',
},
});
@@ -84,12 +76,6 @@ export const useLogs = () => {
// Get the authentication token
const token = getToken();
if (!token) {
setError(new Error('Authentication token not found. Please log in.'));
setLoading(false);
return;
}
// Connect to SSE endpoint with auth token in URL
eventSource = new EventSource(getApiUrl(`/logs/stream?token=${token}`));

View File

@@ -26,10 +26,6 @@ export const callTool = async (
): Promise<ToolCallResult> => {
try {
const token = getToken();
if (!token) {
throw new Error('Authentication token not found. Please log in.');
}
// Construct the URL with optional server parameter
const url = server ? `/tools/call/${server}` : '/tools/call';
@@ -37,7 +33,7 @@ export const callTool = async (
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-auth-token': token,
'x-auth-token': token || '', // Include token for authentication
Authorization: `Bearer ${token}`, // Add bearer auth for MCP routing
},
body: JSON.stringify({
@@ -81,15 +77,11 @@ export const toggleTool = async (
): Promise<{ success: boolean; error?: string }> => {
try {
const token = getToken();
if (!token) {
throw new Error('Authentication token not found. Please log in.');
}
const response = await fetch(getApiUrl(`/servers/${serverName}/tools/${toolName}/toggle`), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-auth-token': token,
'x-auth-token': token || '',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ enabled }),
@@ -123,18 +115,14 @@ export const updateToolDescription = async (
): Promise<{ success: boolean; error?: string }> => {
try {
const token = getToken();
if (!token) {
throw new Error('Authentication token not found. Please log in.');
}
const response = await fetch(
getApiUrl(`/servers/${serverName}/tools/${toolName}/description`),
{
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'x-auth-token': token,
Authorization: `Bearer ${token}`,
'x-auth-token': token || '',
Authorization: `Bearer ${token || ''}`,
},
body: JSON.stringify({ description }),
},

View File

@@ -39,6 +39,14 @@ export default defineConfig({
target: 'http://localhost:3000',
changeOrigin: true,
},
[`${basePath}/config`]: {
target: 'http://localhost:3000',
changeOrigin: true,
},
[`${basePath}/public-config`]: {
target: 'http://localhost:3000',
changeOrigin: true,
},
},
},
});