Desktop self-hosted connection logging (#5410)

Also added in automotic protocol addition if missing. Defaults to
https://

---------

Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
This commit is contained in:
ConnorYoh
2026-01-09 23:01:31 +00:00
committed by GitHub
parent a284dbfc15
commit 72389f5872
6 changed files with 334 additions and 34 deletions

View File

@@ -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."

View File

@@ -20,36 +20,67 @@ export const ServerSelection: React.FC<ServerSelectionProps> = ({ 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<ServerSelectionProps> = ({ 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<ServerSelectionProps> = ({ 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

View File

@@ -101,7 +101,12 @@ export const SetupWizard: React.FC<SetupWizardProps> = ({ 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<SetupWizardProps> = ({ 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<SetupWizardProps> = ({ 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);
}
};

View File

@@ -223,19 +223,24 @@ export class AuthService {
}
async login(serverUrl: string, username: string, password: string): Promise<UserInfo> {
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<LoginResponse>('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;
}

View File

@@ -105,22 +105,95 @@ export class ConnectionModeService {
console.log('Switched to self-hosted mode successfully');
}
async testConnection(url: string): Promise<boolean> {
/**
* 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',
};
}
}

View File

@@ -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