diff --git a/src/backend/database/routes/users.ts b/src/backend/database/routes/users.ts index 1a9f4f47..f56bb423 100644 --- a/src/backend/database/routes/users.ts +++ b/src/backend/database/routes/users.ts @@ -242,11 +242,11 @@ router.post('/oidc-config', authenticateJWT, async (req, res) => { userinfoUrlValue: `"${userinfo_url}"` }); - const isDisableRequest = (!client_id || client_id.trim() === '') && - (!client_secret || client_secret.trim() === '') && - (!issuer_url || issuer_url.trim() === '') && - (!authorization_url || authorization_url.trim() === '') && - (!token_url || token_url.trim() === ''); + const isDisableRequest = (client_id === '' || client_id === null || client_id === undefined) && + (client_secret === '' || client_secret === null || client_secret === undefined) && + (issuer_url === '' || issuer_url === null || issuer_url === undefined) && + (authorization_url === '' || authorization_url === null || authorization_url === undefined) && + (token_url === '' || token_url === null || token_url === undefined); const isEnableRequest = isNonEmptyString(client_id) && isNonEmptyString(client_secret) && isNonEmptyString(issuer_url) && isNonEmptyString(authorization_url) && @@ -259,11 +259,11 @@ router.post('/oidc-config', authenticateJWT, async (req, res) => { isDisableRequest, isEnableRequest, disableChecks: { - clientIdEmpty: !client_id || client_id.trim() === '', - clientSecretEmpty: !client_secret || client_secret.trim() === '', - issuerUrlEmpty: !issuer_url || issuer_url.trim() === '', - authUrlEmpty: !authorization_url || authorization_url.trim() === '', - tokenUrlEmpty: !token_url || token_url.trim() === '' + clientIdEmpty: client_id === '' || client_id === null || client_id === undefined, + clientSecretEmpty: client_secret === '' || client_secret === null || client_secret === undefined, + issuerUrlEmpty: issuer_url === '' || issuer_url === null || issuer_url === undefined, + authUrlEmpty: authorization_url === '' || authorization_url === null || authorization_url === undefined, + tokenUrlEmpty: token_url === '' || token_url === null || token_url === undefined }, enableChecks: { clientIdPresent: isNonEmptyString(client_id), @@ -315,6 +315,27 @@ router.post('/oidc-config', authenticateJWT, async (req, res) => { } }); +// Route: Disable OIDC configuration (admin only) +// DELETE /users/oidc-config +router.delete('/oidc-config', authenticateJWT, async (req, res) => { + const userId = (req as any).userId; + try { + const user = await db.select().from(users).where(eq(users.id, userId)); + if (!user || user.length === 0 || !user[0].is_admin) { + return res.status(403).json({error: 'Not authorized'}); + } + + authLogger.info('OIDC disable request received', { operation: 'oidc_disable', userId }); + + db.$client.prepare("DELETE FROM settings WHERE key = 'oidc_config'").run(); + authLogger.success('OIDC configuration disabled', { operation: 'oidc_disable', userId }); + res.json({message: 'OIDC configuration disabled'}); + } catch (err) { + authLogger.error('Failed to disable OIDC config', err); + res.status(500).json({error: 'Failed to disable OIDC config'}); + } +}); + // Route: Get OIDC configuration // GET /users/oidc-config router.get('/oidc-config', async (req, res) => { diff --git a/src/backend/utils/logger.ts b/src/backend/utils/logger.ts index ee139b19..b59b1e6f 100644 --- a/src/backend/utils/logger.ts +++ b/src/backend/utils/logger.ts @@ -158,7 +158,6 @@ class Logger { } } -// Service-specific loggers export const databaseLogger = new Logger('DATABASE', 'đŸ—„ī¸', '#1e3a8a'); export const sshLogger = new Logger('SSH', 'đŸ–Ĩī¸', '#1e3a8a'); export const tunnelLogger = new Logger('TUNNEL', '📡', '#1e3a8a'); @@ -168,5 +167,4 @@ export const apiLogger = new Logger('API', '🌐', '#3b82f6'); export const authLogger = new Logger('AUTH', '🔐', '#dc2626'); export const systemLogger = new Logger('SYSTEM', '🚀', '#1e3a8a'); -// Default logger for general use export const logger = systemLogger; diff --git a/src/lib/frontend-logger.ts b/src/lib/frontend-logger.ts new file mode 100644 index 00000000..acc45b02 --- /dev/null +++ b/src/lib/frontend-logger.ts @@ -0,0 +1,267 @@ +/** + * Frontend Logger - A comprehensive logging utility for the frontend + * Based on the backend logger patterns but adapted for browser environment + */ + +export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'success'; + +export interface LogContext { + operation?: string; + userId?: string; + hostId?: number; + tunnelName?: string; + sessionId?: string; + requestId?: string; + duration?: number; + method?: string; + url?: string; + status?: number; + statusText?: string; + responseTime?: number; + retryCount?: number; + errorCode?: string; + errorMessage?: string; + [key: string]: any; +} + +class FrontendLogger { + private serviceName: string; + private serviceIcon: string; + private serviceColor: string; + private isDevelopment: boolean; + + constructor(serviceName: string, serviceIcon: string, serviceColor: string) { + this.serviceName = serviceName; + this.serviceIcon = serviceIcon; + this.serviceColor = serviceColor; + this.isDevelopment = process.env.NODE_ENV === 'development'; + } + + private getTimeStamp(): string { + const now = new Date(); + return `[${now.toLocaleTimeString()}.${now.getMilliseconds().toString().padStart(3, '0')}]`; + } + + private formatMessage(level: LogLevel, message: string, context?: LogContext): string { + const timestamp = this.getTimeStamp(); + const levelTag = this.getLevelTag(level); + const serviceTag = this.serviceIcon; + + let contextStr = ''; + if (context && this.isDevelopment) { + const contextParts = []; + if (context.operation) contextParts.push(context.operation); + if (context.userId) contextParts.push(`user:${context.userId}`); + if (context.hostId) contextParts.push(`host:${context.hostId}`); + if (context.tunnelName) contextParts.push(`tunnel:${context.tunnelName}`); + if (context.sessionId) contextParts.push(`session:${context.sessionId}`); + if (context.responseTime) contextParts.push(`${context.responseTime}ms`); + if (context.status) contextParts.push(`status:${context.status}`); + if (context.errorCode) contextParts.push(`code:${context.errorCode}`); + + if (contextParts.length > 0) { + contextStr = ` (${contextParts.join(', ')})`; + } + } + + return `${timestamp} ${levelTag} ${serviceTag} ${message}${contextStr}`; + } + + private getLevelTag(level: LogLevel): string { + const symbols = { + debug: '🔍', + info: 'â„šī¸', + warn: 'âš ī¸', + error: '❌', + success: '✅' + }; + return `${symbols[level]} [${level.toUpperCase()}]`; + } + + private shouldLog(level: LogLevel): boolean { + if (level === 'debug' && !this.isDevelopment) { + return false; + } + return true; + } + + private log(level: LogLevel, message: string, context?: LogContext, error?: unknown): void { + if (!this.shouldLog(level)) return; + + const formattedMessage = this.formatMessage(level, message, context); + + switch (level) { + case 'debug': + console.debug(formattedMessage); + break; + case 'info': + console.log(formattedMessage); + break; + case 'warn': + console.warn(formattedMessage); + break; + case 'error': + console.error(formattedMessage); + if (error) { + console.error('Error details:', error); + } + break; + case 'success': + console.log(formattedMessage); + break; + } + } + + debug(message: string, context?: LogContext): void { + this.log('debug', message, context); + } + + info(message: string, context?: LogContext): void { + this.log('info', message, context); + } + + warn(message: string, context?: LogContext): void { + this.log('warn', message, context); + } + + error(message: string, error?: unknown, context?: LogContext): void { + this.log('error', message, context, error); + } + + success(message: string, context?: LogContext): void { + this.log('success', message, context); + } + + // Convenience methods for common operations + api(message: string, context?: LogContext): void { + this.info(`API: ${message}`, { ...context, operation: 'api' }); + } + + request(message: string, context?: LogContext): void { + this.info(`REQUEST: ${message}`, { ...context, operation: 'request' }); + } + + response(message: string, context?: LogContext): void { + this.info(`RESPONSE: ${message}`, { ...context, operation: 'response' }); + } + + auth(message: string, context?: LogContext): void { + this.info(`AUTH: ${message}`, { ...context, operation: 'auth' }); + } + + ssh(message: string, context?: LogContext): void { + this.info(`SSH: ${message}`, { ...context, operation: 'ssh' }); + } + + tunnel(message: string, context?: LogContext): void { + this.info(`TUNNEL: ${message}`, { ...context, operation: 'tunnel' }); + } + + file(message: string, context?: LogContext): void { + this.info(`FILE: ${message}`, { ...context, operation: 'file' }); + } + + connection(message: string, context?: LogContext): void { + this.info(`CONNECTION: ${message}`, { ...context, operation: 'connection' }); + } + + disconnect(message: string, context?: LogContext): void { + this.info(`DISCONNECT: ${message}`, { ...context, operation: 'disconnect' }); + } + + retry(message: string, context?: LogContext): void { + this.warn(`RETRY: ${message}`, { ...context, operation: 'retry' }); + } + + performance(message: string, context?: LogContext): void { + this.info(`PERFORMANCE: ${message}`, { ...context, operation: 'performance' }); + } + + security(message: string, context?: LogContext): void { + this.warn(`SECURITY: ${message}`, { ...context, operation: 'security' }); + } + + // Specialized logging methods for different scenarios + requestStart(method: string, url: string, context?: LogContext): void { + this.request(`Starting ${method.toUpperCase()} request`, { + ...context, + method: method.toUpperCase(), + url: this.sanitizeUrl(url) + }); + } + + requestSuccess(method: string, url: string, status: number, responseTime: number, context?: LogContext): void { + this.response(`Request completed successfully`, { + ...context, + method: method.toUpperCase(), + url: this.sanitizeUrl(url), + status, + responseTime + }); + } + + requestError(method: string, url: string, status: number, errorMessage: string, responseTime?: number, context?: LogContext): void { + this.error(`Request failed`, undefined, { + ...context, + method: method.toUpperCase(), + url: this.sanitizeUrl(url), + status, + errorMessage, + responseTime + }); + } + + networkError(method: string, url: string, errorMessage: string, context?: LogContext): void { + this.error(`Network error occurred`, undefined, { + ...context, + method: method.toUpperCase(), + url: this.sanitizeUrl(url), + errorMessage, + errorCode: 'NETWORK_ERROR' + }); + } + + authError(method: string, url: string, context?: LogContext): void { + this.security(`Authentication failed`, { + ...context, + method: method.toUpperCase(), + url: this.sanitizeUrl(url), + errorCode: 'AUTH_REQUIRED' + }); + } + + retryAttempt(method: string, url: string, attempt: number, maxAttempts: number, context?: LogContext): void { + this.retry(`Retry attempt ${attempt}/${maxAttempts}`, { + ...context, + method: method.toUpperCase(), + url: this.sanitizeUrl(url), + retryCount: attempt + }); + } + + private sanitizeUrl(url: string): string { + // Remove sensitive information from URLs for logging + try { + const urlObj = new URL(url); + // Remove query parameters that might contain sensitive data + if (urlObj.searchParams.has('password') || urlObj.searchParams.has('token')) { + urlObj.search = ''; + } + return urlObj.toString(); + } catch { + return url; + } + } +} + +// Service-specific loggers +export const apiLogger = new FrontendLogger('API', '🌐', '#3b82f6'); +export const authLogger = new FrontendLogger('AUTH', '🔐', '#dc2626'); +export const sshLogger = new FrontendLogger('SSH', 'đŸ–Ĩī¸', '#1e3a8a'); +export const tunnelLogger = new FrontendLogger('TUNNEL', '📡', '#1e3a8a'); +export const fileLogger = new FrontendLogger('FILE', '📁', '#1e3a8a'); +export const statsLogger = new FrontendLogger('STATS', '📊', '#22c55e'); +export const systemLogger = new FrontendLogger('SYSTEM', '🚀', '#1e3a8a'); + +// Default logger for general use +export const logger = systemLogger; diff --git a/src/ui/Desktop/Admin/AdminSettings.tsx b/src/ui/Desktop/Admin/AdminSettings.tsx index e78b59c0..0b077f6d 100644 --- a/src/ui/Desktop/Admin/AdminSettings.tsx +++ b/src/ui/Desktop/Admin/AdminSettings.tsx @@ -24,6 +24,7 @@ import { getUserList, updateRegistrationAllowed, updateOIDCConfig, + disableOIDCConfig, makeUserAdmin, removeAdminStatus, deleteUser @@ -329,7 +330,7 @@ export function AdminSettings({isTopbarOpen = true}: AdminSettingsProps): React. setOidcError(null); setOidcLoading(true); try { - await updateOIDCConfig(emptyConfig); + await disableOIDCConfig(); toast.success(t('admin.oidcConfigurationDisabled')); } catch (err: any) { setOidcError(err?.response?.data?.error || t('admin.failedToDisableOidcConfig')); diff --git a/src/ui/main-axios.ts b/src/ui/main-axios.ts index 3cfbc732..b5d0e014 100644 --- a/src/ui/main-axios.ts +++ b/src/ui/main-axios.ts @@ -11,6 +11,7 @@ import type { FileManagerFile, FileManagerShortcut } from '../types/index.js'; +import { apiLogger, authLogger, sshLogger, tunnelLogger, fileLogger, statsLogger, systemLogger, type LogContext } from '../lib/frontend-logger.js'; interface FileManagerOperation { name: string; @@ -98,45 +99,112 @@ function getCookie(name: string): string | undefined { } } -function createApiInstance(baseURL: string): AxiosInstance { +function createApiInstance(baseURL: string, serviceName: string = 'API'): AxiosInstance { const instance = axios.create({ baseURL, headers: { 'Content-Type': 'application/json' }, timeout: 30000, }); + // Request interceptor with enhanced logging instance.interceptors.request.use((config) => { + const startTime = performance.now(); + const requestId = `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + + // Store timing and request ID for response logging + (config as any).startTime = startTime; + (config as any).requestId = requestId; + const token = getCookie('jwt'); + const context: LogContext = { + requestId, + method: config.method?.toUpperCase(), + url: config.url, + operation: 'request_start' + }; + if (token) { config.headers.Authorization = `Bearer ${token}`; - } else { - console.log('No token found, Authorization header not set'); + } else if (process.env.NODE_ENV === 'development') { + authLogger.warn('No JWT token found, request will be unauthenticated', context); } + return config; }); + // Response interceptor with comprehensive logging instance.interceptors.response.use( (response) => { - // Log successful requests in development + const endTime = performance.now(); + const startTime = (response.config as any).startTime; + const requestId = (response.config as any).requestId; + const responseTime = Math.round(endTime - startTime); + + const context: LogContext = { + requestId, + method: response.config.method?.toUpperCase(), + url: response.config.url, + status: response.status, + statusText: response.statusText, + responseTime, + operation: 'request_success' + }; + + // Only log successful requests in development and for slow requests if (process.env.NODE_ENV === 'development') { - console.log(`✅ API ${response.config.method?.toUpperCase()} ${response.config.url} - ${response.status}`); + const method = response.config.method?.toUpperCase() || 'UNKNOWN'; + const url = response.config.url || 'UNKNOWN'; + + // Log based on service type + if (serviceName.includes('SSH') || serviceName.includes('ssh')) { + sshLogger.info(`${method} ${url} - ${response.status} (${responseTime}ms)`, context); + } else if (serviceName.includes('TUNNEL') || serviceName.includes('tunnel')) { + tunnelLogger.info(`${method} ${url} - ${response.status} (${responseTime}ms)`, context); + } else if (serviceName.includes('FILE') || serviceName.includes('file')) { + fileLogger.info(`${method} ${url} - ${response.status} (${responseTime}ms)`, context); + } else if (serviceName.includes('STATS') || serviceName.includes('stats')) { + statsLogger.info(`${method} ${url} - ${response.status} (${responseTime}ms)`, context); + } else { + apiLogger.info(`${method} ${url} - ${response.status} (${responseTime}ms)`, context); + } } + + // Performance logging for slow requests + if (responseTime > 3000) { + apiLogger.warn(`Slow request: ${responseTime}ms`, context); + } + return response; }, (error: AxiosError) => { - // Improved error logging + const endTime = performance.now(); + const startTime = (error.config as any)?.startTime; + const requestId = (error.config as any)?.requestId; + const responseTime = startTime ? Math.round(endTime - startTime) : undefined; + const method = error.config?.method?.toUpperCase() || 'UNKNOWN'; const url = error.config?.url || 'UNKNOWN'; - const status = error.response?.status || 'NETWORK_ERROR'; - const message = error.response?.data?.error || (error as Error).message || 'Unknown error'; - - console.error(`❌ API ${method} ${url} - ${status}: ${message}`); - - if (error.response?.status === 401) { - console.warn('🔐 Authentication failed, clearing token'); + const status = error.response?.status; + const message = (error.response?.data as any)?.error || (error as Error).message || 'Unknown error'; + const errorCode = (error.response?.data as any)?.code || error.code; + + const context: LogContext = { + requestId, + method, + url, + status, + responseTime, + errorCode, + errorMessage: message, + operation: 'request_error' + }; + + // Only handle auth token clearing here, let handleApiError do the logging + if (status === 401) { document.cookie = 'jwt=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'; localStorage.removeItem('jwt'); } + return Promise.reject(error); } ); @@ -173,37 +241,52 @@ function getApiUrl(path: string, defaultPort: number): string { // Multi-port backend architecture (original design) // SSH Host Management API (port 8081) export let sshHostApi = createApiInstance( - getApiUrl('/ssh', 8081) + getApiUrl('/ssh', 8081), + 'SSH_HOST' ); // Tunnel Management API (port 8083) export let tunnelApi = createApiInstance( - getApiUrl('/ssh', 8083) + getApiUrl('/ssh', 8083), + 'TUNNEL' ); // File Manager Operations API (port 8084) - SSH file operations export let fileManagerApi = createApiInstance( - getApiUrl('/ssh/file_manager', 8084) + getApiUrl('/ssh/file_manager', 8084), + 'FILE_MANAGER' ); // Server Statistics API (port 8085) export let statsApi = createApiInstance( - getApiUrl('', 8085) + getApiUrl('', 8085), + 'STATS' ); // Authentication API (port 8081) - includes users, alerts, version, releases export let authApi = createApiInstance( - getApiUrl('', 8081) + getApiUrl('', 8081), + 'AUTH' ); // Function to update API instances with new port (for Electron) function updateApiPorts(port: number) { + systemLogger.info('Updating API instances with new port', { + operation: 'api_port_update', + newPort: port + }); + apiPort = port; - sshHostApi = createApiInstance(`http://127.0.0.1:${port}/ssh`); - tunnelApi = createApiInstance(`http://127.0.0.1:${port}/ssh`); - fileManagerApi = createApiInstance(`http://127.0.0.1:${port}/ssh/file_manager`); - statsApi = createApiInstance(`http://127.0.0.1:${port}`); - authApi = createApiInstance(`http://127.0.0.1:${port}`); + sshHostApi = createApiInstance(`http://127.0.0.1:${port}/ssh`, 'SSH_HOST'); + tunnelApi = createApiInstance(`http://127.0.0.1:${port}/ssh`, 'TUNNEL'); + fileManagerApi = createApiInstance(`http://127.0.0.1:${port}/ssh/file_manager`, 'FILE_MANAGER'); + statsApi = createApiInstance(`http://127.0.0.1:${port}`, 'STATS'); + authApi = createApiInstance(`http://127.0.0.1:${port}`, 'AUTH'); + + systemLogger.success('All API instances updated successfully', { + operation: 'api_port_update_complete', + port + }); } // ============================================================================ @@ -222,35 +305,51 @@ class ApiError extends Error { } function handleApiError(error: unknown, operation: string): never { + const context: LogContext = { + operation: 'error_handling', + errorOperation: operation + }; + if (axios.isAxiosError(error)) { const status = error.response?.status; const message = error.response?.data?.error || error.message; const code = error.response?.data?.code; + const url = error.config?.url; + const method = error.config?.method?.toUpperCase(); - // Enhanced error logging - console.error(`🚨 API Error in ${operation}:`, { + const errorContext: LogContext = { + ...context, + method, + url, status, - message, - code, - url: error.config?.url, - method: error.config?.method - }); + errorCode: code, + errorMessage: message + }; + // Enhanced error logging with appropriate logger if (status === 401) { + authLogger.warn(`Auth failed: ${method} ${url} - ${message}`, errorContext); throw new ApiError('Authentication required. Please log in again.', 401, 'AUTH_REQUIRED'); } else if (status === 403) { + authLogger.warn(`Access denied: ${method} ${url}`, errorContext); throw new ApiError('Access denied. You do not have permission to perform this action.', 403, 'ACCESS_DENIED'); } else if (status === 404) { + apiLogger.warn(`Not found: ${method} ${url}`, errorContext); throw new ApiError('Resource not found. The requested item may have been deleted.', 404, 'NOT_FOUND'); } else if (status === 409) { + apiLogger.warn(`Conflict: ${method} ${url}`, errorContext); throw new ApiError('Conflict. The resource already exists or is in use.', 409, 'CONFLICT'); } else if (status === 422) { + apiLogger.warn(`Validation error: ${method} ${url} - ${message}`, errorContext); throw new ApiError('Validation error. Please check your input and try again.', 422, 'VALIDATION_ERROR'); } else if (status && status >= 500) { + apiLogger.error(`Server error: ${method} ${url} - ${message}`, error, errorContext); throw new ApiError('Server error occurred. Please try again later.', status, 'SERVER_ERROR'); } else if (status === 0) { + apiLogger.error(`Network error: ${method} ${url} - ${message}`, error, errorContext); throw new ApiError('Network error. Please check your connection and try again.', 0, 'NETWORK_ERROR'); } else { + apiLogger.error(`Request failed: ${method} ${url} - ${message}`, error, errorContext); throw new ApiError(message || `Failed to ${operation}`, status, code); } } @@ -260,7 +359,7 @@ function handleApiError(error: unknown, operation: string): never { } const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - console.error(`🚨 Unexpected error in ${operation}:`, error); + apiLogger.error(`Unexpected error during ${operation}: ${errorMessage}`, error, context); throw new ApiError(`Unexpected error during ${operation}: ${errorMessage}`, undefined, 'UNKNOWN_ERROR'); } @@ -893,6 +992,15 @@ export async function updateOIDCConfig(config: any): Promise { } } +export async function disableOIDCConfig(): Promise { + try { + const response = await authApi.delete('/users/oidc-config'); + return response.data; + } catch (error) { + handleApiError(error, 'disable OIDC config'); + } +} + // ============================================================================ // ALERTS // ============================================================================