feat: enhance JSON serialization safety & add dxt upload limit (#230)

This commit is contained in:
samanhappy
2025-07-20 19:18:10 +08:00
committed by GitHub
parent 4388084704
commit 20fd355b87
49 changed files with 3068 additions and 378 deletions

View File

@@ -9,6 +9,7 @@ import LoginPage from './pages/LoginPage';
import DashboardPage from './pages/Dashboard';
import ServersPage from './pages/ServersPage';
import GroupsPage from './pages/GroupsPage';
import UsersPage from './pages/UsersPage';
import SettingsPage from './pages/SettingsPage';
import MarketPage from './pages/MarketPage';
import LogsPage from './pages/LogsPage';
@@ -31,6 +32,7 @@ function App() {
<Route path="/" element={<DashboardPage />} />
<Route path="/servers" element={<ServersPage />} />
<Route path="/groups" element={<GroupsPage />} />
<Route path="/users" element={<UsersPage />} />
<Route path="/market" element={<MarketPage />} />
<Route path="/market/:serverName" element={<MarketPage />} />
<Route path="/logs" element={<LogsPage />} />

View File

@@ -0,0 +1,95 @@
import React from 'react';
import { useAuth } from '../contexts/AuthContext';
interface PermissionCheckerProps {
permissions: string | string[];
fallback?: React.ReactNode;
children: React.ReactNode;
}
/**
* Permission checker component for conditional rendering
* @param permissions Required permissions, supports single permission string or permission array
* @param fallback Content to show when permission is denied, defaults to null
* @param children Content to show when permission is granted
*/
export const PermissionChecker: React.FC<PermissionCheckerProps> = ({
permissions,
fallback = null,
children,
}) => {
const hasPermission = usePermissionCheck(permissions);
return hasPermission ? <>{children}</> : <>{fallback}</>;
};
/**
* Permission check hook
* @param requiredPermissions Permissions to check
* @returns Whether user has permission
*/
export const usePermissionCheck = (requiredPermissions: string | string[]): boolean => {
const { auth } = useAuth();
if (!auth.isAuthenticated || !auth.user) {
return false;
}
const userPermissions = auth.user.permissions || [];
if (requiredPermissions === 'x' && !userPermissions.includes('x')) {
return false;
}
// If user has '*' permission, they have all permissions
if (userPermissions.includes('*')) {
return true;
}
// If user is admin, they have all permissions by default
if (auth.user.isAdmin) {
return true;
}
// Normalize required permissions to array
const permissionsToCheck = Array.isArray(requiredPermissions)
? requiredPermissions
: [requiredPermissions];
// Check if user has any of the required permissions
return permissionsToCheck.some(permission =>
userPermissions.includes(permission)
);
};
/**
* Permission check hook - requires all permissions
* @param requiredPermissions Array of permissions to check
* @returns Whether user has all permissions
*/
export const usePermissionCheckAll = (requiredPermissions: string[]): boolean => {
const { auth } = useAuth();
if (!auth.isAuthenticated || !auth.user) {
return false;
}
const userPermissions = auth.user.permissions || [];
// If user has '*' permission, they have all permissions
if (userPermissions.includes('*')) {
return true;
}
// If user is admin, they have all permissions by default
if (auth.user.isAdmin) {
return true;
}
// Check if user has all required permissions
return requiredPermissions.every(permission =>
userPermissions.includes(permission)
);
};
export default PermissionChecker;

View File

@@ -624,9 +624,9 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr
<button
type="button"
onClick={addHeaderVar}
className="bg-gray-200 hover:bg-gray-300 text-gray-700 font-medium py-1 px-2 rounded text-sm flex items-center btn-primary"
className="bg-gray-200 hover:bg-gray-300 text-gray-700 font-medium py-1 px-2 rounded text-sm flex items-center justify-center min-w-[30px] min-h-[30px] btn-primary"
>
+ {t('server.add')}
+
</button>
</div>
{headerVars.map((headerVar, index) => (
@@ -651,9 +651,9 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr
<button
type="button"
onClick={() => removeHeaderVar(index)}
className="bg-gray-200 hover:bg-gray-300 text-gray-700 font-medium py-1 px-2 rounded text-sm flex items-center justify-center min-w-[56px] ml-2 btn-danger"
className="bg-gray-200 hover:bg-gray-300 text-gray-700 font-medium py-1 px-2 rounded text-sm flex items-center justify-center min-w-[30px] min-h-[30px] ml-2 btn-danger"
>
- {t('server.remove')}
-
</button>
</div>
))}
@@ -685,9 +685,9 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr
<button
type="button"
onClick={addHeaderVar}
className="bg-gray-200 hover:bg-gray-300 text-gray-700 font-medium py-1 px-2 rounded text-sm flex items-center btn-primary"
className="bg-gray-200 hover:bg-gray-300 text-gray-700 font-medium py-1 px-2 rounded text-sm flex items-center justify-center min-w-[30px] min-h-[30px] btn-primary"
>
+ {t('server.add')}
+
</button>
</div>
{headerVars.map((headerVar, index) => (
@@ -712,9 +712,9 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr
<button
type="button"
onClick={() => removeHeaderVar(index)}
className="bg-gray-200 hover:bg-gray-300 text-gray-700 font-medium py-1 px-2 rounded text-sm flex items-center justify-center min-w-[56px] ml-2 btn-danger"
className="bg-gray-200 hover:bg-gray-300 text-gray-700 font-medium py-1 px-2 rounded text-sm flex items-center justify-center min-w-[30px] min-h-[30px] ml-2 btn-danger"
>
- {t('server.remove')}
-
</button>
</div>
))}
@@ -761,9 +761,9 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr
<button
type="button"
onClick={addEnvVar}
className="bg-gray-200 hover:bg-gray-300 text-gray-700 font-medium py-1 px-2 rounded text-sm flex items-center btn-primary"
className="bg-gray-200 hover:bg-gray-300 text-gray-700 font-medium py-1 px-2 rounded text-sm flex items-center justify-center min-w-[30px] min-h-[30px] btn-primary"
>
+ {t('server.add')}
+
</button>
</div>
{envVars.map((envVar, index) => (
@@ -788,9 +788,9 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr
<button
type="button"
onClick={() => removeEnvVar(index)}
className="bg-gray-200 hover:bg-gray-300 text-gray-700 font-medium py-1 px-2 rounded text-sm flex items-center justify-center min-w-[56px] ml-2 btn-danger"
className="bg-gray-200 hover:bg-gray-300 text-gray-700 font-medium py-1 px-2 rounded text-sm flex items-center justify-center min-w-[30px] min-h-[30px] ml-2 btn-danger"
>
- {t('server.remove')}
-
</button>
</div>
))}

View File

@@ -0,0 +1,42 @@
// Permission components unified export
export { PermissionChecker, usePermissionCheck, usePermissionCheckAll } from './PermissionChecker';
export { PERMISSIONS } from '../constants/permissions';
// Convenient permission check Hook
export { useAuth } from '../contexts/AuthContext';
// Permission utility functions
export const hasPermission = (
userPermissions: string[] = [],
requiredPermissions: string | string[],
): boolean => {
if (requiredPermissions === 'x' && !userPermissions.includes('x')) {
return false;
}
// If user has '*' permission, it means they have all permissions
if (userPermissions.includes('*')) {
return true;
}
// Normalize required permissions to array
const permissionsToCheck = Array.isArray(requiredPermissions)
? requiredPermissions
: [requiredPermissions];
// Check if user has any of the required permissions
return permissionsToCheck.some((permission) => userPermissions.includes(permission));
};
export const hasAllPermissions = (
userPermissions: string[] = [],
requiredPermissions: string[],
): boolean => {
// If user has '*' permission, it means they have all permissions
if (userPermissions.includes('*')) {
return true;
}
// Check if user has all required permissions
return requiredPermissions.every((permission) => userPermissions.includes(permission));
};

View File

@@ -1,6 +1,8 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { NavLink } from 'react-router-dom';
import { useAuth } from '@/contexts/AuthContext';
import { usePermissionCheck } from '../PermissionChecker';
import UserProfileMenu from '@/components/ui/UserProfileMenu';
interface SidebarProps {
@@ -15,6 +17,7 @@ interface MenuItem {
const Sidebar: React.FC<SidebarProps> = ({ collapsed }) => {
const { t } = useTranslation();
const { auth } = useAuth();
// Application version from package.json (accessed via Vite environment variables)
const appVersion = import.meta.env.PACKAGE_VERSION as string;
@@ -49,6 +52,15 @@ const Sidebar: React.FC<SidebarProps> = ({ collapsed }) => {
</svg>
),
},
...(auth.user?.isAdmin && usePermissionCheck('x') ? [{
path: '/users',
label: t('nav.users'),
icon: (
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path d="M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" />
</svg>
),
}] : []),
{
path: '/market',
label: t('nav.market'),

View File

@@ -6,9 +6,10 @@ interface DeleteDialogProps {
onConfirm: () => void
serverName: string
isGroup?: boolean
isUser?: boolean
}
const DeleteDialog = ({ isOpen, onClose, onConfirm, serverName, isGroup = false }: DeleteDialogProps) => {
const DeleteDialog = ({ isOpen, onClose, onConfirm, serverName, isGroup = false, isUser = false }: DeleteDialogProps) => {
const { t } = useTranslation()
if (!isOpen) return null
@@ -18,12 +19,18 @@ const DeleteDialog = ({ isOpen, onClose, onConfirm, serverName, isGroup = false
<div className="bg-white rounded-lg shadow-lg max-w-md w-full">
<div className="p-6">
<h3 className="text-lg font-medium text-gray-900 mb-3">
{isGroup ? t('groups.confirmDelete') : t('server.confirmDelete')}
{isUser
? t('users.confirmDelete')
: isGroup
? t('groups.confirmDelete')
: t('server.confirmDelete')}
</h3>
<p className="text-gray-500 mb-6">
{isGroup
? t('groups.deleteWarning', { name: serverName })
: t('server.deleteWarning', { name: serverName })}
{isUser
? t('users.deleteWarning', { username: serverName })
: isGroup
? t('groups.deleteWarning', { name: serverName })
: t('server.deleteWarning', { name: serverName })}
</p>
<div className="flex justify-end space-x-3">
<button

View File

@@ -0,0 +1,9 @@
// Predefined permission constants
export const PERMISSIONS = {
// Settings page permissions
SETTINGS_SMART_ROUTING: 'settings:smart_routing',
SETTINGS_SKIP_AUTH: 'settings:skip_auth',
SETTINGS_INSTALL_CONFIG: 'settings:install_config',
} as const;
export default PERMISSIONS;

View File

@@ -274,16 +274,19 @@ tbody tr:hover {
}
.btn-primary {
background-color: var(--color-blue-100) !important;
color: var(--color-blue-800) !important;
background-color: #60a5fa !important;
color: #ffffff !important;
border: none;
border-radius: 8px;
transition: all 0.3s ease;
box-shadow: 0 2px 4px rgba(96, 165, 250, 0.2);
}
.btn-primary:hover {
background-color: var(--color-blue-200) !important;
color: var(--color-blue-800) !important;
background-color: #3b82f6 !important;
color: #ffffff !important;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
}
/* Enhanced button styles for dark theme */

View File

@@ -173,6 +173,9 @@
"cancel": "Cancel",
"refresh": "Refresh",
"create": "Create",
"creating": "Creating...",
"update": "Update",
"updating": "Updating...",
"submitting": "Submitting...",
"delete": "Delete",
"remove": "Remove",
@@ -186,6 +189,7 @@
"dashboard": "Dashboard",
"servers": "Servers",
"groups": "Groups",
"users": "Users",
"settings": "Settings",
"changePassword": "Change Password",
"market": "Market",
@@ -206,6 +210,9 @@
"groups": {
"title": "Group Management"
},
"users": {
"title": "User Management"
},
"settings": {
"title": "Settings",
"language": "Language",
@@ -399,5 +406,41 @@
"serverExistsTitle": "Server Already Exists",
"serverExistsConfirm": "Server '{{serverName}}' already exists. Do you want to override it with the new version?",
"override": "Override"
},
"users": {
"add": "Add User",
"addNew": "Add New User",
"edit": "Edit User",
"delete": "Delete User",
"create": "Create User",
"update": "Update User",
"username": "Username",
"password": "Password",
"newPassword": "New Password",
"confirmPassword": "Confirm Password",
"adminRole": "Administrator",
"admin": "Admin",
"user": "User",
"permissions": "Permissions",
"adminPermissions": "Full system access",
"userPermissions": "Limited access",
"currentUser": "You",
"noUsers": "No users found",
"adminRequired": "Administrator access required to manage users",
"usernameRequired": "Username is required",
"passwordRequired": "Password is required",
"passwordTooShort": "Password must be at least 6 characters long",
"passwordMismatch": "Passwords do not match",
"usernamePlaceholder": "Enter username",
"passwordPlaceholder": "Enter password",
"newPasswordPlaceholder": "Leave empty to keep current password",
"confirmPasswordPlaceholder": "Confirm new password",
"createError": "Failed to create user",
"updateError": "Failed to update user",
"deleteError": "Failed to delete user",
"statsError": "Failed to fetch user statistics",
"deleteConfirmation": "Are you sure you want to delete user '{{username}}'? This action cannot be undone.",
"confirmDelete": "Delete User",
"deleteWarning": "Are you sure you want to delete user '{{username}}'? This action cannot be undone."
}
}

View File

@@ -174,6 +174,9 @@
"cancel": "取消",
"refresh": "刷新",
"create": "创建",
"creating": "创建中...",
"update": "更新",
"updating": "更新中...",
"submitting": "提交中...",
"delete": "删除",
"remove": "移除",
@@ -189,6 +192,7 @@
"settings": "设置",
"changePassword": "修改密码",
"groups": "分组",
"users": "用户",
"market": "市场",
"logs": "日志"
},
@@ -217,6 +221,9 @@
"groups": {
"title": "分组管理"
},
"users": {
"title": "用户管理"
},
"market": {
"title": "服务器市场 - (数据来源于 mcpm.sh"
},
@@ -401,5 +408,41 @@
"serverExistsTitle": "服务器已存在",
"serverExistsConfirm": "服务器 '{{serverName}}' 已存在。是否要用新版本覆盖它?",
"override": "覆盖"
},
"users": {
"add": "添加",
"addNew": "添加新用户",
"edit": "编辑用户",
"delete": "删除用户",
"create": "创建",
"update": "用户",
"username": "用户名",
"password": "密码",
"newPassword": "新密码",
"confirmPassword": "确认密码",
"adminRole": "管理员",
"admin": "管理员",
"user": "用户",
"permissions": "权限",
"adminPermissions": "完全系统访问权限",
"userPermissions": "受限访问权限",
"currentUser": "当前用户",
"noUsers": "没有找到用户",
"adminRequired": "需要管理员权限才能管理用户",
"usernameRequired": "用户名是必需的",
"passwordRequired": "密码是必需的",
"passwordTooShort": "密码至少需要6个字符",
"passwordMismatch": "密码不匹配",
"usernamePlaceholder": "输入用户名",
"passwordPlaceholder": "输入密码",
"newPasswordPlaceholder": "留空保持当前密码",
"confirmPasswordPlaceholder": "确认新密码",
"createError": "创建用户失败",
"updateError": "更新用户失败",
"deleteError": "删除用户失败",
"statsError": "获取用户统计失败",
"deleteConfirmation": "您确定要删除用户 '{{username}}' 吗?此操作无法撤消。",
"confirmDelete": "删除用户",
"deleteWarning": "您确定要删除用户 '{{username}}' 吗?此操作无法撤消。"
}
}

View File

@@ -51,7 +51,7 @@ const LoginPage: React.FC = () => {
</h2>
</div>
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
<div className="rounded-md -space-y-px">
<div className="space-y-4">
<div>
<label htmlFor="username" className="sr-only">
{t('auth.username')}
@@ -62,7 +62,7 @@ const LoginPage: React.FC = () => {
type="text"
autoComplete="username"
required
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-200 dark:border-gray-700 placeholder-gray-500 dark:placeholder-gray-400 text-gray-900 dark:text-white dark:bg-gray-800 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm transition-all duration-200 form-input"
className="appearance-none relative block w-full px-3 py-3 border border-gray-300 dark:border-gray-600 placeholder-gray-500 dark:placeholder-gray-400 text-gray-900 dark:text-white dark:bg-gray-800 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm transition-all duration-200 form-input shadow-sm"
placeholder={t('auth.username')}
value={username}
onChange={(e) => setUsername(e.target.value)}
@@ -78,7 +78,7 @@ const LoginPage: React.FC = () => {
type="password"
autoComplete="current-password"
required
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 dark:border-gray-700 placeholder-gray-500 dark:placeholder-gray-400 text-gray-900 dark:text-white dark:bg-gray-800 rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm login-input transition-all duration-200 form-input"
className="appearance-none relative block w-full px-3 py-3 border border-gray-300 dark:border-gray-600 placeholder-gray-500 dark:placeholder-gray-400 text-gray-900 dark:text-white dark:bg-gray-800 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm login-input transition-all duration-200 form-input shadow-sm"
placeholder={t('auth.password')}
value={password}
onChange={(e) => setPassword(e.target.value)}

View File

@@ -6,6 +6,8 @@ import { Switch } from '@/components/ui/ToggleGroup';
import { useSettingsData } from '@/hooks/useSettingsData';
import { useToast } from '@/contexts/ToastContext';
import { generateRandomKey } from '@/utils/key';
import { PermissionChecker } from '@/components/PermissionChecker';
import { PERMISSIONS } from '@/constants/permissions';
const SettingsPage: React.FC = () => {
const { t, i18n } = useTranslation();
@@ -230,129 +232,131 @@ const SettingsPage: React.FC = () => {
</div>
{/* Smart Routing Configuration Settings */}
<div className="bg-white shadow rounded-lg py-4 px-6 mb-6 page-card">
<div
className="flex justify-between items-center cursor-pointer transition-colors duration-200 hover:text-blue-600"
onClick={() => toggleSection('smartRoutingConfig')}
>
<h2 className="font-semibold text-gray-800">{t('pages.settings.smartRouting')}</h2>
<span className="text-gray-500 transition-transform duration-200">
{sectionsVisible.smartRoutingConfig ? '▼' : '►'}
</span>
</div>
{sectionsVisible.smartRoutingConfig && (
<div className="space-y-4 mt-4">
<div className="flex items-center justify-between p-3 bg-gray-50 rounded-md">
<div>
<h3 className="font-medium text-gray-700">{t('settings.enableSmartRouting')}</h3>
<p className="text-sm text-gray-500">{t('settings.enableSmartRoutingDescription')}</p>
</div>
<Switch
disabled={loading}
checked={smartRoutingConfig.enabled}
onCheckedChange={(checked) => handleSmartRoutingEnabledChange(checked)}
/>
</div>
<div className="p-3 bg-gray-50 rounded-md">
<div className="mb-2">
<h3 className="font-medium text-gray-700">
<span className="text-red-500 px-1">*</span>{t('settings.dbUrl')}
</h3>
</div>
<div className="flex items-center gap-3">
<input
type="text"
value={tempSmartRoutingConfig.dbUrl}
onChange={(e) => handleSmartRoutingConfigChange('dbUrl', e.target.value)}
placeholder={t('settings.dbUrlPlaceholder')}
className="flex-1 mt-1 block w-full py-2 px-3 border rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm border-gray-300 form-input"
disabled={loading}
/>
<button
onClick={() => saveSmartRoutingConfig('dbUrl')}
disabled={loading}
className="mt-1 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md text-sm font-medium disabled:opacity-50 btn-primary"
>
{t('common.save')}
</button>
</div>
</div>
<div className="p-3 bg-gray-50 rounded-md">
<div className="mb-2">
<h3 className="font-medium text-gray-700">
<span className="text-red-500 px-1">*</span>{t('settings.openaiApiKey')}
</h3>
</div>
<div className="flex items-center gap-3">
<input
type="password"
value={tempSmartRoutingConfig.openaiApiKey}
onChange={(e) => handleSmartRoutingConfigChange('openaiApiKey', e.target.value)}
placeholder={t('settings.openaiApiKeyPlaceholder')}
className="flex-1 mt-1 block w-full py-2 px-3 border rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm border-gray-300"
disabled={loading}
/>
<button
onClick={() => saveSmartRoutingConfig('openaiApiKey')}
disabled={loading}
className="mt-1 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md text-sm font-medium disabled:opacity-50 btn-primary"
>
{t('common.save')}
</button>
</div>
</div>
<div className="p-3 bg-gray-50 rounded-md">
<div className="mb-2">
<h3 className="font-medium text-gray-700">{t('settings.openaiApiBaseUrl')}</h3>
</div>
<div className="flex items-center gap-3">
<input
type="text"
value={tempSmartRoutingConfig.openaiApiBaseUrl}
onChange={(e) => handleSmartRoutingConfigChange('openaiApiBaseUrl', e.target.value)}
placeholder={t('settings.openaiApiBaseUrlPlaceholder')}
className="flex-1 mt-1 block w-full py-2 px-3 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm form-input"
disabled={loading}
/>
<button
onClick={() => saveSmartRoutingConfig('openaiApiBaseUrl')}
disabled={loading}
className="mt-1 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md text-sm font-medium disabled:opacity-50 btn-primary"
>
{t('common.save')}
</button>
</div>
</div>
<div className="p-3 bg-gray-50 rounded-md">
<div className="mb-2">
<h3 className="font-medium text-gray-700">{t('settings.openaiApiEmbeddingModel')}</h3>
</div>
<div className="flex items-center gap-3">
<input
type="text"
value={tempSmartRoutingConfig.openaiApiEmbeddingModel}
onChange={(e) => handleSmartRoutingConfigChange('openaiApiEmbeddingModel', e.target.value)}
placeholder={t('settings.openaiApiEmbeddingModelPlaceholder')}
className="flex-1 mt-1 block w-full py-2 px-3 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm form-input"
disabled={loading}
/>
<button
onClick={() => saveSmartRoutingConfig('openaiApiEmbeddingModel')}
disabled={loading}
className="mt-1 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md text-sm font-medium disabled:opacity-50 btn-primary"
>
{t('common.save')}
</button>
</div>
</div>
<PermissionChecker permissions={PERMISSIONS.SETTINGS_SMART_ROUTING}>
<div className="bg-white shadow rounded-lg py-4 px-6 mb-6 page-card">
<div
className="flex justify-between items-center cursor-pointer transition-colors duration-200 hover:text-blue-600"
onClick={() => toggleSection('smartRoutingConfig')}
>
<h2 className="font-semibold text-gray-800">{t('pages.settings.smartRouting')}</h2>
<span className="text-gray-500 transition-transform duration-200">
{sectionsVisible.smartRoutingConfig ? '▼' : '►'}
</span>
</div>
)}
</div>
{sectionsVisible.smartRoutingConfig && (
<div className="space-y-4 mt-4">
<div className="flex items-center justify-between p-3 bg-gray-50 rounded-md">
<div>
<h3 className="font-medium text-gray-700">{t('settings.enableSmartRouting')}</h3>
<p className="text-sm text-gray-500">{t('settings.enableSmartRoutingDescription')}</p>
</div>
<Switch
disabled={loading}
checked={smartRoutingConfig.enabled}
onCheckedChange={(checked) => handleSmartRoutingEnabledChange(checked)}
/>
</div>
<div className="p-3 bg-gray-50 rounded-md">
<div className="mb-2">
<h3 className="font-medium text-gray-700">
<span className="text-red-500 px-1">*</span>{t('settings.dbUrl')}
</h3>
</div>
<div className="flex items-center gap-3">
<input
type="text"
value={tempSmartRoutingConfig.dbUrl}
onChange={(e) => handleSmartRoutingConfigChange('dbUrl', e.target.value)}
placeholder={t('settings.dbUrlPlaceholder')}
className="flex-1 mt-1 block w-full py-2 px-3 border rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm border-gray-300 form-input"
disabled={loading}
/>
<button
onClick={() => saveSmartRoutingConfig('dbUrl')}
disabled={loading}
className="mt-1 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md text-sm font-medium disabled:opacity-50 btn-primary"
>
{t('common.save')}
</button>
</div>
</div>
<div className="p-3 bg-gray-50 rounded-md">
<div className="mb-2">
<h3 className="font-medium text-gray-700">
<span className="text-red-500 px-1">*</span>{t('settings.openaiApiKey')}
</h3>
</div>
<div className="flex items-center gap-3">
<input
type="password"
value={tempSmartRoutingConfig.openaiApiKey}
onChange={(e) => handleSmartRoutingConfigChange('openaiApiKey', e.target.value)}
placeholder={t('settings.openaiApiKeyPlaceholder')}
className="flex-1 mt-1 block w-full py-2 px-3 border rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm border-gray-300"
disabled={loading}
/>
<button
onClick={() => saveSmartRoutingConfig('openaiApiKey')}
disabled={loading}
className="mt-1 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md text-sm font-medium disabled:opacity-50 btn-primary"
>
{t('common.save')}
</button>
</div>
</div>
<div className="p-3 bg-gray-50 rounded-md">
<div className="mb-2">
<h3 className="font-medium text-gray-700">{t('settings.openaiApiBaseUrl')}</h3>
</div>
<div className="flex items-center gap-3">
<input
type="text"
value={tempSmartRoutingConfig.openaiApiBaseUrl}
onChange={(e) => handleSmartRoutingConfigChange('openaiApiBaseUrl', e.target.value)}
placeholder={t('settings.openaiApiBaseUrlPlaceholder')}
className="flex-1 mt-1 block w-full py-2 px-3 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm form-input"
disabled={loading}
/>
<button
onClick={() => saveSmartRoutingConfig('openaiApiBaseUrl')}
disabled={loading}
className="mt-1 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md text-sm font-medium disabled:opacity-50 btn-primary"
>
{t('common.save')}
</button>
</div>
</div>
<div className="p-3 bg-gray-50 rounded-md">
<div className="mb-2">
<h3 className="font-medium text-gray-700">{t('settings.openaiApiEmbeddingModel')}</h3>
</div>
<div className="flex items-center gap-3">
<input
type="text"
value={tempSmartRoutingConfig.openaiApiEmbeddingModel}
onChange={(e) => handleSmartRoutingConfigChange('openaiApiEmbeddingModel', e.target.value)}
placeholder={t('settings.openaiApiEmbeddingModelPlaceholder')}
className="flex-1 mt-1 block w-full py-2 px-3 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm form-input"
disabled={loading}
/>
<button
onClick={() => saveSmartRoutingConfig('openaiApiEmbeddingModel')}
disabled={loading}
className="mt-1 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md text-sm font-medium disabled:opacity-50 btn-primary"
>
{t('common.save')}
</button>
</div>
</div>
</div>
)}
</div>
</PermissionChecker>
{/* Route Configuration Settings */}
<div className="bg-white shadow rounded-lg py-4 px-6 mb-6">
@@ -430,86 +434,90 @@ 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>
<PermissionChecker permissions={PERMISSIONS.SETTINGS_SKIP_AUTH}>
<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>
<Switch
disabled={loading}
checked={routingConfig.skipAuth}
onCheckedChange={(checked) => handleRoutingConfigChange('skipAuth', checked)}
/>
</div>
</PermissionChecker>
</div>
)}
</div>
{/* Installation Configuration Settings */}
<div className="bg-white shadow rounded-lg py-4 px-6 mb-6">
<div
className="flex justify-between items-center cursor-pointer"
onClick={() => toggleSection('installConfig')}
>
<h2 className="font-semibold text-gray-800">{t('settings.installConfig')}</h2>
<span className="text-gray-500">
{sectionsVisible.installConfig ? '▼' : '►'}
</span>
</div>
{sectionsVisible.installConfig && (
<div className="space-y-4 mt-4">
<div className="p-3 bg-gray-50 rounded-md">
<div className="mb-2">
<h3 className="font-medium text-gray-700">{t('settings.pythonIndexUrl')}</h3>
<p className="text-sm text-gray-500">{t('settings.pythonIndexUrlDescription')}</p>
</div>
<div className="flex items-center gap-3">
<input
type="text"
value={installConfig.pythonIndexUrl}
onChange={(e) => handleInstallConfigChange('pythonIndexUrl', e.target.value)}
placeholder={t('settings.pythonIndexUrlPlaceholder')}
className="flex-1 mt-1 block w-full py-2 px-3 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm form-input"
disabled={loading}
/>
<button
onClick={() => saveInstallConfig('pythonIndexUrl')}
disabled={loading}
className="mt-1 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md text-sm font-medium disabled:opacity-50 btn-primary"
>
{t('common.save')}
</button>
</div>
</div>
<div className="p-3 bg-gray-50 rounded-md">
<div className="mb-2">
<h3 className="font-medium text-gray-700">{t('settings.npmRegistry')}</h3>
<p className="text-sm text-gray-500">{t('settings.npmRegistryDescription')}</p>
</div>
<div className="flex items-center gap-3">
<input
type="text"
value={installConfig.npmRegistry}
onChange={(e) => handleInstallConfigChange('npmRegistry', e.target.value)}
placeholder={t('settings.npmRegistryPlaceholder')}
className="flex-1 mt-1 block w-full py-2 px-3 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm form-input"
disabled={loading}
/>
<button
onClick={() => saveInstallConfig('npmRegistry')}
disabled={loading}
className="mt-1 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md text-sm font-medium disabled:opacity-50 btn-primary"
>
{t('common.save')}
</button>
</div>
</div>
<PermissionChecker permissions={PERMISSIONS.SETTINGS_INSTALL_CONFIG}>
<div className="bg-white shadow rounded-lg py-4 px-6 mb-6">
<div
className="flex justify-between items-center cursor-pointer"
onClick={() => toggleSection('installConfig')}
>
<h2 className="font-semibold text-gray-800">{t('settings.installConfig')}</h2>
<span className="text-gray-500">
{sectionsVisible.installConfig ? '▼' : '►'}
</span>
</div>
)}
</div>
{sectionsVisible.installConfig && (
<div className="space-y-4 mt-4">
<div className="p-3 bg-gray-50 rounded-md">
<div className="mb-2">
<h3 className="font-medium text-gray-700">{t('settings.pythonIndexUrl')}</h3>
<p className="text-sm text-gray-500">{t('settings.pythonIndexUrlDescription')}</p>
</div>
<div className="flex items-center gap-3">
<input
type="text"
value={installConfig.pythonIndexUrl}
onChange={(e) => handleInstallConfigChange('pythonIndexUrl', e.target.value)}
placeholder={t('settings.pythonIndexUrlPlaceholder')}
className="flex-1 mt-1 block w-full py-2 px-3 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm form-input"
disabled={loading}
/>
<button
onClick={() => saveInstallConfig('pythonIndexUrl')}
disabled={loading}
className="mt-1 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md text-sm font-medium disabled:opacity-50 btn-primary"
>
{t('common.save')}
</button>
</div>
</div>
<div className="p-3 bg-gray-50 rounded-md">
<div className="mb-2">
<h3 className="font-medium text-gray-700">{t('settings.npmRegistry')}</h3>
<p className="text-sm text-gray-500">{t('settings.npmRegistryDescription')}</p>
</div>
<div className="flex items-center gap-3">
<input
type="text"
value={installConfig.npmRegistry}
onChange={(e) => handleInstallConfigChange('npmRegistry', e.target.value)}
placeholder={t('settings.npmRegistryPlaceholder')}
className="flex-1 mt-1 block w-full py-2 px-3 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm form-input"
disabled={loading}
/>
<button
onClick={() => saveInstallConfig('npmRegistry')}
disabled={loading}
className="mt-1 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md text-sm font-medium disabled:opacity-50 btn-primary"
>
{t('common.save')}
</button>
</div>
</div>
</div>
)}
</div>
</PermissionChecker>
{/* Change Password */}
<div className="bg-white shadow rounded-lg py-4 px-6 mb-6">

View File

@@ -0,0 +1,9 @@
import React from 'react';
const UsersPage: React.FC = () => {
return (
<div></div>
);
};
export default UsersPage;

View File

@@ -210,6 +210,30 @@ export interface ApiResponse<T = any> {
export interface IUser {
username: string;
isAdmin?: boolean;
permissions?: string[];
}
// User management types
export interface User {
username: string;
isAdmin: boolean;
}
export interface UserFormData {
username: string;
password: string;
isAdmin: boolean;
}
export interface UserUpdateData {
isAdmin?: boolean;
newPassword?: string;
}
export interface UserStats {
totalUsers: number;
adminUsers: number;
regularUsers: number;
}
export interface AuthState {