/** * User Settings Service * * Manages user-specific settings including Jira PAT, AI features, and API keys. */ import { logger } from './logger.js'; import { getAuthDatabase } from './database/migrations.js'; import { encryptionService } from './encryptionService.js'; import { config } from '../config/env.js'; const isPostgres = (): boolean => { return process.env.DATABASE_TYPE === 'postgres' || process.env.DATABASE_TYPE === 'postgresql'; }; export interface UserSettings { user_id: number; jira_pat: string | null; jira_pat_encrypted: boolean; ai_enabled: boolean; ai_provider: string | null; ai_api_key: string | null; web_search_enabled: boolean; tavily_api_key: string | null; updated_at: string; } export interface UpdateUserSettingsInput { jira_pat?: string; ai_enabled?: boolean; ai_provider?: 'openai' | 'anthropic'; ai_api_key?: string; web_search_enabled?: boolean; tavily_api_key?: string; } class UserSettingsService { /** * Get user settings */ async getUserSettings(userId: number): Promise { const db = getAuthDatabase(); try { const settings = await db.queryOne( 'SELECT * FROM user_settings WHERE user_id = ?', [userId] ); if (!settings) { // Create default settings return await this.createDefaultSettings(userId); } // Decrypt sensitive fields if encrypted if (settings.jira_pat && settings.jira_pat_encrypted && encryptionService.isConfigured()) { try { settings.jira_pat = await encryptionService.decrypt(settings.jira_pat); } catch (error) { logger.error('Failed to decrypt Jira PAT:', error); settings.jira_pat = null; } } if (settings.ai_api_key && encryptionService.isConfigured()) { try { settings.ai_api_key = await encryptionService.decrypt(settings.ai_api_key); } catch (error) { logger.error('Failed to decrypt AI API key:', error); settings.ai_api_key = null; } } if (settings.tavily_api_key && encryptionService.isConfigured()) { try { settings.tavily_api_key = await encryptionService.decrypt(settings.tavily_api_key); } catch (error) { logger.error('Failed to decrypt Tavily API key:', error); settings.tavily_api_key = null; } } return settings; } finally { await db.close(); } } /** * Create default settings for user */ async createDefaultSettings(userId: number): Promise { const db = getAuthDatabase(); const now = new Date().toISOString(); try { await db.execute( `INSERT INTO user_settings ( user_id, jira_pat, jira_pat_encrypted, ai_enabled, ai_provider, ai_api_key, web_search_enabled, tavily_api_key, updated_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [ userId, null, isPostgres() ? true : 1, isPostgres() ? false : 0, null, null, isPostgres() ? false : 0, null, now, ] ); return await this.getUserSettings(userId) as UserSettings; } finally { await db.close(); } } /** * Update user settings */ async updateUserSettings(userId: number, input: UpdateUserSettingsInput): Promise { const db = getAuthDatabase(); const now = new Date().toISOString(); try { // Ensure settings exist let settings = await this.getUserSettings(userId); if (!settings) { settings = await this.createDefaultSettings(userId); } const updates: string[] = []; const values: any[] = []; if (input.jira_pat !== undefined) { let encryptedPat: string | null = null; if (input.jira_pat) { if (encryptionService.isConfigured()) { encryptedPat = await encryptionService.encrypt(input.jira_pat); } else { // Store unencrypted if encryption not configured (development) encryptedPat = input.jira_pat; } } updates.push('jira_pat = ?'); updates.push('jira_pat_encrypted = ?'); values.push(encryptedPat); values.push(encryptionService.isConfigured() ? (isPostgres() ? true : 1) : (isPostgres() ? false : 0)); } if (input.ai_enabled !== undefined) { updates.push('ai_enabled = ?'); values.push(isPostgres() ? input.ai_enabled : (input.ai_enabled ? 1 : 0)); } if (input.ai_provider !== undefined) { updates.push('ai_provider = ?'); values.push(input.ai_provider); } if (input.ai_api_key !== undefined) { let encryptedKey: string | null = null; if (input.ai_api_key) { if (encryptionService.isConfigured()) { encryptedKey = await encryptionService.encrypt(input.ai_api_key); } else { encryptedKey = input.ai_api_key; } } updates.push('ai_api_key = ?'); values.push(encryptedKey); } if (input.web_search_enabled !== undefined) { updates.push('web_search_enabled = ?'); values.push(isPostgres() ? input.web_search_enabled : (input.web_search_enabled ? 1 : 0)); } if (input.tavily_api_key !== undefined) { let encryptedKey: string | null = null; if (input.tavily_api_key) { if (encryptionService.isConfigured()) { encryptedKey = await encryptionService.encrypt(input.tavily_api_key); } else { encryptedKey = input.tavily_api_key; } } updates.push('tavily_api_key = ?'); values.push(encryptedKey); } if (updates.length === 0) { return settings; } updates.push('updated_at = ?'); values.push(now); values.push(userId); await db.execute( `UPDATE user_settings SET ${updates.join(', ')} WHERE user_id = ?`, values ); logger.info(`User settings updated for user: ${userId}`); return await this.getUserSettings(userId) as UserSettings; } finally { await db.close(); } } /** * Validate Jira PAT by testing connection */ async validateJiraPat(userId: number, pat?: string): Promise { try { const settings = await this.getUserSettings(userId); const tokenToTest = pat || settings?.jira_pat; if (!tokenToTest) { return false; } // Test connection to Jira const testUrl = `${config.jiraHost}/rest/api/2/myself`; const response = await fetch(testUrl, { headers: { 'Authorization': `Bearer ${tokenToTest}`, 'Accept': 'application/json', }, }); return response.ok; } catch (error) { logger.error('Jira PAT validation failed:', error); return false; } } /** * Get Jira PAT status */ async getJiraPatStatus(userId: number): Promise<{ configured: boolean; valid: boolean }> { const settings = await this.getUserSettings(userId); const configured = !!settings?.jira_pat; if (!configured) { return { configured: false, valid: false }; } const valid = await this.validateJiraPat(userId); return { configured: true, valid }; } /** * Check if AI features are enabled for user */ async isAiEnabled(userId: number): Promise { const settings = await this.getUserSettings(userId); return settings?.ai_enabled || false; } /** * Get AI provider for user */ async getAiProvider(userId: number): Promise<'openai' | 'anthropic' | null> { const settings = await this.getUserSettings(userId); return (settings?.ai_provider as 'openai' | 'anthropic') || null; } /** * Get AI API key for user */ async getAiApiKey(userId: number): Promise { const settings = await this.getUserSettings(userId); return settings?.ai_api_key || null; } /** * Check if web search is enabled for user */ async isWebSearchEnabled(userId: number): Promise { const settings = await this.getUserSettings(userId); return settings?.web_search_enabled || false; } /** * Get Tavily API key for user */ async getTavilyApiKey(userId: number): Promise { const settings = await this.getUserSettings(userId); return settings?.tavily_api_key || null; } } export const userSettingsService = new UserSettingsService();