Add authentication, user management, and database migration features
- 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
This commit is contained in:
298
backend/src/services/userSettingsService.ts
Normal file
298
backend/src/services/userSettingsService.ts
Normal file
@@ -0,0 +1,298 @@
|
||||
/**
|
||||
* 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();
|
||||
Reference in New Issue
Block a user