From 4bd07c3ece989aab8206224b872406dab81685a0 Mon Sep 17 00:00:00 2001 From: Haileyesus <118998054+blackmammoth@users.noreply.github.com> Date: Fri, 24 Apr 2026 15:11:25 +0300 Subject: [PATCH] refactor: update import paths for database modules and remove legacy db.js and schema.js files --- eslint.config.js | 3 +- server/database/db.js | 615 ------------------ server/database/schema.js | 102 --- server/index.js | 2 +- server/middleware/auth.js | 2 +- server/modules/database/index.ts | 3 +- .../database/repositories/sessions.db.ts | 78 +++ server/modules/database/schema.ts | 7 +- server/projects.js | 2 +- server/routes/agent.js | 2 +- server/routes/auth.js | 6 +- server/routes/codex.js | 2 +- server/routes/gemini.js | 2 +- server/routes/projects.js | 17 +- server/routes/settings.js | 2 +- server/routes/user.js | 2 +- server/services/notification-orchestrator.js | 2 +- server/services/vapid-keys.js | 3 +- 18 files changed, 102 insertions(+), 750 deletions(-) delete mode 100644 server/database/db.js delete mode 100644 server/database/schema.js diff --git a/eslint.config.js b/eslint.config.js index 742f0c2b..6419a9fd 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -165,9 +165,8 @@ export default tseslint.config( pattern: [ "server/projects.js", "server/sessionManager.js", - "server/database/*.{js,ts}", "server/utils/runtime-paths.js", - ], // provider history loading still resolves session data through these legacy runtime/database files + ], // provider history loading still resolves session data through these legacy runtime files mode: "file", }, { diff --git a/server/database/db.js b/server/database/db.js deleted file mode 100644 index cfcc6c08..00000000 --- a/server/database/db.js +++ /dev/null @@ -1,615 +0,0 @@ -import Database from 'better-sqlite3'; -import path from 'path'; -import fs from 'fs'; -import crypto from 'crypto'; -import { findAppRoot, getModuleDir } from '../utils/runtime-paths.js'; -import { - APP_CONFIG_TABLE_SQL, - USER_NOTIFICATION_PREFERENCES_TABLE_SQL, - VAPID_KEYS_TABLE_SQL, - PUSH_SUBSCRIPTIONS_TABLE_SQL, - SESSIONS_TABLE_SQL, - SESSIONS_LOOKUP_INDEX_SQL, - DATABASE_SCHEMA_SQL -} from './schema.js'; - -const __dirname = getModuleDir(import.meta.url); -// The compiled backend lives under dist-server/server/database, but the install root we log -// should still point at the project/app root. Resolving it here avoids build-layout drift. -const APP_ROOT = findAppRoot(__dirname); - -// ANSI color codes for terminal output -const colors = { - reset: '\x1b[0m', - bright: '\x1b[1m', - cyan: '\x1b[36m', - dim: '\x1b[2m', -}; - -const c = { - info: (text) => `${colors.cyan}${text}${colors.reset}`, - bright: (text) => `${colors.bright}${text}${colors.reset}`, - dim: (text) => `${colors.dim}${text}${colors.reset}`, -}; - -// Use DATABASE_PATH environment variable if set, otherwise use default location -const DB_PATH = process.env.DATABASE_PATH || path.join(__dirname, 'auth.db'); - -// Ensure database directory exists if custom path is provided -if (process.env.DATABASE_PATH) { - const dbDir = path.dirname(DB_PATH); - try { - if (!fs.existsSync(dbDir)) { - fs.mkdirSync(dbDir, { recursive: true }); - console.log(`Created database directory: ${dbDir}`); - } - } catch (error) { - console.error(`Failed to create database directory ${dbDir}:`, error.message); - throw error; - } -} - -// As part of 1.19.2 we are introducing a new location for auth.db. The below handles exisitng moving legacy database from install directory to new location -const LEGACY_DB_PATH = path.join(__dirname, 'auth.db'); -if (DB_PATH !== LEGACY_DB_PATH && !fs.existsSync(DB_PATH) && fs.existsSync(LEGACY_DB_PATH)) { - try { - fs.copyFileSync(LEGACY_DB_PATH, DB_PATH); - console.log(`[MIGRATION] Copied database from ${LEGACY_DB_PATH} to ${DB_PATH}`); - for (const suffix of ['-wal', '-shm']) { - if (fs.existsSync(LEGACY_DB_PATH + suffix)) { - fs.copyFileSync(LEGACY_DB_PATH + suffix, DB_PATH + suffix); - } - } - } catch (err) { - console.warn(`[MIGRATION] Could not copy legacy database: ${err.message}`); - } -} - -// Create database connection -const db = new Database(DB_PATH); - -// app_config must exist before any other module imports (auth.js reads the JWT secret at load time). -// runMigrations() also creates this table, but it runs too late for existing installations -// where auth.js is imported before initializeDatabase() is called. -db.exec(APP_CONFIG_TABLE_SQL); - -// Show app installation path prominently -const appInstallPath = APP_ROOT; -console.log(''); -console.log(c.dim('═'.repeat(60))); -console.log(`${c.info('[INFO]')} App Installation: ${c.bright(appInstallPath)}`); -console.log(`${c.info('[INFO]')} Database: ${c.dim(path.relative(appInstallPath, DB_PATH))}`); -if (process.env.DATABASE_PATH) { - console.log(` ${c.dim('(Using custom DATABASE_PATH from environment)')}`); -} -console.log(c.dim('═'.repeat(60))); -console.log(''); - -const runMigrations = () => { - try { - const tableInfo = db.prepare("PRAGMA table_info(users)").all(); - const columnNames = tableInfo.map(col => col.name); - - if (!columnNames.includes('git_name')) { - console.log('Running migration: Adding git_name column'); - db.exec('ALTER TABLE users ADD COLUMN git_name TEXT'); - } - - if (!columnNames.includes('git_email')) { - console.log('Running migration: Adding git_email column'); - db.exec('ALTER TABLE users ADD COLUMN git_email TEXT'); - } - - if (!columnNames.includes('has_completed_onboarding')) { - console.log('Running migration: Adding has_completed_onboarding column'); - db.exec('ALTER TABLE users ADD COLUMN has_completed_onboarding BOOLEAN DEFAULT 0'); - } - - db.exec(USER_NOTIFICATION_PREFERENCES_TABLE_SQL); - db.exec(VAPID_KEYS_TABLE_SQL); - db.exec(PUSH_SUBSCRIPTIONS_TABLE_SQL); - db.exec(APP_CONFIG_TABLE_SQL); - const hasLegacySessionNamesTable = Boolean( - db.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?").get('session_names') - ); - const hasSessionsTable = Boolean( - db.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?").get('sessions') - ); - - if (hasLegacySessionNamesTable && hasSessionsTable) { - console.log('Running migration: Merging session_names into sessions'); - db.exec(` - INSERT OR REPLACE INTO sessions (session_id, provider, custom_name, created_at, updated_at) - SELECT session_id, provider, custom_name, created_at, updated_at - FROM session_names - `); - db.exec('DROP TABLE session_names'); - } else if (hasLegacySessionNamesTable && !hasSessionsTable) { - console.log('Running migration: Renaming session_names table to sessions'); - db.exec('ALTER TABLE session_names RENAME TO sessions'); - } - - // Remove legacy index name if present and ensure the new index exists. - db.exec('DROP INDEX IF EXISTS idx_session_names_lookup'); - db.exec(SESSIONS_TABLE_SQL); - db.exec(SESSIONS_LOOKUP_INDEX_SQL); - - console.log('Database migrations completed successfully'); - } catch (error) { - console.error('Error running migrations:', error.message); - throw error; - } -}; - -// Initialize database with schema -const initializeDatabase = async () => { - try { - db.exec(DATABASE_SCHEMA_SQL); - console.log('Database initialized successfully'); - runMigrations(); - } catch (error) { - console.error('Error initializing database:', error.message); - throw error; - } -}; - -// User database operations -const userDb = { - // Check if any users exist - hasUsers: () => { - try { - const row = db.prepare('SELECT COUNT(*) as count FROM users').get(); - return row.count > 0; - } catch (err) { - throw err; - } - }, - - // Create a new user - createUser: (username, passwordHash) => { - try { - const stmt = db.prepare('INSERT INTO users (username, password_hash) VALUES (?, ?)'); - const result = stmt.run(username, passwordHash); - return { id: result.lastInsertRowid, username }; - } catch (err) { - throw err; - } - }, - - // Get user by username - getUserByUsername: (username) => { - try { - const row = db.prepare('SELECT * FROM users WHERE username = ? AND is_active = 1').get(username); - return row; - } catch (err) { - throw err; - } - }, - - // Update last login time (non-fatal — logged but not thrown) - updateLastLogin: (userId) => { - try { - db.prepare('UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE id = ?').run(userId); - } catch (err) { - console.warn('Failed to update last login:', err.message); - } - }, - - // Get user by ID - getUserById: (userId) => { - try { - const row = db.prepare('SELECT id, username, created_at, last_login FROM users WHERE id = ? AND is_active = 1').get(userId); - return row; - } catch (err) { - throw err; - } - }, - - getFirstUser: () => { - try { - const row = db.prepare('SELECT id, username, created_at, last_login FROM users WHERE is_active = 1 LIMIT 1').get(); - return row; - } catch (err) { - throw err; - } - }, - - updateGitConfig: (userId, gitName, gitEmail) => { - try { - const stmt = db.prepare('UPDATE users SET git_name = ?, git_email = ? WHERE id = ?'); - stmt.run(gitName, gitEmail, userId); - } catch (err) { - throw err; - } - }, - - getGitConfig: (userId) => { - try { - const row = db.prepare('SELECT git_name, git_email FROM users WHERE id = ?').get(userId); - return row; - } catch (err) { - throw err; - } - }, - - completeOnboarding: (userId) => { - try { - const stmt = db.prepare('UPDATE users SET has_completed_onboarding = 1 WHERE id = ?'); - stmt.run(userId); - } catch (err) { - throw err; - } - }, - - hasCompletedOnboarding: (userId) => { - try { - const row = db.prepare('SELECT has_completed_onboarding FROM users WHERE id = ?').get(userId); - return row?.has_completed_onboarding === 1; - } catch (err) { - throw err; - } - } -}; - -// API Keys database operations -const apiKeysDb = { - // Generate a new API key - generateApiKey: () => { - return 'ck_' + crypto.randomBytes(32).toString('hex'); - }, - - // Create a new API key - createApiKey: (userId, keyName) => { - try { - const apiKey = apiKeysDb.generateApiKey(); - const stmt = db.prepare('INSERT INTO api_keys (user_id, key_name, api_key) VALUES (?, ?, ?)'); - const result = stmt.run(userId, keyName, apiKey); - return { id: result.lastInsertRowid, keyName, apiKey }; - } catch (err) { - throw err; - } - }, - - // Get all API keys for a user - getApiKeys: (userId) => { - try { - const rows = db.prepare('SELECT id, key_name, api_key, created_at, last_used, is_active FROM api_keys WHERE user_id = ? ORDER BY created_at DESC').all(userId); - return rows; - } catch (err) { - throw err; - } - }, - - // Validate API key and get user - validateApiKey: (apiKey) => { - try { - const row = db.prepare(` - SELECT u.id, u.username, ak.id as api_key_id - FROM api_keys ak - JOIN users u ON ak.user_id = u.id - WHERE ak.api_key = ? AND ak.is_active = 1 AND u.is_active = 1 - `).get(apiKey); - - if (row) { - // Update last_used timestamp - db.prepare('UPDATE api_keys SET last_used = CURRENT_TIMESTAMP WHERE id = ?').run(row.api_key_id); - } - - return row; - } catch (err) { - throw err; - } - }, - - // Delete an API key - deleteApiKey: (userId, apiKeyId) => { - try { - const stmt = db.prepare('DELETE FROM api_keys WHERE id = ? AND user_id = ?'); - const result = stmt.run(apiKeyId, userId); - return result.changes > 0; - } catch (err) { - throw err; - } - }, - - // Toggle API key active status - toggleApiKey: (userId, apiKeyId, isActive) => { - try { - const stmt = db.prepare('UPDATE api_keys SET is_active = ? WHERE id = ? AND user_id = ?'); - const result = stmt.run(isActive ? 1 : 0, apiKeyId, userId); - return result.changes > 0; - } catch (err) { - throw err; - } - } -}; - -// User credentials database operations (for GitHub tokens, GitLab tokens, etc.) -const credentialsDb = { - // Create a new credential - createCredential: (userId, credentialName, credentialType, credentialValue, description = null) => { - try { - const stmt = db.prepare('INSERT INTO user_credentials (user_id, credential_name, credential_type, credential_value, description) VALUES (?, ?, ?, ?, ?)'); - const result = stmt.run(userId, credentialName, credentialType, credentialValue, description); - return { id: result.lastInsertRowid, credentialName, credentialType }; - } catch (err) { - throw err; - } - }, - - // Get all credentials for a user, optionally filtered by type - getCredentials: (userId, credentialType = null) => { - try { - let query = 'SELECT id, credential_name, credential_type, description, created_at, is_active FROM user_credentials WHERE user_id = ?'; - const params = [userId]; - - if (credentialType) { - query += ' AND credential_type = ?'; - params.push(credentialType); - } - - query += ' ORDER BY created_at DESC'; - - const rows = db.prepare(query).all(...params); - return rows; - } catch (err) { - throw err; - } - }, - - // Get active credential value for a user by type (returns most recent active) - getActiveCredential: (userId, credentialType) => { - try { - const row = db.prepare('SELECT credential_value FROM user_credentials WHERE user_id = ? AND credential_type = ? AND is_active = 1 ORDER BY created_at DESC LIMIT 1').get(userId, credentialType); - return row?.credential_value || null; - } catch (err) { - throw err; - } - }, - - // Delete a credential - deleteCredential: (userId, credentialId) => { - try { - const stmt = db.prepare('DELETE FROM user_credentials WHERE id = ? AND user_id = ?'); - const result = stmt.run(credentialId, userId); - return result.changes > 0; - } catch (err) { - throw err; - } - }, - - // Toggle credential active status - toggleCredential: (userId, credentialId, isActive) => { - try { - const stmt = db.prepare('UPDATE user_credentials SET is_active = ? WHERE id = ? AND user_id = ?'); - const result = stmt.run(isActive ? 1 : 0, credentialId, userId); - return result.changes > 0; - } catch (err) { - throw err; - } - } -}; - -const DEFAULT_NOTIFICATION_PREFERENCES = { - channels: { - inApp: false, - webPush: false - }, - events: { - actionRequired: true, - stop: true, - error: true - } -}; - -const normalizeNotificationPreferences = (value) => { - const source = value && typeof value === 'object' ? value : {}; - - return { - channels: { - inApp: source.channels?.inApp === true, - webPush: source.channels?.webPush === true - }, - events: { - actionRequired: source.events?.actionRequired !== false, - stop: source.events?.stop !== false, - error: source.events?.error !== false - } - }; -}; - -const notificationPreferencesDb = { - getPreferences: (userId) => { - try { - const row = db.prepare('SELECT preferences_json FROM user_notification_preferences WHERE user_id = ?').get(userId); - if (!row) { - const defaults = normalizeNotificationPreferences(DEFAULT_NOTIFICATION_PREFERENCES); - db.prepare( - 'INSERT INTO user_notification_preferences (user_id, preferences_json, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP)' - ).run(userId, JSON.stringify(defaults)); - return defaults; - } - - let parsed; - try { - parsed = JSON.parse(row.preferences_json); - } catch { - parsed = DEFAULT_NOTIFICATION_PREFERENCES; - } - return normalizeNotificationPreferences(parsed); - } catch (err) { - throw err; - } - }, - - updatePreferences: (userId, preferences) => { - try { - const normalized = normalizeNotificationPreferences(preferences); - db.prepare( - `INSERT INTO user_notification_preferences (user_id, preferences_json, updated_at) - VALUES (?, ?, CURRENT_TIMESTAMP) - ON CONFLICT(user_id) DO UPDATE SET - preferences_json = excluded.preferences_json, - updated_at = CURRENT_TIMESTAMP` - ).run(userId, JSON.stringify(normalized)); - return normalized; - } catch (err) { - throw err; - } - } -}; - -const pushSubscriptionsDb = { - saveSubscription: (userId, endpoint, keysP256dh, keysAuth) => { - try { - db.prepare( - `INSERT INTO push_subscriptions (user_id, endpoint, keys_p256dh, keys_auth) - VALUES (?, ?, ?, ?) - ON CONFLICT(endpoint) DO UPDATE SET - user_id = excluded.user_id, - keys_p256dh = excluded.keys_p256dh, - keys_auth = excluded.keys_auth` - ).run(userId, endpoint, keysP256dh, keysAuth); - } catch (err) { - throw err; - } - }, - - getSubscriptions: (userId) => { - try { - return db.prepare('SELECT endpoint, keys_p256dh, keys_auth FROM push_subscriptions WHERE user_id = ?').all(userId); - } catch (err) { - throw err; - } - }, - - removeSubscription: (endpoint) => { - try { - db.prepare('DELETE FROM push_subscriptions WHERE endpoint = ?').run(endpoint); - } catch (err) { - throw err; - } - }, - - removeAllForUser: (userId) => { - try { - db.prepare('DELETE FROM push_subscriptions WHERE user_id = ?').run(userId); - } catch (err) { - throw err; - } - } -}; - -// Session custom names database operations -const sessionsDb = { - // Set (insert or update) a custom session name - setName: (sessionId, provider, customName) => { - db.prepare(` - INSERT INTO sessions (session_id, provider, custom_name) - VALUES (?, ?, ?) - ON CONFLICT(session_id, provider) - DO UPDATE SET custom_name = excluded.custom_name, updated_at = CURRENT_TIMESTAMP - `).run(sessionId, provider, customName); - }, - - // Get a single custom session name - getName: (sessionId, provider) => { - const row = db.prepare( - 'SELECT custom_name FROM sessions WHERE session_id = ? AND provider = ?' - ).get(sessionId, provider); - return row?.custom_name || null; - }, - - // Batch lookup — returns Map - getNames: (sessionIds, provider) => { - if (!sessionIds.length) return new Map(); - const placeholders = sessionIds.map(() => '?').join(','); - const rows = db.prepare( - `SELECT session_id, custom_name FROM sessions - WHERE session_id IN (${placeholders}) AND provider = ?` - ).all(...sessionIds, provider); - return new Map(rows.map(r => [r.session_id, r.custom_name])); - }, - - // Delete a custom session name - deleteName: (sessionId, provider) => { - return db.prepare( - 'DELETE FROM sessions WHERE session_id = ? AND provider = ?' - ).run(sessionId, provider).changes > 0; - }, -}; - -// Apply custom session names from the database (overrides CLI-generated summaries) -function applyCustomSessionNames(sessions, provider) { - if (!sessions?.length) return; - try { - const ids = sessions.map(s => s.id); - const customNames = sessionsDb.getNames(ids, provider); - for (const session of sessions) { - const custom = customNames.get(session.id); - if (custom) session.summary = custom; - } - } catch (error) { - console.warn(`[DB] Failed to apply custom session names for ${provider}:`, error.message); - } -} - -// App config database operations -const appConfigDb = { - get: (key) => { - try { - const row = db.prepare('SELECT value FROM app_config WHERE key = ?').get(key); - return row?.value || null; - } catch (err) { - return null; - } - }, - - set: (key, value) => { - db.prepare( - 'INSERT INTO app_config (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value' - ).run(key, value); - }, - - getOrCreateJwtSecret: () => { - let secret = appConfigDb.get('jwt_secret'); - if (!secret) { - secret = crypto.randomBytes(64).toString('hex'); - appConfigDb.set('jwt_secret', secret); - } - return secret; - } -}; - -// Backward compatibility - keep old names pointing to new system -const githubTokensDb = { - createGithubToken: (userId, tokenName, githubToken, description = null) => { - return credentialsDb.createCredential(userId, tokenName, 'github_token', githubToken, description); - }, - getGithubTokens: (userId) => { - return credentialsDb.getCredentials(userId, 'github_token'); - }, - getActiveGithubToken: (userId) => { - return credentialsDb.getActiveCredential(userId, 'github_token'); - }, - deleteGithubToken: (userId, tokenId) => { - return credentialsDb.deleteCredential(userId, tokenId); - }, - toggleGithubToken: (userId, tokenId, isActive) => { - return credentialsDb.toggleCredential(userId, tokenId, isActive); - } -}; - -export { - db, - initializeDatabase, - userDb, - apiKeysDb, - credentialsDb, - notificationPreferencesDb, - pushSubscriptionsDb, - sessionsDb, - applyCustomSessionNames, - appConfigDb, - githubTokensDb // Backward compatibility -}; diff --git a/server/database/schema.js b/server/database/schema.js deleted file mode 100644 index 9415fb8b..00000000 --- a/server/database/schema.js +++ /dev/null @@ -1,102 +0,0 @@ -export const APP_CONFIG_TABLE_SQL = `CREATE TABLE IF NOT EXISTS app_config ( - key TEXT PRIMARY KEY, - value TEXT NOT NULL, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP -);`; - -export const USER_NOTIFICATION_PREFERENCES_TABLE_SQL = `CREATE TABLE IF NOT EXISTS user_notification_preferences ( - user_id INTEGER PRIMARY KEY, - preferences_json TEXT NOT NULL, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE -);`; - -export const VAPID_KEYS_TABLE_SQL = `CREATE TABLE IF NOT EXISTS vapid_keys ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - public_key TEXT NOT NULL, - private_key TEXT NOT NULL, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP -);`; - -export const PUSH_SUBSCRIPTIONS_TABLE_SQL = `CREATE TABLE IF NOT EXISTS push_subscriptions ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id INTEGER NOT NULL, - endpoint TEXT NOT NULL UNIQUE, - keys_p256dh TEXT NOT NULL, - keys_auth TEXT NOT NULL, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE -);`; - -export const SESSIONS_TABLE_SQL = `CREATE TABLE IF NOT EXISTS sessions ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - session_id TEXT NOT NULL, - provider TEXT NOT NULL DEFAULT 'claude', - custom_name TEXT NOT NULL, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, - UNIQUE(session_id, provider) -);`; - -export const SESSIONS_LOOKUP_INDEX_SQL = `CREATE INDEX IF NOT EXISTS idx_sessions_lookup ON sessions(session_id, provider);`; - -export const DATABASE_SCHEMA_SQL = `PRAGMA foreign_keys = ON; - -CREATE TABLE IF NOT EXISTS users ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - username TEXT UNIQUE NOT NULL, - password_hash TEXT NOT NULL, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - last_login DATETIME, - is_active BOOLEAN DEFAULT 1, - git_name TEXT, - git_email TEXT, - has_completed_onboarding BOOLEAN DEFAULT 0 -); - -CREATE INDEX IF NOT EXISTS idx_users_username ON users(username); -CREATE INDEX IF NOT EXISTS idx_users_active ON users(is_active); - -CREATE TABLE IF NOT EXISTS api_keys ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id INTEGER NOT NULL, - key_name TEXT NOT NULL, - api_key TEXT UNIQUE NOT NULL, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - last_used DATETIME, - is_active BOOLEAN DEFAULT 1, - FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE -); - -CREATE INDEX IF NOT EXISTS idx_api_keys_key ON api_keys(api_key); -CREATE INDEX IF NOT EXISTS idx_api_keys_user_id ON api_keys(user_id); -CREATE INDEX IF NOT EXISTS idx_api_keys_active ON api_keys(is_active); - -CREATE TABLE IF NOT EXISTS user_credentials ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id INTEGER NOT NULL, - credential_name TEXT NOT NULL, - credential_type TEXT NOT NULL, - credential_value TEXT NOT NULL, - description TEXT, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - is_active BOOLEAN DEFAULT 1, - FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE -); - -CREATE INDEX IF NOT EXISTS idx_user_credentials_user_id ON user_credentials(user_id); -CREATE INDEX IF NOT EXISTS idx_user_credentials_type ON user_credentials(credential_type); -CREATE INDEX IF NOT EXISTS idx_user_credentials_active ON user_credentials(is_active); - -${USER_NOTIFICATION_PREFERENCES_TABLE_SQL} - -${VAPID_KEYS_TABLE_SQL} - -${PUSH_SUBSCRIPTIONS_TABLE_SQL} - -${SESSIONS_TABLE_SQL} - -${SESSIONS_LOOKUP_INDEX_SQL} - -${APP_CONFIG_TABLE_SQL} -`; diff --git a/server/index.js b/server/index.js index bf7b3da0..61e80085 100755 --- a/server/index.js +++ b/server/index.js @@ -50,7 +50,7 @@ import pluginsRoutes from './routes/plugins.js'; import messagesRoutes from './routes/messages.js'; import providerRoutes from './modules/providers/provider.routes.js'; import { startEnabledPluginServers, stopAllPlugins, getPluginPort } from './utils/plugin-process-manager.js'; -import { initializeDatabase, sessionsDb, applyCustomSessionNames } from './database/db.js'; +import { initializeDatabase, sessionsDb, applyCustomSessionNames } from './modules/database/index.js'; import { configureWebPush } from './services/vapid-keys.js'; import { validateApiKey, authenticateToken, authenticateWebSocket } from './middleware/auth.js'; import { IS_PLATFORM } from './constants/config.js'; diff --git a/server/middleware/auth.js b/server/middleware/auth.js index 73749792..c40237b2 100644 --- a/server/middleware/auth.js +++ b/server/middleware/auth.js @@ -1,5 +1,5 @@ import jwt from 'jsonwebtoken'; -import { userDb, appConfigDb } from '../database/db.js'; +import { userDb, appConfigDb } from '../modules/database/index.js'; import { IS_PLATFORM } from '../constants/config.js'; // Use env var if set, otherwise auto-generate a unique secret per installation diff --git a/server/modules/database/index.ts b/server/modules/database/index.ts index db74fd5b..8a11d3ac 100644 --- a/server/modules/database/index.ts +++ b/server/modules/database/index.ts @@ -1,3 +1,4 @@ +export { initializeDatabase } from '@/modules/database/init-db.js'; export { apiKeysDb } from '@/modules/database/repositories/api-keys.js'; export { appConfigDb } from '@/modules/database/repositories/app-config.js'; export { credentialsDb } from '@/modules/database/repositories/credentials.js'; @@ -6,6 +7,6 @@ export { notificationPreferencesDb } from '@/modules/database/repositories/notif export { projectsDb } from '@/modules/database/repositories/projects.db.js'; export { pushSubscriptionsDb } from '@/modules/database/repositories/push-subscriptions.js'; export { scanStateDb } from '@/modules/database/repositories/scan-state.db.js'; -export { sessionsDb } from '@/modules/database/repositories/sessions.db.js'; +export { sessionsDb, applyCustomSessionNames } from '@/modules/database/repositories/sessions.db.js'; export { userDb } from '@/modules/database/repositories/users.js'; export { vapidKeysDb } from '@/modules/database/repositories/vapid-keys.js'; diff --git a/server/modules/database/repositories/sessions.db.ts b/server/modules/database/repositories/sessions.db.ts index 29c5b935..7a2346e8 100644 --- a/server/modules/database/repositories/sessions.db.ts +++ b/server/modules/database/repositories/sessions.db.ts @@ -23,6 +23,11 @@ type SessionMetadataLookupRow = Pick< 'session_id' | 'provider' | 'project_path' | 'jsonl_path' | 'custom_name' | 'created_at' | 'updated_at' >; +type LegacySessionSummary = { + id: string; + summary?: string; +}; + function normalizeTimestamp(value?: string): string | null { if (!value) return null; @@ -185,8 +190,81 @@ export const sessionsDb = { return new Map(rows.map((row) => [row.session_id, row.custom_name])); }, + /** + * Legacy-compatibility method kept for parity with `server/database/db.js`. + * TODO: Remove after all legacy imports are migrated to the new repository API. + */ + setName(sessionId: string, provider: string, customName: string): void { + const db = getConnection(); + db.prepare( + `INSERT INTO sessions (session_id, provider, custom_name) + VALUES (?, ?, ?) + ON CONFLICT(session_id, provider) DO UPDATE SET + custom_name = excluded.custom_name, + updated_at = CURRENT_TIMESTAMP` + ).run(sessionId, provider, customName); + }, + + /** + * Legacy-compatibility method kept for parity with `server/database/db.js`. + * TODO: Remove after all legacy imports are migrated to the new repository API. + */ + getName(sessionId: string, provider: string): string | null { + return sessionsDb.getSessionName(sessionId, provider); + }, + + /** + * Legacy-compatibility method kept for parity with `server/database/db.js`. + * TODO: Remove after all legacy imports are migrated to the new repository API. + */ + getNames(sessionIds: string[], provider: string): Map { + return sessionsDb.getSessionNames(sessionIds, provider); + }, + + /** + * Legacy-compatibility method kept for parity with `server/database/db.js`. + * TODO: Remove after all legacy imports are migrated to the new repository API. + */ + deleteName(sessionId: string, provider: string): boolean { + const db = getConnection(); + return ( + db + .prepare( + `DELETE FROM sessions + WHERE session_id = ? AND provider = ?` + ) + .run(sessionId, provider).changes > 0 + ); + }, + deleteSession(sessionId: string): void { const db = getConnection(); db.prepare('DELETE FROM sessions WHERE session_id = ?').run(sessionId); }, }; + +/** + * Legacy-compatibility helper kept for parity with `server/database/db.js`. + * TODO: Remove after all legacy imports are migrated to the new repository API. + */ +export function applyCustomSessionNames( + sessions: LegacySessionSummary[] | null | undefined, + provider: string +): void { + if (!sessions?.length) return; + + try { + const sessionIds = sessions.map((session) => session.id); + const customNames = sessionsDb.getNames(sessionIds, provider); + + for (const session of sessions) { + const customName = customNames.get(session.id); + if (customName) { + session.summary = customName; + } + } + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + console.warn(`[DB] Failed to apply custom session names for ${provider}:`, message); + } +} diff --git a/server/modules/database/schema.ts b/server/modules/database/schema.ts index 0e7ff21c..690bc7eb 100644 --- a/server/modules/database/schema.ts +++ b/server/modules/database/schema.ts @@ -138,12 +138,13 @@ ${PUSH_SUBSCRIPTIONS_TABLE_SCHEMA_SQL} CREATE INDEX IF NOT EXISTS idx_push_subscriptions_user_id ON push_subscriptions(user_id); ${PROJECTS_TABLE_SCHEMA_SQL} -CREATE INDEX IF NOT EXISTS idx_projects_is_starred ON projects(isStarred); -CREATE INDEX IF NOT EXISTS idx_projects_is_archived ON projects(isArchived); +-- NOTE: These indexes are created in migrations after legacy table-shape repairs. +-- Creating them here can fail on upgraded installs where projects lacks those columns. ${SESSIONS_TABLE_SCHEMA_SQL} CREATE INDEX IF NOT EXISTS idx_session_ids_lookup ON sessions(session_id); -CREATE INDEX IF NOT EXISTS idx_sessions_project_path ON sessions(project_path); +-- NOTE: This index is created in migrations after sessions is rebuilt to include project_path. +-- Creating it here can fail on upgraded installs where the legacy sessions table has no project_path. ${LAST_SCANNED_AT_SQL} diff --git a/server/projects.js b/server/projects.js index 8d8cf5c8..8b7b22e7 100755 --- a/server/projects.js +++ b/server/projects.js @@ -65,7 +65,7 @@ import crypto from 'crypto'; import Database from 'better-sqlite3'; import os from 'os'; import sessionManager from './sessionManager.js'; -import { applyCustomSessionNames } from './database/db.js'; +import { applyCustomSessionNames } from './modules/database/index.js'; import { getModuleDir, findAppRoot } from './utils/runtime-paths.js'; // Snapshot files are kept as incrementing artifacts under .tmp/project-dumps for later review. diff --git a/server/routes/agent.js b/server/routes/agent.js index cdcd3a65..36a683cb 100644 --- a/server/routes/agent.js +++ b/server/routes/agent.js @@ -4,7 +4,7 @@ import path from 'path'; import os from 'os'; import { promises as fs } from 'fs'; import crypto from 'crypto'; -import { userDb, apiKeysDb, githubTokensDb } from '../database/db.js'; +import { userDb, apiKeysDb, githubTokensDb } from '../modules/database/index.js'; import { addProjectManually } from '../projects.js'; import { queryClaudeSDK } from '../claude-sdk.js'; import { spawnCursor } from '../cursor-cli.js'; diff --git a/server/routes/auth.js b/server/routes/auth.js index be4c38c1..dcb2e3ff 100644 --- a/server/routes/auth.js +++ b/server/routes/auth.js @@ -1,9 +1,11 @@ import express from 'express'; import bcrypt from 'bcrypt'; -import { userDb, db } from '../database/db.js'; +import { userDb } from '../modules/database/index.js'; +import { getConnection } from '../modules/database/connection.js'; import { generateToken, authenticateToken } from '../middleware/auth.js'; const router = express.Router(); +const db = getConnection(); // Check auth status and setup requirements router.get('/status', async (req, res) => { @@ -132,4 +134,4 @@ router.post('/logout', authenticateToken, (req, res) => { res.json({ success: true, message: 'Logged out successfully' }); }); -export default router; \ No newline at end of file +export default router; diff --git a/server/routes/codex.js b/server/routes/codex.js index 359b231a..1bf26990 100644 --- a/server/routes/codex.js +++ b/server/routes/codex.js @@ -1,6 +1,6 @@ import express from 'express'; import { deleteCodexSession } from '../projects.js'; -import { sessionsDb } from '../database/db.js'; +import { sessionsDb } from '../modules/database/index.js'; const router = express.Router(); diff --git a/server/routes/gemini.js b/server/routes/gemini.js index 2ff83b71..81b11100 100644 --- a/server/routes/gemini.js +++ b/server/routes/gemini.js @@ -1,6 +1,6 @@ import express from 'express'; import sessionManager from '../sessionManager.js'; -import { sessionsDb } from '../database/db.js'; +import { sessionsDb } from '../modules/database/index.js'; const router = express.Router(); diff --git a/server/routes/projects.js b/server/routes/projects.js index cf3a62e4..51a3f89d 100644 --- a/server/routes/projects.js +++ b/server/routes/projects.js @@ -4,6 +4,7 @@ import path from 'path'; import { spawn } from 'child_process'; import os from 'os'; import { addProjectManually } from '../projects.js'; +import { githubTokensDb } from '../modules/database/index.js'; const router = express.Router(); @@ -311,21 +312,7 @@ router.post('/create-workspace', async (req, res) => { * Helper function to get GitHub token from database */ async function getGithubTokenById(tokenId, userId) { - const { db } = await import('../database/db.js'); - - const credential = db.prepare( - 'SELECT * FROM user_credentials WHERE id = ? AND user_id = ? AND credential_type = ? AND is_active = 1' - ).get(tokenId, userId, 'github_token'); - - // Return in the expected format (github_token field for compatibility) - if (credential) { - return { - ...credential, - github_token: credential.credential_value - }; - } - - return null; + return githubTokensDb.getGithubTokenById(userId, tokenId); } /** diff --git a/server/routes/settings.js b/server/routes/settings.js index e2ce0885..d467c49c 100644 --- a/server/routes/settings.js +++ b/server/routes/settings.js @@ -1,5 +1,5 @@ import express from 'express'; -import { apiKeysDb, credentialsDb, notificationPreferencesDb, pushSubscriptionsDb } from '../database/db.js'; +import { apiKeysDb, credentialsDb, notificationPreferencesDb, pushSubscriptionsDb } from '../modules/database/index.js'; import { getPublicKey } from '../services/vapid-keys.js'; import { createNotificationEvent, notifyUserIfEnabled } from '../services/notification-orchestrator.js'; diff --git a/server/routes/user.js b/server/routes/user.js index 877cd45b..dcb8ecd7 100644 --- a/server/routes/user.js +++ b/server/routes/user.js @@ -1,5 +1,5 @@ import express from 'express'; -import { userDb } from '../database/db.js'; +import { userDb } from '../modules/database/index.js'; import { authenticateToken } from '../middleware/auth.js'; import { getSystemGitConfig } from '../utils/gitConfig.js'; import { spawn } from 'child_process'; diff --git a/server/services/notification-orchestrator.js b/server/services/notification-orchestrator.js index 29836b01..e5ab39cd 100644 --- a/server/services/notification-orchestrator.js +++ b/server/services/notification-orchestrator.js @@ -1,5 +1,5 @@ import webPush from 'web-push'; -import { notificationPreferencesDb, pushSubscriptionsDb, sessionsDb } from '../database/db.js'; +import { notificationPreferencesDb, pushSubscriptionsDb, sessionsDb } from '../modules/database/index.js'; const KIND_TO_PREF_KEY = { action_required: 'actionRequired', diff --git a/server/services/vapid-keys.js b/server/services/vapid-keys.js index 1abaeba1..8fce37e0 100644 --- a/server/services/vapid-keys.js +++ b/server/services/vapid-keys.js @@ -1,7 +1,8 @@ import webPush from 'web-push'; -import { db } from '../database/db.js'; +import { getConnection } from '../modules/database/connection.js'; let cachedKeys = null; +const db = getConnection(); function ensureVapidKeys() { if (cachedKeys) return cachedKeys;