Compare commits

...

3 Commits

9 changed files with 104 additions and 46 deletions

View File

@@ -14,14 +14,17 @@ const initialState: AuthState = {
// Create auth context // Create auth context
const AuthContext = createContext<{ const AuthContext = createContext<{
auth: AuthState; auth: AuthState;
login: (username: string, password: string) => Promise<{ success: boolean; isUsingDefaultPassword?: boolean }>; login: (
username: string,
password: string,
) => Promise<{ success: boolean; isUsingDefaultPassword?: boolean; message?: string }>;
register: (username: string, password: string, isAdmin?: boolean) => Promise<boolean>; register: (username: string, password: string, isAdmin?: boolean) => Promise<boolean>;
logout: () => void; logout: () => void;
}>({ }>({
auth: initialState, auth: initialState,
login: async () => ({ success: false }), login: async () => ({ success: false }),
register: async () => false, register: async () => false,
logout: () => { }, logout: () => {},
}); });
// Auth provider component // Auth provider component
@@ -90,7 +93,10 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
}, []); }, []);
// Login function // Login function
const login = async (username: string, password: string): Promise<{ success: boolean; isUsingDefaultPassword?: boolean }> => { const login = async (
username: string,
password: string,
): Promise<{ success: boolean; isUsingDefaultPassword?: boolean; message?: string }> => {
try { try {
const response = await authService.login({ username, password }); const response = await authService.login({ username, password });
@@ -111,7 +117,7 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
loading: false, loading: false,
error: response.message || 'Authentication failed', error: response.message || 'Authentication failed',
}); });
return { success: false }; return { success: false, message: response.message };
} }
} catch (error) { } catch (error) {
setAuth({ setAuth({
@@ -119,7 +125,7 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
loading: false, loading: false,
error: 'Authentication failed', error: 'Authentication failed',
}); });
return { success: false }; return { success: false, message: error instanceof Error ? error.message : undefined };
} }
}; };
@@ -127,7 +133,7 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
const register = async ( const register = async (
username: string, username: string,
password: string, password: string,
isAdmin = false isAdmin = false,
): Promise<boolean> => { ): Promise<boolean> => {
try { try {
const response = await authService.register({ username, password, isAdmin }); const response = await authService.register({ username, password, isAdmin });
@@ -175,4 +181,4 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
}; };
// Custom hook to use auth context // Custom hook to use auth context
export const useAuth = () => useContext(AuthContext); export const useAuth = () => useContext(AuthContext);

View File

@@ -9,6 +9,7 @@ import React, {
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { ApiResponse, BearerKey } from '@/types'; import { ApiResponse, BearerKey } from '@/types';
import { useToast } from '@/contexts/ToastContext'; import { useToast } from '@/contexts/ToastContext';
import { useAuth } from '@/contexts/AuthContext';
import { apiGet, apiPut, apiPost, apiDelete } from '@/utils/fetchInterceptor'; import { apiGet, apiPut, apiPost, apiDelete } from '@/utils/fetchInterceptor';
// Define types for the settings data // Define types for the settings data
@@ -153,6 +154,7 @@ interface SettingsProviderProps {
export const SettingsProvider: React.FC<SettingsProviderProps> = ({ children }) => { export const SettingsProvider: React.FC<SettingsProviderProps> = ({ children }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { showToast } = useToast(); const { showToast } = useToast();
const { auth } = useAuth();
const [routingConfig, setRoutingConfig] = useState<RoutingConfig>({ const [routingConfig, setRoutingConfig] = useState<RoutingConfig>({
enableGlobalRoute: true, enableGlobalRoute: true,
@@ -746,6 +748,15 @@ export const SettingsProvider: React.FC<SettingsProviderProps> = ({ children })
fetchSettings(); fetchSettings();
}, [fetchSettings, refreshKey]); }, [fetchSettings, refreshKey]);
// Watch for authentication status changes - refetch settings after login
useEffect(() => {
if (auth.isAuthenticated) {
console.log('[SettingsContext] User authenticated, triggering settings refresh');
// When user logs in, trigger a refresh to load settings
triggerRefresh();
}
}, [auth.isAuthenticated, triggerRefresh]);
useEffect(() => { useEffect(() => {
if (routingConfig) { if (routingConfig) {
setTempRoutingConfig({ setTempRoutingConfig({

View File

@@ -44,6 +44,24 @@ const LoginPage: React.FC = () => {
return sanitizeReturnUrl(params.get('returnUrl')); return sanitizeReturnUrl(params.get('returnUrl'));
}, [location.search]); }, [location.search]);
const isServerUnavailableError = useCallback((message?: string) => {
if (!message) return false;
const normalized = message.toLowerCase();
return (
normalized.includes('failed to fetch') ||
normalized.includes('networkerror') ||
normalized.includes('network error') ||
normalized.includes('connection refused') ||
normalized.includes('unable to connect') ||
normalized.includes('fetch error') ||
normalized.includes('econnrefused') ||
normalized.includes('http 500') ||
normalized.includes('internal server error') ||
normalized.includes('proxy error')
);
}, []);
const buildRedirectTarget = useCallback(() => { const buildRedirectTarget = useCallback(() => {
if (!returnUrl) { if (!returnUrl) {
return '/'; return '/';
@@ -100,10 +118,20 @@ const LoginPage: React.FC = () => {
redirectAfterLogin(); redirectAfterLogin();
} }
} else { } else {
setError(t('auth.loginFailed')); const message = result.message;
if (isServerUnavailableError(message)) {
setError(t('auth.serverUnavailable'));
} else {
setError(t('auth.loginFailed'));
}
} }
} catch (err) { } catch (err) {
setError(t('auth.loginError')); const message = err instanceof Error ? err.message : undefined;
if (isServerUnavailableError(message)) {
setError(t('auth.serverUnavailable'));
} else {
setError(t('auth.loginError'));
}
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -131,13 +159,21 @@ const LoginPage: React.FC = () => {
}} }}
/> />
<div className="pointer-events-none absolute inset-0 -z-10"> <div className="pointer-events-none absolute inset-0 -z-10">
<svg className="h-full w-full opacity-[0.08] dark:opacity-[0.12]" xmlns="http://www.w3.org/2000/svg"> <svg
className="h-full w-full opacity-[0.08] dark:opacity-[0.12]"
xmlns="http://www.w3.org/2000/svg"
>
<defs> <defs>
<pattern id="grid" width="32" height="32" patternUnits="userSpaceOnUse"> <pattern id="grid" width="32" height="32" patternUnits="userSpaceOnUse">
<path d="M 32 0 L 0 0 0 32" fill="none" stroke="currentColor" strokeWidth="0.5" /> <path d="M 32 0 L 0 0 0 32" fill="none" stroke="currentColor" strokeWidth="0.5" />
</pattern> </pattern>
</defs> </defs>
<rect width="100%" height="100%" fill="url(#grid)" className="text-gray-400 dark:text-gray-300" /> <rect
width="100%"
height="100%"
fill="url(#grid)"
className="text-gray-400 dark:text-gray-300"
/>
</svg> </svg>
</div> </div>

View File

@@ -558,12 +558,6 @@ const SettingsPage: React.FC = () => {
}); });
}; };
const saveSmartRoutingConfig = async (
key: 'dbUrl' | 'openaiApiBaseUrl' | 'openaiApiKey' | 'openaiApiEmbeddingModel',
) => {
await updateSmartRoutingConfig(key, tempSmartRoutingConfig[key]);
};
const handleMCPRouterConfigChange = ( const handleMCPRouterConfigChange = (
key: 'apiKey' | 'referer' | 'title' | 'baseUrl', key: 'apiKey' | 'referer' | 'title' | 'baseUrl',
value: string, value: string,
@@ -705,6 +699,31 @@ const SettingsPage: React.FC = () => {
} }
}; };
const handleSaveSmartRoutingConfig = async () => {
const updates: any = {};
if (tempSmartRoutingConfig.dbUrl !== smartRoutingConfig.dbUrl) {
updates.dbUrl = tempSmartRoutingConfig.dbUrl;
}
if (tempSmartRoutingConfig.openaiApiBaseUrl !== smartRoutingConfig.openaiApiBaseUrl) {
updates.openaiApiBaseUrl = tempSmartRoutingConfig.openaiApiBaseUrl;
}
if (tempSmartRoutingConfig.openaiApiKey !== smartRoutingConfig.openaiApiKey) {
updates.openaiApiKey = tempSmartRoutingConfig.openaiApiKey;
}
if (
tempSmartRoutingConfig.openaiApiEmbeddingModel !== smartRoutingConfig.openaiApiEmbeddingModel
) {
updates.openaiApiEmbeddingModel = tempSmartRoutingConfig.openaiApiEmbeddingModel;
}
if (Object.keys(updates).length > 0) {
await updateSmartRoutingConfigBatch(updates);
} else {
showToast(t('settings.noChanges') || 'No changes to save', 'info');
}
};
const handlePasswordChangeSuccess = () => { const handlePasswordChangeSuccess = () => {
setTimeout(() => { setTimeout(() => {
navigate('/'); navigate('/');
@@ -1230,13 +1249,6 @@ const SettingsPage: React.FC = () => {
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" 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} 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> </div>
@@ -1256,13 +1268,6 @@ const SettingsPage: React.FC = () => {
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" 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} 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> </div>
@@ -1281,13 +1286,6 @@ const SettingsPage: React.FC = () => {
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" 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} 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> </div>
@@ -1308,15 +1306,18 @@ const SettingsPage: React.FC = () => {
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" 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} 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>
<div className="flex justify-end pt-2">
<button
onClick={handleSaveSmartRoutingConfig}
disabled={loading}
className="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> </div>

View File

@@ -29,7 +29,7 @@ export const login = async (credentials: LoginCredentials): Promise<AuthResponse
console.error('Login error:', error); console.error('Login error:', error);
return { return {
success: false, success: false,
message: 'An error occurred during login', message: error instanceof Error ? error.message : 'An error occurred during login',
}; };
} }
}; };

View File

@@ -61,6 +61,7 @@
"emptyFields": "Username and password cannot be empty", "emptyFields": "Username and password cannot be empty",
"loginFailed": "Login failed, please check your username and password", "loginFailed": "Login failed, please check your username and password",
"loginError": "An error occurred during login", "loginError": "An error occurred during login",
"serverUnavailable": "Unable to connect to the server. Please check your network connection or try again later",
"currentPassword": "Current Password", "currentPassword": "Current Password",
"newPassword": "New Password", "newPassword": "New Password",
"confirmPassword": "Confirm Password", "confirmPassword": "Confirm Password",

View File

@@ -61,6 +61,7 @@
"emptyFields": "Le nom d'utilisateur et le mot de passe ne peuvent pas être vides", "emptyFields": "Le nom d'utilisateur et le mot de passe ne peuvent pas être vides",
"loginFailed": "Échec de la connexion, veuillez vérifier votre nom d'utilisateur et votre mot de passe", "loginFailed": "Échec de la connexion, veuillez vérifier votre nom d'utilisateur et votre mot de passe",
"loginError": "Une erreur est survenue lors de la connexion", "loginError": "Une erreur est survenue lors de la connexion",
"serverUnavailable": "Impossible de se connecter au serveur. Veuillez vérifier votre connexion réseau ou réessayer plus tard",
"currentPassword": "Mot de passe actuel", "currentPassword": "Mot de passe actuel",
"newPassword": "Nouveau mot de passe", "newPassword": "Nouveau mot de passe",
"confirmPassword": "Confirmer le mot de passe", "confirmPassword": "Confirmer le mot de passe",

View File

@@ -61,6 +61,7 @@
"emptyFields": "Kullanıcı adı ve şifre boş olamaz", "emptyFields": "Kullanıcı adı ve şifre boş olamaz",
"loginFailed": "Giriş başarısız, lütfen kullanıcı adınızı ve şifrenizi kontrol edin", "loginFailed": "Giriş başarısız, lütfen kullanıcı adınızı ve şifrenizi kontrol edin",
"loginError": "Giriş sırasında bir hata oluştu", "loginError": "Giriş sırasında bir hata oluştu",
"serverUnavailable": "Sunucuya bağlanılamıyor. Lütfen ağ bağlantınızı kontrol edin veya daha sonra tekrar deneyin",
"currentPassword": "Mevcut Şifre", "currentPassword": "Mevcut Şifre",
"newPassword": "Yeni Şifre", "newPassword": "Yeni Şifre",
"confirmPassword": "Şifreyi Onayla", "confirmPassword": "Şifreyi Onayla",

View File

@@ -61,6 +61,7 @@
"emptyFields": "用户名和密码不能为空", "emptyFields": "用户名和密码不能为空",
"loginFailed": "登录失败,请检查用户名和密码", "loginFailed": "登录失败,请检查用户名和密码",
"loginError": "登录过程中出现错误", "loginError": "登录过程中出现错误",
"serverUnavailable": "无法连接到服务器,请检查网络连接或稍后再试",
"currentPassword": "当前密码", "currentPassword": "当前密码",
"newPassword": "新密码", "newPassword": "新密码",
"confirmPassword": "确认密码", "confirmPassword": "确认密码",