diff --git a/frontend/public/locales/en-GB/translation.toml b/frontend/public/locales/en-GB/translation.toml index c955750bb..ec4d82120 100644 --- a/frontend/public/locales/en-GB/translation.toml +++ b/frontend/public/locales/en-GB/translation.toml @@ -6217,6 +6217,7 @@ description = "Enter the full URL of your self-hosted Stirling PDF server" [setup.server.error] emptyUrl = "Please enter a server URL" +invalidUrl = "Invalid URL format. Please enter a valid URL like https://your-server.com" unreachable = "Could not connect to server" testFailed = "Connection test failed" configFetch = "Failed to fetch server configuration. Please check the URL and try again." diff --git a/frontend/src/desktop/components/SetupWizard/ServerSelection.tsx b/frontend/src/desktop/components/SetupWizard/ServerSelection.tsx index c463ab9a4..1bbe1496f 100644 --- a/frontend/src/desktop/components/SetupWizard/ServerSelection.tsx +++ b/frontend/src/desktop/components/SetupWizard/ServerSelection.tsx @@ -20,36 +20,67 @@ export const ServerSelection: React.FC = ({ onSelect, load const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - // Normalize URL: trim and remove trailing slashes - const url = customUrl.trim().replace(/\/+$/, ''); + // Normalize and validate URL + let url = customUrl.trim().replace(/\/+$/, ''); if (!url) { setTestError(t('setup.server.error.emptyUrl', 'Please enter a server URL')); return; } + // Auto-add https:// if no protocol specified + if (!url.startsWith('http://') && !url.startsWith('https://')) { + console.log('[ServerSelection] No protocol specified, adding https://'); + url = `https://${url}`; + setCustomUrl(url); // Update the input field + } + + // Validate URL format + try { + const urlObj = new URL(url); + console.log('[ServerSelection] Valid URL:', { + protocol: urlObj.protocol, + hostname: urlObj.hostname, + port: urlObj.port, + pathname: urlObj.pathname, + }); + } catch (err) { + console.error('[ServerSelection] Invalid URL format:', err); + setTestError(t('setup.server.error.invalidUrl', 'Invalid URL format. Please enter a valid URL like https://your-server.com')); + return; + } + // Test connection before proceeding setTesting(true); setTestError(null); setSecurityDisabled(false); - try { - const isReachable = await connectionModeService.testConnection(url); + console.log(`[ServerSelection] Testing connection to: ${url}`); - if (!isReachable) { - setTestError(t('setup.server.error.unreachable', 'Could not connect to server')); + try { + const testResult = await connectionModeService.testConnection(url); + + if (!testResult.success) { + console.error('[ServerSelection] Connection test failed:', testResult); + setTestError(testResult.error || t('setup.server.error.unreachable', 'Could not connect to server')); setTesting(false); return; } + console.log('[ServerSelection] ✅ Connection test successful'); + // Fetch OAuth providers and check if login is enabled const enabledProviders: SSOProviderConfig[] = []; try { + console.log('[ServerSelection] Fetching login configuration...'); const response = await fetch(`${url}/api/v1/proprietary/ui-data/login`); // Check if security is disabled (status 403 or error response) if (!response.ok) { + console.warn(`[ServerSelection] Login config request failed with status ${response.status}`); + if (response.status === 403 || response.status === 401) { + console.log('[ServerSelection] Security is disabled on this server'); setSecurityDisabled(true); setTesting(false); return; @@ -65,10 +96,11 @@ export const ServerSelection: React.FC = ({ onSelect, load } const data = await response.json(); - console.log('Login UI data:', data); + console.log('[ServerSelection] Login UI data:', data); // Check if the response indicates security is disabled if (data.enableLogin === false || data.securityEnabled === false) { + console.log('[ServerSelection] Security is explicitly disabled in config'); setSecurityDisabled(true); setTesting(false); return; @@ -90,32 +122,39 @@ export const ServerSelection: React.FC = ({ onSelect, load }); }); - console.log('[ServerSelection] Detected OAuth providers:', enabledProviders); + console.log('[ServerSelection] ✅ Detected OAuth providers:', enabledProviders); } catch (err) { - console.error('[ServerSelection] Failed to fetch login configuration', err); + console.error('[ServerSelection] ❌ Failed to fetch login configuration:', err); // Check if it's a security disabled error if (err instanceof Error && (err.message.includes('403') || err.message.includes('401'))) { + console.log('[ServerSelection] Security is disabled (error-based detection)'); setSecurityDisabled(true); setTesting(false); return; } // For any other error (network, CORS, invalid JSON, etc.), show error and don't proceed + const errorMessage = err instanceof Error ? err.message : 'Unknown error'; + console.error('[ServerSelection] Configuration fetch error details:', errorMessage); + setTestError( - t('setup.server.error.configFetch', 'Failed to fetch server configuration. Please check the URL and try again.') + t('setup.server.error.configFetch', 'Failed to fetch server configuration: {{error}}', { + error: errorMessage + }) ); setTesting(false); return; } // Connection successful - pass URL and OAuth providers + console.log('[ServerSelection] ✅ Server selection complete, proceeding to login'); onSelect({ url, enabledOAuthProviders: enabledProviders.length > 0 ? enabledProviders : undefined, }); } catch (error) { - console.error('Connection test failed:', error); + console.error('[ServerSelection] ❌ Unexpected error during connection test:', error); setTestError( error instanceof Error ? error.message diff --git a/frontend/src/desktop/components/SetupWizard/index.tsx b/frontend/src/desktop/components/SetupWizard/index.tsx index b6e18a6c7..1d8ea370f 100644 --- a/frontend/src/desktop/components/SetupWizard/index.tsx +++ b/frontend/src/desktop/components/SetupWizard/index.tsx @@ -101,7 +101,12 @@ export const SetupWizard: React.FC = ({ onComplete }) => { }; const handleSelfHostedLogin = async (username: string, password: string) => { + console.log('[SetupWizard] 🔐 Starting self-hosted login'); + console.log(`[SetupWizard] Server: ${serverConfig?.url}`); + console.log(`[SetupWizard] Username: ${username}`); + if (!serverConfig) { + console.error('[SetupWizard] ❌ No server configured'); setError('No server configured'); return; } @@ -110,19 +115,35 @@ export const SetupWizard: React.FC = ({ onComplete }) => { setLoading(true); setError(null); + console.log('[SetupWizard] Step 1: Authenticating with server...'); await authService.login(serverConfig.url, username, password); + console.log('[SetupWizard] ✅ Authentication successful'); + + console.log('[SetupWizard] Step 2: Switching to self-hosted mode...'); await connectionModeService.switchToSelfHosted(serverConfig); + console.log('[SetupWizard] ✅ Switched to self-hosted mode'); + + console.log('[SetupWizard] Step 3: Initializing external backend...'); await tauriBackendService.initializeExternalBackend(); + console.log('[SetupWizard] ✅ External backend initialized'); + + console.log('[SetupWizard] ✅ Setup complete, calling onComplete()'); onComplete(); } catch (err) { - console.error('Self-hosted login failed:', err); - setError(err instanceof Error ? err.message : 'Self-hosted login failed'); + console.error('[SetupWizard] ❌ Self-hosted login failed:', err); + const errorMessage = err instanceof Error ? err.message : 'Self-hosted login failed'; + console.error('[SetupWizard] Error message:', errorMessage); + setError(errorMessage); setLoading(false); } }; const handleSelfHostedOAuthSuccess = async (_userInfo: UserInfo) => { + console.log('[SetupWizard] 🔐 OAuth login successful, completing setup'); + console.log(`[SetupWizard] Server: ${serverConfig?.url}`); + if (!serverConfig) { + console.error('[SetupWizard] ❌ No server configured'); setError('No server configured'); return; } @@ -131,13 +152,22 @@ export const SetupWizard: React.FC = ({ onComplete }) => { setLoading(true); setError(null); - // OAuth already completed by authService.loginWithOAuth + console.log('[SetupWizard] Step 1: OAuth already completed'); + console.log('[SetupWizard] Step 2: Switching to self-hosted mode...'); await connectionModeService.switchToSelfHosted(serverConfig); + console.log('[SetupWizard] ✅ Switched to self-hosted mode'); + + console.log('[SetupWizard] Step 3: Initializing external backend...'); await tauriBackendService.initializeExternalBackend(); + console.log('[SetupWizard] ✅ External backend initialized'); + + console.log('[SetupWizard] ✅ Setup complete, calling onComplete()'); onComplete(); } catch (err) { - console.error('Self-hosted OAuth login completion failed:', err); - setError(err instanceof Error ? err.message : 'Failed to complete login'); + console.error('[SetupWizard] ❌ Self-hosted OAuth login completion failed:', err); + const errorMessage = err instanceof Error ? err.message : 'Failed to complete login'; + console.error('[SetupWizard] Error message:', errorMessage); + setError(errorMessage); setLoading(false); } }; diff --git a/frontend/src/desktop/services/authService.ts b/frontend/src/desktop/services/authService.ts index 6f06dff69..78bb13ccf 100644 --- a/frontend/src/desktop/services/authService.ts +++ b/frontend/src/desktop/services/authService.ts @@ -223,19 +223,24 @@ export class AuthService { } async login(serverUrl: string, username: string, password: string): Promise { - try { - console.log('Logging in to:', serverUrl); + console.log(`[Desktop AuthService] 🔐 Starting login to: ${serverUrl}`); + console.log(`[Desktop AuthService] Username: ${username}`); + try { // Validate SaaS configuration if connecting to SaaS if (serverUrl === STIRLING_SAAS_URL) { if (!STIRLING_SAAS_URL) { + console.error('[Desktop AuthService] ❌ VITE_SAAS_SERVER_URL is not configured'); throw new Error('VITE_SAAS_SERVER_URL is not configured'); } if (!SUPABASE_KEY) { + console.error('[Desktop AuthService] ❌ VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY is not configured'); throw new Error('VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY is not configured'); } } + console.log('[Desktop AuthService] Invoking Rust login command...'); + // Call Rust login command (bypasses CORS) const response = await invoke('login', { serverUrl, @@ -247,21 +252,26 @@ export class AuthService { const { token, username: returnedUsername, email } = response; - console.log('[Desktop AuthService] Login successful, saving token...'); + console.log('[Desktop AuthService] ✅ Login response received'); + console.log(`[Desktop AuthService] Username from response: ${returnedUsername || username}`); // Save token to all storage locations try { + console.log('[Desktop AuthService] Saving token to storage...'); await this.saveTokenEverywhere(token); + console.log('[Desktop AuthService] ✅ Token saved successfully'); } catch (error) { - console.error('[Desktop AuthService] Failed to save token:', error); + console.error('[Desktop AuthService] ❌ Failed to save token:', error); throw new Error('Failed to save authentication token'); } // Save user info to store + console.log('[Desktop AuthService] Saving user info...'); await invoke('save_user_info', { username: returnedUsername || username, email, }); + console.log('[Desktop AuthService] ✅ User info saved'); const userInfo: UserInfo = { username: returnedUsername || username, @@ -270,10 +280,60 @@ export class AuthService { this.setAuthStatus('authenticated', userInfo); - console.log('Login successful'); + console.log('[Desktop AuthService] ✅ Login completed successfully'); return userInfo; } catch (error) { - console.error('Login failed:', error); + console.error('[Desktop AuthService] ❌ Login failed:', error); + + // Provide more detailed error messages based on the error type + if (error instanceof Error) { + const errMsg = error.message.toLowerCase(); + + // Authentication errors + if (errMsg.includes('401') || errMsg.includes('unauthorized') || errMsg.includes('invalid credentials')) { + console.error('[Desktop AuthService] Authentication failed - invalid credentials'); + this.setAuthStatus('unauthenticated', null); + throw new Error('Invalid username or password. Please check your credentials and try again.'); + } + // Server not found or unreachable + else if (errMsg.includes('connection refused') || errMsg.includes('econnrefused')) { + console.error('[Desktop AuthService] Server connection refused'); + this.setAuthStatus('unauthenticated', null); + throw new Error('Cannot connect to server. Please check the server URL and ensure the server is running.'); + } + // Timeout + else if (errMsg.includes('timeout') || errMsg.includes('timed out')) { + console.error('[Desktop AuthService] Login request timed out'); + this.setAuthStatus('unauthenticated', null); + throw new Error('Login request timed out. Please check your network connection and try again.'); + } + // DNS failure + else if (errMsg.includes('getaddrinfo') || errMsg.includes('dns') || errMsg.includes('not found') || errMsg.includes('enotfound')) { + console.error('[Desktop AuthService] DNS resolution failed'); + this.setAuthStatus('unauthenticated', null); + throw new Error('Cannot resolve server address. Please check the server URL is correct.'); + } + // SSL/TLS errors + else if (errMsg.includes('ssl') || errMsg.includes('tls') || errMsg.includes('certificate') || errMsg.includes('cert')) { + console.error('[Desktop AuthService] SSL/TLS error'); + this.setAuthStatus('unauthenticated', null); + throw new Error('SSL/TLS certificate error. Server may have an invalid or self-signed certificate.'); + } + // 404 - endpoint not found + else if (errMsg.includes('404') || errMsg.includes('not found')) { + console.error('[Desktop AuthService] Login endpoint not found'); + this.setAuthStatus('unauthenticated', null); + throw new Error('Login endpoint not found. Please ensure you are connecting to a valid Stirling PDF server.'); + } + // 403 - security disabled + else if (errMsg.includes('403') || errMsg.includes('forbidden')) { + console.error('[Desktop AuthService] Login disabled on server'); + this.setAuthStatus('unauthenticated', null); + throw new Error('Login is not enabled on this server. Please enable security mode (DOCKER_ENABLE_SECURITY=true).'); + } + } + + // Generic error fallback this.setAuthStatus('unauthenticated', null); throw error; } diff --git a/frontend/src/desktop/services/connectionModeService.ts b/frontend/src/desktop/services/connectionModeService.ts index c4db4d641..b47720a91 100644 --- a/frontend/src/desktop/services/connectionModeService.ts +++ b/frontend/src/desktop/services/connectionModeService.ts @@ -105,22 +105,95 @@ export class ConnectionModeService { console.log('Switched to self-hosted mode successfully'); } - async testConnection(url: string): Promise { + /** + * Test connection to a server URL and return detailed error information + * @returns Object with success status and optional error message + */ + async testConnection(url: string): Promise<{ success: boolean; error?: string; errorCode?: string }> { console.log(`[ConnectionModeService] Testing connection to: ${url}`); + try { // Test connection by hitting the health/status endpoint const healthUrl = `${url.replace(/\/$/, '')}/api/v1/info/status`; + console.log(`[ConnectionModeService] Health check URL: ${healthUrl}`); + const response = await fetch(healthUrl, { method: 'GET', connectTimeout: 10000, }); - const isOk = response.ok; - console.log(`[ConnectionModeService] Server connection test result: ${isOk}`); - return isOk; + if (response.ok) { + console.log(`[ConnectionModeService] ✅ Server connection test successful`); + return { success: true }; + } else { + const errorMsg = `Server returned status ${response.status}`; + console.error(`[ConnectionModeService] ❌ ${errorMsg}`); + return { + success: false, + error: errorMsg, + errorCode: `HTTP_${response.status}`, + }; + } } catch (error) { - console.warn('[ConnectionModeService] Server connection test failed:', error); - return false; + console.error('[ConnectionModeService] ❌ Server connection test failed:', error); + + // Extract detailed error information + if (error instanceof Error) { + const errMsg = error.message.toLowerCase(); + + // Connection refused + if (errMsg.includes('connection refused') || errMsg.includes('econnrefused')) { + return { + success: false, + error: 'Connection refused. Server may not be running or the port is incorrect.', + errorCode: 'CONNECTION_REFUSED', + }; + } + // Timeout + else if (errMsg.includes('timeout') || errMsg.includes('timed out')) { + return { + success: false, + error: 'Connection timed out. Server is not responding within 10 seconds.', + errorCode: 'TIMEOUT', + }; + } + // DNS failure + else if (errMsg.includes('getaddrinfo') || errMsg.includes('dns') || errMsg.includes('not found') || errMsg.includes('enotfound')) { + return { + success: false, + error: 'Cannot resolve server address. Please check the URL is correct.', + errorCode: 'DNS_FAILURE', + }; + } + // SSL/TLS errors + else if (errMsg.includes('ssl') || errMsg.includes('tls') || errMsg.includes('certificate') || errMsg.includes('cert')) { + return { + success: false, + error: 'SSL/TLS certificate error. Server may have an invalid or self-signed certificate.', + errorCode: 'SSL_ERROR', + }; + } + // Protocol errors + else if (errMsg.includes('protocol')) { + return { + success: false, + error: 'Protocol error. Try using https:// instead of http:// or vice versa.', + errorCode: 'PROTOCOL_ERROR', + }; + } + // Generic error + return { + success: false, + error: error.message, + errorCode: 'NETWORK_ERROR', + }; + } + + return { + success: false, + error: 'Unknown error occurred while testing connection', + errorCode: 'UNKNOWN', + }; } } diff --git a/frontend/src/desktop/services/tauriHttpClient.ts b/frontend/src/desktop/services/tauriHttpClient.ts index b5bbceb93..8ddad03ea 100644 --- a/frontend/src/desktop/services/tauriHttpClient.ts +++ b/frontend/src/desktop/services/tauriHttpClient.ts @@ -87,7 +87,7 @@ class TauriHttpClient { } } - private createError(message: string, config?: TauriHttpRequestConfig, code?: string, response?: TauriHttpResponse): TauriHttpError { + private createError(message: string, config?: TauriHttpRequestConfig, code?: string, response?: TauriHttpResponse, originalError?: unknown): TauriHttpError { const error = new Error(message) as TauriHttpError; error.config = config; error.code = code; @@ -99,6 +99,21 @@ class TauriHttpClient { config: error.config, code: error.code, }); + + // Log detailed error information for debugging + console.error('[TauriHttpClient] Error details:', { + message, + code, + url: config?.url, + method: config?.method, + status: response?.status, + originalError: originalError instanceof Error ? { + name: originalError.name, + message: originalError.message, + stack: originalError.stack, + } : originalError, + }); + return error; } @@ -224,10 +239,38 @@ class TauriHttpClient { // Check for HTTP errors if (!response.ok) { + // Create more descriptive error messages based on status code + let errorMessage = `Request failed with status code ${response.status}`; + let errorCode = 'ERR_BAD_REQUEST'; + + if (response.status === 401) { + errorMessage = 'Authentication failed - Invalid credentials'; + errorCode = 'ERR_UNAUTHORIZED'; + } else if (response.status === 403) { + errorMessage = 'Access denied - Insufficient permissions'; + errorCode = 'ERR_FORBIDDEN'; + } else if (response.status === 404) { + errorMessage = 'Endpoint not found - Server may not support this operation'; + errorCode = 'ERR_NOT_FOUND'; + } else if (response.status === 500) { + errorMessage = 'Internal server error - Please check server logs'; + errorCode = 'ERR_SERVER_ERROR'; + } else if (response.status === 502 || response.status === 503 || response.status === 504) { + errorMessage = 'Server unavailable or timeout - Please try again'; + errorCode = 'ERR_SERVICE_UNAVAILABLE'; + } + + console.error(`[TauriHttpClient] HTTP Error ${response.status}:`, { + url, + method, + status: response.status, + statusText: response.statusText, + }); + const error = this.createError( - `Request failed with status code ${response.status}`, + errorMessage, finalConfig, - 'ERR_BAD_REQUEST', + errorCode, httpResponse ); @@ -258,12 +301,66 @@ class TauriHttpClient { throw error; } - // Create new error for network/other failures - const errorMessage = error instanceof Error ? error.message : 'Network Error'; + // Create detailed error messages for network/other failures + let errorMessage = 'Network Error'; + let errorCode = 'ERR_NETWORK'; + + if (error instanceof Error) { + const errMsg = error.message.toLowerCase(); + + // Connection refused - server not running or wrong port + if (errMsg.includes('connection refused') || errMsg.includes('econnrefused')) { + errorMessage = `Unable to connect to server at ${url}. Server may not be running or port is incorrect.`; + errorCode = 'ERR_CONNECTION_REFUSED'; + } + // Timeout - server too slow or unreachable + else if (errMsg.includes('timeout') || errMsg.includes('timed out')) { + errorMessage = `Connection timed out to ${url}. Server is not responding or is too slow.`; + errorCode = 'ERR_TIMEOUT'; + } + // DNS failure - invalid domain or network issue + else if (errMsg.includes('getaddrinfo') || errMsg.includes('dns') || errMsg.includes('not found') || errMsg.includes('enotfound')) { + errorMessage = `Cannot resolve server address: ${url}. Please check the URL is correct.`; + errorCode = 'ERR_DNS_FAILURE'; + } + // SSL/TLS errors - certificate issues + else if (errMsg.includes('ssl') || errMsg.includes('tls') || errMsg.includes('certificate') || errMsg.includes('cert')) { + errorMessage = `SSL/TLS certificate error for ${url}. Server may have invalid or self-signed certificate.`; + errorCode = 'ERR_SSL_ERROR'; + } + // Protocol errors - wrong protocol (http vs https) + else if (errMsg.includes('protocol') || errMsg.includes('https') || errMsg.includes('http')) { + errorMessage = `Protocol error connecting to ${url}. Try using https:// instead of http:// or vice versa.`; + errorCode = 'ERR_PROTOCOL'; + } + // CORS errors + else if (errMsg.includes('cors')) { + errorMessage = `CORS error connecting to ${url}. Server may not allow requests from this application.`; + errorCode = 'ERR_CORS'; + } + // Generic error with original message + else { + errorMessage = `Network error: ${error.message}`; + errorCode = 'ERR_NETWORK'; + } + + console.error('[TauriHttpClient] Network error:', { + url, + method, + errorType: errorCode, + originalMessage: error.message, + stack: error.stack, + }); + } else { + console.error('[TauriHttpClient] Unknown error type:', error); + } + const httpError = this.createError( errorMessage, finalConfig, - 'ERR_NETWORK' + errorCode, + undefined, + error ); // Run error interceptors