- Implement OAuth 2.0 and PAT authentication methods - Add user management, roles, and profile functionality - Add database migrations and admin user scripts - Update services for authentication and user settings - Add protected routes and permission hooks - Update documentation for authentication and database access
299 lines
8.4 KiB
TypeScript
299 lines
8.4 KiB
TypeScript
/**
|
|
* 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<UserSettings | null> {
|
|
const db = getAuthDatabase();
|
|
try {
|
|
const settings = await db.queryOne<UserSettings>(
|
|
'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<UserSettings> {
|
|
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<UserSettings> {
|
|
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<boolean> {
|
|
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<boolean> {
|
|
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<string | null> {
|
|
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<boolean> {
|
|
const settings = await this.getUserSettings(userId);
|
|
return settings?.web_search_enabled || false;
|
|
}
|
|
|
|
/**
|
|
* Get Tavily API key for user
|
|
*/
|
|
async getTavilyApiKey(userId: number): Promise<string | null> {
|
|
const settings = await this.getUserSettings(userId);
|
|
return settings?.tavily_api_key || null;
|
|
}
|
|
}
|
|
|
|
export const userSettingsService = new UserSettingsService();
|